Skip to content

元数据卡

维度
难度(进阶)
前置理解 TCP/IP(Vol 4)、分布式基础(第16章)
关键词消息队列、Kafka、Pulsar、RabbitMQ、Topic/Partition、消费语义
代码语言CLI / 配置 / 伪代码

你的进度

七座分仓之间需要传送带——不是每来一批货就派人跑一趟,而是建一条自动传送管道。上游只管把货物放上传送带(生产),下游按自己的节奏取货(消费)。即使下游宕机了,货物还在传送带上排队。这就是消息队列。它的最大价值是解耦生产和消费、削峰填谷。

你的任务

这一章带你理解消息队列——它不是数据库,但和数据库一样重要。它是数据系统中的通信基础设施

你会学到:

  1. 为什么需要消息队列——解耦/削峰/异步/跨系统
  2. 消息模型:点对点(Queue)vs 发布订阅(Topic)
  3. Kafka 的架构
  4. Pulsar 的存储计算分离
  5. RabbitMQ 的路由模型
  6. 消费语义的三种级别
  7. 三者的选型对比

破局 · 溯源

1. 为什么需要消息队列?

解耦:宝物转运服务不需要知道烽火台怎么通知。它只管往"treasure_transfer"这个 Topic 里发消息。烽火台服务自己订阅这个 Topic。哪天烽火台想换信号方式了,订阅端代码改,发布端不动。

削峰填谷:平时每秒转运 1000 件宝物,红月探索季一到,每秒冲上 50000 件。仓库工匠处理 1000 件时从从容容。50000 件砸下去——库房通道直接堵死,守卫连下脚的地方都没有。消息传送带夹在中间当缓冲——旺季时货物在传送带上排队,后端仓库按自己的节奏一件一件入库。这是"削峰"。

探索队 → 消息队列 → 仓库
 50000/s 1000/s
 
 堆积 
 货物

异步:冒险者不需要等 3 个同步联络完成。灯塔立刻返回"消息已收",后端慢慢处理。

跨系统通信:微服务之间,A 语言系统(Java)和 B 语言系统(Python)之间——消息队列不关心你用什么语言,只关心你发的消息格式(通常 JSON/Protobuf 序列化)。

2. 消息模型

点对点(Queue 模型)

Producer → [Queue] → Consumer1 (竞争消费)
 Consumer2 (竞争消费)

每条消息只会被一个消费者消费。多个消费者竞争——谁先拿到谁消费。

典型场景:宝物入库调度。1000 件待入库的宝物堆在传送带起点,10 个仓库工匠各站一个取货口——干完一件取下一件。同一件宝物不会被两个工匠处理。

发布订阅(Topic 模型)

Producer → [Topic A] → Consumer Group 1 (所有消息都收)
 
 → Consumer Group 2 (所有消息都收)

消息发布到 Topic,所有订阅了这个 Topic 的 Consumer Group 都会收到消息的全量副本。一条消息广播给多个消费方。

典型场景:宝物转运事件。转运服务往传送管道里丢一条消息——"星辉护符已从龙鳞城出发"。烽火台看到了,开始点火准备接收。仓库登记服务看到了,预分配库位。图谱更新服务看到了,更新流转记录。三件事发生,互不等待。

Kafka 的 Consumer Group 是这两个模型的结合:一个 Consumer Group 内的多个 Consumer 共享 Topic 的 Partition(每条消息被组内一个 Consumer 消费——类似 Queue)。不同 Consumer Group 各自消费全量数据(类似 Pub-Sub)。

3. Kafka 核心

Kafka 是 Apache 软件基金会的顶级开源项目。设计团队来自 LinkedIn,2011 年开源。它目前是消息队列领域的事实标准——并非最适合所有场景,但生态最丰富。

Topic / Partition / Offset

Topic "treasure-transfer"

 Partition 0 [0][1][2][3][4][5][6][7][8][9] ← 追加写
 Partition 1 [0][1][2][3][4][5] 
 Partition 2 [0][1][2][3][4][5][6][7] 

 ↑ ↑
 Producer 写 Consumer 读

基本概念

  • Topic:消息的分类。类似数据库里的表。
  • Partition:Topic 里的分片。分区内消息有序,分区间无序。
  • Offset:分区内每条消息的唯一且单调递增的编号。Kafka 不保存状态,Consumer 自己记住 offset。"我读到 offset 42 了"——类似书签。
  • 日志结构存储:Kafka 只在分区尾部追加写入,不修改已有数据。写入全顺写磁盘——顺序 IO 比随机 IO 快 1000 倍,这是 Kafka 高性能的核心。
