分布式系统中的实际错误处理

发布: (2026年1月12日 GMT+8 21:11)
9 min read
原文: Dev.to

Source: Dev.to

请提供您希望翻译的正文内容,我将为您翻译成简体中文。

传统异常处理的问题

在单台机器上,抛出异常感觉是合理的。堆栈跟踪在那里,调试器捕获它,你就能修复问题。但在分布式系统中,这种思维模型几乎立刻就会失效。

  • 一旦请求跨越进程边界,上下文就开始消失。当异常到达 API 网关或消息中间件时,原始原因往往已经不复存在。
  • 异步调用、后台队列和重试进一步模糊了情况,让你面对的故障虽然在技术上可见,却在实际中毫无用处。

重试可能是危险的

  • 盲目重试可能把小的瞬时问题演变成连锁故障。
  • 短暂的数据库故障突然变成大量重复请求,进一步压垮系统。

云平台带来的额外复杂性

  • 后台作业、无服务器函数和编排器经常报告成功,却在无人关注的地方悄悄记录错误。
  • 从平台的角度看,作业已完成;而从你的角度看,关键逻辑根本没有执行。

最终受害者:用户

  • 用户看到模糊的错误信息。
  • 支持团队无法追踪发生了什么。
  • 工程师只能在深夜翻阅日志,毫无明确的起点。

HTTP 状态码不足以说明问题

状态码只能告诉你出现了错误,却无法说明具体是什么为什么。客户端需要结构化的信息,以便记录、展示,并在不同服务之间进行关联。

实际 .NET 示例

public IActionResult GetUser(int id)
{
    var user = _userService.GetUser(id);
    if (user == null)
    {
        return NotFound(new ErrorResponse
        {
            Code = "USER_NOT_FOUND",
            Message = $"User with id {id} does not exist.",
            CorrelationId = HttpContext.TraceIdentifier
        });
    }

    return Ok(user);
}
  • 错误码 使前端和其他服务能够一致地做出响应。
  • 清晰的消息 有助于用户和支持团队理解问题。
  • 关联 ID 使得能够在日志、队列和下游调用中追踪同一次失败。

幂等性与重复处理

在分布式系统中,重试是不可避免的。网络调用会失败,超时会发生,消息会被重新投递。如果你的系统无法安全地处理重复请求,最终会出现数据损坏或重复的副作用。

  • APIs – 需要使用 Idempotency-Key 请求头。后端必须检查并存储该键,以确保重复请求不会重新执行相同的操作。
  • 后台任务 / 消息消费者 – 将已处理的消息标识存放在高速存储中(Redis、DynamoDB,或带唯一约束的数据库表),以防止重复处理。跳过这一步就是在生产环境中导致对客户二次收费或发送重复邮件的根源。

Logging, Structured Logs & Alerts

  • 在职业早期,我避免记录过多日志,因为感觉噪声太大。在分布式系统中,under‑logging 是比 over‑logging 更大的问题。
  • Structured logs(JSON、键值对)让你可以按关联 ID、用户 ID 或操作名称进行查询。加入环境、请求标识和关键输入值等上下文,使日志成为诊断工具,而不是最后的手段。
  • Alerts 和日志同样重要。记录一个没人看到的错误等同于忽视它。对重复失败、队列积压增长或异常峰值等模式设置告警,以便在用户注意到之前做出响应。

实际建议

  1. 明确 – 仅在真正失败时抛出异常。
  2. 在编排器层面配置带退避的重试
  3. 将关键错误发送到共享警报渠道,让人类真正看到它们。
  4. 对于批处理作业/后台处理,将失败写入专用表或队列。这会创建可检查、可重放或手动解决的日志记录。
  5. 前端错误处理 应该是有意的:
    • 使用 React 错误边界捕获意外失败。
    • 谨慎展示后端错误信息——不显示内部堆栈跟踪,但提供足够的细节以便采取行动。
    • 明确重试行为,让用户知道再次尝试是否合理,或是否需要支持。

部分失败与事务编排

在分布式工作流中,部分失败是不可避免的。在 saga 风格的流程中,一个服务可能成功,而另一个服务可能失败。回滚往往不可能,因此 补偿操作 和清晰的日志记录变得至关重要。

Environment Drift

环境漂移是另一种无声的… (原文在此处截断;如有需要请继续).

分布式系统中的错误处理

  • 开发、预发布和生产环境常因配置不匹配而表现不同。跨环境测试错误场景既繁琐又必不可少。
  • AI 集成带来自身风险。大型语言模型可能超时、返回格式错误的响应或行为不可预测。为这些调用加上 timeoutscircuit breakersstrict response validation,可以防止它们成为新的不稳定来源。
  • 从一开始就为各服务定义 shared error contract。事后补充非常痛苦且容易出错。
  • 将每一次网络调用都视为潜在的失败,即使是内部调用。假设可靠性是系统意外崩溃的根源。
  • 在第一次生产事故发生前就投入 log correlation and searchability。用户受影响后再添加可观测性要困难得多。
  • 设计错误响应时使用明确的 codesmessagescorrelation identifiers,不要仅依赖原始异常。
  • 让所有有副作用的 API 和后台任务 idempotent,或接受会出现重复处理的情况。
  • 记录错误时提供 context,而不仅是堆栈跟踪,并对有意义的模式进行告警。
  • 假设云平台会隐藏故障,除非你主动将其显现。
  • 在 React 应用中,诚实且清晰地展示错误,而不是用通用信息掩盖它们。

讨论提示

你在自己的分布式系统中如何处理部分失败和重试?
哪些模式在事故中帮助了你,哪些方法在压力下失效?
我很想听听你的“战争故事”或不同意见。

提供示例

如果你想要 C# error‑response template 或具体的 idempotency example,请告诉我,我可以分享对我有效的实现。

Back to Blog

相关文章

阅读更多 »