架构评审清单:先看风险,再看方案优雅

0阅读12分钟

评审会的常见困境

每家公司都有过这样的架构评审会:方案负责人讲了 40 分钟 PPT,从领域模型讲到技术选型,画了三张架构图,最后问「大家有什么意见」。会议室安静了几秒,有人问了一个关于缓存一致性的细节问题,另一个人说「监控方案是不是可以复用现有的」,然后主持人看看时间,说「那基本没什么大问题,会后补充一下细节就推进吧」。

两周后上线,流量翻了三倍,某个核心接口的 P99 从 80ms 飙到 2000ms。事后复盘发现,方案里根本没有考虑分页查询的边界,数据库在特定查询模式下会全表扫描。这个风险在评审阶段就能被发现——但没人问。

问题不是团队能力不行,而是评审的注意力放错了地方。多数评审会从方案细节开始讨论,跳过了最重要的前置问题:这个架构要解决什么风险,它还没解决的又是什么。

先看风险,是 CMU SEI 二十年前的结论

架构评审不是比方案漂亮。这个判断有来路。

卡内基梅隆大学软件工程研究所(SEI)在 2000 年代初提出了 ATAM(Architecture Tradeoff Analysis Method),核心思路是:架构评审的目标不是找最优解,而是在系统构建之前暴露架构决策中的风险。ATAM 把评审产出分为四类:

类别含义评审中的角色
风险(Risk)架构决策可能阻碍业务目标达成必须记录并跟踪修复
非风险(Non-risk)架构决策被判断为安全可接受记录即可,避免重复讨论
敏感点(Sensitivity point)某个架构元素对特定质量属性影响显著标记为后续性能/容量测试重点
权衡点(Tradeoff point)某个架构元素同时影响多个质量属性,改善一个可能损害另一个需要显式决策并记录取舍理由

ATAM 的评审流程有 9 个步骤,但对多数团队来说,最实用的部分是它的核心动作:用场景(scenario)驱动评审,而不是用方案细节驱动评审。

场景分三类:

  • 用例场景:系统正常运行时的典型路径
  • 增长场景:如果流量扩大 10 倍,系统如何响应
  • 故障场景:如果某个依赖挂了,系统如何降级

多数评审会只讨论了第一类。后两类被跳过了。

用清单替代即兴发挥

ATAM 的完整流程需要投入 2-3 天,对多数团队的节奏来说太重了。实际操作中,一份结构化清单就能覆盖 80% 的风险识别。

下面这份清单按评审阶段分组,每项对应评审中的一个具体检查动作。

阶段一:评审前——确认问题和边界

#检查项通过标准典型缺失
1方案是否明确了要解决的问题和非目标能在一句话说清「解决 X,不解决 Y」目标模糊,「什么都想兼顾」
2是否列出了核心约束(团队规模、时间窗口、预算、现有技术栈)约束条件写在方案文档前两页约束只存在于负责人脑中
3是否识别了关键利益相关方评审参与人包含运维、测试、产品只有开发参加评审
4是否有可量化的非功能目标(QPS、P99、可用性 SLA)至少一个数字指标只说「高性能」「高可用」

阶段二:评审中——验证架构决策

#检查项通过标准典型缺失
5数据流是否完整覆盖正常路径和异常路径架构图中标注了每条边的成功和失败处理只画了 happy path
6是否存在单点故障,是否有降级方案每个外部依赖都有超时/重试/熔断策略「如果 Redis 挂了……没考虑过」
7状态一致性边界是否清晰跨服务数据同步有明确的最终一致性方案或事务边界假设所有数据强一致
8是否具备可灰度发布和可回滚能力方案中包含灰度切流策略和回滚步骤发布方案是「全量上线」
9安全和权限模型是否覆盖认证、鉴权、数据加密路径已明确安全留到上线前补
10可观测性方案是否完整有 metrics、logging、tracing 三件套的接入方案只说「加日志」
11演进成本是否可控标出未来最可能变化的 2-3 个模块及扩展策略方案看起来像「一步到位」

阶段三:评审后——闭环跟踪

#检查项通过标准典型缺失
12评审风险项是否形成书面记录每个风险有 owner、deadline、验收标准评审结论是「大家知道了」
13是否有架构决策记录(ADR)关键决策有 ADR 文档,含背景、选项、取舍、结论决策散落在会议纪要和聊天中
14是否安排了评审结论的回检节点在里程碑中设了架构复查点评审完即结束,无后续跟踪

三个真实踩过的坑

清单告诉你该检查什么。案例告诉你没检查会怎样。

案例一:缓存穿透没有降级方案

