跳到主要内容
没有生成伤疤的按钮

没有生成伤疤的按钮

天平
天平

· 阅读约 11 分钟

resilient 可以设计,battle-tested 只能经历。

这两个词被当成先后阶梯太久了——先把系统写成 resilient,上线跑一阵子,就自动升级成 battle-tested。这直觉从头就错。它们根本不在一条轴上:一个讲系统内部的结构选择,另一个记录系统跟真实世界交手之后留下的东西。前者看代码就能判断,后者必须看事故记录。把新系统叫 battle-tested,是我最近在招聘 JD 和 README 里见过最不诚实的用词。

我横评里反复说的一件事:不标条件的数据是裸数字。battle-tested 这词,眼下就是工程词汇里最大的裸数字。它不告诉你打过什么规模、见过哪类故障、挂了多久、怎么恢复,甚至不告诉你这些信息全都没有。它只给一种氛围——“我们很靠谱”。而这种氛围,生产环境从来不会免费发给你。

插一句题外话,但也不是真的题外。那位写文章区分这两个词的开发者,动笔之前在柬埔寨的丛林里穿着 Crocs 走完了 17 公里山路。回来之后,他对城市交通、邮件、截止日期的耐心降到了零。我提这个细节是因为,一个人刚在真实、不可控、脚下随时会滑的环境里待过两周,再回头看“演示里跑通了”意味着什么,那个心态是完全不一样的。丛林不会因为你穿错了鞋就对你友善,生产环境也不会因为你写了测试就保证不出那些没被写进测试的事。

什么叫用词不当?一个系统要配得上 battle-tested 这个词,最低限度得有这些记录:它在生产环境里真实失败过,有人定位了根因,修复上线了,留下了复盘文档、回归测试或监控告警规则。这些记录是伤疤。没有伤疤的系统,代码再漂亮,架构图再清晰,测试覆盖率再高,都没有资格用这个词。这不是苛责——能把系统设计成完整、可用、有韧性,本身就是一个该庆祝的里程碑。但那是另一个词该干的活。

有个维度表我一直想画,把它摊开:

判定条件resilientbattle-tested
功能按规格工作必要条件必要条件,但远远不够
处理了开发者预想到的错误路径是是,但“预想到”三个字本身就是局限
像样的测试覆盖可以有必须有,且包含回归测试,不是单测覆盖率
经历过真实流量否必须有时间窗口和流量形态
有失败记录和复盘文档否必须有,这是定义的一部分
监控和告警规则可以有必须是从某次事故里长出来的,不是配上去的
该写进 README 的词well-engineered,ready for early usersbattle-tested as of

这场区分来自一个具体场景:一个功能完整、测试全绿、demo 里怎么看都完成的新服务,该不该在对外文档里用 battle-tested 标榜自己?答案是不该。不是贬低它——把规格里写到的功能都实现、把能想到的错误路径都处理、正常使用不崩、测试覆盖到位,这已经是完整可用的系统,值得好好写进 README。但战争隐喻不能提前套。套上去,就把“看起来完成”和“真的打过仗”之间那条线抹掉了。那条线不是靠努力能跨过去的。它叫时间。

battle-tested 的字面意思比大多数人用的要重得多。系统在真实环境里失败过、被修好了、并把这次失败之外的东西留下来。复盘文档是一道疤,监控告警规则是一道疤,回归测试是一道疤。这些疤是时间戳,是“我们曾经在这里踩空过”的记录。一个新系统,无论多想拥有这种东西,都没有任何按钮能一键生成。你可以把别人的复盘文档抄一遍,把别人的回归测试搬进来,但那些疤不是你的。你的系统还没摔过。

被低估的从来不是“写完功能”那部分。是从韧性到伤疤之间那一段。那段路不是靠堆测试能走完的。

这张表左边是你设计阶段能覆盖的,右边只有生产环境会教你:

低预估维度resilient 阶段你看到什么battle-tested 系统才暴露什么
失败模式处理了预先设计过的错误依赖项不报错但返回畸形数据;两台服务器之间时钟偏移;流量高峰时队列积压放大上万倍;上游一条慢查询耗尽你的连接池
并发边界单请求逻辑正确支付方在毫秒内重发同一条 webhook,两个请求同时查到“尚未处理”,于是重复入账
可观测性基础日志和指标在高压下这些指标是否真的能告诉你系统的真实状态,还是只是活着
运维成熟度部署脚本能跑通回滚、灰度、容量评估、缺陷触达——这些操作层面的东西不会出现在设计文档里
伤疤无事故复盘、监控、回归测试——这是这个词的核心定义

并发那行我要展开。一个真实到让人后脊发凉的场景:支付服务在毫秒内对同一条 webhook 发两次请求,两个实例同时去查数据库,都看到“尚未处理”状态,于是各自入账一次。测试环境不会暴露这个,因为重发通常不会在毫秒级发生;预发环境也不会,除非有人专门做了故障注入。这个 bug 不只是“没写幂等键”——是幂等键的检查和写入之间没有原子性。哪怕代码评审时被提出来过,也大概率被当成“概率太低、先放一放”给放行。然后它在系统上线几个月后的某一天,在一个谁都没想到的时刻第一次出现。到那时候,它就不再是 bug 了——它变成了系统历史的一部分,一个需要用伤疤去标记的事件。

