前言
日常我们通过客户端向服务器请求数据或服务,服务器端则根据请求作出响应。客户端的不断增长需要服务器有更强的处理数据能力。事实上,客户端数量的增长速度已经远远高于服务器硬件的增加速度,这就很可能引起服务器的重负荷,继而产生性能瓶颈。从操作系统层面讲,服务器处理请求 响应 是一次处理 “读”和“写”过程,数据经过 磁盘-->内核缓冲-->应用程序-->socket缓冲-->网卡-->宽带-->手机客户端呈现到用户面前。
缓冲区的存在提高了数据的响应速度,但数据的重复流转占用了不必要的CPU性能和内存宽带。零拷贝技术可以解决数据的重复“搬运”问题,更有效的利用系统资源,从而提高服务器性能。
基本介绍
零拷贝是一种避免CPU将数据从一块存储拷贝到另一块存储的技术。数据拷贝对CPU来讲是一种比较简单的任务,如果CPU的大部分时间在处理数据拷贝的任务上,复杂的逻辑处于等待状态,这将造成资源的浪费。
传统拷贝方式
下图是传统的拷贝方式:
上面是一个 数据从服务器传输到网上的流程,如web应用程序静态内容展示,视频网站流量,电影网站数据下载等应用场景。 可以看出 传统的拷贝流程 涉及到 两次DMA直接拷贝 和两次 CPU拷贝。 CPU在处理系统进程和应用进程时会进行上下文切换 下图是一个上下文切换的时序图:
总共经历了4次上下文切换:
1 用户请求读操作 CPU从服务器应用切换到内核
2 数据从内核缓存返回 CPU从内核缓存切换到服务器应用
3 数据从应用转出 CPU从应用切换到内核
4 数据传输到客户端后 服务器应用获得返回 CPU从内核切换到服务器应用
传统的拷贝流程 服务器应用进行了两次没有意义的拷贝和CPU上下文切换,即数据从磁盘拷贝到应用,再从应用拷出去,这样做对意义不大 但对于大数据量的操作 加重了系统负担。下面将介绍的零拷贝 将会解决此问题。
零拷贝方式
从传统的拷贝方式可以看出 服务器应用只是提供了数据的缓存功能,两次CPU拷贝是多余的,数据可以直接通过内核缓存拷贝到套接字缓存 省去了服务器应用的拷贝流程,如下图所示:
零拷贝方式是如何实现的呢? 这要借助于 Linux内核系统中的 方法:
sendfile(socket, file, len)
其中 socket 是要拷贝的套接字缓存地址,file是磁盘文件地址,len为要拷贝的文件大小
其中套接字缓存 仅仅保存了 读缓存内存地址 网卡缓存通过获取套接字保存的地址 直接从读缓存拷贝文件数据。
通过零拷贝 CPU的上下文切换也从传统拷贝的4次降到了2次,即 应用请求 应用-> 系统内核 ,请求返还 系统内核-应用
大大提升了性能。
两种拷贝模式 的性能比较 横轴为文件大小 纵轴为拷贝耗时
上面的零拷贝 是基于应用程序不会对 文件数据进行修改的情况下 ,如果要进行数据修改 数据还是要加载到 应用内存空间内, 如果应用程序能够直接操作 操作系统内核数据 则不需要拷贝到应用程序内,在Linux中使用mmp()方式完成应用程序数据直接写入内存。
应用程序在调用mmp方法后 数据会先通过 DMA 拷贝到操作系统内核的缓冲区中去,应用程序和操作系统共享内核缓冲区,不需要拷贝 应用程序就
可以操作内核缓冲区数据,write 数据完后 内核缓冲区的数据再拷贝到套接字缓冲区,完成拷贝操作
典型应用
操作系统底层的革新和优化会带来上层应用层巨大的提升 进而创造出五颜六色的世界。废话不说 直奔主题
Java NIO的零拷贝
接口
java.nio.channels.FileChannel.transferTo(long position, long count,WritableByteChannel target)使用方法:
//文件输入通道 FileChannel inputFileChannel = getInputFileChannel("Test.dmg"); //文件输出通道 FileChannel outputFileChannel = getOutFileChannel("postManNIO.dmg"); long totalSize = inputFileChannel.transferTo(0,inputFileChannel.size(),outputFileChannel);最终调的是FileChannelImpl JNI接口实现:
private native long transferTo0(FileDescriptor var1, long var2, long var4, FileDescriptor var6);Netty的零拷贝
1. FileRegion
FileRegion 封装了 Java的java.nio.channels.FileChannel.transferTo接口
2. 应用层的零拷贝CompositeByteBuf
如果要对多段数据操作 正常情况下会 把多段数据组合到一起 系统开辟块内存空间 放进去再进行操作。CompositeByteBuf 是将多段数据
组合到一起 但是引用的还是原数据段 没有开辟内存空间。
Netty推荐使用 Upooled.wrappedBuffer(ByteBuffer... buffers) 方法 ,底层还是生成了CompositeByteBuf对象 如图
static <T> ByteBuf wrappedBuffer(int maxNumComponents, ByteWrapper<T> wrapper, T[] array) { switch (array.length) { case 0: break; case 1: if (!wrapper.isEmpty(array[0])) { return wrapper.wrap(array[0]); } break; default: for (int i = 0, len = array.length; i < len; i++) { T bytes = array[i]; if (bytes == null) { return EMPTY_BUFFER; } if (!wrapper.isEmpty(bytes)) { return new CompositeByteBuf(ALLOC, false, maxNumComponents, wrapper, array, i); } } } return EMPTY_BUFFER; }零拷贝在互联网、大数据应用中有着广泛的应用,如 Web应用 静态文件,视频网站 流数据 及 软件下载等,开源框架也对零拷贝进行了支持 和扩展 ,原理就是 减少数据的转移环节,提高性能 ,如果有兴趣可以动手写个零拷贝程序实现
结语
5G时代已经来临,网路的传输速度可能达到 10G每秒的速度,到时候CPU的处理运算速度和处理能力将成为应用的瓶颈,怎样设计一个处理速度快,性能优良的应用架构,开发人员将会面临新的挑战。零拷贝通过减少数据拷贝次数,降低CPU上下文切换的次数 能够有效提升数据的处理速度 从而降低网络延迟 提升网络吞吐。让CPU 、每片内存空间物尽其用 也是每个开发人员面对的问题和挑战。
参考资料
Zero Copy I: User-Mode Perspective
https://www.linuxjournal.com/article/6345
zero copy技术图解
https://www.jianshu.com/p/8c6b056f73ce
Linux 中的零拷贝技术
https://www.ibm.com/developerworks/cn/linux/l-cn-zerocopy1/index.html