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 命令)