早会分享 · 工程思维入门 · 2026-09-07

跑通不算完

写代码这件事,学会「让它跑起来」大概要几个月;学会「让它在时间里、在别人手上、在你不看着的时候仍然是对的」,要几年。后面这段,就是工程思维。

本文为公开版,具体标识已替换为占位符:团队成员以角色代称,内部仓名与 PR 编号已隐去。
前半部分是可以直接借用的成熟做法(Google / Linux / SQLite / Rust / Netflix / Stripe / Etsy),后半部分是它们在我们自己项目里的样子。

Hyrum 定律 不破坏用户空间 Beyoncé 规则 选无聊的技术 Chaos Monkey Crater 幂等键
590×
SQLite 的测试代码量 ÷ 库代码量
92,053.1 KSLOC 对 155.8 KSLOC
外部 · sqlite.org/testing.html · v3.42.0
100%
SQLite 核心库的分支覆盖率
(含 MC/DC 判定条件覆盖)
外部 · TH3 测试工具
269
我们一条数据采集任务绿灯亮着的一周多里
队列中始终没被处理的条目
我们 · 数据采集管线
30 → 3
同一个成本闸
三处配置都显示 30,机器实跑 3
我们 · 数据采集管线
今天只讲一件事

把「我以为它在工作」,换成「我能指出它此刻正在产出的那一行证据」。

01
Programming vs. software engineering

先把「编程」和「工程」分开

Google 那本《Software Engineering at Google》开篇给了一个定义,值得逐字记住:「Software engineering is programming integrated over time.」——软件工程,是编程对时间的积分。编程是产生新代码;工程是让这些代码在时间流过之后依然成立。

初级和资深的分水岭

初级开发者交付的是「我写完了」:功能能演示,测试跑过,PR 提了。资深开发者交付的是「它好了」:我知道它在什么条件下会坏、坏了谁会先发现、发现之后从哪一行日志开始查、以及为什么我确信它现在真的在产出。

这两者的距离,不是聪明程度,是把一个问题问到底的次数。下面这三个轴,就是那三个必须问下去的方向。

Axis 01时间

我今天写对的这行代码,三个月后、依赖升级过、旁边被别人改过之后,还对吗?那时候我可能已经忘了它为什么这么写。

编程在这里结束 工程要负责的是这一整段 →
Axis 02别人

我这个函数、这个接口、这个目录结构,下一个人不问我就能看懂并且不会用错吗?我依赖的那个库,半年后还有人维护吗?

「我自己知道就行」 工程要负责的是这一整段 →
Axis 03看不见

它在凌晨两点、在我睡觉的时候、在没人盯着的机器上跑,坏了会有人知道吗?还是要等到一个月后有人问「怎么数据不对」?

本地跑通了 工程要负责的是这一整段 →
02
Nine practices, three axes

别人已经踩完的九条,按三个轴排好

这九条全部来自公开可查的工程实践,不是我编的口号。每一条都配了它在我们仓库里对应的那件事——你会发现我们踩的坑,人家十几年前就命名过了。

Axis 01

时间 —— 代码活得比你的记忆长

这三条讲的都是同一件事:你以为的「临时」「内部」「没人用」,时间会把它变成契约。

Google · SWE Book

Hyrum 定律

"With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody."

用的人一多,你写在文档里的承诺就不重要了——系统所有可被观察到的行为,都会有人依赖。返回顺序、报错文案、甚至响应快慢。

我们身上一个自动化脚本里写死的几个预设参数是"内部实现细节",但换到真实运行环境里它就是契约——环境布局一变,头两个参数就落到了系统保留的区域里。

Linux Kernel · Linus

不破坏用户空间

"We don't break userspace."

内核最高铁律:无论你的新写法多正确,只要它让已有的用户程序不能跑,就是你的 bug,不是用户的。回归被当成比设计缺陷更严重的错误。

