错误恢复:大规模场景下的优雅降级
SchemaBridge Team · 2026-01-16 · Resilience, Error Handling, Fault Tolerance
大规模场景下如何处理重试与退避,以及托管式韧性如何防止"重试风暴"。
核心要点 简单粗暴的重试逻辑会把一次故障放大成重试风暴,反过来压垮它正在猛烈请求的那个服务。韧性来自有上限的重试——针对瞬时 HTTP 故障的短暂指数退避,以及后端报告过载时的自适应节流——再加上面向新工作流版本的带自动回滚的渐进式发布,以及针对所有不应由自动化来决定之事的人工恢复工具(Retry、Resolve、Start over)。
"重试风暴":为什么简单粗暴的错误处理会失败
在分布式系统的早期,"错误处理"通常意味着把一段代码包进 try/catch,或许再加一个重试三次的简单 while 循环。在故障罕见且局部的小型隔离环境中,这样做是可行的。但在大规模场景下,在相互连接的微服务网格中,简单粗暴的重试就是一场系统性崩溃的配方,这被称为重试风暴(Retry Storm,或"惊群效应" Thundering Herd)。
设想这样一个场景:在黑色星期五促销期间,你的主数据库在高负载下变得有些迟缓。查询延迟从 10ms 增加到 1100ms。你的 API 设置了标准的 1 秒超时。突然之间,一千个并发 worker 同时触发了这个超时。它们全都捕获了错误,并按照你那个简单循环的逻辑,立即重试查询。
现在,本已难以应对最初 1,000 个请求的数据库,一下子又被额外的 1,000 个请求击中。负载瞬间翻倍。数据库 CPU 飙升至 100%,延迟增加到 5 秒。worker 再次失败,再次重试。系统陷入故障的正反馈循环。你实际上对自己的基础设施发起了 DDoS 攻击,把一次轻微的性能下降变成了整个系统的宕机。
拥抱失败:错误是 API 的一部分
在 SchemaBridge,我们摒弃了"错误是例外"的观念。在事件驱动的世界里,失败和成功一样常见。网络会分区,Pod 会行为异常,第三方 API 会有维护窗口。我们不把错误恢复当作代码中的一个 catch 块,而是当作平台中的托管生命周期——并对哪些内容会被自动重试刻意设定了严格的限制。
SchemaBridge 如何对故障进行分类
要有效地恢复,首先必须理解某件事为什么失败。SchemaBridge 在两个层面上做出判断:
| 层级 | 故障 | 自动发生的处理 |
| :--- | :--- | :--- |
| HTTP 调用(Gateway) | 5xx、429 或 I/O 错误(超时、连接重置) | 在进程内重试,以指数退避最多再尝试 3 次 |
| HTTP 调用(Gateway) | 其他任何 4xx(400、401、403、404…) | 不重试——错误的密码在第二次尝试时也绝不会成功 |
| 步骤(vertex) | 有速率限制的后端出现瞬时过载(external_overload) | 自动重新派发,直至每个 vertex 的预算上限 |
| 步骤(vertex) | 其他所有情况 | 首次尝试即为终态;运行显示为红色并等待运维人员处理 |
默认策略刻意保守:步骤失败除非被证明是瞬时的,否则视为永久失败。我们宁愿把失败呈现给人,也不愿悄悄地重新执行一个可能并不适合重复的副作用。
有上限重试的剖析:退避的数学
HTTP 调用的指数退避
当 Gateway 调用遇到瞬时错误时,平台不会每秒都去猛烈请求目标。它会在两次尝试之间以指数方式延长等待时间。
$$Wait = Base \times 2^{Retry - 1}$$
- 首次尝试失败
- 重试 1:等待 1 秒
- 重试 2:等待 2 秒
- 重试 3:等待 4 秒
总共四次尝试,历时约七秒。如果目标返回带有 Retry-After 头的 429 Too Many Requests,平台会遵循该头,上限为 30 秒。这种短暂且有上限的递进可以吸收网络抖动,而不会让一个正在挣扎的服务沦为受害者。请注意,这种退避没有随机化——没有抖动(jitter)——因此它的设计目的是平滑一次短暂抖动,而不是协调成千上万个独立客户端。
针对过载后端的自适应重新派发
对于真正饱和的后端,短暂退避并不是合适的工具。对于有速率限制的步骤类型(Gateway 步骤和智能体步骤),过载失败会反馈到平台的按目标准入控制中,后者会降低向该目标放行工作的速率(一个 AIMD 控制器,与 TCP 用于拥塞控制的思路相同)。随后,失败的步骤会通过同一条准入路径被自动重新派发——默认最多 3 次,可通过 maxAutoRetries 按 vertex 配置(对于副作用绝不能重复发送的步骤,请将其设为 0)。
{
"type": "GATEWAY",
"config": {
"url": "https://api.example.com/v1/charges",
"method": "POST",
"maxAutoRetries": 5
}
}
由于节流来自准入控制而不是固定的计时器,重试会恰好按目标所需的程度放慢。如果过载持续时间超出预算,该步骤就会失败,运行将等待运维人员处理。
渐进式回滚:工作流发布的安全保障
有时候,重试是徒劳的。如果新的工作流版本引入了 bug,再多的重试也修复不了它。你需要回滚。
SchemaBridge 为工作流版本提供带自动回滚的渐进式发布,以系统工作流(gradual_deploy)的形式实现。
- 逐步放量:可配置比例的流量(默认 10%,最低可至 1%)被路由到新版本,并按间隔逐步提高该比例。
- 健康检查:在每一步,监控运行会将新版本的错误数与一个固定阈值进行比较。一旦超出,发布流程会把新版本的流量调回 0%。
这把有缺陷的工作流发布的影响范围限制在检查发现问题时它已达到的流量比例之内。它适用于工作流版本——而不是基础设施配置——而且它是一张安全网,而非保证:在阈值触发之前新版本已接收的流量仍然受到了影响。
人工介入的恢复:运维人员的工具箱
有些故障需要的是大脑,而不是循环。如果一个工作流因为银行对账与发票金额相差 0.01 美元而失败,没有任何代码能够、也不应该去决定该怎么做。
在 SchemaBridge 中,这样的运行只会以 FAILED 结束,并附带运维人员采取行动所需的一切:
- 可见性:失败的步骤会在运行图和监控视图中以红色标出。
- 取证:运维人员可以查看该步骤的输入、输出和错误信息(例如 "mismatch: 100.00 vs 100.01")。
- 介入:运维人员选择合适的修复方式:
- Retry 重新运行失败的步骤,并从该处继续执行工作流。
- Resolve & continue 接受该失败,为该步骤记录一个由运维人员提供的输出,并让工作流的其余部分继续执行。
- Start over 将整个工作流作为一次新的执行重新运行。
- Resume on new version 在修复了 bug 的工作流版本上,从故障点继续一次失败的运行。
这让"错误"从恐慌和手工改数据库的根源,变成了标准的运维工作流。
示例场景:支付网关故障
设想一家订阅制 SaaS 公司,每天午夜处理一大批续订。某天夜里,其主要支付网关在某个区域发生了长时间故障。
旧方式
基于简单 cron 任务构建的遗留系统在网关宕机时重试三次,随后将订阅标记为"失败 / 支付被拒"。等到工程师醒来时,客户账户可能已经被停用,因为系统判定他们的支付被拒绝,而实际上只是网关宕机了。支持队列被塞满,"账户已取消"邮件之后便是客户流失。
SchemaBridge 方式
现在,如实地设想同一个计费引擎运行在 SchemaBridge 上。
1. 有上限的重试:当 503 开始出现时,每次扣款调用都会以短暂退避进行重试。短暂抖动会被吸收;持续数小时的故障则不会——这些续订运行会失败。
2. 失败不是决策:由于失败的步骤会停止其运行,而不是流向"拒绝"分支,因此不会有人的账户因为一次被误读为拒绝的故障而被取消。失败的运行以红色留在监控视图中,并附有网关返回的错误。
3. 恢复是一次批量操作:网关恢复正常后,运维人员选中失败的运行并批量重试。
预期结果
故障依然会发生,第二天早上依然需要有人去处理。改变的是损失:没有错误的取消,没有多余的邮件,恢复只需一次批量操作,而不是一次数据库清理。这就是韧性的商业价值。
容错设计专家清单
要构建一个有韧性的系统,请遵循以下经验法则:
1. 为每一次写入规划撤销方案:如果你创建了一条记录,就要知道失败时如何删除或归档它——即使这个撤销是由人触发的一个步骤。对称性是一致性的关键。
2. 为你自己掌控的客户端重试加入抖动:当许多独立客户端按相同的时间表重试时,随机性可以防止它们同步成一群蜂拥而至的请求。
3. 宁可让运行停下,也不要走错分支:不要让基础设施故障伪装成业务结果。一个等待人工处理的失败运行,好过一个悄无声息地出错的运行。
4. 审计你的错误路径:我们常常测试"正常路径",却忽视"失败路径"。在预发布(staging)环境中,把某个步骤指向一个返回 503 和 400 的端点,并确认运行在每种情况下都按你的预期运作。
结语:失败是增强韧性的契机
错误恢复不是为了防止 bug,而是为了防止灾难。通过为自动重试设定上限、让过载使系统放慢而不是引发蜂拥,以及为运维人员提供精确的恢复工具,你就能自由地构建复杂的集成,而不必担心"重试风暴"。
本文是 Building the Bridge 系列的一部分。另请参阅:基础设施即工作流,了解如何用可视化逻辑管理云资源。