Errorhandling
错误处理和容错系统
后端工程师实用参考——技术深度、浅显易懂
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_KEY、SENDGRID_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),而不是跨环境手动复制秘密
- 保持登台和生产之间的环境对等尽可能接近——生产中的意外通常来自登台中未发现的差异
- 在启动时记录加载的配置摘要(已编辑机密),以便您可以立即确认应用程序正在运行的值