58.3 架构评审

一句话

评审不是“审批盖章”,而是在不可逆之前,用一群人的经验换一次纠错机会;没有决策与行动的评审,只是开会。

先破除四个误区

误区 现实
“评审 = 审批” 审批只回答“能不能做”;评审要回答“该不该这么做、有没有更合适的方案、失败怎么退”
“评审人越多越好” 人越多越没人负责。3–5 个真正相关的人,比 20 个旁听者有效
“实现完再评审” 实现完只剩“改不改”的沉没成本;评审要在不可逆投入之前
“评审靠权威拍板” 权威可以拍板,但必须留下依据与代价;否则下次还会犯同样的错

评什么,不评什么

该评 理由 不评 放哪
难以逆转的决策(数据模型、分片键、运行时) 改一次伤筋动骨 命名、目录结构 代码规范
跨团队接口/契约 多方依赖,改一处伤一片 单个函数的实现 代码评审
非功能指标(延迟、容量、可用性) 决定架构形态 代码风格 lint/格式化
数据安全与合规 事后补救成本最高 测试用例写法 测试评审
成本与运维复杂度 隐形成本在第二年爆发 局部性能微优化 性能剖析
迁移与回滚方案 决定能不能安全上线;与 58.4 演进路线图 的波次计划对齐 UI 细节 设计评审

🔑 评审的准入条件不是“方案写完了”,而是**“再往下走就要花大钱/改不动了”**。越晚评审,纠错成本越高。

五种评审形式

形式 适用 时长 产出
设计评审会 新系统、重大改造 60 分钟 结论 + 行动项
RFC(书面评审) 跨团队、需要留痕 3–5 天评论期 批准/驳回 + 评论归档
架构委员会 公司级标准、平台选型(选型流程见 58.5 技术选型流程) 30–60 分钟 标准/例外决定
异步评审 分布式团队、小改动 1–2 天 评论 + 结论
事后评审 已上线系统 60 分钟 改进项/债登记

形式服务于风险,不是反过来:一个内部工具的重构,不需要架构委员会。

流程:五步,有时限

① 提交:1 页摘要 + 设计文档 + 关键数据(NFR/成本/备选)
② 预读:评审人 3 个工作日内读完,标出必须回答的问题
③ 评审:60 分钟,先讲问题与约束,再讲方案,最后逐条过问题
④ 结论:通过 / 有条件通过 / 驳回,写清条件与负责人
⑤ 留痕:结论落到 ADR + 行动项进 backlog

时限建议:预读 3 个工作日、评审会 60 分钟、结论 2 个工作日内发出。没有时限的评审,会变成“等某个人有空”。

角色:四个人就够

角色 职责 不该做
作者 讲清问题、方案、代价;记录问题 辩护一切、拒绝替代方案
主持(chair) 控节奏、保证每个关键问题被回答、收敛结论 替作者设计
评审人(3–5) 从各自领域挑风险:数据、SRE、安全、成本 只挑格式/命名
记录 记问题、结论、行动项、owner 参与辩论

主持人最好不是作者:作者容易陷入“解释”,主持人负责“推进”。

评审清单:十条

# 检查项 不合格的表现
1 问题与约束写清了吗? 上来就讲方案
2 至少 2 个备选,且说清为什么不选? 只有一个方案
3 NFR 是数字吗(P99/吞吐/可用性)? “性能更好”“高可用”
4 数据模型与一致性说清了吗? 只画了服务框图
5 失败模式与降级路径? 假设依赖永不失败
6 回滚/迁移方案? 上线即单程
7 安全与合规结论? “后面再补”
8 成本(三年 TCO)? 只算机器,不算人力
9 运维复杂度与可观测性? 没有指标/日志/告警设计
10 谁负责落地、什么时候验收? 没有 owner 与里程碑

第 5 条 “失败模式” 不要用嘴过一遍:可用性专项评审直接跑一遍 FMEA,逐个部件假设故障并打分,见 35.10 FMEA (故障模式与影响分析)。

一个能跑的“评审就绪检查器”

评审会最怕两种材料:没数字和没备选。程序在排会之前先检查,不满足就不排会。

package main

import (
	"fmt"
	"os"
	"strings"
)

