当前位置: 首页 > news >正文

零拷贝应用场景

零拷贝应用场景

1 Netty的零拷贝

大部分场景下,在Netty接收和发送ByteBuffer的过程中会使用直接内存进行Socket通道读写,使用JVM的堆内存进行业务处理,会涉及直接内存、堆内存之间的数据复制。内存的数据复制其实是效率非常低的,Netty提供了多种方法,以帮助应用程序减少内存的复制。

Netty的零拷贝(Zero-Copy)主要体现在五个方面:

  • (1)Netty提供 CompositeByteBuf 组合缓冲区类,可以将多个ByteBuf合并为一个逻辑上的ByteBuf,避免了各个ByteBuf之间的拷贝。
  • (2) Netty提供了 ByteBuf 的浅层复制操作(slice、duplicate),可以将ByteBuf分解为多个共享同一个存储区域的ByteBuf,避免内存的拷贝。
  • (3) 在使用Netty进行文件传输时,可以调用 FileRegion 包装的transferTo()方法直接将文件缓冲区的数据发送到目标通道,避免普通的循环读取文件数据和写入通道所导致的内存拷贝问题。
  • (4)在将一个 byte 数组转换为一个ByteBuf对象的场景下,Netty提供了一系列的包装类,避免了转换过程中的内存拷贝。
  • (5) 如果通道接收和发送ByteBuf都使用直接内存进行Socket读写,就不需要进行缓冲区的二次拷贝。如果使用JVM的堆内存进行Socket读写,那么JVM会先将堆内存Buffer拷贝一份到直接内存再写入Socket中,相比于使用直接内存,这种情况在发送过程中会多出一次缓冲区的内存拷贝。所以,在发送ByteBuffer到Socket时,尽量使用直接内存而不是JVM堆内存。

补充说明:
Netty中的零拷贝和操作系统层面上的零拷贝是有区别的,不能混淆,我们所说的Netty零拷贝完全是基于Java层面或者说用户空间的,它更多的是偏向于应用中的数据操作优化,而不是系统层面的操作优化。

1.1 通过CompositeByteBuf实现零拷贝

CompositeByteBuf可以把需要合并的多个ByteBuf组合起来,对外提供统一的readIndex和writerIndex。CompositeByteBuf只是在逻辑上是一个整体,在CompositeByteBuf内部,合并的多个ByteBuf都是单独存在的。CompositeByteBuf里面有一个Component数组,聚合的ByteBuf都放在Component数组里面,最小容量为16。

在很多通信编程场景下,需要多个ByteBuf组成一个完整的消息。例如,HTTP协议传输时消息总是由Header(消息头)和Body(消息体)组成。如果传输的内容很长,就会分成多个消息包进行发送,消息中的Header就需要重用,而不是每次发送都创建新的Header缓冲区。这时可以使用CompositeByteBuf缓冲区进行ByteBuf组合,避免内存拷贝。

假设有一份协议数据,它由头部和消息体组成,而头部和消息体是分别存放在两个ByteBuf中的,为了方便后续处理,要将两个ByteBuf进行合并,具体如图5-23所示。

在这里插入图片描述
使用CompositeByteBuf合并多个ByteBuf,大致的代码如下:

ByteBuf headerBuf =ByteBuf bodyBuf =CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();cbuf.addComponents(headerBuf, bodyBuf);

不使用CompositeByteBuf,将header和body合并为一个ByteBuf的代码大致如下:

ByteBuf headerBuf =ByteBuf bodyBuf =long length=headerBuf.readableBytes() + bodyBuf.readableBytes()ByteBuf allBuf = Unpooled.buffer(length);
allBuf.writeBytes(headerBuf );//拷贝header数据
allBuf.writeBytes(body);//拷贝body数据

上述过程将header和body都拷贝到了新的allBuf中,这增加了两次额外的数据拷贝操作。所以,使用CompositeByteBuf合并ByteBuf可以减少两次额外的数据拷贝操作。

下面是一段通过CompositeByteBuf来复用header的比较完整的演示代码:

