架构评审清单

架构评审针对系统级决策——模块划分、技术选型、数据流、扩展性、可靠性。它和设计评审的区别在于:设计评审关注「这个功能怎么做」,架构评审关注「系统怎么组织才能长期健康」。

要点

  • 架构评审针对系统级决策:模块划分、技术选型、数据流、扩展性
  • 评审前准备:系统现状图、问题陈述、目标架构、迁移路径、技术选型对比
  • 重点检查:模块边界、数据流向、单点故障、可观测性、部署回滚
  • 针对当前规模设计,但保留清晰的扩展路径

1. 适用时机

  • 新系统/新服务启动
  • 现有系统的重大重构(拆分、合并、迁移)
  • 引入新的中间件或基础设施(数据库、消息队列、缓存层)
  • 系统扩展性出现瓶颈,需要重新设计
  • 跨多个团队的协同设计

不需要架构评审的场景:

  • 单模块内部的设计(属于设计评审)
  • 纯配置调整
  • 第三方服务的常规使用(不是引入新类型的服务)

2. 评审前准备

  • 系统现状图:当前系统的模块划分、数据流、关键依赖
  • 问题陈述:现有架构哪里不够用?瓶颈在哪?
  • 目标架构:期望达到的目标状态,画清楚
  • 迁移路径:从现状到目标,分几步走?每步的风险?
  • 技术选型:选了什么技术?为什么?对比过哪些替代方案?
  • 容量估算:QPS、数据量、存储、带宽的预期规模
  • 成本估算:基础设施成本、人力成本、迁移成本

3. 核心检查项

3.1 模块与边界

  • 模块职责单一,没有「一个模块干所有事」
  • 模块之间边界清楚,依赖方向单向(避免循环依赖)
  • 关键接口有明确契约(输入输出类型、错误码、SLA)
  • 没有「只有作者知道怎么改」的模块
  • 共享逻辑抽到合适的位置(公共库 vs 各自复制)

Hono 示例:模块边界

// 好的模块边界
// apps/api/src/
// ├── modules/
// │   ├── orders/
// │   │   ├── orders.router.ts      # 路由层
// │   │   ├── orders.service.ts     # 业务层
// │   │   ├── orders.repository.ts  # 数据层
// │   │   └── orders.types.ts       # 类型定义
// │   └── users/
// │       └── ...
// └── shared/
//     ├── middleware/               # 共享中间件
//     └── utils/                    # 共享工具
 
// orders.router.ts - 路由层只做路由装配
import { Hono } from 'hono'
import { OrdersService } from './orders.service'
 
export const ordersRouter = new Hono()
 
ordersRouter.post('/', async (c) => {
  const body = await c.req.json()
  const service = new OrdersService(c.env)
  const order = await service.create(body)
  return c.json(order, 201)
})
 
// orders.service.ts - 业务层处理业务逻辑
export class OrdersService {
  private env: Env
 
  constructor(env: Env) {
    this.env = env
  }
 
  async create(input: CreateOrderInput): Promise<Order> {
    // 业务逻辑:校验库存、计算价格、创建订单
    // 不直接操作数据库,通过 repository
  }
}
 
// orders.repository.ts - 数据层只操作数据库
export class OrdersRepository {
  private db: D1Database
 
  constructor(db: D1Database) {
    this.db = db
  }
 
  async findById(id: string): Promise<Order | null> {
    return this.db.prepare('SELECT * FROM orders WHERE id = ?')
      .bind(id)
      .first()
  }
}

循环依赖检查

// 坏的依赖关系:循环依赖
// orders.service.ts 依赖 users.service.ts
// users.service.ts 依赖 orders.service.ts
 
// 解决方法:引入事件或接口层
// 1. 事件驱动
import { EventBus } from '../shared/event-bus'
 
export class OrdersService {
  async create(input: CreateOrderInput) {
    const order = await this.createOrder(input)
 
    // 发布事件,而不是直接调用 UserService
    await this.eventBus.publish('order.created', {
      orderId: order.id,
      userId: order.userId
    })
  }
}
 
// 2. 接口层
interface UserNotifier {
  notifyOrderCreated(userId: string, orderId: string): Promise<void>
}
 
export class OrdersService {
  private notifier: UserNotifier
 
  constructor(notifier: UserNotifier) {
    this.notifier = notifier
  }
 
  async create(input: CreateOrderInput) {
    const order = await this.createOrder(input)
    await this.notifier.notifyOrderCreated(order.userId, order.id)
  }
}

