大家好,我是yes。

咱们都晓得 RocketMQ 和 Kafka 音讯都是存在磁盘中的,那为什么音讯存磁盘读写还能够这么快?有没有做了什么优化?都是存磁盘它们两者的实现之间有什么区别么?各自有什么优缺点?

明天咱们就来一探到底。

存储介质-磁盘

一般而言消息中间件的音讯都存储在本地文件中,因为从效率来看间接放本地文件是最快的,并且稳定性最高。毕竟要是放相似数据库等第三方存储中的话,就多一个依赖少一份平安,并且还有网络的开销。

那对于将音讯存入磁盘文件来说一个流程的瓶颈就是磁盘的写入和读取。咱们晓得磁盘相对而言读写速度较慢,那通过磁盘作为存储介质如何实现高吞吐呢?

程序读写

答案就是程序读写

首先理解一下页缓存,页缓存是操作系统用来作为磁盘的一种缓存,缩小磁盘的I/O操作。

在写入磁盘的时候其实是写入页缓存中,使得对磁盘的写入变成对内存的写入。写入的页变成脏页,而后操作系统会在适合的时候将脏页写入磁盘中。

在读取的时候如果页缓存命中则间接返回,如果页缓存 miss 则产生缺页中断,从磁盘加载数据至页缓存中,而后返回数据。

并且在读的时候会预读,依据局部性原理当读取的时候会把相邻的磁盘块读入页缓存中。在写入的时候会后写,写入的也是页缓存,这样存着能够将一些小的写入操作合并成大的写入,而后再刷盘。

而且依据磁盘的结构,程序 I/O 的时候,磁头简直不必换道,或者换道的工夫很短。

依据网上的一些测试后果,程序写盘的速度比随机写内存还要快。

当然这样的写入存在数据失落的危险,例如机器忽然断电,那些还未刷盘的脏页就失落了。不过能够调用 fsync 强制刷盘,然而这样对于性能的损耗较大。

因而个别倡议通过多正本机制来保障音讯的牢靠,而不是同步刷盘

能够看到程序 I/O 适应磁盘的结构,并且还有预读和后写。 RocketMQ 和 Kafka 都是程序写入和近似程序读取。它们都采纳文件追加的形式来写入音讯,只能在日志文件尾部写入新的音讯,老的音讯无奈更改。

mmap-文件内存映射

从下面可知拜访磁盘文件会将数据加载到页缓存中,然而页缓存属于内核空间,用户空间拜访不了,因而数据还须要拷贝到用户空间缓冲区。

能够看到数据须要从页缓存再通过一次拷贝程序能力拜访的到,因而还能够通过mmap来做一波优化,利用内存映射文件来防止拷贝。

简略的说文件映射就是将程序虚构页面间接映射到页缓存上,这样就无需有内核态再往用户态的拷贝,而且也防止了反复数据的产生。并且也不用再通过调用readwrite办法对文件进行读写,能够通过映射地址加偏移量的形式间接操作

sendfile-零拷贝

既然音讯是存在磁盘中的,那消费者来拉音讯的时候就得从磁盘拿。咱们先来看看个别发送文件的流程是如何的。

简略说下DMA是什么,全称 Direct Memory Access ,它能够独立地间接读写零碎内存,不须要 CPU 染指,像显卡、网卡之类都会用DMA

能够看到数据其实是冗余的,那咱们来看看mmap之后的发送文件流程是怎么的。

能够看到上下文切换的次数没有变动,然而数据少拷贝一份,这和咱们上文提到的mmap能达到的成果是一样的。

然而数据还是冗余了一份,这不是能够间接把数据从页缓存拷贝到网卡不就好了嘛?sendfile就有这个效用。咱们先来看看Linux2.1版本中的sendfile

因为就一个零碎调用就满足了发送的需要,相比 read + write 或者 mmap + write 上下文切换必定是少了的,然而如同数据还是有冗余啊。是的,因而 Linux2.4 版本的 sendfile + 带 「扩散-收集(Scatter-gather)」的DMA。实现了真正的无冗余。

这就是咱们常说的零拷贝,在 Java 中FileChannal.transferTo() 底层用的就是sendfile

接下来咱们看看以上说的几点在 RocketMQ 和 Kafka中是如何利用的。

RocketMQ 和 Kafka 的利用

RocketMQ

采纳Topic混合追加形式,即一个 CommitLog 文件中会蕴含分给此 Broker 的所有音讯,不管音讯属于哪个 Topic 的哪个 Queue 。

所以所有的音讯过去都是程序追加写入到 CommitLog 中,并且建设音讯对应的 CosumerQueue ,而后消费者是通过 CosumerQueue 失去音讯的实在物理地址再去 CommitLog 获取音讯的。能够将 CosumerQueue 了解为音讯的索引。

在 RocketMQ 中不论是 CommitLog 还是 CosumerQueue 都采纳了 mmap。

在发消息的时候默认用的是将数据拷贝到堆内存中,而后再发送。咱们来看下代码。

能够看到这个配置 transferMsgByHeap 默认是 true ,那咱们再看消费者拉音讯时候的代码。

