基础知识

一、零拷贝

目的:

1. 减少或避免不必要的CPU拷贝, 2. 减少用户空间(应用程序自己的空间)和内核空间(linux内核自身的空间,包括进程调度、连接硬件资源、内存分配等)的上下文切换, 3. 减少内存的占用

典型应用:

Netty、Kafka等

基本概念:

1.  缓冲区:是所有I/O的基础,I/O 无非就是把数据移进或移出缓冲区。

2. 虚拟内存:通过虚拟技术,将外部存储设备的一部分空间,划分给系统,作为在内存不足时临时用作数据缓存。

3. 直接内存访问(Direct Memory Access)(DMA):DMA允许不同速度的硬件装置来沟通,而不需要依于 CPU 的大量中断负载,是一种可以大大减轻 CPU 工作量的数据转移方式。基于 DMA 访问方式,系统主内存于硬盘或网卡之间的数据传输可以绕开 CPU 的调度。

额外提一下,RDMA(Remote Direct Memory Access):远程直接内存访问,允许远程访问磁盘IO,减少CPU参与。

缓存队列和消息队列区别_数据

     ------》        

缓存队列和消息队列区别_数据_02

 

 把磁盘控制器缓存区和内核缓冲区之间的数据拷贝,由CPU转移到DMA去做。通过 DMA 和虚拟内存技术,我们实现了 Zero Copy 的目标,IO 设备跟用户程序空间传输数据的过程中,减少数据拷贝次数,减少系统调用,实现 CPU 的零参与,彻底消除 CPU 在这方面的负载。(图片来源网络,链接建参考)

缓存队列和消息队列区别_用户空间_03

 

见下图:把内核空间地址和用户空间的虚拟地址映射到同一个物理地址(下图物理内存蓝色区块),这样DMA就可以填充(读写)对内核和用户空间进程同时可见的缓冲区了。

缓存队列和消息队列区别_用户空间_04

(来源网络)

 

 传统IO

缓存队列和消息队列区别_缓存队列和消息队列区别_05

 

(说明:应用进程的堆内和堆外内存都在用户空间)

4次用户态到内核态上下文切换、4次拷贝(2次CPU复制)。

 零拷贝可以通过FileChannel的sendfile或者mmap+write方式实现,减少用户空间和内核空间之间的内存拷贝/CPU复制/上下文切换。

mmap+write实现的零拷贝

缓存队列和消息队列区别_用户空间_06

 

用户空间和内核空间映射

4次用户态到内核态的上下文切换、3次拷贝(其中一次CPU复制)。

sendfile实现的零拷贝 

(有文档介绍说linux 内核2.6以后版本不支持sendfile)

缓存队列和消息队列区别_零拷贝_07

 

2次上下文切换、3次拷贝(其中1次CPU复制)。

对比:

mmap+write方式多了2次mmap/write用户态到内核态的上下文切换,他们比传统IO(fileInputstream.write/read方式)少1次CPU复制(都用DMA复制的情况下)。

DMA复制,减少了2次磁盘和内核数据传输导致的CPU占用。

 

二、Page Cache

Page cache是通过将磁盘中的数据缓存到内存中,为了减少磁盘I/O操作,提高性能。

由物理page组成,内容对应磁盘的block。大小是动态变化的,可以扩大,也可以在内存不足时缩小。一个page通常包括多个block。

page cache可以大大加快文件的读写速度,一次读取或写入4k的数据,节省连接的各种开销。程序将数据先写入page cache,在fsync到磁盘(page cache回写)中。

但是,一旦断电或者是故障,数据会丢失,没办法保障数据安全。

page cache会根据策略刷入磁盘,比如,2G内存,规定50%刷入磁盘,超过1G后就会将page cache刷入磁盘。如果不足1G,这部分内存称为脏页,如果忽然断电,脏页会丢失的。如果直接来了一个超过2G的数据写入了page cache,会通过LRU算法(最近最常用的数据保留),通过swap交换硬盘的空间,将最不常用的数据刷入swap区中。脏页会先刷到磁盘,才可以淘汰,保证数据不丢。但是不是脏页,会直接通过LRU淘汰调最远最不常用数据。