package com.crazymakercircle.netty.bytebuf;
//…
public class CompositeBufferTest {static Charset utf8 = Charset.forName("UTF-8");@Testpublic void byteBufComposite() {CompositeByteBuf cbuf = ByteBufAllocator.DEFAULT.compositeBuffer();//消息头ByteBuf headerBuf = Unpooled.copiedBuffer("疯狂创客圈:", utf8);//消息体1ByteBuf bodyBuf = Unpooled.copiedBuffer("高性能Netty", utf8);cbuf.addComponents(headerBuf, bodyBuf);sendMsg(cbuf);//在refCnt为0前, retainheaderBuf.retain();cbuf.release();cbuf = ByteBufAllocator.DEFAULT.compositeBuffer();//消息体2bodyBuf = Unpooled.copiedBuffer("高性能学习社群", utf8);cbuf.addComponents(headerBuf, bodyBuf);sendMsg(cbuf);cbuf.release();}private void sendMsg(CompositeByteBuf cbuf) {//处理整个消息for (ByteBuf b :cbuf) {int length = b.readableBytes();byte[] array = new byte[length];//将CompositeByteBuf中的数据统一复制到数组中b.getBytes(b.readerIndex(), array);//处理一下数组中的数据System.out.print(new String(array, utf8));}System.out.println();}
}

在上面的程序中,调用CompositeByteBuf的addComponents()方法向自身增加了ByteBuf对象实例。对于所添加的ByteBuf, Heap ByteBuf、Direct ByteBuf均可。

如果CompositeByteBuf内部只存在一个ByteBuf,则调用其hasArray()方法,返回的是这个唯一实例hasArray()方法的值;如果有多个ByteBuf,则其hasArray()方法会返回false。

另外,调用CompositeByteBuf的nioBuffer()方法可以将CompositeByteBuf实例合并成一个新的NIO ByteBuffer缓冲区(注意:不是Netty的ByteBuf缓冲区)​。演示代码如下:

package com.crazymakercircle.netty.bytebuf;
//…
public class CompositeBufferTest {@Testpublic void intCompositeBufComposite() {CompositeByteBuf cbuf = Unpooled.compositeBuffer(3);cbuf.addComponent(Unpooled.wrappedBuffer(new byte[]{1, 2, 3}));cbuf.addComponent(Unpooled.wrappedBuffer(new byte[]{4}));cbuf.addComponent(Unpooled.wrappedBuffer(new byte[]{5, 6}));//合并成一个的Java NIO缓冲区ByteBuffer nioBuffer = cbuf.nioBuffer(0, 6);byte[] bytes = nioBuffer.array();System.out.print("bytes = ");for (byte b : bytes) {System.out.print(b);}cbuf.release();}
}

1.2 通过wrap操作实现零拷贝

Unpooled提供了一系列的wrap包装方法,可以帮助大家方便、快速地包装出CompositeByteBuf实例或者ByteBuf实例,而不用进行内存拷贝。

Unpooled包装CompositeByteBuf的操作使用起来更加方便。例如,上一小节的header与body的组合可以调用Unpooled.wrappedBuffer()方法。大致的代码如下:

ByteBuf headerBuf =ByteBuf bodyBuf =ByteBuf allByteBuf = Unpooled.wrappedBuffer(headerBuf , bodyBuf );

Unpooled类提供了很多重载的wrappedBuffer()方法,将多个ByteBuf包装为CompositeByteBuf实例,从而实现零拷贝。这些重载方法大致如下:

public static ByteBuf wrappedBuffer(ByteBuffer buffer)
public static ByteBuf wrappedBuffer(ByteBuf buffer)
public static ByteBuf wrappedBuffer(ByteBuf… buffers)
public static ByteBuf wrappedBuffer(ByteBuffer… buffers)

除了通过Unpooled包装CompositeByteBuf之外,还可以将byte数组包装成ByteBuf。如果将一个byte数组转换为一个ByteBuf对象,大致的代码如下:

byte[] bytes =ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);

通过调用Unpooled.wrappedBuffer()方法将bytes包装为一个UnpooledHeapByteBuf对象,在包装的过程中不会有拷贝操作,所得到的ByteBuf对象和bytes数组共用同一个存储空间,对bytes的修改也是对ByteBuf对象的修改。

如果不是调用Unpooled.wrappedBuffer()包装方法,那么传统的做法是将此byte数组的内容拷贝到ByteBuf中,大致的代码如下:

byte[] bytes =ByteBuf byteBuf = Unpooled.buffer();
byteBuf.writeBytes(bytes);

显然,传统的转换方式是有额外的内存申请和拷贝操作的,既浪费了内存空间,又需要耗费内存复制的时间。相对而言,Unpooled提供的wrap操作既复用了空间,又节省了时间。

Unpooled提供了多个包装字节数组的重载方法,大致如下:

public static ByteBuf wrappedBuffer(byte[] array)
public static ByteBuf wrappedBuffer(byte[] array, int offset, int length)
public static ByteBuf wrappedBuffer(byte[]… arrays)

Unpooled类还提供了一些其他的避免零拷贝的方法,具体可以参见其源码,这里不再赘述。

2 Kafka 零拷贝

除了消息顺序追加、页缓存等技术,Kafka还使用零拷贝(Zero-Copy)技术来进一步提升性能。所谓的零拷贝是指将数据直接从磁盘文件复制到网卡设备中,而不需要经由应用程序之手。零拷贝大大提高了应用程序的性能,减少了内核和用户模式之间的上下文切换。对 Linux操作系统而言,零拷贝技术依赖于底层的 sendfile(​)方法实现。对应于 Java 语言,FileChannal.transferTo(​)方法的底层实现就是sendfile(​)方法。

单纯从概念上理解“零拷贝”比较抽象,这里简单地介绍一下它。考虑这样一种常用的情形:你需要将静态内容(类似图片、文件)展示给用户。这个情形就意味着需要先将静态内容从磁盘中复制出来放到一个内存buf中,然后将这个buf通过套接字(Socket)传输给用户,进而用户获得静态内容。这看起来再正常不过了,但实际上这是很低效的流程,我们把上面的这种情形抽象成下面的过程:

在这里插入图片描述

首先调用read(​)将静态内容(这里假设为文件A)读取到tmp_buf,然后调用write(​)将tmp_buf写入Socket,如图5-23所示。

在这个过程中,文件A经历了4次复制的过程:

