☰
146次迭代后的技术债治理:从债务地图到可持续重构
2026/10/16 12:50:48 网站建设 项目流程

146这个数字,是我们某个核心后台系统从“能跑就行”到“每次发布都提心吊胆”所经历的迭代次数。第1版的时候,模块边界清晰,测试顺手,发版像呼吸一样自然;到第146次迭代结束,情况完全反过来了——代码里散布着各种临时补丁,同一个业务逻辑在不同地方各写各的,改一个bug时常带出两个新bug,新人入职光摸清环境就要一周,而每一次上线都像一场赌博。

这篇文章想说的,不是“技术债有害”这种人人都知道的大道理,而是我们团队从这146次迭代的混乱里爬出来的完整治理过程。我把这段经历整理成了一套可复用的方法:怎么给存量代码做体检、怎么给债务排优先级、怎么在不耽误业务迭代的情况下持续还债,以及治理过程中最容易被忽视的坑。如果你是那种维护着老系统、每天被各种历史包袱拖累的开发者,或者团队刚准备启动技术债治理但不知道从哪下刀,这套思路应该能直接拿走用。

1. 146次迭代之后:技术债是怎么一步步沉淀下来的

1.1 所有债务几乎都是“当时最合理”的短期选择

回看那146次迭代的提交记录,我最大的感受是:没有任何一次代码变更是某个人故意埋雷。每一个后来被我们痛骂的坏味道,在当时都有相当合理的理由。

典型场景一,联调时间紧张,接口字段还没定,为了先把流程跑通,直接在前端代码里写死了几个配置值;典型场景二,需求边界不清晰,产品说“先做一版看看效果”,于是复制了一份老逻辑改了几行就上线;典型场景三,线上慢查询报警,不敢动底层查询逻辑,直接在上面套了一层缓存,但完全没考虑缓存失效策略。这些决策单独看全都是“正确”的,因为它们帮团队在当下保住了交付时间。

但问题在于:短期选择做完了,没有后续的清理动作。开发者的注意力永远被下一个需求占据,那些临时代码就留在了系统里,像信用卡账单一样开始产生利息。等这类的临时方案积累到一定数量级,系统就不再是“有几个坏味道”的程度,而是整体架构开始失去解释性——新人看不懂为什么要这么写,老人也解释不清某段逻辑到底覆盖了哪个特殊场景。

1.2 系统发出这五个信号时,治理窗口已经打开了

技术债是有症状的。如果你所在的项目出现了下面五个信号中的任意两三个,说明系统已经进入了“借新还旧”的恶性循环,这个时候启动治理不是可选项,而是必选项。

第一个信号是耦合度失控:改一个看似独立的小功能,需要连带修改七八个文件,每次改动的影响范围远超预期。第二个信号是构建和测试时间持续变长,不是说一次两次的波动,而是每个迭代都能感觉到变慢,跑完整个测试集从最初的十分钟变成了四十分钟。第三个信号是发布变成重大事件,每次上线都需要多名核心开发待命,回滚脚本比功能代码还长。

第四个信号更隐蔽:代码评审开始流于形式。因为核心业务代码太复杂,评审者不敢提意见,怕自己没看懂;提了意见也无从下手,因为“这段代码动了可能会出问题”。于是评审变成了走过场,烂代码继续合入,系统继续恶化,形成了负反馈循环。第五个信号是新人上手时间无限拉长,老员工成为系统的“活文档”,所有知识都靠口头传递,而这部分人在组织里往往是最忙的。

当我们意识到这五个信号在系统里全部出现时,团队达成了一致:不治不行了。

2. 第一刀:建立完整的债务地图

2.1 债务清单应该长什么样

所谓治理,第一步不是动手改代码,而是先知道债在哪。这个道理听起来很简单,但真正执行的时候,大多数人会犯同一个错误:列了一张类似“登录模块要重构”“数据库连接要优化”这样模糊的待办清单,结果过两周就搁置了。

我们最终使用的债务清单不是这种“想法列表”,而是像系统体检报告一样,每一笔债都有明确的坐标。当时我们建立的债务台账包含这么几个字段:所属模块、债务类型、触发场景、风险等级、估算的修复成本、发现时间和关联的故障记录。债务类型包括代码坏味道、架构问题、测试缺失、配置混乱等,触发场景则要写清楚“这笔债什么时候会咬人”,比如“每次活动大促前需要手工修改XX配置”。

