跳到内容

13.2 风险驱动的服务提取与 Strangler 迁移

服务拆分不是把目录复制到新仓库,而是改变部署、数据、故障和团队责任的边界。迁移成功的标志也不是“服务数量增加”,而是目标约束得到改善,并且新增复杂度可被运营。

先写出拆分假设

一个可验证的拆分理由应包含现状、目标和度量,例如:

计分模块占峰值 CPU 的 70%,但必须随低负载的报名模块一起扩容。把计分提取为独立服务后,希望基础设施成本下降 25%,同时结算成功率不低于 99.95%。

常见有效驱动包括:

  • 独立扩缩能显著降低成本或资源争用;
  • 发布节奏差异造成真实等待;
  • 故障隔离有明确业务价值;
  • 合规或数据驻留要求形成硬边界;
  • 团队可以端到端拥有构建、部署和运行。

“微服务更先进”“代码很多”都不是足够的决策依据。

选择第一条切缝

优先选择业务边界清楚、数据所有权明确、调用扇入可控的能力。第一步应小到能验证平台能力,又大到能产生独立价值。

拆分前至少盘点:

  • 上下游调用者和协议;
  • 读写的表、文件与缓存;
  • 事务和一致性要求;
  • 峰值流量、延迟预算和失败语义;
  • 值班、告警、容量和恢复责任。

Strangler:逐步替换流量

Strangler Fig 迁移在旧系统前建立路由边界,让新旧实现并存:

text
客户端


Gateway / Facade
  ├── /scores/query  ──> 新计分服务
  ├── /scores/write  ──> 旧单体
  └── 其他请求       ──> 旧单体

一次安全迁移通常包含:

  1. 为旧能力建立可观测的契约边界;
  2. 新服务实现同一契约并做影子读取或离线对比;
  3. 按租户、用户或流量比例切读请求;
  4. 迁移写入所有权;
  5. 停止旧路径,观察一个完整业务周期;
  6. 删除旧代码和兼容设施。

最后一步不可省略。永久双轨会让临时迁移层成为下一代遗留系统。

写请求不能盲目回退

下面的策略很危险:新服务超时后自动调用旧单体。超时只表示客户端没有收到结果,不表示新服务没有提交;回退可能造成重复扣款或重复结算。

写路径需要:

  • 稳定的幂等键;
  • 可查询的操作状态;
  • 明确区分“确定失败”和“结果未知”;
  • 重试预算与退避;
  • 对账与人工修复路径。

读请求在陈旧数据可接受时更容易回退,但仍应记录来源和一致性差异。

迁移数据所有权

直接双写两个数据库会产生部分成功。更可靠的选择取决于阶段:

历史回填 + 变更捕获

先按快照回填历史数据,再用 CDC 捕获增量。切换前按业务主键、计数、校验和与关键不变式对账。

Transactional Outbox

旧系统在同一数据库事务中写业务数据和 Outbox,由独立发布器把变更交给新服务。它避免“数据库已提交、消息没发出”的窗口,但消费者仍要幂等。

Expand and Contract

协议或 schema 先扩展到新旧版本都能接受,迁移生产者和消费者后,再删除旧字段。破坏性变更不能与流量切换绑在同一个不可逆步骤。

数据所有权完成迁移后,旧系统不应继续把新服务数据库当共享表访问。

把故障模型当成设计输入

进程内调用变成网络调用后,会新增:

  • 连接失败、超时和部分响应;
  • 重试放大与排队;
  • 版本并存;
  • 调用链追踪和跨服务鉴权;
  • 服务正常但依赖退化的灰色故障。

每个远程调用都应有超时,但不是每个失败都应重试。只有幂等或带幂等保护的操作适合自动重试;重试还要有次数、总时长和抖动,避免同步风暴。

每一步都保留退出条件

迁移计划至少写明:

项目示例
成功指标p95 延迟、成功率、成本、发布等待时间
不变量积分总和、每场比赛只结算一次
切流单位内部账号 → 5% 用户 → 25% → 100%
停止条件错误率高于基线 0.2 个百分点
回退方式路由恢复旧读路径;写路径暂停并对账
最终清理删除旧表写入、旧 API 和迁移开关

完成迁移后重新评估最初假设。如果独立服务没有改善目标指标,应允许合并或停止继续拆分,而不是用沉没成本推动更多服务。

下一章讨论服务边界已经存在之后的基础通信模式:发现、网关、超时、重试和断路器如何组合成可预测的调用链。

参考资料

Built with VitePress | Software Systems Atlas