27.5 单体内部的演进
一句话
单体内部的演进顺序是固定的:先分包(可读)→ 再立边界(可校验)→ 再拆数据(可独立)→ 最后才谈拆进程;跳过前三步直接拆服务,等于把泥球装进容器。
四个阶段与退出标准
| 阶段 | 做什么 | 退出标准 | 常见错误 |
|---|---|---|---|
| ① 分包 | 按业务能力重组目录 | 新人能按目录找到 80% 的代码 | 按技术层(service/dao)切 |
| ② 立边界 | 加依赖规则、定义模块 API | CI 能阻止跨模块内部依赖 | 规则只写在文档 |
| ③ 拆数据 | 每模块独立 schema/表前缀 | 改表只影响一个模块 | 共享表 + 视图绕过 |
| ④ 拆进程 | 只拆 “有独立伸缩/发布/隔离需求” 的模块 | 该模块可独立部署且回滚 | 按模块数量平均拆 |
代码:从 “直接调用” 到 “接口 + 实现”
// 演进前:订单模块直接依赖用户模块的具体实现
func (s *OrderService) Create(uid int64) error {
u := userRepo.Get(uid) // 跨模块内部调用,边界被击穿
return s.repo.Insert(u.ID)
}
// 演进后:订单只依赖自己定义的接口(依赖倒置),实现由装配层注入
type UserReader interface { Get(ctx context.Context, id int64) (User, error) }
func (s *OrderService) Create(ctx context.Context, uid int64) error {
u, err := s.users.Get(ctx, uid) // 面向接口,可替换、可 mock
if err != nil { return err }
return s.repo.Insert(ctx, u.ID)
}
实测:用 “退出标准” 验收演进,而不是靠感觉
演进到阶段 ③(拆数据/立边界)后,用同一脚本复测:
| 退出标准 | 修复前 | 修复后 | 是否达标 |
|---|---|---|---|
| 跨模块 internal 违规 | 1 | 0 | ✅ |
| 模块级循环依赖 | 1 | 0 | ✅ |
| 最高扇入 | 2 | 下降(改为依赖公开接口) | ✅ |
| 单次变更影响模块数 | 2–3 | 1–2 | ✅ |
这就是 “可验证的退出标准”:每个阶段都用同一组指标复测,达标才进入下一阶段。没有这套验收,“演进” 很容易停在 “目录看起来很整齐” 的中间态。
反模式
| 反模式 | 后果 |
|---|---|
| 把 “拆服务” 当演进第一步 | 分布式复杂度先到,边界问题没解决 |
| 只做 ①②、不做 ③ | 数据仍共享,独立发布无从谈起 |
| 按模块数平均拆进程 | 拆出一堆 “必须一起发” 的服务 |
| 演进没有度量 | 无法判断 “是否变好”,也无法说服管理层 |
| 演进期间冻结所有需求 | 业务不可接受,最终半途而废 |
| 演进按“时间”推进而不是按“指标”推进 | 到点就宣布完成,实际耦合没降 | | 只测一次不复测 | 新代码又把环加回来,无人察觉 |
两条铁律
🔑 每一步都要有可验证的退出标准;没有标准就会停在 “看起来整齐” 的中间态。
🔑 只有 “独立伸缩/发布/故障隔离” 三者之一成立,才值得拆进程。
自检
Q1 为什么 “依赖倒置” 是拆进程前的必要准备?
▸ 答案:因为拆进程本质是把进程内调用换成跨进程调用:只有当调用方依赖的是自己定义的接口时,替换实现(本地 → 远程)才不需要改业务代码。直接依赖具体实现时,拆进程等于全量重写。
Q2 “按模块数平均拆” 为什么错?
▸ 答案:因为拆分的依据是变更与伸缩的独立性,不是数量。平均拆会把 “总是一起改” 的模块拆成两个服务,导致每次需求都要双发、双回滚,协作成本上升而收益为零。
Q3 演进到阶段 ③ 时,如何避免 “用视图绕过数据隔离”?
▸ 答案:把视图也纳入依赖规则:跨模块只能读对方暴露的 API/只读物化视图,且该视图由对方 owner 维护。绕过检查的 JOIN 要在评审中显式拒绝,否则数据边界会被 “临时需求” 反复打穿。
Q4 怎么衡量演进是否有效?
▸ 答案:三个指标:单次变更影响模块数(应下降)、循环依赖数(应为 0)、独立发布的服务占比(应上升)。指标不动而结构大改,说明在做 “形式重构”。
Q5 为什么 “阶段 ②(立边界)” 不能跳过?
▸ 答案:因为没有边界时,阶段 ③(拆数据)会立刻失败:你无法判断 “哪些表属于哪个模块”,也挡不住别的模块直接读写。边界规则是拆数据的前提条件——它先回答 “谁拥有什么”,数据归属才有依据。
Q6 演进过程中,如何避免 “为了达标而做形式化重构”?
▸ 答案:把每个阶段的退出标准写成可自动检查的指标(违规数、环数、扇入、影响模块数),并在 CI 里持续跑。指标不降就不是重构,只是搬代码。另一条纪律:每次重构都必须绑定一个真实的业务痛点(发布慢/改一处坏三处),否则容易被当成 “技术自嗨” 而失去支持。
Q7 演进到 “拆进程” 前,还有哪条指标必须看?
▸ 答案:独立发布能力:该模块能否在不发布其他模块的情况下上线、回滚。它无法靠静态分析得出,要做一次真实演练(改一行、只发这个模块、观察其他模块是否受影响)。演练通过,拆进程才有意义;否则拆出来仍是 “必须一起发” 的分布式单体。
故障复盘(典型模式,脱敏)
| 阶段 | 现象 | 判断 | 动作 |
|---|---|---|---|
| 拆服务后 | 订单服务与库存服务必须同版本发布 | 数据仍共享一张表 | 拆 schema |
| 灰度 | 无法只发一个服务,回滚要双回滚 | 独立发布能力未验证 | 演练单发 |
| 演练 | 单发订单服务导致库存读不到数据 | 缺少契约与兼容层 | 加双写+兼容读 |
| 达标后 | 可单发、可单回滚 | 退出标准通过 | 继续拆下一个 |
根因:跳过 “拆数据” 与 “独立发布演练” 直接拆进程。代价:两个月无效拆分成分布式单体。根治:每个阶段用同一组指标复测(违规 0、环 0、单发通过)再进入下一阶段。
关联
- 前置:27.4 大泥球的形成路径
- 并列:27.2 模块化单体、30.2 服务粒度
- 下游:28.5 遗留系统集成 (绞杀者 / 防腐层)(待补)、58.4 演进路线图