  • (1)调用read(​)时,文件A中的内容被复制到了内核模式下的Read Buffer中。
  • (2)CPU控制将内核模式数据复制到用户模式下。
  • (3)调用write(​)时,将用户模式下的内容复制到内核模式下的Socket Buffer中。
  • (4)将内核模式下的Socket Buffer的数据复制到网卡设备中传送。

在这里插入图片描述

从上面的过程可以看出,数据平白无故地从内核模式到用户模式“走了一圈”​,浪费了2次复制过程:第一次是从内核模式复制到用户模式;第二次是从用户模式再复制回内核模式,即上面4次过程中的第2步和第3步。而且在上面的过程中,内核和用户模式的上下文的切换也是4次。

如果采用了零拷贝技术,那么应用程序可以直接请求内核把磁盘中的数据传输给Socket,如图5-24所示。

在这里插入图片描述

零拷贝技术通过DMA(Direct Memory Access)技术将文件内容复制到内核模式下的Read Buffer 中。不过没有数据被复制到 Socket Buffer,相反只有包含数据的位置和长度的信息的文件描述符被加到Socket Buffer中。DMA引擎直接将数据从内核模式中传递到网卡设备(协议引擎)​。这里数据只经历了2次复制就从磁盘中传送出去了,并且上下文切换也变成了2次。零拷贝是针对内核模式而言的,数据在内核模式下实现了零拷贝。

参考

书名:Java高并发核心编程 卷1:NIO、Netty、Redis、ZooKeeper
作者:尼恩

http://www.dtcms.com/a/299977.html

相关文章:

  • 【Spring AI】大模型服务平台-阿里云百炼
  • 基于cooragent的旅游多智能体的MCP组件安装与其开发
  • javaSE 6
  • connect系统调用及示例
  • Go-Elasticsearch v9 安装与版本兼容性
  • Docker常用命令详解:以Nginx为例
  • 求hom_math_2d的角度值
  • Aerospike架构深度解析:打造web级分布式应用的理想数据库
  • JS实现数字变化时,上下翻滚动画效果
  • 本地部署智能家居集成解决方案 ESPHome 并实现外部访问
  • 五分钟系列-文本搜索工具grep
  • 【工具】好用的浏览器AI助手
  • 【MySQL】VARCHAR(10) 和 VARCHAR(100) 的区别
  • 大模型蒸馏(distillation)---从DeepseekR1-1.5B到Qwen-2.5-1.5B蒸馏
  • 拒绝SQL恐惧:用Python+pyqt打造任意Excel数据库查询系统
  • C++ - 仿 RabbitMQ 实现消息队列--服务端核心模块实现(四)
  • 丝杆升降机应用在食品机械行业有什么特殊的要求吗
  • Java BeanUtils 类详解:作用、语法与示例
  • springboot 基于签名的安全通信
  • 深入解析YARN中的FairScheduler与CapacityScheduler:资源分配策略的核心区别
  • Aerospike Java客户端进阶:对象映射与Spring Data集成实战
  • spring Could 高频面试题
  • 【科普】java和html和lvgl生成页面有什么区别,还有什么方法可以生成?
  • 数据库HB OB mysql ck startrocks, ES存储特点,以及应用场景
  • 通过服务启动应用的流程(类似SystemUi启动流程)
  • Linux笔记5——常用命令-4
  • 深入浅出学习 KNN 算法:从原理到数字识别实践
  • 【Linux庖丁解牛】— 日志进程池 !
  • 大模型系列——Dify:知识库与外部知识库
  • SSH连接失败排查与解决教程: Connection refused