引言
消息队列的存储架构是决定其可靠性、吞吐量、延迟性能的核心因素,直接影响业务场景适配能力。本文聚焦三款主流消息队列 ——Kafka(LinkedIn 开源,侧重高吞吐)、RocketMQ(阿里开源,金融级特性突出)、JMQ(京东开源,侧重高可用与灵活性),从存储模型、数据组织、索引设计等维度展开深度对比,为技术选型与架构优化提供参考。
本文将从概念辨析出发,系统拆解主流存储模型与存储引擎的设计逻辑,对比 JMQ、Kafka、RocketMQ的技术选型差异与架构设计。
一、Kafka存储架构
1.1 核心存储模型:分区日志流

Topic - 主题
Kafka学习了数据库里面的设计,在里面设计了topic(主题),这个东西类似于关系型数据库的表,此时我需要获取中国移动的数据,那就直接监听中国移动订阅的Topic即可。
Partition - 分区
Kafka还有一个概念叫Partition(分区),分区具体在服务器上面表现起初就是一个目录,一个主题下面有多个分区,这些分区会存储到不同的服务器上面,或者说,其实就是在不同的主机上建了不同的目录。这些分区主要的信息就存在了.log文件里面。跟数据库里面的分区差不多,是为了提高性能。
至于为什么提高了性能,很简单,多个分区多个线程,多个线程并行处理肯定会比单线程好得多。
Topic和partition像是HBASE里的table和region的概念,table只是一个逻辑上的概念,真正存储数据的是region,这些region会分布式地存储在各个服务器上面,对应于kafka,也是一样,Topic也是逻辑概念,而partition就是分布式存储单元。这个设计是保证了海量数据处理的基础。我们可以对比一下,如果HDFS没有block的设计,一个100T的文件也只能单独放在一个服务器上面,那就直接占满整个服务器了,引入block后,大文件可以分散存储在不同的服务器上。
注意:

Kafka 以「主题(Topic)- 分区(Partition)」为核心组织数据,每个分区本质是一个 append-only 的日志流,消息按生产顺序追加存储,保证分区内消息有序性。
优点:可以充分利用磁盘顺序读写高性能的特性。存储介质也可以选择廉价的SATA磁盘,这样可以获得更长的数据保留时间、更低的数据存储成本。
1.2 数据组织:分段日志文件

这个9936472之类的数字,就是代表了这个日志段文件里包含的起始offset,也就说明这个分区里至少都写入了接近1000万条数据了。
Kafka broker有一个参数,log.segment.bytes,限定了每个日志段文件的大小,最大就是1GB,一个日志段文件满了,就自动开一个新的日志段文件来写入,避免单个文件过大,影响文件的读写性能,这个过程叫做log rolling,正在被写入的那个日志段文件,叫做active log segment。
1.3 消息读/写过程

写消息:
读消息:
1.4 关键技术
Kafka 作为高性能的消息中间件,其超高吞吐量的核心秘诀之一就是深度依赖 PageCache + 顺序 I/O + mmap 内存映射的组合。
PageCache,中文名称为页高速缓冲存储器。它是将磁盘上的数据加载到内存中,当系统需要访问这些数据时,可以直接从内存中读取,而不必每次都去读取磁盘。这种方式显著减少了磁盘I/O操作,从而提高了系统性能。
mmap(Memory-mapped file)是操作系统提供的一种将磁盘文件与进程虚拟地址空间建立映射关系的核心技术,本质是让进程通过直接操作内存地址的方式读写文件,无需传统的 read/write 系统调用。核心价值在于零拷贝和内存式文件访问,尤其适合大文件、高吞吐、随机访问的场景。
将日志段(.log)文件映射到内存,生产者写入时直接写内存(内核异步刷盘),消费者读取时直接从内存读取,实现超高吞吐(Kafka 的 “顺序写 + mmap” 是其高性能核心);

