从零拷贝视角看性能优化 | 青训营

90 阅读2分钟

1. 传统文件传输过程

1.1 Linux文件读写流程

用户视角:

image-20230824214515725.png

具体逻辑:

  • 发生了 4 次用户态与内核态的上下文切换
  • 发生了 4 次数据拷贝

image-20230824214633493.png

1.2 上下文切换

用户态和内核态:用户空间和内核空间权限不同,内核态权限为0,最大。

为什么区分用户空间和内核空间?隔离操作系统程序和应用程序,在内核空间运行的程序出现bug会造成整个机子崩溃。

内核空间为什么不能直接使用用户空间的数据?内核空间不能信任用户空间的指针。

image-20230824214746531.png

1.3 传统I/O操作的瓶颈

1.3.1 I/O轮询

优点:实现简单

缺点:占用CPU全部资源,效率低。

image-20230824215134056.png

1.3.2 I/O中断

优点:对比 I/O 轮询的方式一定程度上释放了CPU 资源

缺点:在大数据量传输的情况下CPU 会反复中断

image-20230824215249834.png

1.3.3 DMA传输

优点:彻底减少了一次CPU拷贝

缺点:依赖设备硬件支持

image-20230824215425327.png

2. 零拷贝的实现方式

零拷贝并不是0次拷贝数据,而是:

  • 减少用户空间和内核空间之间的 CPU 拷贝次数
  • 减少上下文切换次数

image-20230824220045528.png

2.1 mmap+write

memory map,做虚拟地址映射,在内核空间内做数据拷贝。

4次上下文交换,1次CPU拷贝

image-20230824215719909.png

2.2 sendfile

2次上下文交换,1次CPU拷贝

image-20230824215905144.png

2.3 sendfile + gather

2次上下文交换,0次CPU拷贝

image-20230824215954730.png

2.4 splice

2次上下文交换,0次CPU拷贝

image-20230824220016839.png

3. Go 语言中的实现

可以splice,选择splice;可以sendfile,选择sendfile;都不支持,选择原始的方法mmap+write。

image-20230824220209562.png

原始的mmap+write:

image-20230824220509708.png

4. 零拷贝的应用

4.1 Kafka

image-20230824220600467-16928859615431.png

4.1.1 顺序读写

producer 每次会追加写入到 partition

consumer 每次消费的时候,根据 offset进行顺序读取

批量刷盘

==只要是顺序读写,性能都会好很多,顺序读写的磁盘和顺序读写的SSD传输性能差不多==

4.1.2 页缓存技术

利用 linux 的page cache 技术

异步落盘,减少磁盘I/O次数

通过 Replication 机制去解决数据丢失的问题

4.1.3 零拷贝之mmap

image-20230824220921274.png

4.1.4 零拷贝之sendfile

consumer 不需要对数据进行修改,可以采用零拷贝方式

对比传统read() + write():节省了2次cpu拷贝、2次上下文切换

image-20230824220945789.png

4.2 RocketMQ

通过mmap + write() 实现,满足小数据需求

通过文件预热的方式来解决缺页的问题

image-20230824221215890.png

mmap + write()可以对数据进行修改,适合RocketMQ这个偏业务的消息队列

Kafka的数据传输过程中不需要发生修改,使用sendfile,减少上下文切换的次数,实现高并发。

如果Kafka的生产者和消费者两者速度差不多,所有消息可以全部存储在page cache中,这也是高并发的一个原因。