跳到主要内容

带有延迟预算的紧急开关:你的故障处理从未达到的标准

· 阅读需 13 分钟
Tian Pan
Software Engineer

运维手册上写着“禁用代理”。值班人员照做了。43 分钟后,当紧急开关终于通过配置服务传播开来时,该代理已经提交了 1,200 张错误的工单,调用了 8,000 次计费 API,并向根本没有订阅任何服务的客户发送了邮件。运维手册是正确的,但它也是徒劳的,因为没有人衡量过当代理每秒钟都在造成破坏时,“禁用代理”实际上需要多长时间。

!["https://opengraph-image.blockeden.xyz/api/og-tianpan-co?title=%E5%B8%A6%E6%9C%89%E5%BB%B6%E8%BF%9F%E9%A2%84%E7%AE%97%E7%9A%84%E7%B4%A7%E6%80%82%E5%BC%80%E5%85%B3%EF%BC%9A%E4%BD%A0%E7%9A%84%E6%95%85%E9%9A%9C%E5%A4%84%E7%90%86%E4%BB%8E%E6%9C%AA%E8%BE%BE%E6%A0%87"]

大多数 AI 功能都配有紧急开关,就像大多数建筑都配有灭火器一样:有人签字确认它的存在,却没人计时到达它需要多久。合规审查会问“是否有紧急开关?”,答案是肯定的。而故障现场会问“止血有多快?”,答案则取决于底层管道恰好需要的时间——团队中从未有人针对该功能造成破坏的速度测量过这个数字。

这种不匹配正是问题的核心。一个遏制时间长于其破坏扩散时间的功能,交付的只是“遏制剧场”(Containment Theater)。

每个 AI 功能都有一个隐式的 RTO

在传统的事件响应中,恢复时间目标(RTO)是你为每个服务声明的属性。对于 AI 功能,RTO 是由更微妙的东西暗示的:失控模型积累破坏的速度。

如果你的代理以每秒 50 次请求的速度调用付费第三方 API,且每次调用耗费 5 美分,那么每一秒钟的错误运行都会耗费 2.50 美元。如果你的代理向客户可见的队列写入数据,每一秒钟都会增加你需要手动对账的条目。如果你的代理发送邮件,每一秒钟都是你无法挽回的声誉损失。

将该速率乘以你的遏制延迟——即从“我们决定停止”到“它实际停止”之间的实际时间(Wall-clock time)——你就得到了故障的最低成本底线。这不是漏洞的成本,不是回归测试的成本,而是紧急开关管道的成本,每当出现问题时,这笔费用都会由你或客户承担。

这是没有人测量的数字。团队测量模型准确性、评估分数、延迟、Token 支出。值班人员按下按键与代理停止响应之间的延迟,是一个存在于“我们发布功能”与“我们遏制功能”之间的数字,并且在每一个路线图中都被忽略了。

紧急开关是如何在无人察觉的情况下积累出三分钟延迟的

纸面上的紧急开关是一个配置旗标(Config flag)。实际操作中的紧急开关是一个请求,它必须穿越多个系统,然后处理流量的节点才会相信旗标已经更改。

一个典型的路径如下:

  • 值班人员在特性开关仪表板中编辑配置。到控制平面的网络往返:约 200 ms。
  • 仪表板写入全局 KV 存储。同步到区域只读副本:对于亚秒级同步提供商需要 1–3 秒,对于依赖定期同步的提供商则长得多。
  • KV 存储位于 CDN 之后。CDN 缓存 TTL:通常为 30–60 秒,有时更高。
  • 每个应用程序 Pod 上的 SDK 轮询 CDN。默认轮询间隔:大多数提供商为 60 秒,可配置但很少进行调整。
  • 应用程序代码通过 SDK 读取旗标值并决定是否调用模型。旗标在每次请求时检查,但 SDK 仅按其轮询节奏重新获取。
  • Web 客户端(如果有的话)通过其自身的 SDK 以自身的轮询间隔调用应用服务器。再增加 30–60 秒。

累积延迟是每个方向上最慢跳转的总和。一个选择了默认轮询时间为 60 秒的托管特性开关 SDK 的团队,实际上交付了一个底线为一分钟的紧急开关。一个 CDN 位于旗标服务之前的团队,在这个底线之上叠加了 CDN TTL。一个客户端每两分钟重新获取一次旗标的团队,又叠加了一层。