能够看到 RocketMQ 默认把音讯拷贝到堆内 Buffer 中,再塞到响应体外面发送。然而能够通过参数配置不通过堆,不过也并没有用到真正的零拷贝,而是通过mapedBuffer 发送到 SocketBuffer 。

所以 RocketMQ 用了程序写盘、mmap。并没有用到 sendfile ,还有一步页缓存到 SocketBuffer 的拷贝。

而后拉音讯的时候严格的说对于 CommitLog 来说读取是随机的,因为 CommitLog 的音讯是混合的存储的,然而从整体上看,音讯还是从 CommitLog 程序读的,都是从旧数据到新数据有序的读取。并且一般而言音讯存进去马上就会被生产,因而音讯这时候应该还在页缓存中,所以不须要读盘。

而且咱们在下面提到,页缓存会定时刷盘,这刷盘不可控,并且内存是无限的,会有swap等状况,而且mmap其实只是做了映射,当真正读取页面的时候产生缺页中断,才会将数据真正加载到内存中,这对于音讯队列来说可能会产生监控上的毛刺。

因而 RocketMQ 做了一些优化,有:文件预调配和文件预热

文件预调配

CommitLog 的大小默认是1G,当超过大小限度的时候须要筹备新的文件,而 RocketMQ 就起了一个后盾线程 AllocateMappedFileService,一直的解决 AllocateRequest,AllocateRequest其实就是预调配的申请,会提前准备好下一个文件的调配,避免在音讯写入的过程中调配文件,产生抖动。

文件预热

有一个warmMappedFile办法,它会把以后映射的文件,每一页遍历多去,写入一个0字节,而后再调用mlockmadvise(MADV_WILLNEED)

咱们再来看下this.mlock,外部其实就是调用了mlockmadvise(MADV_WILLNEED)

mlock:能够将过程应用的局部或者全副的地址空间锁定在物理内存中,避免其被替换到swap空间。

madvise:给操作系统倡议,说这文件在不久的未来要拜访的,因而,提前读几页可能是个好主见。

RocketMQ 小结

程序写盘,整体来看是程序读盘,并且应用了 mmap,不是真正的零拷贝。又因为页缓存的不确定性和 mmap 惰性加载(拜访时缺页中断才会真正加载数据),用了文件事后调配和文件预热即每页写入一个0字节,而后再调用mlockmadvise(MADV_WILLNEED)

Kafka

Kafka 的日志存储和 RocketMQ 不一样,它是一个分区一个文件。

Kafka 的音讯写入对于单分区来说也是程序写,如果分区不多的话从整体上看也算程序写,它的日志文件并没有用到 mmap,而索引文件用了 mmap。但发消息 Kafka 用到了零拷贝。

对于音讯的写入来说 mmap 其实没什么用,因为音讯是从网络中来。而对于发消息来说 sendfile 比照 mmap+write 我感觉效率更高,因为少了一次页缓存到 SocketBuffer 中的拷贝。

来看下Kafka发消息的源码,最终调用的是 FileChannel.transferTo,底层就是 sendfile。

从 Kafka 源码中我没看到有相似于 RocketMQ的 mlock 等操作,我感觉起因是首先日志也没用到 mmap,而后 swap 其实能够通过 Linux 零碎参数 vm.swappiness 来调节,这里倡议设置为1,而不是0。

假如内存真的有余,设置为 0 的话,在内存耗尽的状况下,又不能 swap,则会忽然停止某些过程。设置个 1,起码还能拖一下,如果有良好的监控伎俩,还能给个机会发现一下,不至于忽然停止。

RocketMQ & Kafka 比照

首先都是程序写入,不过 RocketMQ 是把音讯都存一个文件中,而 Kafka 是一个分区一个文件

每个分区一个文件在迁徙或者数据复制层面上来说更加得灵便

然而分区多了的话,写入须要频繁的在多个文件之间来回切换,对于每个文件来说是程序写入的,然而从全局看其实算随机写入,并且读取的时候也是一样,算随机读。而就一个文件的 RocketMQ 就没这个问题。

从发送音讯来说 RocketMQ 用到了 mmap + write 的形式,并且通过预热来缩小大文件 mmap 因为缺页中断产生的性能问题。而 Kafka 则用了 sendfile,相对而言我感觉 kafka 发送的效率更高,因为少了一次页缓存到 SocketBuffer 中的拷贝。

并且 swap 问题也能够通过零碎参数来设置。

最初

这篇文章两头写 RocketMQ 卡壳了,源码还是不太熟,有点绕。 多亏丁威大佬的点拨。不然我就陷入了死胡同出不来了。

最初再举荐下丁威大佬和周继锋大佬的《RocketMQ技术底细:RocketMQ架构设计与实现原理》。对 RocketMQ 有趣味的同学能够看看。

文章如果哪里有纰漏请放松分割我,感激!


我是 yes,从一点点到亿点点,咱们下篇见

往期举荐:

音讯队列面试热点一锅端

图解+代码|常见限流算法以及限流在单机分布式场景下的思考

表弟面试被虐我教他缓存连招

面试官:说说Kafka解决申请的全流程

Kafka索引设计的亮点

Kafka日志段读写剖析