绞杀者模式与遗留系统迁移¶
30 秒版(开场)¶
绞杀者(Strangler Fig):逐路由、逐能力替换旧系统。关键不是“同时写两套库”,而是每阶段明确 唯一 source of truth、同步方向、数据回放/对账和回滚条件,避免把迁移做成永久双主。
3 分钟版(精讲深度)¶
- 是什么:在旧系统外围设 Routing Facade(网关/BFF),按 URL/用户/百分比把流量导向新旧实现。
- 为什么:迁移讨论几乎总会落到「怎么把 PHP/Java 老单体迁到 Go 微服务」;大爆炸风险不可接受。
- 怎么做:按垂直切片迁移;先做只读/影子验证,再切写 ownership。数据同步优先用同事务 outbox/CDC、可重放 backfill 与校验;若短期应用双写无法避免,也必须有幂等、失败记录和明确主写,不能忽略任一写错误。
10 分钟版(原理 + 图示)¶
flowchart TB
Client[客户端] --> GW[Routing Facade / API Gateway]
GW -->|90%| Legacy[遗留单体]
GW -->|10% 灰度| NewGo[Go 新服务]
NewGo --> NewDB[(新库)]
Legacy --> OldDB[(旧库)]
OldDB -.->|CDC / backfill| NewDB
Reconcile[对账 Job] --> NewDB
Reconcile --> OldDB
迁移阶段模板(叙事用)
| 阶段 | 动作 | 回滚 |
|---|---|---|
| 0 | 只读 API 读新写旧 | 关开关回 100% 旧 |
| 1 | 旧库仍为主写,CDC/outbox 同步新库并影子校验 | 停同步,读写旧 |
| 2 | 短暂停写或双向兼容窗口后切换新库为唯一主写 | 只有在反向同步、schema 兼容和演练通过时才能切回 |
| 3 | 下线旧模块 | 保留只读备份 |
Go 侧常见落地
- 网关:Nginx/APISIX 按 path 路由;或 Go BFF 内
if feature.NewOrder()转发 - 同步:业务事务写 outbox 后 relay,或 Debezium/Canal CDC 单向同步
- 对账:按主键范围比对记录版本、关键字段与金额汇总;仅比较总行数/checksum 可能漏掉相互抵消的错误
生产场景¶
- 支付链路迁移:先迁查询,最后迁扣款;支付 never 允许 silent failure
- Session 粘滞:旧系统有 session,迁移期 JWT 统一或 SSO 桥接
- 与 S-ARCH-19 关系:19 讲单体→微服务概念;本题讲 怎么安全落地
排查与工具¶
- 特性开关:LaunchDarkly / 自研 config center
- 指标:新旧路径 错误率、P99、业务成功率 对比
- 影子流量:复制真实读请求,但必须屏蔽写、副作用、外部通知和计费
架构取舍¶
| 双写 | CDC |
|---|---|
| 实现快 | 解耦应用 |
| 应用复杂 | 延迟、顺序需处理 |
何时不用绞杀者:系统极小、或旧系统可整体停机窗口重写(罕见)。
深挖问答¶
- 迁移同步不一致怎么办? → 每阶段定义 source of truth、记录失败事件、可幂等重放并对账修复;不能默认“第二次写失败也先返回成功”。
- 如何选第一个切片? → 边界清晰、流量可灰度、非资金核心路径优先。
- 组织阻力? → 联合 KPI、小步快跑、可视化迁移看板(架构师软技能)。
- Go 与旧 Java 共存事务? → 避免跨库 2PC;Saga + 幂等(见 S-DIST-05)。
反模式与事故¶
- Big Bang 切换 → 大促前全量切,回滚不及
- 无对账 → 双写静默丢单
- 按层迁移(先迁所有 DAO)→ 长期两套并行,成本失控
代码示例¶
func (h *OrderHandler) Create(c *gin.Context) {
if h.flags.UseNewOrderService(c.Request.Context(), userID) {
h.newSvc.Create(c)
return
}
h.legacyProxy.Create(c)
}