RocketMQ与Kafka IO区别
引言
简单说说RocketMQ
与Kafka
的IO区别。如果文章写错了,请下面留言给我。
Kafka 存储原理
Kafka
在创建topic
时会指定partition数量
,在物理层面上有几个partition
就有几个文件夹
,类似这样:
topic与partition
topic名称-partition序号
topic
名称为test
,partition
数量为3
,在存储路径下(默认是/tmp/kafka****
)就会有这3个文件夹:test-0
、test-1
、test-2
。
这里还有个
consumer
根据partition
数量的负载均衡
公式:
M = Math.ceil(PartitionSize/ConsumerCount)
Ci = [P(i * M),P((i + 1) * M -1)]
例如上面3个分区,那么2个消费者的情况下
M = Math.ceil(3/2) = 2
C0 = [P(0 * 2),P(1 * 1)] = [P(0),P(1)] 消费者0分配到分区0,1
C1 = [P(1 * 2),P(2 * 1)] = [P(2),P(2)] 消费者1分配到分区2
partition与segment
- 每个
partition
下又有多个segment
文件。 - 每个
segment
对应3个文件
,index
,log
,timeindex
(0.8版本之前没有timeindex
),看名字就知道是将索引和日志本身区分开了。 segment
数值最大为64位long
大小,起始序号为上一个segment
的最后一条消息
的offset
值。
举个栗子:比如上面的名为test
的topic
,我们进入test-0
文件夹,里面会有这些文件:
00000000000000000000.index
(默认为10M大小的0x00填充文件,记录逻辑偏移对应的物理偏移)00000000000000000000.log
00000000000000000000.timeindex
(默认为(10M-4B)大小的0x00填充文件,记录当前时间戳对应的当前偏移-基础偏移)leader-epoch-checkpoint
(leader-epoch-checkpoint
中保存了每一任leader
开始写入消息时的offset
会定时更新,follower
被选为leader
时会根据这个确定哪些消息可用,这个文件每个分区下只有一个
)
由于是刚创建的topic
,那么起始offset
肯定就是0
,这里说明一下如果这个segment
最后一条消息偏移为3999
,第二个segment
结构开始从4000
存储消息,那么文件就会是00000000000000003999.index
这样开始。
如何利用索引确定消息位置
通过消息的逻辑offset
二分查找即可确定index文件名
,通过index文件
再次查找距离最近的元数据逻辑offset
去确定log文件
的物理offset
,有了物理offset
即可使用sendfile
(java中对应的的是channel.transferTo
)进行零拷贝发送了。(额外科普一条:In Linux kernels before 2.6.33, out_fd must refer to a socket. Since Linux 2.6.33 it can be any file。
)
缺点
:在分区数量过多
或topic数量过多
的情况下,会打开很多个fd
,无论是否使用内存映射或零拷贝技术,在分区数量过多的情况下它写入的性能都是直线下降的。这就是为什么Kafka
大多会用来做日志,而不是高并发复杂业务场景下的流量削峰。
消息有序性
在CloudStream
的文章中我略过了partitionKeyExpressiond
的配置,这里正好补上,继承PartitionKeyExtractorStrategy
后自己可以实现,其意义在于自己选择分区,这就说明了一件事,在生产消息
的时候我们可以确保某一类的消息到指定的partition
中(在RocketMQ入门
中讲过顺序消息),也就保证了消息的有序性
,因为Kafka是顺序存储
。
需要注意的是,分布式情况轮询或多线程下,虽然接收到消息是有序的,但是你无法保证是否出现retry或处理这条消息时的时间多久,也许最后要做的事会变成无序状态,比如入库。
RocketMQ 存储原理
官方原图:
RocketMQ 设计 官方文档
消息存储架构图中主要有下面三个跟消息存储相关的文件构成。
-
CommitLog
:消息主体以及元数据的存储主体,存储Producer
端写入的消息主体内容,消息内容不是定长的。单个文件大小默认1G ,文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件。 -
ConsumeQueue
:消息消费队列,引入的目的主要是提高消息消费的性能,由于RocketMQ
是基于主题topic
的订阅模式,消息消费是针对主题进行的,如果要遍历commitlog
文件中根据topic
检索消息是非常低效的。Consumer
即可根据ConsumeQueue
来查找待消费的消息
。其中,ConsumeQueue(逻辑消费队列)
作为消费消息的索引
,保存了指定Topic下的队列消息
在CommitLog
中的起始物理偏移量offset
,消息大小size
和消息Tag
的HashCode值
。consumequeue
文件可以看成是基于topic的commitlog索引文件
,故consumequeue
文件夹的组织方式如下:topic/queue/file
三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}
。同样ConsumeQueue文件采取定长设计,每一个条目共20个字节,分别为8字节的commitlog物理偏移量
、4字节的消息长度
、8字节tag hashcode
,单个文件由30W个条目组成,可以像数组一样随机访问
每一个条目,每个ConsumeQueue文件大小约5.72M; -
IndexFile
:IndexFile(索引文件)
提供了一种可以通过key或时间区间来查询消息的方法。Index文件的存储位置是:$HOME \store\index${fileName}
,文件名fileName
是以创建时的时间戳命名
的,固定的单个IndexFile
文件大小约为400M,一个IndexFile
可以保存2000W个索引
,IndexFile
的底层存储设计为在文件系统中实现HashMap结构
,故rocketmq
的索引文件其底层
实现为hash索引
。
大概就是说RocketMQ
的物理IO句柄,就只有CommitLog
和ConsumeQueue
(理解为kafka中index文件
的作用,当然消息长度等等也在这里面了),虽然也有分段设计,但是没有多个分区对应多个log的情况了,rocketmq将所有消息顺序写入一个文件中,虽然单机情况下吞吐量不如Kafka
,但是在高并发
和topic数量增多
的情况下,依然稳定。