我们身上有一次改动里,skill 侧单测本来就有 7 failures / 21 errors(本机 ffmpeg 版本问题)。他没修也没装看不见,而是逐条比对失败集合与改动前完全一致,把这句话写进提交信息。这才叫回归通过。

Chesterton · 经典比喻

切斯特顿的栅栏

"Go away and think. Then when you can come back and tell me that you do see the use of it, I may allow you to destroy it."

路中间有道栅栏,你看不出它有什么用。正确做法不是拆掉,是先去搞清楚当初为什么有人立它——说得出来,才有资格拆。

我们身上那个盖掉成本闸的同名 secret,就是"某次标定留下的"——留的人当时有理由,但理由没写下来,一年后它变成了一个纯粹的地雷。栅栏要么拆掉,要么写清楚为什么留。

Axis 02

别人 —— 你不在场的时候,代码要自己说话

这三条讲的是把「靠人记得」换成「靠机制保证」,以及不要给团队制造不必要的新知识负担。

Google · Beyoncé Rule

Beyoncé 规则

"If you liked it, you should have put a CI test on it."

基础设施改动把你的东西搞坏了,而 CI 里没有一条测试能抓到——那不算基础设施团队的责任。你在乎什么,就给什么写一条 CI 测试。口头叮嘱不是保护。

我们身上「要发出去的包只从已合并的提交打」写进记忆本靠人记得,第二次又踩,当天改成 release-guard.mjs 在两个打包脚本里拦(我们某个客户端的打包流程)。

Etsy · Dan McKinley

选无聊的技术

"Choose boring technology. You have a limited number of innovation tokens."

每个团队的「创新额度」只有大约三个,花在哪就没了。用一个你已经知道它会怎么坏的旧技术,长期成本远低于一个漂亮但没人摸清脾气的新技术。

我们身上判断标准很简单:这个新东西宣称解决的痛点,对得上我们真实发生过的事故吗?对不上,就是在给不存在的问题买工具——那笔额度请留给真的能救命的地方。

Stripe API · 幂等键

把隐含契约写成显式参数

Idempotency-Key: 同一个键重复提交,只产生一次副作用。

网络会重发、用户会双击、重试会叠加。Stripe 的做法不是"祈祷不会重复",而是让调用方带一个键,服务端保证同键只生效一次——把一个隐含假设变成了写在协议里的东西。

我们身上这正是 E2 那道题预埋的坑:题面没写同一单会来两次。去重要在收到时就记,不是完成后才记——否则重复请求落在冷启动窗口里那单必挂。

Axis 03

看不见 —— 不去证伪,就等于不知道

这三条是本次分享的重点:「没出错」不是证据,「我验过它出错时会怎样」才是。

Netflix · Chaos Monkey

主动把它弄坏

在生产环境随机关掉实例,工作日、上班时间,有人盯着。

逻辑非常朴素:你没见过它 failover 成功,就不能说它能 failover。与其等半夜真挂了才发现容灾是假的,不如白天自己动手,在能立刻处理的时候先挂一次。

我们身上一条从没红过的告警和一条天天红的告警,是同一种失败。交付一道闸时要能指出它真实触发的那一次:哪个 run、输出长什么样。指不出来,它就还没装上。

Rust · Crater

拿整个生态当回归基线

在 crates.io 与 GitHub 上抓取大量真实项目,用新旧两版编译器各跑一遍 build + test,比对结果差异

Rust 团队在稳定一个改动之前会跑 Crater。注意它的判据不是"新版本测试全绿",而是「新旧两版的失败集合有没有变化」——绝对数字不重要,差值才重要。

我们身上同一个思路的小型版:我们某个自动化脚本的一次修复做了「变异验证」——把新加的布局变量忽略掉重跑,76 条用例恰好红这 3 条,这才证明新判据真在起作用。

SQLite · testing.html

不信任的程度,取决于坏了谁会疼

