什么是零拷贝

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了什么是零拷贝相关的知识,希望对你有一定的参考价值。

参考技术A 零拷贝描述的是客户端与服务器之间数据传输过程中,需要拷贝的问题

**客户端**

普通拷贝:用户发起指令给内核,内核拷贝磁盘的文件到内核缓冲区,然后由内核缓冲区拷贝到用户缓冲区,然后再由用户缓冲区拷贝到内核缓冲区,内核缓冲区通过网络发送到服务器,经过三次拷贝

零拷贝:直接通过内核空间不经过用户空间进行发送(实现方式:1、api,2、内核空间的内存地址和用户空间的内存地址映射到同一块)(使用场景: Java NIO、Netty,kafaka,rocketMq)

**服务端**

常见的IO 多路复用方式有【select、poll、epoll】

就是很多个网络I/O复用一个或少量的线程来处理这些连接

select/poll模型是通过轮询遍历(fd_set集合)所有内核缓冲器内的文件描述符(fd),发现就绪的文件描述符(fd),则处理fd数据;select 在单个进程中能打开的 fd 是有限制的,默认是1024

【 什么是 fd:在 linux 中,内核把所有的外部设备都当成是一个文件来操作,对一个文件的读 写会调用内核提供的系统命令,返回一个 fd( 文件描述符)。而对于一个 socket 的读写也会有 相应的文件描述符,成为 socketfd 】

epoll 是基于事件驱动方式来代替顺序扫描,是监听就绪的fd,轮询就绪的fd的方式

【由于 epoll 能够通过事件告知应用进程哪个 fd 是可读的,所以我们也称这种 IO 为异步非 阻塞 IO ,当然它是伪异步的,因为它还需要去把数据从内核同步复制到用户空间中,真正的    异步非阻塞,应该是数据已经完全准备好了,我只需要从用户空间读就行(AIO)】

epoll的两种触发机制(水平触发(默认)和边缘触发)

边缘触发把如何处理数据的控制权完全交给了开发者,比如,读取一个http的请求,开发者可以决定只读取http中的headers数据就停下来,开发者有机会更精细的定制这里的控制逻辑

nio中所有的通信都是面向缓冲区的,操作数据之前先要开辟一个Buffer的缓冲区指定缓冲区大小ByteBuffer.allocate(1024)

一台机器理论能支持的连接数

首先,在确定最大连接数之前,大家先跟我来先了解一下系统如何标识一个tcp 连接。系统用一个四元组来唯一标识一个TCP 连接: (source_ip, source_port, destination_ip,destination_port)。即(源IP,源端口,目的 IP,目的端口)四个元素的组合。只要四个元素的组合中有一个元素不一样,那就可以区别不同的连接,

比如:

你的IP 地址是 11.1.2.3, 在 8080 端口监听

那么当一个来自 22.4.5.6 ,端口为 5555 的连接到达后,那么建立的这条连接的四元组为 :

(11.1.2.3, 8080,

22.4.5.6, 5555)

这时,假设上面的那个客户(22.4.5.6)发来第二条连接请求,端口为 6666,那么,新连接 的四元组为(11.1.2.3, 8080,

22.4.5.6, 5555)

那么,你主机的 8080 端口建立了两条连接;

通常来说,服务端是固定一个监听端口,比如 8080,等待客户端的连接请求。在不考虑地址重用的情况下,及时 server 端有多个ip,但是本地监听的端口是独立的。所以对于tcp 连接的4 元组中,如果destination_ip 和destination_port 不变。那么只有source_ip 和source_port 是可变的,因此最大的 tcp 连接数应该为 客户端的ip 数 乘以 客户端的端口数。在 IPV4 中, 不考虑 ip 分类等因素,最大的 ip 数为 2 的 32 次方 ;客户端最大的端口数为 2 的 16 次方, 也就是 65536.   也就是服务端单机最大的tcp 连接数约为 2 的48 次方。

当然,这只是一个理论值,以linux 服务器为例,实际的连接数还取决于

1、内存大小(因为每个 TCP 连接都要占用一定的内存)、

2、文件句柄限制,每一个 tcp 连接都需要占一个文件描述符,一旦这个文件描述符使用完了,新来的连接会返回一个“Can’t open so many files”的异常。如果大家知道对于操作系统最大可以打开的文件数限制,就知道怎么去调整这个限制

a)可以执行【ulimit -n】得到当前一个进程最大能打开1024 个文件,所以你要采用此默认配置最多也就可以并发上千个 TCP 连接。

b) 可以通过【vim /etc/security/limits.conf】去修改系统最大文件打开数的限制

softnofile 2048

hard nofile 2048

表示修改所有用户限制、soft/hard 表示软限制还是硬限制,2048 表示修改以后的值

c) 可以通过【cat /proc/sys/fs/file-max】查看linux 系统级最大打开文件数限制,表示当前这个服务器最多能同时打开多少个文件

当然,这块还有其他很多的优化的点,这里不是这节课的目标

3.  带宽资源的限制

NIO的好处,Netty线程模型,什么是零拷贝

NIO

Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)

介绍Netty线程模型前,首先会介绍下经典的Reactor线程模型,目前大多数网络框架都是基于Reactor模式进行设计和开发,Reactor模式基于事件驱动,非常适合处理海量的I/O事件 Reactor模式首先是事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler

Reactor单线程模型