3.2 数据流与存储

  • 数据流向清楚,没有「不知道从哪来、不知道到哪去」的数据
  • 数据所有权明确(谁写入、谁读取、谁拥有真实来源)
  • 一致性模型清楚(强一致还是最终一致?哪些场景需要哪种?)
  • 数据存储选型合理(关系型 vs 文档型 vs KV vs 时序 vs 向量)
  • 数据量增长预期清楚,存储方案能撑住

Hono 示例:数据所有权

// 数据所有权:每个实体只有一个服务可以写入
 
// orders-service - 拥有订单数据
export class OrdersService {
  async createOrder(input: CreateOrderInput) {
    // 只有这个服务可以写入 orders 表
    return this.db.prepare('INSERT INTO orders ...')
  }
 
  async getOrder(id: string) {
    return this.db.prepare('SELECT * FROM orders WHERE id = ?')
  }
}
 
// users-service - 拥有用户数据
export class UsersService {
  async getUser(id: string) {
    return this.db.prepare('SELECT * FROM users WHERE id = ?')
  }
}
 
// 跨服务查询:通过 API 调用,而不是直接访问数据库
export class OrdersService {
  async getOrderWithUser(orderId: string) {
    const order = await this.getOrder(orderId)
 
    // 通过 API 调用获取用户信息
    const user = await this.httpClient.get(`/users/${order.userId}`)
 
    return { ...order, user }
  }
}

3.3 技术选型

  • 选型理由基于具体约束,不是「因为流行」或「简历上好看」
  • 团队有能力维护这个技术栈(不是只有一个人懂)
  • 社区活跃度、商业支持、许可证风险已评估
  • 与现有技术栈集成成本可接受
  • 考虑了退出成本(如果选错了,换掉有多难?)

技术选型决策记录

## 决策:选择 Cloudflare D1 作为主数据库
 
### 背景
- 应用部署在 Cloudflare Workers
- 需要关系型数据库存储订单、用户数据
- 预期数据量:< 1000 万行
 
### 考虑的方案
1. **Cloudflare D1**
   - 优势:与 Workers 深度集成、零运维、按用量计费
   - 劣势:单region、只读副本有限
   - 成本:$0.50/GB/月
 
2. **PlanetScale (MySQL)**
   - 优势:功能丰富、多 region
   - 劣势:需要额外运维、冷启动延迟
   - 成本:$39/月起
 
3. **Supabase (PostgreSQL)**
   - 优势:功能丰富、实时订阅
   - 劣势:冷启动延迟、需要管理连接池
   - 成本:$25/月起
 
### 决策
选择 D1。
 
### 理由
1. 应用已在 Workers,D1 集成最紧密
2. 数据量 < 1000 万行,D1 容量足够
3. 不需要多 region 写入
4. 成本最低
5. 团队有 SQLite 经验
 
### 退出成本
如果未来需要多 region,可以迁移到 PlanetScale,
数据导出为 SQL 文件即可。

3.4 扩展性

  • 水平扩展路径清楚(加机器能线性提升性能吗?)
  • 单点故障已识别并有应对
  • 有状态组件的扩展方案(数据库分片、缓存集群)
  • 热点数据/热点用户的处理方案
  • 未来 3-6 个月的扩展需求能支持

Hono 示例:水平扩展

// Cloudflare Workers 自动水平扩展
// 但需要注意:无状态设计
 
import { Hono } from 'hono'
 
const app = new Hono()
 
// 好的设计:无状态
app.get('/orders/:id', async (c) => {
  const orderId = c.req.param('id')
 
  // 从 D1 读取,任何实例都能处理
  const order = await c.env.DB.prepare('SELECT * FROM orders WHERE id = ?')
    .bind(orderId)
    .first()
 
  return c.json(order)
})
 
// 坏的设计:有状态(内存缓存)
const cache = new Map() // 每个 Worker 实例有自己的缓存
 
app.get('/users/:id', async (c) => {
  const userId = c.req.param('id')
 
  // 不同实例的缓存不共享
  if (cache.has(userId)) {
    return c.json(cache.get(userId))
  }
 
  const user = await c.env.DB.prepare('SELECT * FROM users WHERE id = ?')
    .bind(userId)
    .first()
 
  cache.set(userId, user) // 只在这个实例有效
  return c.json(user)
})
 
// 正确做法:使用 KV 或 Cache API
app.get('/users/:id', async (c) => {
  const userId = c.req.param('id')
 
  // 使用 KV 作为缓存
  const cacheKey = `user:${userId}`
  let user = await c.env.KV.get(cacheKey, 'json')
 
  if (!user) {
    user = await c.env.DB.prepare('SELECT * FROM users WHERE id = ?')
      .bind(userId)
      .first()
 
    // 缓存 5 分钟
    await c.env.KV.put(cacheKey, JSON.stringify(user), {
      expirationTtl: 300
    })
  }
 
  return c.json(user)
})

