RocketMQ高性能之底层存储设计

说在前面

RocketMQ在底层存储上借鉴了Kafka,可是也有它独到的设计,本文主要关注深入影响着RocketMQ性能的底层文件存储结构,中间会穿插一点点Kafka的东西以做为对比。数据库

例子

Commit Log,一个文件集合,每一个文件1G大小,存储满后存下一个,为了讨论方即可以把它当成一个文件,全部消息内容所有持久化到这个文件中;Consume Queue:一个Topic能够有多个,每个文件表明一个逻辑队列,这里存放消息在Commit Log的偏移值以及大小和Tag属性。centos

为了简述方便,来个例子缓存

假如集群有一个Broker,Topic为binlog的队列(Consume Queue)数量为4,以下图所示,按顺序发送这5条内容各不相同消息。网络

发送消息并发

先简单关注下Commit Log和Consume Queue。异步

RMQ文件全貌oop

RMQ的消息总体是有序的,因此这5条消息按顺序将内容持久化在Commit Log中。Consume Queue则用于将消息均衡地排列在不一样的逻辑队列,集群模式下多个消费者就能够并行消费Consume Queue的消息。性能

Page Cache

了解了每一个文件都在什么位置存放什么内容,那接下来就正式开始讨论这种存储方案为何在性能带来的提高。优化

一般文件读写比较慢,若是对文件进行顺序读写,速度几乎是接近于内存的随机读写,为何会这么快,缘由就是Page Cache。spa

Free命令

先来个直观的感觉,整个OS有3.7G的物理内存,用掉了2.7G,应当还剩下1G空闲的内存,但OS给出的倒是175M。固然这个数学题确定不能这么算。

OS发现系统的物理内存有大量剩余时,为了提升IO的性能,就会使用多余的内存当作文件缓存,也就是图上的buff / cache,广义咱们说的Page Cache就是这些内存的子集。

OS在读磁盘时会将当前区域的内容所有读到Cache中,以便下次读时能命中Cache,写磁盘时直接写到Cache中就写返回,由OS的pdflush以某些策略将Cache的数据Flush回磁盘。

可是系统上文件很是多,即便是多余的Page Cache也是很是宝贵的资源,OS不可能将Page Cache随机分配给任何文件,Linux底层就提供了mmap将一个程序指定的文件映射进虚拟内存(Virtual Memory),对文件的读写就变成了对内存的读写,能充分利用Page Cache。不过,文件IO仅仅用到了Page Cache仍是不够的,若是对文件进行随机读写,会使虚拟内存产生不少缺页(Page Fault)中断。

映射缺页

每一个用户空间的进程都有本身的虚拟内存,每一个进程都认为本身全部的物理内存,但虚拟内存只是逻辑上的内存,要想访问内存的数据,还得经过内存管理单元(MMU)查找页表,将虚拟内存映射成物理内存。若是映射的文件很是大,程序访问局部映射不到物理内存的虚拟内存时,产生缺页中断,OS须要读写磁盘文件的真实数据再加载到内存。如同咱们的应用程序没有Cache住某块数据,直接访问数据库要数据再把结果写到Cache同样,这个过程相对而言是很是慢的。

可是顺序IO时,读和写的区域都是被OS智能Cache过的热点区域,不会产生大量缺页中断,文件的IO几乎等同于内存的IO,性能固然就上去了。

说了这么多Page Cache的优势,也得稍微提一下它的缺点,内核把可用的内存分配给Page Cache后,free的内存相对就会变少,若是程序有新的内存分配需求或者缺页中断,刚好free的内存不够,内核还须要花费一点时间将热度低的Page Cache的内存回收掉,对性能很是苛刻的系统会产生毛刺。

刷盘

刷盘通常分红:同步刷盘和异步刷盘

刷盘方式总览

同步刷盘

在消息真正落盘后,才返回成功给Producer,只要磁盘没有损坏,消息就不会丢。

同步刷盘——GroupCommit

通常只用于金融场景,这种方式不是本文讨论的重点,由于没有利用Page Cache的特色,RMQ采用GroupCommit的方式对同步刷盘进行了优化。

异步刷盘

读写文件充分利用了Page Cache,即写入Page Cache就返回成功给Producer,RMQ中有两种方式进行异步刷盘,总体原理是同样的。

