在谈论Kafka高性能时不得不提到零拷贝。Kafka通过采用零拷贝大大提供了应用性能,减少了内核和用户模式之间的上下文切换次数。那么什么是零拷贝,如何实现零拷贝呢?

什么是零拷贝

WIKI中对其有如下定义:

"Zero-copy" describes computer operations in which the CPU does not perform the task of copying data from one memory area to another.

从WIKI的定义中,我们看到“零拷贝”是指计算机操作的过程中,CPU不需要为数据在内存之间的拷贝消耗资源。而它通常是指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式。

零拷贝给我们带来的好处

  • 减少甚至完全避免不必要的CPU拷贝,从而让CPU解脱出来去执行其他的任务
  • 减少内存带宽的占用
  • 通常零拷贝技术还能够减少用户空间和操作系统内核空间之间的上下文切换

零拷贝的实现

零拷贝实际的实现并没有真正的标准,取决于操作系统如何实现这一点。零拷贝完全依赖于操作系统。操作系统支持,就有;不支持,就没有。不依赖Java本身。

传统I/O

在Java中,我们可以通过InputStream从源数据中读取数据流到一个缓冲区里,然后再将它们输入到OutputStream里。我们知道,这种IO方式传输效率是比较低的。那么,当使用上面的代码时操作系统会发生什么情况:

 
传统IO.jpg

这是一个从磁盘文件读取并且通过socket写出的过程,对应的系统调用如下:

read(file,tmp_buf,len)
write(socket,tmp_buf,len)
  1. 程序使用read()系统调用。系统由用户态转换为内核态(第一次上线文切换),磁盘中的数据有DMA(Direct Memory Access)的方式读取到内核缓冲区(kernel buffer)。DMA过程中CPU不需要参与数据的读写,而是DMA处理器直接将硬盘数据通过总线传输到内存中。
  2. 系统由内核态转换为用户态(第二次上下文切换),当程序要读取的数据已经完成写入内核缓冲区以后,程序会将数据由内核缓存区,写入用户缓存区),这个过程需要CPU参与数据的读写。
  3. 程序使用write()系统调用。系统由用户态切换到内核态(第三次上下文切换),数据从用户态缓冲区写入到网络缓冲区(Socket Buffer),这个过程需要CPU参与数据的读写。
  4. 系统由内核态切换到用户态(第四次上下文切换),网络缓冲区的数据通过DMA的方式传输到网卡的驱动(存储缓冲区)中(protocol engine)

可以看到,传统的I/O方式会经过4次用户态和内核态的切换(上下文切换),两次CPU中内存中进行数据读写的过程。这种拷贝过程相对来说比较消耗资源

内存映射方式I/O
 
mmap.jpg
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);

这是使用的系统调用方法,这种方式的I/O原理就是将用户缓冲区(user buffer)的内存地址和内核缓冲区(kernel buffer)的内存地址做一个映射,也就是说系统在用户态可以直接读取并操作内核空间的数据。

  1. mmap()系统调用首先会使用DMA的方式将磁盘数据读取到内核缓冲区,然后通过内存映射的方式,使用户缓冲区和内核读缓冲区的内存地址为同一内存地址,也就是说不需要CPU再讲数据从内核读缓冲区复制到用户缓冲区。
  2. 当使用write()系统调用的时候,cpu将内核缓冲区(等同于用户缓冲区)的数据直接写入到网络发送缓冲区(socket buffer),然后通过DMA的方式将数据传入到网卡驱动程序中准备发送。

可以看到这种内存映射的方式减少了CPU的读写次数,但是用户态到内核态的切换(上下文切换)依旧有四次,同时需要注意在进行这种内存映射的时候,有可能会出现并发线程操作同一块内存区域而导致的严重的数据不一致问题,所以需要进行合理的并发编程来解决这些问题。

通过sendfile实现的零拷贝I/O
 
sendfile.jpg
sendfile(socket, file, len);

通过sendfile()系统调用,可以做到内核空间内部直接进行I/O传输。

  1. sendfile()系统调用也会引起用户态到内核态的切换,与内存映射方式不同的是,用户空间此时是无法看到或修改数据内容,也就是说这是一次完全意义上的数据传输过程。
  2. 从磁盘读取到内存是DMA的方式,从内核读缓冲区读取到网络发送缓冲区,依旧需要CPU参与拷贝,而从网络发送缓冲区到网卡中的缓冲区依旧是DMA方式。

依旧有一次CPU进行数据拷贝,两次用户态和内核态的切换操作,相比较于内存映射的方式有了很大的进步,但问题是程序不能对数据进行修改,而只是单纯地进行了一次数据的传输过程。

