13.2 风险驱动的服务提取与 Strangler 迁移
服务拆分不是把目录复制到新仓库,而是改变部署、数据、故障和团队责任的边界。迁移成功的标志也不是“服务数量增加”,而是目标约束得到改善,并且新增复杂度可被运营。
先写出拆分假设
一个可验证的拆分理由应包含现状、目标和度量,例如:
计分模块占峰值 CPU 的 70%,但必须随低负载的报名模块一起扩容。把计分提取为独立服务后,希望基础设施成本下降 25%,同时结算成功率不低于 99.95%。
常见有效驱动包括:
- 独立扩缩能显著降低成本或资源争用;
- 发布节奏差异造成真实等待;
- 故障隔离有明确业务价值;
- 合规或数据驻留要求形成硬边界;
- 团队可以端到端拥有构建、部署和运行。
“微服务更先进”“代码很多”都不是足够的决策依据。
选择第一条切缝
优先选择业务边界清楚、数据所有权明确、调用扇入可控的能力。第一步应小到能验证平台能力,又大到能产生独立价值。
拆分前至少盘点:
- 上下游调用者和协议;
- 读写的表、文件与缓存;
- 事务和一致性要求;
- 峰值流量、延迟预算和失败语义;
- 值班、告警、容量和恢复责任。
Strangler:逐步替换流量
Strangler Fig 迁移在旧系统前建立路由边界,让新旧实现并存:
客户端
│
▼
Gateway / Facade
├── /scores/query ──> 新计分服务
├── /scores/write ──> 旧单体
└── 其他请求 ──> 旧单体一次安全迁移通常包含:
- 为旧能力建立可观测的契约边界;
- 新服务实现同一契约并做影子读取或离线对比;
- 按租户、用户或流量比例切读请求;
- 迁移写入所有权;
- 停止旧路径,观察一个完整业务周期;
- 删除旧代码和兼容设施。
最后一步不可省略。永久双轨会让临时迁移层成为下一代遗留系统。
写请求不能盲目回退
下面的策略很危险:新服务超时后自动调用旧单体。超时只表示客户端没有收到结果,不表示新服务没有提交;回退可能造成重复扣款或重复结算。
写路径需要:
- 稳定的幂等键;
- 可查询的操作状态;
- 明确区分“确定失败”和“结果未知”;
- 重试预算与退避;
- 对账与人工修复路径。
读请求在陈旧数据可接受时更容易回退,但仍应记录来源和一致性差异。
迁移数据所有权
直接双写两个数据库会产生部分成功。更可靠的选择取决于阶段:
历史回填 + 变更捕获
先按快照回填历史数据,再用 CDC 捕获增量。切换前按业务主键、计数、校验和与关键不变式对账。
Transactional Outbox
旧系统在同一数据库事务中写业务数据和 Outbox,由独立发布器把变更交给新服务。它避免“数据库已提交、消息没发出”的窗口,但消费者仍要幂等。
Expand and Contract
协议或 schema 先扩展到新旧版本都能接受,迁移生产者和消费者后,再删除旧字段。破坏性变更不能与流量切换绑在同一个不可逆步骤。
数据所有权完成迁移后,旧系统不应继续把新服务数据库当共享表访问。
把故障模型当成设计输入
进程内调用变成网络调用后,会新增:
- 连接失败、超时和部分响应;
- 重试放大与排队;
- 版本并存;
- 调用链追踪和跨服务鉴权;
- 服务正常但依赖退化的灰色故障。
每个远程调用都应有超时,但不是每个失败都应重试。只有幂等或带幂等保护的操作适合自动重试;重试还要有次数、总时长和抖动,避免同步风暴。
每一步都保留退出条件
迁移计划至少写明:
| 项目 | 示例 |
|---|---|
| 成功指标 | p95 延迟、成功率、成本、发布等待时间 |
| 不变量 | 积分总和、每场比赛只结算一次 |
| 切流单位 | 内部账号 → 5% 用户 → 25% → 100% |
| 停止条件 | 错误率高于基线 0.2 个百分点 |
| 回退方式 | 路由恢复旧读路径;写路径暂停并对账 |
| 最终清理 | 删除旧表写入、旧 API 和迁移开关 |
完成迁移后重新评估最初假设。如果独立服务没有改善目标指标,应允许合并或停止继续拆分,而不是用沉没成本推动更多服务。
下一章讨论服务边界已经存在之后的基础通信模式:发现、网关、超时、重试和断路器如何组合成可预测的调用链。
参考资料
- Martin Fowler, Strangler Fig Application
- AWS Prescriptive Guidance, Strangler fig pattern
- Chris Richardson, Transactional Outbox