在上一篇文章中,主要介绍了rocketmq消息的存储流程。其主要使用了mmap的零拷贝技术实现了硬盘和内存的映射,从而提高了读写性能。在流程中有一个非常有意思的预热方法并没有详细分析,因为其中涉及到了一些系统方法的调用。而本文就从该方法入手,进而分享除了mmap之外,还有哪些零拷贝方法,以及他们的系统底层调用是怎样的。

本文的主要内容

1.page cache与mmap的关系

2.rocketmq对零拷贝的使用和优化

3.transferTo/From的零拷贝

4.splice的零拷贝

1.page cache与mmap的关系

page cache允许系统将一部分硬盘上的数据存放在内存中,使得对这部分数据的访问不需要再读取硬盘了,从而提高了读写性能。我理解这就是所谓的内核缓存。page cache以页为单位,一般一页为4kb。当程序需要将数据写入文件时,并不会,也不能直接将数据写到磁盘上,而是先将数据复制到page cache中,并标记为dirty,等待系统的flusher线程定时将这部分数据落到硬盘上。

对于用户程序来说,因为不能直接访问内核缓存,所以读取文件数据都必须等待系统将数据从磁盘上复制到page cache中,再从page cache复制一份到用户态的内存中。于是读取文件就产生了2次数据的复制:硬盘=>page cache,page cache=>用户态内存。同样的数据在内存中会存在2份,这既占用了不必要的内存空间,也产生了冗余的拷贝。针对此问题,操作系统提供了内存映射机制,对于linux来说,就提供了mmap操作。

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的内存中,实现文件磁盘地址和进程内存地址的映射关系。映射完成后,进程就可以直接读写操作这一段内存,而系统会自动回写dirty页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。

2.rocketmq对零拷贝的使用和优化

map的底层调用

rocketmq创建mappedFile对象后,会调用其init方法,完成了最终的映射操作。调用的方法是fileChannel.map。

查看FileChannelImpl.map:

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;
...
}

继续查看map0方法:

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

发现其是一个native方法,于是就需要去jdk源码中看看了。

查看jdk源码:/src/java.base/unix/native/libnio/ch/FileChannelImpl.c

#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);
}

因此fileChannel.map最底层调用就是linux的系统方法mmap。

mmap系统方法:为进程创建虚拟地址空间映射

参考说明:https://man7.org/linux/man-pages/man2/mmap.2.html

warmMappedFile的底层调用

rocketmq在创建完mmap映射后,还会作一个预热

查看mappedFile.warmMappedFile方法:

public void warmMappedFile(FlushDiskType type, int pages) {
ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
int flush = 0;
//用0来填充文件,特别注意这里i每次递增都是OS_PAGE_SIZE,查看可以看到是1024*4,即4kb
//因此初始化是以页为单位填充的
for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) {
byteBuffer.put(i, (byte) 0);
//如果需要同步刷盘,那么如果写入mappedByteBuffer的数据超过了指定页数,就做一次强制刷盘
if (type == FlushDiskType.SYNC_FLUSH) {
//i是当前写入的数据位置,flush是已经刷盘的数据位置,如果差值大于指定的页数pages,就做一次强制刷盘
if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) {
flush = i;
mappedByteBuffer.force();
}
}
...
}
//全部填充完毕后,如果配置了同步刷盘,就再做一次强制刷盘操作
if (type == FlushDiskType.SYNC_FLUSH) {
mappedByteBuffer.force();
}
//这里是对内存再做一些预处理
this.mlock();
}

接着查看mlock方法:

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进行内存的锁定。

参考说明:https://man7.org/linux/man-pages/man2/mlock.2.html

madvise系统方法:该方法功能很多,主要是给系统内核提供内存处理建议,可以根据需要传入参数。

在rocketmq中,传入的参数是MADV_WILLNEE,该参数的意思是告诉系统内核,这块内存一会儿就会用到,于是系统就会提前加载被映射的文件数据到内存中,这样就不会在需要使用的时候才去读取磁盘,影响性能。其他建议类型可以参考下面的链接。

参考说明:https://man7.org/linux/man-pages/man2/madvise.2.html

落盘的底层调用

上面的分析仅仅是创建mappedFile的过程,而在实际存储消息的时候,无论是使用堆外内存还是直接使用mappedByteBuffer,都需要额外的刷盘任务负责保证数据写入磁盘。因此接下去看下刷盘的底层调用是什么。

查看MappedFile.flush方法:

public int flush(final int flushLeastPages) {
...
if (writeBuffer != null || this.fileChannel.position() != 0) {
//如果使用了堆外内存,则调用fileChannel的force方法
this.fileChannel.force(false);
} else {
//如果使用的是mappedByteBuffer,则调用相应的force方法
this.mappedByteBuffer.force();
}
...
}

该方法比较简单,根据是否启用堆外内存,调用不同的force方法。