理想状态下的零拷贝I/O
 
sendfile2.jpg

依旧是系统调用sendfile()

sendfile(socket, file, len);

可以看到,这是真正意义上的零拷贝,因为其间CPU已经不参与数据的拷贝过程,也就是说完全通过其他硬件和中断的方式来实现数据的读写过程吗,但是这样的过程需要硬件的支持才能实现。

借助于硬件上的帮助,我们是可以办到的。之前我们是把页缓存的数据拷贝到socket缓存中,实际上,我们仅仅需要把缓冲区描述符传到socket缓冲区,再把数据长度传过去,这样DMA控制器直接将页缓存中的数据打包发送到网络中就可以了。

  1. 系统调用sendfile()发起后,磁盘数据通过DMA方式读取到内核缓冲区,内核缓冲区中的数据通过DMA聚合网络缓冲区,然后一齐发送到网卡中。

可以看到在这种模式下,是没有一次CPU进行数据拷贝的,所以就做到了真正意义上的零拷贝,虽然和前一种是同一个系统调用,但是这种模式实现起来需要硬件的支持,但对于基于操作系统的用户来讲,操作系统已经屏蔽了这种差异,它会根据不同的硬件平台来实现这个系统调用

Java的实现

NIO的零拷贝
  File file = new File("test.zip");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fileChannel = raf.getChannel();
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("", 1234));
// 直接使用了transferTo()进行通道间的数据传输
fileChannel.transferTo(0, fileChannel.size(), socketChannel);

NIO的零拷贝由transferTo()方法实现。transferTo()方法将数据从FileChannel对象传送到可写的字节通道(如Socket Channel等)。在内部实现中,由native方法transferTo0()来实现,它依赖底层操作系统的支持。在UNIX和Linux系统中,调用这个方法将会引起sendfile()系统调用。

使用场景一般是:

  • 较大,读写较慢,追求速度
  • M内存不足,不能加载太大数据
  • 带宽不够,即存在其他程序或线程存在大量的IO操作,导致带宽本来就小

以上都建立在不需要进行数据文件操作的情况下,如果既需要这样的速度,也需要进行数据操作怎么办?
那么使用NIO的直接内存!

NIO的直接内存
  File file = new File("test.zip");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fileChannel = raf.getChannel();
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());

首先,它的作用位置处于传统IO(BIO)与零拷贝之间,为何这么说?

  • IO,可以把磁盘的文件经过内核空间,读到JVM空间,然后进行各种操作,最后再写到磁盘或是发送到网络,效率较慢但支持数据文件操作。
  • 零拷贝则是直接在内核空间完成文件读取并转到磁盘(或发送到网络)。由于它没有读取文件数据到JVM这一环,因此程序无法操作该文件数据,尽管效率很高!

而直接内存则介于两者之间,效率一般且可操作文件数据。直接内存(mmap技术)将文件直接映射到内核空间的内存,返回==一个操作地址(address)==,它解决了文件数据需要拷贝到JVM才能进行操作的窘境。而是直接在内核空间直接进行操作,省去了内核空间拷贝到用户空间这一步操作。

NIO的直接内存是由==MappedByteBuffer==实现的。核心即是map()方法,该方法把文件映射到内存中,获得内存地址addr,然后通过这个addr构造MappedByteBuffer类,以暴露各种文件操作API。

由于MappedByteBuffer申请的是堆外内存,因此不受Minor GC控制,只能在发生Full GC时才能被回收。而==DirectByteBuffer==改善了这一情况,它是MappedByteBuffer类的子类,同时它实现了DirectBuffer接口,维护一个Cleaner对象来完成内存回收。因此它既可以通过Full GC来回收内存,也可以调用clean()方法来进行回收。

另外,直接内存的大小可通过jvm参数来设置:-XX:MaxDirectMemorySize。

NIO的MappedByteBuffer还有一个兄弟叫做HeapByteBuffer。顾名思义,它用来在堆中申请内存,本质是一个数组。由于它位于堆中,因此可受GC管控,易于回收。

参考
https://blog.csdn.net/localhost01/article/details/83422888
https://blog.csdn.net/cringkong/article/details/80274148

作者:攀山客
链接:https://www.jianshu.com/p/497e7640b57c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