3.5 可靠性与容错

  • 下游依赖失败时,本模块如何处理(超时、降级、熔断)
  • 重试策略合理(指数退避、最大重试次数、幂等保证)
  • 异步操作有失败处理(死信队列、告警、补偿)
  • 关键数据有备份和恢复方案
  • 故障演练计划(定期测试容错机制是否真的有效)

Hono 示例:重试与降级

import { Hono } from 'hono'
 
const app = new Hono()
 
// 带重试的外部调用
async function fetchWithRetry(
  url: string,
  options: RequestInit,
  maxRetries = 3
): Promise<Response> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, {
        ...options,
        signal: AbortSignal.timeout(5000) // 5 秒超时
      })
 
      if (response.ok) {
        return response
      }
 
      // 5xx 错误才重试
      if (response.status >= 500) {
        throw new Error(`HTTP ${response.status}`)
      }
 
      // 4xx 错误不重试
      return response
    } catch (error) {
      if (i === maxRetries - 1) {
        throw error
      }
 
      // 指数退避:1s, 2s, 4s
      const delay = Math.pow(2, i) * 1000
      await new Promise(resolve => setTimeout(resolve, delay))
    }
  }
 
  throw new Error('Max retries exceeded')
}
 
// 降级策略
app.get('/products/recommendations', async (c) => {
  try {
    // 尝试调用推荐服务
    const recommendations = await fetchWithRetry(
      'https://recommendation-service.internal/api/recommend'
    )
    return c.json(await recommendations.json())
  } catch (error) {
    // 降级:返回热门推荐
    console.error('Recommendation service failed:', error)
 
    const fallback = await c.env.KV.get('recommendations:fallback', 'json')
    return c.json(fallback || [])
  }
})

3.6 安全边界

  • 网络边界清楚(哪些对外、哪些对内、哪些是 DMZ)
  • 认证和授权机制统一(不是每个服务各搞一套)
  • 敏感数据识别清楚,加密存储和传输
  • 攻击面最小化(不暴露不必要的接口、端口、信息)
  • 审计日志完整(能追溯关键操作)

3.7 可观测性

  • 关键业务指标有监控(订单量、注册数、转化率)
  • 系统指标有监控(CPU、内存、QPS、延迟、错误率)
  • 分布式追踪接入(能看到一个请求在所有服务间的流转)
  • 日志规范统一(结构化、分级、trace id 贯穿)
  • 告警规则合理(不漏报、不乱报)

3.8 部署与运维

  • 部署方式清楚(蓝绿、金丝雀、滚动更新)
  • 回滚方案明确(回滚到哪个版本、需要多久、数据怎么办)
  • 配置管理统一(环境变量、密钥、功能开关)
  • 发布流程自动化(CI/CD 覆盖、无手工步骤)
  • 运维文档完整(部署、扩缩容、故障处理)

3.9 演进与迁移

  • 从现状到目标的迁移路径分阶段,每段可独立交付
  • 新旧系统能并行运行一段时间(灰度切换)
  • 数据迁移方案完整(旧数据怎么处理、双写还是迁移)
  • 回退路径存在(迁移失败能退回原系统)
  • 迁移成本可接受(不是「推倒重来」)

4. 架构评审的常见问题

架构评审容易陷入两种极端:

  1. 过度设计:为了「未来可能」的需求,引入复杂架构。评审时问「这个复杂度现在就需要吗?」
  2. 过度简化:为了「先上线」,忽略关键的非功能需求。评审时问「上线三个月后会遇到什么问题?」

好的架构评审应该:

  • 针对当前规模设计,但保留清晰的扩展路径
  • 复杂度的引入要有具体场景支撑,不是「预防性」的
  • 取舍明确记录,未来维护者能理解当时的决策背景

5. 通过标准

  • 系统边界和模块职责清晰
  • 关键决策有对比和取舍记录
  • 单点故障已识别,有应对措施
  • 数据流向和一致性模型清楚
  • 可观测性方案完整
  • 部署、回滚、迁移方案可行
  • 评审意见已记录,有跟进人和截止时间

6. 维护建议

架构评审清单需要随技术栈演进而更新:

  • 引入新类型的基础设施(如向量数据库、边缘计算)时,补充相关检查项
  • 发生过架构相关的事故后,把根因对应的检查项加进来
  • 团队规模变化时,调整模块划分和职责相关的要求
  • 每半年复审一次,删除过时的检查项
架构评审清单 - Hermes Engineering 工程知识体系 - AI共学社