前言
上篇文章王子和大家讨论了一下RocketMQ生产者发送消息的底层原理,今天我们接着这个话题,继续深入聊一聊RocketMQ的Broker是如何持久化的。
Broker的持久化对于整个RocketMQ的运行起着至关重要的作用,为什么这么说呢?
其实解释起来很容易,因为消息中间件要实现的功能不仅仅是消息的发送和接收,它本身还要有很强大的存储能力,把来自各个系统的消息持久化到磁盘上。
只有这样,在其他系统消费消息时才能从磁盘中读取想要的消息。
如果不持久化到磁盘上,而是通过内存存储消息,一是内存无法存储大量的消息,二是出现故障消息将会丢失。
所以,Broker的持久化是比较核心的机制,它决定了MQ消息吞吐量,和保证消息的可靠性。
今天我们就来聊一聊,Broker是如何持久化的。
CommitLog
首先我们思考一下,当Broker接收到生产者发来的消息后,内部会做些什么呢?
这时候我们就引入了一个新的概念CommitLog,它就是一个日志文件。Broker接收到消息后的第一步就是把消息写到这个日志文件中,而且是顺序写入的。
那么CommitLog文件是怎么存储的呢?
它可不是直接一个日志文件进行存储的,而是分成很多份存储在磁盘中,每一份限定了最大1GB。
当Broker接收到新的消息时就会顺序的追加到日志文件的末尾,而当文件大小到了1GB,就会新创建一个日志文件,新的消息就会写入新的日志文件,循环往复。
MessageQueue是如何存储的
刚才我们说了,Broker接收到消息要把消息存储到日志文件中,那么上篇文章我们讲的MessageQueue又是如何存储的呢?
这时候我们就又引出了一个新的概念,ConsumeQueue文件,每个MessageQueue会存储到多个ConsumeQueue文件中。
再给大家更详细的说一下,其实Broker的磁盘上是有类似$HOME/store/consumequeue/{topic}/{queueid}/{filename}这样的目录的:
这里面的{topic}代表的就是我们声明的Topic,{queueid}代表的就是我们单个的MessageQueue,而{filename}就是我们的存储文件多个ConsumeQueue文件了。
在ConsumeQueue文件中其实存储的是一条消息对应在CommitLog中的offset(偏移量)。
这么说可能小伙伴们不太理解,别急,王子接下来画个图跟大家仔细的聊一聊。
现在我们假设生产者发送了一个消息到一个Topic中,这个Topic的名字叫orderTopic,并指定了它有两个MessageQueue:MessageQueue1、MessageQueue2。
那么Broker接收到消息后,首先把数据存放到了CommitLog中。
然后每个MessageQueue对应了一个ConsumeQueue(实际可以是多个),对应的是ConsumeQueue1、ConsumeQueue2。
那么现在在Broker的磁盘上就有了两个路径文件:
$HOME/store/consumequeue/orderTopic/MessageQueue1/ConsumeQueue1
$HOME/store/consumequeue/orderTopic/MessageQueue2/ConsumeQueue2
然后呢,在写入CommitLog文件后,会同时将消息在CommitLog文件中的位置(偏移量offset)写入到对应的ConsumeQueue中。
所以,ConsumeQueue中存储的其实是消息的引用地址,同时还会存储消息的tag、hashcode以及消息的长度。每条数据20个字节进行存储。
当我们获取消息的时候就可以通过ConsumeQueue中的引用地址去CommitLog中找到我们想要的消息了。
这样解释起来相信小伙伴们应该可以明白了吧。
写入CommitLog的性能探索
接下来我们聊下一个话题,当Broker获取到消息写入CommitLog中时,是如何保证写入性能的呢?
为什么要优化写入的性能呢?因为这一步骤的写入性能直接影响着Broker的吞吐量。
如果每次写入消息速度很慢,那么每秒能处理的消息数量自然就会跟着大大减少了,这个相信小伙伴们都可以理解。
那么RocketMQ针对这一步骤是怎么做的呢?
实际上,它采用了OS操作系统的PageCache和顺序写两个机制,来提升了写入CommitLog的性能。
首先我们之前就聊过了,Broker在写入CommitLog时,采用的是顺序写入的方式,每次只要在文件的末尾追加写入数据就可以了,这样的方式要比随机写入的方式性能提升不少。
另外,其实写入CommitLog日志时,并不是直接将数据写入到磁盘文件中的,而是先写入OS操作系统的PageCache中,然后由OS操作系统的后台线程选择时间,异步化的把PageCache中的数据同步到物理磁盘中的。
所以通过顺序写+写入PageCache+异步刷盘这么一套优化过后,其实写入CommitLog的性能甚至可以和直接写入内存相媲美。
也正是因为这,才保证了Broker的高吞吐量。
同步刷盘和异步刷盘
刚才我们聊到的就是异步刷盘的策略,Broker在写入OS的PageCache之后,就直接返回给生产者ACK了。
这样,生产者就会认为我的消息已经成功发送给了Broker。那么这样的策略是否会存在问题呢?
其实简单想一想就会明白,问题肯定是存在的。
因为OS操作系统的PageCache也是一种缓存,如果写入了缓存就认为发送成功没有问题了,那如果缓存还没来得及刷新到物理磁盘,这个时候Broker挂掉了,会发生什么呢?
当然,这个时候缓存中的消息数据就会丢失,无法恢复!
所以说技术的选择上是有舍有得的,如果选择了异步刷盘的策略,就会大大提高Broker的吞吐量,但同时也会有丢失消息的隐患。
那么什么是同步刷盘策略呢?
其实同步刷盘就是跳过了PageCache这一步骤,当生产者发送消息给Broker后,Broker必须把数据存到真实的物理磁盘中之后才会返回ACK给生产者,这个时候生产者才会断定消息发送成功了。
消息一旦写入物理磁盘,除非是磁盘硬件损坏,导致数据丢失。否则我们就可以认为消息是不会丢失的了。
在同步刷盘策略下,假如没有刷到物理磁盘上时Broker挂掉了,这时是不会返回ACK给生产者的,那么生产者会认为发送失败,进行消息重发机制就可以了。
当主从切换完成后,消息就会正常的写入Broker了。所以这种策略是可以保证消息不会丢失的。
还是那句话,技术的选择是有舍有得的,使用同步刷盘策略保证了消息的可靠性,但同时会降低Broker的吞吐量。
所以具体选择哪种策略,还要根据实际的业务需求来定夺了。
总结
好了,今天王子和大家深入的聊了聊Broker是如何持久化的,介绍了什么是CommitLog,什么是ConsumeQueue。
又和大家聊了聊写入CommitLog的实现细节,如何通过PageCache保证性能。
最后和大家介绍了同步刷盘、异步刷盘的不同之处。
那今天的分享就到这里了,我们下期再见。