架构评审的 PPT 上画着三个服务,线上跑着十一个。多出来的八个没有一个是"设计过的":两个是某次大促的临时扩容没回收,三个是"先这么接一下"的旁路,两个是把定时任务包成了服务,还有一个没人说得清来历,但所有订单都要经过它。
这类偏离不会自己暴露。图画得越漂亮,偏离越难被发现——因为没人会拿生产流量去核对一张幻灯片。对齐检查要做的不是重画架构图,而是让实际形态留下证据,然后把证据和意图之间的差写成待办。
1. 偏离的四个来源
| 来源 | 形成方式 | 典型时间尺度 | 可识别信号 |
|---|---|---|---|
| 临时方案固化 | 应急上线,事后无人回收 | 数周到数月 | 名字带 temp/old,无 owner |
| 绕过路径 | 主链路不通,另起一条 | 数天 | 同一业务有两条写入路径 |
| 配置漂移 | 手工改配置、环境不一致 | 逐次累积 | 环境行为不同,配置不在仓库 |
| 代码腐化 | 边界被逐步越过 | 数季 | 依赖方向与设计文档相反 |
四者的共同点是每一步都合理:应急上线是当时最优解,旁路是当时唯一能通的路。所以纠偏不能追问"当初为什么",只能看"现在这条路径还有没有人认领"。
2. 偏离本身不是问题,未记录才是
系统演化必然产生偏离,把偏离清零既不现实也不经济。判别标准只有一条:这个偏离是否在某人脑子里有记录、有 owner、有失效条件。
| 状态 | 定义 | 风险 |
|---|---|---|
| 已记录 + 有 owner | 写在 ADR 或清单里,知道为何存在 | 低,可管理 |
| 已记录 + 无 owner | 写了,没人负责回收 | 中,会过期 |
| 未记录 + 有 owner | 只有一人知道 | 高 |
| 未记录 + 无 owner | 无人知道为何存在 | 最高,无人敢动 |
对齐检查的产出就是把第三、四类变成第一类:不消灭,先让它可见。
3. 看流量与依赖,不看图
| 证据源 | 能回答 | 盲区 |
|---|---|---|
| 调用链(trace) | 谁真的调谁、耗时分布 | 异步与消息链路常缺失 |
| 依赖清单(运行时) | 连了哪些下游 | 看不见"该连而没连" |
| 配置快照 | 实际生效的参数 | 配置中心改过的不在快照里 |
| 网关日志 | 外部真实调用面 | 内网直连绕过网关 |
四个源要交叉:trace 给主链路,网关给对外面,配置快照给行为差异。单独任何一个都会给出看起来很完整的错误答案——只看 trace 会以为某服务已下线,实际仍有批处理在调它。
// 依赖清单的最小取法(按环境分别取,不要只看一个)
ss -tnp state established | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn
// 输出:每个下游 IP 的活跃连接数
// 用途:和 trace 里的调用方列表做差集,差集就是"未被观测到的依赖"
4. 临时方案的三个特征
| 特征 | 检测方式 | 为什么危险 |
|---|---|---|
| 没有名字 | 服务名是 IP、随机串、人名拼音 | 无法被检索,等于不存在 |
| 没有 owner | 无 CODEOWNERS、工单无人认领 | 出事无人敢改 |
| 无文档但在关键路径 | trace 频次高、文档没有 | 影响面未知,变更即盲改 |
三条同时成立时,它的实际地位是"影子核心系统":不在图上,失效即停摆,也是复盘里最常见的"没想到它这么关键"。
判据很直接:在关键路径上、但不在任何清单里的组件,优先于任何优化项处理。 先给它名字和 owner,再谈别的。
5. ADR 与实现的一致性检查
ADR 记录的是决策,代码记录的是结果。两者会分叉,分叉点就是偏离点。
─── 每个 ADR 至少要能被问三个问题
1) 决策里写的约束,现在是否还成立?(如"只允许同步调用")
2) 决策里写的例外,是否已经变成常态?(如"临时允许直连 DB")
3) 若今天重做这个决策,前提是否变了?(如流量量级、团队规模)
// 三个问题里有一个答"变了",这个 ADR 就该被更新或废止
// 注意:ADR 不更新不是文档问题,是决策失效而无人知晓
常见失效:ADR 写着"服务间只走 HTTP,不允许共享数据库",两年后三个服务直连同一库,没人记得有过这条。不是执行不力,是没有检查点。
6. 检查频率与成本
| 方式 | 成本 | 覆盖 | 漏什么 |
|---|---|---|---|
| 全量手工盘点 | 数人日,季度级 | 全面 | 盘点完就过期 |
| 抽样 trace 复核 | 半天,月度 | 主链路 | 低频与异步路径 |
| 自动化依赖断言 | 一次性投入 + 维护 | 代码内可见的边界 | 运行时与基础设施层 |
| 事故驱动复核 | 几乎为零 | 出问题的地方 | 一切没出问题的地方 |
组合才有效:断言守代码内边界,月度抽样看运行时,复盘补盲区。 全量盘点当常规动作是浪费,产出完成当天就开始贬值。
─── 一份可执行的检查节奏
月度: 抽 3 条主链路 trace,对一遍依赖清单
季度: 配置快照 diff(prod vs stage),差异逐条写原因
每次事故: 复盘补一问——哪个未记录偏离放大了它
每年: 过一遍 ADR,回答三个问题,废止或更新
7. 偏离的三种处置
| 处置 | 成本 | 适用 | 风险 |
|---|---|---|---|
| 承认并记录 | 最低(写一条) | 短期、影响可控 | 无人回访则变永久 |
| 纳入设计 | 中(改图、改基线) | 已验证有效、长期存在 | 把技术债洗成设计 |
| 消除 | 高(改造、下线) | 关键路径且无人负责 | 改造风险高于偏离本身 |
判据是影响面 × 无人负责程度:影响大且无人负责的必须消除,影响小且有人负责的登记即可。最危险的是把第二项当第一项用——"纳入设计"让问题从待办消失,却没支付任何成本。
8. 架构守护的自动化手段
| 手段 | 拦住什么 | 维护成本 |
|---|---|---|
| 依赖规则(禁止 A→B) | 分层违规、循环依赖 | 低,规则少而稳 |
| 分层断言(controller 不碰 dao) | 越层调用 | 中,重构期频繁误报 |
| 边界检查(API 清单) | 内部实现被外部依赖 | 中,需随契约更新 |
| 命名/包结构约定 | 结构漂移 | 低,易形式化空转 |
// 守护断言的写法要点:断言"禁止什么",而不是"必须长什么样"
noClasses().that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAPackage("..web..")
// 理由:禁止式断言在架构演进时仍然成立,形状式断言会锁死设计
// 维护规则:每条断言写明引入原因与失效条件,否则三年后没人敢删
自动化的价值是把"对齐"从事后盘点变成持续约束,代价是它只看得见代码里写出来的关系:基础设施偏离与跨进程旁路,一条都拦不住。
9. 产物是待办条目,不是报告
最常见的失败形态:产出三十页现状报告,会上宣讲后归档,三个月后没人打开。
有效的产物只有一种形态——可指派的条目:
─── 一条合格的偏离条目
偏离: 订单查询存在直连 DB 的旁路(绕过 order-service)
来源: 2024 年大促应急
证据: trace 中 12% 的查询来自 batch-worker 直连
影响: 分库后该路径会失效,影响面未知
处置: 登记(短期)→ 纳入设计(Q2 评估)→ 消除(分库前必须)
owner: @order-team
失效条件: 若 batch-worker 在分库前下线,本条自动关闭
判据:条目数不是产出,被指派且有失效条件的条目数才是。 没有 owner 的条目会沉淀成第二份报告。
10. 度量:四个数
| 指标 | 定义 | 读法 | 误读风险 |
|---|---|---|---|
| 未记录偏离数 | 检查发现的无记录项 | 真实债规模 | 只统计被看见的 |
| 临时方案数 | 名字含 temp/old 且无 owner | 影子系统规模 | 改名即可规避 |
| 依赖违规数 | 断言命中的条数 | 边界守得住吗 | 规则太严时虚高 |
| 对齐到处置时长 | 从发现到有 owner 的天数 | 机制是否在转 | 快速关闭可能只是标记 |
// 判读顺序
1) 未记录偏离数下降而临时方案数不降 → 登记了但没处置
2) 依赖违规数下降但架构没变 → 规则被放宽或断言被跳过
3) 对齐到处置时长上升 → 检查产出的条目没人接,机制空转
第二个是指标自噬:违规数好看,通常意味着约束被删掉,而不是结构变好。
11. 排错:五种"检查做了但没用"
| 现象 | 真实原因 | 处理 |
|---|---|---|
| 报告很厚没人动 | 产物不可指派 | 条目 + owner + 失效条件 |
| 每次结论都一样 | 只看代码,没看运行时 | 加 trace 与网关日志 |
| 图更新了,事故照旧 | 图是意图,证据是流量 | 以流量为准重画 |
| 断言误报被关掉 | 规则按形状写死 | 改写禁止式断言 |
| 临时方案越查越多 | 检查口径包含所有旁路 | 只统计关键路径上的 |
最后一条值得强调:把全部旁路列进清单会让清单失去优先级。只把"关键路径且无 owner"的算必须处理,其余登记备查。
12. 边界:什么时候不必做正式对齐
| 场景 | 值得做吗 | 理由 |
|---|---|---|
| 单体、单进程部署 | 不值得 | 编译器保证依赖,无跨进程偏离 |
| 代码小到能一次读完 | 不值得 | 全员共享同一份心智模型 |
| 团队与系统规模匹配 | 收益低 | 偏离还在人的记忆里 |
| 刚上线、组件数稳定 | 收益低 | 偏离尚未固化 |
| 多团队、组件数持续增长 | 值得 | 心智模型开始分叉 |
| 已出现说不清来历的组件 | 值得 | 影子系统已存在 |
判据:当"系统是什么样"已不能靠一个人的记忆回答时,对齐检查才有收益。 在那之前,它只是给所有人都清楚的事实补一份文档。
13. 自测
- 图上三个服务、线上十一个,多出来的八个最可能来自哪几类来源?
- 为什么"已记录但无 owner"的偏离仍然算风险?
- 只看 trace 会漏掉哪类依赖?用什么补齐?
- 一个组件没名字、没 owner、却在关键路径上,先做什么?
- "纳入设计"这种处置为什么可能掩盖问题?
- 依赖违规数下降,怎么判断是结构变好还是约束被删?