
上个月我们终于把内部代号为T3Code的技术专项正式关单了。这个代号没什么玄机就是今年团队排到的第三个技术专项顺手用来给一批工单做前缀。但整个周期走下来它实际承担的分量比代号重得多既是一次对核心业务系统的架构升级也是一次对存量代码的全面治理还是一次团队协作方式的翻新。做这个项目的起因很现实业务迭代速度上来了需求排期越来越紧可每次改代码都像在雷区里走路。改一个订单状态流转要手动翻四五个配置点新同学入职三周还在靠注释猜业务逻辑每次发布日研发、测试、运维一起盯着灰度日志像拆弹。我们心里都清楚系统不是一天烂掉的但谁也不想当那个在高速路上换轮胎的人。直到年初一次线上故障根因定位花了两天最后发现是五年前加的一段老逻辑在特定条件下覆盖了新配置。那次之后立项申请一周内就批下来了。这篇文章算是一份完整的复盘记录把我们从立项、盘点、分层改造到上线踩坑的完整链路写出来。如果你也在带一个体量不小、历史包袱很重的业务系统或者正打算发起一场代码治理/架构升级可以参考我们的打法与教训。1. 为什么系统会走到改不动、不敢改这一步T3Code立项前的问题拆解立项之前我们花了整整一周做问题梳理。很多团队一谈重构就想着推倒重来但其实大部分系统根本不需要重写需要的是把一个已经失去约束力的代码库重新管起来。我们先用数据说话把不舒服变成可量化的问题。1.1 三个真实痛点耦合、无测试、变更不可见第一个痛点是耦合。订单模块看起来是一个完整模块实际上内部通过静态方法、共享工具类、全局配置中心跟十几个模块纠缠在一起。我们做过一次粗糙的依赖扫描订单模块直接间接引用的类超过两千个其中三分之一属于其他业务模块。这意味着改订单模块的任何一个公共方法影响范围都无法预估。第二个痛点是没有测试。核心模块的单元测试覆盖率长期停留在个位数集成测试几乎为零。不是大家不想写是系统根本没按可测试的方式设计数据库连接直接散落在业务代码里外部接口调用写死在静态方法中想 mock 都无从下手。测试缺失带来的直接后果是任何一次重构都没有安全网谁也不敢动老代码。第三个痛点更隐蔽变更的不可见性。同一个业务动作可能在接口层做一次校验在 service 层做一次状态流转在 DAO 层又做一次数据拼装甚至还有定时任务里补一段历史数据修复逻辑。想要完整追踪一个字段从进入到落库的生命周期得顺着调用链跳七八个类。这种代码更像补丁叠补丁而不是一个清晰的业务表达。1.2 立项目标不是重构是止血 分层 自动化基于这三个痛点我们给 T3Code 定了三个核心目标每个目标都有验收标准目标具体含义验收标准止血先解决最容易引发线上故障的耦合点核心模块的隐性依赖数量下降 80%分层建立清晰的架构边界让依赖方向可管理新代码必须遵循分层约束违反即构建失败自动化用测试和流水线替代人工检查核心模块覆盖率提升到 60%发布支持一键回滚我还特别加了一条不做什么清单不搞微服务拆分不换编程语言不把前端一起重写。原因很简单一次只解决一个主要矛盾。微服务拆分听起来高级但在业务边界都没理清楚的阶段贸然拆只会把进程内的耦合变成跨服务的网络调用问题更麻烦。这个决定后来被证明非常关键它让我们把全部精力放在代码结构本身而不是被分布式带来的新问题牵着走。1.3 定项目代号的一个小经验T3Code 这个代号是在排期会上随手定的。我的习惯是给技术专项起个简短、可复用的代号后续所有相关的分支名、工单前缀、文档目录都用它。这样做的好处是几个月后想回顾整个项目的全貌只要搜这个代号就能把所有碎片拼起来。比用重构二期这种模糊叫法好用得多。2. 动手之前先把家底盘清楚真实调用关系、代码资产与技术选型很多项目失败死在第一步还没搞清楚现状就开干。我们用了接近三周做现状盘点这一段急不得盘点质量直接决定后面的改造顺序和风险评估。2.1 画出真实的调用关系而不是嘴上说的架构图我们做的第一件事是静态依赖扫描。用工具扫描全部代码生成类级别和模块级别的依赖图。结果跟想象中差别很大架构文档里画的订单模块依赖库存模块是一对一关系实际代码里订单模块还悄悄依赖了用户模块的私用工具类、支付模块的枚举定义、甚至营销模块的缓存 key 常量。很多依赖都是历史上顺手拿过来用累积起来的。我们把这些意外依赖全部记录下来按两个维度打标依赖的深度直接还是传递和依赖的必要性合理还是历史遗留。这一步做完心里基本有数了大概 30% 的跨模块依赖属于明显的路走偏了应该通过接口隔离另外一部分虽然绕但还有历史原因需要先封装再逐步收敛。2.2 热点代码与废代码改造顺序的依据除了依赖关系我们还用动态链路跟踪梳理了线上热点。哪个接口调用量最大、哪个方法执行时间最长、哪个存储过程还在被凌晨定时任务依赖这些数据直接决定改造优先级。有意思的是我们同时发现了不少僵尸代码从上线就没被调用过的控制器、被注释掉的配置、只在测试环境生效的开关。这部分代码混在主干里严重干扰了代码阅读。T3Code 期间我们顺手清了将近两万行此类代码让整个代码库的可读性提升了一个档次。2.3 分层决策为什么选三层 防腐层而不是微服务技术选型阶段我们内部有过一次很激烈的讨论。一部分同学主张借这个机会直接上微服务另一部分包括我主张先做模块化分层。最终定下来的是逻辑上采用经典的三层结构接口层、应用层、领域层依赖方向必须从上往下跨模块交互全部走接口不允许直接访问对方的内部实现类。没有选微服务的核心原因有三一是团队规模不支持总共十几个后端拆出五六个服务后每个服务连一个完整负责人都不够二是基础设施没准备好服务发现、链路追踪、分布式事务这些底座都还是薄弱项三是业务边界本身模糊强行微服务只会把模糊边界变成网络开销。而模块分层是在同一个代码库内解决依赖问题部署方式暂时不变风险要低一个数量级。2.4 防腐层设计让老代码和新代码能共存增量改造期间老代码和新代码必然要共存一段时间。为了避免新写的代码被老系统的坏味道二次污染我们在关键边界上加了防腐层。这个概念不复杂本质上就是二十行代码的事老系统对外提供的任何数据都必须经过防腐层转换成新架构里定义的领域模型再进入新代码。举个例子老订单模块返回的是一个大而全的 OrderDTO二十多个字段带着各种历史含义。新代码里我们定义了 OrderSummary只保留新业务需要的几个字段。转换逻辑统一收口在防腐层里这样新代码永远不会被老模型的脏数据带偏。防腐层还能帮我们逐步收敛当某个老模型在防腐层里被引用的次数降为零就可以安心删除。3. 改造路线图用绞杀者模式让老代码体面退休T3Code 最核心的实施策略是采用增量式改造业内通常叫绞杀者模式或绞杀者植物模式。思路很简单不试图一次性替换整个系统而是围绕老系统一点点生长出新结构每完成一块就把老的那块绞杀掉直到老系统彻底退休。3.1 为什么拒绝推倒重来我们很清楚推倒重写是最危险的选项。业务系统里沉淀了大量隐性规则某个状态为什么不能从 A 直接到 C某个金额为什么要做两次精度处理这些规则文档里没写全只存在于代码细节和运维同学的脑子里。真要推倒重来哪怕需求评审做得再细也一定会漏掉一批边界行为上线就是故障。增量改造的好处是每一块改造都有明确边界改造期间老逻辑还在线上运行就算新代码出了偏差还能随时切回老链路。风险被切成小块逐一消化而不是攒一次大的爆炸。3.2 第一批改造对象怎么选评估矩阵改造顺序很讲究。我们建了一个评估矩阵给每个候选模块打分维度包括业务重要性、调用热度、耦合程度、可测试性、团队熟悉度。总分越高越优先处理。最终第一批选了三个模块用户通知中心、订单查询、对账文件生成。它们的共同点是业务逻辑相对独立对外接口清晰不涉及太复杂的事务边界而且测试同学手上正好有现成的验证用例。低风险、高收益、好验证这三条是第一批改造模块的黄金标准。反过来我们刻意没碰订单状态机。那个模块是所有交易的核心行为规则极其复杂而且改动它会影响十几个下游系统。它被排到第三批等工具链和团队信心都成熟了再动。3.3 特征测试先给老代码拍一张行为快照改造老代码最怕的一件事是老代码的行为可能是错的但它错得有历史合理性。比如某个接口对非法参数的处理方式是返回 null这从编码规范角度是错的可如果上游依赖了这个 null 来做空判断你改成抛异常就是把线上搞炸。解决办法是特征测试也叫黄金主测试。做法很简单在改造之前先给老模块写一批测试输入一组真实历史数据断言输出结果与老代码完全一致。这些测试不是为了验证正确性而是把现状行为固化下来作为后续改造的对照基准。这批测试我们写了大概两个星期过程很枯燥但回报巨大。在原模块改造上线后我们直接在新实现上跑特征测试一次就跑出了七处行为差异——全是老代码里隐藏的特殊分支比如当商品类型为虚拟商品时运费模板字段为 null 但金额必须为 0这类写都没写进文档的规则。没有这层保险这七处差异上线后大概率就是七次线上事故。3.4 迁移七步法从搭脚手架到清理老代码每个模块的迁移我们都执行同样的七步流程这套流程后来沉淀成团队的标准动作搭框架在主干上新建模块目录按三层结构建包配好依赖约束规则。建防腐层定义老模块与新模块之间的转换接口两边互不感知对方内部实现。迁移逻辑按业务用例逐个迁移每迁移一个用例就跑一遍对应特征测试。并行运行新模块与老模块同时上线通过开关控制流量走哪条链路。灰度放量先让 1% 流量走新实现观察日志与错误率逐步提升到 10%、50%、100%。稳定观察全量后保持双写或并行状态至少一个完整业务周期我们定的是两周。清理老代码确认新实现稳定删除老实现与相关配置把特征测试升级为正式回归测试。这套流程的关键是每一小步都可回滚。灰度放量阶段一旦发现异常把开关翻回去就行不需要发版、不需要回滚数据库整个过程控制在分钟级。4. 实测中的意外比预期多花两倍时间的三个坑计划归计划T3Code 实际推进中还是踩了不少坑。挑三个最具代表性的写在这里它们都不是什么高深的技术难题恰恰是项目管理中最容易被忽略的细节。4.1 第一个坑全局静态状态像隐形的鬼魂我们第一个改造的模块是用户通知中心逻辑不复杂接口也就五六个。特征测试也写了一轮感觉挺稳。结果灰度到 30% 流量时收到告警通知发送成功率下降。排查链路是这样的先看新实现日志发现部分通知发送任务抛了空指针异常信息指向一个用户偏好配置对象。继续追踪发现这个偏好配置是通过一个全局静态方法加载的而这个静态方法在每次请求时会被老代码的某个拦截器顺便重置。我们的新代码读取配置的时间点在重置之后于是拿到的全是 null。这个问题的根源是隐式依赖新模块看似只依赖自己的代码实际运行时却被老系统的全局状态绑架了。我们的解法分两步第一步立刻在接入层加一层显式依赖注入把偏好配置的读取时机提前到老拦截器执行之前第二步在代码扫描规则里加了一条禁令——新模块代码不允许直接访问任何 static 非 final 的全局容器。这个坑给我们的教训是代码结构和运行时行为是两回事。依赖扫描只能看到显式引用显式的坑一抓一个准但静态变量、ThreadLocal、全局单例这种隐式状态比显式依赖危险十倍。排查隐式依赖最有效的办法不是读代码而是看灰度期间线上的完整调用链日志。4.2 第二个坑特征测试只录了正常动作漏了异常分支特征测试帮我们兜住了大量行为差异但有一类差异它没兜住——异常路径。我们当时的测试数据主要来自线上正常请求的采样天然缺少异常分支的数据。直到全量上线后的第三天财务同学反馈某类退款订单在账单文件里缺少金额字段。查下来发现老代码在处理订单已部分退款这种情况时有一个隐晦的补位逻辑如果主金额字段为空会把最近一笔退款金额写入账单的备注字段。这个逻辑非常冷门只在少量历史订单上触发我们的特征测试数据集里恰好没有这类数据。这个坑的修复本身很简单补上逻辑就行。但暴露了一个方法论问题特征测试的样本质量决定了行为快照的完整度。后来我们改进了测试数据采集方式不只采样正常流量还专门从历史工单、线上异常日志、客服反馈表里挖掘边界输入把低频但真实的分支补进数据集。4.3 第三个坑切换流量的回滚预案差点变成滚不回去第二个模块订单查询改造时我们在设计阶段就考虑了数据兼容新代码读的是一张新表老代码读的是老表两边通过一个同步任务保持数据一致。结果灰度期间一切正常全量两周后准备清理老代码时发现一个严重问题新表的数据只有全量上线后的增量历史数据靠同步任务回填但回填任务因为源表一个字段类型不匹配凌晨跑批直接失败了而且失败后没有告警一直在静默重试。这意味着如果此时回滚老代码用户查询历史订单时会发现数据不完整。我们的回滚预案只准备了流量切回去没考虑数据也得跟着切回去。修复方案很朴素重新修正回填任务的类型映射全量重跑一遍数据补齐后才继续清理动作。这个坑给我们的教训是任何涉及数据迁移的改造回滚预案必须同时覆盖应用层回滚和数据层回滚。永久性的数据变更尤其要谨慎最好在切换前就搞定完整回填而不是把回填拖到切换之后。4.4 踩坑之后的复盘机制这三个坑暴露之后我们调整了项目节奏每次灰度放量前增加一次隐式变量排查清单评审每次全量切换前增加一次数据完整性演练。额外付出的时间成本不多但直接把后期返工量砍掉了一大半。我把这三个坑整理成了表格贴在了团队 wiki 上作为后续所有技术改造项目的默认检查项坑点触发条件拦截手段全局静态状态干扰新模块与老模块共享进程灰度期间全链路日志分析禁止 static 可变容器特征测试样本覆盖不全只采样正常流量结合工单和异常日志补充边界样本数据回填沉默失败同步任务无告警回填任务必须加成功率和时延告警5. 收官复盘T3Code 给团队留下了哪些长期资产项目关单不代表一切结束。我们花了额外一周做完整复盘把数据、流程、团队变化都梳理了一遍。这部分我觉得比单点的技术方案更重要因为技术方案能解决眼前问题而流程和认知的升级才是长期资产。5.1 硬指标变化效率、质量与可维护性直接看数据。改造涉及的核心模块一共四个覆盖了全年 70% 以上的线上请求量。项目前后对比指标改造前改造后核心模块单元测试覆盖率个位数58%跨模块隐性依赖数量23040 以下需求平均交付周期7 天3.5 天线上故障年度累计11 次4 次新同学上手业务代码时间约 3 周约 1 周还有一个容易被忽视的数字删除的僵尸代码约两万行。代码量减少但功能全部保留这对后续维护成本的降低立竿见影。5.2 流程制度的沉淀从靠人自觉到靠机制约束T3Code 最值钱的产出不是代码本身而是三套机制第一套是依赖约束构建检查。我们在构建流水线里加了一步自动化检查任何模块如果引用了其他模块的内部实现类构建直接失败。新代码想跨模块调用必须走对方提供的公开接口。这个约束一上线架构腐化的速度立刻被摁住了。第二套是灰度开关体系。所有模块改造都通过配置中心控制放量比例运营和客服可以在不联系研发的情况下紧急把某条链路切回老实现。这个能力在项目之后变成所有新功能的标配。第三套是代码所有权制度。每个模块指定一个负责人和一个后备负责人在代码评审时有最终决策权。以前出了问题大家互相推现在每个模块都有明确话事人线上告警第一时间就能定位到人。5.3 二期规划T3Code 结束后我们打算做什么一期解决了代码结构问题二期我们想推进两件事一是可观测性把链路追踪和日志体系补完善目标是线上出问题能在五分钟内定位到具体代码行二是性能优化针对热点接口做缓存和读写分离改造。这两件事放在一期的后面做是顺理成章的结构理顺了性能优化的改动才敢动手链路追踪建好了未来任何改造都有更细的排查抓手。不过二期具体排期还没定得看业务侧节奏优先级上我会更偏向可观测性性能优化可以跟着业务痛点走。6. 最后说点自己人之间的大实话T3Code 整个项目从立项到收尾前后花了大约四个半月。比最初计划的三个月超出了不少但整体节奏还在可控范围内。复盘时大家最大的共识是技术型项目的排期一定要预留 buffer这个 buffer 不是给写代码留的是给踩坑留的。我们最后统计下来纯开发时间只占总时长的 55% 左右剩下全在测试补样本、排查隐式依赖、修数据同步这些看起来不起眼但绕不过去的事情上。如果你想在自己团队发起类似的项目我给三条最朴素的建议第一先花充足时间做现状盘点别急着写代码你把依赖关系画清楚的那一刻改造方案其实已经呼之欲出了第二从最容易被验证的模块下手头两个模块成功上线带来的团队信心比任何激励都管用第三一定把不改什么也写进立项文档它能帮你挡掉一半以上的需求干扰。T3Code 这个代号以后大概还会出现在我们的分支名里不过作为一个阶段的标记它已经完成了自己的使命。系统的代码还在一天天演进但至少现在我们改订单模块时不用再心惊胆战地翻那两千个类了。