在热带雨林里,绞杀榕的种子落到一棵老树的高处,先长出气根,气根沿着树干垂到地面,慢慢变粗成网。老树还活着,但它的养分、阳光和空间被一点点让渡给榕树。多年后,榕树长成新树,老树只剩躯壳。软件架构中的绞杀者模式正是如此:让新系统像榕藤一样,在老系统照常运行时,把功能和流量一点点替换过来。 很多团队一听“上微服务”,第一反应是推倒重来。但业务复杂、不能停机的系统,根本承受不起大爆炸式重构——那就像拿整座旧城换新区,任何意外都会把团队拖入绝境。绞杀者模式给出另一条路:新需求由新服务承接,老功能按依赖关系逐个迁移,入口用一层门面统一分流,数据用临时同步过渡,流量从小比例灰度放大,异常时能一键回滚。当老代码被逐步清空,迁移才算真正完成。

岗位关联

对架构师和中高级工程师来说,掌握绞杀者模式意味着可以在业务不停机的前提下偿还技术债,把高风险架构改造变成可回滚的小步推进,是系统演进中最实用的护身技能。

目录
  1. 可视化演示
  2. 迁移起步
  3. 摸清家底
  4. 遮断入口
  5. 新服务建设
  6. 数据解耦
  7. 流量切换
  8. 回滚兜底
  9. 收尾清理
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

通过四阶段步进演示绞杀者模式,展示门面路由器如何逐步将流量从单体系统切换到微服务,实现低风险架构演进。

本节配有交互动效(进入视口后自动加载,约 3KB)。

绞杀者模式 (Strangler Pattern) 门面路由拦截并分发流量 新微服务逐步替换旧模块 单体模块被绞杀后虚线标记 迁移进度 = 已迁移模块数 / 总模块数 × 100% 最终单体系统安全退役

迁移起步

热带雨林里的绞杀榕不会先用雷劈倒宿主。它的种子落在高枝上,先长出气根,气根落地变粗,再慢慢把宿主树围成一张网。对老系统也一样,正确的第一步不是制定“把单体删掉”的计划,而是决定:下一个新功能,放到新的微服务里。

在软件架构里,那种所有模块挤在一个进程里、一次发布全带上线的老系统,就是宿主树;微服务则是从外围长出的新根。绞杀者模式要求旧系统继续提供服务,新需求、新页面尽量先落在新服务上。已经稳定运行的老功能,除非必须改动,否则不急着重写。这就是“新功能先行”。

真正落地时,要把业务切成一个个小方块(业务域),每个迭代只迁移一个方块。比如订单域独立成一个新服务,权限、消息、报表等边界先不动。每完成一块,整体流量占比就上升一点。这样,迁移不是一场革命,而是一系列可以随时停下来观察的小实验。

用榕树的话说,前几根气根最吃力。只要让第一个新服务真正承接线上流量,后面每一轮迁移都是在复制同样的节奏。

迁移进度=N新服务承载请求N所有请求×100%\text{迁移进度} = \dfrac{N_{\text{新服务承载请求}}}{N_{\text{所有请求}}} \times 100\%
迁移进度公式的核心是:用真实流量的占比来度量重构完成度,而不是看代码改了多少行。
  1. 分子\(N_{\text{新服务承载请求}}\) 代表已经由新服务处理的请求数量。
    N新服务承载请求N_{\text{新服务承载请求}}
  2. 分母\(N_{\text{所有请求}}\) 代表系统入口收到的全量请求数量。
    N所有请求N_{\text{所有请求}}
  3. 整体占比两者相除再乘 \(100\%\),得到可观测的迁移进度百分比。
    N新服务承载请求N所有请求×100%\dfrac{N_{\text{新服务承载请求}}}{N_{\text{所有请求}}} \times 100\%

摸清家底

砍树前,先看清树冠怎么交错。榕藤若乱长,会把还健康的枝条压断。迁移老系统之前,最值钱的工作不是写代码,而是摸清家底。

把单体里的模块像枝条一样列出来:谁调用谁、谁访问哪张表、哪个接口被多少上游依赖。用依赖图把纠缠关系画出来,再按业务域和变更频率找稳定边界。越稳定的接口,越适合先切成契约;越混乱的地方,越要留在后面处理。

