错误处理和容错系统

后端工程师实用参考——技术深度、浅显易懂


1. 心态

错误是不可避免的

您无法阻止每个错误 - 目标是快速检测、遏制和恢复。停止思考*“我如何阻止它损坏?”并开始思考“当*损坏时会发生什么?”

容错心态

在最坏的情况发生之前就进行设计。即使内部组件出现故障,构建良好的系统也能保证用户交易无缝进行。


2. 常见错误类型

A. 逻辑错误

🔴 最危险的类型 — 无声故障,没有崩溃或异常

系统继续运行——没有崩溃,没有例外——但它产生错误的结果。这些都是无声的杀手:表面上一切看起来都很好,但实际上却有错误的数据流过。

为什么它们很难捕捉:

  • 没有可见的崩溃或堆栈跟踪
  • 该应用程序似乎运行正常
  • 通常是通过错误的输出或用户投诉发现的——为时已晚

现实世界的例子:

电子商务折扣计算中的错误导致系统“向用户付款”而不是向他们收费——负交易价值仅出现在财务报告中。

根本原因:

  • 误解业务需求
  • 算法实现不正确
  • 未考虑边缘情况

预防策略:

  • 强大的单元+集成测试
  • 边缘情况验证(例如最低价格 = 0)
  • 专注于业务逻辑的代码审查
  • 监控异常输出值(例如负交易金额)

影响: 直接财务损失,削弱用户信任,比运行时错误更难调试


B. 约束违规(数据库错误)

🟡 数据库层 - 在数据库级别暴露的应用程序验证差距

当违反应用程序级规则时数据库抛出的错误 - 例如插入重复值、违反外键或将必填字段留空。这些通常表明您的输入验证层中存在差距。

数据库错误类型:

  • 连接错误: 当应用程序由于凭据错误、服务器停机或网络问题等问题而无法连接到数据库时发生。
  • 约束违规: 当数据库规则(如主键、外键或唯一约束)在数据插入或更新过程中被破坏时就会发生。
  • 验证失败: 当输入数据在存储到数据库之前不满足所需的格式、范围或条件时出现。
  • 查询错误: 当 SQL 查询语法或逻辑存在错误,导致数据库无法执行时发生。

常见示例:

  • 将重复项插入 UNIQUE 列(例如,同一电子邮件注册两次)
  • 添加父表中不存在外键的记录
  • NULL 插入 NOT NULL

根本原因:

  • 输入验证缺失或较弱
  • 竞争条件(例如并发注册时重复的电子邮件)
  • 交易管理不善

预防策略:

  • 在前端和后端级别进行验证
  • 使用数据库约束作为安全网——而不是作为主要验证
  • 使用正确的事务并在失败时回滚
  • 返回用户友好的错误消息,而不是原始数据库错误

C. 外部服务错误

🟡 第三方/网络 — 超出您控制范围的故障

源自“系统外部”的故障 — 第三方 API 宕机、达到速率限制或网络基础设施中断。您无法控制这些,但您需要优雅地处理它们

常见故障模式:

  • 网络故障 — 服务器和外部服务之间的物理或路由级连接丢失
  • 连接超时 — 外部服务接受了连接,但响应时间过长
  • DNS 解析失败 — 您的服务器无法解析外部服务的主机名(例如 api.stripe.com 不返回 IP)
  • 网络分区 — 网络分裂会导致您的服务器和外部服务失去彼此的可见性,即使两者都在运行
  • 速率限制 (HTTP 429) — 您已超出给定时间窗口内允许的请求数量;外部 API 暂时拒绝您
  • 全面服务中断 — 第三方提供商完全关闭(例如 AWS us-east-1 中断、Stripe 事件)

现实世界的例子:

您的支付服务调用 Stripe 的 API 向用户收费。 Stripe 遇到事件并返回 503 Service Unavailable。如果没有正确的处理,您的应用程序会崩溃或在重试时向用户收取两次费用,从而造成财务和信任损失。

根本原因:

  • 过度依赖外部服务且没有后备计划
  • 没有重试逻辑或重试执行不力(例如,敲击速率受限的 API)
  • 出站 HTTP 调用缺少超时 — 您的线程无限期挂起
  • 当依赖关系下降时没有监控或警报

为什么它们至关重要:

  • 可以降低面向用户的核心功能(例如支付、身份验证、通知)
  • 很难在本地复制——通常只是在生产中表现出来
  • 如果不隔离,可能会级联成内部故障(请参阅:断路器模式)

预防策略:

  • 在每个出站 HTTP 调用上设置明确的 连接和读取超时
  • 实施具有指数退避的重试逻辑 - 每次重试之间等待更长的时间(例如 1 秒 → 2 秒 → 4 秒 → 放弃)
  • 使用断路器模式——连续N次失败后,停止调用服务一段时间并立即返回回退响应
  • 将关键操作(例如发送电子邮件、处理网络钩子)存储在队列中,以便稍后重试而不会丢失数据
  • 通过状态页面监控第三方正常运行时间并就错误率升高发出警报
  • 设计优雅降级 — 如果推荐服务关闭,则显示默认列表而不是空白屏幕

D. 输入验证错误

🔵 API 入口点 — 在造成损坏之前捕获不良数据