这一切都不是任何人的错。每个组件都为一个不是“立即停止 AI 代理造成破坏”的用例做出了合理的默认决定。这些默认设置组合成了一个原始架构从未考虑过的遏制时间。

破坏时间与遏制时间的倒置

结构性问题可以用一句话概括:遏制时间超过其破坏扩散时间的功能,等同于没有紧急开关

如果代理在每次请求中能造成五秒钟不可逆的破坏,而紧急开关需要三分钟才能传播,那么到开关切换时,已经发生了 36 倍于“代理能做的最坏情况”的破坏。紧急开关是虚设的,破坏却是真实的。

这与“我们需要让紧急开关更快”的构想不同。更快固然好,但更重要的准则是针对每个功能将遏制时间与破坏时间相匹配。有些功能的破坏时间以毫秒计——任何在没有撤销功能的情况下写入外部系统的操作。有些功能的破坏时间以小时计——任何纯咨询性的、破坏性积累缓慢的操作。每个功能的紧急开关都需要一个短于破坏时间的传播预算,否则该团队就在没有明说的情况下,接受了这种倒置作为设计选择。

特性开关不是一个原语(Primitive)。传播预算才是。旗标是表面;传播预算是该表面必须满足的契约。

分层开关:因为单一延迟标准无法应对所有故障

摆脱倒置局面的方法并不是“让每个 Flag 都瞬间生效”。这种做法用成本、可靠性和复杂性换取了大多数功能并不需要的预算。真正的出路是在技术栈的不同层面设计“多个”开关,每个开关都有经过测量的激活延迟,并明确在何种故障下该动用哪一个。

一个有效的分层设计通常如下所示:

  • 第一层:进程内熔断器(In-process circuit breaker)。 应用程序在每次模型调用时从本地内存读取的 Flag,通过推送(Server-Sent Events、Websocket)而非轮询进行更新。激活延迟:毫秒级到几秒钟。使用场景:高频损耗型故障,即 Agent 的每次调用都在产生不可逆的损害。
  • 第二层:全集群配置开关(Fleet-wide config toggle)。 采用默认轮询机制的托管功能旗标服务,这是大多数团队都会提供的紧急开关。激活延迟:几秒钟到一分钟。使用场景:低频损耗型故障,你希望更改能同步到所有环境,并在进程重启后依然有效。
  • 第三层:部署级禁用(Deployment-level disable)。 通过 CI 部署代码更改,彻底移除 Agent 的调用路径。激活延迟:几分钟到数十分钟。使用场景:长尾故障,你有足够的时间按标准流程处理,并希望回滚状态比任何 Flag 状态都持久。

这里的准则是了解每个功能的“爆炸时间(blast time)”,并接入激活延迟短于该时间的、成本最低的层级。一个每晚运行的批处理 Agent 不需要第一层开关。而一个直接面向客户并会写入外部系统的实时 Agent,可能既需要第一层也需要第二层开关。

第一层开关最值得深思,因为大多数团队都会跳过它。进程内熔断器也是你放置自动触发器的地方——例如动作计数超过阈值、错误率超出范围、单次会话成本超出预算等——这些触发器可以在无需人工介入的情况下直接关闭开关。人的工作是验证熔断器是否生效;而熔断器的工作是在人做出反应之前先行采取行动。

激活延迟应作为数字写入运维手册(Runbook)

你能对紧急开关设计做出的成本最低、杠杆最高的改进,并不是增加一个新开关,而是把数字写在现有开关的旁边。

大多数运维手册会写着“通过功能旗标仪表盘禁用 Agent”。下一行应该紧跟着写上:“预期传播时间:45 秒。如果 60 秒后 Agent 仍在产生请求,请升级至第三层(提交 PR 并部署,约 8 分钟)。”

这个数字能同时起到多重作用。它告诉轮值人员何时该停止等待并开始升级处理。它告诉事故指挥官(Incident Commander)这次事故不可缩减的成本是多少——如果 Agent 每秒造成 1 美元的损失,而紧急开关需要 60 秒生效,那么在任何人有机会采取行动之前,每次事故都有 60 美元的保底损失。它告诉事后复盘人员衡量标准:开关是否达到了预定的时间预算?它还告诉下一个设计类似功能的团队该按什么标准制定预算。

