Comparthing Logo
消息队列可靠性模式分布式系统云架构错误处理云基础设施

死信队列与内存重试

死信队列和内存重试代表了分布式系统中处理消息处理故障的两种截然不同的方法,死信队列可以持久隔离问题消息,而内存重试则可以提供轻量级、低延迟的恢复,而无需持久性开销。

亮点

  • 死信队列会无限期地保留失败的消息,因此对于审计和合规性场景至关重要。
  • 内存重试的执行开销仅为微秒级,而队列操作的延迟则在毫秒级以上。
  • 死信队列(DLQ)使独立的运维团队能够在不部署应用程序代码更改的情况下管理故障。
  • 如果没有断路器加以限制,内存方法中的重试风暴可能会引发级联故障。

死信队列是什么?

持久消息队列,用于捕获失败的消息以便稍后检查和重新处理。

  • 邮件在超过最大重试次数后被移至死信队列,但邮件内容和元数据将完整保留。
  • 最初由 IBM MQ 和 JMS 等企业消息系统推广,现在已成为 AWS SQS、Azure 服务总线和 RabbitMQ 的标准配置。
  • 实现解耦故障分析,无需阻塞主处理管道,使团队能够修复问题并重放消息。
  • 通常与监控和警报系统集成,以便在消息进入死信状态时通知操作员。
  • 支持基于时间的过期策略,AWS SQS 死信队列默认保留消息最多 14 天。

内存内重试是什么?

立即重试逻辑在同一进程内执行,无需外部消息持久化。

  • 重试策略通常采用指数退避,每次尝试之间的延迟时间翻倍(例如,1秒、2秒、4秒、8秒)。
  • Polly (.NET)、Resilience4j (Java) 和 Retry (Python) 等框架提供了可配置的重试策略以及熔断器模式。
  • 除了处理应用程序现有的内存和 CPU 之外,不会消耗额外的基础设施资源
  • 如果应用程序在重试过程中崩溃,则完全失败,丢失重试状态,并可能丢失原始操作上下文。
  • 最适合应对网络故障、数据库连接超时和临时服务不可用等瞬态故障。

比较表

功能 死信队列 内存内重试
持久性 在单独的队列中持久化消息存储 短暂的,仅存在于应用程序内存中
故障恢复 即使应用程序崩溃和重启,系统也能正常运行。 如果在重试期间进程终止,则数据丢失。
基础设施成本 额外的队列存储和传输成本 除了应用程序之外,无需额外的基础设施。
运营可视性 内置指标、警报和回放功能 需要自定义日志记录和监控
延迟影响 由于队列操作导致延迟较高 延迟极低,立即重试执行
使用案例契合度 需要保证处理的关键工作流程 非关键操作中的瞬态故障
消息排序 可以保持或破坏原有秩序 自然地保持加工过程中的顺序
团队协作 支持对修复和回放操作拥有不同的团队所有权 与应用程序部署紧密相关

详细对比

可靠性和耐用性保证

当您绝对不能丢失任何消息时,死信队列 (DLQ) 的优势就显现出来了。一旦消息进入 DLQ,即使整个服务重启,它也会一直保留在那里,直到有人显式地处理它。相比之下,如果您的 Pod 崩溃或进程在部署期间被终止,内存重试机制就会彻底失效。因此,对于金融交易、库存更新或任何与合规性相关的操作,DLQ 显然是最佳选择。

性能和延迟特征

内存重试在速度上完胜。它无需网络跳转、无需队列 API 调用、无需序列化开销,只需快速休眠后重试即可。对于每秒处理数千条消息的高吞吐量系统而言,这种速度差异会显著累积。死信队列 (DLQ) 会引入可测量的延迟,尤其是在消息必须跨越网络边界才能到达单独的队列服务时。一些团队会采用混合方案,使用内存重试进行快速的临时修复,而将死信队列作为最终的安全保障。

操作复杂性和调试

死信队列 (DLQ) 创建了一个清晰的操作边界。值班工程师接到呼叫后,会检查死信队列,修复底层错误,然后重放消息。这是一个易于理解的工作流程。内存重试会将故障埋没在应用程序日志中,通常需要日志聚合和自定义仪表板才能发现重试正在进行。当重试次数耗尽时,故障解析器将面临噩梦般的局面,尤其是在微服务架构中,故障可能会在任何人注意到之前就向下蔓延。

规模化成本考量

云队列服务按请求和存储消息数量收费。一个繁忙的死信队列(DLQ)如果存储了数百万条消息,会显著增加您的账单,尤其是在保留策略较为宽松的情况下。从基础设施的角度来看,内存重试几乎是免费的,但它们会消耗内存,并且如果重试风暴没有限制,可能会导致其他线程资源不足。对于成本敏感的初创公司来说,这通常会促使他们选择内存方案,直到收入足以抵消可靠性带来的额外成本。