场景:一个内容平台的详情页接口,QPS 日常 500,高峰期 3000。架构方案是「MySQL + Redis 缓存」,缓存未命中时查库。

翻车:某天运营推送了一个冷门内容的链接,大量用户同时访问。这些内容在 Redis 中不存在(之前从未被访问过),所有请求穿透到 MySQL,数据库 CPU 瞬间打满,整个服务不可用。

根因:评审时只讨论了「缓存命中率」,没有人问「缓存未命中时怎么办」。方案中没有缓存穿透防护、没有数据库查询降级、没有限流。

修复

// ❌ 坏做法:直接查库,无防护
async function getArticle(id: string) {
  let data = await redis.get(`article:${id}`)
  if (!data) {
    // 缓存未命中,直接查库
    // 大量并发请求同时穿透到数据库
    data = await db.query('SELECT * FROM articles WHERE id = ?', id)
    await redis.set(`article:${id}`, data, 'EX', 3600)
  }
  return data
}
// ✅ 好做法:空值缓存 + 互斥锁 + 降级
async function getArticle(id: string) {
  let data = await redis.get(`article:${id}`)
 
  // 空值缓存:防止缓存穿透
  if (data === '__NULL__') {
    return null
  }
 
  if (!data) {
    // 互斥锁:同一时刻只有一个请求去查库
    const locked = await redis.set(
      `lock:article:${id}`, '1', 'EX', 10, 'NX'
    )
    if (locked) {
      try {
        data = await db.query(
          'SELECT * FROM articles WHERE id = ?', id
        )
        if (data) {
          await redis.set(`article:${id}`, data, 'EX', 3600)
        } else {
          // 内容不存在,缓存空值,防止反复穿透
          await redis.set(`article:${id}`, '__NULL__', 'EX', 60)
        }
      } finally {
        await redis.del(`lock:article:${id}`)
      }
    } else {
      // 没拿到锁的请求,短暂等待后重试或降级
      await sleep(50)
      return getArticle(id)
    }
  }
 
  return data
}

评审清单对照:第 6 项「是否存在单点故障,是否有降级方案」——如果评审时问一句「如果缓存全部 miss 会怎样」,这个问题在设计阶段就能暴露。

案例二:跨服务数据一致性边界不清

场景:一个电商系统的订单服务调用库存服务扣减库存,再调用支付服务发起扣款。三个服务各自有数据库。

翻车:支付服务扣款成功,但回调通知订单服务时网络超时,订单状态未更新。用户钱扣了,订单显示「待支付」。客服电话被打爆。

根因:方案用了「同步调用链」完成跨服务事务,没有补偿机制。评审时大家默认「网络不会超时」,因为内网环境。但实际生产中,GC 停顿、网络抖动、服务重启都会导致超时。

修复:引入本地消息表 + 最终一致性方案。

// ❌ 坏做法:同步调用链,无补偿
async function placeOrder(order: Order) {
  // 1. 创建订单
  await orderDb.insert(order)
  // 2. 扣库存(远程调用)
  await inventoryService.deduct(order.items)
  // 3. 发起支付(远程调用)
  await paymentService.charge(order.amount)
  // 4. 更新订单状态
  await orderDb.update(order.id, { status: 'paid' })
  // 如果第 3 步成功但回调超时,第 4 步不执行
  // 订单永远卡在「待支付」
}
// ✅ 好做法:本地消息表 + 状态机 + 补偿
async function placeOrder(order: Order) {
  // 用事务保证订单写入和消息写入的原子性
  await db.transaction(async (tx) => {
    await tx.insert('orders', {
      ...order,
      status: 'pending_payment' // 明确初始状态
    })
    await tx.insert('outbox_messages', {
      aggregate_id: order.id,
      event_type: 'order.created',
      payload: order,
      status: 'pending' // 待发送
    })
  })
 
  // 后台任务轮询 outbox,投递到消息队列
  // 库存服务和支付服务消费消息,各自处理
  // 如果支付回调超时,由定时任务触发状态补偿
}
 
// 补偿任务:处理超时未支付的订单
async function compensateStaleOrders() {
  const staleOrders = await orderDb.query(
    `SELECT * FROM orders
     WHERE status = 'pending_payment'
     AND created_at < NOW() - INTERVAL 30 MINUTE`
  )
  for (const order of staleOrders) {
    // 先查支付服务的真实状态,再决定补偿动作
    const payResult = await paymentService.query(order.id)
    if (payResult?.status === 'success') {
      await orderDb.update(order.id, { status: 'paid' })
    } else {
      await orderDb.update(order.id, { status: 'cancelled' })
    }
  }
}