零拷贝的原理及Java实现的更多相关文章

  1. 【Netty技术专题】「原理分析系列」Netty强大特性之ByteBuf零拷贝技术原理分析

    零拷贝Zero-Copy 我们先来看下它的定义: "Zero-copy" describes computer operations in which the CPU does n ...

  2. 零拷贝Zero copy-linux and java

    背景-几种拷贝方式 方式1:Copying in Two Sample System Calls read(file, tmp_buf, len); write(socket, tmp_buf, le ...

  3. java的零拷贝机制

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

  4. NIO零拷贝的深入分析

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

  5. 零拷贝(Zero-copy) 浅析及其应用

    相信大家都有过面经历,如果跟面试官聊到了操作系统,聊到了文件操作,可能会问你普通的文件读写流程,它有什么缺点,你知道有什么改进的措施.我们经常听说 零拷贝,每次可能只是背诵一些面试要点就过去了,今天我 ...

  6. Linux、JDK、Netty中的NIO与零拷贝

    一.先理解内核空间与用户空间 Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间,分别对应着下图中, CPU 特权等级分为4个,Linux 使用 Ring 0 和 Ring 3. 内核空 ...

  7. 操作系统IO之零拷贝技术

    磁盘可以说是计算机系统最慢的硬件之一,读写速度相差内存 10 倍以上,所以针对优化磁盘的技术非常的多,比如零拷贝.直接 I/O.异步 I/O 等等,这些优化的目的就是为了提高系统的吞吐量,另外操作系统 ...

  8. 转载一篇关于kafka零拷贝(zero-copy)通俗易懂的好文

    原文地址 https://www.cnblogs.com/yizhou35/p/12026263.html 零拷贝就是一种避免CPU 将数据从一块存储拷贝到另外一块存储的技术. DMA技术是Direc ...

  9. kafka零拷贝

    Kafka之所以那么快的另外一个原因就是零拷贝(zero-copy)技术.本文我们就来了解Kafka中使用的零拷贝技术为什么那么快. 传统的文件拷贝 传统的文件拷贝通常需要从用户态去转到核心态,经过r ...

随机推荐

  1. vue2-org-tree 基于VUE的部门组织架构组件,增删节点实现

    本文所用组件传送门:vue-org-tree 本文基于antd (其他前端组件框架操作基本都类似的: iview,elementui,boostrap-vue...) 当然,github上还有其他类似 ...

  2. Oracle 多行拼接函数 :[Err] ORA-00904: "WM_CONCAT": 标识符无效

    SELECT LISTAGG(GOODSNAME,';') WITHIN GROUP (ORDER BY GOODSNAME) AS sd WM_CONCAT()  方法被替代成:LISTAGG(GO ...

  3. C学习笔记-typedef

    typedef是一种高级数据特性,它能使某一类型创建自己的名字 typedef unsigned char BYTE; typedef struct man MAN; BYTE b = 0x12; 与 ...

  4. 【Java 基础】Java 基础索引

    Java 基础 注解 [注解]深入理解Java注解类型(@Annotation) [注解]Java注解(1)-基础 [注解]Java注解(2)-运行时框架 [注解]Java注解(3)-源码级框架

  5. 如何在Windows中手动生成SSH密钥?(转)

    在Windows上,您可以通过多种方式创建SSH密钥.Windows需要SSH客户端,但在其操作系统上没有默认的SSH客户端.请注意,Windows目前正在测试本机OpenSSH应用程序,一般,不提倡 ...

  6. Linx

    1. 2. 2. 3. 5. Vi 猜数字 第二十个裴伯拉数字 1 1 2 3 5 8 2 3 求小于3000的裴伯拉数列 4 5 递归方式1到100 和 6 7 100 以内奇数.偶数和 8 Sss ...

  7. Web前端开发HTML基础

    HTML是英文Hyper Text Mark-up Language(超文本标记语言)的缩写,他是一种制作万维网页面标准语言(标记),相当于定义统一的一套规则,大家都来遵守他,这样就可以让浏览器根据标 ...

  8. 福建工程学院第十四届ACM校赛M题题解 fwt进阶,手推三进制fwt

    第九集,结束亦是开始 题意: 大致意思就是给你n个3进制的数字,让你计算有多少对数字的哈夫曼距离等于i(0<=i<=2^m) 思路: 这个是一个防ak题,做法是要手推公式的fwt 大概就这 ...

  9. Box-shadow制作漂亮的外阴影输入框

    背景:之前做项目中的一个移动端页面,关于在搜索框中输入信息查找对应的照片 改了几次ui图之后,最终的搜索框的设计图如下: 开始做页面的时候,就想到了用box-shadow 来实现外阴影边框.用bord ...

  10. ITCAST-C# 委托

    using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace _12委 ...