流量路由:不只是切流,是染色
绞杀者模式不是什么新鲜玩意儿,Martin Fowler十几年前就提了。但每次看到团队在重构时一刀切,我还是觉得——这模式被严重低估了。或者更糟,被用歪了。核心不是那个代理层,是流量的逐步接管策略。 很多人以为搞个nginx做个分流就算绞杀,错。大错特错。真正的绞杀,像装修老房子:你人还得住里面,先建新墙,再把水电气慢慢接到新墙,最后才拆旧墙。这个过程里,最关键的是——你凭什么决定哪盏灯先走新线路?
答案:染色。 每个进来的请求,得带上某种标记,决定它是被“绞”到新系统,还是继续苟活在旧系统。标记可以是用户ID哈希、地区、租户,甚至一个灰度流量百分比。但真正优雅的实现,不是if-else,而是一棵动态匹配树。我们内部管它叫路由引擎,说到底就是个带优先级的规则集合,从最具体的(比如“内测用户张三”)到最宽泛的(“10%随机流量”),逐条匹配。核心数据结构往往是一个前缀树或基数树,插入、查找、更新都在微秒级——否则规则一多,代理层自己先崩了。说实话,我第一次看到这个设计时,那种工程美感……就好像看钟表内部齿轮咬合,精确到你忍不住骂一句:这帮人脑子怎么长的?
路由引擎不是静态的。随着新系统功能模块的成熟,规则要能动态加载、灰度扩大,甚至在某些接口出错时自动回退。这本质上是一个分布式状态机,协调新旧两套系统的心跳。如果这个状态机设计得不好,所谓“绞杀”就会变成“绞杀自己”。
数据不会说谎:一次真实压测的启示
说再多原理,不如看一次惨痛的对比。去年我们有一个支付核心系统迁移,旧系统是个单块怪兽,跑在物理机上,QPS撑死500,再往上延迟就飙到3秒。团队最初想做一次性的“大爆炸”重写,方案预估需要冻结业务迭代6个月,还要一个8小时的停机窗口——对支付来说,8小时意味着什么?意味着老板想把你的头拧下来当球踢。
最后逼得没办法,走了绞杀者模式。我们用了一个季度,逐步把付款、退款、对账三个模块分别绞杀到新的微服务上。压测数据很有意思:全量切换方案在模拟演练中,停机时间确实花了6.5小时(这还是顺利的情况),而且上线后一周内冒出47个严重缺陷,大部分是数据不一致和接口兼容性问题。而绞杀者模式呢?历时12周,每周灰度增加10%流量,零停机,总共出现3个线上问题,全部在5分钟内回滚到旧系统,影响用户数不超过0.1%。再看吞吐:新系统刚开始时只有600QPS,但随着优化,到第三个月已经稳定在1200QPS,延迟中位数从旧系统的400ms降到120ms。不是新系统天生厉害,而是逐步切流给了我们优化的时间窗口,这是大爆炸重写永远做不到的——后者仿佛在黑暗中纵身一跃,前者则是摸着石头过河,每一脚都踩实了。
不过话说回来,数据好看不代表没代价。那个路由引擎的规则管理,差点把我们折腾死。
那些年踩过的坑,真金白银换来的
一、数据同步地狱很多人以为绞杀模式就是代理加两个服务,简单。天真。最难的是数据。旧系统用Oracle,新系统用MySQL,订单状态同步晚了2秒,用户就看见支付成功但订单没显示——投诉电话立马打过来。我们最初用双写,结果新系统挂了,旧系统还在写,数据就乱了。那段时间我每天做梦都是binlog。后来学乖了,用事务发件箱模式:旧系统在数据库事务里,把事件写入一个本地事件表,然后用Debezium CDC抓取变更发送到Kafka,新系统消费。最终一致性是保证了,但引入了一套消息基础设施,运维复杂度直接翻倍。所以要搞绞杀,先想清楚:你愿意为数据同步多付多少成本?
二、接口契约陷阱
我见过一个团队,以为接口兼容了,新旧系统的返回字段名、类型都一样——上线后60%的请求因为新系统对一个nullable字段返回了空字符串,而旧系统返回null,客户端直接崩溃。半夜两点爬起来修,那种痛苦,懂的人都懂。解决方案?必须上消费者驱动契约测试。用Pact这样的工具,让客户端定义期望的响应示例,每次改动都要通过契约验证。这个东西一开始觉得麻烦,但救过我们不止一次命。
三、监控盲区
流量切着切着,你发现突然搞不清一个报错到底来自新系统还是旧系统——因为链路追踪ID在代理层没保持一致。我们后来强行在路由引擎里注入统一的trace-id头,强制新旧系统都透传,再结合Jaeger做全链路。否则你就像瞎子在黑屋子里抓猫,抓到什么全靠运气。这三个坑,每个都是真金白银买回来的教训,希望你们别重蹈覆辙。
所以,下次再有人让你拿出重构方案时,想想绞杀者模式——但记住,别光看它的优雅,背后的沟壑足以让你摔个嘴啃泥。