这段盘点通常产出两类清单:依赖清单和契约清单。依赖清单回答“切这个模块会牵动谁”,契约清单回答“新旧系统之间用什么协议衔接”。有了这两份清单,迁移顺序就从拍脑袋变成了排序题。

摸清家底之后,你知道哪根枝条可以先被绞杀,哪根还得再养一养。

Cm=∑i=1n1[模块 m 依赖 i]C_m = \sum_{i=1}^{n} \mathbf{1}[\text{模块 } m \text{ 依赖 } i]
这个式子计算一个模块的耦合度:每依赖一个外部对象就加 1,数值越高说明这个模块越难被单独绞杀。
  1. 求和符号\(\sum_{i=1}^{n}\) 表示遍历模块可能依赖的全部对象。
    ∑i=1n\sum_{i=1}^{n}
  2. 指示函数\(\mathbf{1}[\cdots]\) 表示“有依赖记 1,没有依赖记 0”。
    1[模块 m 依赖 i]\mathbf{1}[\text{模块 } m \text{ 依赖 } i]
  3. 耦合度\(C_m\) 越大,说明模块纠缠越深,切分前要准备更多过渡方案。
    Cm=∑i=1n1[模块 m 依赖 i]C_m = \sum_{i=1}^{n} \mathbf{1}[\text{模块 } m \text{ 依赖 } i]

遮断入口

绞杀榕的气根不是直接扎进树心,而是先贴着树干织成一张网。对应在架构里,就是在老系统门前加一层 Strangler Facade(绞杀者门面)。它通常由反向代理或应用网关实现,所有客户端请求先进这个门,再由它决定放去老系统还是新服务。

门面要做三件事:统一鉴权、协议转换、路由。客户端不必知道新老地址,只要继续访问老接口;门面内部把属于新服务的请求转发过去,其余继续走老系统。这样,调用方完全感知不到迁移,改动被锁在入口一层。

刚开始,门面可能只是原样转发;随着新服务增加,门面里会多出越来越多路由规则。每一条规则都是藤蔓的一个“附着点”,让新服务开始分享原本属于老系统的流量。

门面像树前的交通警察。老系统不再是唯一出入口,新藤有了合法的生长通道。

T新=∑i=1nwi⋅piT_{\text{新}} = \sum_{i=1}^{n} w_i \cdot p_i
新系统拿到的总流量占比,等于每一类请求的流量占比乘以它被路由到新服务的权重,最后全部加起来。
  1. 流量占比\(p_i\) 表示第 \(i\) 类请求占总流量的比例。
    pip_i
  2. 路由权重\(w_i\) 表示这类请求被路由到新服务的比例,取值范围是 0 到 1。
    wiw_i
  3. 累加求和把所有类别的乘积加起来,就得到新服务实际承接的总流量占比。
    T新=∑i=1nwi⋅piT_{\text{新}} = \sum_{i=1}^{n} w_i \cdot p_i

新服务建设

光有气根还只是草,每一根都必须能独立吸收养分。新服务建设也是这样:从老系统中抽出来的能力,要有独立代码仓、独立部署单元、独立上线节奏。

把业务能力以 API 形式开放,接口契约先定,内部实现后填。新服务不要偷偷读老系统的数据库,一切访问通过接口完成;如果暂时绕不开,也要把共享访问单独封装,等数据解耦完成后再收口。

理想情况下,每个新服务对应一个业务域,团队能够独立修改、独立发布。哪怕一开始只有一个接口、一张表,也要把它当成“亲生服务”养活,而不是老代码的临时壳子。

新服务上线后先看成功率,再谈放量,就像藤蔓先看看自己扎没扎稳。

成功率=C成功调用C总调用×100%\text{成功率} = \frac{C_{\text{成功调用}}}{C_{\text{总调用}}} \times 100\%
成功率是新服务能否承受流量的基础指标:成功调用数占总调用数的比例,低于阈值就不能继续放量。
  1. 总调用\(C_{\text{总调用}}\) 表示新服务收到的全部请求次数。
    C总调用C_{\text{总调用}}
  2. 成功调用\(C_{\text{成功调用}}\) 表示其中正常返回的请求次数。
    C成功调用C_{\text{成功调用}}
  3. 成功率两者相除再乘 \(100\%\),得到新服务的健康度。
    C成功调用C总调用×100%\frac{C_{\text{成功调用}}}{C_{\text{总调用}}} \times 100\%

