在热带雨林里,绞杀榕的种子落到一棵老树的高处,先长出气根,气根沿着树干垂到地面,慢慢变粗成网。老树还活着,但它的养分、阳光和空间被一点点让渡给榕树。多年后,榕树长成新树,老树只剩躯壳。软件架构中的绞杀者模式正是如此:让新系统像榕藤一样,在老系统照常运行时,把功能和流量一点点替换过来。 很多团队一听“上微服务”,第一反应是推倒重来。但业务复杂、不能停机的系统,根本承受不起大爆炸式重构——那就像拿整座旧城换新区,任何意外都会把团队拖入绝境。绞杀者模式给出另一条路:新需求由新服务承接,老功能按依赖关系逐个迁移,入口用一层门面统一分流,数据用临时同步过渡,流量从小比例灰度放大,异常时能一键回滚。当老代码被逐步清空,迁移才算真正完成。
对架构师和中高级工程师来说,掌握绞杀者模式意味着可以在业务不停机的前提下偿还技术债,把高风险架构改造变成可回滚的小步推进,是系统演进中最实用的护身技能。
可视化演示
通过四阶段步进演示绞杀者模式,展示门面路由器如何逐步将流量从单体系统切换到微服务,实现低风险架构演进。
本节配有交互动效(进入视口后自动加载,约 3KB)。
绞杀者模式 (Strangler Pattern) 门面路由拦截并分发流量 新微服务逐步替换旧模块 单体模块被绞杀后虚线标记 迁移进度 = 已迁移模块数 / 总模块数 × 100% 最终单体系统安全退役
迁移起步
热带雨林里的绞杀榕不会先用雷劈倒宿主。它的种子落在高枝上,先长出气根,气根落地变粗,再慢慢把宿主树围成一张网。对老系统也一样,正确的第一步不是制定“把单体删掉”的计划,而是决定:下一个新功能,放到新的微服务里。
在软件架构里,那种所有模块挤在一个进程里、一次发布全带上线的老系统,就是宿主树;微服务则是从外围长出的新根。绞杀者模式要求旧系统继续提供服务,新需求、新页面尽量先落在新服务上。已经稳定运行的老功能,除非必须改动,否则不急着重写。这就是“新功能先行”。
真正落地时,要把业务切成一个个小方块(业务域),每个迭代只迁移一个方块。比如订单域独立成一个新服务,权限、消息、报表等边界先不动。每完成一块,整体流量占比就上升一点。这样,迁移不是一场革命,而是一系列可以随时停下来观察的小实验。
用榕树的话说,前几根气根最吃力。只要让第一个新服务真正承接线上流量,后面每一轮迁移都是在复制同样的节奏。
- 分子\(N_{\text{新服务承载请求}}\) 代表已经由新服务处理的请求数量。
- 分母\(N_{\text{所有请求}}\) 代表系统入口收到的全量请求数量。
- 整体占比两者相除再乘 \(100\%\),得到可观测的迁移进度百分比。
摸清家底
砍树前,先看清树冠怎么交错。榕藤若乱长,会把还健康的枝条压断。迁移老系统之前,最值钱的工作不是写代码,而是摸清家底。
把单体里的模块像枝条一样列出来:谁调用谁、谁访问哪张表、哪个接口被多少上游依赖。用依赖图把纠缠关系画出来,再按业务域和变更频率找稳定边界。越稳定的接口,越适合先切成契约;越混乱的地方,越要留在后面处理。
这段盘点通常产出两类清单:依赖清单和契约清单。依赖清单回答“切这个模块会牵动谁”,契约清单回答“新旧系统之间用什么协议衔接”。有了这两份清单,迁移顺序就从拍脑袋变成了排序题。
摸清家底之后,你知道哪根枝条可以先被绞杀,哪根还得再养一养。
- 求和符号\(\sum_{i=1}^{n}\) 表示遍历模块可能依赖的全部对象。
- 指示函数\(\mathbf{1}[\cdots]\) 表示“有依赖记 1,没有依赖记 0”。
- 耦合度\(C_m\) 越大,说明模块纠缠越深,切分前要准备更多过渡方案。
遮断入口
绞杀榕的气根不是直接扎进树心,而是先贴着树干织成一张网。对应在架构里,就是在老系统门前加一层 Strangler Facade(绞杀者门面)。它通常由反向代理或应用网关实现,所有客户端请求先进这个门,再由它决定放去老系统还是新服务。
门面要做三件事:统一鉴权、协议转换、路由。客户端不必知道新老地址,只要继续访问老接口;门面内部把属于新服务的请求转发过去,其余继续走老系统。这样,调用方完全感知不到迁移,改动被锁在入口一层。
刚开始,门面可能只是原样转发;随着新服务增加,门面里会多出越来越多路由规则。每一条规则都是藤蔓的一个“附着点”,让新服务开始分享原本属于老系统的流量。
门面像树前的交通警察。老系统不再是唯一出入口,新藤有了合法的生长通道。
- 流量占比\(p_i\) 表示第 \(i\) 类请求占总流量的比例。
- 路由权重\(w_i\) 表示这类请求被路由到新服务的比例,取值范围是 0 到 1。
- 累加求和把所有类别的乘积加起来,就得到新服务实际承接的总流量占比。
新服务建设
光有气根还只是草,每一根都必须能独立吸收养分。新服务建设也是这样:从老系统中抽出来的能力,要有独立代码仓、独立部署单元、独立上线节奏。
把业务能力以 API 形式开放,接口契约先定,内部实现后填。新服务不要偷偷读老系统的数据库,一切访问通过接口完成;如果暂时绕不开,也要把共享访问单独封装,等数据解耦完成后再收口。
理想情况下,每个新服务对应一个业务域,团队能够独立修改、独立发布。哪怕一开始只有一个接口、一张表,也要把它当成“亲生服务”养活,而不是老代码的临时壳子。
新服务上线后先看成功率,再谈放量,就像藤蔓先看看自己扎没扎稳。
- 总调用\(C_{\text{总调用}}\) 表示新服务收到的全部请求次数。
- 成功调用\(C_{\text{成功调用}}\) 表示其中正常返回的请求次数。
- 成功率两者相除再乘 \(100\%\),得到新服务的健康度。
数据解耦
最难的不是让新藤长出来,而是让它有自己的根。单体系统的数据通常集中在同一个数据库里,订单、支付、库存都搅在同一片土壤中。数据解耦不能靠一次大迁移把整个库搬走。
实战做法是“数据抽离 + 临时同步”。先把某个域的数据在逻辑上划归新服务,老系统继续读写旧表;新旧库之间用消息队列或定时任务同步,让两边保持最终一致。必要时允许短暂共享库,等新服务稳定后再把老系统的读路径切断。
关键认知是:从“强一致”切换到“最终一致”不是降级,而是微服务架构的默认选项。业务上可以接受几秒延迟的场景,先用同步机制对齐,不要为了强一致把新服务重新绑回老库。
等新根长实了,再剪断与老库之间的临时管道,藤蔓才算有了自己的土地。
- 写源\(t_{\text{写源}}\) 是业务操作写入老库或源库所花费的时间。
- 传输\(t_{\text{传输}}\) 是事件或数据经过消息通道的传输时间。
- 写目标\(t_{\text{写目标}}\) 是目标库落盘的时间。
流量切换
藤蔓不是一天遮满树冠的。它今天先伸出一根枝条,明天再伸第二根。流量切换就是:先从小流量开始,让少量真实请求走新服务,验证通过后再逐步放大。
灰度发布和权重调整是两种常见手段。灰度通常按用户、按地域分桶;权重调整则让入口按百分比把请求分给新老两端。实践中常把两者结合:先灰度 1% 的真实用户,再按 5%、20%、50%、100% 慢慢放量,每一步之间留足观察周期。
金丝雀验证说的是:先放一只体质敏感的小鸟进矿洞试毒。放到新服务的少量请求就是金丝雀,它们一旦失败,立刻叫停,而不是让整棵老树陪葬。
每步增量设上限,比一步到位更安全。藤蔓慢慢收紧,树冠才不会突然失血。
- 当前比例\(w_t\) 表示当前已经切到新服务的流量比例。
- 本轮增量\(\Delta_t\) 表示这一轮要增加的比例,控制在上限以内。
- 下一轮比例\(w_{t+1}\) 表示经过本轮放量后的新流量比例。
回滚兜底
绞杀榕也有长歪的时候。成熟的做法是每根藤都留一根“保险绳”:一旦风大,剪断藤蔓,宿主树还能继续活。回滚兜底,就是系统的保险绳。
在新旧切换时,门面上的开关和功能开关要能随时把流量切回老系统;老服务不能因为新服务上线就被销毁。接口层保持版本兼容,新老两个版本同时在线一段时间。每次放量前,先验证回滚动作本身是否可用。
监控告警是把控回滚时机的手电筒:看错误率、延迟、数据库连接数、同步延迟。指标一旦越界,值班人员点一键回退,而不是紧急修代码。
回滚不是失败,而是安全策略的一部分。藤蔓可以剪掉重长,老树不能先被砍倒。
- 已迁移模块\(M_{\text{已迁移}}\) 表示已经切到新服务的模块数量。
- 可回退模块\(M_{\text{可回退}}\) 表示其中带开关、能切回老系统的模块数量。
- 覆盖率两者相除得到 \(R_{\text{c}}\),越接近 1 说明兜底越完整。
收尾清理
当新树已经亭亭如盖,老树不再提供养分时,榕藤就可以把宿主树最后的残枝清理掉。收尾清理不是单纯技术动作,而是组织动作:把老模块从路由里摘掉、停止实例、删除老代码、清理依赖配置。
老模块下线不能按“代码行数”一刀切,而要按业务影响面分批下线。每下线一个模块,同步更新契约文档、监控面板和团队知识库,避免后来者对着幽灵代码猜历史。
团队切换也很关键:原来维护单体的团队,成为各个新服务的 owner,告警、排障、迭代都由新团队负责。最后做一次复盘归档,把迁移中的决策、坑位和指标记录成团队资产。
老树不是被砍倒,而是被一个更适应新环境的系统完整取代。这正是绞杀者模式的优雅之处。
- 指示函数\(\mathbf{1}[\cdots]\) 表示如果老模块 \(m\) 还在运行就记为 1,否则记为 0。
- 求和对所有老模块求和。
- 遗留债务\(B\) 是还在运行的旧模块总数,\(B\) 越小越好。
AI 应用场景
金融核心账务系统改造
阶段:架构演进与迁移规划
解决的问题:金融账务系统是典型高耦合单体,停机窗口极短、监管要求高,传统做法根本不敢动。AI 时代的新功能又要求快速上线,团队需要一条不停机的渐进改造路径。
具体案例:某支付平台把“用户积分查询”这一新需求直接交给新服务实现,老交易继续走单体。通过消息队列同步积分数据,使用门面网关灰度 5% 用户验证响应时间,一旦超时立即一键切回老系统。三周后新服务稳定,流量逐步放到 100%。
电商订单与库存数据解耦
阶段:数据链路重构
解决的问题:订单和库存数据在同一张库里,拆服务不拆库会形成“假微服务”,跨服务强事务又会让系统重新绑死。
具体案例:某电商平台拆分订单服务时,让新订单服务写自己的订单表,通过消息队列把订单事件广播给库存和积分服务。库存服务消费事件后更新库存,最终一致;大促期间同步延迟被控制在 2 秒以内,没有出现超卖。
遗留 CRM 系统有序下线
阶段:技术债治理与系统下线
解决的问题:老 CRM 被销售、客服、财务多个团队共用,牵一发动全身,长期无人敢改。
具体案例:团队在门面层把客户主数据服务逐步迁移到新平台,每季度只下线一个旧模块,并同步清理依赖和脚本。一年后老单体只剩登录壳,最后整体关停,业务零感知。
Python 演示
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())
self.legacy_weight = {m: 1.0 for m in self.modules}初始状态下所有模块都走老系统,这是迁移的起点。def migrate(self, module, ratio):把某个模块的目标新流量比例写进来,而不是立刻删除老系统。self.new_weight[module] = round(ratio, 2)将新服务权重保存下来,供后续路由判断。if random.random() < self.new_weight[module]:用随机数做加权路由,新权重越大,请求进入新服务的概率越高。self.call_count[module]["legacy"] += 1记录仍然走老系统的请求数,方便计算真实迁移比例。new_rate = self.call_count[module]["new"] / total if total else 0用统计结果计算实际占比,验证配置与真实流量是否一致。
常见误区
误区反正都要换成微服务,干脆把老代码全部重写一版,新旧系统二选一,这样最干净。
正解绞杀者的精髓是不让老树停止运行。新功能先由新服务交付,老能力按依赖关系逐步被替换,老系统一直在线,每一步都可回滚。全量重写等于把风险一次性放大,与绞杀者模式的目标完全相反。
为何常见:人们习惯二进制替换式的软件升级思维,误以为架构演进也必须一次性到位。
误区拆了服务就得马上拆库,否则不算真正的微服务。
正解代码拆分和数据拆分节奏可以不同。先用临时同步或事件同步保证两份数据最终一致,等服务稳定后再撤掉老库依赖。若一步拆库,订单、支付跨库事务会先压垮团队。
为何常见:微服务与数据库解耦的终态被当成了出发条件,忽略了路径设计。
误区新服务验证通过后,老模块放着就是浪费,应该马上删掉。
正解老模块是回滚的安全网。应保留一段观察窗口,等监控全部正常、流量全部切走后才下线;删除要分模块分批做,并同步清理依赖和配置。
为何常见:清理技术债的成就感让人追求删除的快感,忽略了兜底时间窗。
误区只要加了网关和路由,老系统就能自动被新服务替换。
正解网关只是入口遮断,真正的替换还取决于数据、依赖和团队运维能力。调用方、异步消息、定时任务、权限配置都要纳入绞杀范围,否则漏网业务仍会写老库。
为何常见:入口处的可视效果最强,容易让人误以为核心难题已经解决。
面试题
Q1. 绞杀者模式的核心思路是什么?
- A.一次性重写全部老代码
- B.用新服务渐进替换旧能力← 正确答案
- C.只升级数据库不碰应用
- D.把单体变成多个进程即可
答案:B。绞杀者模式不追求重写,而是让新服务像榕藤一样逐步把老能力替换掉。A 是大爆炸式重构,C 过于片面,D 只改变部署方式,都不符合渐进替换的精髓。
Q2. 开始迁移前,团队最应该先做哪件事?
- A.立刻把流量切到新服务
- B.梳理依赖并确认契约← 正确答案
- C.删除所有旧系统的代码
- D.一次性拆分所有数据库
答案:B。摸清家底是迁移的前提。B 能让你知道切分影响面;A、C、D 都跳过了风险分析,等于先开枪再画靶,失败概率极高。
Q3. Strangler Facade 在绞杀者模式中的核心作用是什么?
- A.统一接收请求并路由到新旧系统← 正确答案
- B.替代老系统的全部数据库事务
- C.自动生成微服务的所有代码
- D.让新服务只能同步调用老系统
答案:A。门面相当于树前的交通警察,统一收口后再分流。它不能解决事务、代码生成等问题,所以 B、C 错误;D 把路由机制理解成强制同步调用,不符合解耦目标。
Q4. 迁移过程中上线一个新功能,最合适的做法是什么?
- A.等老功能全部迁移完再开发
- B.新功能直接由新服务承接← 正确答案
- C.新功能同时写入所有数据库
- D.关闭新功能等待领导发话
答案:B。新功能先行是绞杀者模式的关键策略,让新服务在真实需求中生长。等全部迁移完等于大爆炸,C 会造成数据重复,D 没有落地价值。
Q5. 用权重调整切换流量时,正确步骤是?
- A.一次切到百分之一百
- B.从小比例灰度逐步放大← 正确答案
- C.只在运维窗口批量切换
- D.用测试环境模拟流量即可
答案:B。流量切换要像藤蔓逐渐覆盖树冠,先灰度小比例、验证稳定再加量。A、C 风险集中;D 只做模拟,没有验证真实线上依赖。
Q6. 新旧数据库短暂共存时,合理的过渡措施包括?
- A.用改库工具直接停用老库
- B.保留临时双向同步通道← 正确答案
- C.通过事件消息达到最终一致← 正确答案
- D.禁止任何跨服务的远程调用
答案:BC。过渡期用同步通道和事件消息让两边最终一致,是实用做法。A 直接停库风险大,D 禁止远程调用不现实,所以正确答案是 B、C。
Q7. 下面哪项是回滚兜底的必要条件?
- A.新旧接口完全互相不兼容
- B.门面有开关且老系统仍存活← 正确答案
- C.数据库已经完成最终拆分
- D.老代码已经全部删除完毕
答案:B。回滚的前提是还能回到老路,门面开关控制流量、老系统继续运行,才能一键切回。A、D 与回滚相反;C 拆完库反而更难回滚。
Q8. 判断老模块可以下线的核心依据是什么?
- A.新服务已经稳定运行足够久← 正确答案
- B.老代码看着不碍眼
- C.团队想换新框架
- D.对外接口全部关闭
答案:A。下线不是看代码长相,而是看新服务经过足够长稳定时间、监控指标正常,同时流量已全部切走。B、C 是主观冲动,D 只是结果之一。
Q9. 绞杀者模式中,数据解耦的常用做法包括?
- A.保留双向同步的过渡通道← 正确答案
- B.用跨服务强事务保证所有操作
- C.通过事件驱动达到最终一致← 正确答案
- D.把全量数据在一天内搬完
答案:AC。过渡通道和事件驱动能让新旧库在过渡期对齐,是实用方案。跨服务强事务会把微服务重新绑成单体,D 一步搬库风险过高,所以选 A、C。
Q10. 关于绞杀者模式适用的边界条件,下列说法正确的是?
- A.适合高风险遗留系统的平滑演进← 正确答案
- B.可以完全避免分布式复杂性
- C.需要治理数据一致与监控告警← 正确答案
- D.仅适用于从零开始的新项目
答案:AC。绞杀者模式解决遗留系统演进,同时必须面对数据一致性、可观测性、回滚等分布式复杂度,所以 A、C 正确;B 说“完全避免”过于绝对,D 与适用场景相反。