跳到内容

第 5 卷 / 第 17 章 / 第 1 课

17.1 从消息生命周期理解 Kafka、Pulsar 与 RabbitMQ ​

消息系统不是“在两个服务中间放一个缓冲区”这么简单。它定义消息由谁持久化、按什么范围排序、消费进度保存在哪里、失败后怎样重新交付,以及积压最终由谁承担。

先画完整生命周期 ​

一次可靠消息至少经过:

text
producer business state
  -> serialize + choose destination/key
  -> broker receives
  -> broker persists/replicates
  -> producer gets confirmation
  -> consumer fetches/delivery
  -> consumer changes downstream state
  -> consumer records acknowledgement/offset
  -> retention or deletion

任何两个相邻步骤之间断电或断网,都可能产生“对方已经做了,但我没收到确认”的不确定状态。可靠设计不能只写 send() 和 ack() 两个调用。

Queue 与 Log 的消费位置不同 ​

传统 queue 通常由 broker 跟踪哪些 delivery 尚未确认,确认后消息可以删除或不再给该消费者。多个消费者可竞争同一队列中的工作。

持久日志把消息追加到有序分区,消费者保存自己的 position/offset。消息不会因为一个消费者读过就立即删除,其他消费组可以独立重放,直到超过保留策略。

两者都能做工作分发和发布订阅,但成本模型不同:

  • queue 强在灵活路由、逐条确认和任务投递;
  • log 强在高吞吐追加、独立消费进度与历史重放;
  • log 的重放要求下游处理幂等并能承受旧事件;
  • queue 的大量未确认 delivery 会占用 broker 与消费者内存。

Kafka:分区日志与消费组 ​

Kafka topic 被拆成 partition,每个 partition 是带 offset 的有序追加日志。顺序保证只在单 partition 内成立,不存在默认的 topic 全局顺序。

Producer 的 key 通常参与分区选择:同一业务实体若必须按序处理,应使用稳定 key,例如 order_id。但热门 key 会形成单分区热点;增加 partition 还可能改变默认分区映射,因此 key 到 partition 的演进要纳入设计。

在传统 consumer group 模型中,一个 partition 在同一时刻由该组中的一个 consumer 负责。消费者扩到超过 partition 数量后,多出的实例没有 partition 可分。扩缩容或故障会触发 rebalance,处理中的消息可能跨消费者重新分配。Kafka 新的 share group 是另一种按记录共享和确认的模型,不能套用“消费者数不能超过分区数”;本节后面的 offset 讨论针对传统消费组。

提交的 offset 通常是下次要读取的位置:成功完成 offset=40 后提交 41。若并行处理 40、41,41 先完成而 40 尚未落地,就不能把提交位置推进到 42,否则重启会跳过 40。

offset 表示“组处理到哪里”,不是业务完成证明。提交 offset 太早会丢处理,太晚会重复处理:

text
poll record
process business side effect
commit offset

若业务写入成功后、offset 提交前崩溃,消息会再次出现。因此消费者仍要幂等。

Kafka 的保留策略按时间/大小删除旧 segment;log compaction 则逐步移除同 key 的旧记录,并为存活 key 保留最新值;压缩不是瞬间去重,删除 tombstone 也有保留期限,不能把它描述成普通消息队列的“消费即删”。

Pulsar:Broker 与持久日志存储分离 ​

Pulsar broker 负责连接、topic 所有权、调度和消费分发,BookKeeper bookie 负责持久消息日志,metadata store 负责协调元数据。Broker 可相对轻量地迁移 topic 服务权,存储容量则通过 bookie 独立扩展。

这种分离并不代表存储和计算完全没有协调成本。Ledger 创建、ensemble 写入、ack quorum、游标和 backlog 仍决定写确认与恢复行为。选 Pulsar 应来自多租户、topic 数量、存储扩展或 geo-replication 等具体需求,而不是只因为“架构更先进”。

RabbitMQ:Exchange 路由到 Queue ​

RabbitMQ 中 producer 发布到 exchange,exchange 根据 binding 把消息路由到一个或多个 queue:

text
producer -> exchange --binding--> queue -> consumer

常见 exchange:

  • direct:routing key 精确匹配;
  • topic:按以点分隔的模式匹配;
  • fanout:忽略 routing key,广播到所有绑定队列;
  • headers:按 header 条件路由。

Exchange 负责路由,不是消息最终存放位置。若消息没有匹配队列,是否退回或丢弃取决于 mandatory、alternate exchange 等配置;不能默认认为“发布成功就一定有人收到”。

Queue 类型和持久化设置影响故障语义。Publisher confirms 表示 broker 已按所选队列类型承担消息责任;consumer acknowledgement 则表示消费端已经处理到可删除/推进的位置。两类确认方向相反,不能互相替代。尤其不可路由的消息也可能收到 publisher confirm;需结合 mandatory 返回或其他路由检查判断是否进入了目标队列。

不按品牌,而按约束选型 ​

需求首先评估需要验证
事件日志、重放、多独立消费组Kafka / Pulsarpartition 数、保留期、积压磁盘与重放吞吐
复杂路由、工作队列、逐条 ackRabbitMQqueue 类型、prefetch、重投与死信策略
大量 topic、Broker 与存储独立扩展PulsarBookKeeper 运维、ledger/backlog 与多租户隔离
简单异步任务且已有数据库先评估数据库队列/outbox锁竞争、轮询、清理和扩展上限

产品对比必须固定版本、部署拓扑和可靠性配置。关闭复制和刷盘得到的基准吞吐,不能用于承诺生产可靠性下的性能。

参考 ​

下一节从故障点出发,解释 at-most-once、at-least-once 和 exactly-once 为什么都必须注明作用范围。

Built with VitePress | Software Systems Atlas