与现代架构的融合

事件驱动架构和无服务器函数使得死信队列(DLQ)比以往任何时候都更加重要。AWS Lambda、Azure Functions 和 Google Cloud Functions 都原生支持死信配置。内存重试机制更适合传统的应用服务器和长时间运行的进程。然而,Kubernetes 和临时计算的兴起实际上使内存策略变得更加复杂,容器可能在几乎没有预警的情况下被终止,这使得死信队列对那些以前避免使用它们的团队也越来越有吸引力。

优点与缺点

死信队列

优点

  • + 保证信息持久性
  • + 清晰的操作交接
  • + 原生云集成
  • + 支持回放和审核
  • + 隔离故障影响

继续

  • 额外基础设施成本
  • 更高的端到端延迟
  • 需要重放机制
  • 可能会积累过期消息
  • 更复杂的架构

内存内重试

优点

  • + 极低的延迟
  • + 无需额外基础设施
  • + 初期实施起来比较简单
  • + 运营成本极低
  • + 快速故障反馈

继续

  • 进程崩溃导致数据丢失
  • 隐蔽行动
  • 可能导致重试风暴
  • 与应用程序生命周期紧密耦合
  • 事后调试更难

常见误解

神话

死信队列消除了应用程序中任何重试逻辑的必要性。

现实

死信队列(DLQ)是重试失败后的最终归宿,而不是重试逻辑的替代品。大多数实现仍然会在判定消息失效之前立即或延迟重试。如果没有中间重试机制,任何瞬态故障都会立即导致死信队列被大量数据淹没。

神话

内存内重试总是更快,因此性能更好。

现实

虽然单次重试速度更快,但无限制的内存重试会导致线程池饱和,从而降低系统整体吞吐量。当重试风暴触发熔断器或使下游服务过载时,这种性能优势会迅速消失。

神话

死信队列中的邮件稍后会自动处理。

现实

死信队列(DLQ)是被动存储的,除非有人或系统进行明确操作,否则这些消息不会被处理。许多团队发现,由于没有人构建重放管道,导致大量消息在死信队列中积压了数月之久。

神话

你必须在死信队列和内存重试之间做出选择。

现实

这些模式相辅相成,相得益彰。最具弹性的系统采用内存重试机制,并结合指数退避算法实现快速恢复,然后在达到合理的阈值后启动死信队列(DLQ)。这种分层方法能够同时覆盖瞬态和持续性故障模式。

神话

内存重试不适用于分布式系统。

现实

虽然内存重试机制不如死信队列(DLQ)那样稳健,但在分布式系统中,对于幂等性强、非关键性的操作,它仍然是一种常见且合适的策略。关键在于使重试策略与故障的实际业务后果相匹配,而不是想当然地认为一种模式可以适用于所有情况。

神话

死信队列可防止系统故障期间消息丢失。

现实

死信队列(DLQ)仅对已被队列系统接收的消息有效。如果消息由于网络分区或生产者故障而从未到达主队列,死信队列也无法神奇地将其恢复。端到端可靠性也需要生产者端的持久化机制。

常见问题解答