bash
# 创建 Topic
kafka-topics.sh --create --topic treasure-transfer --partitions 6 --replication-factor 3

# 生产消息
kafka-console-producer.sh --topic treasure-transfer --bootstrap-server localhost:9092
> {"item_id": "r001" "from_vault": "dragon_scale" "to_vault": "star_radiance"}

# 消费消息
kafka-console-consumer.sh --topic treasure-transfer --from-beginning --bootstrap-server localhost:9092

Partition 与顺序

关键洞察:Kafka 只保证分区内消息有序,不保证跨分区有序。 如果你需要严格的消息顺序,把相关消息路由到同一个分区(例如用 user_id 对分区数取模)。

Producer 按 key 路由到分区:
hash(item_id) % num_partitions → partition_id

同一个 item_id 的消息 → 同一个分区 → 顺序消费

Consumer Group

Topic "treasure-transfer" (6 partitions)
P0 P1 P2 P3 P4 P5
 
 
 
 
 C0 C1 C2 C3 C4 
 
 Consumer Group "transfer-processors"

每个分区被一个 Consumer 独占。分区数 = Group 内的最大并行度。

当 Consumer 加入或离开 Group——触发 Rebalance:重新分配 Partition 给 Consumer。Rebalance 期间该 Group 不可消费。

Kafka 为什么快

  1. 顺序 IO:写入全是 append-only,没有随机写。
  2. 零拷贝(Zero-Copy)sendfile() 系统调用——数据从磁盘到网卡,不经过用户空间内存。避免 2 次用户态/内核态上下文切换。
  3. 批量处理:Producer 可以攒一批消息再发送,Consumer 可以一批一批拉取,网络吞吐高。
  4. PageCache:Kafka 利用操作系统的 PageCache 做缓存,不自己实现 cache。近期写入的热数据大概率在内存中。

Kafka 的局限

  • 消息积压时性能不降这是它的设计目标,但消息丢失的风险在默认配置下存在(异步刷盘)。
  • 不支持延迟消息、死信队列等功能——这些需要额外组件或自行实现。
  • 单 Partition 内严格有序导致写请求串行——吞吐受限于单 Partition。

4. Pulsar:存储计算分离

Apache Pulsar 是 Yahoo 在 2016 年开源的消息队列。它最大的创新是存储与计算分离

架构对比

Kafka 架构(一体化):
Broker = 存储 + 计算
Broker 挂了 → 数据丢失 + 服务不可用
扩容 = 数据需要重新平衡

Pulsar 架构(分层):
Broker(无状态,计算层)↔ BookKeeper(有状态,存储层)
 扩容 Broker 零代价
BookKeeper 节点挂了 ↔ 数据在其他副本上,自动恢复

Pulsar 底层存储依赖 Apache BookKeeper——一个专门为日志存储设计的分布式系统。

BookKeeper 的设计

 
 Entry 0 Entry 1 Entry 2 
 
 
 
 
 
 Bookie 1 Bookie 2 
 (Ledger X) (Ledger X)
  • Entry:基本写入单元(类似 Kafka 的一条消息)。
  • Ledger:一组 Entry 的有序集合(类似 Kafka 的 Partition)。
  • Bookie:BookKeeper 的存储节点。
  • Ensemble:一个 Ledger 的 Entry 被条带化写入的 Bookie 集合。

Segment-Oriented Storage:Pulsar 的 Topic 不是存储在一个连续的分区文件中。Topic 被切分成多个 Segment(分段),每个 Segment 是一个 Ledger。Segment 可以独立地存储在 BookKeeper 中,独立进行副本复制和恢复。

好处

  • Broker 无状态——水平扩展快
  • 可以单独为存储扩容(BookKeeper 节点)或计算扩容(Broker 节点)
  • BookKeeper 支持无限扩容(不像 Kafka 的 Partition 数有限制)

Pulsar 的差异化能力

  • 分层存储:消息可以自动从 BookKeeper(热数据)迁移到 S3/HDFS(冷数据),不影响写入。
  • 大消息支持:直接用 Kafka 传 10MB 消息性能很差。Pulsar 的分段机制天然支持大消息,不会阻塞队列。
  • 百万级 Topic:Kafka 的 Partition 数量超过 10000-20000 后管理压力大。Pulsar 设计上支持百万级别 Topic。

5. RabbitMQ:消息路由

RabbitMQ 是古老但可靠的消息队列(2007 年发布),用 Erlang 编写。它的根本模型是 AMQP 0-9-1——Exchange/Binding/Routing。

Exchange/Binding/Routing 模型

Producer → [Exchange] → (Binding + Routing Key) → [Queue] → Consumer

 Exchange 类型决定了消息去哪

