前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面试系列之-rocketmq零拷贝原理

面试系列之-rocketmq零拷贝原理

作者头像
用户4283147
发布2022-12-29 20:06:24
8530
发布2022-12-29 20:06:24
举报
文章被收录于专栏:对线JAVA面试对线JAVA面试

传统版本

一台服务器把本机磁盘文件的内容发送到客户端,一般分为两个步骤:

  • read:读取本地文件内容;
  • write:将读取的内容通过网络发送出去;

传统的WEB服务器在收到请求后,从磁盘读取数据,然后将数据写到网卡,通过网卡发送给客户端,这一读一写的过程中就涉及数据的拷贝:

  1. 应用程序向操作系统发起read调用,CPU从用户态切换到内核态;
  2. CPU访问磁盘;
  3. 磁盘准备好数据,CPU将数据从磁盘拷贝内核空间(DMA把数据从磁盘拷贝到内核态缓冲区);
  4. CPU将系统内存中的数据拷贝到用户空间,同时从内核态切换到用户态;
  5. 用户应用发起write系统调用,CPU将数据从用户空间拷贝到内核空间(CPU把数据从用户缓冲区拷贝到内核的网络驱动的socket缓冲区),同时从用户态切换到内核态;
  6. CPU将数据从内核空间拷贝到网卡(DMA把数据从网络驱动的socket缓冲区拷贝到网卡的缓冲区中),通过网卡返回给客户端,CPU从内核态切换为用户态;

read和write两个操作发生了两次系统调用,每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态,也就是消息发送过程中一共发生了4次用户态与内核态的上下文切换;另外还发生了4次数据拷贝,其中两次是DMA的拷贝,另外两次则是通过CPU拷贝的;要想提高文件传输的性能,就需要减少用户态与内核态的上下文切换和内存拷贝的次数;

零拷贝机制

零拷贝技术实现的方式通常有mmap+write和sendfile两种;

mmap+write(RocketMQ使用mmap)

mmap() 系统调用函数在调用进程的虚拟地址空间中创建一个新映射,这个映射会直接把内核缓冲区里的数据映射到用户空间,这样就不用从内核空间到用户空间来回复制数据了;

过程改进
  1. 应用进程调用mmap(),DMA把数据从磁盘拷贝到内核缓冲区里;
  2. 应用进程调用write(),CPU直接将内核缓冲区的数据拷贝到socket缓冲区中;
  3. DMA把数据从内核的socket缓冲区拷贝到网卡的缓冲区里;
细节过程
  1. 应用成都发起mmap系统调用,CPU从用户态切换到内核态;
  2. CPU建立内核空间与用户空间的内存地址映射;
  3. CPU通过DMA访问磁盘,将数据加载到内核空间;
  4. mmap调用返回,CPU从内核态切换为用户态;
  5. 用户应用发起write系统调用,CPU从用户态切换到内核态;
  6. CPU将数据从映射内存空间复制到发送缓冲区;
  7. CPU通过DMA将数据写到网卡;
  8. write调用返回,CPU从内核态切换为用户态;

mmap + write的方式依然需要4次用户态与内核态的上下文切换,但是少了一次内存拷贝(CPU把数据从内核态缓冲区拷贝到用户缓冲区,四次CPU状态切换,两次DMA拷贝,一次内存拷贝),RocketMQ选择了这种mmap + write方式,因为这种方式即使频繁调用,使用小块文件传输,效果会比sendfile更好;但是这样不能很好的利用DMA方式,会比sendfile多消耗CPU, mmap映射的内存分配与释放的安全性控制复杂,需要避免JVM Crash问题;

sendfile(kafka使用sendfile)

通过使用sendfile(),数据可以直接在内核空间进行传输,因此避免了用户空间和内核空间之间来回复制拷贝,同时由于使用sendfile替代了read + write从而节省了一次系统调用,也就是2次用户态与内核态的上下文切换,整个过程发生了2次用户态与内核态的上下文切换和3次内存拷贝;(两次CPU状态切换,一次CPU内存拷贝,两次DMA拷贝)

过程改进
  1. 应用进程调用sendfile(),DMA控制器把数据从硬盘中拷贝到读缓冲区,上下文从用户态转向内核态;
  2. CPU直接将内核缓冲区的数据拷贝到socket缓冲区中;
  3. DMA把数据从网络驱动的socket缓冲区拷贝到网卡的缓冲区中,上下文从内核态切换回用户态,sendfile()调用返回;
细节过程
  1. 用户应用发起sendfile系统调用,CPU从用户态切换到内核态;
  2. CPU利用DMA技术将数据加载到内核缓冲区;
  3. CPU将数据从内核缓冲区复制到socket缓冲区;
  4. CPU利用DMA将数据拷贝到网卡;
  5. sendfile系统调用返回,CPU从内核态切换为用户态;
网卡支持SG-DMA技术的sendfile
过程改进
  1. DMA将磁盘上的数据拷贝到内核缓冲区里;
  2. 缓冲区描述符和数据长度传到socket缓冲区,这样网卡的SG-DMA控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到socket缓冲区中,这样就减少了一次数据拷贝;

变化后发生了2次用户态与内核态的上下文切换和2次内存拷贝;

