架构反模式与自查清单
反模式(anti-pattern)是「看起来像解决方案、反复出现、且有明确代价」的做法。它们的共同点是当时都能自圆其说:拆服务是为了解耦,加缓存是为了性能,上 CQRS 是为了扩展。本章先列十个高频反模式的症状、后果与改法,再给一份可以逐项打勾的自查清单——它覆盖本教程 01 到 21 章的要点,适合在设计评审前、上线前与季度复查时各跑一遍。
十个高频反模式
1. 分布式单体(distributed monolith)
- 症状:服务拆了十几个,但每次发版要同时上线;一个服务改字段,其他服务跟着改;本地起不了任何一个服务。
- 后果:拿到了微服务的全部复杂度(网络、部署、排障),却没拿到独立交付的收益,故障域反而扩大。
- 改法:先合并回去做模块化单体,用依赖治理理顺边界,再按独立变更节奏拆;判断标准是「能不能单独发布而不影响别人」。
2. 共享数据库(shared database)
- 症状:多个服务连同一个库、读写同一批表;跨服务「顺手 join 一下」;任何人的 SQL 都能改别人的数据。
- 后果:表结构变成事实上的全局契约,任何变更都要跨团队协调;数据所有权消失,一致性无法在单个服务内保证。
- 改法:每份数据指定唯一属主,其他服务只能通过接口或只读副本访问;同步走事件,查询走读模型,过渡期用视图或双写加对账逐步收口。
3. 按技术分层拆服务(layered services)
- 症状:网关服务、业务逻辑服务、数据访问服务各是一组独立部署的微服务,一次请求穿过三层。
- 后果:每次业务改动要同时改三层、联调三个团队;延迟被层层放大;边界随技术而非业务变化,越拆越耦合。
- 改法:按业务能力或限界上下文(bounded context)拆分,让一次业务变更尽量落在一个服务内;技术分层只保留在服务内部。
4. 滥用 CQRS(CQRS everywhere)
- 症状:每个服务都拆出命令侧与查询侧、各自一套存储与同步链路;读模型延迟成为新的客服问题。
- 后果:多出一套同步、对账、重建与监控成本;简单 CRUD 场景被复杂化,故障点成倍增加。
- 改法:只在读写模型差异真实存在(聚合维度不同、性能量级不同、需要多来源合并)时引入,并写明可接受的延迟上限;其余场景用同一模型加缓存即可。
5. 无主的边界(ownerless boundary)
- 症状:某个服务或模块「大家都用、没人负责」;出问题在群里 @ 所有人;接口文档过期没人改。
- 后果:变更无人评审、故障无人值班、技术债无人偿还,边界会随时间自发腐化。
- 改法:每个服务、每份数据、每个接口都有明确 owner 与值班安排;无主资产要么认领,要么下线。
6. 只增不减的接口(append-only API)
- 症状:字段只加不删,版本只升不降,废弃接口挂着三年还被调用;文档写着「已废弃」,实际无人迁移。
- 后果:契约越来越重,兼容负担永久累积;新实现必须兼容所有历史怪癖,重构成本指数上升。
- 改法:新增字段保持向后兼容,删除字段走「标记废弃 → 公告期 → 埋点确认零调用 → 下线」流程,并写进接口版本策略。
7. 把网关当业务层(fat gateway)
- 症状:路由、鉴权之外,网关里塞了业务编排、参数拼装、字段裁剪,甚至事务逻辑。
- 后果:网关成为变更最频繁、最不可测的单点;多端需求互相牵制;网关故障即全站不可用。
- 改法:网关只做跨切面的事(路由、鉴权、限流、协议转换、可观测性注入);多端适配下沉到 BFF(Backend for Frontend),业务逻辑归业务服务。
8. 可观测性事后补(observability afterthought)
- 症状:上线后才发现没有大盘、traceId 断在中间、日志没有结构化、告警阈值靠猜。
- 后果:故障时无法定位,恢复时间靠运气;事后补埋点要改所有服务的调用约定,成本远高于设计期定下来。
- 改法:把 SLI/SLO、上下文传播约定、日志字段规范、指标标签白名单、告警契约写进设计评审与上线清单。
9. 为了微服务而微服务(microservices for its own sake)
- 症状:团队十几人、业务量不大,却规划了三十个服务;拆分理由是「业界都这么做」。
- 后果:交付速度下降,运维与协调成本超出团队能力;最终被迫合并,白付一轮迁移成本。
- 改法:先看约束——独立扩缩容需求、独立发布节奏、团队规模、故障隔离要求,任一不成立就先做模块化单体,同时在代码里留好拆分缝。
10. 架构文档与代码不一致(documentation drift)
- 症状:架构图画的是三年前的版本;文档里的服务早已下线;新人按文档理解系统,得出错误结论。
- 后果:架构决策失去可追溯性,评审基于错误前提;事故复盘找不到当初的约束。
- 改法:关键决策用架构决策记录(ADR)与代码同仓同评审;架构图尽量由代码与部署清单生成,或至少与其绑定校验;文档过期即视为缺陷。
反模式的四个共同根源
| 根源 | 表现 | 对策 |
|---|---|---|
| 把手段当目标 | 为了「微服务」「事件驱动」而设计,而不是为了质量属性 | 每个结构决定都要能回答它解决哪条质量属性场景 |
| 边界按技术划分 | 按分层、按框架、按数据库拆,而不是按业务拆 | 按业务能力与限界上下文划界,并写清数据所有权 |
| 约束不可验证 | 只写「要高可用」,不写指标与验收人 | 质量属性场景化:指标、条件、验收方 |
| 决策不留痕 | 当时为什么这么定没人知道,于是没人敢改 | ADR 记录背景、选项、结论与代价,只追加不修改 |
自查清单
用法:按下面的时机逐项打勾,打不勾的写成「未通过项 + 责任人 + 期限」。清单是下限而不是上限——全部通过不代表架构优秀,只代表没有明显的结构性欠债。
| 时机 | 重点跑哪一组 | 产出 |
|---|---|---|
| 设计评审前 | 一组(边界与依赖)+ 四组(演进与治理) | 拆分理由与数据所有权写进 ADR |
| 上线前 | 二组(数据与一致性)+ 三组(通信与弹性) | 未通过项清单、责任人与关闭期限 |
| 季度复查 | 四组全跑,并与上一轮结果对比 | 技术债清单更新与演进计划 |
一组:边界与依赖
- 每个服务与模块都有唯一 owner、可检索的对外契约和明确的数据所有权。
- 依赖方向单向、无环;跨模块调用只走公开接口,内部实现不被外部引用。
- 拆分依据是业务能力或限界上下文,不是技术分层或团队现状的临时妥协。
- 没有服务直接读写别人的库、表或缓存键空间;确有例外时有记录与收口计划。
- 服务边界与团队边界大体一致(康威定律),跨边界变更需要双方评审。
- 新建服务能回答「独立部署与独立扩缩容的理由」,而不是「业务大就拆」。
- 共享的是契约与库,不是运行时状态、数据库连接或本地缓存。
二组:数据与一致性
- 每份数据有唯一属主,写入路径唯一,其他方只读副本或通过接口读取。
- 跨服务写操作要么是本地事务加 Outbox,要么有补偿与对账,不存在「两边各写一半」。
- 关键业务操作在数据库唯一索引上做幂等,而不是靠应用层「先查后写」。
- 金额、库存、额度类字段用整数或定点表示,且有可复算的流水或分录。
- 读模型允许滞后时,滞后上限已知、业务方认可,调用方或界面有对应兜底。
- 缓存的失效策略与不一致收敛路径明确,缓存不承担「唯一真相」的角色。
- 数据迁移或结构变更采用双写、双读或比对校验,并有明确回滚点。
- 删除、归档与保留期策略明确,不存在只增不减、永不清理的主表。
三组:通信与弹性
- 所有跨进程调用都显式设置超时,且超时预算逐层递减。
- 重试只出现在幂等接口上,并有次数上限、退避与抖动。
- 熔断、隔离、降级按依赖重要性分级配置,不是全局一刀切。
- 消息消费是幂等的,失败有死信队列与人工处置入口。
- 顺序敏感的场景有分区键或序号机制,不依赖「单队列天然有序」的假设。
- 服务分级明确,非核心依赖故障不拖垮核心链路。
- 容量水位有压测基线,扩容动作有文档化步骤与耗时预估。
- 依赖故障时的表现经过演练(故障注入或混沌实验),而不是只写在预案里。
四组:演进与治理
- 关键架构决策有 ADR,包含背景、备选方案、结论与代价。
- 架构文档与实际代码和部署结构一致,不一致有更新机制并纳入评审。
- 接口版本策略与废弃流程明确:新增兼容,删除有公告期与零调用验证。
- 技术债有清单、有优先级、有偿还节奏,与技术决策绑定而不是「有空再说」。
- 可观测性在需求阶段定义:关键指标、日志字段、链路埋点与告警阈值。
- 每次事故复盘都有结论,并落到具体改动(测试、监控、代码或预案)。
- 演进路径清晰,能在模块化单体与服务化之间双向调整,不制造不可逆绑定。
- 每个季度用本清单复查一次,未通过项有责任人与关闭状态。
小结:反模式的共同根源是把手段当目标、边界按技术划分、约束不可验证、决策不留痕;十个高频反模式分别落在边界、数据、通信与治理四类结构性问题里,改法都指向同一件事——让每个结构决定都能对应到一条可验收的质量属性场景。自查清单按「边界与依赖、数据与一致性、通信与弹性、演进与治理」四组逐条打勾,在评审前、上线前与季度复查中反复使用,未通过项必须带责任人与期限,否则清单只是纸面安慰。