四种 Exchange 类型:
1. Direct Exchange:路由 Key 精确匹配 → 消息到指定 Queue
2. Topic Exchange:路由 Key 通配符匹配 → "task.*" 匹配 "task.created" "task.updated"
3. Fanout Exchange:广播 → 消息到所有绑定 Queue
4. Headers Exchange:按消息 Headers 中的键值对匹配
bash
# 创建 Exchange
rabbitmqadmin declare exchange name=my_exchange type=direct

# 创建 Queue
rabbitmqadmin declare queue name=treasure_transfer_queue

# 绑定:Exchange → Queue,关系 = "路由 Key"
rabbitmqadmin declare binding source=my_exchange \
 destination=treasure_transfer_queue routing_key=treasure.transferred

# 消息发布
rabbitmqadmin publish exchange=my_exchange \
 routing_key=treasure.transferred payload='{"item_id": "r001"}'

RabbitMQ vs Kafka 的关键区别

维度RabbitMQKafka
消息模型Exchange → Queue(路由驱动)Topic → Partition(日志驱动)
消息保证默认 durable + confirm = 几乎不丢需配置达到不丢
性能峰值数十万/秒数百万/秒
延迟微秒级毫秒级
堆积能力有限(内存优先设计)极强(磁盘顺序 IO)
消息顺序单一 Queue 内有序Partition 内有序
复杂路由Exchange 类型丰富路由能力弱

RabbitMQ 的优势低延迟 + 灵活路由的实时通信场景。Kafka 的优势在大规模吞吐 + 消息持久化 + 流处理的场景。

6. 消费语义

不管用哪个消息队列,消费语义是必须理解的三个概念:

At Most Once(至多一次):消息可能丢失,但绝不会重复。

Consumer 收到消息 → 立刻提交 offset → 处理业务逻辑
如果消费过程中崩溃 → offset 已提交 → 重平衡后跳过此消息 → 丢了

代价最低,风险最大。适合不需要精确计数的场景(日志审计、指标收集)。

At Least Once(至少一次):消息不会丢失,但可能重复。

Consumer 收到消息 → 处理业务逻辑 → 提交 offset
如果消费后提交前崩溃 → 重平衡后重新消费 → 处理了两次

最常见的生产配置。大部分仓库能接受"偶尔重复",不能接受"丢了"。幂等性设计(同一件宝物入库两次不报错)是配套要求。

Exactly Once(恰好一次):消息既不会丢也不会重复。

实现方式 1:在同一个本地事务里完成"消费处理 + offset 提交"
 
 BEGIN TX 
 处理消息 
 提交 offset 
 COMMIT 
 

实现方式 2:幂等写入(Idempotent Writes)+ 去重(Deduplication)
 处理消息 → 写数据库(INSERT IF NOT EXISTS)
 即使处理两次,第二次不会产生新数据

Exactly Once 的代价

  • 高:需要事务支持(Kafka 在 0.11 之后支持幂等 Producer + 事务 API)
  • 或高:需要外部幂等层
  • 性能损失明显(Kafka 的 Exactly Once 需要协调和事务日志)

生产建议:用 At Least Once + 幂等性 实现"业务上的 Exactly Once"。这是大多数微服务架构的务实选择。

7. Kafka vs Pulsar vs RabbitMQ 选型

场景推荐原因
大吞吐探索日志/转运事件/法阵指标流Kafka10 万+/秒,日志结构,堆积能力强
流处理(Flink/Spark Streaming)Kafka生态最成熟(Kafka Streams Kafka Connect)
微服务异步通信Pulsar / RabbitMQ路由灵活、延迟低、功能丰富
任务队列、工作流RabbitMQ消息确认、死信、延迟等特性最全
大量 Topic + 存储分离需求Pulsar百万 Topic,Broker 无状态,弹性好
云原生、K8s 部署Pulsar和 K8s 亲和性好,存储计算分离
团队熟悉 Java/ScalaKafka / Pulsar都是 JVM 生态
团队熟悉 Erlang 或需要轻量消息RabbitMQ配置简单运维成本低

深入冒险:Kafka 的 Exactly Once 实现

Kafka 0.11+ 加入了 Exactly Once 支持,做法是:

  1. 幂等 Producer:每个 Producer 有一个 Producer ID(PID),每条消息带序列号。Broker 接收到相同 PID + 相同序列号的重复消息时,直接丢弃。解决了"Producer 发送消息后网络抖动导致重试发送"的重复问题。

  2. 事务 API:允许以事务方式写入多个 Partition 或多个 Topic。commit 前所有消息对消费者不可见。如果 Producer 崩溃了,事务会 abort,不会出现部分写入。

  3. Consume-Transform-Produce 的 Exactly Once:从 Kafka 读消息 → 处理 → 写入 Kafka 和外部系统。Kafka 提供了这个全流程的 Exactly Once 保证,但代价是性能下降(事务 coordinator 的开销),并且只保证写入 Kafka 的环节——外部系统(数据库)的写入需要自己处理。