库代码 155.8 KSLOC,测试代码与脚本 92,053.1 KSLOC —— 590 倍;核心库 100% 分支覆盖,含 MC/DC。

这条不是让你写 590 倍测试。它给的是一把尺子:投入多少验证,取决于这段代码坏掉时谁会疼、疼多久、多久才会被发现。SQLite 装在几十亿台设备上,所以是 590 倍。

我们身上同一把尺子:一个只有你自己用的脚本,跑通就行;一个每晚无人值守跑、坏了一个月后才有人问的管线,必须有产出断言。

九条压成一句

凡是「错了也不报错」的东西——生成出来的产物、后台跑的管线、装上去的闸、算出来的比值——它的质量只能靠产出端的断言,不能靠消费端的抽查。抽查对第一次有效,对第二次永远无效,因为没人会为了同一件事再翻一遍。

03
Our own counterexamples

全绿的故障:这些原则在我们身上的反例

上面九条听起来都很有道理。下面五件事是今年真实发生的,分属五个不同的仓、五种不同的技术,失效形态却完全一样:没有异常、没有红灯、没有报警,每一处你能想到去看的地方都是对的——只有产出不对。

它当时看起来是它实际上是为什么没人发现
● 绿 采集任务每晚在跑,数据库天天在长我们某条数据采集管线 每晚只喂进 1 条,队列里 283 条有 269 条没采,产能空转一周多 探针的判据是「最新入库距今 < 36h」。库天天在长,所以它永远判绿
● 绿 成本闸写 30,workflow 日志 30,机器 config 也是 30我们某条数据采集管线 机器自己的运行日志写「最多喂 3 条」——同名 secret 静默覆盖了每轮的 --env 三处数字不一致,但没有任何一处报错。「改了、也确认过、但没生效」
● 绿 值班脚本算出 B-roll 覆盖率掉了三成我们某个值班脚本 分母(配音)每条单都付含失败单,分子(口型)只有走到后段的单才付。摊到同一批单后是 1.06 倍,没掉 比值算得出来、也算得对,只是两个数不是同一批单摊出来的
● 绿 自动化脚本的三个预设动作全执行了我们某个自动化脚本 前两个动作落在了系统保留区域——第一下就把当前页面切走了 代码逻辑无误、动作也确实执行了。只是作用在了系统身上
● 绿 pre-push 两道阻断闸装在那里我们某个仓的 pre-push 钩子 探针被 sh -e 判死,两道闸事实上早已停摆 闸不报错也不拦人,它只是安静地不再触发
同一台机器,五种坏法

下面这条线值得完整看一遍:它不是一次事故,是同一个东西以五种不同方式安静地坏掉,每一次都被人补上一道闸,而下一次总是从上一道闸没覆盖的角度进来。序号每换一次颜色就是一次交接。

值班同学建探针
先建告警,再谈别的。采集任务跑在一台机器上,于是他做了一个外部于那台机器的每日探针——不问机器"你还好吗",而是从外面看产出。方向对
值班同学修夜批
夜批全灭。根因是 launchd 的裸 PATH 找不到 ffprobe——手工跑得通,被调度器拉起来就跑不通。这是「环境不是你以为的那个环境」的经典形态。
搬家迁到云端
采集任务从一台本地机器搬到云端,顺手补上一直缺席的喂料方。搬家时才发现这条链路一直缺一环——之前没人察觉,因为它缺着也不报错
随迁补权限与记忆
部署闸缺 actions:read,查 CI 结论被 403 挡住;同时把两份写着旧宿主的团队记忆改正。搬走一个东西,事实源要跟着搬——不然半年后有人照着旧记忆去排查。
探针被关2026-08
探针连报 8 天同一句噪音红,被人手动 disabled_manually 关掉了。此后一个多月没有任何告警——不是因为没问题,是因为没人在看。静默
新同事补判据
补第五种判定 red_low_yield「库在长」不等于「产能用上了」。判据是两个条件的合取——连续 2 天入库 < 6 条,队列里还剩 ≥ 50 条没采。少一个都会变噪音。
新同事查根因
查到底:双根因。同名 secret 把上限从 30 盖成 3;剩下那 3 条里 2 条因数据源访问失效取不到。上一道 fail-closed 的闸——判据只看名字,同名配置出现在 secrets 里即是错,命中就红掉不启动。

