Strangler Fig 模式深度指南:渐进式遗留系统迁移的艺术
"不要一次性重写整个系统——逐步接管它。"
在软件工程的世界里,遗留系统就像一颗根系深入土壤的老树。你不可能在一夜之间连根拔起,那样只会让系统崩溃。Strangler Fig 模式(迁移 strangler) 就是来解决这个问题的:它教你如何像寄生植物一样,一步步"绞杀"旧系统,让新系统在旧系统的位置上茁壮成长。
为什么我们需要 Strangler Fig?
想象一下这些场景:
- 你接手了一个 15 年的单体应用,技术栈老旧得找不到文档
- 客户要求新功能,但你不敢在核心代码上冒险
- 重写团队尝试了两次,都以失败告终——新系统永远无法完全替代旧系统
- 你接手时发现系统已经没人敢碰了,任何改动都像在拆炸弹
这些都是真实的痛点。传统的"大爆炸"式重写(Big Bang Rewrite)失败率极高,原因很简单:
- 时间跨度太长——两年后,业务需求早就变了
- 风险太高——一次性替换所有功能几乎不可能
- 团队疲惫——没人能同时维护两套系统
Strangler Fig 模式的核心思想是:不要重写,而是逐步迁移。 就像一棵绞杀榕(Strangler Fig)在旧树上生长,最终完全取代它。
模式的核心机制
Strangler Fig 模式有四个关键组件:
1. 基础设施代理(Infrastructure Proxy)
在用户和旧系统之间建立一个"门神",所有请求先经过它。这可以是 API Gateway、反向代理(如 Nginx、Envoy),或者是你的业务层中的一个路由模块。
用户请求 → 代理层 → [决定走新还是走旧]
2. 特征开关(Feature Toggle)
这是控制迁移进度的"开关"。每个要迁移的功能都有一个开关——打开时走新系统,关闭时走旧系统。这样你可以:
- 先把流量切 1% 到新系统,观察是否有问题
- 逐步增加比例,直到 100%
- 如果新系统出问题,立即切回旧系统
3. 防腐层(Anti-Corruption Layer)
这是新旧系统之间的"翻译官"。新系统不需要直接理解旧系统的数据结构,而是通过防腐层来转换:
新系统 ←→ 防腐层 ←→ 旧系统
防腐层负责:
- 数据格式转换(JSON ↔ XML ↔ 旧格式)
- 接口适配(REST ↔ RPC)
- 业务规则桥接
4. 增量迁移路径(Incremental Migration Path)
这是最重要的——你要规划一条"蚕食"路线。每次只迁移一个子域、一个模块、一个功能。常见的迁移顺序:
- 最外围的边界服务——如通知、搜索、报表(风险低,收益明显)
- 独立的子域——如用户中心、订单管理(有一定复杂度但相对独立)
- 核心业务域——最后才迁移最关键的部分(风险最高)
实战案例:电商订单系统的迁移
让我们通过一个真实的例子来理解。假设你有一个 PHP 写的单体电商系统,现在要用 Go 重构。
步骤一:建立代理层
// nginx 配置示例
location /api/ {
# 所有请求先经过代理层
proxy_pass http://migration-proxy;
}
代理层根据配置决定路由:
// 路由配置
routes := map[string]string{
"/api/products": "new", // 走新 Go 服务
"/api/orders": "old", // 走旧 PHP 服务
"/api/users": "both", // 双写,验证一致性
}
步骤二:迁移第一个功能——产品搜索
搜索是相对独立的功能,出问题不��响核心流程。
// 新 Go 服务 - 产品搜索
func SearchProducts(q string) []Product {
// 从新数据库查询
return newDB.Search(q)
}
配置路由:
{
"feature": "product-search",
"enabled": true,
"target": "new",
"fallback": "old"
}
步骤三:双写验证
对于关键数据(如订单),你要确保新旧系统数据一致:
// 双写逻辑
func CreateOrder(order Order) {
// 同时写入新旧系统
oldResult := oldPHPAPI.CreateOrder(order)
newResult := newGoAPI.CreateOrder(order)
// 比对结果,不一致则告警
if oldResult.ID != newResult.ID {
alert("数据不一致!")
}
}
步骤四:逐步切流
// 阶段一:1% 流量到新系统
{"target": "new", "percentage": 1}
// 阶段二:观察一周后增加到 10%
{"target": "new", "percentage": 10}
// 阶段三:增加到 50%
{"target": "new", "percentage": 50}
// 阶段四:100%,旧系统下线
{"target": "new", "percentage": 100}
模式的优势与风险
优势
| 优势 | 说明 |
|---|---|
| 降低风险 | 每次只迁移一小部分,出问题可快速回滚 |
| 持续交付 | 新功能可以在新系统上开发,不影响旧系统 |
| 业务连续 | 用户无感知,平滑过渡 |
| 团队适应 | 团队可以逐步学习新技术栈 |
| 可验证 | 每个迁移都可以验证后再继续 |
风险与应对
| 风险 | 应对策略 |
|---|---|
| 数据一致性 | 双写 + 定期比对 + 补偿机制 |
| 性能差异 | 提前做性能测试,切流后监控 |
| 旧系统成为阻碍 | 为旧系统设置"日落日期",强制迁移 |
| 迁移范围蔓延 | 严格控制每次迁移的边界 |
何时使用这个模式?
Strangler Fig 模式适用于以下场景:
✅ 适用
- 遗留系统需要迁移到新架构
- 无法停止服务进行大规模重写
- 技术栈需要升级(Ruby → Go,Java → Kotlin)
- 微服务化拆分单体应用
❌ 不适用
- 旧系统已经无法维护(需要先修复核心问题)
- 业务逻辑完全未知且无文档
- 迁移成本超过重写成本
实施检查清单
在开始迁移之前,确保你具备:
- [ ] 完整的旧系统 API 文档或逆向得到的接口定义
- [ ] 可访问的测试环境和生产环境的灰度发布能力
- [ ] 监控和告警系统,能够区分新旧系统的请求
- [ ] 回滚机制,能够在发现问题后立即切回旧系统
- [ ] 团队对新旧两套系统都有一定的了解
总结
Strangler Fig 模式教会我们的,不仅是一种迁移技术,更是一种工程哲学:
渐进优于激进,迭代优于瀑布,可控优于冒险。
当你面对一个庞大的遗留系统时,不要想着一口吃掉它。像绞杀榕一样,找到一个切入点,慢慢生长,最终取代它。
下次当你面对"要不要重写"这个问题时,先问问自己:能不能先切 1% 的流量试试?
本文是架构模式系列的第 32 篇。如果你对这个话题感兴趣,欢迎关注并交流你的实践经验。
Top comments (0)