老系统越堆越乱,却又”不敢动、不能停”——这是每个技术团队迟早会遇到的死局。绞杀者模式(Strangler Fig Pattern)是 Martin Fowler 提出的一套遗留系统重构手法:不一次性重写,而是在老系统前面加一层路由,把功能逐个”绞杀”式迁移到新服务,全程零宕机。本文给你能直接落地的 Nginx 与 CI 步骤。
一、为什么”大爆炸重写”几乎必败
很多团队一上来就想”推倒重来”:用新框架重写一遍,切流量,老系统下线。听起来干净,现实却很残酷——重写周期往往以年计,而业务需求不会等你。等到新系统好不容易上线,老系统又已经演化出一堆新的分支逻辑,两边永远对不齐。
更致命的是风险集中:一次性切换 = 一次性把所有未知缺陷同时暴露。一旦出问题,回滚成本极高。下面这张对比表说清了两种路线的本质差别:
| 维度 | 大爆炸重写 | 绞杀者模式 |
|---|---|---|
| 切换方式 | 某天零点全量切换 | 按功能灰度、逐路径迁移 |
| 宕机风险 | 高,且集中爆发 | 低,单点可快速回退 |
| 交付节奏 | 漫长,期间无产出 | 每迁完一个功能即可上线 |
| 回滚成本 | 整体回滚,伤筋动骨 | 只把该路径指回老系统 |
| 适合场景 | 体量小、逻辑稳定 | 核心系统、长期演进 |
二、绞杀者模式到底是什么
名字来自一种植物:绞杀榕(Strangler Fig)的种子落在 host 树冠上,气根一路垂到地面、扎进土里,慢慢长成包裹老树的网状结构,最终老树腐朽,新树独立成型。
映射到软件:老系统是被包裹的” host 树”,我们在它前面放一个路由/代理层作为”气根入口”。新功能写成独立服务,逐步接管流量;当所有路径都被新服务接管后,老系统自然”枯萎”下线——全程用户无感知。
三、落地四步法
1. 找到系统的”接缝”
接缝(Seam)是能被拦截的请求边界:一个 API 前缀、一个页面路由、一张数据库表。优先挑独立、低耦合、价值高的模块下手,比如”用户通知”比”订单核心”更适合做第一刀。第一刀必须小到一两天能完成,用来验证整条管线。
2. 在前面放一个路由/代理层
这是模式成立的关键。所有外部流量先过代理,由代理决定:走老系统,还是走新服务。没有这一层,你就只能靠改前端代码分流,等于埋雷。
3. 逐个把功能搬进新服务
每迁完一个功能,就在代理上把对应路径指到新服务,并保留指回老系统的能力(开关或配置)。每步都可独立验证、独立回退。
4. 下线老路径
当某一块所有路径都走新服务、且稳定运行一段时间后,删掉老代码与该路径的回退配置。注意:不要提前删,留观测窗口(建议 2–4 周)确认无残留调用。
四、一个真实可用的流量拦截示例
假设老系统跑在 127.0.0.1:8080,新服务跑在 127.0.0.1:9000。用 Nginx 反向代理做路由层,把 /api/notify/* 切到新服务,其余仍走老系统:
upstream legacy {
server 127.0.0.1:8080;
}
upstream notify_new {
server 127.0.0.1:9000;
}
server {
listen 80;
server_name api.example.com;
# 绞杀者入口:新服务先接管 /api/notify 这一"接缝"
location /api/notify/ {
proxy_pass http://notify_new;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 其余流量继续走老系统
location / {
proxy_pass http://legacy;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
回退只需把 /api/notify/ 的 proxy_pass 改回 http://legacy 并 nginx -s reload。如果你的反向代理还没配熟,可以先看这篇 Nginx 反向代理完整配置:负载均衡 + HTTPS,把 HTTPS 与超时也一并补齐。
五、用 CI 守住重构质量
渐进式迁移最怕”新服务悄悄和老系统行为不一致”。把契约测试与回归测试挂进 CI,每次提交都跑一遍新老对比。一个最小可用的 GitHub Actions 流水线:
name: strangler-ci
on:
push:
branches: [ main ]
jobs:
contract-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: 启动新服务
run: docker compose up -d notify_new
- name: 跑契约/回归测试(对比老系统基线)
run: go test ./test/contract -run TestNotify
- name: 失败则禁止合并
if: failure()
run: exit 1
CI 不只是跑测试,更是迁移的刹车:行为一漂移就拦在合并前。需要完整 CI/CD 搭建流程的,参考 GitHub Actions 实战:从零搭建 CI/CD 流水线;新服务本身建议用容器隔离,可照搬 Docker Compose 一键编排 的套路。
六、三个最容易翻车的坑
1. 共享数据库没切干净。新服务若还直连老库同一张表,等于没真正解耦。先加防腐层(ACL),再逐步迁表,最后切断直连。
2. 代理层成了新单点。路由层一旦挂了全部流量归零。务必给代理单独做高可用与监控,别和老系统共用一台机器。
3. 迁到一半就停工。绞杀者模式最怕”迁了三个功能后项目被叫停”,留下半新半旧的夹生系统。要么定死排期每周必须迁一个接缝,要么干脆别开始。
七、怎么判断你的系统该不该用
满足任意两条,就值得上绞杀者模式:① 系统已上线 3 年以上、无人敢改;② 有稳定且持续的线上流量、不能停;③ 业务逻辑复杂、找不到干净的重写边界;④ 团队还得并行接新需求。反之,若系统小、流量低、逻辑清晰,直接重写反而更省事。
八、迁移期间怎么确认”没搬歪”
渐进式最大的隐患是”行为悄悄漂移”:新服务返回 200,但字段顺序、错误码、空值处理与老系统微妙不同,等到全量切换才爆雷。应对办法是影子流量(Shadow Traffic):代理把一份请求同时发给新老两套,只认老系统的返回,但把新服务的响应落日志做差异比对。差异归零后再真正切流。
同时盯三个指标:① 新服务错误率是否低于老系统基线;② P99 延迟有无劣化;③ 关键业务埋点(如下单、注册)转化率是否持平。任一项异常,就退回该路径走老系统,别硬切。迁移不是”发完就完”,而是”连续观测两周无异常,才算真的迁完”。
绞杀者模式不是银弹,但它把”不可能的大重写”拆成了”每天都能交付的小步”。先放路由层、再切第一个接缝——你会在不动老系统的前提下,真把新系统跑起来。




