跳到内容

第 6 卷 / 第 13 章 / 第 2 课

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

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

先写出拆分假设 ​

一个可验证的拆分理由应包含现状、目标和度量。下面数字是假设案例,用于说明决策写法:

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

这还不是结论。CPU 占比不能直接推出可节省费用,应在可比流量和时间窗口下测量实例、网络、存储与新增运维成本;成功率按逻辑结算请求统计,超时/未知结果要纳入,不能用多次重试把分母做大。

常见有效驱动包括:

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

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

选择第一条切缝 ​

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

拆分前至少盘点:

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

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 和迁移开关

停止阈值也要定义统计窗口和最少样本数。例如基线错误率为 0.1%,增加 0.2 个百分点后的阈值是 0.3%,不是 0.1002%。读路径回退还需验证旧端新鲜度;写入接管后,回退必须先重新交接写入权并追平数据,不能只拨回开关。

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

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

参考资料 ​

Built with VitePress | Software Systems Atlas