数据解耦

最难的不是让新藤长出来,而是让它有自己的根。单体系统的数据通常集中在同一个数据库里,订单、支付、库存都搅在同一片土壤中。数据解耦不能靠一次大迁移把整个库搬走。

实战做法是“数据抽离 + 临时同步”。先把某个域的数据在逻辑上划归新服务,老系统继续读写旧表;新旧库之间用消息队列或定时任务同步,让两边保持最终一致。必要时允许短暂共享库,等新服务稳定后再把老系统的读路径切断。

关键认知是:从“强一致”切换到“最终一致”不是降级,而是微服务架构的默认选项。业务上可以接受几秒延迟的场景,先用同步机制对齐,不要为了强一致把新服务重新绑回老库。

等新根长实了,再剪断与老库之间的临时管道,藤蔓才算有了自己的土地。

t同步延迟=t写源+t传输+t写目标t_{\text{同步延迟}} = t_{\text{写源}} + t_{\text{传输}} + t_{\text{写目标}}
数据同步延迟由三段时间组成:写入源库的时间、消息传输的时间、写入目标库的时间。延迟越小,两边数据越接近实时一致。
  1. 写源\(t_{\text{写源}}\) 是业务操作写入老库或源库所花费的时间。
    t写源t_{\text{写源}}
  2. 传输\(t_{\text{传输}}\) 是事件或数据经过消息通道的传输时间。
    t传输t_{\text{传输}}
  3. 写目标\(t_{\text{写目标}}\) 是目标库落盘的时间。
    t写目标t_{\text{写目标}}

流量切换

藤蔓不是一天遮满树冠的。它今天先伸出一根枝条,明天再伸第二根。流量切换就是:先从小流量开始,让少量真实请求走新服务,验证通过后再逐步放大。

灰度发布和权重调整是两种常见手段。灰度通常按用户、按地域分桶;权重调整则让入口按百分比把请求分给新老两端。实践中常把两者结合:先灰度 1% 的真实用户,再按 5%、20%、50%、100% 慢慢放量,每一步之间留足观察周期。

金丝雀验证说的是:先放一只体质敏感的小鸟进矿洞试毒。放到新服务的少量请求就是金丝雀,它们一旦失败,立刻叫停,而不是让整棵老树陪葬。

每步增量设上限,比一步到位更安全。藤蔓慢慢收紧,树冠才不会突然失血。

wt+1=wt+Δt,Δt≤10%w_{t+1} = w_t + \Delta_t, \quad \Delta_t \le 10\%
流量放大要设增量上限:下一轮新流量比例等于当前比例加上一个本轮增量,增量一般不超过 10 个百分点。
  1. 当前比例\(w_t\) 表示当前已经切到新服务的流量比例。
    wtw_t
  2. 本轮增量\(\Delta_t\) 表示这一轮要增加的比例,控制在上限以内。
    Δt≤10%\Delta_t \le 10\%
  3. 下一轮比例\(w_{t+1}\) 表示经过本轮放量后的新流量比例。
    wt+1=wt+Δtw_{t+1} = w_t + \Delta_t

回滚兜底

绞杀榕也有长歪的时候。成熟的做法是每根藤都留一根“保险绳”:一旦风大,剪断藤蔓,宿主树还能继续活。回滚兜底,就是系统的保险绳。

在新旧切换时,门面上的开关和功能开关要能随时把流量切回老系统;老服务不能因为新服务上线就被销毁。接口层保持版本兼容,新老两个版本同时在线一段时间。每次放量前,先验证回滚动作本身是否可用。

监控告警是把控回滚时机的手电筒:看错误率、延迟、数据库连接数、同步延迟。指标一旦越界,值班人员点一键回退,而不是紧急修代码。

回滚不是失败,而是安全策略的一部分。藤蔓可以剪掉重长,老树不能先被砍倒。

