先说结论:14个月,重建一个运行了八年的老平台,同时把AI能力从零推到生产环境,时间是够的,但前提是不能把两件事当成两个阶段来排。团队内部给这个项目起的代号是“Meta重建计划”,取意Metamorphosis——不是修修补补,而是让系统的组织形态彻底变一次。如果只让我用一句话概括这次经验,那就是:重建基础的过程,恰恰是推进前沿最好的抓手;前沿业务的需求,反过来会逼着基础架构做出真正有价值的改变。
这篇文章里的“我”不是一个旁观者,而是全程参与了目标拆解、链路梳理、切换决策和踩坑复盘的人。下面所有的时间线、指标、阶段设置,都来自可追溯的项目记录,不是事后拍脑袋编出来自我安慰的。如果你现在正负责一个老平台的现代化改造,或者正处于“既要保证业务稳定又要搞创新”的双重压力下,这篇文章值得你慢慢看。我会把目标设定、阶段排序、数据迁移、链路切换、前沿落地和具体踩坑全部展开,尽量做到可以直接参考。
1. 为什么老平台必须先“断臂重生”:病灶清单比路线图更重要
1.1 一个运行八年平台的核心病灶
我们面对的平台,核心服务两个,加起来120万行代码,表面上跑得还行,但每一次发版都像拆炸弹。部署一套新环境平均要40分钟,回滚主要靠人肉操作;数据库当年为了扛流量按业务后缀做了分库分表,结果查询经常要拼表名;更麻烦的是核心链路上有三十多个定时任务在互相补偿,谁都不敢轻易改动。
真正的转折点是某个周五下午,一次看似普通的配置变更引发了全链路雪崩,从发现问题到流量恢复用了两个多小时。业务方的原话是:“你们再这么玩,我们就换体系。”这句话很难听,但点醒了我:老平台的问题不是某一个代码写错了,而是整个系统的组织方式已经撑不住现在的业务形态了。
具体来说,我们当时面临几个核心病灶:
- 发布频率极低:核心服务一个月只敢发一次版,因为每次发版都要协调四五个团队,发布窗口内任何异常都可能演变成几小时的故障。
- 数据边界混乱:订单、用户、库存、结算的数据散落在多个库里,表与表之间靠定时任务同步,跨域查询经常要绕好几个弯。
- 新能力无处安放:AI模型需要的实时特征数据根本拿不到,老系统只支持T+1的离线批量计算,业务方想要“实时推荐”和“实时风控”基本无从谈起。
- 过程不可观测:调用链只有日志级别的串联,没有统一的Trace体系,排查一个问题平均要翻四五个系统。
这些病灶单独拎出来都不算致命,但叠加在一起,就构成了一个“动哪里都会出事”的僵局。我们当时做了一个决定:不追求推倒重来,而是先用一张清单把“坏”排序,再决定14个月怎么花。
1.2 “坏系统清单”的排序逻辑
很多团队一谈重建就画目标架构图,恨不得全部换成最新的框架、最新的中间件。我的建议恰恰相反:先不要谈架构,先回答一个问题——你现在最痛的是什么?
我们当时把所有问题分成三类:
- 影响发布频率的坏:发版慢、回滚难、环境准备时间长。
- 影响数据正确性的坏:跨系统补偿逻辑多、对账靠人工、数据口径不统一。
- 影响新能力上线的坏:无法动态配置、不能灰度、实时特征数据缺失。
排序之后,我们把14个月的资源做了一个粗分配:60%用于解决前两类问题,30%用于建立新的基础设施,10%留给前沿探索。这个分配和很多人的直觉相反,因为看起来“重建基础”占了绝大部分精力,但事实上,如果没有前两类问题的解决,后面所谓的基础设施也只是空中楼阁。
为了把“坏系统清单”变成可执行的目标,我们给每一项都定了量化指标。这里可以分享一张当时的基线表:
| 指标 | 改造前 | 目标 | 实际结果 |
|---|---|---|---|
| 核心服务发布频率 | 每月1次 | 每周3次 | 每周4次 |
| 发布准备时长 | 40分钟 | 10分钟 | 8分钟 |
| 线上故障MTTR | 78分钟 | 15分钟 | 13分钟 |
| 实时特征数据延迟 | T+1 | 秒级 | 秒级 |
| 核心链路依赖数量 | 32个定时任务 | 少一半 | 降到8个 |
写下这些数字不是为了好看,而是为了让整个团队有共同的靶心。尤其是后面做切换时,每次灰度能不能继续,不用吵,直接把指标拉出来看就行。这是我这次项目里觉得最值钱的第一步:先定义“什么算好”,再开始动手。
2. 14个月的时间线不是直线:前一阶段的目标是让下一阶段更好做
2.1 0-3月:只诊断、不迁移,把“不知道”变成“知道”
重建项目最常见的失败方式,就是用“不知道现状的勇气”去推翻现状。所以我们规定:前三个月绝对不写一行迁移代码,只做诊断。
这三个月里我们干了四件事:
- 全链路梳理:用代码调用链分析工具把所有核心服务之间的调用关系跑出来,标出Top30的核心链路,然后逐条过。
- 依赖图谱建设:把数据库表、消息队列、外部API的依赖关系画成图谱,找出那些“谁都在用但没人负责”的公共模块。
- 容量基线测试:对每条核心链路做压测,搞清楚当前系统的真实水位线,而不是凭感觉说“扛不住”。
- 故障演练:人为注入故障,看系统会不会自愈、监控能不能第一时间发现。
前两个相对常规,后两个是这次项目的转折点。故障演练做完,我们发现了12个高危依赖——比如某个基础服务一旦慢调用超过100毫秒,下游十个服务全部跟着超时;又比如某个缓存集群只要故障超过30秒,核心下单链路就会整体熔断。这些发现直接改写了后续的技术方案:加缓存、加熔断、加隔离,优先级全部上调。
很多人觉得前三个月“没产出”,但我认为这是整个14个月里最被低估的阶段。如果没有这三个月,后面切换至少要多两倍的返工。诊断的目的不是让自己安心,而是让所有决策都有据可循。
2.2 4-7月:数据资产盘点倒逼模块解耦
到了第四个月,我们才开始动代码。但动手的顺序不是“拆服务”,而是“拆数据”。
这其中的逻辑很简单:表与表之间的JOIN一旦拆开,代码层面的耦合会自动暴露出来。如果数据边界不清晰,拆出来的所谓微服务还会通过数据库互相藕断丝连,那只是把单体搬到了多个部署单元里而已。
我们当时的具体做法分三步:
- 给所有核心表打领域标签:订单域、用户域、库存域、结算域。
- 找出跨域JOIN和跨域事务,逐一分类:哪些是真正需要合并的,哪些只是查询时的便利。
- 把一部分“强同步调用”改成事件流,核心方式是监听数据库增量变更,转换成领域事件。
伪代码大概是这样的:
# 监听增量变更并转换为领域事件 def handle_binlog(record): if record.table == "orders": if record.op == "UPDATE" and record.after.status != record.before.status: producer.send("order.state.changed", { "order_id": record.after.id, "old_status": record.before.status, "new_status": record.after.status, "ts": record.timestamp, })这种做法的好处在于:老系统不需要感知消息投递,只要数据库的增量日志被解析即可;迁移期间,新老系统可以同时消费事件流,互不干扰。我们用了大概两个月,把三套核心库中两套的领域边界拆清楚了,同时把跨域定时任务从32个降到了14个。
这一阶段最常见的诱惑是“顺手把框架也换了”,我强烈建议克制住。拆数据边界已经够痛了,再叠加框架升级,出问题时根本定位不了是数据问题还是框架问题。
2.3 8-11月:核心链路切换的节奏设计
真正让团队神经紧绷的是流量切换阶段。我们先用了一个“三段式”的结构:影子读、双写、灰度切流。
- 影子读阶段:把线上流量复制一份打到新系统,只读不写,持续跑三到四周。这个阶段不看别的,就看结果差异率。
- 双写阶段:写入同时落老库和新库,老库仍然是事实源,新库通过异步任务同步数据,持续四到六周,观察数据一致性和写入延迟。
- 灰度切流阶段:逐步把线上流量从老系统切到新系统,每次切量都盯着核心指标。
具体的切量节奏和验收标准,我们当时是这样设计的:
| 阶段 | 持续时间 | 流量比例 | 通过标准 |
|---|---|---|---|
| 影子读 | 第9-10周 | 0%,全量回放 | 差异率 ≤ 0.01%,P99延迟差 ≤ 10% |
| 双写 | 第11-14周 | 0%,写入双跑 | 双写失败率 ≤ 0.001%,差异趋势收敛 |
| 灰度切流 | 第15-18周 | 5% → 20% → 50% → 100% | 每次切换后观察15分钟,错误率无上升,P99稳定 |
这里我想分享一个我们内部叫“3-5-2灰度口诀”的经验:每次切换的比例增量不超过20%,观察窗口不少于10分钟,必须盯2个核心指标(错误率、P99延迟)。这不是什么高深理论,而是血的教训换来的——早期有一次从20%直接切到80%,结果新系统的一个隐藏问题瞬间放大,逼着我们当天回滚,白白浪费了一周。
双写阶段最容易忽视的是“写入顺序”。我们采用的是“先写老库,再异步写新库”,因为老库是事实源,任何一笔写失败都能通过重试补齐。如果把新库当作主写,一旦新库出问题,老系统就会立刻断流,业务损失无法挽回。
2.4 12-14月:把前沿能力装进已经稳定的地基
基础稳定之后,剩下的三个月是用来“推进前沿”的。很多人以为前沿就是堆模型、堆算力,但实际上,前沿能力落地最大的瓶颈是数据基础设施。
举一个例子:订单状态事件流稳定运行后,我们立刻把实时特征接入了新的推荐和风控链路。之前风控是T+1的离线批处理,模型再好也只能第二天跑;现在基于实时事件流,同一个风险识别可以做到秒级响应,召回率还提升了将近一倍。这就是“重建基础”对“推进前沿”的杠杆作用。
我们当时给团队定了一个很奇怪的规矩:每个核心模块切换完成后,必须挂上一个新能力,才叫“完成”。比如订单模块切完,实时风控立刻接入;用户模块切完,推荐系统立刻换用新的实时特征服务。这样做的好处是,避免出现“重建完了,但业务毫无感知”的尴尬。
前沿能力的选择也很重要。我们不是一上来就铺很多线,而是只挑两个最能让业务方感知到差异的点:推荐实时化和风控实时化。这两个点都有明确的业务指标,也最容易验证重建成果。三个月时间,足够把这两个能力从实验变成生产可用。
3. 最容易被低估的三类底层工作:兼容层、数据迁移和回滚方案
3.1 防腐层不是中间件,是“新旧共存”的边界协议
很多团队会在迁移方案里纠结“大爆炸”还是“绞杀者”,我们毫不犹豫选了“绞杀者”,也就是新旧系统共存、逐步替换。但共存的前提是必须有一个清晰的防腐层。
这个防腐层不是某种特定的中间件,而是一个边界协议。它要做两件事:第一,把老系统的接口协议翻译成新系统能理解的协议;第二,把流量切换策略控制在配置层面,而不是散落在代码里。
举个路由规则的示意:
{ "service": "order_center", "strategy": "traffic-split", "rules": [ { "condition": "user_id % 100 < 5", "target": "new" }, { "condition": "internal_call == true", "target": "new" }, { "default": "old" } ] }有了这一层,切量不需要发版,只需要在配置中心调整百分比。这极大降低了切换的心理门槛——反正随时随地能退回去。但这里也有一个忠告:防腐层本身会腐化。如果你一直往里堆翻译逻辑,它最终会变成第二个“老系统”。我们当时规定,每个迭代都要清理防腐层中过时的翻译规则,让它的厚度保持在“够用就好”。
3.2 数据迁移三段式:影子读、双写、切换
数据迁移是整个项目里最不能出错的环节。我们实践下来,有几个细节是常规文档不会写的。
影子读阶段,重点不是“结果一模一样”,而是“差异可解释”。新系统和老系统的数据模型不同,字段命名、时区、精度都可能造成合理差异。如果一上来就追求零差异,团队会陷入无尽的对账中。正确做法是先把所有差异分成“可解释差异”和“不可解释差异”,每天只看后者数量是否收敛。
双写阶段,对账脚本不能只看行数或金额,而要对关键字段做哈希比对。我们当时用的比对逻辑大致是这样的伪SQL:
SELECT COUNT(*) AS diff_cnt FROM ( SELECT order_id, status, amount, updated_at FROM old_order_table WHERE updated_at >= '{start_time}' MINUS SELECT order_id, status, amount, updated_at FROM new_order_table WHERE updated_at >= '{start_time}' );实际操作中我们并没有用MINUS,而是对每条记录生成一个校验哈希,然后比对哈希集合。这样既能发现漏数据,也能发现字段级差异。每天跑完对账后,差异报告会直接推到群里,而不是躺在离线任务日志里,这一点非常重要:数据问题只有暴露在所有人眼前,才会被真正重视。
3.3 回滚方案的三条命:镜像可回退、数据可回滚、流量可切换
我见过太多迁移项目,回滚方案写得很漂亮,但真正遇到故障时根本执行不了。原因很简单:回滚不是一两个命令,而是一条完整的逃生通道。
我们给回滚方案定了“三条命”原则:
- 镜像可回退:每次发布前把所有服务打镜像,保留最近5个版本。回滚时不是重新构建,而是秒级拉起旧镜像。
- 数据可回滚:数据库变更全部采用expand-migrate-contract模式——先加列,再双写,再回填,最后才删旧列。任何一步出问题,都只回滚当前阶段,而不是整体回滚。
- 流量可切换:切量开关独立于代码,放在配置中心。一旦发现异常,回滚就是把百分比调回0,而不是发布一个旧版本。
这里有一条值得刻在墙上的经验:所有回滚都要先回答一个问题——你的数据是无损回滚还是尽力回滚?如果回答不了,就不要开始双写。我们早期就因为没想清楚这个问题,差点在切换时遇到无法恢复的尴尬。
4. 踩坑实录:几个差点让计划失去意义的决策
4.1 双写延迟不一致的那一周
进入双写阶段大概两周后,某天凌晨对账任务跑出了一条刺眼的数字:差异率超过0.5%。当时整个人都从床上弹起来了。
排查过程很曲折。我们一开始怀疑是同步任务丢消息,后来发现真相更隐蔽:老系统在部分调用链上用的是“本地事务+扫表补偿”,新系统用的则是“消息最终一致”。在退款和新建订单并发出现的边界场景下,老库已经落数据,但新库的消息还没到,对账脚本直接把这个差异标记成了错误。事实上这些差异里很大一部分是“最终一致窗口”内的正常现象。
解决方式有两个层次:
- 给对账任务加“延迟窗口”,允许30分钟内的差异存在。
- 把资金、库存这类强一致链路单独拎出来做实时对账,不给延迟窗口。
这次踩坑给我的教训是:对账任务本身也要设计,而不是简单把两张表拉出来做diff。如果一开始就按业务差异分级,我们至少能省下三天排查时间。
4.2 一次回滚把缓存设计问题暴露了
切换某模块到新系统后,业务方很快反馈:数据还是老值,刷新也不变。一开始怀疑是浏览器缓存,后来定位到是新系统的缓存设计出了问题。
老系统里有一个约定俗成的习惯:写入后立即读,靠数据库的事务保证一致性。新系统为了性能,在缓存策略上选择了“更新时删除缓存”,理论上下一次读取会重新加载。但问题在于,切换流量时,缓存里还存着老系统的旧快照,新系统读到的还是旧值。
这个坑的根因是缓存键和业务版本没有绑定。后来我们把缓存key里面加了一个版本号,每次切换流量或发布新版本时,整体版本号+1,让旧缓存瞬间失效。这个方案虽然会让切换瞬间的缓存命中率下降,但相比“命中旧数据”带来的业务错误,这点性能损失完全可以接受。
迁移期间的缓存设计有一条原则:宁可多miss几次,也不要命中旧数据。因为多miss的代价是几次查询,命中旧数据的代价是用户信任。
4.3 配置管理与代码管理脱节,差点导致全链路超时
第三个坑很隐蔽,甚至和系统架构无关,但造成的后果最严重。
当时为了调新环境的超时参数,有同事把配置中心里的一个配置直接从新环境复制到了生产环境,覆盖了生产环境原本更保守的超时阈值。结果上线后,核心链路瞬间出现大量超时,因为新的超时设置比服务端实际处理时间还要短。更麻烦的是,这个配置是在配置中心直接改的,没有经过代码评审,等到监控报警时,我们花了很久才意识到问题出在配置上。
从那以后,我们定了一条铁律:配置中心走代码评审,配置变更记录可审计,配置发布和代码发布一样分级。并且给配置中心加了版本diff,每次改动都要能看清改了什么、谁改的、为什么改。
配置的CI/CD远没有代码那么普及,但它和代码一样需要工程化。这是我这次项目里最便宜也最有效的一个投资,早做早好。
5. 复盘后的两个“会保留”和两个“绝不会再做”
5.1 值得保留的决定1:把前沿能力写进验收标准
如果再来一次,我依然会把“前沿能力上线”作为模块改造的完成条件。我们团队对“模块改造完成”的定义不是“老功能能跑”,而是“新能力能上线”。比如订单模块切换完成后,实时风控模型立刻拿到了事件流数据,这才算完成。
这个决定改变了团队的思维方式:前端业务需求不再是“改造完成”之后才考虑的后续项,而是和基础重建同步推进的验收标尺。它不仅保证了业务方对重建有感知,也让做底层的人能感受到自己做的东西真正在创造价值。
5.2 值得保留的决定2:用故障演练而不是代码评审来验收
代码评审当然要做,但它的盲区是“静态”。代码看起来没问题,不代表系统真正扛得住问题。我们后期每个阶段切换前,都会做一次人为故障注入,模拟依赖宕机、消息积压、缓存穿透这些场景。结果每次都能发现和代码评审结论完全不同的脆弱点。
印象最深的一次演练,是人为把下游服务延迟拉大,结果新系统的连接池被打穿,反而老系统因为线程池更小、拒绝更快,没有拖垮全链路。那次演练直接改写了连接池配置,而不是事后靠线上事故来教训我们。
5.3 下次不会再做的事1:用“统一技术栈”把老代码全部重写
“统一技术栈”这个口号听起来很漂亮,但它的成本往往被严重低估。我们中途一度想把老代码全部改写成新框架,后来发现很多“看起来旧但正确”的代码,一旦重写就会引入新的边界问题。
现在我的看法变了:要重写的是数据边界和交互协议,不是每一行代码。老代码能跑且有测试保护的,就让它继续跑;只有当它成为新能力的瓶颈时,才值得动刀。
5.4 下次不会再做的事2:追求双写阶段的“零差异”
双写阶段开始时,我们内部有一句话说“差异必须为0,否则不能切换”。听起来很严谨,实际执行下来差点把自己逼死。因为业务系统的差异天然存在,比如日志类数据、画像类数据,晚几秒根本无人在意;资金、库存、状态机这类强一致数据,却一丝一毫都不能差。
正确的做法是把差异分级:
- 必须绝对一致:资金、库存、订单状态机。
- 允许短暂不一致,但窗口内必须收敛:日志、画像、辅助字段。
- 允许有上限差异,且只影响非核心场景:推荐计数、通知状态。
健康的标准不是diff为0,而是diff在窗口内收敛到0,并且没有方向性偏差。所谓方向性偏差,就是同一类差异总是往新系统或老系统单边倾斜,那才是真正需要警惕的信号。
14个月走下来,我最大的体会是:这个时间本身不是一个固定的数字,而是一个节奏隐喻。如果你在周期里只做重建基础,团队会疲,业务会等不及;如果你只做推进前沿,地基会塌。把两者揉在一起,用同一个节奏推进,才是真正值得复制的部分。
最后分享一个我们从项目第一天就用到现在的小习惯:把“基础重建看板”和“前沿探索看板”放在同一张物理白板上,每天例会同时看两边的卡片。只有当“前沿需求”反过来频繁追问基础架构要数据、要弹性、要时效的时候,重建才不会沦为自娱自乐的技术洁癖;也只有当天天在改底层的时候还坚持往产品里塞新AI能力,团队才不会在前线被磨掉心气。
如果你正准备启动一个重建项目,我建议你第一周就做两件事:把坏系统清单写出来,把一个最近最想要的前沿能力钉在看板最上方。剩下的节奏,14个月里自然会告诉你。