零拷贝流程示意图
零拷贝过程:
1.5 设计优势
二、RocketMQ存储架构
Kafka的每个Partition都是一个完整的、顺序写入的文件,但当Partition数量增多时,从操作系统的角度看,这些写入操作会变得相对随机,这可能会影响写入性能。
2.1 核心存储模型:分离式设计
RocketMQ采用「CommitLog + ConsumeQueue + IndexFile」三层结构,彻底分离数据存储与索引查询:
CommitLog:消息的原始日记本
CommitLog是RocketMQ存储消息的物理文件,所有消息都会按到达顺序写入这个文件。你可以把它想象成一本不断追加的日记本——每条消息都是按时间顺序记录的新日记。
// 消息存储的核心逻辑简化示例(非源码)
public void putMessage(Message message) {
// 1. 将消息序列化为字节数组
byte[] data = serialize(message);
// 2. 计算消息物理偏移量
long offset = commitLog.getMaxOffset();
// 3. 将数据追加到CommitLog文件末尾 commitLog.append(data);
// 4. 返回消息的全局唯一物理偏移量
return offset;
}
消息写入CommitLog时有三个关键特性:
举个例子,当生产者发送三条消息时,CommitLog文件可能长这样:
0000000000000000000(文件1,1GB)
2|--消息A(offset=0)
3|--消息B(offset=100)
4|--消息C(offset=200)
500000000001073741824(文件2,起始偏移量1073741824)
温馨提示:虽然CommitLog是顺序写,但读取时需要配合索引结构,否则遍历文件找消息就像大海捞针。
消费队列ConsumeQueue:消息的快速目录
如果每次消费都要扫描CommitLog,性能会惨不忍睹。于是RocketMQ设计了ConsumeQueue——它是基于Topic和Queue的二级索引文件。
每个ConsumeQueue条目包含三个关键信息(固定20字节):
1| CommitLog Offset (8字节) | Message Size (4字节) | Tag Hashcode (8字节) |
这相当于给CommitLog里的消息做了一个目录:
TopicA-Queue0的ConsumeQueue
2|--0(对应CommitLog偏移0的消息A)
3|--100(对应CommitLog偏移100的消息B)
4|--200(对应CommitLog偏移200的消息C)
当消费者拉取TopicA-Queue0的消息时:
关键设计点:
索引文件IndexFile:消息的全局字典
如果需要根据MessageID或Key查询消息,ConsumeQueue就不够用了。这时候就要用到IndexFile这个全局索引。
IndexFile的结构类似HashMap:
当写入消息时:
// 索引构建过程简化示意
public void buildIndex(Message message) {
// 计算Key的hash值
int hash = hash(message.getKey());
// 定位到对应的Slot槽位
int slotPos = hash % slotNum;
// 在Index区域追加新条目
indexFile.addEntry(hash, message.getCommitLogOffset());
}
查询时通过两次查找快速定位:
性能优化必知:
理解这些底层机制,下次遇到消息查询性能问题或者磁盘IO瓶颈时,就知道该从CommitLog的写入模式还是ConsumeQueue的索引结构入手排查了。
2.2 数据流转机制
存储过程全景图
现在把各个模块串起来看消息的生命周期:
整个过程就像图书馆的管理系统:
2.4 设计优势
三、JMQ存储架构
JMQ的消息存储分别参考了Kafka和RocketMQ存储设计上优点,并根据京东内部的应用场景进行了改进和创新。
3.1 核心存储模型:分区日志 + 队列兼容

JMQ存储的基本单元是PartitionGroup。在同一个Broker上,每个PartitionGroup对应一组消息文件(Journal Files),顺序存放这个Topic的消息。
与Kafka类似,每个Topic包含若干Partition,每个Partition对应一组索引文件(Index Files),索引中存放消息在消息文件中的位置和消息长度。消息写入时,收到的消息按照对应的PartitionGroup写入依次追加写入消息文件中,然后异步创建索引并写入对应Partition的索引文件中。
以PartionGroup为基本存储单元的设计,在兼顾灵活性的同时,具有较好的性能,并且单个PartitionGroup可以支持更多的并发。
3.2 消息读/写过程

写消息:
JMQ的写操作使用DirectBuffer作为缓存,数据先写入DirectBuffer,再异步通过FileChannel写入到文件中。
读消息:
JMQ采用定长稠密索引设计,每个索引固定长度。
JMQ消费读操作99%以上都能命中缓存(JMQ设计的堆外内存与文件映射的一种缓存机制),避免了Kafka可能遇到的Cache被污染,影响性能和吞吐的问题。同时直接读内存也规避了RocketMQ在读取消息存储的日志数据文件时容易产生较多的随机访问读取磁盘,影响性能的问题。(当没有命中缓存时,会默认降级为通过Mmap的方式读取消息)。
四、竞品对比分析
| JMQ | Kafka | |
| 存储模型 | 以PartitionGroup为基本存储单元,支持高并发写入 | 以Partition为基本存储单元,支持灵活的数据复制和迁移 |
| 消息写入性能 | - 单副本异步写入性能与 Kafka 相当 - 三副本异步写入性能优于 Kafka | - 单副本异步写入性能与 JMQ 相当 - 三副本异步写入性能略低于 JMQ |
| 同步写入性能 | - 同步写入性能稳定,几乎不受网络延迟影响 | - 同步写入性能受网络延迟影响较大,稳定性略逊于 JMQ |
| 多分区性能 | - 多分区异步写入性能与 Kafka 相当 - 同步写入性能略低于 Kafka | - 多分区同步写入性能更稳定,适合高并发场景 |
| 副本机制 | 支持异步复制,副本间数据同步性能较好 | 支持异步和同步复制,副本机制成熟,适合复杂部署 |
| 跨机房部署 | - 同步写入性能基本不受影响 - 异步写入性能下降 | - 同步写入性能受网络延迟影响较大 - 异步写入性能下降 |
| 适用场景 | - 对同步写入性能要求高 - 副本异步吞吐要求高 - 大规模微服务集群 | - 复杂分区的高并发同步写入 - 大规模分布式系统 - 多语言生态支持丰富 |
在单副本场景下,JMQ与Kafka的单机写入性能均十分出色,均可达到网络带宽上限。
然而,在更贴近生产环境的三副本场景中,两者特性出现分化:
JMQ在三副本异步写入下的极限吞吐优势明显,且在跨机房部署时,其同步写入性能表现良好,几乎不受网络延迟影响;而Kafka则在多分区同步写入场景下展现出更稳定的性能,衰减小于JMQ。在大部分异步吞吐场景及不同消息体下的性能趋势上,两者表现相当。
综上所述,JMQ尤其适合对同步写入性能和副本异步吞吐有极高要求的场景,而Kafka在复杂分区的高并发同步写入方面适应性更广。