零拷贝技术学习笔记 | 青训营

31 阅读4分钟

在现代计算机系统中,零拷贝技术是一种优化方法,旨在最大限度地减少数据在内存和设备之间的复制操作,从而提高数据传输的效率。在 Rust 编程语言中,我们可以利用一些库和系统调用来实现零拷贝技术,包括 mmap + write、sendfile、sendfile + DMA 收集以及 splice 等。在本文中,我将为你总结这些技术,并提供相应的Rust示例代码。

在消息中间件 kafka 和 RocketMQ 中,也深度应用零拷贝技术进行优化,以达到较高的吞吐量,我在本文中也进行了总结。

mmap + write

mmap(Memory Map)是一种在Linux系统中常用的内存映射技术,它允许你将一个文件或者其它设备的内容映射到进程的地址空间,使得这些内容在内存中可以被直接访问,而无需进行传统的读取或写入操作,通过这种方式,文件数据只会从磁盘 DMA 拷贝到内核空间,而无需从内核空间拷贝到用户空间。

sendfile

sendfile方法是一种在两个文件描述符之间直接传输数据的系统调用,它避免了数据从内核空间到用户空间的复制,从而实现了零拷贝。

下面是一个使用nix中的sendfile方法进行文件传输的示例:

use nix::fcntl::{open, OFlag};
use nix::sys::sendfile::sendfile;
use nix::sys::stat::{stat, Mode};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let fstat = stat("src.txt")?;

    let source_fd = open("src.txt", OFlag::O_RDONLY, Mode::empty())?;
    let dest_fd = open("dst.txt", OFlag::O_WRONLY | OFlag::O_CREAT, Mode::empty())?;

    let mut remain = fstat.st_size;
    while remain > 0 {
        sendfile(dest_fd, source_fd, None, 4096)?;
        remain -= 4096;
    }

    Ok(())
}

sendfile + DMA 收集

DMA(Direct Memory Access)是一种数据传输技术,它允许外设直接访问内存,而无需 CPU 的干预。在 Linux 系统中,可以通过 sendfile 系统调用结合 DMA 收集功能,实现零拷贝的文件传输。

splice

splice函数在 Linux 系统中用于在两个文件描述符之间进行数据传输,从而实现高效的数据移动,而无需在用户空间和内核空间之间进行数据复制。以下是一个使用nix库进行splice操作的示例:

首先使用stat()方法获取了源文件(src.txt)的详细信息,以获取文件大小。然后,调用pipe()方法创建了一个匿名管道,获得了读取端和写入端的文件描述符(read_pipewrite_pipe)。分别打开了源文件和目标文件,循环通过管道零拷贝文件,把传输长度设置为页大小(4096字节),有助于优化传输性能。设置SPLICE_F_MOVE标志位,可以提示内核尽量移动数据而不是拷贝数据。

use nix::fcntl::{open, OFlag, splice, SpliceFFlags};
use nix::unistd::pipe;
use nix::sys::stat::{stat, Mode};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let fstat = stat("src.txt")?;

    let (read_pipe, write_pipe) = pipe()?;

    let source_fd = open("src.txt", OFlag::O_RDONLY, Mode::empty())?;
    let dest_fd = open("dst.txt", OFlag::O_WRONLY | OFlag::O_CREAT, Mode::empty())?;

    let mut remain = fstat.st_size;
    while remain > 0 {
        splice(source_fd, None, write_pipe, None, 4096, SpliceFFlags::SPLICE_F_MOVE)?;
        splice(read_pipe, None, dest_fd, None, 4096, SpliceFFlags::SPLICE_F_MOVE)?;
        remain -= 4096;
    }

    Ok(())
}

kafka 性能优化技术总结

顺序读写

producer 每次会追加写入到 partition(对 segment 文件进行追加写),consumer 每次消费的时候,根据 offset 进行顺序读取,并且通过批量刷盘的方式来减少磁盘 I/O 的次数。

页缓存技术

kafka 利用 linux 的 page cache 技术,将页面缓存到内存中,并且通过异步落盘,减少磁盘 I/O 次数。如果发生服务器掉电,内存中的数据可能会丢失,通过 Replication 机制去解决数据丢失的问题。

零拷贝之 mmap

kafka 会给一些数据建立稀疏索引,稀疏索引的作用是帮助查找到数据的一个大致的范围,稀疏索引保存在文件内,因此 kafka 使用mmap的方式来读取稀疏索引文件,减少在内核空间和用户空间中的拷贝次数。

零拷贝之 sendfile

consumer 的作用只是从磁盘上读取文件并通过网卡转发出去,不需要对这段数据进行修改,这非常适合使用sendfile来进行 I/O。再加上 kafka 可能会在 page cache 中缓存一些未落盘的数据,或者预读一些可能用的数据到 page cache 中,所以效率更加高,这也是 kafka 支持百万级吞吐量的一个原因。

RocketMQ 性能优化技术总结

RocketMQ 的功能更完善、更全面,相比于 kafka,它支持定时消息、消息过滤等。也由于需要支持这些功能,因此要使文件内容对用户进程可见,不能使用sendfile了。因此 RocketMQ 的吞吐量要低于 kafka。

mmap

RocketMQ 把收到的消息通过顺序写入记录到 commit log 中,并建立对应的 consumer queue(消息索引),consumer 通过 consumer queue 来获取消息。commit log 和 consumer queue 都是通过 mmap 的方式来读取的,所以 RocketMQ 采用定长结构来存储文件,方便一次性把文件映射到内存中。通过文件预热的方式来解决可能存在的缺页问题。