基于RocketMQ源码理解零拷⻉与顺序写
1、顺序写加速⽂件写⼊磁盘
通常应⽤程序往磁盘写⽂件时,由于磁盘空间不是连续的,会有很多碎⽚。所以我们去写⼀个⽂件时,也就⽆法把⼀个⽂件写在⼀块连续的磁盘空间中,⽽需要在磁盘多个扇区之间进⾏⼤量的随机写。这个过程中有⼤量的寻址操作,会严重影响写数据的性能。⽽顺序写机制是在磁盘中提前申请⼀块连续的磁盘空间,每次写数据时,就可以避免这些寻址操作,直接在之前写⼊的地址后⾯接着写就⾏。
Kafka官⽅详细分析过顺序写的性能提升问题。Kafka官⽅曾说明,顺序写的性能基本能够达到内存级别。⽽如果配备固态硬盘,顺序写的性能甚⾄有可能超过写内存。⽽RocketMQ很⼤程度上借鉴了Kafka的这种思想。
2、刷盘机制保证消息不丢失
在操作系统层⾯,当应⽤程序写⼊⼀个⽂件时,⽂件内容并不会直接写⼊到硬件当中,⽽是会先写⼊到操作系统中的⼀个缓存PageCache中。PageCache缓存以4K⼤⼩为单位,缓存⽂件的具体内容。这些写⼊到PageCache中的⽂件,在应⽤程序看来,是已经完全落盘保存好了的,可以正常修改、复制等等。但是,本质上,PageCache依然是内存状态,所以⼀断电就会丢失。因此,需要将内存状态的数据写⼊到磁盘当中,这样数据才能真正完成持久化,断电也不会丢失。这个过程就称为刷盘。
PageCache是源源不断产⽣的,⽽Linux操作系统显然不可能时时刻刻往硬盘写⽂件。所以,操作系统只会在某些特定的时刻将PageCache写⼊到磁盘。例如当我们正常关机时,就会完成PageCache刷盘。另外,在Linux中,对于有数据修改的PageCache,会标记为Dirty(脏⻚)状态。当Dirty Page的⽐例达到⼀定的阈值时,就会触发⼀次刷盘操作。例如在Linux操作系统中,可以通过/proc/meminfo⽂件查看到Page Cache的状态。
RocketMQ对于何时进⾏刷盘,也设计了两种刷盘机制,同步刷盘和异步刷盘。只需要在broker.conf中进⾏配置就⾏。
RocketMQ到底是怎么实现同步刷盘和异步刷盘的,还记得吗?
3、零拷⻉加速⽂件读写
零拷⻉(zero-copy)是操作系统层⾯提供的⼀种加速⽂件读写的操作机制,⾮常多的开源软件都在⼤量使⽤零拷⻉,来提升IO操作的性能。对于Java应⽤层,对应着mmap和sendFile两种⽅式。接下来,咱们深⼊操作系统来详细理解⼀下零拷⻉。
1:理解CPU拷⻉和DMA拷⻉
我们知道,操作系统对于内存空间,是分为⽤户态和内核态的。⽤户态的应⽤程序⽆法直接操作硬件,需要通过内核空间进行操作转换,才能真正操作硬件。这其实是为了保护操作系统的安全。正因为如此,应⽤程序需要与⽹卡、磁盘等硬件进⾏数据交互时,就需要在⽤户态和内核态之间来回的复制数据。⽽这些操作,原本都是需要由CPU来进⾏任务的分配、调度等管理步骤的,早先这些IO接⼝都是由CPU独⽴负责,所以当发⽣⼤规模的数据读写操作时,CPU的占⽤率会⾮常⾼。
之后,操作系统为了避免CPU完全被各种IO调⽤给占⽤,引⼊了DMA(直接存储器存储)。由DMA来负责这些频繁的IO操作。DMA是⼀套独⽴的指令集,不会占⽤CPU的计算资源。这样,CPU就不需要参与具体的数据复制的⼯作,只需要管理DMA的权限即可。
DMA拷⻉极⼤的释放了CPU的性能,因此他的拷⻉速度会⽐CPU拷⻉要快很多。但是,其实DMA拷⻉本身,也在不断优化。
引⼊DMA拷⻉之后,在读写请求的过程中,CPU不再需要参与具体的⼯作,DMA可以独⽴完成数据在系统内部的复制。但是,数据复制过程中,依然需要借助数据总进线。当系统内的IO操作过多时,还是会占⽤过多的数据总线,造成总线冲突,最终还是会影响数据读写性能。
为了避免DMA总线冲突对性能的影响,后来⼜引⼊了Channel通道的⽅式。Channel,是⼀个完全独⽴的处理器,专⻔负责IO操作。既然是处理器,Channel就有⾃⼰的IO指令,与CPU⽆关,他也更适合⼤型的IO操作,性能更⾼。
这也解释了,为什么Java应⽤层与零拷⻉相关的操作都是通过Channel的⼦类实现的。这其实是借鉴了操作系统中的概念。
⽽所谓的零拷⻉技术,其实并不是不拷⻉,⽽是要尽量减少CPU拷⻉。
2:再来理解下mmap⽂件映射机制是怎么回事。
mmap机制的具体实现参⻅配套示例代码。主要是通过java.nio.channels.FileChannel的map⽅法完成映射。
以⼀次⽂件的读写操作为例,应⽤程序对磁盘⽂件的读与写,都需要经过内核态与⽤户态之间的状态切换,每次状态切换的过程中,就需要有⼤量的数据复制。
在这个过程中,总共需要进⾏四次数据拷⻉。⽽磁盘与内核态之间的数据拷⻉,在操作系统层⾯已经由CPU拷⻉优化成了DMA拷⻉。⽽内核态与⽤户态之间的拷⻉依然是CPU拷⻉。所以,在这个场景下,零拷⻉技术优化的重点,就是内核态与⽤户态之间的这两次拷⻉。
⽽mmap⽂件映射的⽅式,就是在⽤户态不再保存⽂件的内容,⽽只保存⽂件的映射,包括⽂件的内存起始地址,⽂件⼤⼩等。真实的数据,也不需要在⽤户态留存,可以直接通过操作映射,在内核态完成数据复制。
这个拷⻉过程都是在操作系统的系统调⽤层⾯完成的,在Java应⽤层,其实是⽆法直接观测到的,但是我们可以去JDK源码当中进⾏间接验证。在JDK的NIO包中,java.nio.HeapByteBuffer映射的就是JVM的⼀块堆内内存,在HeapByteBuffer中,会由⼀个byte数组来缓存数据内容,所有的读写操作也是先操作这个byte数组。这其实就是没有使⽤零拷⻉的普通⽂件读写机制。
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
/*
hb = new byte[cap];
offset = 0;
*/
}
⽽NIO把包中的另⼀个实现类java.nio.DirectByteBuffer则映射的是⼀块堆外内存。在DirectByteBuffer中,并没有⼀个数据结构来保存数据内容,只保存了⼀个内存地址。所有对数据的读写操作,都通过unsafe魔法类直接交由内核完成,这其实就是mmap的读写机制。
mmap⽂件映射机制,其实并不神秘,我们启动任何⼀个Java程序时,其实都⼤量⽤到了mmap⽂件映射。例如,我们可以在Linux机器上,运⾏⼀下下⾯这个最简单不过的应用程序:
import java.util.Scanner;
public class BlockDemo {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
final String s = scanner.nextLine();
System.out.println(s);
}
}
通过Java指令运⾏起来后,可以⽤jps查看到运⾏的进程ID。然后,就可以使⽤lsof -p {PID}的⽅式查看⽂件的映射情况。
这⾥⾯看到的mem类型的FD其实就是⽂件映射。
cwd 表示程序的⼯作⽬录。rtd 表示⽤户的根⽬录。 txt表示运⾏程序的指令。下⾯的1u表示Java应⽤的标准输出,2u表示Java应⽤的标准错误输出,默认的/dev/pts/1是linux当中的伪终端。通常服务器上会写 java xxx 1>text.txt 2>&1 这样的脚本,就是指定这⾥的1u,2u。
最后,这种mmap的映射机制由于还是需要⽤户态保存⽂件的映射信息,数据复制的过程也需要⽤户态的参与,这其中的变数还是非常多的。所以,mmap机制适合操作⼩⽂件,如果⽂件太⼤,映射信息也会过⼤,容易造成很多问题。通常mmap机制建议的映射⽂件⼤⼩不要超过2G 。⽽RocketMQ做⼤的CommitLog⽂件保持在1G固定⼤⼩,也是为了⽅便⽂件映射。
3:梳理下sendFile机制是怎么运⾏的。
sendFile机制的具体实现参⻅配套示例代码。主要是通过java.nio.channels.FileChannel的transferTo⽅法完成。
sourceReadChannel.transferTo(0,sourceFile.length(),targetWriteChannel);
早期的sendfile实现机制其实还是依靠CPU进⾏⻚缓存与socket缓存区之间的数据拷⻉。但是,在后期的不断改进过程中,sendfile优化了实现机制,在拷⻉过程中,并不直接拷⻉⽂件的内容,⽽是只拷⻉⼀个带有⽂件位置和⻓度等信息的⽂件描述符FD,这样就⼤⼤减少了需要传递的数据。⽽真实的数据内容,会交由DMA控制器,从⻚缓存中打包异步发送到socket中。
为什么⼤家都喜欢⽤这个场景来举例呢?其实我们去看下Linux操作系统的man帮助⼿册就能看到⼀部分答案。使⽤指令man 2 sendfile就能看到Linux操作系统对于sendfile这个系统调⽤的⼿册。
2.6.33版本以前的Linux内核中,out_fd只能是⼀个socket,所以⽹上铺天盖地的⽼资料都是拿⽹卡来举例。但是现在版本已经没有了这个限制。
sendfile机制在内核态直接完成了数据的复制,不需要⽤户态的参与,所以这种机制的传输效率是⾮常稳定的。sendfile机制⾮常适合⼤数据的复制转移。
最后,⽐较mmap和sendfile这两种零拷⻉的实际机制,会发现他们两者的⼀些使⽤区别:
mmap需要⽤户态的配合,所以,性能相⽐sendfile要差⼀点。但是,另⼀⽅⾯,mmap机制可以在⽤户态操作数据,所以mmap对数据的处理,相⽐sendfile更灵活。
实际上,RocketMQ相⽐于Kafka。Kafka⼤量运⾏了sendfile来进⾏消息传递,尤其是把⽂件从磁盘读取到⽹卡发送的过程。⽽RocketMQ则⼤量运⽤了mmap机制。所以,RocketMQ相⽐于Kafka,也体现出了这样的不同。RocketMQ功能相对丰富,⽽Kafka的性能则更⾼。