用这个表格拉出来的第一版清单,总共有63项。我印象很深的有这么几类:一个核心订单模块的函数圈复杂度超过了50,包含大量嵌套逻辑,没人敢动;一套外部接口对接逻辑被复制到了五个服务里,每次接口升级要分头改五遍;还有一批持续了快一年的临时监控开关,代码里散落着十多个魔法变量。把这些信息全部落到表格里之后,大家第一次对着同一张地图讨论问题,而不是各自凭印象觉得“这里很乱那里很烂”。

2.2 用工具和事故记录补齐盲区

光靠开发者的主观记忆来做盘点一定会有盲区,尤其是那些“已经习惯了”的烂代码——人在一个环境里待久了,会对很多坏味道麻木。为了尽量让债务地图完整,我们做了三路信息收集。

第一路是工具扫描。静态分析工具以零人工干预的方式跑了一遍代码库,重点看圈复杂度、重复代码、过长的类和函数、未处理异常这些指标。第二路是把过去一年所有线上故障、慢查询、紧急修复记录全部翻出来,逐一对照代码库定位——“从故障反推”是最有效的找债方式,因为每一笔真实债务几乎都对应着一次线上事故。

第三路有点反直觉,我们要求每个模块的负责人主动列出“自己最不想碰的代码在哪、为什么不想碰”。这个凭感觉的答案往往比工具扫描更能暴露问题,因为它背后是真实的开发效率损失。工具只能告诉我们代码哪里复杂,而长期维护代码的人知道哪里复杂得毫无道理。

三路信息汇总后,债务清单从63项扩充到了117项。但要注意,这117项里并不全是真正的债,有些是主观偏好,这就涉及到下一步的取舍。

2.3 学会区分真债和非债

拿到117项清单后,我们做的第一件事不是开工,而是砍掉了一部分“假债”。这是个很关键的步骤,处理不好会让整个治理项目变味。

有一类问题我们直接划掉了,比如“这个命名风格我不喜欢”“函数应该拆分得更细一点”“这里应该把if else改成多态”之类。不是说这些建议不对,而是它们属于代码风格和过度设计的范畴,跟技术债的“影响交付效率、影响系统稳定”这两个核心特征不匹配。把它们混进债务清单,只会让治理变成一场无尽的代码风格战争,最后什么都推不动。

真正的技术债必须满足至少一个条件:它正在降低交付效率(比如每次改动都要花额外的时间去理解和绕坑),或者正在增加线上风险(比如某个隐藏的时序问题随时可能导致数据不一致)。按照这个标准重新过滤后,117项缩减到了79项,这79项才是治理真正要面对的债务。

这一步的经验是:债务清单是用来服务业务交付和系统稳定的,不是用来满足开发者审美洁癖的。每一条都要写清楚“它为什么会降低效率或增加风险”,写不清楚的就先放一边。

3. 让排序可解释:优先级不该看谁嗓门大

3.1 四个排序维度

债务清单有了,79项不可能一次性全还完,必须排优先级。这是治理项目里最容易爆发争论的环节:后端觉得自己模块的连接池该换了,前端觉得自己的状态管理该重写了,每个人都觉得自己负责的部分最重要。

后来我们定了四个排序维度,每个维度按1到5分打分,最终按总分排序。第一个维度是影响面,这笔债涉及多少个核心功能、多少用户量级;第二个是触发频率,这笔债多久被踩中一次,是天天踩还是半年碰一回;第三个是风险等级,这笔债如果爆了会是什么后果,是损失一点性能还是引发数据错误;第四个是修复成本,包括改动工作量和回归风险,成本越低越应该优先处理。

这四个维度打分的窍门在于,不能让拥有代码的人自己打分。当时我们就踩过这个坑,某模块的负责人给自己的债务打了很高的分,理由是“影响面很大”,但其他同事一问,那个模块实际只承接了不到百分之五的流量。后来我们改成让每个债务项在组内公开打分,并且要求打分时必须说出具体依据,靠嗓门和职级压人的情况才消失。

3.2 用“债务利息”的视角算经济账

除了上面四维打分,我还推荐一个很实用的视角:用“债务利息”来做二次验证。技术债最大的问题不是它本身,而是持有它需要持续付出利息,这个利息就是每次相关改动时额外消耗的时间和风险成本。

