一 Page Cache内存高并发读写问题分析
由上一篇RocketMq架构分析中,我们知道RocketMq为了提高吞吐量,在broker消息落盘的时候,先将消息顺序写入page cache,然后再异步刷盘。当并发量很大的时候,broker会向page cache中大量写入消息,同时consumer又会大量的拉取消息,如果当page cache中的消息还没有同步到commitLog文件中,那么就会映射到page cache中去获取消息,那么就会出现page cache一边处理大量写入操作,一边又要处理大量读取操作,在这种高并发吞吐的读写竞争下,就会出现broker的经典问题,broker会抛出broker busy异常,原因就是broker过于繁忙,出现这种异常时,可能会阻塞住你的业务,或者执行失败,那么这种情况肯定是不能接受的。那么这种情况如何处理呢?
二 基于jvm offheap的内存读写分离机制
rocketmq中有一种机制,叫transientStorePoolEnable机制,可以称他为瞬时存储池启用机制。在压力真的大到出现broker busy的情况时,你可以开启这种机制,来实现一个内存级别的读写分离,这种机制实现的原理。
一般来说,在一个服务器上部署一个系统之后,这个系统作为一个jvm进程会运行在操作系统中,而内存一般分为3中,一种是jvm管理的堆内存,第二种是jvm的堆外内存(offheap),第三种就是page cache,也就是os管理的内存,那么transientStorePoolEnable机制就是应用了jvm的堆外内存和page cache来实现的,具体原理如下图所示:
broker会先将消息顺序写入到jvm的堆外内存,然后定时批量的写入到page cache中,consumer读取消息还是从page cache中读取,这样就实现了读写分离,完美解决了page cache中的读写资源竞争问题。
三 jvm offheap+page cache数据丢失问题
凡事都有利有弊,虽然rocketmq用jvm offheap和page cache的二级缓存方案,实现了内存级别的读写分离,成功解决了高并发下的资源争夺问题,但同时又大大提高了数据丢失的风险。
数据丢失主要分两种情况:
第一种:broker的jvm进程崩溃,因为某些原因退出了,那么jvm内存里的数据就会丢失。
第二种:broker所在的服务器崩溃了。那么这种情况下,jvm内存和page cache内存里的数据都会丢失。
如果我们的业务可以容忍丢失及小量数据的话,并且还是在服务器宕机的极端场景下才会出现的数据丢失,那么我们就可以采用page cache异步刷盘的策略,如果在这种情况下,并发量极高,导致page cache都承受不住了,那么我们可以开启多级缓存来承受高并发读写,提高rocketmq的吞吐量。
如果你的业务不允许出现数据丢失,例如金融业务等等,那么你只能牺牲性能和吞吐量,直接让消息每次都直接写入磁盘,这样就能保证数据不丢失。
高吞吐量、消息可能会丢失;数据不丢失,但是吞吐量会下降;这两种模式需要结合自己的业务去选择。
四 Broker写入与读取流程性能优化的总结
1.写入流程
默认就是直接写入os page cache里,mappedfile机制来实现的,把磁盘文件映射成一块内存,写文件=写内存,然后直接返回成功,内存级别的顺序写入,可以承受很高的并发量。并且这种模式下,jvm进程崩溃了,page cache中的数据是不会丢失的。
对于ConsumerQueue和indexFile写入,这两种是异步写入,也是性能的一个提升点,只要数据在commitLog中没有丢失,哪怕异步写入失败了,consumerQueue也能根据当前最大的offset,找到写入失败的消息,数据是可以恢复的。同样,broker的jvm崩溃了,根据commitLog数据也是可以恢复ConsumerQueue的数据的。
2.存储结构
ConsumerQueue存储结构是经过了极大的优化设计的,他的存储结构很精巧,每个消息在ConsumerQueue中存储的长度都是固定的(8个字节的offset、4字节size、8字节hash),每个文件都是存储30w消息,topic目录下有多个queue目录,queue目录下有多个磁盘文件,而每个Queue磁盘文件大小也都是一样大,都是5.72MB。
CommitLog的每个文件默认大小也是1GB,满了就写下一个文件,文件名是消息的起始offset,拉取消息时可以直接根据消息offset定位到磁盘文件,这也是提升读取效率的一大亮点。每个文件都不大,因为过大的文件,会影响数据的读写效率。
3.读取优化
根据消息逻辑offset偏移量,定位到你的ConsumerQueue的磁盘文件,在磁盘文件里,就可以根据消息体内记录的offset,直接定位到commitLog里的物理偏移量,第二次定位就可以把消息读取出来了。
高并发的情况下对page cache进行读写竞争的时候,可以开启transientStorePoolEnable机制来实现读写分离,此时读取的速度是内存级的。