客户端发送到您的 API 的数据不正确或格式错误 — 类型错误、缺少必填字段、格式无效或值超出范围。这些应该在系统的边界**被捕获,并在数据接触您的业务逻辑或数据库之前立即使用明确的 HTTP 400 Bad Request 拒绝。

将输入验证视为 API 的保镖:它在门口检查凭据,以便垃圾数据永远不会有机会从内部破坏您的系统。

现实世界的例子:

用户使用 age: "twenty-three" 而不是 age: 23 提交注册表单。如果没有验证,该字符串就会传播到您的数据库中,破坏下游分析查询,并破坏基于年龄的资格逻辑 - 所有这些都来自一个错误的输入。

常见示例:

  • 当字段需要数字时发送 "price": "free"
  • 在注册请求中省略 email 等必填字段
  • 将日期传递为 "31-13-2024" — 无效月份
  • 提交超出允许范围的值(例如订单上的 quantity: -5
  • 传递无效的枚举值(例如,当仅存在 "admin""user" 时,使用 role: "superadmin"

根本原因:

  • 没有服务器端验证(仅依赖于前端验证 - 永远不安全)
  • 盲目信任客户端输入——客户端可以被完全操纵或绕过(例如通过 Postman 或curl)
  • 模糊或缺失的 API 合同(没有模式,没有可接受格式的文档)
  • 验证逻辑分散在代码库中,而不是集中在入口点

为什么它们至关重要:

  • 未经验证的输入是许多安全漏洞(SQL 注入、XSS、缓冲区溢出)的根本原因
  • 混入的坏数据清理起来代价高昂——你可能不会发现它,直到它破坏了下游的某些东西
  • 不一致的验证会导致不可预测的系统行为
  • 糟糕的错误消息会阻碍开发人员与您的 API 集成

预防策略:

  • 始终在服务器端进行验证,无论前端做什么——前端验证是用户体验,服务器端验证是安全
  • 使用模式验证库(例如 Zod、Joi、Pydantic、Yup)来定义和强制每个传入请求的形状
  • 在 API 入口点验证数据类型、必填字段、字符串格式、数值范围和枚举值
  • 返回描述性错误消息,告诉调用者到底出了什么问题:"field 'email' must be a valid email address" 而不仅仅是 "invalid input"
  • 使用 OpenAPI/Swagger 等工具记录您的 API 合同,以便客户在发送请求之前就知道期望的内容

E. 配置错误

服务器启动 — 启动时发现错误配置的环境

由于环境变量、机密、功能标志或基础设施设置缺失、不正确或配置错误而导致的服务器端问题。这些通常在启动时出现 - 在您的应用程序准备好服务任何流量之前 - 尽管结构不良的应用程序有时会让它们溜到运行时,在那里它们会导致难以诊断的故障。

将配置视为墙后的布线。当它做得正确时,没有人会注意到。当错误出现时,一切都不起作用——而且错误消息通常与实际问题毫无关系。

现实世界的例子:

您的应用程序已成功部署到生产环境,但从未在环境中设置 DATABASE_URL 。服务器启动,接受传入流量,然后在第一次数据库调用时抛出一个神秘的 connection refused 错误 - 影响每个用户,而团队则忙着找出它在暂存中工作良好的原因。

常见示例:

  • 缺少 DATABASE_URL — 应用程序启动但在第一个数据库查询时崩溃
  • 生产中的 JWT_SECRET 错误 — 在生产中签名的令牌在生产中被拒绝,将所有人注销
  • NODE_ENV 在生产中设置为 development — 调试日志公开,禁用优化
  • 缺少第三方 API 密钥(例如 STRIPE_SECRET_KEYSENDGRID_API_KEY) — 付款或电子邮件功能默默失败
  • 错误的端口绑定 — 应用程序在端口 3000 上启动,但负载均衡器需要 8080
  • 云存储存储桶名称不正确 - 文件上传看似成功,但写入不存在或错误的存储桶

根本原因:

  • 启动时不验证环境变量
  • 本地、临时和生产环境之间的差异未记录或强制执行
  • 手动管理的秘密不一致(复制粘贴错误、忘记变量)
  • 没有新团队成员或部署的 .env.example 文件或环境变量文档

为什么它们至关重要:

  • 当应用程序看起来“正在运行”时,可以悄悄地破坏生产中的整个功能
  • 错误配置的机密(例如在生产中使用测试 Stripe 密钥)可能会导致真正的财务或数据后果
  • 如果没有正确的启动日志记录,则很难进行调试 - 错误通常会远离错误配置的变量
  • 影响整个应用程序,而不仅仅是一个用户或一个请求

预防策略:

  • 在启动时验证所有必需的环境变量并快速失败并出现描述性错误:Missing required env var: DATABASE_URL。启动时发生严重崩溃比运行缺少配置的应用程序要好得多
  • 使用 配置验证库(例如 Node.js 的 envalid,Python 的 pydantic-settings)在启动时强制执行类型、必填字段和允许的值
  • 在您的存储库中维护一个 .env.example 文件 — 一个列出每个所需变量和占位符值的模板,因此部署不会意外丢失变量
  • 使用秘密管理器(例如 AWS Secrets Manager、HashiCorp Vault、Doppler),而不是跨环境手动复制秘密
  • 保持登台和生产之间的环境对等尽可能接近——生产中的意外通常来自登台中未发现的差异
  • 在启动时记录加载的配置摘要(已编辑机密),以便您可以立即确认应用程序正在运行的值