1. 背景
让我们梳理一下本系列文章整体脉络。
- 首先,Linux Block Driver - 1 介绍了一个只有 200 行源码的 Sampleblk 块驱动的实现。
- 然后,在 Linux Block Driver - 2 中,我们在 Sampleblk 驱动创建了 Ext4 文件系统,并做了一个
fio
顺序写测试。测试中我们利用 Linux 的各种跟踪工具,对这个fio
测试做了一个性能个性化分析。 - 而在 Linux Block Driver - 3 中,我们利用 Linux 跟踪工具和 Flamegraph 来对文件系统层面上的文件 IO 内部实现,有了一个概括性的了解。
本文将继续之前的实验,围绕这个简单的 fio
测试,探究 Linux 块设备驱动的运作机制。除非特别指明,本文中所有 Linux 内核源码引用都基于 4.6.0。其它内核版本可能会有较大差异。
2. 准备
阅读本文前,可能需要如下准备工作,
- 参考 Linux Block Driver - 1 中的内容,加载该驱动,格式化设备,装载 Ext4 文件系统。
- 按照 Linux Block Driver - 2 中的步骤,运行
fio
测试。
本文将在与前文完全相同 fio
测试负载下,在块设备层面对该测试做进一步的分析。
3. Block IO Pattern 分析
3.1 写请求大小
Linux 4.6 内核的块设备层的预定义了 19 个通用块层的 tracepoints。这些 tracepoints,可以通过如下 perf 命令来列出来,
$ sudo perf list block:*
List of pre-defined events (to be used in -e):
block:block_bio_backmerge [Tracepoint event]
block:block_bio_bounce [Tracepoint event]
block:block_bio_complete [Tracepoint event]
block:block_bio_frontmerge [Tracepoint event]
block:block_bio_queue [Tracepoint event]
block:block_bio_remap [Tracepoint event]
block:block_dirty_buffer [Tracepoint event]
block:block_getrq [Tracepoint event]
block:block_plug [Tracepoint event]
block:block_rq_abort [Tracepoint event]
block:block_rq_complete [Tracepoint event]
block:block_rq_insert [Tracepoint event]
block:block_rq_issue [Tracepoint event]
block:block_rq_remap [Tracepoint event]
block:block_rq_requeue [Tracepoint event]
block:block_sleeprq [Tracepoint event]
block:block_split [Tracepoint event]
block:block_touch_buffer [Tracepoint event]
block:block_unplug [Tracepoint event]
我们可以利用 block:block_rq_insert
来跟踪获取 fio
测试时,该进程写往块设备 /dev/sampleblk1 IO 请求的起始扇区地址和扇区数量,
$ sudo perf record -a -g --call-graph dwarf -e block:block_rq_insert sleep 10
因为我们指定了记录调用栈的信息,所以,perf script
可以获取 fio
从用户态到内核 block:block_rq_insert
tracepoint 的完整调用栈的信息。并且,给出了主次设备号,相关操作,及起始扇区和扇区数,
$ sudo perf script | head -n 20
fio 73790 [000] 1011438.379090: block:block_rq_insert: 253,1 W 0 () 3510 + 255 [fio]
5111e1 __elv_add_request (/lib/modules/4.6.0-rc3+/build/vmlinux)
518e64 blk_flush_plug_list (/lib/modules/4.6.0-rc3+/build/vmlinux)
51910b blk_queue_bio (/lib/modules/4.6.0-rc3+/build/vmlinux)
517453 generic_make_request (/lib/modules/4.6.0-rc3+/build/vmlinux)
517597 submit_bio (/lib/modules/4.6.0-rc3+/build/vmlinux)
107de ext4_io_submit ([ext4])
c6bc ext4_writepages ([ext4])
39cd3e do_writepages (/lib/modules/4.6.0-rc3+/build/vmlinux)
390b66 __filemap_fdatawrite_range (/lib/modules/4.6.0-rc3+/build/vmlinux)
3d5d96 sys_fadvise64 (/lib/modules/4.6.0-rc3+/build/vmlinux)
203c12 do_syscall_64 (/lib/modules/4.6.0-rc3+/build/vmlinux)
8bb721 return_from_SYSCALL_64 (/lib/modules/4.6.0-rc3+/build/vmlinux)
7fd1e61d7d4d posix_fadvise64 (/usr/lib64/libc-2.17.so)
4303b3 file_invalidate_cache (/usr/local/bin/fio)
41a79b td_io_open_file (/usr/local/bin/fio)
43f40d get_io_u (/usr/local/bin/fio)
45ad89 thread_main (/usr/local/bin/fio)
45cffc run_threads (/usr/local/bin/fio)
[...snipped...]
使用简单的处理,我们即可发现这个测试在通用块层的 IO Pattern,
$ sudo perf script | grep lock_rq_insert | head -n 20
fio 71005 [000] 977641.575503: block:block_rq_insert: 253,1 W 0 () 3510 + 255 [fio]
fio 71005 [000] 977641.575566: block:block_rq_insert: 253,1 W 0 () 3765 + 255 [fio]
fio 71005 [000] 977641.575568: block:block_rq_insert: 253,1 W 0 () 4020 + 255 [fio]
fio 71005 [000] 977641.575568: block:block_rq_insert: 253,1 W 0 () 4275 + 255 [fio]
fio 71005 [000] 977641.575569: block:block_rq_insert: 253,1 W 0 () 4530 + 255 [fio]
fio 71005 [000] 977641.575570: block:block_rq_insert: 253,1 W 0 () 4785 + 255 [fio]
fio 71005 [000] 977641.575570: block:block_rq_insert: 253,1 W 0 () 5040 + 255 [fio]
fio 71005 [000] 977641.575571: block:block_rq_insert: 253,1 W 0 () 5295 + 255 [fio]
fio 71005 [000] 977641.575572: block:block_rq_insert: 253,1 W 0 () 5550 + 8 [fio]
fio 71005 [000] 977641.575572: block:block_rq_insert: 253,1 W 0 () 5558 + 255 [fio]
fio 71005 [000] 977641.575573: block:block_rq_insert: 253,1 W 0 () 5813 + 255 [fio]
fio 71005 [000] 977641.575574: block:block_rq_insert: 253,1 W 0 () 6068 + 255 [fio]
fio 71005 [000] 977641.575574: block:block_rq_insert: 253,1 W 0 () 6323 + 255 [fio]
fio 71005 [000] 977641.575575: block:block_rq_insert: 253,1 W 0 () 6578 + 255 [fio]
fio 71005 [000] 977641.575576: block:block_rq_insert: 253,1 W 0 () 6833 + 255 [fio]
fio 71005 [000] 977641.575577: block:block_rq_insert: 253,1 W 0 () 7088 + 255 [fio]
fio 71005 [000] 977641.575779: block:block_rq_insert: 253,1 W 0 () 7343 + 255 [fio]
fio 71005 [000] 977641.575781: block:block_rq_insert: 253,1 W 0 () 7598 + 8 [fio]
fio 71005 [000] 977641.577234: block:block_rq_insert: 253,1 W 0 () 3510 + 255 [fio]
fio 71005 [000] 977641.577236: block:block_rq_insert: 253,1 W 0 () 3765 + 255 [fio]
bitesize-nd.stp 是 Systemtap 写的统计块 IO 的字节数大小分布的工具。
基于该工具,简单修改后,即可按照 Block IO 请求扇区数来统计,请参考 bio_sectors.stp 的源码。
$ sudo ./bio_sectors.stp
Tracing block I/O... Hit Ctrl-C to end.
^C
I/O size (sectors):
[...snipped...]
process name: fio
value |-------------------------------------------------- count
0 | 0
1 | 17
2 | 26
4 | 63
8 |@@@ 2807
16 | 398
32 | 661
64 |@@ 1625
128 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 38490
256 | 0
512 | 0
可以看到,128 ~ 255 扇区的分布占了绝大多数,本例中,实际上这个区间的 IO 请求都是 255 个扇区,与之前 perf 查看的结果一致。
3.2 写延迟分析
iosnoop 不但可以了解块设备上的 IO 请求大小,更有从 IO 请求发起到完成的延迟时间的信息。下面我们在运行 fio
测试时,使用 iosnoop
来获得的相关信息。
首先,我们需要得到 fio
测试所用的块设备的主次设备号,
$ mount | grep sample
/dev/sampleblk1 on /mnt type ext4 (rw,relatime,seclabel,data=ordered)
[yango@localhost ~]$ ls -l /dev/sampleblk1
brw-rw----. 1 root disk 253, 1 Aug 12 22:10 /dev/sampleblk1
然后,运行 iosnoop
来获取所有在 /dev/sampleblk1
上的 IO 请求,
$ sudo iosnoop -d 253,1 -s -t
Tracing block I/O. Ctrl-C to end
STARTs ENDs COMM PID TYPE DEV BLOCK BYTES LATms
11165.028153 11165.028194 fio 11425 W 253,1 4534 130560 0.04
11165.028196 11165.028210 fio 11425 W 253,1 4789 130560 0.01
11165.028211 11165.028224 fio 11425 W 253,1 5044 130560 0.01
11165.028227 11165.028241 fio 11425 W 253,1 5299 130560 0.01
11165.028244 11165.028258 fio 11425 W 253,1 5554 130560 0.01
11165.028261 11165.028274 fio 11425 W 253,1 5809 130560 0.01
11165.028276 11165.028290 fio 11425 W 253,1 6064 130560 0.01
11165.028295 11165.028309 fio 11425 W 253,1 6319 130560 0.01
11165.028311 11165.028312 fio 11425 W 253,1 6574 4096 0.00
11165.029896 11165.029937 fio 11425 W 253,1 2486 130560 0.04
11165.029939 11165.029951 fio 11425 W 253,1 2741 130560 0.01
11165.029952 11165.029965 fio 11425 W 253,1 2996 130560 0.01
11165.029968 11165.029981 fio 11425 W 253,1 3251 130560 0.01
11165.029982 11165.029995 fio 11425 W 253,1 3506 130560 0.01
11165.029998 11165.030012 fio 11425 W 253,1 3761 130560 0.01
11165.030012 11165.030026 fio 11425 W 253,1 4016 130560 0.01
11165.030029 11165.030042 fio 11425 W 253,1 4271 130560 0.01
11165.030044 11165.030045 fio 11425 W 253,1 4526 4096 0.00
11165.030095 11165.030135 fio 11425 W 253,1 4534 130560 0.04
可以看到,该输出不但包含了 IO 请求的大小,而且还有 IO 延迟时间。如,130560 字节正好就是 255 扇区,4096 字节,恰好就是 8 个扇区。因此,IO 大小和之前其它工具得到时类似的。
而在发出 255 扇区的 IO 请求延迟是有变化的,大致是 0.01 毫秒或者 0.04 毫秒,大概是百纳秒级别的延迟。
iosnoop
在短时间内会产生大量的输出,每个 IO 请求的 IO 延迟时间都可能有很大差异,如何能对 fio
测试的延迟有没有更好的数据呈现方式呢?
Heatmap 就是一个这样的工具,其具体使用方法如下,
$ sudo ./iosnoop -d 253,1 -s -t > iosnoop.log
$ grep '^[0-9]' iosnoop.log | awk '{ print $1, $9 }' | sed 's/\.//g' | sed 's/$/0/g' > trace.txt
$ ./trace2heatmap.pl --unitstime=us --unitslatency=us --maxlat=200 --grid trace.txt> heatmap.svg
于是,基于 iosnoop
工具得到的数据,我们生成了下面的热点图 (Heatmap),
右击该图片,在新窗口打开,在图片范围内移动鼠标,即可看到不同的延迟时间所占 IO 请求数据采样的百分比。
例如,颜色最红的那一行代表采样最多的 IO 延迟,在横轴时间是 40 秒时,延迟范围大概是 8 ~ 12 微妙,具有这样延迟的 IO 请求站了全部采样的 76%。
3.3 文件和块 IO 延迟的比较
在 Linux Block Driver - 2 中,我们介绍过 fio
的输出中自带 IO 延迟的计算和数值分布的统计。
例如,下面的输出就是这个 fio
测试的一个结果,
job1: (groupid=0, jobs=1): err= 0: pid=22977: Thu Jul 21 22:10:28 2016
write: io=1134.8GB, bw=1038.2MB/s, iops=265983, runt=1118309msec
clat (usec): min=0, max=66777, avg= 1.63, stdev=21.57
lat (usec): min=0, max=66777, avg= 1.68, stdev=21.89
clat percentiles (usec):
| 1.00th=[ 0], 5.00th=[ 1], 10.00th=[ 1], 20.00th=[ 1],
| 30.00th=[ 1], 40.00th=[ 1], 50.00th=[ 2], 60.00th=[ 2],
| 70.00th=[ 2], 80.00th=[ 2], 90.00th=[ 2], 95.00th=[ 3],
| 99.00th=[ 4], 99.50th=[ 7], 99.90th=[ 18], 99.95th=[ 25],
| 99.99th=[ 111]
lat (usec) : 2=49.79%, 4=49.08%, 10=0.71%, 20=0.34%, 50=0.06%
lat (usec) : 100=0.01%, 250=0.01%, 500=0.01%, 750=0.01%, 1000=0.01%
lat (msec) : 2=0.01%, 4=0.01%, 10=0.01%, 20=0.01%, 50=0.01%
lat (msec) : 100=0.01%
如果仔细分析上面的结果,可以发现,其中 clat 和 lat 的分布要明显好于 iosnoop 的结果。这是为什么呢?
其实这很好解释:因为 fio
的 clat 和 lat 是文件同步 IO 的延迟,该 IO 模式是 buffer IO,即文件的读写是基于文件的 page cache 的,是内存的读写。因此 clat 和 lat 的延迟要小很多。
而本章中,iosnoop
的 IO 延迟是块 IO 的延迟。文件系统 buffer IO 的读写并不会直接触发块设备的读写,因此,iosnoop
的 IO 请求和 fio
的 IO 请求根本不是同一个 IO 请求。
如果还记得 Linux Block Driver - 3 里的分析,我们知道,
这里的 iosnoop
的 IO 请求,都是 fio
通过调用 fadvise64,使用 POSIX_FADV_DONTNEED 把 /mnt/test 在 page cache 里的数据 flush 到磁盘引发的。
3.4 块 IO 吞吐量和 IOPS
运行 fio
测试时,我们可以利用 iostat(1) 命令来获取指定块设备在测试中的吞吐量 (throughput) 和 IOPS。
$ iostat /dev/sampleblk1 -xmdz 1
Linux 4.6.0-rc3+ (localhost.localdomain) 08/25/2016 _x86_64_ (2 CPU)
Device: rrqm/s wrqm/s r/s w/s rMB/s wMB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sampleblk1 0.00 0.37 0.00 59.13 0.00 6.50 225.31 0.01 0.15 0.00 0.15 0.02 0.12
Device: rrqm/s wrqm/s r/s w/s rMB/s wMB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sampleblk1 0.00 168.00 0.00 8501.00 0.00 932.89 224.74 0.77 0.10 0.00 0.10 0.02 14.40
Device: rrqm/s wrqm/s r/s w/s rMB/s wMB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sampleblk1 0.00 63.00 0.00 8352.00 0.00 909.64 223.05 0.89 0.11 0.00 0.11 0.02 16.30
Device: rrqm/s wrqm/s r/s w/s rMB/s wMB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sampleblk1 0.00 59.00 0.00 8305.00 0.00 908.45 224.02 0.98 0.13 0.00 0.13 0.02 17.50
Device: rrqm/s wrqm/s r/s w/s rMB/s wMB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sampleblk1 0.00 36.00 0.00 8536.00 0.00 936.51 224.69 1.06 0.13 0.00 0.13 0.02 19.00
[...snipped...]
其中,rMB/s 和 wMB/s 就是读写的吞吐量,而 r/s 和 w/s 就是 IOPS。
本例中的输出可以得到如下结论,
- sampleblk1 块设备的写吞吐量 (wMB/s) 是 908 ~ 932 MB/s。
- IOPS (w/s) 大概在 8300 ~ 8500。
- 平均的 IO 请求大小 (avgrq-sz) 为 220 左右
- 平均写等待时间 (w_await) 是 0.10 ~ 0.15 毫秒
需要说明的是,此处的的吞吐量和 IOPS 与如下所示的 fio
返回的输出里的有很大不同,
write: io=1134.8GB, bw=1038.2MB/s, iops=265983, runt=1118309msec
本例的测试中,fio
返回的是应用程序的 IO 吞吐量和 IOPS,而 iostat
返回的是底层一个块设备层面的吞吐量和 IOPS。
4. 小结
本文通过使用 Linux 下的各种追踪工具 Systemtap,Perf,iosnoop
(基于 ftrace 和 tracepoint),及 iostat
来分析 fio 测试时,底层块设备的运行情况。
我们掌握了本文中块设备 IO 在 fio 测试的主要特征,块 IO size,IO 延迟分布。这是性能分析里 resource analysis 方法的一部分。
关于 Linux 动态追踪工具的更多信息,请参考延伸阅读章节里的链接。
5. 延伸阅读
- Linux Block Driver - 1
- Linux Block Driver - 2
- Linux Block Driver - 3
- Linux Perf Tools Tips
- Using Linux Trace Tools - for diagnosis, analysis, learning and fun
- Flamegraph 相关资源
- Ftrace: The hidden light switch
- Device Drivers, Third Edition
- Ftrace: Function Tracer
- The iov_iter interface
- Toward a safer fput
- Linux Crash - background
- Linux Crash - coding notes
- Linux Crash White Paper (了解 crash 命令)