Linux IO和Zero Copy总结

张佳

前言

最近工作中一个系统要基于cannal订阅binlog数据,然后本地消费,但是发现IO居高不下,通常情况达到150%以上,顺着这个问题阅读了canal中关于网络数据处理的源码,发现性能并不是好,后面不得不切换到改用kafka消费,而kafka作为一个高性能消息处理中间件能够实现海量消息处理的一个因素是因为Zero Copy,借此机会重新总结了下Linux下IO的一些知识。

几种IO介绍

Linux中文件访问的几种方式

  1. 标准文件访问 (缓存IO)
    用户访问磁盘中数据时,通常通过read()或者write()调用完成,操作系统会先将数据读取或者写入到page cache中,然后再写入磁盘,Linux默认写入磁盘方式为deferred write

  2. 同步访问

    相对于标准访问方式,同步访问需要在写入磁盘后才返回,而标准访问可以在写入page cache后就返回

  3. 内存映射
    Linux通过将一块内存区域和一个外部设备或者文件关联起来,操作系统将对内存的操作映射为对外部设备或文件的操作,Linux提供了mmap

  4. 直接访问(直接IO)

    直接在用户空间访问文件,不需要内核空间的page cache支持,对于一些通过应用程序自身进行缓存控制的程序(例如数据库)比较适用

  5. 异步访问

    用户程序在写入或者读取磁盘数据时无需阻塞,由操作系统异步,提升了用户程序的效率

缓存IO和直接IO对比

缓存IO需要由DMA将文件数据读入到page cache,然后通过page cache读取数据,如果一个文件需要频繁访问,能够减少文件直接访问次数,但是在大量文件访问的程序中,存在很多用户程序,内核page cache和磁盘文件的数据copy,就会带来很高的cpu负载。

如何进行直接IO调用

linux中文件操作主要通过read(),write(),open()这三个操作完成,在函数操作中通过O_DIRECT标识符完成

直接IO的使用场景

由于直接IO调用减少了数据的copy次数,对于一些需要大量数据复制(到内存或者网络)或者对数据缓存能够通过应用控制(例如数据库)的场景,会有很大的性能提升

Zero Copy

Linux  中传统的 I/O 操作是一种缓冲 I/O,I/O 过程中产生的数据传输通常需要在缓冲区中进行多次的拷贝操作,例如一次网络请求中,如果需要读取磁盘文件,需要经过四次copy过程和多次的内核和用户态的上下文切换,下面是一次传统web请求中,从磁盘读取数据到返回数据结果的过程中数据在内存中的copy过程

也可以通过Efficient data transfer through zero copy这篇文章详细了解

zero copy技术可以避免数据的多次copy,减少不必要的上下文切换(内核态和用户态),降低CPU的资源浪费,尤其在处理大量网络请求的应用中,zero copy显得更加重要。

linux中zero copy实现方式主要实现方式

  1. 直接IO技术(减少page cache)

  2. 从page cache 直接copy到目标缓冲区

  3. mmap():应用程序调用了 mmap() 之后,数据会先通过 DMA 拷贝到操作系统内核的缓冲区中去。接着,应用程序跟操作系统共享这个缓冲区,适用于大数据范围的copy,但是当多个进行修改时,会收到一个SIGBUS中断信号,而且必须有效的解决才能避免一些问题。

  4. sendfile():sendfile() 系统调用利用 DMA 引擎将文件中的数据拷贝到操作系统内核缓冲区中,然后数据被拷贝到与 socket 相关的内核缓冲区中去

  5. splice():Linux2.6.17引入,和sendFile机制类似,但是可以在fin和fout之间互相copy数据,而不是单向。

  6. copy-on-write:多个进程共享一块buffer,当一个进程需要修改buffer数据时,会将buffer数据copy到进行内存单独修改,COW技术通过读共享内存减少了内存的copy

  7. 通过硬件的支持,直接从kernel buffer到网卡等终端,减少了一次kernel buffer 到socket buffer的copy

Java中的Zero Copy

  1. 通过java.nio.channels.FileChanneltransferTo()可以实现zero copy,在linux类系统中,transferTo调用了操作系统splice()方法,下面一个图是传统 Java copy API和 FileChannel zero copy的耗时比较,大约有65%的性能提升
  2. 通过java.nio.channels.FileChannel(MapMode mode,long position, long size)实现mmap,相对于传统的BIO等文件操作尤其是大文件的读写,性能有较大的提升。

    Java 原生API目前没有Direct IO的实现,目前gitlab上有JNA的库,可以基于此库实现Linux DirectIO

Zero Copy的实际应用场景

  1. Nginx,在Nginx性能优化中,有一个配置是sendfile on/off,以下是官方文档的说明

By default, NGINX handles file transmission itself and copies the file into the buffer before sending it. Enabling the sendfile directive eliminates the step of copying the data into the buffer and enables direct copying data from one file descriptor to another

当nginx在处理静态资源请求时,开启sendfile对性能有一定的提升,当然还需要根据实际场景,结合其他配置使用。可以通过Optimisations Nginx, bien comprendre sendfile, tcpnodelay et tcpnopush详细了解

  1. C10k问题的解决中,Zero Copy对于IO的提升是一个关键点,具体可以通过The C10K problem查看

  2. Kafka,Netty,RocketMQ等中间件

    消息中间件中对于消息持久化通常保存在文件中,对于一个高吞吐量的消息中间件,对文件读写通常采用以下方案(Java为例)

    • NIO:主要在java.nio包中,通过FileChannel相关方法实现
     FileChannel fileChannel = new RandomAccessFile(new File("message.data"), "rw").getChannel();
     //然后通过read和write方法写入数据
     fileChannel.read(ByteBuffer[] dsts, int offset, int length)
     fileChannel.write(ByteBuffer[] srcs, int offset, int length)
  • MMAP:RocketMQ默认采用异步mmap的方式实现消息刷盘
     MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, filechannel.size();

参考资料

Zero Copy I: User-Mode Perspective