在Linux内核中,文件的每个数据块最多只能对应一个page cache项,它通过两个数据结构来管理这些cache项,一个是radix tree,另一个是双向链表。
Radix tree是一种搜索树,Linux内核利用这个数据结构,快速查找脏的(dirty)和回写的(writeback)页面,得到其文件内偏移,从而对page cache进行快速定位。图1是radix tree的一个示意图,该radix tree的分叉为4(22),树高为4,用来快速定位8位文件内偏移。
另一个数据结构是双向链表,Linux内核为每一片物理内存区域(zone)维护active_list和 inactive_list两个双向链表,这两个list主要用来实现物理内存的回收。这两个链表上除了文件Cache之外,还包括其它匿名 (Anonymous)内存,如进程堆栈等。

Page Cache 操作API

FileChannel:

缓存队列和消息队列区别_用户空间_08

 

 

读写主要两种,1. FileChannel的read/write/sendfile(linux内核有些版本不支持sendfile)

 

                    2. mmap

 mmap原理

缓存队列和消息队列区别_数据_09

 

 传统文件I/O和内存文件映射的过程图的区别。内存文件映射是将文件直接映射至用户空间内存,未经过内核空间缓冲区的拷贝,相对于传统的I/O减少一次内存拷贝。

transferFrom和transferTo原理

缓存队列和消息队列区别_缓存队列和消息队列区别_10

 

 Page Cache回写

page cache回写是指 将page cache写入磁盘中。回写后,系统也会将page cache这部分内存回收。

触发条件

1. 空间层面: 脏数据占比阈值(dirty_background_ratio)+ 脏数据大小阈值(dirty_background_bytes,优先级别高于前者)。

分级别:

1)略超阈值,比如脏数据占比默认为10%,超过10%,不足20%,后端异步线程回写;

2)严重超阈值,比如脏数据占比超过20%,堵塞page cache write程序,进行回写。

2. 从时间的层面:即周期性的回写(dirty_writeback_interval)

3. 用户主动发起sync()/msync()/fsync()调用

线程

缓存队列和消息队列区别_零拷贝_11

 

三、mmap、allocateDirect+write和allocate+write性能对比

Java中NIO的核心缓冲就是ByteBuffer,所有的IO操作都是通过这个ByteBuffer进行的;

Bytebuffer有两种: 分配HeapByteBufferByteBuffer buffer = ByteBuffer.allocate(int capacity);分配DirectByteBufferByteBuffer buffer = ByteBuffer.allocateDirect(int capacity);

缓存队列和消息队列区别_缓存队列和消息队列区别_12

 

HeapByteBuffer会多一次拷贝:

传统 BIO 是面向 Stream 的,底层实现可以理解为写入的是 byte 数组,调用 native 方法写入 IO,传的参数是这个数组,就算GC改变了内存地址,但是拿这个数组的引用照样能找到最新的地址,对应的方法时是:FileOutputStream.write

 但是NIO,为了提升效率,传的是内存地址,省去了一次间接应用。GC会回收无用对象,同时还会进行碎片整理,移动对象在内存中的位置,来减少内存碎片。如果在调用系统调用时,发生了GC,导致HeapByteBuffer内存位置发生了变化,但是内核态并不能感知到这个变化导致系统调用读取或者写入错误的数据。而DirectByteBuffer不受GC控制。所以HeapByteBuffer会多一次拷贝到堆外内存的过程。(题外话 mmap用到的MappedByteBuffer也是堆外)

1. Direct buffer(allocateDirect)是相当于固定的内核buffer还是JVM进程内的堆外内存?J

VM进程的堆外内存,属于用户空间。

2. Direct buffer的好处和坏处