Rc=M可回退M已迁移R_{\text{c}} = \frac{M_{\text{可回退}}}{M_{\text{已迁移}}}
回滚覆盖率等于所有已迁移模块中可回退模块的比例。理想状态下这个值应该等于 1,也就是每个迁移都留了后路。
  1. 已迁移模块\(M_{\text{已迁移}}\) 表示已经切到新服务的模块数量。
    M已迁移M_{\text{已迁移}}
  2. 可回退模块\(M_{\text{可回退}}\) 表示其中带开关、能切回老系统的模块数量。
    M可回退M_{\text{可回退}}
  3. 覆盖率两者相除得到 \(R_{\text{c}}\),越接近 1 说明兜底越完整。
    Rc=M可回退M已迁移R_{\text{c}} = \frac{M_{\text{可回退}}}{M_{\text{已迁移}}}

收尾清理

当新树已经亭亭如盖,老树不再提供养分时,榕藤就可以把宿主树最后的残枝清理掉。收尾清理不是单纯技术动作,而是组织动作:把老模块从路由里摘掉、停止实例、删除老代码、清理依赖配置。

老模块下线不能按“代码行数”一刀切,而要按业务影响面分批下线。每下线一个模块,同步更新契约文档、监控面板和团队知识库,避免后来者对着幽灵代码猜历史。

团队切换也很关键:原来维护单体的团队,成为各个新服务的 owner,告警、排障、迭代都由新团队负责。最后做一次复盘归档,把迁移中的决策、坑位和指标记录成团队资产。

老树不是被砍倒,而是被一个更适应新环境的系统完整取代。这正是绞杀者模式的优雅之处。

B=∑m1[老模块 m 仍在运行]B = \sum_{m} \mathbf{1}[\text{老模块 } m \text{ 仍在运行}]
遗留债务指标统计仍然在运行的老模块个数。随着迁移推进,这个数字应该不断变小并趋近于 0。
  1. 指示函数\(\mathbf{1}[\cdots]\) 表示如果老模块 \(m\) 还在运行就记为 1,否则记为 0。
    1[老模块 m 仍在运行]\mathbf{1}[\text{老模块 } m \text{ 仍在运行}]
  2. 求和对所有老模块求和。
    ∑m\sum_{m}
  3. 遗留债务\(B\) 是还在运行的旧模块总数,\(B\) 越小越好。
    B=∑m1[老模块 m 仍在运行]B = \sum_{m} \mathbf{1}[\text{老模块 } m \text{ 仍在运行}]

AI 应用场景

金融核心账务系统改造

阶段:架构演进与迁移规划

解决的问题:金融账务系统是典型高耦合单体,停机窗口极短、监管要求高,传统做法根本不敢动。AI 时代的新功能又要求快速上线,团队需要一条不停机的渐进改造路径。

具体案例:某支付平台把“用户积分查询”这一新需求直接交给新服务实现,老交易继续走单体。通过消息队列同步积分数据,使用门面网关灰度 5% 用户验证响应时间,一旦超时立即一键切回老系统。三周后新服务稳定,流量逐步放到 100%。

电商订单与库存数据解耦

阶段:数据链路重构

解决的问题:订单和库存数据在同一张库里,拆服务不拆库会形成“假微服务”,跨服务强事务又会让系统重新绑死。

具体案例:某电商平台拆分订单服务时,让新订单服务写自己的订单表,通过消息队列把订单事件广播给库存和积分服务。库存服务消费事件后更新库存,最终一致;大促期间同步延迟被控制在 2 秒以内,没有出现超卖。

遗留 CRM 系统有序下线

阶段:技术债治理与系统下线

解决的问题:老 CRM 被销售、客服、财务多个团队共用,牵一发动全身,长期无人敢改。

具体案例:团队在门面层把客户主数据服务逐步迁移到新平台,每季度只下线一个旧模块,并同步清理依赖和脚本。一年后老单体只剩登录壳,最后整体关停,业务零感知。

Python 演示

