互联网三高-高性能之IO网络技术底层机制
1 DMA技术
1.1 概念
直接内存访问
允许某些硬件系统能够独立于CPU直接读写操作系统内存,不需要CPU介入
DMAC(DMA控制器)控制数据传输操作,这时CPU可以去做其他操作
1.2 应用程序从磁盘读取数据
1.3 DMA工作
① 从磁盘缓冲区到内核缓冲区的拷贝工作 (读)
② 从网卡设备到内核的 Socket Buffer 的拷贝工作(读)
③ 从内核缓冲区到磁盘缓冲区的拷贝工作(写)
④ 从Socket Buffer 到内核的 网卡设备 的拷贝工作(写)
1.4 DMA性能损害
4次内核态和用户态之间的上下文切换
4次缓冲区拷贝
2 零拷贝
2.1 零拷贝作用
减少不必要的内核缓冲区和用户缓冲区的拷贝工作
减少内核态和用户态的上下文切换
2.2 零拷贝技术实现
(1)方式一:mmap + write(2个函数)
① 原理
操作系统使用虚拟地址,多个虚拟地址可以指向用一个物理地址
把内核空间和用户空间的虚拟地址映射到同一个物理地址,就不需要来回复制数据了
mmap系统函数:把内核缓冲区数据映射到用户空间
② 损耗
4次内核态和用户态之间的上下文切换
3次拷贝(2次DMA拷贝,1次CPU拷贝)
③ 哪些框架使用了这个
RocketMQ:主要是mmap,也有小部分使用sendfile
消息存盘和网络发送使用mmap,单个commitLog文件大小默认为1G
(2)方式二:sendfile(函数)
① 原理
sendfile()函数,替代了read()和write()两个系统调用
② 损耗
2次用户态和内核态之间的上下文切换
3次拷贝(2次DMA拷贝,1次CPU拷贝)
③ 改进
Linux2.4+版本改进了sendfile,利用DMA Gather(带有收集功能的DMA),变成了真正的零拷贝(没有了CPU拷贝)
④ 改进版损耗
2次用户态和内核态之间的上下文切换
2次拷贝(2次DMA拷贝)
⑤ 哪些框架使用
Nginx
Kafka:主要是snedfile,小部分使用mmap
在客户端和Broker进行数据传输时,Broker使用sendfile函数调用
3 Java使用零拷贝(依赖于底层的系统实现)
3.1 FileChannel:文件通道
是Java NIO中用于文件读/写、内存映射、数据强制持久化及高效传输的核心通道类,其底层通过操作系统原生接口实现文件操作,支持线程安全并发访问,常被用于高效的网络/文件的数据传输。
(1)创建方式
只读:FileInputStream.getChannel()
可写:FileOutputStream.getChannel()
读写:RandomAccessFile.getChannel()
(2)mmap的实现
map()方法:MappedByteBuffer map(MapMode mode,long position, long size)
把文件映射成内存映射文件
一次map大小限制为2G,过大map会增加虚拟内存回收和重新分配的压力,直接报错
(3)sendfile的实现
① transferTo():long transferTo(long position, long count, WritableByteChannel target)
将字节从此通道的文件传输到给定的可写入字节通道
返回值为真实拷贝的size,最大拷贝为2G,超过2G部分将丢弃
② transferFrom():long transferFrom(ReadableByteChannel src,long position, long count)
将字节从给定的可读取字节通道传输到此通道的文件中
注意:只有FileChannel才支持transfer这种高效复制方式,SocketChannel等都不支持transfer模式。
FileChannel --> FileChannel
FileChannel --> SocketChannel
3.2 Java文件拷贝不同方式
(1)普通的Java IO流
/*** 普通的Java IO流 完成文件复制* @param inputFilePath* @param outputFilePath*/
private static void inputStreamCopyFile(String inputFilePath, String outputFilePath) {final long start = System.currentTimeMillis();try(FileInputStream fis = new FileInputStream(inputFilePath);final FileOutputStream fos = new FileOutputStream(outputFilePath);) {byte[] b = new byte[64];int len;while ((len = fis.read(b)) != -1) {fos.write(b);}} catch (Exception e) {e.printStackTrace();}final long end = System.currentTimeMillis();log.info("【inputStreamCopyFile】,总耗时:{}", (end - start));
}
(2)普通的Java带Buffer的IO流
/*** 普通的Java带Buffer的IO流 完成文件复制* @param inputFilePath* @param outputFilePath*/
private static void bufferedInputStreamCopyFile(String inputFilePath, String outputFilePath) {final long start = System.currentTimeMillis();try(final BufferedInputStream bis = new BufferedInputStream(new FileInputStream(inputFilePath));final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(outputFilePath));) {byte[] b = new byte[64];int len;while ((len = bis.read(b)) != -1) {bos.write(b);}} catch (Exception e) {e.printStackTrace();}final long end = System.currentTimeMillis();log.info("【bufferedInputStreamCopyFile】,总耗时:{}", (end - start));
}
(3)零拷贝实现之mmap的IO流
/*** 零拷贝实现之mmap的IO流 完成文件复制* @param inputFilePath* @param outputFilePath*/
private static void mmapCopyFile(String inputFilePath, String outputFilePath) {final long start = System.currentTimeMillis();try(final FileChannel inputChannel = new FileInputStream(inputFilePath).getChannel();final FileChannel outputChannel = new RandomAccessFile(outputFilePath,"rw").getChannel();) {long size = inputChannel.size();log.info("【mmapCopyFile】 fileSize: {}", size);final MappedByteBuffer mapIn = inputChannel.map(FileChannel.MapMode.READ_ONLY, 0, size);final MappedByteBuffer mapOut = outputChannel.map(FileChannel.MapMode.READ_WRITE, 0, size);for(int i=0; i<size; i++) {final byte b = mapIn.get(i);mapOut.put(b);}} catch (Exception e) {e.printStackTrace();}final long end = System.currentTimeMillis();log.info("【mmapCopyFile】,总耗时:{}", (end - start));
}
(4)零拷贝实现之sendfile的IO流
/*** 零拷贝实现之sendfile的IO流 完成文件复制* @param inputFilePath* @param outputFilePath*/
private static void sendfileCopyFile(String inputFilePath, String outputFilePath) {final long start = System.currentTimeMillis();try(final FileChannel inputChannel = new FileInputStream(inputFilePath).getChannel();final FileChannel outputChannel = new FileOutputStream(outputFilePath).getChannel();) {// 方式1:针对小于2G的,最大拷贝为2G,超过2G部分将丢弃// inputChannel.transferTo(0,inputChannel.size(), outputChannel);// 方式1:针对大于2G的final long size = inputChannel.size();for(long left = size; left > 0;) {final long transferSize = inputChannel.transferTo((size - left), left, outputChannel);// 剩下还有多少left = left - transferSize;log.info("文件总大小:{},拷贝大小:{},剩下大小:{}", size, transferSize, left);}} catch (Exception e) {e.printStackTrace();}final long end = System.currentTimeMillis();log.info("【sendfileCopyFile】,总耗时:{}", (end - start));
}
4 TCP底层设计交互原理
4.1 三次握手
三次握手流程
① 一次握手:客户端发送一个SYN数据包到服务端,用来请求建立连接,状态变为SYN_SEND(同步发送状态)
报文中,SYN=1,同时随机生成初始序列seq=x
② 二次握手:服务端接收到客户端的SYN数据包,并回复一个SYN+ACK数据包,用来确认连接,状态变为SYN_RECEIVED(同步接收状态)
确认报文中,ACK=1,SYN=1,确认序号是ack=x+1,同时随机初始化一个序列号 seq=y
③ 三次握手:客户端收到服务器的SYN+ACK数据包,并回复一个ACK数据包,用来确认连接建立完成,状态变为ESTABLISHED(建⽴连接)
确认报文 ACK=1,ack=y+1,seq=x+1
第三次握手可以携带数据了
4.2 四次挥手
四次挥手流程
① 一次挥手:客户端发送FIN报文段,用于关闭客户端到服务端的数据传输,表示客户端的数据发送完毕,客户端进入FIN_WAIT_1状态
② 二次挥手:服务端收到客户端的FIN报文段后,发送ACK报文段,确认收到了客户端的FIN报文段,服务端进入CLOSE_WAIT状态,客户端接收到这个确认包后进入FIN_WAIT_2状态
③ 三次挥手:服务端发送FIN报文段,用于关闭服务端到客户端的数据传输,表示服务端的数据发送完毕,服务端进入LAST_ACK状态,等待客户端的最后一个ACK
④ 四次挥手:客户端收到服务端的FIN报文段后,发送ACK报文段,确认收到了服务端的FIN报文段;客户端接收后进入TIME_WAIT状态,在此阶段下等待2MSL时间(2个最大段生命周期),如果这个时间间隔内没有收到服务端的请求,进入CLOSED状态;服务端接收到ACK确认包后,也进入CLOSED状态