查看FileChannelImpl.force方法:

public void force(boolean var1) throws IOException {
...
do {
//调用FileDispatcher的force方法
var2 = this.nd.force(this.fd, var1);
} while(var2 == -3 && this.isOpen());
...
}

查看FileDispatcherImpl.force方法,会发现其调用的force0的natvie方法,因此直接看jdk源码

JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl_force0(JNIEnv *env, jobject this,
jobject fdo, jboolean md)
{
...
result = fsync(fd);
...
}

因此fileChannel.force的底层就是调用了fsync方法

fsync系统方法:将内核内存中有修改的数据同步到相应文件的磁盘空间

参考说明:https://man7.org/linux/man-pages/man2/fsync.2.html

查看MappedByteBuffer的force方法,可以看到直接调用了force0的native方法:

JNIEXPORT void JNICALL
Java_java_nio_MappedByteBuffer_force0(JNIEnv *env, jobject obj, jobject fdo,
jlong address, jlong len)
{
int result = msync(a, (size_t)len, MS_SYNC);
...
}

因此mappedByteBuffer.force的底层调用了msync方法

msync系统方法:将mmap映射的内存空间中的修改同步到文件系统中

参考说明:https://man7.org/linux/man-pages/man2/msync.2.html

因此做一个总结,rocketmq对零拷贝的使用和优化分为5步:

1.调用系统mmap方法进行虚拟内存地址映射

2.用0来填充page cache,初始化文件

3.调用系统mlock方法,防止映射的内存被放入swap空间

4.调用系统madvise方法,使得文件会被系统预加载

5.根据是否启用堆外内存,调用fsync或者msync刷盘

transferTo/From的零拷贝

在使用fileChannel时,如果不需要对数据作修改,仅仅是传输,那么可以使用transferTo或者transferFrom进行2个channel间的传递,这种传递是完全处于内核态的,因此性能较好。

简单的例子如下:

SocketChannel sc = SocketChannel.open(new InetSocketAddress("localhost", 8090));
FileChannel fc = new RandomAccessFile("filename", "r").getChannel();
fc.transferTo(0, 100, sc);

查看FileChannelImpl.transferTo方法,最终会调用到transfer0方法,调用链如下:

transferTo->transferToDirectly->transferToDirectlyInternal->transferTo0

查看jdk源码:/src/java.base/unix/native/libnio/ch/FileChannelImpl.c

...
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_transferTo0(JNIEnv *env, jobject this,
jobject srcFDO,
jlong position, jlong count,
jobject dstFDO)
{
#if defined(__linux__)
off64_t offset = (off64_t)position;
jlong n = sendfile64(dstFD, srcFD, &offset, (size_t)count);
...
#elif defined (__solaris__)
result = sendfilev64(dstFD, &sfv, 1, &numBytes);
...
#elif defined(__APPLE__)
result = sendfile(srcFD, dstFD, position, &numBytes, NULL, 0);
...
#endif
}
...

根据不同的系统调用sendfile方法。

sendfile系统方法:在内核态中进行两个文件描述符之间数据的阐述

参考说明:https://man7.org/linux/man-pages/man2/sendfile.2.html

splice的零拷贝

在查询资料的过程中,了解到Linux 2.6.17支持了splice。该方法和sendFile类似,也是直接在内核中完成了数据的传输。区别在于sendfile将磁盘数据加载到内核缓存后,需要一次CPU拷贝将数据拷贝到socket缓存,而splice是更进一步,连这个CPU拷贝也不需要了,直接将两个内核空间的buffer进行pipe。

好像java对此并没有支持,所以就不深究了。

参考说明:https://man7.org/linux/man-pages/man2/splice.2.html

到此从rocketmq的mmap到其他零拷贝的底层调用分析就结束了,总结如下:

1.rocketmq底层采用了mmap的零拷贝技术提高读写性能。

2.使用了mlock和madvise进一步优化性能

3.根据是否使用堆外内存选择调用fsync或者msync进行刷盘

4.sendfile实现了内核态的数据拷贝,java中有fileChannel.transferTo/From支持该靠左

5.Linux2.6.17新支持了splice的零拷贝,可能比sendfile更优秀,但java中目前好像还未有支持。