举个例子,账上有一笔“写死的第三方接口地址”这种债,可能每次联调都需要人工改配置再改回来,每次消耗两小时;另一笔债是一个隐藏的并发问题,平时不触发,但每月总有那么一次导致数据错乱,每次光排查就要花四个人天。哪笔债该优先还?表面看写死配置的债务天天烦人,但从成本角度算,并发问题一个月消耗四个人天,写死配置半年才消耗不到一个人天,前者的优先级明显应该更高。

我们当时把每笔债都估算了一个“年化利息”,也就是不修它的话,一年会给团队增加多少工作量。这个数字不需要多精确,估算误差百分之五十都不影响排序,但有了这个数字,向业务方和上级解释优先级就会变得非常容易——毕竟大家都能理解“还债是投资,利息是成本”这个逻辑。

3.3 业务方必须参与排序

这个观点可能有些人不同意,但我坚持认为:技术债的优先级排序不能只由技术团队内部决定。道理很简单,技术债的治理成本占用的也是业务交付资源,还一笔债就相当于暂时抽调人力不去做某个业务需求,如果不让业务方参与决策,最终的结果要么是业务方认为技术团队在做无关紧要的事,要么是技术团队还的债跟业务优先级完全脱节。

我们当时的做法是:把排序前十的债务项翻译成业务语言,列成一张“风险与成本对照表”。比如某项债务的表述不写“核心模块圈复杂度超标”,而是写“该模块接近大促高峰期时每次改动需要额外一天回归测试,且存在上线失误风险”。业务方看到这样的描述后,很快就能判断这笔债是否值得在近期安排。最后排序的结果是双方的共识,治理推行起来阻力会小很多。

4. 治理的执行方式:存量债务与增量债务分开打

4.1 存量债务:先止血,再动刀

排序之后进入执行阶段。我们对存量债务采取的是“先止血、再动刀”的原则,这个顺序非常重要。

所谓止血,就是对于那些在排序中风险等级高、暂时又排不上重构计划的债务,先通过非侵入手段控制住爆雷风险。常见的止血手段包括加监控告警、加接口熔断和降级、对关键数据做对账任务。比如我们有一笔“历史订单数据读取逻辑复杂且无缓存策略”的债,短期没法重构,就先在读取路径上加了超时控制和失败降级,同时在订单详情页埋了耗时监控。这样即使这笔债仍然存在,至少它恶化的第一时间我们就能知道,而不至于直接引发线上事故。

止血完成,才轮到真正的“动刀”。动刀时我们一般从三种战术里选一种:第一种是整体重写,适用于模块本身不大、逻辑清楚但实现极其糟糕的情况;第二种是局部重构,适用于模块规模较大、但可以按功能边界逐步拆解的情况;第三种是加防腐层,适用于外部依赖混乱、但内部逻辑还能忍受的情况。三种战术的适用性对比如下表:

战术选择适用场景主要风险推荐周期
整体重写模块小、业务边界明确、现状实现极差行为偏离,隐藏需求丢失2-4周
局部重构模块大但可拆分,有清晰的接口边界改动牵连,回归不彻底按功能逐段推进
防腐层外部依赖混乱、内部逻辑相对稳定防腐层本身变成新复杂度1-2周

实际执行里,我们做的第一个重构案例是一个持续被抱怨的用户权限模块。它的老实现里有一个函数承担了鉴权和权限过滤两种职责,任何一个需求变更都会牵动它的逻辑,测试覆盖率也几乎为零。我们没有一上来就重写整个模块,而是先用一周时间把权限过滤的部分从函数里拆出来,单独抽成一个新接口,让原来那段逻辑只保留鉴权功能。这一步做完,后续的权限规则改动就再也不需要动那段老逻辑了,利息立刻开始下降。

4.2 一个具体的重构小例子

很多读者会问:重构存量代码的时候,怎么保证行为不变?这里分享一个我们团队一直在用的思路——给老代码先穿上“防护衣”,也就是写表征测试(characterization test),然后才动手改代码。

所谓表征测试,就是对现有实现输入一组样本数据、记录当前的输出结果,然后把“当前输出”作为断言写进测试里。它不验证代码逻辑对不对,只验证重构之后行为和重构之前保持一致。这步听起来简单,实践里非常救命。

举个实际的代码示例。老代码里有一段比较价格区间的逻辑,写得嵌套极深,我们用 Python 简化示意一下:

def match_sku(cart, sku): # 老逻辑:混杂了价格判断、库存判断和地区判断 if cart is not None: if cart["price"] >= sku["price"]: if sku["stock"] > 0: if cart["region"] in ("CN", "HK"): return True return False