生产环境本质上是你测试套件的一部分。不是比喻。那些在 CI 里永远跑不出来的竞态、那些在预发环境不构造极端条件就永远不触发的边界,最终都会在生产环境里以事故的形式触发。然后你把它修掉、写进回归测试,你的测试套件才真正长大。battle-tested,说穿了,就是“已经让生产环境替我跑完了一部分只有它能跑的测试”。

还有一个反直觉的对比。代码可以久经战场却不具韧性:一个给内部用的小服务,低流量下跑了五年没事,因为没人攻击它、没人把它逼到上限。结果某天它依赖的一个外部 API 开始变慢,因为没有超时控制,整个服务线程被慢慢耗死。它有没有历史?有。它有没有韧性?没有。反过来,一个新写的服务,超时、重试、熔断、缓存降级全部配齐,但还没见过真实流量——它有韧性,但它没上过战场。这两个例子足够说明,resilient 和 battle-tested 之间连正相关都不一定保证,更别说当成先后阶梯。它们必须分开看:一个回答“它被设计成了什么样”,另一个回答“它实际经历过什么”。

再说一遍那个让我不太舒服的数字——不对,不是数字,是比例。评论区有人提,线上应用膨胀到最后,百万行代码里约有六成是补丁式改动。这个绝对数字我不验证,但方向我认。补丁是什么?是系统跟现实碰撞之后留下的修改痕迹。六成代码都写成补丁,说明“设计阶段能预测到的”其实只是完整系统的一小部分。这个比例本身就在说:最初的设计文档,哪怕写得多认真,也只是个起点,不是一个自洽完整的系统画卷。

演示环境下有一段断层,大部分人看不见。能展示的部分——功能跑通、UI 到位、测试绿了、监控面板酷炫——这些东西在脚手架成熟、AI 辅助把想法变成可运行应用的成本急剧下降的今天,越来越便宜。便宜到不能再当区分标准。真正贵的是那 80% 看不见的判断力:预判哪些设计会在八个月后腐坏;判断什么时候不应该自己造轮子;诚实地估算工期,而不是给个让人今天开心的数字;深夜里生产环境挂掉的时候,保持手不抖、定位不偏、回滚不误伤。这 80%,在 demo 里被有意无意地藏了起来,因为它没法被“展示”,只能被“经历”。

别误会。我不是说 AI 写的应用低人一等,更不觉得“真正的工程师就该一切手写”。那论调蠢透了。AI 把从想法到可运行应用的周期缩得前所未有地短,更多人能动手造东西,这是好事。问题从来不在工具。在人把工具的产出贴上一个需要时间去证明的标签。AI 辅助生成的完整应用完全可以 well-engineered——测试能加、错误路径能处理、规格能逐条验收。但它就是不能叫 battle-tested。不是它用了 AI,是它没有历史。手写的也一样。

生产环境不会等你准备好之后才开始教做人。它没有课程表,每次上课都不提前通知。那些设计阶段没被枚举到的边缘情况,不一定真的罕见——它们只是无法事先被穷举。出现的概率可能不低,但出现在你预测清单里的概率几乎为零。UnitBuilds 那句话说得很直接:边缘情况第一次出现,通常就是生产环境。这条我认同。所谓 edge case,不是“不会发生”,而是“在开发者的事前想象力之外”。它会在生产环境里以非边缘的方式出现,比如在流量高峰、被重试放大、或者跟你完全没料到的另一个模块发生交互。到那时候,你能依赖的只有两样:系统本身的韧性够不够撑住,以及你有没有在之前积累足够多的伤疤来快速定位。

如果你还在纠结该给你刚写完的那个服务写什么词,这是一张场景化建议表:

你的系统状态该用的词不该用的词
功能完成、测试覆盖到位、还没见过真实流量well-engineeredbattle-tested
有过几个真实用户、没出大事、但还没有事故记录ready for early usersproduction-grade
在特定流量形态下跑过、有失败记录、有复盘文档battle-tested as ofbattle-tested(裸用)
跑了很多年、没人敢动、也没有任何复盘文档遗留稳定系统,碰之前先补监控battle-tested

如果你真想用 battle-tested,至少学学那个英国开放银行 CTO 的提法:加上 as of,带时间窗口和流量形态。哪段时间、多大并发、见过哪些故障类别、恢复耗时多少、有没有需要人工介入。这样这个词才可证伪,过期了要更新。这比光秃秃一个“battle-tested”有用十倍,至少不会骗到下一个接手的人。

还有一条从评论区里捞出来的实践,是韧性到战场检验的过渡动作:发布之前,让开发者和内部同事、真实用户一起坐下来,专门集中一段时间一起过。不是 demo 展示,是带着真实数据一起点、一起输入、一起在尴尬的地方卡住。这比直接甩到生产环境要好,也比永远不出门的模拟要好。它不产生真正的伤疤,但能让那些“设计阶段没想到”的事情提前漏出来一部分。

说句公正补丁:我没在骂 resilient。能把系统写到完整、可用、有韧性,是实打实的工程功绩,写进 README 完全应该。但“战争”不是营销词。它指的是生产环境真的把你的系统打趴下过,你把它修好了,留下了记录。没有生成伤疤的按钮,没有任何架构设计能替代事故记录,没有工具能把时间压缩掉。这个结论跟版本号、大模型、新框架都没关系。只跟一件事有关:生产环境不会停止教做人,教过的每一课都留疤。你要用那个词,先确认你的系统摔过,而且你知道那些疤长在哪。

天平
天平

一个品类拉 N 个方案上秤:维度对比表、benchmark、按场景给选型建议。

查看主页 →