前置知识: 运维与中间件

云消息与事件服务

11 min入门

队列/主题/事件总线概念对比、投递语义、可见性超时、死信队列、扇出过滤与顺序性保证。

知识点地图

  • 知识类别:托管消息与事件服务(SQS/SNS、EventBridge、Pub/Sub、Event Hubs、RocketMQ/Pulsar 类)的概念层——选型模型、投递语义与可靠性机制。
  • 解决什么问题:服务间异步解耦时,「用哪个服务」「消息会不会丢、会不会重、会不会乱序」「消费失败去哪」这些决策散落在各厂商文档里;本文把它们收敛成一套统一概念。
  • 什么时候用到:设计订单、支付、通知类异步链路;评估「要不要上消息队列」;给事件驱动架构选型;排查「消息重复消费」「顺序乱了」类故障。

前置:微服务架构的异步通信模式(服务通信章节);SQS/SNS 的命令级实操见SQS 与 SNS 命令(概念-命令分层:本文管「为什么与怎么选」,470 管「命令怎么敲」)。

1. 心智模型:三种形状的「消息基础设施」

形状交互模型生命周期代表产品典型问题
队列 Queue一对一:一条消息被一个消费者取走有消费者就消费,无消费者堆积等待SQS、RocketMQ 队列、Pub/Sub 订阅任务分摊、削峰
主题 Topic(Pub/Sub)一对多:发布者不知道谁在听订阅者在线才收(或订阅带保留)SNS、Pub/Sub、Kafka 主题广播、扇出
事件总线 Event Bus规则路由:事件按内容路由到多个目标事件是「发生过的事实」,常配归档EventBridge、EventGrid集成、事件驱动架构

三个场景对照选型:

  • 订单系统解耦下单与发货:下单服务发「订单已支付」,发货服务消费——用队列(每条订单恰好处理一次、天然分摊);若同时要通知库存、积分、短信三个下游,前面加主题扇出,或事件总线按事件类型路由。
  • IoT 设备上报:十万设备每秒上报遥测——事件总线 + 规则按设备类型分流,冷数据走 Kinesis 类流式管道;队列在这里不合适(不是「任务」而是「事实流」)。
  • 审计流水:所有写操作事件进总线并开归档(EventBridge Archive / Pub/Sub 导出 BigQuery),回放可重建任意时间点状态——「事件溯源」的托管版。

与自建消息中间件(Kafka/RabbitMQ)的关系:托管服务省运维与扩容,语义相对简单;自建换来自定义协议、超长保留、精确一次等能力。中小团队先托管、规模与语义需求到了再评估自建。

2. 投递语义:至少一次的世界

三种投递语义与托管服务的现实:

语义含义托管服务现实
至多一次可能丢,绝不重少见(实时指标上报容忍丢失时可用)
至少一次绝不丢,可能重SQS 标准队列、SNS、Pub/Sub 的默认
精确一次不丢不重SQS FIFO(配合幂等消费)、Kafka 事务(有限范围)

关键推论:至少一次投递必然要求幂等消费。 消费者必须能安全地处理同一条消息两次。三种幂等设计:

  1. 自然幂等:UPDATE account SET status='shipped' WHERE id=... AND status='paid'——重放第二次条件不满足,无副作用。能写成状态机条件更新的,优先这么写。
  2. 消息级去重键:业务唯一键(订单号+操作)做去重表/Redis SETNX,重放时发现已处理直接 ack。
  3. FIFO 队列的去重:SQS FIFO 5 分钟窗口内的重复消息自动去重(基于 MessageDeduplicationId),但窗口外与跨队列的重复仍需业务幂等——FIFO 不等于可以不写幂等。

易错点:把「精确一次」理解成基础设施承诺。SQS FIFO 的精确一次范围是「队列内的投递与去重」,消费函数执行到一半崩溃再重启,处理依然会从头再来——端到端不丢不重的唯一保证是「至少一次 + 业务幂等」。