这段代码的问题是三个判断硬耦合在一起,任何一个需求变化都会改到这里。重构的第一步不是重写判断逻辑,而是先把各种输入组合的实测输出固定下来,比如“价格高、有库存、地区CN返回True”“价格低、无库存、地区HK返回False”等等,写成一组基线测试。然后才动手把三个判断拆开,拆的过程中每个节点跑一遍基线测试,任何行为变化都能第一时间被发现。

等到拆完确认行为一致,我们再针对新的结构补充真正意义上的单元测试,把“防护衣”转成“正经测试”。这个过程虽然多花了一些写测试的时间,但比起重构上线后炸掉再花几天排查,性价比高出了一个数量级。

4.3 增量债务:质量门禁不是卡人,是保护节奏

存量债务还了一部分之后,最怕的是新债务的增速超过偿还速度,那跟一边还信用卡一边继续透支没区别。所以从治理项目启动的第二天开始,我们就同步建立了增量债务的控制机制。

我们的增量控制不是那种复杂到让人窒息的规则体系,核心就三条:一是在CI流水线里加入新增代码的圈复杂度阈值检查,超标的合并请求不允许通过;二是在代码评审模板里强制要求填写“变更影响范围”,评审者只需要确认这个影响范围和实际改动是否一致,而不需要对每行代码做道德审判;三是对新增的核心业务逻辑强制要求至少覆盖主要分支的测试,否则红灯。

这套机制初始运行的头两周,团队内部确实有抵触情绪,主要声音是“流程太慢,一个改动被反复打回”。后来我们把门禁的数量控制在三条以内,并且允许例外申请——确实有紧急热修,可以先合并再补测试,但必须由负责人在三天内补上。这套“少而坚定”的门禁体系运行一个月后,合并请求被拒的次数大幅度下降,不是大家妥协了,而是新的坏味道在产生之前就被挡掉了。

5. 治理过程中最容易翻车的三个场景

5.1 重构完没做行为对比,线上栽了跟头

我们治理项目推进到第二个月时,差点因为一次重构事故前功尽弃。当时某个订单状态流转的模块被列为重点重构对象,我们按照之前的战术抽了个函数、理顺了分支逻辑,测试也跑过了,自我感觉很好。结果上线后第一天,线上就发现一组特定业务场景下的订单状态展示异常,原因是老代码里有一段顺序相关的副作用逻辑,光靠常规单测根本测不出来。

这个事故给我们的教训非常直接:重构上线前,除了跑测试,还要在预发环境做一次黄金链路巡检,用真实业务数据完整走一遍主流程,把每一步的输出和重构前的结果做对比。这里说的对比不是肉眼看,而是接一套记录日志,把关键节点的业务数据落成快照,用脚本逐字段比对。做过一次之后我们发现,很多老逻辑的行为根本不在代码注释里,只有真实数据能告诉我们它的实际行为。从那以后,“行为对比”成了我们所有重构类改动上线的硬性前置条件。

5.2 老板看不到收益,治理差点被叫停

这是所有技术债治理项目的共同风险:治理是对系统长期稳定性的投资,但它的产出不像业务需求那样直观,很难在短期看到回报。我们项目进行到第三个月时,来自上级的压力明显变大,因为从业务视角看,这个团队花了不少人力在“没有新功能”的事情上。

扭转这个局面靠的不是解释,而是数据。我们做了一个简单的治理成效看板,右边是重构涉及的模块列表,左边是这些模块在治理前后的关键数据对比,包括线上故障次数、平均修复时长、接口平均响应时间、代码提交到发布的上线周期。几组数字出来的效果很直观:核心订单模块的故障次数从每月三次降到零次,发布周期从两天缩短到半天。我拿着这两组数据去汇报,之后再也没有人质疑治理项目值不值了。

这里我想多说一句:技术债治理一定要从立项第一天就收集基线数据。没有基线数据,后面所有成效都是自说自话。我们前期就是因为没意识到这一点,导致前两个月的成果只能靠事后补数据来估算,费了很大劲。

5.3 把技术债治理搞成了一场代码风格运动

另一个我们差点踩进去的坑,是治理过程中出现了“为了重构而重构”的倾向。原因也很现实:大家做了几个月治理,觉得单纯修债务不够过瘾,开始想着顺便把代码风格、命名、注释都统一一遍,甚至提出要对某些模块做“现代化改造”。

