
这是一个示例下文提到的团队、产品和时间线均为虚构,旨在展示故障是如何发生的。文中未描述任何真实的公司、客户或测量数据。
事件经过
- 1
09:10,发布页面进入 CMS
一个六人的产品团队在一个无头 CMS 和静态构建的营销网站上工作。一位编辑准备发布一篇合作伙伴公告,其中链接到一个上周刚注册的合作伙伴域名。
- 2
09:14,编辑点击发布
发布 Webhook 调用了一个函数。该函数针对合作伙伴域名执行链接回溯检查调用,并被编写为在任何非成功响应时阻断发布。
- 3
09:14,构建从未启动
两件事同时失败。该域名是全新的,因此我们的索引可能尚未收录。另外,团队密钥的预付费余额已耗尽,因此付费的后续调用返回了 HTTP 402。
- 4
09:40,团队发现问题
编辑报告了一个模糊的错误。工程师查看函数日志,看到 402 状态码和空结果,花了二十分钟才弄清楚究竟是哪个原因导致了阻断。
- 5
10:05,手动完成发布
有人通过手动部署绕过了钩子。发布延迟了 55 分钟,且绕过操作未留下任何记录,无法得知跳过了哪些检查。

发布前团队检查了哪些内容
实际上很少。测试列表仅包含知名域名,这就是为什么直到合作伙伴链接出现时,这个漏洞仍未被发现。对知名站点的测试只能证明管道连接正常,却无法说明对于索引尚未收录的域名会发生什么。
更好的测试列表应包含三种类型的域名:一个成熟的域名、一个小众垂直领域的域名和一个近期注册的域名。在任何人信任该钩子之前,应将这三类域名都通过钩子进行测试。
测试覆盖了哪些域名?
仅覆盖了简单的域名。列表中的每个域名都有多年的历史记录,因此每个响应都是完整的,没有人看到空响应会对发布产生什么影响。
添加一个真正的新域名,并手动读取其结果。如果备注显示为“bad”(错误)而不是预期的“unknown”(未知),则在发布前需要重写规则。
预演运行遗漏了什么?
预演运行使用的密钥余额充足,因此空余额路径从未被执行。没有人编写过故意耗尽信用的测试。
确保该路径受到测试。每季度强制触发一次 402,并查看编辑人员随后看到的内容,因为没有人运行的失败路径就是没有人能信任的路径。
原因分析
检查门将所有非成功响应都视为最终判决。这是根本原因。余额问题和未知域名只是关于工具链和索引的事实,与页面本身无关。
我们的图数据库大约每月重建一次。在上次刷新后注册的域名可能显示为空,而新合作伙伴是发布页面的正常情况。没有人对此进行检查。团队仅使用旧的和知名的域名(如 nytimes.com)对钩子进行了测试。
第二个遗漏点是静默。函数记录了错误,但没有发送警报,编辑人员看到一个通用的失败消息,没有任何提示表明信用额度已耗尽。
成本从来不是问题。每次调用 2 个信用点,这个虚构站点一个月的检查总成本约为 $0.60,与 成本表 中 3,000 次发布行的数据相同。检查成本很低,但在 2026 年仍让他们损失了一个小时。
402 状态码值得单独列出一项。预付费余额是一种可监控的状态,因此如果在 10,000 个信用点(即 $1)时设置一个简单的警报,就会在几天前触发。
团队写下的行动项
负责人是角色,而非具体人员。所有项目均为示例的一部分。| 变更 | 负责人 | 原因 |
|---|
| 将 402 视为跳过,并向平台频道发送警报 | 平台 | 信用点是计费状态,而非页面状态 |
|---|
| 将新域名的空结果视为未知,而非错误 | 平台 | 索引滞后约一个月 |
|---|
| 用通俗语言向编辑显示原因 | 编辑工具 | 因阅读日志损失 20 分钟 |
|---|
| 在条目中记录跳过的检查 | 平台 | 手动部署未留下痕迹 |
|---|
| 使用本周注册的域名测试钩子 | 质量保证 | 旧域名掩盖了差距 |
|---|
发布周的预演
在下一次发布前执行这些操作,而不是在下一次失败后再做。
- 1
选择一个无人检查过的域名
选择一个近期注册的域名,并对其调用免费概览操作。确认返回的稀疏结果被记录为“未知”,而非“阻塞”。
- 2
耗尽测试余额
使用余额较小的测试密钥,并故意让其运行至零。观察是否出现 402 错误、是否触发警报,以及发布流程是否继续进入构建阶段。
- 3
大声朗读编辑器的提示信息
请一位未见过该钩子的同事阅读错误文本。如果对方无法说明下一步该怎么做,则在发布前重写该提示信息。
- 4
将跳过操作记录到条目中
确认跳过的检查会在条目中留下带有日期和原因的备注。这样,手动部署就不会留下无人能解释的空白。

