题干与适用场景
设计一个异步消息流程:消息经过有限次重试后进入死信队列(DLQ),运营人员修复根因后可以按条件重放。说明消息如何进入 DLQ、如何调查、如何选择批次、如何避免重复副作用,以及如何保护正常流量。
这道题适用于后端、平台、基础设施和数据工程岗位。Amazon 的软件开发面试主题强调运用技术解决问题,而不是背诵细节。AWS 文档说明 DLQ 用于隔离未成功消费的消息,并应配合告警和重试次数;Google Pub/Sub 文档提供死信主题与 replay/seek 语义。重点是故障边界和运营闭环。
面试官考察点
- 区分暂时性错误、毒丸消息、业务拒绝和过期消息。
- DLQ 是否保存版本、租户、分区、追踪 ID 和失败原因。
- 重放是否具备幂等键、范围、速率限制、审批和停止条件。
- 是否明确顺序、保留期、重复投递和至少一次语义。
- 是否用指标验证恢复,而不是只说“重新放回主队列”。
回答前需要澄清的问题
- 投递语义是至少一次、至多一次,还是业务上要求有效一次?
- 哪些失败可以重试,哪些必须人工处理或丢弃?
- 消息保留多久?过期消息还有价值吗?
- 同一聚合键是否必须有序?
- 消费者副作用是否支持幂等或去重?
- 谁可以查看、重放和删除消息,是否需要审计?
- 正常流量 SLO、队列容量和重放容量是多少?
- 重放失败后进入同一 DLQ 还是独立 replay-DLQ?
30 秒回答框架
“我先定义至少一次投递、保留期和顺序键。消费者对可重试错误使用有上限的退避,对毒丸或业务拒绝记录原因后转入 DLQ。DLQ 保存原始消息、尝试次数、消费者版本和追踪 ID。修复后按租户、版本或时间窗创建重放批次,先隔离验证,再限速投递;消费者用幂等键保护副作用。监控 DLQ 深度、最老消息、重放失败率和重复率,超过阈值就暂停。”
分步骤深入解答
步骤一:定义失败状态机
正常队列、重试、DLQ、人工修复和 replay-DLQ 应是可区分状态。不可重试的业务拒绝不能无限重试。
步骤二:设计消息元数据
保存事件 ID、业务幂等键、产生时间、租户或分区、模式版本、尝试次数、原始追踪 ID 和错误分类。原始载荷不可变,重放只改变投递元数据。
步骤三:确定重试与入 DLQ 规则
设置最大接收次数、退避上限和保留期。AWS SQS 文档指出 DLQ 有源队列和区域约束,并应配置告警。过期或撤销消息应记录原因后人工处置。
步骤四:设计安全重放流程
重放请求包含筛选条件、目标版本、速率上限、最大批量、审批人和过期时间。先抽样验证,再分批投递;重放队列与正常队列隔离。
| 控制点 | 目的 | 失败动作 |
|---|---|---|
| 幂等键 | 防止重复副作用 | 拒绝或返回已处理结果 |
| 速率上限 | 保护消费者和下游 | 暂停重放 |
| 批次范围 | 限制影响面 | 缩小筛选条件 |
| 审批与审计 | 追踪责任 | 阻止未授权操作 |
| replay-DLQ | 隔离再次失败 | 生成新诊断批次 |
步骤五:处理顺序与并发
需要有序时按聚合键分组,避免同键的正常消费与重放并行。不要声称队列天然提供端到端 exactly-once。
步骤六:保护副作用
消费者以事件 ID 或业务幂等键做条件写。支付、邮件等不可回滚副作用需要幂等请求键和结果查询;重放工具不能假设删除消息就足够。
步骤七:可观测和运营闭环
监控 DLQ 深度、最老消息年龄、错误分类比例、重放吞吐、失败率、重复副作用和下游延迟。每个批次记录操作者、原因、时间、结果和停止记录。
步骤八:容量、兼容和停止
估算最大重放速率,保证新消费者兼容旧模式。根因未修复、下游过载或重复率上升时立即暂停,保留原始证据。
高质量示范回答
“我设计的是至少一次投递的订单事件流程。网络超时最多重试 5 次并退避;模式错误、权限拒绝和过期事件进入 DLQ。每条消息保存事件 ID、订单 ID、租户、模式版本、首次入队时间、尝试次数、错误分类和 trace ID,原始载荷不可变。
修复消费者后,运营人员创建批次,指定租户、时间范围、目标版本、每秒上限、审批人和过期时间。先抽取 50 条在隔离消费者上验证,再以正常流量 10% 的速率投递。订单状态表以事件 ID 做幂等条件写,支付调用使用同一业务幂等键。相同订单的正常消息和重放消息不能并行。
告警覆盖最老消息、重放失败率、重复写入和下游延迟;任何阈值触发都暂停批次并把再次失败的消息放入 replay-DLQ。批次记录筛选、版本、操作者、结果和停止原因。”
常见错误
- 把 DLQ 当垃圾桶,不保留错误分类和原始证据。
- 无限重试毒丸消息,持续阻塞正常流量。
- 只说“放回主队列”,没有批次、审批和速率控制。
- 假设队列提供端到端 exactly-once。
- 没有幂等键,重放造成重复扣款、发信或状态推进。
- 忽略顺序键,导致旧事件覆盖新状态。
- 只监控队列长度,不监控最老消息和业务结果。
- 重放与正常消费共用容量,恢复流量再次过载。
追问及应对
追问一:为什么不直接增加最大重试次数?
重试适合暂时性故障;毒丸、模式错误和永久业务拒绝会浪费容量。次数应结合故障类型、保留期和等待成本决定。
追问二:重放时怎样保证同一订单有序?
按订单键分区,阻止同键并行,并在正常消费和重放之间建立互斥;若允许无序,要说明状态转换如何防止旧事件覆盖新状态。
追问三:下游没有幂等接口怎么办?
建立本地去重和结果查询;对不可逆副作用改为人工核对、补偿流程或供应商支持的幂等机制。
追问四:重放再次失败怎么办?
进入独立 replay-DLQ,保留原批次和新错误信息,暂停相关筛选条件并通知负责人。
追问五:如何估算重放速率?
根据消费者能力、下游配额、正常流量余量和恢复时间设定上限,先小批量压测,再逐步提高。
追问六:什么时候应该丢弃消息?
只有业务明确判定消息过期、撤销或无价值,并完成审计和责任确认后丢弃;保留摘要和原因。