3. 消息队列选型与优化:Kafka、Pulsar、RabbitMQ 对比,分区策略与吞吐量调优

消息队列选型,说白了就是选一个能扛住你业务峰值的「数据管道」。我见过太多团队,上来就拍脑袋选 Kafka,结果业务场景是小规模异步解耦,运维成本反而比 RabbitMQ 高出一大截。也见过用 RabbitMQ 硬扛日均百亿消息的,最后集群频繁脑裂。

这一章,咱们就掰开揉碎聊聊这三个主流 MQ 的差异。我会结合我在生产环境踩过的坑,帮你理清选型思路和调优手段。

3.1 三大消息队列核心差异

先看一张对比表,心里有个底:

维度 Kafka Pulsar RabbitMQ
存储模型 分区日志(PageCache + 磁盘) 分层存储(BookKeeper + 可卸载至 S3) 队列/交换机(内存 + 可选惰性队列)
吞吐量 百万级/秒(单分区顺序写) 百万级/秒(读写分离) 万级/秒(内存瓶颈)
消息保留 基于时间/大小(日志段删除) 基于时间/大小/游标(可无限保留) ACK 后删除(TTL 有限)
消费模型 Pull + 消费者组重平衡 Pull + 游标管理(无重平衡) Push/Pull + AMQP 路由
运维复杂度 中(依赖 ZooKeeper/KRaft) 高(BookKeeper + Broker 双组件) 低(Erlang 集群自动)

核心结论:Kafka 适合「大吞吐、重日志、顺序消费」;Pulsar 适合「多租户、无限留存、地理复制」;RabbitMQ 适合「低延迟、灵活路由、中小规模」。

3.2 分区策略:吞吐量的命门

分区数不是越多越好。我见过一个团队把 Kafka 分区设到 1000 个,结果 Broker 上文件句柄爆了,Leader 选举花了 40 分钟。为什么?

  • 分区 = 文件句柄 + 线程数:每个分区对应一个日志段目录,Broker 需要管理大量文件。
  • 分区 = 重平衡粒度:分区越多,消费者组重平衡时间越长(尤其是 Eager 协议)。
  • 分区 = 顺序写退化:分区过多导致磁盘 IO 随机化,顺序写优势被削弱。

3.2.1 分区数计算公式

我个人习惯用这个公式做初始估算:

分区数 = max(目标吞吐量 / 单分区吞吐量, 消费者线程数)

# 举例:目标 100MB/s,单分区约 20MB/s(机械盘顺序写)
# 消费者线程 8 个
分区数 = max(100 / 20, 8) = 5

经验值:Kafka 单分区吞吐量在 SSD 上约 50-80MB/s,机械盘约 15-25MB/s。Pulsar 由于读写分离,单分区吞吐量更高,但分区数建议控制在 Broker 核心数 × 2 以内。

3.2.2 分区分配策略

我曾经在生产环境遇到过「数据倾斜」——某个分区堆积了 80% 的消息,其他分区空闲。原因很简单:分区 key 设计不合理。

  • 轮询策略:适合无 key 消息,均匀分布,但无法保证顺序。
  • Hash 策略:相同 key 进入同一分区,保证局部顺序。但 key 分布不均时会导致倾斜。
  • 自定义分区器:比如按用户 ID 范围分段,或者用一致性哈希。

避坑指南:我曾经用用户手机号后四位做分区 key,结果尾号 0-3 的用户占了 60%,直接导致三个分区打满。后来改用用户 ID 的 CRC32 取模,才解决倾斜问题。

3.3 吞吐量调优实战

调优不是玄学,是参数和硬件之间的博弈。下面是我在几个项目里总结的「三板斧」。

3.3.1 生产者端调优

参数 推荐值 说明
batch.size 65536 (64KB) 批次越大吞吐越高,但延迟增加
linger.ms 5-20 等待批次填满的时间,不要设 0
compression.type lz4 或 zstd 压缩比高,CPU 换带宽
acks 1 Leader 确认即可,不等待所有副本
max.in.flight.requests 5 管道化请求,提升吞吐

