绞杀者模式实战:遗留系统渐进式重构不宕机

老系统越堆越乱,却又”不敢动、不能停”——这是每个技术团队迟早会遇到的死局。绞杀者模式(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://legacynginx -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 延迟有无劣化;③ 关键业务埋点(如下单、注册)转化率是否持平。任一项异常,就退回该路径走老系统,别硬切。迁移不是”发完就完”,而是”连续观测两周无异常,才算真的迁完”。

绞杀者模式不是银弹,但它把”不可能的大重写”拆成了”每天都能交付的小步”。先放路由层、再切第一个接缝——你会在不动老系统的前提下,真把新系统跑起来。

上一篇 限流误杀线上故障:Nginx 与网关限流陷阱根治
下一篇 HTTP/3 与 QUIC 实战:网站提速部署指南