Linux 环境写文件如何稳定跑满磁盘 I/O 带宽?
准备
要求:
在限制内存的情况下,假定我们每次写入 4k 的数据,如何保证kill -9
不丢数据的情况下,仍然稳定的跑满磁盘的 IO ?因为需要保证kill -9
不丢数据,所以fwrite()
就不在我们的考虑范围之内了. 又因为限制内存,所以直观的想法是直接 Direct IO, 但 Direct IO 能否跑满磁盘 IO 呢?
机器配置:
CPU: 64核 Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHz
磁盘: Intel Optane SSD
测试磁盘IO性能:
官方称读/写带宽是 2400/2000 MB/s
, 我们利用fio
来进行实测:
顺序读性能:
1 | sudo fio --filename=test -iodepth=64 -ioengine=libaio --direct=1 --rw=read --bs=2m --size=2g --numjobs=4 --runtime=10 --group_reporting --name=test-read |
结果:
1 | READ: bw=2566MiB/s (2691MB/s), 2566MiB/s-2566MiB/s (2691MB/s-2691MB/s), io=8192MiB (8590MB), run=3192-3192msec |
顺序写性能:
1 | sudo fio --filename=test -iodepth=64 -ioengine=libaio -direct=1 -rw=write -bs=1m -size=2g -numjobs=4 -runtime=20 -group_reporting -name=test-write |
结果:
1 | WRITE: bw=2181MiB/s (2287MB/s), 2181MiB/s-2181MiB/s (2287MB/s-2287MB/s), io=8192MiB (8590MB), run=3756-3756msec |
实测读写带宽: 2566/2181 MB/s
实验一: Buffer IO 写入
因为是限制内存,所以 Buffer IO 不在我们的考虑范围内,但是我们先来测试一下 Buffer IO 的具体性能到底如何? 我们使用最简单的方法,因为我们的 CPU 核数是64,所以直接 64 线程单次4K
字节 Buffer IO 写入, 即通过操作系统的 Page Cache 的策略来缓存,刷盘:
代码片段: 完整代码
1 | static char data[4096] attribute((aligned(4096))) = {'a'}; |
我们通过O_APPEND
单次 4k 追加写入,之后通过vmstat
来保留120s
的写入带宽:
1 | vmstat 1 120 > buffer_io |
经过最后的测试数据整理,我们发现 Buffer IO 的性能基本能稳定跑满带宽, 其中只有一次 I/O 抖动:
实验二: 4K 单次 Direct IO 写入
Buffer IO 利用 Page Cache 帮助我们缓存了大量的数据,必然提高了写入带宽,但假如在限制内存的情况下,Buffer IO 就不是正确的解决方案了,这次我们绕过 Page Cache, 直接 Direct IO 单次4K
写入:
代码片段: 完整代码
唯一需要修改的地方就是在open()
中加入O_DIRECT
标志:
1 | int data_fd = ::open(fname.c_str(), O_RDWR | O_CREAT | O_APPEND | O_DIRECT, 0645); |
通过vmstat
获取写入带宽数据, 整理如下:
通过数据我们发现,单次4k的 Direct IO 写入无法跑满磁盘的I/O带宽,仅仅只有800MB/S
实验三: mmap 写入
通过前面这两个实验我们发现,Buffer IO 是可以跑满磁盘 I/O 的,那我们可以尝试模拟 Buffer IO 的写入方式,使用较少的内存来达到 Buffer IO 的写入效果.
我们使用mmap
来实现 Buffer IO 写入,通过限定的 Buffer Block 来模拟 Page Cache 的聚合效果, 实验中我们使用memcpy
来完成数据拷贝,Buffer Block 我们设定为4K * 4
, 与 Direct IO 的不同,我们这次限定即16KB
的单次写入:
代码片段: 完整代码
main()
函数不变,修改线程的writer()
函数:
1 | static char data[4096] attribute((aligned(4096))) = {'a'}; |
我们通过vmstat
来获取写入带宽数据,我们发现mmap
的16K
写入可以跑满磁盘带宽,但 I/O 抖动较大,无法形成类似于 Buffer IO 稳定的写入.
我们通过perf
生成火焰图分析:
通过pref
生成分析瓶颈时发现,写入writer()
时触发了大量的Page Fault
, 即缺页中断,而mmap()
本身的调用也有一定的消耗(mmap()
的源码分分析,我们实验三的思路是: 首先fallocate
一个大文件,然后mmap()
内存映射16k
的 ‘Block’, memcpy()
写满之后,游标右移重新mmap()
,以此循环.
实验四: 改进的 mmap 写入
为了避免mmap()
的开销,我们使用临时文件在写入之前mmap()
映射,之后循环利用这16K
的Block, 避免mmap()
系统调用的开销:
代码片段: 完整代码
1 | void MapRegion(int fd, uint64_t file_offset, char** base) { |
使用vmstat
来获取写入速度的数据, 整理如下:
这次避免了mmap()
的开销,写入速度可以稳定保持在2180 MB/S
左右,且没有 I/O 抖动.
内存使用也仅仅只有18000KB
, 大约18M
:
结论:
下面是四种方式的写入速度对比:
在限制内存,且需要kill -9
不丢数据的情况下,我们可以使用mmap()
来模拟 Buffer IO,但为了避免频繁mmap()
的造成的 Page Fault 开销,我们需要一个临时文件来做我们的内存映射. 这种方法可以保证我们的写入速度稳定且kill -9
不至于丢失数据.