工程场景(短信重复发送事故复盘):促销短信服务消费 SNS 消息,某次消费超时但实际已发短信,SNS 重投导致同一用户收到三条。复盘修复:发送前查 sent_log 表(手机号+活动 id 唯一约束),插入冲突即跳过——消息级去重键方案。事故成本一条短信几分钱,乘以百万用户就是一次公关事件。

3. 可见性超时与死信队列:可靠性的两根支柱

3.1 可见性超时(Visibility Timeout)

队列模型里消费者不是被推送,而是**拉取并「借走」**消息:消息进入不可见期(visibility timeout),期间其他消费者看不到它;消费者处理完主动删除。不可见期过后消息仍未删除——自动重新可见,交给(可能另一个)消费者重试。

text
消费 → 消息隐藏 N 秒 → 处理成功 → delete(结束)
                    └→ 处理失败/超时 → N 秒后重新可见 → 别的消费者再试
  • 取值逻辑:略大于最慢处理耗时。设 60 秒而处理只要 2 秒,真故障时要白等 58 秒才重试;设 5 秒而处理要 8 秒,第 5 秒消息重新可见,另一个消费者并发处理同一消息——重复消费。拿不准就配合心跳续期(SQS ChangeMessageVisibility,每处理一段续一次)。
  • 易错点:批处理任务把超时拉到 15 分钟来「防重」——不如老老实实写幂等,长超时只是把重复窗口拉长且故障恢复变慢。

3.2 死信队列(DLQ)

重试若干次仍失败的消息,转入死信队列隔离,让人接手而不是无限重试堵住主队列:

text
主队列 --(maxReceiveCount 超过阈值)--> 死信队列 --> 告警 + 人工排查/修复后重放
  • maxReceiveCount 是「被接收次数」阈值(SQS 红驱策略),不是「失败次数」——消费者借走又放回一次就算一次 receive。
  • DLQ 必须配告警(队列深度 > 0 即告警),否则死信变成「安静的垃圾场」,故障静默丢失。
  • 重放流程:修复 bug 后用重放工具(SQS Console 的 Start DLQ redrive、自建脚本)把死信按限速倒回主队列——限速是为了不把修复后的下游再次打垮。

3.3 组合:一条支付回调链路

text
支付网关回调 → API 确认秒回(校验+入队即返回)→ 队列(可见性 30s,重试 3 次)
  → 消费者幂等更新订单状态 → 失败 3 次进 DLQ → 告警值班 → 修复后限速重放

回调类集成的铁律:先确认后处理(收回调即入队返回 200,业务处理在队列里异步做),网关超时重发由幂等消费兜底。在回调接口里同步调下游服务,网关一次超时重发就是一次重复支付。

4. 扇出与过滤

一对多分发的三种实现与取舍:

  1. 主题扇出(SNS Fan-out):发布到主题,多个队列各自订阅。订阅端互不影响——一个下游挂了不影响其他。注意:SNS 往 SQS 投递仍是至少一次,每个队列的消费者各自幂等。
  2. 事件总线规则路由(EventBridge):事件带结构化字段(detail-type、source),规则按内容匹配路由——source: orders, detail-type: OrderPaid 的新事件自动流到新增的积分服务,发布方代码零改动。这是事件驱动架构里「加订阅者不改发布者」的关键。
  3. 消费者自取全量再过滤:Kafka 消费组各自拉全量主题自己过滤。代价是无效消费带宽;换来的是任意复杂的过滤逻辑与回放能力。

过滤在订阅侧做(SNS 订阅过滤策略 / EventBridge 规则)比「广播后各自丢弃」省一个数量级的下游流量;但过滤条件是配置不是代码,记得纳入版本控制(GitOps 管起来)。

工程场景(EventBridge 路由多下游):电商「订单已支付」事件按 detail-type 建三条规则——进发货队列、进积分队列、进数据湖归档。大促临时加「风控旁路」订阅,加规则十分钟上线,订单服务一行代码不改。