这类想法一冒头就被我叫停了。原因在前面提过——债务治理的核心收益是“降低风险和交付成本”,而风格统一、框架升级这类事情虽然也有价值,但它属于另一种类型的工作,排期和风险控制都不一样。把它们混在一起,会模糊治理的边界,让评审者不知道该关注什么,也让债务治理从一个有清晰目标的短期战役变成一场没完没了的工程运动。

我们的原则是:债务清单上有什么就做什么,不夹带私货。哪怕看到某个模块里有非常别扭的命名,只要它不影响理解和改动效率,就先记进“代码优化建议”而不是“技术债务”,等目标明确了再处理。有了这个边界,整个治理项目的推进速度快了非常明显。

6. 从“还债”到“不欠新债”:让债务上限成为团队共识

6.1 为团队设置债务预算

到治理项目的后半程,我们开始思考一个更本质的问题:技术债可能永远无法清零,只要系统在演进、需求在变化,就会不断产生新的临时方案。所以比“消灭债务”更重要的是建立一套机制,让债务处于“有预算、有计划、可追踪”的状态。

我们引入了“债务预算”的概念:每个迭代允许团队登记一定数量的新增技术债,但每一笔新增债务都必须满足三个条件——必须说明原因,必须标注触发频率和影响范围,必须给出明确的偿还日期。如果你说不清这三条,这笔债就不允许以“技术债”的名义留在代码里,而是要以正常需求的形式立刻处理。

这个规则对团队习惯的改变很大。以前有人写了一个临时方案,心里想的是“下次有空再修”,但后面永远没空。现在“下次有空”变成了一个具体的日期,就像从“我尽量还钱”变成了“这个月月底前还清”。有些开发者因为不想给自己挖坑,开始自觉地在写临时方案的时候顺手做得稍微干净一点,新债务的数量明显下降。

6.2 把债务治理放进迭代节奏,而不是依赖热血

另一个把治理变成长期机制的关键,是把它排进固定的迭代节奏。我们团队的做法是每周固定预留半天,专门处理债务清单上的高优先项,这段雷打不动,不允许被业务需求挤占。这半天不安排琐碎的修修补补,而是由当周值班的开发认领一到两项债务,做完之后更新债务看板上的状态。

同时我们在团队的每周例会上增加一个固定环节:用五分钟快速过一遍债务看板,看新增了多少、偿还了多少、逾期了几笔。这个环节虽然只有五分钟,但作用很大——它让债务变得“可见”,任何人都能看出当前团队是在积累债务还是在偿还债务。逾期债务也会被自动标记出来,负责人需要在周会上解释原因和新的偿还时间。

这种机制运行了大概两个迭代之后,团队里形成了默契:债务不是某个人私底下背负的道德负担,而是整个团队共同管理的项目资产。大家会对新增债务说“没问题,但你得给他设个还账日期”,语气平淡得像是讨论预订会议室一样自然。

6.3 文化层面:允许说烂,但必须给出路

最后还想聊聊文化层面。技术债治理真正难的地方,不是流程设计得不够精妙,而是团队里能不能形成一种对“烂代码”的安全讨论氛围。在很多团队里,代码写得差是个敏感的指责对象,大家宁可装作看不见也不愿意在评审里指出问题,结果烂代码就在沉默中越积越多。

我们后来建立了一条不成文的规定:任何人可以说“这段代码设计不合理”,但同一句话后面一定要跟一个改进方向或修复建议,不然就是不负责的抱怨。这条规则既保护了写代码的人不被负能量淹没,也逼着发现问题的人深入思考问题本身,而不是停留在情绪层面。时间长了以后,大家把“发现坏味道”看成一种为团队节省未来成本的行为,而不是对某个人的攻击。

这其实是整个治理项目走到最后最有价值的部分。流程和工具都可以照搬,但如果没有一个允许正视问题的团队文化,再好的债务看板最后也只是一块没人维护的电子白板。

做个阶段性总结的话,我的体会是:技术债治理不是一次重写系统的机会,也不是一场代码洁癖的狂欢,它是在承认现实的前提下,让系统逐步恢复到可理解、可预测、可持续演进的状态。从146次迭代的混乱到债务地图上的79个红点,再到每周雷打不动的债务偿还日程,这条路我们走了大概半年。如果你所在的项目也正处在类似的阶段,别急着动手删代码,先从画一张债务地图开始,你会看到混乱背后其实有一张清晰的路径图。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询