type design struct {
	title        string
	problem      bool
	nfrs         map[string]string // 指标 -> 目标值(必须是数字)
	alternatives int
	decision     bool
	rollback     bool
	cost         bool
	security     bool
	owner        string
	migration    bool
}

type finding struct {
	level, item, detail string
}

func review(d design) []finding {
	var fs []finding
	add := func(level, item, detail string) { fs = append(fs, finding{level, item, detail}) }

	if !d.problem {
		add("FAIL", "问题定义", "没有说清要解决什么问题、影响谁")
	}
	quantified := 0
	for _, v := range d.nfrs {
		if strings.ContainsAny(v, "0123456789") {
			quantified++
		}
	}
	if quantified < 3 {
		add("FAIL", "非功能指标", fmt.Sprintf("只有 %d 个带数字的指标(至少 3 个:延迟/吞吐/可用性)", quantified))
	}
	if d.alternatives < 2 {
		add("FAIL", "备选方案", fmt.Sprintf("只有 %d 个备选(至少 2 个,否则不是选型是通报)", d.alternatives))
	}
	if !d.decision {
		add("FAIL", "决策与后果", "没有写选了什么、放弃了什么、代价是什么")
	}
	if !d.rollback {
		add("FAIL", "回滚方案", "没有回滚/降级路径;上线即单程")
	}
	if !d.cost {
		add("WARN", "成本", "没有三年 TCO 或资源估算")
	}
	if !d.security {
		add("WARN", "安全", "没有安全/合规评审结论")
	}
	if d.owner == "" {
		add("WARN", "负责人", "没有明确 owner,评审后无人跟进")
	}
	if !d.migration {
		add("WARN", "迁移/灰度", "没有迁移步骤与灰度策略")
	}
	return fs
}

func run(d design) bool {
	fs := review(d)
	fmt.Printf("== 评审就绪检查:%s ==\n", d.title)
	if len(fs) == 0 {
		fmt.Println("  PASS 全部检查通过,可以提交评审")
		return true
	}
	fail := 0
	for _, f := range fs {
		fmt.Printf("  %-4s %-12s %s\n", f.level, f.item, f.detail)
		if f.level == "FAIL" {
			fail++
		}
	}
	if fail > 0 {
		fmt.Printf("  结论:不满足评审条件(FAIL %d)——补齐后再排评审,别让评审会变成写作课\n\n", fail)
		return false
	}
	fmt.Println("  结论:可以提交评审(WARN 项在会上确认)")
	fmt.Println()
	return true
}

func main() {
	good := design{
		title:   "订单服务读写分离",
		problem: true,
		nfrs: map[string]string{
			"p99": "120ms", "qps": "8000", "availability": "99.95%",
		},
		alternatives: 3,
		decision:     true,
		rollback:     true,
		cost:         true,
		security:     true,
		owner:        "order-team",
		migration:    true,
	}
	weak := design{
		title:        "缓存换成 Redis Cluster",
		problem:      true,
		nfrs:         map[string]string{"qps": "更高"},
		alternatives: 1,
		decision:     true,
		rollback:     false,
		cost:         false,
		security:     false,
		owner:        "",
		migration:    false,
	}

	okA := run(good)
	okB := run(weak)
	fmt.Printf("可以提交评审:A=%v,B=%v\n", okA, okB)
	if !okA || !okB {
		os.Exit(1)
	}
}

本机运行结果(退出码 1):

== 评审就绪检查:订单服务读写分离 ==
  PASS 全部检查通过,可以提交评审
== 评审就绪检查:缓存换成 Redis Cluster ==
  FAIL 非功能指标        只有 0 个带数字的指标(至少 3 个:延迟/吞吐/可用性)
  FAIL 备选方案         只有 1 个备选(至少 2 个,否则不是选型是通报)
  FAIL 回滚方案         没有回滚/降级路径;上线即单程
  WARN 成本           没有三年 TCO 或资源估算
  WARN 安全           没有安全/合规评审结论
  WARN 负责人          没有明确 owner,评审后无人跟进
  WARN 迁移/灰度        没有迁移步骤与灰度策略
  结论:不满足评审条件(FAIL 3)——补齐后再排评审,别让评审会变成写作课

可以提交评审:A=true,B=false

注意:检查器只保证“材料够评审”,不保证“方案对”。它挡掉的是浪费大家时间的会,不是替代判断。