这个数字必须是“测量”出来的,而不是估算的。激活演练是获取该数字的唯一真实来源。选择流量较低的时段,在预发布或灰度环境中拨动开关,记录从按下按键到最后一个请求处理完毕的时钟时间。每季度做一次,因为底层系统会发生漂移——SDK 会升级,CDN 配置会更改,轮询间隔可能会被为了优化成本的人员调整。六个月前的演练数据在今天只能算是民间传说。

“遏制剧场”式的组织架构模式

大多数组织之所以交付“遏制剧场(Containment-Theater)”,并非源于刻意设计,而是因为紧急开关及其保护的功能由不同的团队负责。

AI 团队构建 Agent,平台团队负责功能旗标服务,部署团队负责 CI/CD。轮值人员则是本周抽到下下签的倒霉蛋,通常是那些从未亲手激活过开关的人。每个团队的动力都是做好自己的部分——Agent 能跑通,旗标系统在线,部署流水线是绿色的。没有人有动力去测量开关相对于 Agent 破坏速率的“端到端”延迟,因为这种测量不在任何人的 KPI 里,也不在任何人的运维手册里。

这就是为什么紧急开关的延迟往往会默默地退化。每个团队都做出了局部合理的更改——平台团队增加了 CDN 以减轻旗标服务的负载,AI 团队增加了重试层以处理瞬时失败,部署团队为了安全延长了灰度观察期——每一次更改都增加了端到端测试从未重新测量的延迟。

解决方案是结构性的:将紧急开关的延迟预算分配给单一负责人,通常是负责该 AI 功能的团队,并赋予他们要求路径上任何团队做出更改的权力。平台团队的轮询间隔现在是可以协商的,CDN 的 TTL 是可以协商的,CI 的灰度周期也是可以协商的。这种协商是基于 AI 团队根据功能破坏速率推导出的预算进行的,任何违反预算的更改都需要获得批准。

这种协商还会产生一个有用的产物:一份紧急开关经过的所有系统的“地图”,并标注了每一跳的实测延迟。当紧急开关未能成功激活时,这份地图是轮值人员能拥有的最有用的工具,因为它告诉了他们应该首先排查哪一跳。

你不愿进行的演练

任何团队日程表中最令人不安的测试就是线上的紧急停止开关(kill-switch)演练:在低流量窗口期触发生产环境的开关,确认智能体(agent)停止运行,然后再将其恢复。大多数团队不进行这种演练,因为演练本身可能引发故障的风险足以成为逃避的理由。

这种风险是真实存在的。但 进行演练的代价同样真实,而且这种代价会以团队难以察觉的方式累积,直到他们真正需要这个开关的那天,却发现它并不像记忆中那样奏效。

一个合理的演练频率是每季度一次:执行演练,记录激活耗时,记录数据,并与上一季度进行对比。失效通常表现为“偏移(drift)”——如果上季度激活紧急停止开关需要 12 秒,而本季度需要 47 秒,这就在提醒你期间发生的平台变更产生了某些影响。而调查这种偏移的时机应该是现在,而不是在发生故障的过程中。

合规审查类开关的本质

为了通过合规审查而存在的紧急停止开关,是为了被 检查 而设计的,而不是为了被 使用。合规清单只问它是否存在,只要格子里填了内容,就算通过。

而能控制真实故障的紧急停止开关则完全是另一回事:它是一项基础设施,拥有经过测量的激活延迟、针对延迟设定的预算、能证明延迟真实性的演练,以及匹配不同影响范围(blast profiles)的分级体系。它是一个系统,而不仅仅是一个勾选框。它由其所保护功能的团队所有,而不是由托管该服务的团队所有。它的操作手册(runbook)里写满了具体数字,而不是美好愿景。

这两种紧急停止开关在合规文档中看起来一模一样。但在你本季度最糟糕的一天,也就是凌晨 3 点时,它们看起来会截然不同。混淆这两者的团队,直到需要它的那一天,才会发现自己交付的到底是哪一种。

架构上的结论很简单,尽管实现起来并不容易:每个 AI 功能都有一个隐含的恢复时间目标(RTO),这取决于它造成破坏的速度。如果你没有根据该 RTO 测量紧急停止开关的激活延迟,那你交付的就不是一个紧急停止开关——而是一个关于它的 想法。发现这两者区别的代价,正是那个紧急停止开关原本应该阻止的故障。

References:Let's stay in touch and Follow me for more thoughts and updates