常见陷阱

  1. "Kafka 的 Partition 越多越好":Partition 越多吞吐越高?不完全对。每个 Partition 有 Leader 选举、元数据维护、文件句柄占用。Partition 数过多导致 Rebalance 时间变长、集群元数据膨胀。经验值:单 Broker 不超过 2000 个 Partition。

  2. "消息队列保证不丢消息":没有绝对不丢。RabbitMQ 投递到 Queue 前崩溃了、Kafka 在收到消息但未刷盘前断电了、Pulsar BookKeeper 没写够确认数。你可以在配置上无限接近不丢——同步刷盘 + acks=all + 副本数≥3——但延迟和吞吐的代价你自己承受。

  3. "RocketMQ 是更好的 Kafka":RocketMQ 和 Kafka 的设计哲学不同。RocketMQ 在消息可靠性、延迟消息、事务消息等功能上更丰富。Kafka 在大规模吞吐和流处理生态上更强。选谁取决于场景。

  4. "Consumer Group 内的 Consumer 数不能超过 Partition 数":可以超过,但超出的 Consumer 会被闲置(Partition 不够分配)。所以不一定需要 Consumer 数和 Partition 数相等——如果你需要留一些 Consumer 做故障替换,可以多启动几个。

  5. "消息队列是银弹":消息队列解决了解耦和异步问题,但引入了新问题:消息语义(重复/丢失)、消息顺序、死信处理、延迟监控、Backpressure 控制。引入消息队列前,先想清楚"不用它行不行"。


通关挑战

  • ** 热身**:说出消息队列的三种主要用途(解耦、削峰、异步)。解释 Kafka 的 Offset 是什么。
  • ** 挑战**:对比 Kafka 和 RabbitMQ 在消息路由上的设计差异。给一个"宝物转运系统需要按转运类型(入库/出库/转移)路由消息"的场景,用 RabbitMQ 的 Exchange 模型和 Kafka 的 Topic 模型分别设计。
  • ** 观察(30 分钟)**:用 Docker 启动 Kafka:docker run -d --name kafka -p 9092:9092 apache/kafka-native。创建一个 Topic,生产 1000 条消息,用 Consumer 确认消费。观察 Kafka 的日志结构存储。

验收标准

读完后你能:

  • 解释消息队列如何解耦堡垒子系统、削峰填谷
  • 区分 Queue 模型和 Topic 模型
  • 描述 Kafka 的 Topic/Partition/Offset 关系和 Consumer Group 机制
  • 理解 Pulsar 存储计算分离的优势
  • 认识 RabbitMQ 的 Exchange/Binding/Routing 模型
  • 说出 At Most Once / At Least Once / Exactly Once 的差异和应用
  • 在 Kafka / Pulsar / RabbitMQ 之间做出合理选型

常见卡点

  • Partition 和 Consumer 的关系:一个 Partition 只能被一个 Consumer(在同一个 Group 内)消费。但一个 Consumer 可以消费多个 Partition。理解了这个,你就理解了 Kafka 的并发模型。
  • Offset 是存储在哪里:消费者自己提交 offset(Kafka 0.9+ 存储在 __consumer_offsets 这个内部 Topic 里)。Consumer 崩溃后恢复,从上次提交的 Offset 开始消费。
  • Pulsar 的存储计算分离到底好在哪:Kafka 的 Broker 是存储 + 计算的统一体。Pulsar 的 Broker 无状态,BookKeeper 管理存储。好处:可独立扩缩容、Broker 崩溃不影响数据、可以热升级 Broker。

现在不需要理解

  • BookKeeper 的详细一致性协议
  • Kafka 的 Rebalance 协议(Stop The World、Eager Rebalance、Cooperative Rebalancing)
  • Pulsar 的 Geo-Replication 配置细节
  • RabbitMQ 的集群部署和镜像队列

旅人笔记

消息队列是数据系统的"静脉系统",让数据在服务之间有序流动。Kafka 用日志结构和顺序 IO 做出了高性能,Pulsar 用存储计算分离做出了弹性,RabbitMQ 用 Exchange 模型做出了灵活路由。消息队列的最大价值:解耦、削峰、异步——选型看场景,场景决定模型。

下一站预告

Built with VitePress | Software Systems Atlas