What is the difference between FileChannel and MappedByteBuffer implementation? Why is the performance so poor?

Environment

mac 10.14
2.2 GHz Intel Core i7
APPLE SSD AP0512M (a partner below shows the opposite result, which is related to the hard drive)

problem description

when you look at the RocketMQ source code, you can see that there are two ways to write data to MappedFile:

  1. write writeBuffer first, then write writeBuffer to FileChannel and then call force () to flush disk;
  2. data is directly written to MappedByteBuffer, and force () is called to brush the disk.

my question is why not just use the second method? So I verified the write performance in two ways through the following code.

related codes

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

output:

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

my understanding is that both methods write data to pageCache and then refresh the disk. Why does it take so much time? what is the implementation principle of the two methods?

in general, the use of RocketMQ is asynchronous flushing, and the pageCache mechanism of OS will be used to achieve high performance. The problem described above is aimed at synchronous flushing. According to the first test of @ Tyrael, the performance of mbb is higher than that of db+fc, which makes me wonder why we don"t use mbb directly.
Apr.24,2022

the reason why RocketMQ asynchronous flash disk provides DirectBuffer + FileChannel is that the problem of concurrent reading and writing of pageCache has not been solved. For more information:
clipboard.png


MappedByteBuffer
will create a DirectBuffer temporary storage when writing the disk. Because the memory address required by the operating system cannot be changed when writing the disk, but java heap management may move data around in gc, so an off-heap DirectBuffer is needed to store temporary data. Compared to the direct DirectBuffer operation, it is an additional step.

the above is a mistake.
I tested three situations, macpro 14mid and Aliyun ecs,macpro18. From the point of view of the test, it should be related to the system hardware and the system itself. But there is no regular appearance

2019-01-15 update

I have read a lot of materials these days and summed up an answer that should be the correct answer:
about the different results obtained by linux and mac under java8, the force method of
FileChannel and the force method of MappedByteBuffer are finally called the fsync and msync methods in the system.

    In
  • mac, the fsync method makes it clear that this method does not necessarily force disk writing (where it was written in man page, I think it should be written to kernel's own cache, and fsync returns success at this time, and then kernel writes slowly to the disk can refer to the article on the instructions under macos). But at the same time, a F_FULLFSYNC fcntl is provided to force disk writing. Msync does not specify it, so it can be regarded as mandatory disk writing.
  • In
  • linux, the fsync method makes it clear that the method will not return until the disk is written, otherwise it will always block. This is actually similar to the effect of msync. Others say that in this case, msync is similar to fsync. That's why I have a similar time-consuming situation in my comments. But in theory, msync should be faster than fsync, and I suspect that the test code did not reach the bottleneck of fsync.

with regard to the slow FileChannel under java11,
here I found that the FileChannel-sharpwrite () method actually slows down the pace. I try to find out the implementation of the write0 method of java8 and java11 under c, and see that the write0 () code is the same, so I can't find out the specific reason. Suspicion may be different from the write method called by write0.

Summary.
the efficiency of jni is strongly related to the system, and different system implementations may lead to different results, so the question in the comments is to write a test script on the actual machine environment to test it, and use which one meets your needs. Then the future machine environment will be unified.

Menu