好处:

a. 相比HeapByteBuffer,少一次堆内拷贝到堆外的过程

b. gc压力小

坏处:

自己管理内存。创建开销大。

 

对比代码:

 

//allocateDirect+write
@Test
public void testWriteFile2() throws Exception {
final String fileName = "/Users/chengzhiliang/temp/test.dat";
File file = new File(fileName);
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fileChannel = raf.getChannel();byte b = 110;
ByteBuffer directBuffer = ByteBuffer.allocateDirect(writeSize);
for (int i = 0; i < writeSize; i++) {
directBuffer.put(b);
}//write direct buffer
long start = SystemClock.now();for (int i = 0; i < totalSize / writeSize; i++) {
directBuffer.flip();
fileChannel.write(directBuffer);
}System.out.println("Write direct byte buffer to channel elapse " + (SystemClock.now() - start) + " ms");
fileChannel.close();
}
}//mmap
@Test
public void testWriteFile1() throws Exception {
final String fileName = "/Users/chengzhiliang/temp/test.dat";
File file = new File(fileName);
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fileChannel = raf.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(writeSize);
byte b = 110;
for (int i = 0; i < writeSize; i++) {
buffer.put(b);
}//write mapped byte buffer
//mappedByteBuffer属于堆外内存
long start = SystemClock.now();
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, totalSize);
for (int i = 0; i < totalSize / writeSize; i++) {
buffer.flip();
mappedByteBuffer.put(buffer);
}System.out.println("Write mapped byte buffer elapse " + (SystemClock.now() - start) + " ms");
fileChannel.close();
}import org.junit.Test;
import java.io.File;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;public class Demo {
    private final int writeSize = 1024 * 1024; // 单次写入大小
    private final int totalSize = 1024 * 1024 * 1024; // 总的大小//allocate+write
    @Test
    public void testWriteFile() throws Exception {
        final String fileName = "/Users/chengzhiliang/temp/test.dat";
        File file = new File(fileName);
        RandomAccessFile raf = new RandomAccessFile(file, "rw");
        FileChannel fileChannel = raf.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(writeSize);
        byte b = 110;
        for (int i = 0; i < writeSize; i++) {
            buffer.put(b);
        }        //write byte buffer
        long start = SystemClock.now();        for (int i = 0; i < totalSize / writeSize; i++) {
            buffer.flip();
            fileChannel.write(buffer);
        }        System.out.println("Write byte buffer to channel elapse " + (SystemClock.now() - start) + " ms");
        fileChannel.close();
    }import org.junit.Test;
import java.io.File;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;public class Demo {
    private final int writeSize = 1024 * 1024; // 单次写入大小
    private final int totalSize = 1024 * 1024 * 1024; // 总的大小//allocate+write
    @Test
    public void testWriteFile() throws Exception {
        final String fileName = "/Users/chengzhiliang/temp/test.dat";
        File file = new File(fileName);
        RandomAccessFile raf = new RandomAccessFile(file, "rw");
        FileChannel fileChannel = raf.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(writeSize);
        byte b = 110;
        for (int i = 0; i < writeSize; i++) {
            buffer.put(b);
        }        //write byte buffer
        long start = SystemClock.now();        for (int i = 0; i < totalSize / writeSize; i++) {
            buffer.flip();
            fileChannel.write(buffer);
        }        System.out.println("Write byte buffer to channel elapse " + (SystemClock.now() - start) + " ms");
        fileChannel.close();
    }//mmap
@Test
public void testWriteFile1() throws Exception {
final String fileName = "/Users/chengzhiliang/temp/test.dat";
File file = new File(fileName);
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fileChannel = raf.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(writeSize);
byte b = 110;
for (int i = 0; i < writeSize; i++) {
buffer.put(b);
}//write mapped byte buffer
//mappedByteBuffer属于堆外内存
long start = SystemClock.now();
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, totalSize);
for (int i = 0; i < totalSize / writeSize; i++) {
buffer.flip();
mappedByteBuffer.put(buffer);
}System.out.println("Write mapped byte buffer elapse " + (SystemClock.now() - start) + " ms");
fileChannel.close();
}import org.junit.Test;
import java.io.File;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;public class Demo {
    private final int writeSize = 1024 * 1024; // 单次写入大小
    private final int totalSize = 1024 * 1024 * 1024; // 总的大小//allocate+write
    @Test
    public void testWriteFile() throws Exception {
        final String fileName = "/Users/chengzhiliang/temp/test.dat";
        File file = new File(fileName);
        RandomAccessFile raf = new RandomAccessFile(file, "rw");
        FileChannel fileChannel = raf.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(writeSize);
        byte b = 110;
        for (int i = 0; i < writeSize; i++) {
            buffer.put(b);
        }        //write byte buffer
        long start = SystemClock.now();        for (int i = 0; i < totalSize / writeSize; i++) {
            buffer.flip();
            fileChannel.write(buffer);
        }        System.out.println("Write byte buffer to channel elapse " + (SystemClock.now() - start) + " ms");
        fileChannel.close();
    }//allocateDirect+write
@Test
public void testWriteFile2() throws Exception {
final String fileName = "/Users/chengzhiliang/temp/test.dat";
File file = new File(fileName);
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fileChannel = raf.getChannel();byte b = 110;
ByteBuffer directBuffer = ByteBuffer.allocateDirect(writeSize);
for (int i = 0; i < writeSize; i++) {
directBuffer.put(b);
}//write direct buffer
long start = SystemClock.now();for (int i = 0; i < totalSize / writeSize; i++) {
directBuffer.flip();
fileChannel.write(directBuffer);
}System.out.println("Write direct byte buffer to channel elapse " + (SystemClock.now() - start) + " ms");
fileChannel.close();
}
}//mmap
@Test
public void testWriteFile1() throws Exception {
final String fileName = "/Users/chengzhiliang/temp/test.dat";
File file = new File(fileName);
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fileChannel = raf.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(writeSize);
byte b = 110;
for (int i = 0; i < writeSize; i++) {
buffer.put(b);
}//write mapped byte buffer
//mappedByteBuffer属于堆外内存
long start = SystemClock.now();
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, totalSize);
for (int i = 0; i < totalSize / writeSize; i++) {
buffer.flip();
mappedByteBuffer.put(buffer);
}System.out.println("Write mapped byte buffer elapse " + (SystemClock.now() - start) + " ms");
fileChannel.close();
}import org.junit.Test;
import java.io.File;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;public class Demo {
    private final int writeSize = 1024 * 1024; // 单次写入大小
    private final int totalSize = 1024 * 1024 * 1024; // 总的大小//allocate+write
    @Test
    public void testWriteFile() throws Exception {
        final String fileName = "/Users/chengzhiliang/temp/test.dat";
        File file = new File(fileName);
        RandomAccessFile raf = new RandomAccessFile(file, "rw");
        FileChannel fileChannel = raf.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(writeSize);
        byte b = 110;
        for (int i = 0; i < writeSize; i++) {
            buffer.put(b);
        }        //write byte buffer
        long start = SystemClock.now();        for (int i = 0; i < totalSize / writeSize; i++) {
            buffer.flip();
            fileChannel.write(buffer);
        }        System.out.println("Write byte buffer to channel elapse " + (SystemClock.now() - start) + " ms");
        fileChannel.close();
    }

 

对比结果,总共写入1GB文件,采用上述三次方式,每次分别写入1KB、10KB、100KB、1MB,发现100KB是个分水岭,1KB和10KB 性能mmap > allocateDirect(堆外) > allocate(堆内);100KB以后, allocateDirect > mmap.

自研MQ采用allocate+write写,mmap读的方式。

 

四、各MQ的存储结构对比

待完善。。。。

缓存队列和消息队列区别_用户空间_13