python43 行
import random class StranglerMigration:    def __init__(self):        self.modules = ["用户", "订单", "支付", "库存"]        self.legacy_weight = {m: 1.0 for m in self.modules}        self.new_weight = {m: 0.0 for m in self.modules}        self.call_count = {m: {"legacy": 0, "new": 0} for m in self.modules}     def migrate(self, module, ratio):        ratio = max(0.0, min(1.0, ratio))        self.legacy_weight[module] = round(1.0 - ratio, 2)        self.new_weight[module] = round(ratio, 2)     def route(self, module):        if random.random() < self.new_weight[module]:            self.call_count[module]["new"] += 1            return "新服务"        self.call_count[module]["legacy"] += 1        return "老系统"     def simulate(self, rounds=2000):        for _ in range(rounds):            for module in self.modules:                self.route(module)        return self.summary()     def summary(self):        lines = []        for module in self.modules:            total = sum(self.call_count[module].values())            new_rate = self.call_count[module]["new"] / total if total else 0            lines.append("%s 新流量占比: %.1f%%" % (module, new_rate * 100))        return "\n".join(lines)  if __name__ == "__main__":    sm = StranglerMigration()    sm.migrate("用户", 0.8)    sm.migrate("订单", 0.5)    sm.migrate("支付", 0.2)    sm.migrate("库存", 0.0)    print(sm.simulate())
目的:用流量权重模拟绞杀者模式的渐进迁移,观察不同模块在新老系统之间的流量分配结果。
  1. self.legacy_weight = {m: 1.0 for m in self.modules}初始状态下所有模块都走老系统,这是迁移的起点。
  2. def migrate(self, module, ratio):把某个模块的目标新流量比例写进来,而不是立刻删除老系统。
  3. self.new_weight[module] = round(ratio, 2)将新服务权重保存下来,供后续路由判断。
  4. if random.random() < self.new_weight[module]:用随机数做加权路由,新权重越大,请求进入新服务的概率越高。
  5. self.call_count[module]["legacy"] += 1记录仍然走老系统的请求数,方便计算真实迁移比例。
  6. new_rate = self.call_count[module]["new"] / total if total else 0用统计结果计算实际占比,验证配置与真实流量是否一致。

常见误区

误区反正都要换成微服务,干脆把老代码全部重写一版,新旧系统二选一,这样最干净。

正解绞杀者的精髓是不让老树停止运行。新功能先由新服务交付,老能力按依赖关系逐步被替换,老系统一直在线,每一步都可回滚。全量重写等于把风险一次性放大,与绞杀者模式的目标完全相反。

为何常见:人们习惯二进制替换式的软件升级思维,误以为架构演进也必须一次性到位。

误区拆了服务就得马上拆库,否则不算真正的微服务。

正解代码拆分和数据拆分节奏可以不同。先用临时同步或事件同步保证两份数据最终一致,等服务稳定后再撤掉老库依赖。若一步拆库,订单、支付跨库事务会先压垮团队。

为何常见:微服务与数据库解耦的终态被当成了出发条件,忽略了路径设计。

误区新服务验证通过后,老模块放着就是浪费,应该马上删掉。

正解老模块是回滚的安全网。应保留一段观察窗口,等监控全部正常、流量全部切走后才下线;删除要分模块分批做,并同步清理依赖和配置。

为何常见:清理技术债的成就感让人追求删除的快感,忽略了兜底时间窗。

误区只要加了网关和路由,老系统就能自动被新服务替换。

正解网关只是入口遮断,真正的替换还取决于数据、依赖和团队运维能力。调用方、异步消息、定时任务、权限配置都要纳入绞杀范围,否则漏网业务仍会写老库。

为何常见:入口处的可视效果最强,容易让人误以为核心难题已经解决。

面试题

Q1. 绞杀者模式的核心思路是什么?

单选题 · easy

  • A.一次性重写全部老代码
  • B.用新服务渐进替换旧能力← 正确答案
  • C.只升级数据库不碰应用
  • D.把单体变成多个进程即可

答案:B。绞杀者模式不追求重写,而是让新服务像榕藤一样逐步把老能力替换掉。A 是大爆炸式重构,C 过于片面,D 只改变部署方式,都不符合渐进替换的精髓。

Q2. 开始迁移前,团队最应该先做哪件事?