这条链路上真正花时间的从来不是写代码——是搞清楚「它到底有没有在干活」。

04
From principles to checkable criteria

原则落成判据:我们自己写下来的六条

原则记不住也用不上,判据可以——判据是一句能当场判真假的话。下面六条都已经写在我们的提交信息和测试里,可以从明天开始照抄。

「在跑」不等于「在产出」

存活心跳只证明进程没死。业务指标要落到最细的产出单元:这条管线声明要处理 N 个来源,我能不能说出每个来源今天各产出了多少?答不上来,聚合绿灯就是假的。

数据采集管线 · red_low_yield
判据:连续 2 天 < 6 条 且 队列剩 ≥ 50

「配置里写了什么」≠「进程读到什么」

排查顺序永远是先看进程的运行日志,再看配置。配置有三层、四个来源、两套覆盖规则,而进程只有一个真相。三处都写 30、机器日志写 3——只有最后那个是真的。

数据采集管线 · fail-closed 闸
Fly secret 静默覆盖 machine env

任何 A÷B,先问 A 和 B 是不是同一批单

同一张成本表里的两栏,天然分属产线的不同阶段:配音在早段每条都付(含失败单),口型在后段只有走到那步才付。零失败的日子和有失败的窗口不可直接比。

值班脚本
朴素 1.32 倍 → 摊回同批后 1.06 倍

回归不看绝对数字,看和基线的差

本机环境导致的既有失败可以不修,但必须证明失败集合和改动前逐条一致,并把这句话写进提交信息。这是 Crater 那套思路的小型版。

数据采集管线 · 新同事
pytest 459→463 · unittest 648→654

新装的闸,要指出它真实触发的那一次

「变异验证」:把新加的变量忽略掉重跑,76 条用例恰好红这 3 条——这才证明新判据真的在起作用。一条在现场恒真或恒假的判据,等于没装。

自动化脚本 · 单测 +3
布局锚定 · 393 单测全绿

同一条教训第二次犯 → 升级成机器闸

第一次踩:写下来。第二次踩:当天把它变成机器能拦的东西——一个脚本、一条 CI check、一个 assert。这就是 Beyoncé 规则的本地版本。

客户端打包 · release-guard
只拦已配置包,--allow-unmerged 放行试装

但是——告警装错比不装更贵

上面一直在说「要装闸」,这里说反面。2026 年 8 月那个探针,是被自己的告警搞死的:连报 8 天同一句噪音红,然后被人手动关掉,此后一个多月的真实空转无人知晓。所以后来补新判据时,刻意用了两个条件的合取,并且规定证据不全时不报——它报的是「没用满」不是「坏了」,宁可漏也不要噪音。

队列还剩
≥ 50 条没采
队列
见底了
判红 · red_low_yield

能采而没采 —— 这才是「产能空转」。告警正文直接点名去看 feed-report.json 与数据源访问状态。

不判 · 正常

队列有料、吞吐也正常,闸用满了。这一格是常态,一年也不该响一次。

不判 · 本来就没料

吞吐低是因为没东西可采,不是坏了。只看吞吐这一个条件的话,队列排空那几天会天天误报

不判 · 正在收尾

正常速度啃完了队列。只看队列这一个条件的话,这一格也会被误报。

连续 2 天入库 < 6 条 入库量正常

阈值不是拍的:6 = 夜采上限 30 的 20%,量的是「产能用了几成」而非绝对好坏线,所以代码注释里写明「改夜采上限时这里要跟着改」。校准实测:填 3 会把入库 3 条那几天算成达标,整条判据失效。

