如果你第一次听到“恭介的法则(rule)”这个名字大概率会以为它是某个动漫角色的隐藏设定或者某本书的章节名。实际上它既不是动漫梗也不是什么高深理论而是我们项目组内部流传的一套工作规则集合。这套规则从哪来名字是哪位同事起的已经没人说得清了大家只知道只要遇到某些反复出现的坑就有人搬出相应的法则来“说事”。久而久之它们被统一收进文档成了新人入职必读、评审时刻必提的内部公约。这套公约到底管什么一句话限制团队里那些反复出现的低级失误。它覆盖编码细节、系统设计、线上排障、协作流程四个面每条规则都可以表达成“遇到 X优先做 Y不要做 Z”的条件式背后基本都挂着一个真实的事故或返工案例。它适合给谁看如果你正在带一个不大不小的研发团队如果你不希望自己写的代码总在评审里被挑来挑去也好奇一套“经验”如何能变成“规则”这篇内容应该值得你花十分钟。1. 法则的由来与全局思路1.1 名字的真相一套内部“过滤器”而非教条先说清楚一个误区恭介的法则(rule)里面说的每一条都不是金科玉律。把它想象成安检口的液态物品限制——它不是说所有液体都危险而是说在登机这个特定场景下超过 100ml 的液体需要单独处理。同理我们内部每条法则也都有明确的适用场景它是一个“过滤器”帮你在具体场景里快速决定怎么走。比如我们有一条规则叫“业务代码里不允许出现 sleep”这当然不是说线程休眠这件事本身有罪而是说在业务请求线程里睡上一秒去等异步结果很容易把线程池占满进而拖垮整个服务。到了测试代码里为了模拟时序问题sleep 又是被允许的。如果没有场景限制这种规则列出来只会被骂成教条。这也是为什么我们管这一套叫“法则”而不是“最佳实践”。最佳实践听起来太宏大每个人理解都可能不一样而法则是一个动作指示讲究的是直接可执行。你不需要先理解三遍再决定遵不遵守看到条件照着动作走就是。1.2 一条法则怎么诞生复盘-提炼-固化这组规则不是某一天被某个人一口气设计出来的。它的流动路径和大多数团队内部方法论的进化完全一样。第一步事故复盘。每次线上故障、代码回滚、无效返工结束后我们都会问一个问题如果再遇到同类情况我们希望当时的自己提前知道什么第二步把这个答案改写成 if-then 句式并且附上背景案例。第三步放到周会评审检查清单里让规则进入日常动作。第四步等规则被反复使用三个月以上才正式进入文档成为正式版本的恭介的法则。举个例子。有一次配置中心改了一个默认值结果下游大量服务在没显式配置的情况下行为全变差点酿成故障。复盘结论是配置默认值的变更必须双人复核。于是诞生了一条规则“任何共享配置项的默认值修改需要至少两人确认影响范围。”你看它不是“注意配置安全”这种空话而是直接给出了动作和人数。这里的关键是表述质量。失败的规则往往死于模棱两可。“要保证代码质量”“注意测试覆盖”这类话每个人都觉得自己在做但执行起来完全无法验收。真正可执行的规则应该是“新增方法超过 40 行必须拆分”“没有用例的异常处理分支必须说明理由”。这种规则有明确的边界可以争论可以检查可以违反后拉出来复盘。1.3 为什么必须把经验固化成文字人的记忆是有限的团队越大经验扩散得越慢。如果一切靠传帮带新来的同学就得踩一遍前辈踩过的所有坑这既不现实也不公平。把经验固化成文字规则核心价值是把“个人发现”变成“团队共识”。新人入职读完这套文档他对团队做事方式的了解基本就超过了部分老员工。当然规则多了也会成为负担所以我们内部有个硬性约束法则总数控制在 15 条以内。一旦超过就合并同类项或者干脆把规则产品化做成 CI 检查、代码模板、自动化工具的一部分。规则死在文档里不可怕可怕的是文档里躺着一堆没人执行还占着脑容量的规则。2. 编码与设计层面的核心法则2.1 最小改动法则修 bug 时不顺手重构先讲一个让我印象深刻的事故。某同学接到一个需求把一个接口返回的字段类型从字符串改成整数。改动本来很小但他看到相关工具函数里有一段 List 转 Map 的逻辑写得很别扭顺手就重构了。结果上线后某个下游页面大面积空白。定位到最后发现是重构时空值处理策略和原来不一样。那个页面依赖的接口字段确实修好了但工具函数的隐式行为被改变了。所以最小改动法则的第一条表述是修复一个问题时diff 里只允许出现与根因直接相关的行。任何重构性质的修改哪怕是顺手删一行都必须单独开分支、单独提合并请求。实际执行时最容易吵起来的是“到底算不算相关”。我们后来给了三个判断标准用起来顺了很多。第一这个改动是否直接阻断复现路径第二这个改动是否会触碰同一个状态变量第三这个改动是否会改变外部可见行为三个问题都是否那这个改动就属于无关修改应该拿出去。典型例子修空指针时把旁边的变量命名改掉这三条都不满足不应该出现在同一个 diff 里。这条法则最大的阻力不是技术是人的代码洁癖。很多人看到烂代码就手痒。我的建议是把重构欲望写进 TODO放到项目看板里单独排期。一次改动只做一件事回归测试才可能有边界。2.2 显式优于隐式消灭裸奔的魔法值如果说最小改动法则管的是“一次改多少”那显式优于隐式法则管的则是“别人读你代码时需要猜多少”。我们吃过一个真实的亏。订单系统里有一个状态字段线上告警显示 order_status3值班同学翻遍接口文档又去问两个人最后才确认“3”代表“冻结中”。问题根源是不同模块里各写各的数字字面量有的用 3有的用“FROZEN”的字符串有的用英文单词根本没有统一枚举。于是规则原文变成跨模块共享的数值、字符串、状态码必须定义成常量或枚举并且注明业务含义和来源任何隐式依赖比如“必须先调用 A 再调用 B”这样的时序约定必须写进注释或配置不能只存在于老同事的脑子里。我们自己扫垃圾字面量时常用的命令很简单grep -rnE [0-9] src/main/java然后逐个判断这些数字是不是业务状态。如果你发现同一个 3 在项目里同时表示“冻结中”和“已删除”别犹豫这就是技术债应该尽快改成枚举。还要注意一个度显式不意味着把所有局部循环变量都定义成常量。for 循环里的 i、下标、临时的百分比这种局部上下文很清楚的值没必要过度设计。显式原则针对的是那些跨越模块边界、跨越服务边界的约定因为在那些边界地方上下文信息最容易丢失。这条法则延伸到配置领域就是禁止隐式默认值。任何一个被读取的配置项都必须在配置中心里显式声明哪怕它的值就是默认值。否则你无法知道有多少服务在依赖“没有配置时的行为”改默认值就是一场灾难。2.3 快速失败法则让问题大声喊出来吞异常这种事每个成熟团队都经历过。有一次一个订单回调处理某位开发在 catch 到异常之后为了“不打扰调用方”返回了一个成功状态。结果上游认为处理成功不再重试订单卡在“处理中”整整一天。最后靠手工脚本修复数据一整天都在救火。快速失败法则的表述是当程序进入不应该出现的分支立刻记录带上下文的错误日志并且让调用方感知失败。禁止吞异常禁止返回伪造的成功值禁止为了调用链好看而静默返回空集合。try: refund_result refund_api.create(order_id) if refund_result.status ! SUCCESS: raise RefundFailed(order_id, refund_result.code) except Exception: logger.error(refund failed, extra{order_id: order_id, stage: create}) raise这段代码有几个细节值得注意。第一日志里带了 order_id 和 stage线上排查时直接能捞到这一条而不是只有一句“Exception occurred”。第二异常重新抛出去让调用方决定怎么处理而不是在本层假装成功。第三对于“可恢复”的场景也必须是显式恢复比如明确写“失败后标记订单为需人工介入并发送告警”而不是 catch 后 return null。很多人担心快速失败会导致用户体验变差这是误解。快速失败针对的是内部调用链和后台任务面向用户的环节当然可以兜底但兜底动作必须有日志、有监控、有告警。沉默的兜底等于没有兜底。3. 架构与系统层面的取舍法则3.1 三份数据法则关键数据必须能回到昨天很多团队以为有了主从复制就是高可用其实不是。主从复制防的是单机故障但防不住同一个版本 bug、同一个机架停电、同一次误操作。我们内部有一条三份数据法则用户资产、订单、核心配置三类数据至少有三个互相独立的副本存在而且其中至少一个副本支持按时间点恢复。这里的“三份”不是指同一个数据库里建三个实例。脱离故障域的多副本才有意义。比如一份是生产主从库一份是每日逻辑备份到对象存储第三份是跨区域的归档同步。日常只用前两份第三份是最后的安全网。真正让我意识到备份不是目的的是一次恢复演练。我们模拟“误删一张订单明细表”的场景结果发现每日逻辑备份因为磁盘满静默失败了三天而跨区域归档因为只保留最近 7 天正好把要恢复的时段覆盖住了勉强顶住。从那次之后备份的恢复流程被改成按季度演练并且每次演练都必须记录恢复耗时和失败点。副本类型存放位置恢复方式演练频率生产主从机房 A高可用切换按发布演练逻辑备份对象存储全库/单表恢复每季度跨区域归档异地按时间点回放每半年还有一个经常被忽略的点备份要支持单表恢复和误删恢复。如果只能全库恢复一条 delete 语句就得把整个业务回滚到昨天影响范围会被无限放大。3.2 依赖最少化法则加一个库之前先回答三个问题依赖管理是架构治理里最容易被忽视的一环。很多人看到一个好用的开源库就想引入却忘了每个新依赖都会带来四笔账升级维护、漏洞监控、构建体积、兼容性测试。长期看这笔账一点不比自研便宜。所以依赖最少化法则规定为项目引入新的外部依赖前必须回答三个问题。第一这个功能能不能用语言内置能力或者现有依赖实现第二这个库社区还活跃吗所谓活跃不是下载量大而是近一年内有没有版本发布、issue 有没有人回应、关键维护者有没有跑路迹象。第三如果这个库停更或者爆出高危漏洞团队能不能在两周内自研替代三个问题只要有一个过不去就要认真斟酌。我们犯过的错误是为了解析某个特殊格式的字段引入了一个只有几百行的小工具库。挺方便但半年后库被爆出个漏洞所有人都没听说过这库只好临时升级、回归十几个接口折腾一周。其实标准库加上一个正则和两个私有方法就解决了。成熟的团队一般会维护一份依赖清单记录每个外部依赖的用途、版本、上次升级时间、负责人。每季度过一遍。依赖不是越多越强而是越少越稳。少一个依赖就是少一块木桶的短板。3.3 接口收敛法则第一位调用方出现之前不抽象写代码时最怕的不是没有设计而是设计得太早。接口收敛法则的表述很简单没有第二个真实调用方之前不做泛化抽象。很多同学喜欢在第一个需求时就把接口设计得很“优雅”抽象出所谓的“渠道接口”“策略模型”“扩展点”理由是未来可能要支持更多渠道、更多类型。但现实里想象中的未来和真实的未来几乎总是对不上。过度抽象带来的结果是后续代码不得不围着错误模型打转最后用一堆适配器硬凑。我印象很深的例子是通知模块。有同学提前设计了一个“渠道抽象层”抽象出短信、邮件、App 推送的统一接口。结果真实业务接入的是一种内部办公软件推送字段含义完全不一样抽象层反而成了障碍。后来重构时发现最早抽象出来的概念没有一条字段能直接复用。当然不提前抽象不等于不做分层。分层是水平能力划分比如把订单业务和订单存储分开这是必须的。抽象是垂直需求建模是给一类未来可能出现的需求先搭框架。这两者不能混为一谈。判断起来也简单数一下当前真实调用方只有一个就返回具体类型等到第二个、第三个调用方出现并且需求开始分歧那时候再提取公共接口成本并不高而且提取出来的抽象才经得起考验。4. 排障与问题定位的实战法则4.1 复现优先法则先还原现场再谈修复线上故障面前最大的诱惑是凭经验立刻下判断。但经验太丰富的人有时候反而会翻车因为他见过的相似场景太多容易把新故障套进旧模板。我们有一条复现优先法则遇到线上故障先收集完整现场并尝试复现无法复现时不得直接提交修复代码。第一次听到这条规则的人会觉得太保守了故障都发生了还要等复现黄花菜都凉了。但真按这条走救场效率反而高。为什么会这样因为不能复现的 bug你所谓的“修复”很可能修的是影子。我们有一个真实案例一个支付回调偶发延迟开发凭直觉认定是队列积压直接加了消费者线程数。结果线程多了以后数据库压力上升把偶发延迟变成了大面积超时。最后查出来根因根本不是队列而是上游回调超时时间配置太短。如果当时先复现至少能先看到问题的完整链路不会往错误方向加资源。实际操作时我们要求一线处理人先填一张“复现卡”环境信息、触发步骤、预期行为、实际行为、关键日志。填完这张卡问题往往就已经缩小了一半。如果填不出来说明现场信息收集不够要继续采集请求 ID、时间戳、日志片段、线程栈、配置快照一个都不能少。如果实在复现不了就进入现场增强模式在疑似路径上增加上下文日志保留线程 dump打开全链路追踪。等再次触发时拿到完整链路再做判断。慢是慢一点但方向不会错。4.2 二分定位法则用排除法代替猜谜排障最怕的不是难而是各执一词。你怀疑缓存他怀疑数据库另一个同事说肯定是发布脚本的问题会议开着开着就变成辩论赛。这时候没有比二分定位法则更实用的工具了。二分定位法则的核心每个验证动作都要直奔“将可能性减半”的目标而不是凭喜好深挖某一条链路。最典型的两个应用场景一个是代码版本层面的 git 二分一个是线上流量层面的灰度对比。git bisect start git bisect bad git bisect good v2.3.1上面这个命令不太好懂但意思其实就是标记当前版本是坏的标记之前某个版本是好的然后 git 会自动 checkout 中间版本。你在中间版本上跑一次问题验证告诉它是好是坏它就会不断把搜索范围缩小一半。几百个提交的区间五六轮就能定位到引入问题的那个提交。线上故障也可以用同样的思路。发布后功能异常但不确定是不是新版本引入的可以把 10% 的流量切到老版本上对比错误率或者按时间窗口二分确认问题是不是从某个发布点之后开始出现。这类动作不需要理解整个系统只需要一个明确的可观测指标比如接口错误率、P99 耗时、订单失败数。我的体会是二分法的价值不只是快它让讨论进入同一个框架。每次结果出来就杀掉一批被排除的可能性争论自然就停了。4.3 日志先行法则发布前先想清楚怎么排障有一句值得贴墙的话没有日志的代码等于上线后开盲盒。日志先行法则说的就是每个对外接口、异步任务、定时任务在开发阶段就要规划好结构化日志发布前必须检查日志能不能支撑排障。结构化日志不是随手 println它至少应该包含这几个字段时间戳、级别、trace_id、服务名、方法名、业务主键、耗时、消息。放到一起大概是这样的{time:2025-06-01T10:00:00.123Z,level:INFO,trace_id:abc123,service:order-service,method:createOrder,order_id:ORD20250601001,cost_ms:86,msg:order created}trace_id 尤其重要。分布式系统里一个请求会经过多个服务如果没有 trace_id你就只能靠时间戳和关键字在好几台机器的日志里大海捞针。有了它一次请求的所有日志可以被串起来链路一目了然。日志级别的心得也顺便说一下。info 记录业务结果debug 记录输入参数和中间变量warn 记录可恢复的意外error 只记录真正需要人工介入的错误。最常见的问题是有人把 error 当成垃圾桶在 catch 块里把整个请求对象打印出来。大对象打印不仅拖慢速度还可能把敏感信息写进日志。正确做法是只打印关键 ID 和阶段标记。反例我也见过不少最典型的是老系统里一行 System.out.println(e)。打印出来的堆栈既没有业务上下文也没有请求 ID线上出了问题排查全靠猜。所以日志先行法则本质上不是日志技术问题而是开发习惯问题。5. 协作与流程法则5.1 评审四问法则把代码评审变成决策评审代码评审经常走偏要么变成找变量命名的文字游戏要么变成内行看门道外行看热闹。后来我们把评审重心收敛到四个问题每个合并请求描述里都必须回答。时间久了评审质量明显上了一个台阶。第一个问题这个变更解决什么问题要求作者一句话说清楚目标说不清说明自己都没想明白。第二个问题会影响哪些已有功能、接口或数据这个问题的价值在边界意识逼着作者去梳理上下游。第三个问题怎么验证这个变更是正确的最让人头疼的答案就是“手工自测通过”这里要求写清楚用什么数据、调哪个接口、预期什么输出。第四个问题如果出问题怎么回滚或止血有些变更根本没法回滚比如数据迁移那就要提前想好止血方案。一个很深的感触是如果每个人在开发前就过一遍这四个问题相当一部分事故在开始之前就能被消灭。曾经有一个字段类型变更开发本来打算直接把整数字段改成字符串结果第二问让他发现这个字段被十几个下游消费者引用于是加了新字段做兼容避免了一场联调事故。实操上我们把四问做成了 PR 模板开启合并请求时必须填写不填不让提交。工具强制比人提醒可靠得多。5.2 估算翻倍法则把不确定性说在明面上开发估算是一门玄学但我后来发现真正的问题不是技术估算本身而是大家习惯性地只算“编码时间”把联调、自测、文档、环境等待、意外返工全部排除在外。估算翻倍法则的表述是估时 纯编码时间 自测时间 联调时间 文档时间 20% 机动对外承诺再上浮 50%。很多人一听上浮 50% 就觉得在偷懒其实不是。你可以把它理解成技术账算清楚协作账另算。举个典型例子“改一个接口字段”这种任务纯编码确实只要两小时。但上游接口还没部署测试环境被别的项目占用下游联调需要对方配合加上自测和文档一天就没了。这些不写进估算最后必然变成加班和延期。我们给估时加了一个检查清单依赖方排期明确了吗测试数据准备好了吗升级会不会动数据库文档需要更新哪些页面每多一个“没准备”估时就多一段缓冲。这套方法未必能让估算变准但至少能让风险暴露在开工之前。向上汇报的时候老练的负责人其实更信任能说出风险的估算。真正损害信任的不是估多了而是承诺了却不断延期。5.3 结论留痕法则讨论五分钟以上必须有文字结论口头讨论最占便宜因为说过的话没有成本。但问题在于三周后对质的时候每个人都按对自己有利的方式回忆。我们为了避免这种内耗定了一条结论留痕法则任何会议、评审或技术讨论结束之前必须有一个人输出文字结论否则这个讨论等于没发生。结论不需要长篇纪要只需要一行决策快照讨论背景、选项、最终决定、理由、责任人和日期。格式大致是这样背景选项决定理由负责人日期订单回调偶发失败A 重试 B 告警 C 忽略B避免静默丢失某同学2025-06-01有过一次教训方案评审会开了一个多小时讨论了老接口新参数是否兼容最后没人写结论。三周后上线时两个同事对当初的共识理解完全相反硬生生返工了一周。从那以后会议结束前主持人必须在白板或文档里当场写下结论散会的前提是结论已经存在。有些人觉得写结论浪费时间但对比一下返工成本这点时间简直太划算了。讨论的价值在于收敛收敛的最后一步就是留下文字。6. 常见问题与排查技巧实录6.1 法则执行不下去怎么办任何方法论都可能死在执行力上。我们内部最常见的情况是一条规则写了开会强调了过两周就没人提了。后来总结出来的解法是把规则嵌入工具流而不是只靠人记。评审四问做成了 PR 模板CI 强制检查配置默认值双人复核做成了发布工单里的必填项备份恢复演练直接排进值班日历。当规则变成流程的一部分执行力就自动有了。另一个关键是责任人机制。每条法则至少要有一个“捍卫者”他的职责是在评审和事故复盘中主动对照规则指出违反情况并定期检查这条规则还活不活。没有主人的规则早晚会变成文档里的一段装饰。6.2 法则之间发生冲突怎么办规则多了以后一定会打架。最典型的是最小改动法则和后续的功能迭代你说好不提重构但新功能必须改同一个模块到底能不能顺手把结构理一理我们的处理原则有三个优先级。第一优先是保证本次变更可验证、可回滚、影响可控。第二优先是技术债要显式记录不许偷偷摸摸改。第三优先才是追求代码美感。如果一次改动里既包含需求变更又包含重构必须拆成两个提交或者两个分支重构单独走评审。换句话说最小改动法则管的不是“不能改”而是“一次只改一件事”。法则之间出现矛盾说明场景发生了变化。这时不要硬套规则回到第一原则减少风险保留可验证性。规则是为了降低决策成本不是替你做判断。6.3 排障场景速查表最后放一张我们内部排障时经常对照的速查表。它不是标准答案但大部分常见故障都能从里面找到第一步动作。症状优先动作对应法则发布后接口变慢拿 trace_id 查全链路耗时分布日志先行/二分定位数据不一致先对时间线和日志快照不要急着改代码复现优先偶发失败无规律增强现场日志保留 dump 和堆栈复现优先依赖服务超时先看上游错误码和超时配置显式优于隐式备份恢复不出来查备份任务日志并马上做恢复演练三份数据功能发布后异常先判断是否回滚再查代码变更评审四问/最小改动这张表的目的不是让你照抄而是想说明一个习惯排障的第一步不是开始敲命令而是先选择一个能把可能性减半的动作。想清楚再动手往往比手快更重要。6.4 定期清理法则让规则保持精简很多人以为方法论越多越好但规则太多以后记忆成本会超过收益。我们每季度会做一次规则盘点标准很直接如果一条规则在连续两个季度内没有被任何人引用过也没出现过违反案例它要么已经内化成默认动作要么已经被人遗忘。前者归档后者决定重新强调还是直接删除。每次盘点都会删掉几条过时的规则也可能新增一两条从最近事故里提炼出来的教训。规则总数始终保持在一个团队能不看文档背出来的程度大约是十条上下。这个体量刚好能形成共同语言开会时有人提一句“这不违反评审四问吗”所有人才会立刻反应过来。我自己在这套规则上断断续续用了三四年最大的体会是规则真正起作用的方式不是约束而是减少重复决策。别人踩过的坑你用条件反射绕开别人吵过的话题你用一句话收场。团队的合作成本就是被这些细小的确定性一点点省下来的。如果你也想在自己的团队里攒一套类似的规则我的建议是别急着从“应该怎么做”开始而是先从“最近一次踩坑的教训是什么”开始。同一个类型的坑出现两次再把它写成规则。每一条能活下来的规则背后一定要有一个值得记住的故事。没有故事支撑的规则连个愿意捍卫它的人都不会存在。