修复方案的编写方式
团队将一个通用失败拆分为三种情况,因为每种情况的责任人不同。401 是密钥问题,402 是账单状态,而新域名的空结果仅仅是“未知”。每种情况都有独立的分支和独立的提示信息。这就是整个变更内容。
无需阅读堆栈跟踪即可知道发生了哪种情况。警报会指明状态,编辑器会看到一条说明下一步该做什么的提示。
为何余额警报设置在远高于零的位置?
一万积分相当于发布价格下的 $1,这是一个较小的金额,但仍能在余额提前耗尽时提供充足的预警。将警报设置在人类会注意到的地方,而不是设置在一个周五无人打开的日志中。
每天大声失败一次的检查,优于在发布期间无声失败的检查。
与团队商定该数值,并将其与密钥轮换规则的规则写在同一位置,以便两者一起进行审查。
新域名是否应计为失败?
否。索引尚未发现的域名是索引的事实,而非页面的问题。将其存储为带有日期的“未知”,并让编辑器决定是否等待下一次刷新。
该规则同样适用于任何稀疏结果。将其视为缺失数据,而非对合作伙伴的裁决。
将未知结果记录在单独的列表中,以便在下一次审查时可见其模式,而不是将其淹没在通用日志中。
复制内容
链接数据失败时保持开放,警报失败时保持响亮
链接检查不应成为编辑发布页面的唯一障碍。让页面通过,标记条目,并确保负责密钥的人员无法忽略该失败。
- 在代码中区分 401、402 和空结果
- 将余额警报设置在远高于零的位置(参见成本表)
- 在发布日前在预发环境中演练失败场景
团队事后提出的问题
评审会议在每个版本的故事中都提出了同样的几个问题。以下是虚构团队对这些问题的回答,以及为什么这些回答在不同组织之间会有所不同。
这些回答并非放之四海而皆准,每个回答都取决于页面的用途。重点是在下一次发布之前将答案落实到纸面上。
闸门是否应该失败并关闭?
对于这个虚构的团队来说,否。链接检查是基于月度索引构建的信号,它不应成为编辑发布页面的唯一障碍。
将策略写成一句话,以便下一位工程师能够区分预期阻塞和错误。
更大的余额能否防止该问题?
它只会延迟问题,而无法修复设计缺陷。更大的余额仍可能耗尽,而将每个非成功状态都视为裁决的闸门仍会在遇到第一个未知域名时阻塞。
修复方案是将结果拆分为三种,并设置提前触发的警报。余额只是故事的一部分。
对你构建的任何闸门问同样的问题:哪种失败会是无声的,谁会最先注意到它?
关于假设的说明
真实团队的做法会有所不同。你的团队可能出于良好原因选择失败时关闭闸门,例如受监管的网站,其中每个出站链接都由人工审核。这很好。只需将其作为一个明确的选择即可。
Webhook 运行手册 将相同的决策列为了检查清单,误区列表 涵盖了导致该虚构团队陷入陷阱的信念。