架构评审清单:先看风险,再看方案优雅
评审会的常见困境
每家公司都有过这样的架构评审会:方案负责人讲了 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 项「是否具备可灰度发布和可回滚能力」——评审时如果追问「回滚方案是什么,数据兼容吗」,「全量上线 + 格式硬切」的组合不可能通过。
评审流程:从准备到闭环
把清单和案例中的教训串起来,一次架构评审的完整流程如下:
这个流程的核心逻辑是:预审过滤掉准备不足的方案,评审会用场景而非方案细节驱动讨论,评审后必须有闭环机制。
四种常见评审模式的对比
不同规模和风险等级的架构变更,适用不同的评审深度。
| 维度 | 轻量级(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. 画架构图
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 分钟轻量评审到半天深度评审的不同场景。
最后提醒一点:清单不是目的,提出正确的问题才是。每次评审结束时问自己:如果这个方案上线后出了问题,最可能出在哪里?如果这个问题在评审会上被问过,答案写下来了,那就是一次有效的评审。
参考资料
- CMU SEI — Architecture Tradeoff Analysis Method Collection — ATAM 方法论的官方资料,包含 9 步流程和四类评审产出
- Martin Fowler — Architecture Decision Record — ADR 的定义、格式和最佳实践
- AWS — Master Architecture Decision Records (ADRs) — AWS 对 ADR 实践的指导,含模板和生命周期管理
- Microsoft Learn — Maintain an Architecture Decision Record — Azure Well-Architected 框架中的 ADR 维护指南
- Spotify Engineering — When Should I Write an ADR — Spotify 关于 ADR 使用时机的工程实践
- 京东云 — 上线三板斧:可灰度、可验证、可回滚 — 上线风控的工程实践,灰度与回滚策略
- 阿里云 — 可灰度设计原则 — 阿里云卓越架构中的灰度设计原则