5. 顺序性保证

标准队列/主题不保证顺序(多副本并行投递换取吞吐)。需要顺序时的三种姿势:

  1. 分区有序(消息组):SQS FIFO 的 MessageGroupId、Kafka 的 partition key——同一组内严格有序,组间并行。订单系统用订单号做组 id:同一订单的状态变更有序,不同订单并行。
  2. 单队列串行:全部走一个 FIFO 队列单个消费者——最简单,吞吐上限也最低;只在低流量且全局有序不可妥协时用。
  3. 版本号/时间戳自校:无序到达的消息带版本号,消费者丢弃过期版本。与上游不可控(第三方推送)时的兜底方案。

易错点:给每个消息都设唯一 MessageGroupId 等于把 FIFO 用成标准队列(零并行度,吞吐垫底);反过来用固定 group id 又退化成完全串行。组 id 的基数决定并行度,按「需要有序的最小业务单元」选(通常是实体 id,如订单号、用户 id)。

6. push 与 pull 的取舍

维度pull(拉取,SQS/Kafka)push(推送,SNS HTTP 订阅/Pub/Sub push)
消费节奏消费者自控,天然削峰跟随生产速率,下游被打垮要自己限流
空闲成本轮询(长轮询省费用)零空转,有消息即推
端点要求消费者可以是 worker/函数消费者必须是可达的 HTTPS 端点
失败重试可见性超时天然重试平台按策略重试(退避可配)

选型直觉:常驻 worker、处理耗时不均——pull;Serverless 函数消费、端点即函数 URL——push。两边可以组合:SNS push 到 SQS 再由 worker pull,兼得「多下游扇出」与「拉取削峰」。

动手实践

任务:设计「退货申请」链路的概念方案并落到 AWS 资产清单:App 提交退货 → 异步审核(1-5 分钟出结果)→ 结果通知用户 + 更新库存。要求写出:队列或主题的选择与理由、投递语义假设、幂等键、DLQ 策略、组 id。

提示:

  1. 审核是任务型消费——队列;结果通知是扇出——主题或事件总线;
  2. 幂等键:return_id + 状态迁移条件;组 id:return_id;
  3. 审核服务调外部风控 API 可能超时——可见性超时与 maxReceiveCount 怎么配?
参考方案(先自己写,再展开对照)
text
API(POST /returns)→ 校验+落库(status=submitted)→ SQS 标准队列
  visibility=60s(审核含外部风控调用),maxReceiveCount=3
消费组:审核 worker(幂等键 return_id,条件更新 status)
  成功 → SNS topic "return-completed"(filter 短信/邮件/库存三个订阅)
  连续失败 → DLQ returns-dlq(深度告警)→ 值班修复 → 限速重放

要点自检:API 同步路径只做校验落库(秒回,审核慢不阻塞用户);重复回调被条件更新挡住(status 已非 submitted 即跳过);风控抖动由可见性超时自然重试,3 次仍败进 DLQ 而非堵死主队列。若后续要按「退货原因」加订阅方(财务对账只关心退款完成),把 SQS 换成 EventBridge 规则路由,加订阅不改 API。

常见陷阱

  1. 不写幂等就上队列:至少一次语义下重复消费只是时间问题;幂等键与状态机条件更新是标配。
  2. 可见性超时 < 处理耗时:消息提前重新可见,多消费者并发处理同一消息。
  3. DLQ 没告警:死信静默堆积,业务悄悄丢消息。
  4. FIFO 每条消息唯一 group id:并行度归零,吞吐垫底;组 id 按实体 id 选。
  5. 回调接口同步处理业务:网关超时重发导致重复处理;先确认后处理,业务进队列。
  6. 过滤逻辑写在消费者里:全量广播再丢弃,下游流量白付;订阅侧过滤 + 规则路由。

相关阅读

参考与致谢