后端生产系统实际上是如何失败的

发布: (2026年2月22日 GMT+8 21:20)
13 分钟阅读
原文: Dev.to

抱歉,我需要您提供要翻译的具体文本内容(文章正文),才能为您进行简体中文翻译。请把文章的文字粘贴在这里,我会保持原有的 Markdown 格式、代码块和链接不变,只翻译正文部分。

介绍

生产环境中的系统往往会遇到事故,虽然有些系统比其他系统更常见。大多数情况下,当生产中出现问题时,代码正是按其编写的方式在运行。问题在于,生产环境会引入一些无法事先完全模拟的条件。本文将讨论这些故障是如何实际发生的,将其归纳为三种模式,说明这些模式为何危险,并简要阐述可以汲取的教训。

生产系统之所以出错,并不是因为代码本身有缺陷,而是因为现实并不总是保持一致。

前提条件

在继续之前,请注意本文适用于:

  • 后端工程师
  • 运行生产系统的人员
  • 任何在仪表盘显示“绿色”但用户仍在抱怨的人

Failure Patterns

Failure Pattern #1: Cascading Failures

级联故障发生在系统中的某个服务变慢或失效时,进而影响依赖该服务的其他部分的行为。即使是小的用户操作,例如重试,也可能导致级联故障。

Example

我曾参与过一个项目,出现了级联故障。某些数据库查询因其复杂度和数据量的增长而形成瓶颈。更糟的是,连接池已达到最大槽位,导致后续的数据库调用无法被处理。这会产生两种可能的情形:

  1. 用户的请求被突然取消,随后再次尝试。
  2. 请求在系统中停留,等待空闲连接来执行查询。

第二种情况演变成了级联故障:系统在同一时间尝试处理超出应有量的请求,同时还要处理常规的进入请求。这导致等待时间变长;即使是登录这样简单的操作也变得缓慢。在某些情况下,CPU 使用率飙升至 100%,整个系统变得无响应,进入慢速状态。

What happens in the background?

每个请求在其生命周期内都占用一个线程。线程池(即为进程分配的线程数量)是有限的;当请求堆积时,它们会消耗可用的线程,导致新请求陷入等待状态。真正被耗尽的往往不是“服务器”本身,而是线程池、数据库连接或队列工作者。

Why is this dangerous?

  • Slowness is contagious. 缓慢的组件或服务会影响其他服务的交付,向用户呈现一个已损坏的系统。
  • Partial health illusion. 系统在单独观察时可能看起来健康,但由于级联故障整体上会失效。取决于设计,一些服务可能仍然运行,但它们对受影响服务的依赖会导致整个系统崩溃。

Lessons learned

  1. Timeouts – 确保对长时间运行的请求或批处理作业设置合理的超时,以便在超出预期时间后终止。超时可以应用于已知的瓶颈,例如外部提供者调用或重量级数据库查询。
  2. Circuit breakers – 将流量从失效的服务或依赖转移到健康的备选方案。例如,当第三方支付提供商宕机时,断路器可以切换到另一家提供商,直至主服务恢复。

Failure Pattern #2: Partial Failures

部分故障指的是系统的某一部分失效,而其他部分仍然正常运行,导致结果不完整或不一致。它们往往隐蔽,却可能代价高昂。

Example

我曾在一个支付系统工作,用户可以发起扣款。某位用户尝试为其卡片扣款,但未在规定时间内收到响应,于是再次重试。支付服务短暂宕机,未能完整处理请求,但仍接受了进入队列的请求。当服务恢复后,它把每个请求当作独立交易处理,未意识到第二个请求是重试,从而导致双重扣费。

从用户的角度来看,重试是合理的行为;而从系统的角度来看,每个请求看起来都是唯一的,于是产生了重复。技术上没有 bug;每一步在单独观察时都合乎逻辑。

Why is this dangerous?

部分故障会使系统处于一种“介于之间”的状态:

  • User view: “它没有成功。”
  • System view: “部分成功了。”

这会产生 divergent truth(真相分歧),即某些操作成功,某些失败,系统与用户对结果的认知不一致。

Lessons learned

  • Idempotency – 设计操作时保证幂等性,使得重试不会产生副作用(如双重扣费)。
  • Transactional guarantees – 在适当的场景使用原子事务或两阶段提交,确保要么全部成功,要么全部回滚。