从rocketmq入手,解析各种零拷贝的jvm层原理的更多相关文章

  1. RocketMQ 零拷贝

    一.零拷贝原理:Consumer 消费消息过程,使用了零拷贝,零拷贝包含以下两种方式: 1.使用 mmap + write 方式  (RocketMQ选择的方式:因为有小块数据传输的需求,效果会比 s ...

  2. Netty源码解析 -- 零拷贝机制与ByteBuf

    本文来分享Netty中的零拷贝机制以及内存缓冲区ByteBuf的实现. 源码分析基于Netty 4.1.52 Netty中的零拷贝 Netty中零拷贝机制主要有以下几种 1.文件传输类DefaultF ...

  3. linux独有的sendfile系统调用--“零拷贝,高效”

    参考:http://blog.csdn.net/caianye/article/details/7576198 如今几乎每个人都听说过Linux中所谓的"零拷贝"特性,然而我经常碰 ...

  4. java的零拷贝机制

    转:https://blog.csdn.net/zhouhao88410234/article/details/77574689?fps=1&locationNum=9 为何要懂零拷贝原理?因 ...

  5. 走进科学之揭开神秘的&quot;零拷贝&quot;

    前言 "零拷贝"这三个字,想必大家多多少少都有听过吧,这个技术在各种开源组件中都使用了,比如kafka,rocketmq,netty,nginx等等开源框架都在其中引用了这项技术. ...

  6. Linux中“零拷贝”

    服务器响应一个http请求的步骤 把磁盘文件读入内核缓冲区 从内核缓冲区读到内存 处理(静态资源不需处理) 发送到网卡的内核缓冲区(发送缓存) 网卡发送数据 数据从第一步中的内核缓冲区到第四步的内核缓 ...

  7. 零拷贝-zero copy

    Efficient data transfer through zero copy Zero Copy I: User-Mode Perspective 0. 前言 在阅读RocketMQ的官方文档时 ...

  8. Java基础-零拷贝技术应用案例

    Java基础-零拷贝技术应用案例 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 零拷贝技术在Hadoop生态圈中很多组件得到应用,典型的比如kafka组件,它就很成功的应用了零拷贝 ...

  9. 走进科学之揭开神秘的&quot;零拷贝&quot;!

        "零拷贝"这三个字,想必大家多多少少都有听过吧,这个技术在各种开源组件中都使用了,比如kafka,rocketmq,netty,nginx等等开源框架都在其中引用了这项技术 ...

  10. NIO零拷贝的深入分析

    深入分析通过Socket进行数据文件传递中的传统IO的弊端以及NIO的零拷贝实现原理,及用户空间和内核空间的切换方式 传统的IO流程 在这个过程中: 数据从磁盘拷贝进内核空间缓冲区 从内核空间缓冲区拷 ...

随机推荐

  1. CTSC2015 酱油记

    终于又到写酱油记的时间了...不过开心不起来诶.. Day 0 晚上睡不着觉也不造为啥... 起来看了一本亚里亚小说,继续睡,睡不着... 又起来看了一本亚里亚小说,继续睡,睡不着... 然后...死 ...

  2. MySQL 数据库中用户表中口令登陆设置

    工具:MyEclipse8.5.apache-tomcat-6.0.43.MySQL5.6 问题:项目是同事发给我,正常运行之后,使用MySQL表里的管理员数据登陆时,提示“仅限于非总部工号登录!” ...

  3. 在WPF的WebBrowser控件中抑制脚本错误

    原文:在WPF的WebBrowser控件中抑制脚本错误 今天用WPF的WebBrowser控件的时候,发现其竟然没有ScriptErrorsSuppressed属性,导致其到处乱弹脚本错误的对话框,在 ...

  4. 长平狐 Cocos2d-x 的“HelloWorld” 深入分析

                              Cocos2d-x 的“HelloWorld” 深入分析 本节所用Cocos2d-x版本:cocos2d-1.0.1-x-0.12.0 不能免俗,一 ...

  5. 51Nod 1509加长棒

    传送门 http://www.51nod.com/onlineJudge/questionCode.html#!problemId=1509 现在有三根木棒,他们的长度分别是a,b,c厘米.你可以对他 ...

  6. 深入理解类成员函数的调用规则(理解成员函数的内存为什么不会反映在sizeof运算符上、类的静态绑定与动态绑定、虚函数表)

    本文转载自:http://blog.51cto.com/9291927/2148695 总结: 一.成员函数的内存为什么不会反映在sizeof运算符上?             成员函数可以被看作是类 ...

  7. 【Python】unittest-2-断言

    Unittest中的断言 1.  python unintest单元测试框架提供了一整套内置的断言方法. (1)如果断言失败,则抛出一个AssertionError,并标识该测试为失败状态 (2)如果 ...

  8. LR中错误代号为27796的一个解决方法

    问题:   曾经遇到过一个问题,在一次性能测试过程中,使用http协议的多用户向服务器发送请求.设置了持续时间,出现错误为:27796, Failed to connect to server 'ho ...

  9. 4-12 xhr协议介绍(及其相关ajax), css:@keyframs rule; http://coffeescrip网站

    https://segmentfault.com/a/1190000004322487 介绍xhr(XMLHttpRequest协议).底部有相关学习知识连接. w3cschool有基础. Anima ...

  10. POJ 2485 Highways 最小生成树 (Kruskal)

    Description The island nation of Flatopia is perfectly flat. Unfortunately, Flatopia has no public h ...