目录
一、I/O 工作方式
1.1 DMA
二、零拷贝
2.1 传统数据拷贝
2.2 Java 应用数据处理
2.3. 零拷贝流程
三、零拷贝案例
四、总结
磁盘可以说是计算机系统中最慢的硬件之一,读写速度相差内存10倍以上,所以针对磁盘的优化技术非常多。下面以文件传输为切入线分析 I/O 的工作方式。
在没有 DMA 技术前,I/O 是这样工作的,如下图:
可以看到,整个传输过程,都需要 CPU 亲自搬运数据,,而且这个过程是阻塞的,CPU 不能做其他的工作,简单的搬运几个字符问题不大,如果是搬运大量的数据,肯定忙不过来。于是 DMA 技术诞生了。
DMA(Direct Memory Access,直接内存访问)技术是一种在计算机系统中用于高效数据传输的技术,它允许数据在设备和内存之间直接传输,而无需 CPU 的干预。DMA 技术显著提高了数据传输的速度和系统的整体性能,因为它减少了 CPU 的负担,使得 CPU 可以专注于其他更重要的任务。
工作流程图如下:
可以看到,整个数据传输过程,CPU不在参与数据搬运的工作,而是全程由DMA完成,但是CPU在这个过程中也是必不可少的,因为传输什么数据,从哪里传输到哪里,都需要CPU来告诉DMA控制器。
从上述过程可以看到,传统的数据传输模型中,数据在不同系统组件之间传输时,往往需要再用户空间和内核空间之间进行多次拷贝。这种多次数据拷贝不仅消耗 CPU 计算资源,增加了上下文切换的开销,还可能导致较高的延迟和系统吞吐量的下降。在数据较小的情况下,这种影响可能不明显,但在大数据和高速网络环境中,频繁的数据拷贝成为了明显的性能瓶颈。
下面来看一个例子,了解下传统数据传输的过程。如果服务端要提供文件传输功能,我们能想到的最简单的方式:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。
传统I/O的工作方式是,数据读取和写入是从用户空间到内核空间来回复制,内核空间的数据是通过操作系统层面的I/O接口从磁盘读取或写入的。
在这个过程中共发生了 4 次用户态与内核态的上下文切换,因为发生了两次系统调用,分别为 read 和 write,每次系统调用都需先从用户态切换到内核态,等内核任务完成后,在从内核态切换到用户态。
上下文切换的成本是比较高的,在高并发场景下,上下文切换过多的话,就会被放大,影响整体的性能。
在这个过程中发生了 4 次拷贝动作,其中两次是 DMA 完成的拷贝,两次是 CPU 来完成的拷贝。
我们只是搬运一份数据,就涉及到了 4 次数据拷贝,4 次上下文切换,在高并发系统中,这种操作是很糟糕的,严重影响性能。
这里在扩展一下,在 Java 应用中,实际上可能会涉及 6 次数据拷贝。
Java 的类实例一般在 JVM 的堆上分配的,而 Java 是通过 JNI 调用 C 代码来完成 socket 通信的,JVM 内存之外的部分叫做本地内存,C 程序代码在运行过程中用到的内存就是本地内存中分配的。
Java NIO API 提供了两种 Buffer 来接收数据:HeapByteBuffer 和 DirectByteBuffer,下面代码是如何创建两种Buffer。
那 HeapByteBuffer 和 DirectByteBuffer 有什么区别呢?HeapByteBuffer 对象本身在 JVM 堆上分配,并且它持有的字节数组 byte[] 也是在 JVM 堆上分配。但是如果用 HeapByteBuffer 来接收网络数据,需要把数据从内核先拷贝到一个临时的本地内存,再从临时本地内存拷贝到 JVM 堆上,而不是直接从内核拷贝到JVM堆上。这是为什么呢?因为数据从内核拷贝到JVM堆上的过程中,JVM 可能会发生 GC,GC 过程中对象可能被移动,也就是说 JVM 堆上的字节数组有可能被移动,这样的话 Buffer 地址就失效了。如果这中间经过本地内存中转,从本地内存到 JVM 堆拷贝的过程中可以保证不做 GC。
如果使用 HeapByteBuffer,你会发现 JVM 堆和内核之间多了一层中转,而DirectByteBuffer 用来解决这个问题,DirectByteBuffer 对象本身在 JVM 堆上,但是它持有的字节数组不是从 JVM 堆上分配的,而是从本地内存分配的。DirectByteBuffer 对象中有个 Long 类型字段 address,记录着本地内存地址,这样在接收数据的时候直接把这个本地内存地址传给 C程序,C 程序会将网络数据从内核拷贝到这个本地内存,这种方式比 HeapByteBuffer 少了一次拷贝,因此一般来说它的速度比 HeapByteBuffer 快好几倍。
所以如果在使用中使用的是 HeapByteBuffer,那么数据拷贝就会是 6 次了。
Linux 内核从2.4版本开始,对于网卡支持 SG-DMA 技术的情况下,sendFile() 系统调用的过程发生了变化。
这就是所谓的零拷贝技术,因为我们没有在内存层面去拷贝数据,也就是全程没有通过CPU来搬运数据,所有的数据都通过DMA来进行传输。与传统方式相比,减少了两次上下文切换和数据拷贝次数,只需要两次上下文切换就可以完成文件传输,而且两次数据拷贝都不要CPU的参与,所以,零拷贝技术可以把文件传输的性能提高至少一倍以上。
下面看一个简单的案例来对比下零拷贝技术对性能的提升。
public static void traditionalCopy(File input,File output) throws Exception{
StopWatch stopWatch = new StopWatch();
stopWatch.start();
@Cleanup
RandomAccessFile read = new RandomAccessFile(input, "rw");
@Cleanup
RandomAccessFile write = new RandomAccessFile(output, "rw");
byte[] buf = new byte[1024];
int len;
while ((len = read.read(buf)) != -1) {
write.write(buf, 0, len);
}
stopWatch.stop();
System.out.println("traditionalCopy耗时" + stopWatch.getTotalTimeSeconds());
}
public static void zeroCopy(File input,File output) throws Exception {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
FileInputStream fis = new FileInputStream(input);
FileChannel srcChannel = fis.getChannel();
FileOutputStream fos = new FileOutputStream(output);
FileChannel destChannel = fos.getChannel();
srcChannel.transferTo(0, input.length(), destChannel);
stopWatch.stop();
System.out.println("zeroCopy耗时:" + stopWatch.getTotalTimeSeconds());
}
实际测试,一个 816MB 的文件,传统方式的时间大概在 6-12s 之间,用 NIO 零拷贝的方式在 1s 左右,相差悬殊,文件越大性能差距越明显。
有很多开源框架都使用了零拷贝技术来提升性能,下面介绍一些:
零拷贝技术的出现和发展,是为了适应高速网络和大规模数据处理的需要,解决传统数据传输机制的效率低下问题,优化系统资源利用,提高数据传输和处理的速度与性能。随着技术的进步,零拷贝技术在现代操作系统和高性能网络编程框架中得到了广泛应用,成为提高系统效率和响应速度的关键技术之一。
往期经典推荐:
深入理解I/O模型-CSDN博客
Sentinel与Nacos强强联合,构建微服务稳定性基石的重要实践_nacos sentinel-CSDN博客
Kafka VS RabbitMQ,架构师教你如何选择_rabbitmq和kafka选型-CSDN博客
TiDB存储引擎TiKV揭秘-CSDN博客
深入浅出 Drools 规则引擎-CSDN博客