单选题 · easy

  • A.立刻把流量切到新服务
  • B.梳理依赖并确认契约← 正确答案
  • C.删除所有旧系统的代码
  • D.一次性拆分所有数据库

答案:B。摸清家底是迁移的前提。B 能让你知道切分影响面;A、C、D 都跳过了风险分析,等于先开枪再画靶,失败概率极高。

Q3. Strangler Facade 在绞杀者模式中的核心作用是什么?

单选题 · mid

  • A.统一接收请求并路由到新旧系统← 正确答案
  • B.替代老系统的全部数据库事务
  • C.自动生成微服务的所有代码
  • D.让新服务只能同步调用老系统

答案:A。门面相当于树前的交通警察,统一收口后再分流。它不能解决事务、代码生成等问题,所以 B、C 错误;D 把路由机制理解成强制同步调用,不符合解耦目标。

Q4. 迁移过程中上线一个新功能,最合适的做法是什么?

单选题 · mid

  • A.等老功能全部迁移完再开发
  • B.新功能直接由新服务承接← 正确答案
  • C.新功能同时写入所有数据库
  • D.关闭新功能等待领导发话

答案:B。新功能先行是绞杀者模式的关键策略,让新服务在真实需求中生长。等全部迁移完等于大爆炸,C 会造成数据重复,D 没有落地价值。

Q5. 用权重调整切换流量时,正确步骤是?

单选题 · mid

  • A.一次切到百分之一百
  • B.从小比例灰度逐步放大← 正确答案
  • C.只在运维窗口批量切换
  • D.用测试环境模拟流量即可

答案:B。流量切换要像藤蔓逐渐覆盖树冠,先灰度小比例、验证稳定再加量。A、C 风险集中;D 只做模拟,没有验证真实线上依赖。

Q6. 新旧数据库短暂共存时,合理的过渡措施包括?

多选题 · mid

  • A.用改库工具直接停用老库
  • B.保留临时双向同步通道← 正确答案
  • C.通过事件消息达到最终一致← 正确答案
  • D.禁止任何跨服务的远程调用

答案:BC。过渡期用同步通道和事件消息让两边最终一致,是实用做法。A 直接停库风险大,D 禁止远程调用不现实,所以正确答案是 B、C。

Q7. 下面哪项是回滚兜底的必要条件?

单选题 · hard

  • A.新旧接口完全互相不兼容
  • B.门面有开关且老系统仍存活← 正确答案
  • C.数据库已经完成最终拆分
  • D.老代码已经全部删除完毕

答案:B。回滚的前提是还能回到老路,门面开关控制流量、老系统继续运行,才能一键切回。A、D 与回滚相反;C 拆完库反而更难回滚。

Q8. 判断老模块可以下线的核心依据是什么?

单选题 · hard

  • A.新服务已经稳定运行足够久← 正确答案
  • B.老代码看着不碍眼
  • C.团队想换新框架
  • D.对外接口全部关闭

答案:A。下线不是看代码长相,而是看新服务经过足够长稳定时间、监控指标正常,同时流量已全部切走。B、C 是主观冲动,D 只是结果之一。

Q9. 绞杀者模式中,数据解耦的常用做法包括?

多选题 · hard

  • A.保留双向同步的过渡通道← 正确答案
  • B.用跨服务强事务保证所有操作
  • C.通过事件驱动达到最终一致← 正确答案
  • D.把全量数据在一天内搬完

答案:AC。过渡通道和事件驱动能让新旧库在过渡期对齐,是实用方案。跨服务强事务会把微服务重新绑成单体,D 一步搬库风险过高,所以选 A、C。

Q10. 关于绞杀者模式适用的边界条件,下列说法正确的是?

多选题 · extreme

  • A.适合高风险遗留系统的平滑演进← 正确答案
  • B.可以完全避免分布式复杂性
  • C.需要治理数据一致与监控告警← 正确答案
  • D.仅适用于从零开始的新项目

答案:AC。绞杀者模式解决遗留系统演进,同时必须面对数据一致性、可观测性、回滚等分布式复杂度,所以 A、C 正确;B 说“完全避免”过于绝对,D 与适用场景相反。