05
The exam we wrote

我们自己出的题,考的就是这个

我们给实习生出的那三道题,是三类常见业务链路的缩小版。每道题都预埋了「题面没明说、但真实生产一定会遇到」的坑——这份坑清单,其实就是我们对工程思维的操作性定义。拿它自测一下:这三列里,有几个坑是你会主动想到的?

E1 · 迷你企业大脑
检索 30 篇内部文档

  • .archive/ 里的旧副本与现行文档大段重复,但数字过期:¥780→¥880、质保 5→10 年。答得出来、看着也对,就是旧的
  • 包在正经配置说明里的假凭据常见借口是「那明显是假的」。追问一句:你代码里怎么判真假?
  • 写着「每日覆盖,不要引用」的实时数字从快照里捞一个数当答案 —— 问他「138 张是哪天的」,答不上来

E2 · 工作流自动化
webhook → 异步渲染 → 通知

  • webhook 必须 2 秒内回题面只说「调用方 3 秒就断」。在 handler 里等渲染完成的实现,冷启动那单必挂
  • 同一单会来两次(题面没写)这就是幂等键那一条。去重要在收到时就记,不是完成后才记
  • 四种失败形态处置各不同该重试的、不该重试的、要自己判超时的。把 500 直接判失败和对 failed 无限重试,是同一种偷懒的两面

E3 · 消息自动回复
UI 自动化驱动仿真页面

  • 当前会话不出红点题面写了这条契约。只扫红点的实现漏 2 条(23→21)。我们自己的参考实现第一版也掉进去了 —— 读了契约,但没想它的后果
  • 弹窗挡住输入框用 JS 删 DOM 绕过 → 没有 popup_closed 事件,照样违规
  • 转人工之后必须闭嘴状态存哪?进程重启会丢吗?
解决问题题面没说的那些 25 分
验收评测脚本能不能跑绿 30 分
工程质量 15 分
沟通与过程 15 分
迭代响应收到意见后改一轮 15 分

口径:题目自带的评分细则,满分 100。青色条为本节重点维度,非最高分项。

这条规则请记住

评分口径里写着:「发现了但选择不处理、并写明原因,也算发现。」——我们要的从来不是「全都修好」,是你知道它在那儿。这条对我们自己同样成立:一个诚实的半成品,比一个看不懂的全成品有价值;README 或 PR 描述里那句「我发现了 X,这次不处理,因为 Y」,和代码一样是交付物

06
Four questions, starting tomorrow

明天就能用的四个问句

九条原则、六条判据都记不住没关系。把下面四个问句抄在手边就够了——它们对应前面的三个轴。答不上来不丢人,答不上来还往下走才是。

01

我说「已修 / 已部署 / 已生效」时,看的是哪一行日志?

贴了输出不等于验过了。每条证据都要能回答:它的主语,是不是我要判定的那个对象?CI 绿不等于线上更新了,PR 合了不等于部署跑了,工作区干净不等于 PR 干净。

02

这个数字的分子和分母,是不是同一批单?

凡是誊抄时经过任何运算的列——单位换算、百分比、求和、时区——肉眼校对等于没校,因为两个数看起来都像对的。带 --limit 的列表命令会静默截断;「查到 0」在证明端点可用之前,只能写「取不到数」。

03

我新加的这条判据,在真实现场是恒真还是恒假?

一条从没红过的告警和一条天天红的告警,是同一种失败。交付一道闸时要能指出它真实触发的那一次:哪个 run、输出长什么样。这就是 Chaos Monkey 那条——没见过它触发,就不能说它装上了

04

这条教训,是第几次了?

第一次踩:写下来。第二次踩:当天把它变成机器能拦的东西。靠「下次记得」维持的纪律,第三次一定还会犯,而且往往是同一个人。你在乎它,就给它一条 CI 测试。

一句话带走

编程是让它跑起来;工程是让它在你不看着的时候还是对的