结论怎么写:三种,都要可执行

结论 含义 必须写清
通过 可以按方案实施 生效条件(如“先补监控”)
有条件通过 大方向可以,但必须先满足条件 每条条件 + owner + 截止时间 + 谁来验收
驳回 方案不可行 驳回理由 + 建议方向 + 何时可再提

“原则上同意”是最坏的结论:既没有授权,也没有约束。评审结束前,主持人必须把结论落成上面三种之一。

评审之后:ADR 与行动项

评审的产出不是会议纪要,而是:

  1. 一篇 ADR:记录背景、备选、决策、后果(见 58.1 ADR 实践)。
  2. 行动项清单:每条有 owner、截止时间、验收方式,进 backlog。
  3. 例外记录:如果决定“先欠着”,登记为技术债(见 58.2 技术债管理)。

没有这三样,评审就只是一次“大家聊过了”。

生产落地:指名 → 可抄 → 可验 → 可退

指名:评审材料用 Markdown/Confluence,模板固定(问题/约束/备选/NFR/数据/失败/回滚/成本/安全/迁移);跨团队评审用 RFC 流程(GitHub PR 或文档评论);结论落 ADR。技术方案写作方法见 80.4 技术方案写作(待补),决策框架见 4.6 决策框架与决策记录。

可抄(评审会开场模板):

## 今天要决定什么
- 决策:订单服务是否采用读写分离(而不是分库)
- 不决定:分片键怎么选(下次单独评审)

## 约束
- P99 ≤ 150ms;峰值 8k QPS;可用性 99.95%
- 团队 4 人;迁移窗口 6 周

## 必须回答的问题
1. 主从延迟对下单有什么影响?怎么兜底?
2. 回滚路径是什么?多久能回?
3. 三年 TCO 与当前方案差多少?

可验:本节检查器真跑过,输出如上;把它放在评审邀请前,材料不齐就不排会。人工验证:随机抽一次已完成的评审,检查是否有 ADR、是否有带 owner 的行动项、结论是否属于三种之一。

可退:评审结论被推翻时,重新评审并更新 ADR(不要删旧记录);有条件通过的条件没满足就上线,按事故处理。评审流程本身太重时,先减评审人、缩会议,而不是取消评审。

反模式

反模式 后果
评审会变成作者答辩 评审人不敢提反对,方案缺陷被带到线上
没有备选方案 评审只能二选一:同意或得罪人
评审在实现之后 沉没成本让“驳回”不可行
20 人旁听、3 人发言 责任分散,没人对结论负责
结论是“原则上同意” 没有授权也没有约束,后续扯皮
没有 ADR 与行动项 三个月后没人记得决定了什么
只挑格式/命名 真正的风险(数据、失败模式)没人看
评审人=作者 自己审自己,失去纠错意义

自检

Q1 架构评审和代码评审的边界在哪?

▸ 答案:架构评审看难以逆转、跨团队、影响非功能指标的决策(数据模型、分片键、运行时、接口契约、成本);代码评审看实现质量(正确性、可读性、命名、测试)。架构评审不替代代码评审,代码评审也不该决定架构。

Q2 为什么“评审人越多越好”是错的?

▸ 答案:评审的价值来自“真正相关的人认真读并提问”,不是人数。人越多,责任越分散、发言越少、会议越长,最后变成旁听。3–5 个覆盖数据、SRE、安全、成本等关键视角的人足够;其他人看 ADR 即可。

Q3 评审为什么必须在“不可逆投入之前”?

▸ 答案:评审的目的是纠错,纠错成本随投入增加而上升。实现完之后,推翻方案意味着重写,团队会倾向于“差不多就上”,评审退化成背书。越早评审,备选方案越多、改动越便宜。

Q4 “有条件通过”必须写清什么?

▸ 答案:每条条件、owner、截止时间、验收方式。缺任何一项,“有条件”就会变成“没条件”:没人负责、没人验收,条件永远不满足。主持人要在结论里逐条列出,并在截止时间回看。

Q5 评审结束后必须产出哪三样东西?

▸ 答案:① ADR(记录背景、备选、决策、后果);② 带 owner/截止/验收的行动项;③ 若决定“先欠着”,登记为技术债。没有这三样,评审就只是聊天,无法追溯也无法执行。

关联