凌晨两点半,我被一通告警电话从床上拉起来。我给自己定的规矩是,这种时候先不开会,直接翻代码。t3code这个名字,就是在这种被线上故障反复折磨的夜晚里慢慢冒出来的。生产环境核心支付链路的成功率在十五分钟内掉了八个点,排障排到三点多,最后定位到一个三年前加的“应急脚本”上:它没有设置超时,依赖的上游接口刚好在升级,于是一层层把调用全部拖死。
类似的场景在我带团队的这几年里反复出现。代码量越大,真正给我“随时可能爆雷”感觉的,反而不是那些新开发的高复杂度功能,而是大量散落在仓库各处的野生代码——没有人愿意接手,没有人敢重构,每次上线前所有人都绕着它们走。我认真琢磨了一个问题:能不能像管理风险等级一样管理代码本身,让每一段代码从一开始就带着明确的级别标签,让团队一看到这个标签就知道该怎么对待它。我把这套方法叫作 t3code,也就是三级代码体系。
1. 一次凌晨事故,让我想给代码分个级
1.1 团队里那批让我睡不着觉的野生代码
先说那次事故的细节。告警显示支付成功率暴跌,第一反应查网关、查数据库、查容器资源,全都没问题。后来一点一点剥调用链,才发现三年前有人为了配合一次临时活动,往核心服务里塞了一个后台脚本,脚本每隔几秒就去拉一次上游接口的实时状态,用来刷新一份本地缓存。上游接口升级后响应时间从50毫秒变成了三秒,脚本里的HTTP客户端没有设置超时,连接全部挂起,最终把线程池耗尽。核心服务一卡,所有支付请求跟着排队。
这种代码有个共同特征:它们不是被设计进来的,而是被“临时凑”进来的。凑的时候没有人想过它会活这么久,更没有人给它配套超时、重试、降级和监控。等到发现问题时,它已经悄悄成为系统里最脆弱的环节。我复盘了整个团队的历史提交,发现类似的情况还有很多——核心交易链路上挂着改了一版的临时补丁、老模块里埋着一段没有任何日志的关键逻辑、新同学接手时看着一个几百行的函数完全不知道从哪下手。
1.2 为什么统一规范这条路走不通
一开始我的想法很简单:是不是团队缺少一份统一规范?于是我参考了不少团队的代码规范文档,把命名、格式、注释、错误码、日志、异常处理全都写了一遍,还让每个模块对照整改。结果可想而知——推不动。
原因不复杂。统一规范默认了一个前提:所有代码都应该用同一套最高标准来写。但现实是,一个仓库里的代码,生命周期、风险等级、变更频率差异极大。核心支付链路和一次性数据迁移脚本,本来就不该用同一套评审流程和质量要求。强行统一,要么标准定得低,核心代码得不到足够保护;要么标准定得高,任何人都觉得迁移脚本也要测试覆盖率90%是荒诞的,规范最后变成一纸空文。
规范只有一套,粒度太粗,管不住“该重管的”也推不动“该放行的”。我真正需要的不是一套规范,而是一套可以区隔不同代码类型的框架:让该慎重的代码有足够重的流程,让该轻快的代码能轻快流动,同时不让后者污染前者。
1.3 分级要解决的三件事:可预期、可接管、可演进
带着这个问题,我把“给代码分等级”的想法梳理成了三个明确目标。
第一是可预期。团队里任何人,只要看到一个模块被标成了T1,不需要读代码就能知道:这模块在核心调用链上,变更必须走完整评审,出错会触发告警,动手之前要有心理准备。看到T3就得知道:这是临时或边缘代码,随时可能被清理,不能在上面搭建关键功能。
第二是可接管。新同学或者转岗同事接手一块代码时,最高成本永远是“这代码到底能不能改、该从哪里看起”。有了级别,接手的人先看级别定义,再按级别去看对应的文档、测试和红线要求,而不是在一堆历史遗留里摸索。
第三是可演进。代码不是越稳定越好,而是该稳定的时候稳定,该流动的时候流动。有了级别,团队敢对T3代码大胆清理,对T2代码常规重构,对T1代码走重型变更流程。每一类代码都有明确的演进路径,而不是所有代码都躺在“能不动就不动”的泥潭里。
1.4 t3code这个命名是怎么来的
t3code中的“3”代表三个层级的代码分类,T代表Tier,也就是层级。这个名字故意压得很短,因为我在团队里推广时发现,概念一旦太长就没人愿意挂在嘴边。大家可以简单记成:T1是最重要的核心资产,T2是常规业务代码,T3是可以容忍的临时与边缘代码。后面所有规则,都围绕这三个档位展开。
2. t3code的三层定义与判定标准
2.1 T1代码:可以称之为“系统底座”的那部分
T1代码是团队赖以生存的核心资产。这类代码的特征非常明显:位于最关键的业务调用链上,被多个模块复用,一旦出问题直接影响用户可感知的功能或导致资损。
典型例子包括:支付核心流程、用户鉴权服务、订单状态机、基础中间件封装、统一配置中心等。这些代码对变更的容忍度极低,一个小改动可能引发全局故障。
所以T1代码的规则是“重”:变更必须经过完整的代码评审、必须有对应的单元测试和集成测试、必须走全量回归,核心逻辑还要求有人review边界条件和回滚方案。T1代码必须有owner,每个T1模块都必须有一个明确的责任人,不允许出现“代码活着但没人认领”的状态。
在判定上,我习惯先问自己三个问题:这行代码挂了,最坏结果是什么?它被多少个上游依赖?它能被自动替换或自动降级吗?如果答案分别是“很严重”“很多”“不能”,那它大概率是T1。
2.2 T2代码:日常业务逻辑的主力军
T2代码占了仓库里的大头。它有一定的重要性,但没有T1那么生死攸关;它也需要质量保障,但不需要T1那么重的流程。
典型例子包括:普通的业务接口、报表查询、后台管理功能、非核心的异步任务。这类代码挂了之后,会带来功能和体验上的问题,但通常不会直接击穿资金链路或核心服务。
T2代码的规则是“稳”:必须有基本的单元测试、调用方要处理好异常和超时、接口要有清晰入参出参、变更走常规评审即可。它不需要每个接口都做性能压测,也不需要大规模安全评审,但该有的质量底线不能丢。
我用一个简单的判断标准来区分T1和T2:如果这个模块挂了,用户在3到5分钟内会投诉吗?会投诉,可能涉及核心链路,按T1看;不会投诉或者投诉影响面很小,按T2管理。
2.3 T3代码:允许存在的临时与边缘代码
很多团队不愿意承认一个事实:仓库里必然存在一部分“不太正经”的代码。比如运营活动的一次性脚本、数据订正的临时任务、本地调试用的工具、某个还没验证完的试验性补丁。
这些代码不是说可以乱写,而是它们天然不适合套用T1/T2的重量级流程。如果把每个临时脚本都要求配文档、全测试、走评审,结果只有两个:要么流程被打破,要么正规代码里混进一堆影子。
T3代码的规则是“短”:允许存在,但必须有明确的生命周期。我定的标准是,T3代码最长存活周期不超过30天,到期后自动清理,或者申请升级为T2甚至T1。升级意味着补足对应的文档、测试和评审,而不是换一个标签就完事。
T3代码还有一个硬性要求:不能作为任何T1/T2模块的底层依赖。T1模块调用一个T3脚本,等于把一个按时炸弹放在底座下,这绝对不允许。
2.4 三档标准对照表
| 维度 | T1(核心资产) | T2(业务应用) | T3(临时/边缘) |
|---|---|---|---|
| 代码位置 | 核心调用链、底层公共能力 | 常规业务服务、管理后台 | 脚本、调试代码、一次性任务 |
| 变更流程 | 完整评审+全量回归+灰度 | 常规Review+单元测试 | 轻量检查+有效期限制 |
| 质量要求 | 高覆盖率、强文档、性能保障 | 基本测试、清晰接口 | 仅要求可读与可清理 |
| 风险容忍度 | 极低,出错需要告警与兜底 | 中等,允许降级处理 | 高,可随时下线 |
| 生命周期 | 长期稳定,持续维护 | 长期演进,常规维护 | 短期存在,到期转正或删除 |
| 责任归属 | 必须有明确owner | 建议有负责人 | 谁创建谁负责清理 |
这张表我打印出来贴在工位上盯了很久。它基本奠定了t3code后续所有工具化动作的基础——没有这张表之前,所有讨论都是“我觉得应该重视代码质量”,有了这张表之后,讨论变成了“这段代码到底属于哪一档,该用哪一列的要求去约束它”。
3. 把t3code落到错误码体系里
3.1 先说三句难听的话
代码分级的价值,如果只停留在文档和理念层面,其实是空的。我推动落地做的第一件事,就是改造团队的错误码体系。原因很朴素:错误码是每个开发每天都在打交道的东西,也是最能直观体现“代码层级”的载体。
但在动手之前,我要先泼三盆冷水。第一盆,多数团队的错误码是“野生的”,同一个错误今天叫500明天叫40003,客户端判断逻辑跟猜谜一样。第二盆,很多错误被静默吞掉了,catch block里写个log就空手返回,等问题爆发时日志里什么有效信息都没有。第三盆,错误码没有级别概念,核心链路和边缘脚本共用同一套错误编号,告警一响,所有人全量神经紧绷,最后发现只是一个后台任务常规失败。
3.2 一套可落地的三级错误码格式
我给团队定的错误码格式并不复杂,核心就一点:错误码必须自解释层级。
格式是这样的:
第一位:代码层级(A代表T1,B代表T2,C代表T3) 第二到四位:模块编号(001~999,每个模块申请一段) 第五到七位:具体错误序号(001~999,按错误类型递增)举例来说:
- A001001:支付模块核心服务调用超时,属于T1级别错误
- B010101:订单创建失败,属于T2级别业务错误
- C000001:某个数据订正脚本运行异常,属于T3级别错误
光看错误码的前缀,接收方就能判断该用什么策略来处理。A开头的错误必须立刻报警、必须有兜底动作;B开头的错误可以记录日志、尝试重试、给用户友好提示;C开头的错误只进日志中心,不触发告警打扰。
3.3 三种级别错误码的响应策略
级别不同,错误码的消费方式也应该完全不同。我见过最大的问题就是团队把所有错误码一视同仁:告警规则不区分级别,监控大盘不区分级别,就连值班人员处理优先级也不区分级别。结果核心错误和边缘错误混在一起,真正重要的告警常常被淹没。
T1错误码的响应策略是“报警+兜底+复盘”。报警必须全渠道触达,兜底动作必须提前设计好,比如降级到本地缓存、自动重试、切换备用通道。T1错误出现后,必须输一份复盘报告,根因没查清之前,问题不允许关闭。
T2错误码的策略是“记录+重试+提示”。记录结构化日志,方便排查;按业务规则决定是否自动重试;给用户返回可理解的提示语,而不是甩一个“系统开小差了”。
T3错误码的策略是“沉默+跟踪”。它只进日志系统,不触发告警,但需要按照T3代码的过期时间持续跟踪。如果T3错误在一个周期内反复出现,恰恰说明这段临时代码不能继续临时的状态,要么立刻转正,要么立刻清理。
3.4 落地时的代码示例
如果团队用的是Java,我会推荐用枚举统一管理错误码,把层级直接做成枚举字段。示例大概长这样:
public enum ErrorCode { CORE_PAY_TIMEOUT("A001001", "支付核心服务调用超时", Level.T1), ORDER_CREATE_FAILED("B010101", "订单创建失败,请稍后重试", Level.T2), FIX_SCRIPT_EXCEPTION("C000001", "数据订正脚本运行异常", Level.T3); private final String code; private final String message; private final Level level; ErrorCode(String code, String message, Level level) { this.code = code; this.message = message; this.level = level; } public String getCode() { return code; } public String getMessage() { return message; } public Level getLevel() { return level; } }使用的时候也很简单,业务侧直接引用枚举,避免手写字符串中文案。
if (paymentResponse == null || paymentResponse.isTimeout()) { throw new BizException(ErrorCode.CORE_PAY_TIMEOUT); }监控脚本在采集告警时,直接读ErrorCode中的level字段,决定是否触发告警通道。这样“错误码分级”就不需要靠人肉记忆,而是从源头就结构化了。我在团队里用了半年,最大的感受是:大家写错误码时的态度变了,因为每个人都清楚自己正在处理的是哪个级别的代码,错误码不再是随便填的字段,而是一种责任声明。
4. 用t3code拉动代码评审与质量门禁
4.1 分层准入标准:什么代码能进入什么级别
错误码体系跑通之后,下一个动作是把t3code嵌入开发流程本身。我踩过的教训是:如果分级只停留在“静态标签”,团队成员很快就忘了它的存在。必须有强制手段,让每一行新代码在进入某个级别之前,都通过对应的准入检查。
我定的准入标准很简单,写在评审规范里:
- T1代码准入:必须有设计文档,说明模块在系统中的定位、依赖关系、故障预案;必须列出性能指标,包括预期QPS和耗时上限;必须通过安全评审;必须指定owner。
- T2代码准入:必须有接口说明文档;必须有基本单元测试;必须在代码评审中确认异常处理和超时设置;建议指定负责人。
- T3代码准入:只需要在代码标记处写明有效期和创建人,并注明“到期后转正或删除”。
这个标准不追求完美,追求的是“可执行”。每一档都只卡最关键的几项,避免回到统一规范那种“什么都要求”的老路。
4.2 差异化的Review清单怎么写
代码评审是我最看重的落地环节。很多团队的Review看什么都顺眼,因为没有针对性的检查清单,评审全靠个人感觉。我按不同级别分别列了清单,评审人对照着打钩就行。
T1代码Review重点看五件事:并发安全、边界条件、回滚预案、监控告警是否齐备、是否引入了不必要依赖。这五项里任何一项出问题,评审直接打回。
T2代码Review重点看三件事:业务逻辑是否正确、接口是否清晰可维护、异常路径是否有处理。不追求面面俱到,但业务正确性不能放过。
T3代码Review只问一个问题:有效期写了没有,能不能按计划清理。如果T3代码连到期时间都没有,直接拒绝合并。
我试过让同一个评审人用不同清单去审不同级别的代码,效果比想象好很多。因为评审人不再需要“无差别地怀疑每一个地方”,只需要聚焦在级别对应的风险点上,评审效率上来了,大家也更愿意认真看。
4.3 CI门禁按级别动态配置
流程有了,还得让机器兜底。我们用的CI系统支持按变更目录执行不同任务,我把这个能力用了起来。
核心思路:用目录结构划分代码层级,T1的核心代码统一放在platform目录下,T2的业务代码放在service目录下,T3的临时代码放在scripts目录下。然后写一个简单的检查脚本,根据变更文件所在目录,决定执行哪一组门禁。
# run_level_checks.sh CHANGED_DIR=$(git diff --name-only HEAD~1 | cut -d/ -f1 | sort -u) if [[ "$CHANGED_DIR" == "platform" ]]; then echo "检测到T1代码变更,触发全量回归与覆盖率门禁" bash ./scripts/run_full_regression.sh exit $? fi if [[ "$CHANGED_DIR" == "scripts" ]]; then echo "检测到T3代码变更,检查有效期与清理计划" bash ./scripts/check_t3_expire.sh exit $? fi“platform”变更必须过关全量回归,“scripts”变更只查有效期,中间层走常规测试。这样的配置跑了大半年,没有一次核心模块漏过回归测试,也没有一个临时脚本无限期地活在仓库里。目录结构之所以能当武器,是因为它把抽象的分级定义具象成了可检查的文件路径。
4.4 定级判断的四个维度
新人最常问的问题是:“我怎么知道一段代码是T1还是T2?”我给他们的判断框架很简单,就四个维度。
维度一,影响面。代码被多少个模块依赖,是不是核心调用链路上的必经节点。影响面越大,级别越高。
维度二,复用度。是被多个业务场景复用的通用能力,还是一次性的特定实现。复用度越高,越值得用T1标准保护。
维度三,失败代价。代码出错后会造成什么影响,是画面不好看,还是交易失败,还是资损。失败代价越重,级别越高。
维度四,兜底能力。系统是否有自动降级、自动重试、人工紧急介入的机制。兜底能力越弱,代码越要慎重对待。
前三个维度从需求和架构出发,第四个维度从运维现实出发。这四者综合下来,我还没遇到一个模块定级明显不准的情况。
5. 落地t3code最容易翻车的四个坑
5.1 坑一:分级变成了贴标签
我第一次在团队推t3code时,犯过最典型的错误:花了两天时间给仓库里所有代码标注级别,但除了注释之外什么都没变。Review还是那套流程,CI还是那条流水线,告警还是那个报警规则。结果不到一个月,那些T1、T2、T3的标记成了纯粹的噪音。
教训很直接:分级必须绑定强制动作。如果标注了T1却没有对应的评审规则和回归要求,这个标注就是无效的。后来我强制要求,每标一个T1模块,必须同时配置好它的owner、错误码前缀和CI检查规则。做不到这三件事,就不允许标T1。
5.2 坑二:对存量代码搞一刀切
给存量代码定级时,团队里出现了两种极端意见。一种说“所有线上代码都很重要,统统标T1”,另一种说“老代码都是历史遗留,统统标T3”。这两条路我都走过,都失败了。
全部标T1的结果是Review流程彻底堵死,任何老代码改动都触发全量回归,团队干脆不动老代码了,用各种“绕过手段”进行修改。全部标T3的结果更危险,做底层服务的老模块被当成边缘代码随意改,上线出了事又怪分级制度不合理。
正确的做法是“按调用链影响面逐模块认定”,并且给过渡期。短时间内无法确认的模块,先标记为“TBD待定”,在30天内由主程和owner逐个梳理。存量代码定级用渐进式,不要指望一天清完。
5.3 坑三:文档变成了第二套代码
把T1代码门槛抬得过高,会带来一个新问题:文档膨胀。我见过一个项目,核心模块配套了四十多页设计文档,覆盖了所有历史决策、演进过程、遗留问题。看起来很完备,实际上没人维护,代码改了十几轮,文档还停留在两年前的架构图里。文档最终成了谣言来源,新人照着文档理解代码,误入歧途。
后来我把T1文档砍成了两部分:契约和红线。契约包括对外接口、数据格式、故障预案;红线包括不允许做什么、不允许依赖什么。其余内容全部下沉到代码注释里。T3代码干脆不写外部文档,只要求在注释里注明有效期和创建目的。文档不是越多越好,文档的价值在于精准对齐,而不是独立于代码的第二套真相。
5.4 坑四:定级之后没有owner
有一段时间,我们的T1模块虽然在目录上分好了,在CI上配好了门禁,却没有指定具体的负责人。这就出现了一个尴尬场景:核心模块出了告警,值班人到处找人问“这个模块现在谁在管”,问了半天发现原负责的同事半年之前就转岗了。
T1和T2必须落实owner机制。不要求单模块单人负责,但必须有名有姓的人为这个模块的质量、变更和故障响应兜底。owner变更时,要有一个正式的交接动作,至少在代码仓库的OWNERS文件里更新责任人。没有owner的T1模块,在系统上应当被视为“不合规状态”,不允许进行高风险变更。
这四个坑,我每个都亲身踩过。踩完之后最深的感受是:任何管理机制的价值,都不在于概念多精致,而在于它能不能让团队在具体动作中感受到约束和保护。约束是给代码的,保护其实是给人的。
最后再分享一点个人体会。t3code真正改变我们团队的,不是仓库里多了几个标签,也不是错误码从四位变成了七位,而是大家讨论代码的方式变了。以前评审时最常听到的问题是“这样写行不行”,现在大家会先问一句“这行代码属于什么级别,我们该用哪套标准要求它”。有了级别才有边界,有了边界才有一致行动。
如果你也被野生代码折磨过,我不建议一上来就做代码大扫除,那太容易半途而废。先花一个下午把代码分成T1、T2、T3三层,再给错误码加上一级标识,让这两个小改动先跑起来。规则立住之后,自动化工具和流程建设只是顺着杆子往上爬的事情。