评审清单对照:第 7 项「状态一致性边界是否清晰」——评审时如果画出跨服务的数据流并追问「每一步失败后数据在哪、谁来修复」,同步调用链的问题会立刻暴露。

案例三:「全量上线」没有回滚路径

场景:一个 SaaS 平台的计费模块重构,从按分钟计费改为按秒计费。方案评审时重点讨论了精度提升和数据迁移,发布方案写了「凌晨 2 点全量部署」。

翻车:上线后发现按秒计费导致账单量暴增 60 倍,下游的账单导出服务内存溢出。回滚时发现新代码已经写入了按秒格式的账单记录,旧代码无法解析新格式的数据,回滚等于数据损坏。

根因:方案没有考虑回滚时的数据兼容性,也没有灰度策略。评审时没有人问「如果上线后要回滚,数据怎么办」。

修复:引入灰度发布 + 数据格式向前兼容。

// ❌ 坏做法:硬切数据格式,无法回滚
function generateBill(usage: Usage) {
  return {
    userId: usage.userId,
    amount: usage.seconds * ratePerSecond, // 新格式:按秒
    // 旧代码期望的是 minutes 字段,回滚后无法解析
  }
}
// ✅ 好做法:数据格式向前兼容 + 灰度标记
function generateBill(usage: Usage, mode: 'legacy' | 'per_second') {
  const base = {
    userId: usage.userId,
    // 同时保留两种粒度的数据,确保新旧代码都能读取
    amount_minutes: usage.minutes * ratePerMinute,
    amount_seconds: usage.seconds * ratePerSecond,
    billing_mode: mode,
    version: 2 // 显式版本号
  }
  return base
}
 
// 灰度切流:按用户百分比逐步切换
function getBillingMode(userId: string): 'legacy' | 'per_second' {
  const hash = hashCode(userId) % 100
  if (hash < GRAY_PERCENTAGE) { // 初期 5%,逐步调大
    return 'per_second'
  }
  return 'legacy'
}

评审清单对照:第 8 项「是否具备可灰度发布和可回滚能力」——评审时如果追问「回滚方案是什么,数据兼容吗」,「全量上线 + 格式硬切」的组合不可能通过。

评审流程:从准备到闭环

把清单和案例中的教训串起来,一次架构评审的完整流程如下:

流程图画布 · 115%
Mermaid 流程图加载中...

这个流程的核心逻辑是:预审过滤掉准备不足的方案,评审会用场景而非方案细节驱动讨论,评审后必须有闭环机制。

四种常见评审模式的对比

不同规模和风险等级的架构变更,适用不同的评审深度。

维度轻量级(30 分钟)标准评审(1-2 小时)深度评审(半天)ATAM 完整流程(2-3 天)
适用场景单服务内部改动跨服务/跨团队变更核心链路重构全新系统或重大迁移
参与人技术 lead + 1-2 人架构师 + 相关团队代表跨部门 + 运维 + 安全全利益相关方
清单覆盖阶段二中的 5-7 项全部阶段一 + 阶段二全部三个阶段ATAM 9 步完整流程
产出物风险清单(口头或文档)风险清单 + ADR风险清单 + ADR + 复审计划完整评估报告
典型风险遗漏跨服务影响时间投入需要控制需要协调多方时间成本高,小项目用不起
评审模式风险识别深度团队时间成本适合频率
轻量级中——依赖评审人经验低——30 分钟每个迭代 2-3 次
标准评审较高——有清单引导中——准备 + 会议约 3 小时每月 1-2 次
深度评审高——覆盖多维度高——半天到一天每季度或重大版本
ATAM 完整流程最高——系统化方法最高——多天投入每年 1-2 次或关键里程碑

好/坏做法对照:评审中的关键动作

以下四组对比聚焦评审中最容易做错的几个环节。

1. 描述架构目标

<!-- ❌ 坏做法:目标模糊,无法验证 -->
本系统需要高性能、高可用、易扩展。
采用微服务架构,保证系统的灵活性和可维护性。
<!-- ✅ 好做法:目标可量化,非目标明确 -->
目标:
- 订单查询接口 P99 < 200ms,QPS 支撑 5000
- 核心链路可用性 > 99.95%(月度不可用 < 22 分钟)
- 支持未来 6 个月内接入新的支付渠道
 
非目标:
- 本次不重构用户认证模块
- 不支持跨区域多活(后续规划)

2. 画架构图

流程图画布 · 115%
Mermaid 流程图加载中...
流程图画布 · 115%
Mermaid 流程图加载中...