单线程模型下,所有的IO操作都由同一个Reactor线程来完成,负责接收客户端的连接,读取消息,发送应答

Reactor多线程模型

由一个Reactor线程-Acceptor线程用于监听服务端,接收客户端连接请求,网络I/O操作读、写等由Reactor线程池负责处理,绝大多数场景下,Reactor多线程模型都可以满足性能需求,但是,在极个别特殊场景中,一个Reactor线程负责监听和处理所有的客户端连接可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能

Reactor主从多线程模型

服务端使用一个独立的主Reactor线程池来处理客户端连接,当服务端收到连接请求时,从主线程池中随机选择一个Reactor线程作为Acceptor线程处理连接 链路建立成功后,将新创建的SocketChannel注册到sub reactor线程池的某个Reactor线程上,由它处理后续的I/O操作

Netty线程模型

Netty同时支持Reactor单线程模型 、Reactor多线程模型和Reactor主从多线程模型,用户可根据启动参数配置在这三种模型之间切换,服务端启动时,通常会创建两个NioEventLoopGroup实例,对应了两个独立的Reactor线程池

 
   
   
 
  1. EventLoopGroup bossGroup = new NioEventLoopGroup(1);

  2. EventLoopGroup workerGroup = new NioEventLoopGroup();

  3. try {

  4. ServerBootstrap b = new ServerBootstrap();

  5. b.group(bossGroup, workerGroup)

  6. .channel(NioServerSocketChannel.class)

  7. .option(ChannelOption.SO_BACKLOG, 100)

  8. .handler(new LoggingHandler(LogLevel.INFO))

  9. .childHandler(new ChannelInitializer() {

  10. @Override

  11. public void initChannel(SocketChannel ch) throws Exception {

  12. ......


bossGroup负责处理客户端的连接请求,workerGroup负责处理I/O相关的操作,执行系统Task、定时任务Task等。

用户可根据服务端引导类ServerBootstrap配置参数选择Reactor线程模型,进而最大限度地满足用户的定制化需求;同时,为了最大限度地提升性能,netty很多地方采用了无锁化设计,如为每个Channel绑定唯一的EventLoop,这意味着同一个Channel生命周期内的所有事件都将由同一个Reactor线程来完成,这种串行化处理方式有效地避免了多线程操作之间锁的竞争和上下文切换带来的开销。此外,每个Reactor线程配备了一个task队列和Delay task队列,分别用于存放系统Task和周期性Task,也就是说每个Reactor线程不仅要处理I/O事件,还会处理一些系统任务和调度任务。

零拷贝

零拷贝是指计算机操作的过程中,CPU不需要为数据在内存之间的拷贝消耗资源。而它通常是指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式。Zero Copy的模式中,避免了数据在用户空间和内存空间之间的拷贝,从而提高了系统的整体性能。Linux中的sendfile()以及Java NIO中的FileChannel.transferTo()方法都实现了零拷贝的功能,而在Netty中也通过在FileRegion中包装了NIO的FileChannel.transferTo()方法实现了零拷贝。

传统方法->把字节从文件拷贝到套接字

 
   
   
 
  1. File.read(fileDesc, buf, len);

  2. Socket.send(socket, buf, len);


拷贝的操作需要四次用户模式和内核模式间的上下文切换,而且在操作完成前数据被复制了四次

  1. send() 系统调用返回,结果导致了第四次的上下文切换。DMA 引擎将数据从内核缓冲区传到协议引擎,第四次拷贝独立地、异步地发生 。

transferTo

transferTo() 方法将数据从文件通道传输到了给定的可写字节通道。在内部,它依赖底层操作系统对零拷贝的支持;在 UNIX 和各种 Linux 系统中,此调用被传递到 sendfile() 系统调用中

transferTo() 方法引发 DMA 引擎将文件内容拷贝到一个读取缓冲区。然后由内核将数据拷贝到与输出套接字相关联的内核缓冲区。数据的第三次复制发生在 DMA 引擎将数据从内核套接字缓冲区传到协议引擎时。改进的地方:我们将上下文切换的次数从四次减少到了两次,将数据复制的次数从四次减少到了三次(其中只有一次涉及到了 CPU)。但是这个代码尚未达到我们的零拷贝要求。如果底层网络接口卡支持收集操作 的话,那么我们就可以进一步减少内核的数据复制。在 Linux 内核 2.4 及后期版本中,套接字缓冲区描述符就做了相应调整,以满足该需求。这种方法不仅可以减少多个上下文切换,还可以消除需要涉及 CPU 的重复的数据拷贝。对于用户方面,用法还是一样的,但是内部操作已经发生了改变:transferTo() 方法引发 DMA 引擎将文件内容拷贝到内核缓冲区。

数据未被拷贝到套接字缓冲区。取而代之的是,只有包含关于数据的位置和长度的信息的描述符被追加到了套接字缓冲区。DMA 引擎直接把数据从内核缓冲区传输到协议引擎,从而消除了剩下的最后一次 CPU 拷贝。

Netty的零拷贝

  • Netty 提供了 CompositeByteBuf 类, 它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝.

  • 通过 wrap 操作, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作.

  • ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝.

  • 通过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题.


以上是关于什么是零拷贝的主要内容,如果未能解决你的问题,请参考以下文章

Java IO篇:什么是零拷贝?

NIO的好处,Netty线程模型,什么是零拷贝

零拷贝

零拷贝详解

NIO中的零拷贝

一文读懂零拷贝