fio 是一个常见的 IO 性能测试工具。它功能强大,上至文件系统,下至裸盘都能支持,但同时也参数繁多,目前的最新版本(3.27)中已经定义了至少237种一级参数,再加上这些参数的不同取值,一套组合拳下来能把人打的找不着北。所幸我们并不需要了解全部的参数,在使用的时候,只要到脚本库里面找到一个现成的命令,Ctrl+C / Ctrl+V一下即可(至少我是这样)。
比如我常用的一个命令模板:
|
|
但在反复的性能测试中,我观察到 fio 不合常理的慢,它经常卡在一个叫Laying out IO file(s)
的阶段,有时甚至比它跑 benchmark 的时间还要长。对于这样的浪费我自然是不能忍受的,所以不禁好奇,fio 到底在干什么?
Laying out IO file(s)
当挂载好了一个全新的目录,开始跑 fio 的时候,我们会看到 fio 输出一段Laying out IO file(s)
,然后就一动不动的 hang 住几分钟。看起来它是在准备些什么文件,具体都干了些啥,可以通过strace
观察到:
|
|
注意,这里一定要加-f
,因为 fio 会创建子线程,并通过子线程完成 benchmark,如果不加-f
的话,我们只能观察到主线程的行为,真正发生的 IO 却看不到。
打开strace
日志,可以看到在真正的 benchmark 之前,有这么几个关键的 IO 调用
- 调用
fallocate
,失败了,因为底层文件系统目前还不支持这个语义 - 然后就顺序写各个 block,每次写一个字节,直到写满特定的 size
- 挨个写完以后,又来了一次
ftruncate
调用 - 接着又从头写了一遍文件,不过不同的是,这次每个 block 都填满了,写的是随机内容(随机程度跟 fio 那几个 buffer 参数有关)
原来所谓的Laying out IO files
就是在随机创建文件啊,就这.gif?
但为啥会写两遍呢?明明有了第4步就够了,为啥要多做第2步,让我们多等一倍的时间?
这个迷惑的行为困扰了我很久,后来才无意中发现,这很有可能是 glibc 的行为,即在发现底层文件系统不支持fallocate
之后,它模拟了一遍fallocate
的语义,通过用\0
来填充的方式,确保底层文件系统能预留指定的空间大小?
|
|
所以为了提升Laying out IO file(s)
的速度,我们大概有两种思路:
- 要么就不要删掉测试文件,让 fio 可以复用。
- 要么就通过参数
--fallocate=none
,直接告诉它不要预调用fallocate
,免得又触发 glibc 的一次顺序写,从而节省一半的时间。
IO Pattern
fio 的--rw
参数支持多种 IO 模式,比如常见的顺序读写、随机读写。但在真正 benchmark 的过程中,发出的 IO 请求是怎样的,随机程度怎样,是否覆盖了所有的文件地址空间?
这个问题同样可以用strace
来解答,我们截取了后半部分 IO 调用。
|
|
由于我们的 ioengine 是libaio
,所以 IO 请求通过io_submit
来完成。可以看到,fio 一次发出了一批io_submit
调用,然后调用io_getevents
一次只等待一个结果,拿到结果后又立即发出一个io_submit
请求,这样确保 in-flight 的请求正好是16个,正符合--iodepth=16
设定的参数👏🏻。
但这个 log 观察 IO Pattern 就有点麻烦了,因为strace
的输出里还夹杂着其他的系统调用。这时,我又找到了另一个 fio 的参数——--write_iolog
,它可以将 IO 相关的参数直接 dump 出来,让用户有个量化的感知:
顺序读——read
|
|
看起来很符合预期,顺序读就是按顺序每个 block 读取一遍。
顺序写——write
|
|
行为同上。
随机读——randread
|
|
随机读看起来是每次随机选取一个 offset,来读取一个 block 。而且统计后能发现,fio 把每个地址空间都不多不少的访问了一次。
随机写——randwrite
|
|
行为同上。
随机读写——randrw
|
|
看起来是读写操作穿插着进行,而且每一个 offset 仅且仅读写一次。