rocketmq中关于零拷贝使用的是linux系统中的mmap技术,而kafka对于零拷贝的技术实现是sendfile,为什么rocketmq为什么不使用和kafka一样的sendfile?
相对于sendfile的大容量传输,mmap的小容量传输更适合rocketmq,而rocketmq由于是用java开发的,大对象会对jvm的垃圾回收机制有一定影响,因为有小块数据传输的需求,效果会比sendfile更好,故选择了mmap。
对于将消息存入磁盘文件来说一个流程的瓶颈就是磁盘的写入和读取。我们知道磁盘相对而言读写速度较慢,那通过磁盘作为存储介质如何实现高吞吐呢?
首先了解一下pageCache,页缓存是操作系统用来作为磁盘的一种缓存,减少磁盘的I/O操作。
- 在写入磁盘的时候其实是写入页缓存中,使得对磁盘的写入变成对内存的写入。写入的页变成脏页,然后操作系统会在合适的时候将脏页写入磁盘中。
- 在读取的时候如果页缓存命中则直接返回,如果页缓存 miss 则产生缺页中断,从磁盘加载数据至页缓存中,然后返回数据。
并且在读的时候会预读,根据局部性原理当读取的时候会把相邻的磁盘块读入页缓存中。在写入的时候会后写,写入的也是页缓存,这样存着可以将一些小的写入操作合并成大的写入,然后再刷盘。
而且根据磁盘的构造,顺序I/O的时候,磁头几乎不用换道,或者换道的时间很短。
根据网上的一些测试结果,顺序写盘的速度比随机写内存还要快。
当然这样的写入存在数据丢失的风险,例如机器突然断电,那些还未刷盘的脏页就丢失了。不过可以调用 fsync 强制刷盘,但是这样对于性能的损耗较大。
因此一般建议通过多副本机制来保证消息的可靠,而不是同步刷盘。
可以看到顺序I/O适应磁盘的构造,并且还有预读和后写。RocketMQ和Kafka都是顺序写入和近似顺序读取。它们都采用文件追加的方式来写入消息,只能在日志文件尾部写入新的消息,老的消息无法更改。
这个地方记录一下我在生产中遇到的一个问题:
【我们也遇到这个问题,当时是俞老师为山区学生准备的课,注课将近150万的用户数据,当时出现了消息积压在同步库存的一个woker,总共两个woker 4核8G cpu和load的非常高,解决办法就是零时申请了2台机器,分配消费完了再注课】
集群平时运行的好好的,有一次月底鹰眼系统,日志解析程序的消费者发现tps非常的低且broker机器IO压力较大,消费者平时都不开,月底一次性几天跑完(应用的锅)。后面经过测试和排查发现,broker所在机器的内存命中缺失比正常的增长要快,通过研究源码的原理,得出了一个结论:
- 生产消费同时开启的时候,消费堆积不能过大。
- 原因:因为堆积超过内存时,生产者持有的热区数据是最新的commitlog,消费是旧的commitlog,如果消费进程很多,还有可能是很多个旧的commitlog要加载到内存PageCache中,commitlog大小1G,OS做一次内存映射后对应的文件数据尽可能多的预加载至内存中,从而达到内存预热的效果,所以至少每次1G,如果要消费的数据分散,那么就会加载很多G的数据到内存,而生产者是只往一个最新的commitlog顺序写,这样会带来一个读写内存的竞争,导致较多的内存-磁盘数据置换,tps降低。
- 解决:一般堆积较大的时候,停掉生产一般都可以保持较高的消费速度,待堆积较低后再启动生产
- 零拷贝机制能减少用户空间和操作系统内核空间的上下文切换
- 减少内存的占用
- 减少cpu的使用
如java在linux系统上,读取一个磁盘文件,并发送到远程端的服务
- 发出read系统调用,会导致用户空间到内核空间的上下文切换,然后再通过DMA将文件中的数据从磁盘上读取到内核空间缓冲区
- 接着将内核空间缓冲区的数据拷贝到用户空间进程内存,然后read系统调用返回。而系统调用的返回又会导致一次内核空间到用户空间的上下文切换
- write系统调用,则再次导致用户空间到内核空间的上下文切换,将用户空间的进程里的内存数据复制到内核空间的socket缓冲区(也是内核缓冲区,不过是给socket使用的),然后write系统调用返回,再次触发上下文切换
- 至于socket缓冲区到网卡的数据传输则是独立异步的过程,也就是说write系统调用的返回并不保证数据被传输到网卡
【一共有四次用户空间与内核空间的上下文切换。四次数据copy,分别是两次CPU数据复制,两次DMA数据复制】
- 发出mmap系统调用,导致用户空间到内核空间的上下文切换。然后通过DMA引擎将磁盘文件中的数据复制到内核空间缓冲区
- mmap系统调用返回,导致内核空间到用户空间的上下文切换
- 这里不需要将数据从内核空间复制到用户空间,因为用户空间和内核空间共享了这个缓冲区
- 发出write系统调用,导致用户空间到内核空间的上下文切换。将数据从内核空间缓冲区复制到内核空间socket缓冲区;write系统调用返回,导致内核空间到用户空间的上下文切换
- 异步,DMA引擎将socket缓冲区中的数据copy到网卡
【通过mmap实现的零拷贝I/O进行了4次用户空间与内核空间的上下文切换,以及3次数据拷贝;其中3次数据拷贝中包括了2次DMA拷贝和1次CPU拷贝】
从Linux 2.4版本开始,操作系统提供scatter和gather的SG-DMA方式,直接从内核空间缓冲区中将数据读取到网卡,无需将内核空间缓冲区的数据再复制一份到socket缓冲区
- 发出sendfile系统调用,导致用户空间到内核空间的上下文切换。通过DMA引擎将磁盘文件中的内容复制到内核空间缓冲区
- 这里没把数据复制到socket缓冲区;取而代之的是,相应的描述符信息被复制到socket缓冲区。该描述符包含了两种的信息:
- 内核缓冲区的内存地址
- 内核缓冲区的偏移量
- sendfile系统调用返回,导致内核空间到用户空间的上下文切换。DMA根据socket缓冲区的描述符提供的地址和偏移量直接将内核缓冲区中的数据复制到网卡
【sendfile实现的I/O使用了2次用户空间与内核空间的上下文切换,以及2次数据的拷贝,而且这2次的数据拷贝都是非CPU拷贝。这样一来我们就实现了最理想的零拷贝I/O传输了,不需要任何一次的CPU拷贝,以及最少的上下文切换】
- mmap 适合⼩数据量读写,sendFile适合⼤⽂件传输
- mmap 需要4次上下⽂切换、3次数据拷⻉;sendFile 需要3次上下⽂切换、最少2次数据拷⻉。
- sendFile 可以利⽤DMA⽅式,减少CPU拷⻉,mmap 则不能(必须从内核拷⻉到socket 缓冲 区)。
在这个选择上:RocketMQ在消费消息时,使⽤了mmap。kafka使⽤了sendFIle。
场景:
- ⽂件较⼤,读写较慢,追求速度
- JVM内存不够,不能加载太⼤的数据
- 内存宽带不够,即存在其他程序或线程存在⼤量的IO操作,导致带宽本来就⼩
技术: Java的NIO、Netty、 kafka 、rocketMQ