第二篇:《CommitLog + ConsumeQueue:一条日志解决 Kafka 多分区随机写的难题》

标签:RocketMQ | CommitLog | ConsumeQueue | 存储模型 | 顺序写 | 零拷贝 | Kafka | 面试

适合人群:想搞懂 RocketMQ 存储为什么这么设计、被"顺序写高性能"一句话打发过的工程师

阅读收益:搞清楚 CommitLog 和 ConsumeQueue 各自存了什么、为什么 RocketMQ 要用"写一条、读两次"的模型、Kafka 的多分区存储在 Topic 数量增长后为什么会出现随机写、以及 ConsumeQueue 的 20 字节到底装了什么

前言:一个被"顺序写"掩盖的问题

几乎所有 RocketMQ 入门文章都会说:CommitLog 顺序写,所以性能高。这句话没错,但它没有回答一个更关键的问题:Kafka 的 Partition 也是顺序写,为什么 RocketMQ 还要另搞一套?

Kafka 的每个 Partition 是一个 append-only 的 segment 文件,写入也是顺序的。单看一个 Partition,Kafka 的写入路径同样是顺序写。问题不在单个 Partition 内部,在多个 Partition 之间。当一个 Broker 承载几十上百个 Topic、每个 Topic 有多个 Partition 时,生产者并发写入不同的 Partition,磁盘上实际发生的是多个文件之间的交替写入------每个文件内部是顺序的,但文件之间的写指针在来回跳。Topic 数量越多,这种"跨文件的随机性"越明显。

RocketMQ 的 CommitLog 设计,本质上是对这个问题的一个回答:把所有 Topic 的所有消息,写进同一个逻辑文件。

一、CommitLog:唯一的"真相来源"

CommitLog 是 RocketMQ 存储层最底层的文件。无论消息属于哪个 Topic、哪个队列,全部顺序追加到 CommitLog 中。单个文件默认 1 GB,文件名是 20 位的起始物理偏移量(如 00000000000000000000),写满自动滚动到下一个文件。

这个设计的效果是:在任意时刻,Broker 只有一条活跃的写入文件。无论有多少 Topic、多少队列,生产者并发写入最终都被串行追加到这条 CommitLog 的末尾。Topic 数量增长不再制造跨文件的随机写,写入路径的磁盘行为与 Topic 数量解耦。

代价是 CommitLog 里的消息是混杂的。消息 A 属于 order-topic 的队列 0,紧接着的消息 B 可能属于 inventory-topic 的队列 3。你无法从 CommitLog 本身直接知道"某个 Topic 的下一条消息在哪"。这就是 ConsumeQueue 要解决的问题。

二、ConsumeQueue:20 字节的"二级索引"

ConsumeQueue 是消费者实际"看见"的队列。每个 Topic 的每个队列对应一个 ConsumeQueue 目录,目录里的每条记录固定 20 字节:

8 字节:消息在 CommitLog 中的物理偏移量;

4 字节:消息长度;

8 字节:Tag 的哈希值,用于消费端过滤。

消费者拉取消息时,先读 ConsumeQueue 拿到偏移量和长度,再根据偏移量去 CommitLog 读出完整消息。这是一个典型的"二级索引 → 回表"模型,和 MySQL 的二级索引找聚簇索引在结构上完全同构。

这个"读两次"的流程经常被拿来和 Kafka 对比。Kafka 的消费者直接从 Partition 文件读取完整消息,一次读取。RocketMQ 需要先读索引再读数据,两次读取。看起来 Kafka 更高效,但这个对比漏掉了写入侧的收益:RocketMQ 用一次额外的索引读取,换来了写入侧对 Topic 数量不敏感的顺序写。

三、顺序写 + 零拷贝:高性能的真正组合

CommitLog 的高性能不单来自"顺序写",来自顺序写 + mmap 零拷贝的组合。

CommitLog 的写入通过 MappedFile(mmap 内存映射)完成,消息先写入 PageCache,写入 PageCache 即返回生产者 ACK(异步刷盘模式下)。消费时,如果消息还在 PageCache 中,读取直接命中内存,不产生磁盘 I/O;如果不在,操作系统按"跳跃读"的方式从磁盘预读连续数据页。

ConsumeQueue 的构建是异步的:ReputMessageService 后台线程读取 CommitLog 中已写入的消息,异步构建 ConsumeQueue 和 IndexFile。写入 CommitLog 是同步的,构建索引是异步的,索引构建的延迟不影响写入吞吐。这也是为什么 CommitLog 的写入可以保持极低的延迟------它不等待索引完成。

对比 Kafka:Kafka 依赖操作系统的 PageCache 和 sendfile 零拷贝,消费者读取时数据从 PageCache 直接到网卡,不经过用户态。RocketMQ 的 mmap 路径在写入侧同样避免了用户态到内核态的拷贝,但消费路径需要两次读取(索引 + 数据),这是它的结构性代价。

四、代价清单:这个设计不是免费的

CommitLog + ConsumeQueue 的分离设计有几个必须正视的代价:

第一,读放大。 每次消费至少两次读取(ConsumeQueue + CommitLog),索引本身也占用存储和内存。在消费吞吐敏感的场景中,这个开销真实存在。

第二,ConsumeQueue 重建。 ConsumeQueue 是异步构建的派生数据。如果 ConsumeQueue 文件损坏或落后,需要从 CommitLog 重新构建。CommitLog 是唯一真相来源,ConsumeQueue 是可再生的。这和 Kafka 的 Partition 文件"自包含"的设计不同------Kafka 的 Partition 文件本身就是完整数据,不需要额外的重建步骤。

第三,刷盘策略决定了可靠性边界。 异步刷盘(默认)写入 PageCache 就返回 ACK,Broker 断电可能丢失 PageCache 中尚未刷盘的消息;同步刷盘(flushDiskType=SYNC_FLUSH)真正写入磁盘才返回,可靠性更高但吞吐下降。消息中间件的可靠性不是免费的,它由刷盘策略和主从复制共同定义。

本篇小结

相关推荐
Alluxio18 小时前
Alluxio + OCI:为 AI 工作负载实现亚毫秒访问延迟与可扩展吞吐
运维·数据库·人工智能·分布式·缓存·ai·语言模型
阿卡好可爱1 天前
Lua 热更新不生效:改完文件线上仍是旧逻辑,package.loaded 与 upvalue 重绑定详解
数据结构·分布式·后端·架构·lua
我不会起名字3222 天前
消费回调固定 return nil:错误传播与 Kafka 位点提交的区别
后端·mongodb·kafka·go·im·故障排查
qq_452396232 天前
第十篇:《Kafka 安全加固:SASL、SSL、ACL 与多租户隔离》
安全·kafka·ssl
用户3610588626123 天前
KafkaProducer 元数据管理:send() 卡住时,它在等什么
kafka
方方洛3 天前
ray教程-28-附录G-CICD与Slurm与云上调度
人工智能·分布式·机器学习
空奈qwq3 天前
【Kafka 从入门到实战】从手写一个队列到生产级消息中间件:架构、分区、消费组、零拷贝全解析
分布式·架构·kafka
山甫aa4 天前
锁一篇搞定:乐观锁、悲观锁、CAS、数据库锁与分布式锁【从零开始的 Web 后端学习】
java·数据库·分布式·后端·学习·web