可视性与监控 – 在仪表盘和警报中呈现部分故障状态,以便运维人员在不一致扩大之前采取行动。

故障模式 #3: [第三模式占位符]

(原始内容在描述第三模式之前就结束了。请在可用时在此插入相应的描述、风险和经验教训。)

Source:

附加故障模式

故障模式 #2:重复请求

重复请求是不可避免的;用户刷新页面、客户端应用重新发送请求,或其他系统自动重试。后端必须假设这种情况:

“此请求可能会被发送多次”

并妥善处理。系统可以使用请求标识符实现幂等性,使它们将来自同一来源的重试视为同一次请求,并通过以下方式避免重复:

  • 返回第一次请求的结果,或
  • 丢弃较早的请求并处理最新的请求(各系统自行决定)。

故障模式 #3:静默失败

静默失败是最致命的,因为它们最难被发现。后台任务可能悄然失败,或报告未能生成。乍一看一切正常,直到有人在几天后注意到数据不匹配。

静默失败并不一定意味着系统崩溃;它们通常在以下 任意 情况发生时出现:

  • 系统仍在运行
  • 请求看似成功
  • 没有触发警报
  • 仪表盘显示“正常”

……但业务结果却错误。

本质上,静默失败是错误信号 传递到观察正确性的层面的故障模式。一次简单的缓存写入失败或事件已发布却从未被消费,都可能是静默失败的指示。

为什么这很危险?

对于静默失败,系统使用者可能永远不会立刻察觉,团队也会误以为一切正常。这会导致问题累积,直至业务受到影响,例如:

  • 订单未付款
  • 付款后未发送发票
  • 事件未被消费

后端随后会留下“历史腐败”。在许多情况下,修复 bug 并不能修复已产生的损害,因为新数据是正确的,而旧数据仍然错误。团队必须采用以下技术:

  • 回填数据
  • 事件重新处理
  • 一次性迁移脚本

经验教训

  • 可观测性 – 每个后端系统的必备要素。若实现不当,几乎毫无价值。良好的可观测性让你知道系统是否在做正确的事。
  • 日志 – 应记录涉及的实体(例如 orderIDtransactionReferenceID)、失败原因以及后续步骤。优质日志能够触发警报、追踪流程,并加速发现静默失败。
  • 指标 – 对检测静默失败至关重要。由于静默错误发生在请求 成功 时,域指标如 orders_count_totalevents_published_totalcompleted_payments_totalabandoned_payments_total 等非常有帮助。利用这些指标来断言关系或触发警报。
    • 示例:当 abandoned_payments_total 超过阈值 orders_count_totalcompleted_payments_total 出现显著偏差时,发出警报。
  • 警报 – 只有在可操作时才有意义。仅提示 “错误率上升” 的警报是噪音,缺乏足够信息进行处理。可操作的警报应告诉用户 出了什么问题、在哪里查看以及为何重要

总结: 如果警报没有告诉你下一步该做什么,那它就是噪音。

同样必须明白,“工作正常” 并不等同于 “正确”。在处理静默失败时尤为如此。虽然后端系统会优化可用性、吞吐量和弹性,但也必须优化正确性。

结论

生产故障很少源于“糟糕的代码”。它们的产生是因为现实世界的条件——资源限制、网络分区、第三方服务中断以及人为行为——无法在测试环境中完全复现。了解这三种故障模式、它们为何危险,并应用相应的经验教训(超时、断路器、幂等性等),可以显著提升系统的弹性。

结论

生产故障并不是在警报触发时开始的——它们始于假设未被检查。目标不是零故障;而是 可见、可理解且可恢复的故障。生产系统默认不会大声报错;它们会悄然失效——除非我们专门设计避免这种情况。

作为工程师,我们必须:

  1. 在构建系统时考虑故障模式。
  2. 接受生产问题是不可避免的。
  3. 以能够长期保持系统稳健的方式响应它们。
0 浏览
Back to Blog

相关文章

阅读更多 »

TAC 后端服务内部 SDK

概述 该软件包为 TAC 内的后端服务提供了标准化的共享 Software Development Kit SDK。它集中管理 API 客户端、业务逻辑…