【java】FileChannel 和 MappedByteBuffer 实现上有什么不同?为什么性能差这么多?

环境

mac 10.14
2.2 GHz Intel Core i7
APPLE SSD AP0512M (底下有小伙伴测出相反的结果,跟硬盘有关系)

问题描述

看RocketMQ源码的时候看到数据写到MappedFile有两种方式:

  1. 先写入 writeBuffer, 再将writeBuffer 写入到 FileChannel 再调用 force()刷盘;
  2. 数据直接写入MappedByteBuffer, 调用force() 刷盘;

我的问题是为什么不直接采用第二种方法?于是我通过下面的代码验证两种方式的写性能。

相关代码

import java.io.*;

import java.nio.ByteBuffer;

import java.nio.MappedByteBuffer;

import java.nio.channels.FileChannel;

public class MMapTest {

static File file= new File("./test.txt");

static ByteBuffer buffer;

static int fileSize = 8 * 1024 * 1024;

static boolean del = true;

public static void main(String[] args) {

init(1);

deleteFile();

int[] sizes = {128,256,512,4096,8192,1024*16,1024*32,1024*128,1024*512};

try {

for (int size : sizes) {

testDBChannel(size);

testMappedByteBuffer(size);

System.out.println();

}

} catch (IOException e) {

e.printStackTrace();

}

}

private static void init(int size) {

buffer = ByteBuffer.allocateDirect(size);

}

private static void deleteFile() {

file.delete();

}

private static void testDBChannel(int size) throws IOException {

init(size);

RandomAccessFile rw = new RandomAccessFile(file, "rw");

FileChannel channel = rw.getChannel();

int writeSize = 0;

Long start = System.currentTimeMillis();

while (writeSize < fileSize) {

buffer.clear();

buffer.put(new byte[size]);

buffer.flip();

channel.position(writeSize);

channel.write(buffer);

channel.force(false);

writeSize += size;

}

//channel.force(false);

System.out.println("DirectBuffer + FileChannel write " + size + " bytes every time cost: " + (System.currentTimeMillis() - start) + "ms");

if(del)

deleteFile();

}

private static void testMappedByteBuffer(int size) throws IOException {

init(size);

RandomAccessFile rw = new RandomAccessFile(file, "rw");

FileChannel channel = rw.getChannel();

MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, fileSize);

int writeSize = 0;

Long start = System.currentTimeMillis();

while (writeSize < fileSize) {

map.put(new byte[size]);

map.force();

writeSize += size;

}

//map.force();

System.out.println("MappedByteBuffer write " + size + " bytes every time cost: " + (System.currentTimeMillis() - start) + "ms");

if(del)

deleteFile();

}

}

输出:

DirectBuffer + FileChannel write 128 bytes every time cost: 3577ms

MappedByteBuffer write 128 bytes every time cost: 13518ms

DirectBuffer + FileChannel write 256 bytes every time cost: 1968ms

MappedByteBuffer write 256 bytes every time cost: 7044ms

DirectBuffer + FileChannel write 512 bytes every time cost: 1001ms

MappedByteBuffer write 512 bytes every time cost: 3037ms

DirectBuffer + FileChannel write 1024 bytes every time cost: 659ms

MappedByteBuffer write 1024 bytes every time cost: 1274ms

DirectBuffer + FileChannel write 4096 bytes every time cost: 214ms

MappedByteBuffer write 4096 bytes every time cost: 331ms

DirectBuffer + FileChannel write 8192 bytes every time cost: 137ms

MappedByteBuffer write 8192 bytes every time cost: 168ms

DirectBuffer + FileChannel write 16384 bytes every time cost: 77ms

MappedByteBuffer write 16384 bytes every time cost: 86ms

DirectBuffer + FileChannel write 32768 bytes every time cost: 44ms

MappedByteBuffer write 32768 bytes every time cost: 58ms

DirectBuffer + FileChannel write 131072 bytes every time cost: 16ms

MappedByteBuffer write 131072 bytes every time cost: 25ms

DirectBuffer + FileChannel write 524288 bytes every time cost: 10ms

MappedByteBuffer write 524288 bytes every time cost: 21ms

我的理解是两种方式都都是将数据写入到pageCache中再刷盘的,为什么耗时差这么多,具体两种方式的实现原理是什么?

一般情况下使用RocketMQ 都是异步刷盘,会利用OS的pageCache机制达到很高的性能;上面描述的这个问题是针对同步刷盘情况,按照 @Tyrael 第一种测试,SATA盘情况下,mbb 的性能 是要高于 db+fc 的,更加让我怀疑为什么不直接用mbb。

回答

MappedByteBuffer
在写盘时会自己创建一个DirectBuffer暂存的。因为写盘时操作系统要求传递的内存地址不能变,但是java堆管理在gc中可能把数据搬来搬去,所以需要一个堆外的DirectBuffer来存临时数据。相比直接DirectBuffer操作,是增加了一步的。

以上是看错了问题。
我测试了三种情况,macpro 14mid,阿里云ecs,macpro18。从测试情况上看应该是跟系统硬件,系统本身有关系。但是没有什么规律的样子

2019-01-15更新

这几天参阅了大量资料,总结了一个应该是正确答案的答案:
关于java8下,linux与mac得到的结果不同的解答,
FileChannel的force方法与MappedByteBuffer的force方法,最终调用的是系统中的fsync与msync方法。

  • mac中,fsync方法明确说明了该方法不一定强制写盘(man page里也没有明确说写到哪了,我认为应该是写到了kernel自己的缓存里,此时fsync就返回成功了,然后kernel再缓慢的写盘 可以参考这篇文章中间的关于macos下的说明),但是同时提供了一个F_FULLFSYNC fcntl来进行强制写盘操作,msync没有说明,可以认为是强制写盘。
  • linux中, fsync方法明确说明了该方法只有写盘后才返回否则一直阻塞。这跟msync其实是类似的效果了。也有人说这种情况下,msync类似于fsync。这也是我在评论里有一个耗时差不多的情况的原因。但是从理论上,msync应该快于fsync,我怀疑是测试代码并没有达到fsync的瓶颈。

关于java11下FileChannel慢的要死的问题,
这里我发现其实是FileChannel#write()方法拖慢了节奏,尝试着找了一下java8与java11在c下的write0方法的实现,看write0()代码上都是一样的,所以也没找出具体原因。怀疑可能跟write0调用的write方法有区别。

总结。
jni的效率其实跟系统强关联,不同系统实现不同,可能会导致不同的结果,所以评论中的问题关于要用哪个,我建议是,在实际的机器环境上写测试脚本测试一下,哪个符合你的需求,就用哪个。然后以后的机器环境做统一。

RocketMQ 异步刷盘之所以提供DirectBuffer + FileChannel,主要是未了解决pageCache读写并发的问题,参考:
【java】FileChannel 和 MappedByteBuffer 实现上有什么不同?为什么性能差这么多?

以上是 【java】FileChannel 和 MappedByteBuffer 实现上有什么不同?为什么性能差这么多? 的全部内容, 来源链接: utcz.com/a/75297.html

回到顶部