两种方案对比
  1. 使用mmap + write方式
  • 优点:即使频繁调用,使用小块文件传输,效率也很高;
  • 缺点:不能很好的利用DMA方式,会比sendfile多消耗CPU,内存安全性控制复杂,需要避免JVM Crash问题;
  1. 使用sendfile方式
  • 优点:可以利用DMA方式,消耗CPU较少,大块文件传输效率高,无内存安全性问题;
  • 缺点:小块文件效率低于mmap方式,只能是BIO方式传输,不能使用NIO;RocketMQ选择了第一种方式,mmap+write方式,因为有小块数据传输的需求,效果会比sendfile更好;

rocketmq零拷贝原理源码解析

初始化map的底层调用
代码语言:javascript
复制
public MappedByteBuffer map(MapMode var1, long var2, long var4) throws IOException{
    ...
    //调用map0方法完成映射,并返回内存地址
    var7 = this.map0(var6, var36, var10);
    ...
    //根据内存地址创建MappedByteBuffer对象,供java层面的操作
    var37 = Util.newMappedByteBuffer(var35, var7 + (long)var12, var13, var15);
    return var37;
    ...
}

private native long map0(int var1, long var2, long var4) throws IOException;

#define mmap64 mmap
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
                                     jint prot, jlong off, jlong len){
    ...
    //这里调用的是mmap64,但是在文件开头define了mmap64就是mmap方法
    mapAddress = mmap64(
        0,                    /* Let OS decide location */
        len,                  /* Number of bytes to map */
        protections,          /* File permissions */
        flags,                /* Changes are shared */
        fd,                   /* File descriptor of mapped file */
        off);                 /* Offset into file */
    ...
    //返回映射完成的内存地址
    return ((jlong) (unsigned long) mapAddress);
}

rocketmq创建mappedFile对象后,会调用其init方法,完成了最终的映射操作。调用的方法是fileChannel.map;fileChannel.map最底层调用就是linux的系统方法mmap(mmap系统方法:为进程创建虚拟地址空间映射);

rocketmq在创建完mmap映射后,还会作一个预热,mappedFile.warmMappedFile方法调用底层的mlock方法:

代码语言:javascript
复制
public void mlock() {
    final long address = ((DirectBuffer) (this.mappedByteBuffer)).address();
    Pointer pointer = new Pointer(address);
    int ret = LibC.INSTANCE.mlock(pointer, new NativeLong(this.fileSize));
    int ret = LibC.INSTANCE.madvise(pointer, new NativeLong(this.fileSize), 
									LibC.MADV_WILLNEED);
}

mlock方法主要做了2个系统方法的调用,mlock和madvise: mlock系统方法:锁定内存中的虚拟地址空间,防止其被交换系统的swap空间中;swap空间就是磁盘上的一块空间,当内存不够用时,系统会将部分内存中不常用的数据放到磁盘上;mmap本身就是为了提高读写性能,如果被映射的内存数据被放到了磁盘上,那就失去了mmap的意义了,所以要做一个mlock进行内存的锁定;

madvise系统方法:该方法功能很多,主要是给系统内核提供内存处理建议,可以根据需要传入参数;在rocketmq中,传入的参数是MADV_WILLNEE,该参数的意思是告诉系统内核,这块内存一会儿就会用到,于是系统就会提前加载被映射的文件数据到内存中,这样就不会在需要使用的时候才去读取磁盘,影响性能;

刷盘时的map调用

在实际存储消息的时候,无论是使用堆外内存还是直接使用mappedByteBuffer,都需要额外的刷盘任务负责保证数据写入磁盘,调用不同的force方法会得到不同的效果;

MappedByteBuffer底层使用了操作系统的mmap机制,FileChannel#map()方法就会返回MappedByteBuffer;DirectByteBuffer虽然实现了MappedByteBuffer,不过DirectByteBuffer默认并没有直接使用mmap机制;

FileChannelImpl.force方法
代码语言:javascript
复制
// FileChannelImpl.force方法
JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl_force0(JNIEnv *env, jobject this,
                                          jobject fdo, jboolean md){
    ...
	//fsync系统方法:将内核内存中有修改的数据同步到相应文件的磁盘空间
    result = fsync(fd);
    ...
}
MappedByteBuffer的force方法
代码语言:javascript
复制
// MappedByteBuffer的force方法
JNIEXPORT void JNICALL
Java_java_nio_MappedByteBuffer_force0(JNIEnv *env, jobject obj, jobject fdo,
                                      jlong address, jlong len){
	//msync系统方法:将mmap映射的内存空间中的修改同步到文件系统中
    int result = msync(a, (size_t)len, MS_SYNC);
    ...
}
rocketmq对零拷贝的使用和优化分为5步
  1. 调用系统mmap方法进行虚拟内存地址映射;
  2. 用0来填充page cache,初始化文件;
  3. 调用系统mlock方法,防止映射的内存被放入swap空间;
  4. 调用系统madvise方法,使得文件会被系统预加载;
  5. 根据是否启用堆外内存,调用fsync或者msync刷盘;
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-11-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 对线JAVA面试 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 传统版本
  • 零拷贝机制
    • mmap+write(RocketMQ使用mmap)
      • 过程改进
      • 细节过程
    • sendfile(kafka使用sendfile)
      • 过程改进
      • 细节过程
    • 网卡支持SG-DMA技术的sendfile
      • 过程改进
    • 两种方案对比
    • rocketmq零拷贝原理源码解析
      • 初始化map的底层调用
        • 刷盘时的map调用
          • FileChannelImpl.force方法
          • MappedByteBuffer的force方法
        • rocketmq对零拷贝的使用和优化分为5步
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档