核心思路:用「攒批 + 压缩 + 异步」换取吞吐量。如果你对延迟敏感(< 10ms),可以适当降低 batch.size 和 linger.ms。

3.3.2 Broker 端调优

  • 磁盘选择:SSD 是必须的,尤其是 Kafka 的日志刷盘。我建议用 NVMe 做数据盘,SATA SSD 做日志盘。
  • PageCache 调优:Kafka 重度依赖 PageCache,建议给 OS 留 30% 以上内存。别把堆内存设太大,否则 PageCache 不够用。
  • 网络线程模型:num.network.threads 设为 CPU 核心数 × 2,num.io.threads 设为 CPU 核心数 × 1.5。
  • 日志刷盘:flush.messages 设 10000,flush.ms 设 1000。别用同步刷盘,吞吐量会掉一个数量级。

3.3.3 消费者端调优

消费者调优的核心是「拉取速率」和「处理速率」的匹配。我见过一个案例:消费者处理一条消息要 100ms,但拉取窗口设了 500 条,结果内存 OOM。

  • max.poll.records:根据单条消息大小和处理耗时调整。一般设 500-1000。
  • fetch.max.bytes:单次拉取最大数据量,建议 50MB。
  • enable.auto.commit:关掉!手动提交 offset,配合批量处理。
  • 消费线程模型:每个分区一个消费线程,或者用线程池异步处理。

我的习惯:消费者端用「拉取 → 批量处理 → 批量提交」模式。比如每拉取 1000 条或每 5 秒提交一次 offset,这样能减少提交频率,提升吞吐。

3.4 消息队列选型决策树

下面这张 SVG 图是我自己总结的选型逻辑,你可以直接拿来用:

消息队列选型决策树 消息队列选型 吞吐量 > 50万/秒? 需要灵活路由? 多租户/无限留存? → Kafka → RabbitMQ → Pulsar 选型说明: • 吞吐量 > 50万/秒:Kafka 是首选,分区日志模型天然支持高吞吐。 • 需要灵活路由(Topic 交换机、死信队列):RabbitMQ 更合适。 • 多租户隔离 + 无限消息留存:Pulsar 的分层存储架构是唯一解。 • 混合场景:比如高吞吐 + 灵活路由,可以考虑 Pulsar 或 Kafka + 轻量路由层。 • 运维能力:团队 Erlang 经验少就别碰 RabbitMQ 大集群;Kafka 和 Pulsar 的运维 工具链更成熟(Cruise Control、Pulsar Manager)。 • 我个人的建议:如果团队只有 3-5 人,优先选 Kafka,社区大、坑少、资料多。

3.5 避坑指南:我踩过的三个坑

最后分享几个真实教训,希望能帮你少走弯路。

  1. Kafka 分区数设太多:之前有个项目分区数设了 200,结果 Broker 频繁 Full GC。后来发现是分区过多导致元数据膨胀。建议单 Broker 分区数不超过 100。
  2. RabbitMQ 内存阈值没设:RabbitMQ 默认内存阈值是 40%,但有个团队没改,结果流量高峰时直接触发 Flow Control,消息全堵在生产者端。建议设到 60%-70%,并开启镜像队列。
  3. Pulsar BookKeeper 磁盘写满:Pulsar 的 BookKeeper 默认不会自动清理过期数据,需要配合 retention 策略。我遇到过磁盘写满导致 Bookie 宕机,整个集群不可写。后来加了监控和自动清理脚本才解决。

最后提醒一句:选型没有银弹。别迷信某个 MQ 的「官方性能数据」,那都是在理想环境下测的。你的网络、磁盘、数据模型、消费逻辑,都会影响最终效果。最好的办法是拿你的真实业务流量做压测,用数据说话。