究竟是什么原因导致邮件进入死信队列?
消息通常会在用尽配置的重试次数后进入死信队列 (DLQ),这可能意味着 SQS 接收次数超过最大限制、多个消费者传递失败,或者被应用程序代码显式拒绝。具体的触发条件因平台而异,AWS SQS 使用重试策略来指定最大接收次数,而 Azure 服务总线则跟踪传递次数。一旦超过该阈值,消息传递基础架构会自动将消息移动或复制到关联的死信队列。
内存重试机制如何处理进程重启或崩溃?
它们不具备这种能力,这是它们的根本局限。任何重试状态都仅仅存在于运行进程的堆内存中。如果应用程序崩溃、在部署过程中被终止,或者容器被重新调度,所有待处理的重试及其上下文都会消失。对于必须在这些事件发生后仍然能够运行的操作,你需要持久化的重试机制,无论是死信队列(DLQ)、基于数据库的作业队列,还是像 Celery 或 Hangfire 这样的分布式任务系统。
能否在同一个系统中将死信队列与内存重试机制结合起来?
没错,这实际上是许多团队的最佳实践。典型的模式是:首先在内存中进行指数退避重试,以实现即时的瞬态恢复,例如在几秒钟内尝试三次。如果三次都失败,则将消息或操作发布到支持死信队列 (DLQ) 的队列中进行持久化处理。这样既能利用内存重试的速度处理突发事件,又能利用死信队列的安全性来应对持续存在的问题。
应该对死信队列设置哪些监控?
至少应配置队列深度、最旧消息的年龄和传入消息速率的告警。死信队列 (DLQ) 到达数量的突然激增通常表明部署中存在错误。消息年龄告警可以捕获未发生重放的情况。许多团队还会跟踪 DLQ 消息与成功处理消息的比率,以此作为健康指标。CloudWatch、Azure Monitor 或 Datadog 都可以通过分页器集成来显示这些指标。
除了死信队列和内存重试之外,还有其他方法吗?
多种模式可以满足类似的需求。Outbox 模式将事件与业务数据以事务方式持久化,从而确保原子性。Saga 模式使用补偿操作来管理长时间运行的分布式事务。像 Sidekiq 或 pg-boss 这样的基于数据库的作业队列无需专用消息代理即可实现持久化。事件溯源从仅追加的日志中重建状态,因此其重试语义有所不同。正确的选择取决于您的一致性要求和现有基础架构。
如何安全地重放死信队列中的消息?
切勿在未进行检查的情况下直接将消息重放回原始队列,否则如果根本原因仍然存在,将会导致无限循环。正确的做法是,将死信队列 (DLQ) 消息转移到单独的分析环境中,检查代表性样本以识别故障模式,修复根本问题,然后有选择地分批重放并进行监控。AWS 提供 DLQ 重放功能,而 Amazon EventBridge Pipes 等工具可以自动执行条件重放工作流程。
内存重试的良好重试策略应该具备哪些要素?
带抖动的指数退避是黄金标准。如果没有抖动,来自多个客户端的同步重试可能会对正在恢复的服务造成群体攻击。限制最大延迟以防止无限等待,并始终设置最大重试次数。考虑使用熔断器,当失败率超过阈值时完全停止重试,从而让下游服务有时间恢复,而不是在它们宕机时继续承受重试压力。
无服务器函数是否能很好地支持内存重试?
不完全是这样。Lambda 函数和类似函数的设计初衷是无状态且生命周期短。15 分钟的最大执行时间意味着内存中的重试窗口受到限制。更重要的是,如果 Lambda 函数失败,整个执行上下文都会消失。无服务器架构强烈倾向于外部状态,因此使用死信队列 (DLQ) 或带有内置重试逻辑的 Step Functions 比内存方法更自然。
这些方法在消息排序方面有何不同?
死信队列(DLQ)会使消息顺序保证变得复杂。如果主队列采用先进先出(FIFO)原则,那么除非平台明确地维护顺序,否则将消息移入或移出死信队列可能会扰乱消息顺序。单个消费者内部的内存重试机制自然会维护该消费者消息的顺序,尽管多个消费者仍然并行处理消息。一些系统使用序列号或应用层排序来在任何重试机制之后重建正确的顺序。
死信队列需要考虑哪些安全因素?
死信队列 (DLQ) 包含与主队列相同的敏感数据,有时甚至更多,因为它们包含故障上下文。请应用相同的加密、访问控制和审计日志记录。谨慎使用重放机制,如果下游系统不具备幂等性,重新处理旧消息可能会引发意想不到的副作用。某些受监管的行业要求在访问或重放 DLQ 消息之前,必须经过明确的审批流程。
何时应该完全避免内存重试?
当处理过程会产生非幂等性的副作用时,例如因重试而导致信用卡被重复扣款(后果不堪设想),应避免使用重试。当需要严格保证只执行一次且缺乏去重机制时,也应避免使用重试。对于长时间运行的操作,如果进程可能不足以完成重试,则不应依赖重试。此外,当运维团队需要在不部署代码更改的情况下了解故障模式时,也不应使用重试。
企业规模的成本对比情况如何?
典型的 AWS 配置,使用 SQS 标准队列和死信队列 (DLQ),每百万条消息的运行成本可能只有几美元,再加上保留消息的存储成本。对于每月处理数十亿条消息的系统来说,这笔费用就相当可观了。内存重试将成本转移到计算资源上,而计算资源本身就已经包含在您的账单中。然而,重试风暴可能会导致 CPU 和内存使用量激增,从而可能需要更大的实例。大多数总体拥有成本分析都倾向于对低关键性、高容量的工作使用内存重试,而对低容量的关键工作流程使用死信队列。

裁决

当消息丢失不可接受且运维团队需要明确的故障边界进行管理时,应选择死信队列。当速度至关重要、基础设施简洁性至关重要且故障是真正的暂时性而非系统性故障时,应选择内存重试。许多成熟的系统实际上结合了这两种方法,使用内存重试进行即时恢复,并将死信队列作为最终的保障措施。

相关比较