RMQ异步刷盘方式

刷盘由程序和OS共同控制

先谈谈OS,当程序顺序写文件时,首先写到Cache中,这部分被修改过,但却没有被刷进磁盘,产生了不一致,这些不一致的内存叫作脏页(Dirty Page)。

脏页原理

脏页设置过小,Flush磁盘的次数就会增长,性能会降低;脏页设置太大,性能会提升,但万一OS宕机,脏页来不及刷盘,消息就丢了。

Linux脏页配置

上图为centos系统的默认配置,dirty_ratio为阻塞式flush的阈值,而dirty_background_ratio是非阻塞式的flush。

RMQ消费场景对性能的影响

RMQ想要性能高,那发送消息时,消息要写进Page Cache而不是直接写磁盘,接收消息时,消息要从Page Cache直接获取而不是缺页从磁盘读取。

好了,原理回顾完,从消息发送和消息接收来看RMQ中被mmap后的Commit Log和Consume Queue的IO状况。

RMQ发送逻辑

发送时,Producer不直接与Consume Queue打交道。上文提到过,RMQ全部的消息都会存放在Commit Log中,为了使消息存储不发生混乱,对Commit Log进行写以前就会上锁。

Commit Log顺序写

消息持久被锁串行化后,对Commit Log就是顺序写,也就是常说的Append操做。配合上Page Cache,RMQ在写Commit Log时效率会很是高。

Commit Log持久后,会将里面的数据Dispatch到对应的Consume Queue上。

Consume Queue顺序写

每个Consume Queue表明一个逻辑队列,是由ReputMessageService在单个Thread Loop中Append,显然也是顺序写。

消费逻辑底层

消费时,Consumer不直接与Commit Log打交道,而是从Consume Queue中去拉取数据

Consume Queue顺序读

拉取的顺序从旧到新,在文件表示每个Consume Queue都是顺序读,充分利用了Page Cache。

光拉取Consume Queue是没有数据的,里面只有一个对Commit Log的引用,因此再次拉取Commit Log。

Commit Log随机读

Commit Log会进行随机读

Commit Log总体有序的随机读

但整个RMQ只有一个Commit Log,虽然是随机读,但总体仍是有序地读,只要那整块区域还在Page Cache的范围内,仍是能够充分利用Page Cache。

运行中的RMQ磁盘与网络状况

在一台真实的MQ上查看网络和磁盘,即便消息端一直从MQ读取消息,也几乎看不到进程从磁盘拉数据,数据直接从Page Cache经由Socket发送给了Consumer。

对比Kafka

文章开头就说到,RMQ是借鉴了Kafka的想法,同时也打破了Kafka在底层存储的设计。

Kafka分区模型

Kafka中关于消息的存储只有一种文件,叫作Partition(不考虑细化的Segment),它履行了RMQ中Commit Log和Consume Queue公共的职责,即它在逻辑上进行拆分存,以提升消费并行度,又在内部存储了真实的消息内容。

Partition顺序读写

这样看上去很是完美,无论对于Producer仍是Consumer,单个Partition文件在正常的发送和消费逻辑中都是顺序IO,充分利用Page Cache带来的巨大性能提高,可是,万一Topic不少,每一个Topic又分了N个Partition,这时对于OS来讲,这么多文件的顺序读写在并发时变成了随机读写。

Kafka Partition随机读写状况

这时,不知道为何,我忽然想起了「打地鼠」这款游戏。对于每个洞,我打的地鼠老是有顺序的,可是,万一有10000个洞,只有你一我的去打,无数只地鼠有先有后的出入于每一个洞,这时还不是随机去打,同窗们脑补下这场景。

固然,思路很好的同窗立刻发现RMQ在队列很是多的状况下Consume Queue不也是和Kafka相似,虽然每个文件是顺序IO,但总体是随机IO。不要忘记了,RMQ的Consume Queue是不会存储消息的内容,任何一个消息也就占用20 Byte,因此文件能够控制得很是小,绝大部分的访问仍是Page Cache的访问,而不是磁盘访问。正式部署也能够将Commit Log和Consume Queue放在不一样的物理SSD,避免多类文件进行IO竞争。

相关文章
相关标签/搜索