3. 制定发布方案

# ❌ 坏做法:一步到位,无回滚考虑
deploy:
  strategy: rolling
  batch_size: 100%   # 全量发布
  rollback: manual    # 手动回滚,无自动化
  data_migration: v2_schema  # 新 schema 不兼容旧代码
# ✅ 好做法:灰度 + 自动回滚 + 数据兼容
deploy:
  strategy: canary
  canary_percentage: 5    # 先切 5% 流量
  observation_window: 30m # 观察 30 分钟
  auto_rollback:
    trigger:
      error_rate: "> 1%"
      p99_latency: "> 500ms"
  data_migration:
    phase: forward_compatible  # 新代码兼容新旧数据格式
    # 第二阶段再清理旧字段

4. 记录架构决策

<!-- ❌ 坏做法:决策散落在聊天中,无法追溯 -->
群里讨论了一下,决定用 Kafka 而不是 RabbitMQ。
原因是 Kafka 性能好。
<!-- ✅ 好做法:ADR 格式,背景、选项、取舍完整 -->
# ADR-023: 消息队列选型
 
## 状态
已接受 (2026-06-20)
 
## 背景
订单事件需要异步通知下游 3 个服务,日均消息量 200 万。
要求消息顺序性(同一订单的事件必须有序)。
 
## 选项
| 维度 | Kafka | RabbitMQ |
|------|-------|----------|
| 吞吐量 | 百万级/秒 | 万级/秒 |
| 顺序保证 | partition 内有序 | 单队列有序 |
| 运维成本 | 较高,需 ZooKeeper/KRaft | 较低 |
| 团队经验 | 2 人熟悉 | 全组熟悉 |
 
## 决策
选择 Kafka。订单量级下 RabbitMQ 吞吐量足够,
但 partition 级别的顺序保证更简单可靠。
运维成本由平台团队统一承担。
 
## 后果
- 正:顺序性保证简单,扩展性强
- 负:团队需要学习 Kafka 运维,预计 2 周上手期

ADR:让评审结论不丢失

Martin Fowler 对 ADR 的定义是:一份简短的文本,捕获并解释一个与产品或生态系统相关的重要决策。

ADR 的关键规则:

规则说明
一旦接受,不可修改决策变了就写新的 ADR 替换旧的,标注旧 ADR 为「已替代」
存放在代码仓库和代码一起版本管理,开发者提交 PR 时能看到
一份只记一个决策不要把多个决策混在一份 ADR 里
包含背景和取舍不只要记「选了什么」,还要记「为什么没选其他」
标注复审条件写清楚「当 X 条件变化时,这个决策需要重新评估」

Spotify 的工程实践建议:只为有显著影响的决策写 ADR。团队需要自行定义「显著影响」的阈值——通常是跨团队、涉及数据迁移、影响非功能目标、或者未来很可能需要回退的决策。

评审的五个实用技巧

清单和模板是骨架,评审质量最终取决于评审人的提问方式。几个在实践中验证过有效的方法:

用「乘十除十」探测边界。问方案负责人:如果流量乘以 10,系统哪里先崩?如果数据量除以 10,哪些组件变得不必要?这能快速暴露过度设计和设计不足。

盯住服务边界。跨服务的调用、数据同步、事件通知是风险高发区。评审时把架构图中的服务边界全部标红,逐个问「这条边断了会怎样」。

指定「反对者」。当团队对某个方案达成高度一致时,安排一个人专门唱反调。不是因为他觉得方案有问题,而是他的职责就是找出漏洞。这能打破群体思维。

翻历史事故。把过去半年的线上事故拿出来,逐个问「当前方案是否会产生同样的问题」。历史事故是最好的风险清单。

要求结构化产出。评审会结束时必须产出:风险清单(含 owner 和 deadline)、ADR(关键决策)、复审计划(如果有高风险项)。没有产出的评审等于没评审。

小结

架构评审的核心不是选出最优方案,而是在投入实现之前把风险摊开。一份好的评审清单能把评审的注意力从「方案漂不漂亮」拉回到「风险有没有被识别和处置」。

本文的清单覆盖了评审前、评审中、评审后三个阶段共 14 个检查项。结合 ATAM 的场景驱动思路和 ADR 的决策记录实践,可以适配从 30 分钟轻量评审到半天深度评审的不同场景。

最后提醒一点:清单不是目的,提出正确的问题才是。每次评审结束时问自己:如果这个方案上线后出了问题,最可能出在哪里?如果这个问题在评审会上被问过,答案写下来了,那就是一次有效的评审。

参考资料

评论 0

0 / 1000