< 返回版块

ipconfiger 发表于 2024-02-04 00:08

Tags:mmap,rust,性能

Rust使用Mmap榨干机器性能极限(1)

事情开始于下面的数据,学过计算机体系结构课程的大致都能理解:

CPU 1级缓存 .................... 0.5ns 分支预测 ....................... 5ns CPU 2级缓存 .................... 7ns 互斥锁 加锁/释放 ................ 25ns 主内存 ......................... 100ns 压缩1K数据 ..................... 3us 在1G带宽中发送2K数据 ............. 20us SSD硬盘随机读取 ................. 150us 主内存中顺序1M数据 ............... 250us 同一个数据中心来回 ............... 0.5ms SSD中顺序读取1M数据 ............. 1ms 硬盘寻址 ....................... 10ms 硬盘顺序读取1M数据 ............... 20ms 从加拿大发送数据到荷兰再返回 ........ 150ms

看表得知,当我们往磁盘写日志的时候吞吐量受到磁盘本身速度的限制,铁定是没有写内存来得快的。但是内存呢又比硬盘贵,所以如果为了效率把数据全网内存里怼,那多少是有点壕。Mmap作为一种将磁盘上文件映射到内存中的技术,可以给我们更高效读写数据的能力。

往项目里添加依赖

crate.io 里有两个库,一个是mmap,一个是mmap2,因为mmap这个库不怎么活跃了,所以这里我们用mmap2.

在Cargo.toml中添加

[dependencies]
memmap2 = "0.9.4"

然后通过

use std::fs::OpenOptions;
use memmap2::MmapMut;
use std::path::PathBuf;

let path = PathBuf::from("./map_file");
let file = OpenOptions::new().read(true).write(true).create(true).open(path)?
let mmap = unsafe { MmapMut::map_mut(&file)? };

这样mmap就是一个映射到磁盘上文件./map_file 的大byte数组,read是必须打开的,不然运行会报权限错误。

mmap在使用的时候,必须固定文件的大小,也就是说需要先固定内存的区域,然后再写数据

let path = PathBuf::from("./map_file");
let file = OpenOptions::new().read(true).write(true).append(true).create(true).open(path)?
let mmap = unsafe { MmapMut::map_mut(&file)? };
mmap.set_len(1024); //这里设置了1K的大小
(&mut mmp[0..4).write_all(&[1,2,3,4])?;
mmp.flush_async_range(0, 4)?;

这里我们只需要注意计算好索引位置,就能往指定的区块写入数据了。要注意的是,如果不执行flush,数据是不会回写到磁盘上的,这里有四种回写模式,前两种是带async和不带的,不带的是同步回写,也就是函数执行完后回确保数据已经回写到磁盘上了。带async是异步回写,也就是执行后会立即返回,只是确保把数据丢进了操作系统的IO队列,至于什么时候写成功,是操作系统控制的。另外两种是带range和不带的,不带range的回写是每一次执行都把内存文件全部覆写到磁盘文件里,如果文件很大就会占用很多的IO,带Range的是指定区域覆写,每次回写的IO有限,可以节约大量资源。建议用带Range的。

最后我们把读和写分别放到两个线程里。

thread::spwan(||{
    let path = PathBuf::from("./map_file");
    let file = OpenOptions::new().read(true).write(true).append(true).create(true).open(path)?
    let mmap = unsafe { MmapMut::map_mut(&file)? };
    mmap.set_len(40); //这里设置了大小
    for i in 0..10 {
        (&mut mmp[i*4..i*4+4).write_all(&[1,2,3,4])?;
        thread::sleep(Duration::from_sec(1));
    }
    mmp.flush_async_range(i*4, 4)?;
    
}); //  写线程
thread::spwan(||{
    let path = PathBuf::from("./map_file");
    let file = OpenOptions::new().read(true).write(true).append(true).create(true).open(path)?
    let mmap = unsafe { MmapMut::map_mut(&file)? };
    mmap.set_len(40); //这里设置了大小
    loop {
        println!("{:?}", mmap.as_ref())
        thread::sleep(Duration::from_sec(1));
    }
});

执行后的效果,首先每秒会打印出内存映射文件的内容,这里两个文件分别在两个不同线程里,但是文件是同一个,所以映射出来的mmap在两个线程里其实都是指向同一个内存块,所以看起来没联系,但是当写线程的内存块改变后,读线程里的内容获取出来也会变更。

因为这里写入的其实是内存,所以速度会非常的快,吞吐量会非常的大,如果你的系统需要走大量的IO,又想能持久化,又想快要走内存读写的话,好好的参悟mmap的使用,会非常的有帮助。

这里介绍了mmap的基本用法,下一篇,我们会借鉴Kafka的存储机制,来看看如何使用mmap来实现一个快速的日志持久化系统,

评论区

写评论
zylthinking 2024-02-04 10:04

靠一招鲜想搞啥性能极限。。。何况 mmap 也不是啥新鲜玩意

内存映射和 read 在内核上没啥不一样的, 都是从磁盘读入 page cache, 话说 read 因为可以预读在一些场合比mmap性能更高。

mmap 胜在从 page cache 到应用层不再有内存拷贝而已。

这个话题和 rust 关系其实不大, 不但 rust 可以使用 mmap, 简直 js 也能用

1 共 1 条评论, 1 页