# Linux 核心設計: Block I/O(3): Block I/O 追蹤工具 ## 前言 當前 Linux kernel 採用的 Block I/O 框架是採用 [Multi-Queue Block IO Queueing Mechanism (blk-mq)](https://docs.kernel.org/block/blk-mq.html)。 ![image](https://hackmd.io/_uploads/BJmgKm2Gge.png) 如上圖所見。上層的 I/O library 或檔案系統所請求的 I/O 操作會以 `struct bio` 形式來描述,交付給 Block Layer。Block Layer 再將 `struct bio` 整合成一個 `struct request`(可能與現有的 `struct request` 合併,或者為其建立新的 `struct request`),交付給底層的 device driver。每個 device driver 有各自對應所需處理的 `request_queue` 可以接受這些 `struct request`。 由此可知,I/O 的完成將經歷多個步驟。檔案系統、I/O scheduler、在裝置中處理的時間等,都可能是造成 I/O 延遲的原因。因此,對於 I/O 造成之延遲的分析,如果要探究瓶頸所在,勢必需要能夠拆解出每個階段所耗費時間的工具。這就是 Linux 中 [blk tracer](https://docs.kernel.org/trace/ftrace.html#the-tracers) 引入的目的。藉由此工具,我們可以蒐集 Block I/O 的相關資訊。不過 blk tracer 的直接輸出可能不易解讀。更為普遍的使用方式是透過 [blktrace](https://git.kernel.dk/cgit/blktrace/) 來控制 blk tracer,然後再由 `blkparse` 來將輸出格式化成可讀的資訊。 ## `blktrace` + `blkparse` 下表中依序列出了一個 I/O 操作所可能經歷的事件之代號與對應函式名稱。 | Id | Function | Description | |:--- |:-------------------- |:---------------------------------------------- | | Q | block_bio_queue | 新的 `bio` 被加入到 Block Layer 的 queue 中 | | B | block_bio_bounce | `bio` 被複製到 bounced buffer(因為 DMA 的限制) | | X | block_split | 一個 `bio` 被拆分成兩個 `bio` | | M | block_bio_backmerge | `bio` 被 I/O scheduler back-merge 到 queue 中的 `request` | | F | block_bio_frontmerge | `bio` 被 I/O scheduler front-merge 到 queue 中的 `request` | | G | block_getrq | 獲取一個 free `request` 結構以處理 `bio` | | P | block_plug | `bio` 被禁止分派到 `request_queue` | | I | block_rq_insert | `request` 被加入到 `request_queue` | | U | block_unplug | `bio` 被允許分派到 `request_queue` | | A | block_rq_remap | `request` 被 remap 到不同裝置 | | D | block_rq_issue | 向 device driver 發出待處理的 `request` | | C | block_rq_complete | `request` 完成 | 執行 `blktrace` + `blkparse`,我們將能採樣每個 block I/O 事件的進行與執行時間。 ``` $ sudo blktrace -d /dev/nvme1n1p2 -o blktrace_out $ blkparse -i blktrace_out -o - -d blktrace.bin ``` 如果毋須保留 `blktrace` 的輸出,可以直接將結果 pipe 給 `blkparse`。具體的使用範例如下所示: ``` $ sudo blktrace -d /dev/nvme1n1p2 -o - | blkparse -i - 259,1 10 1 0.000000000 978 A WS 6142404488 + 8 <- (259,3) 6141353864 259,3 10 2 0.000001905 978 Q WS 6142404488 + 8 [NetworkManager] 259,3 10 3 0.000011710 978 G WS 6142404488 + 8 [NetworkManager] 259,3 10 4 0.000012731 978 P N [NetworkManager] 259,3 10 5 0.000017918 978 D WS 6142404488 + 8 [NetworkManager] 259,3 10 6 1.375993973 0 C WS 6142404488 + 8 [0] 259,1 2 1 1.376345034 498 A WSM 3999900032 + 8 <- (259,3) 3998849408 259,3 2 2 1.376347119 498 Q WSM 3999900032 + 8 [jbd2/nvme1n1p2-] 259,3 2 3 1.376354175 498 G WSM 3999900032 + 8 [jbd2/nvme1n1p2-] ... ``` `blktrace` 的每行輸出是固定的格式。以上方的最後一行輸出為範例,從中我們能得知的資訊是: * `259,3`: 進行此 I/O 操作的裝置之 major ID 為 259 且 minor ID 為 3 * 第一個 `2` 表示在 CPU2 上運行 * 第二個 `3` 表示這個 I/O 操作的序號(Sequence number) * `1.376354175` 是完成這個操作所發生在時間軸上的時間(秒) * `498` 是 PID * `G` 及對應上表中的單字代號 * `WSM` 表示對應 RWBS,參考 [`fill_rwbs`](https://elixir.bootlin.com/linux/v6.15.1/source/kernel/trace/blktrace.c#L1142) * R: `BLK_TC_READ` * W: `BLK_TC_WRITE` * D: `BLK_TC_DISCARD` * M: `BLK_TC_META` * N: `BLK_TN_MESSAGE` * `3999900032 + 8`: 表示 starting block 在 `3999900032`,且操作的長度是 8 個 sectors * `[jbd2/nvme1n1p2-]` 是 process 名稱 ## btt 雖然 `blktrace` 可以蒐集每一次 I/O 操作的各階段資訊,但繁雜的資訊可能使人無從下手。想取得更方便分析的統計數據,我們可以使用內建的 `btt` 來彙整。 ``` $ sudo blktrace -d /dev/nvme1n1p2 -o - | blkparse -i - -d blktrace.bin $ btt -i blktrace.bin ``` 一個 I/O 的完成可大致分成三個階段,`btt` 對各個階段的耗時進行統計: 1. I/O 進入 Block I/O 層,插入或合併到 `request_queue`,成為一個新的 request * Q2I: 插入 I/O,這又可以分解為兩個組成: * Q2G: 從 `bio` 發生到取得 `request` 的時間,表示 I/O scheduler 的相關效率 * G2I: 從 `request` 被建立到加入 `request_queue` 的時間 * Q2M: 從 `bio` 發生到 merge 至 `request` 的時間,表示 I/O scheduler 的相關效率 2. I/O 從 `request_queue` 到進入底層驅動程式 * I2D: 新的 `request` 在 `request_queue` 的等待時間 * M2D: I/O 被 merge 至 `request` 後,在 `request_queue` 的等待時間 3. I/O 進入驅動程序,到完成後返回到 Block I/O 層 * D2C: Device driver 從收到 `request` 到處理完畢的時間 `btt` 另外也統計兩個時間段: * Q2Q: 兩個新的 `bio` 發生的時間間隔,表示 I/O 進行的頻繁程度 * Q2C: I/O 完整生命週期的長度。即從新的 I/O 請求產生到完成所需要的時間 ### Basic Output ``` ==================== All Devices ==================== ALL MIN AVG MAX N --------------- ------------- ------------- ------------- ----------- Q2Q 0.000001027 0.000067603 0.001175166 39 Q2G 0.000000495 0.000002482 0.000007205 6 G2I 0.000329385 0.000550971 0.000863522 3 Q2M 0.000000307 0.000000445 0.000002261 34 I2D 0.000002023 0.000002617 0.000003670 3 M2D 0.000002563 0.000015287 0.000032745 34 D2C 0.000019841 0.000048032 0.000374409 40 Q2C 0.000025326 0.000104925 0.001089623 40 ``` 上表中可以看到各個階段包含最小、最大、平均時間與發生的次數的資訊。`N` 欄表示的是各階段發生的次數。 當追蹤 device mapper device 時,我們也會分解這些 I/O 的 Q2A 和 Q2C 時間。 ``` ==================== Device Overhead ==================== DEV | Q2G G2I Q2M I2D D2C ---------- | --------- --------- --------- --------- --------- (259, 3) | 0.3549% 39.3831% 0.3604% 0.1870% 45.7770% ---------- | --------- --------- --------- --------- --------- Overall | 0.3549% 39.3831% 0.3604% 0.1870% 45.7770% ``` 利用上面的數據,可以大致了解 I/O 在哪些階段花了最多的時間。 ``` ==================== Device Merge Information ==================== DEV | #Q #D Ratio | BLKmin BLKavg BLKmax Total ---------- | -------- -------- ------- | -------- -------- -------- -------- (259, 3) | 40 6 6.7 | 8 112 160 672 ``` I/O 進行的一個關鍵的衡量指標是了解 Block I/O 層在發送到底層驅動程式前,對 `request` 的合併。如上表,展示了傳入 I/O 的數量 (`#Q`)、最終發出的 `request` 數量 (`#D`) 以及其中比率。此外也展示了產生的最小、平均值和最大 I/O block 數,例如這裡顯示平均的 I/O 是 112 個 block,因此 6 * 112 總共 672 個 block 被進行。 ``` ==================== Device Q2Q Seek Information ==================== DEV | NSEEKS MEAN MEDIAN | MODE ---------- | --------------- --------------- --------------- | --------------- (259, 3) | 40 99998169.2 0 | 0(39) ---------- | --------------- --------------- --------------- | --------------- Overall | NSEEKS MEAN MEDIAN | MODE Average | 40 99998169.2 0 | 0(39) ``` ``` ==================== Device D2D Seek Information ==================== DEV | NSEEKS MEAN MEDIAN | MODE ---------- | --------------- --------------- --------------- | --------------- (259, 3) | 6 666654461.3 0 | 0(5) ---------- | --------------- --------------- --------------- | --------------- Overall | NSEEKS MEAN MEDIAN | MODE Average | 6 666654461.3 0 | 0(5) ``` 這裡顯示的統計數據是連續 I/O queue 與 I/O sumbit 之間 sector 距離的變化。 其中 `NSEEKS` 表示 seek (提交給底層驅動程式的 I/O)數量,`MEAN` 指 I/O 之間的平均距離。此外也統計了 seek 的中位數(`MEDIAN`)以及眾數(`MODE`, 包含值以及括號中的計數)。 當 `MEDIAN` 為 0,代表向前與向後的 seek 之數量趨於相等。而 `MODE` 中的計數與 NSEEKS 總數的筆值越大,則表示 I/O 越來自於相鄰的 sector,意味著底層處理這些 I/O 的效率能更好。 ``` ==================== Plug Information ==================== DEV | # Plugs # Timer Us | % Time Q Plugged ---------- | ---------- ---------- | ---------------- (259, 3) | 1( 0) | 94.935708781% DEV | IOs/Unp IOs/Unp(to) ---------- | ---------- ---------- (259, 3) | 0.0 0.0 (259, 1) | 0.0 0.0 ``` 在運行期間,`request_queue` 可能會被阻塞(plugged),此時 `request` 無法被交付到底層的驅動程式。上表的上半部分展示了 `request_queue` 處於這種狀態的頻率。 `request_queue` unplugged 會有兩個原因: 1. kernel 中某個子系統明確發出的 unplugged 請求。 2. 如果 `request_queue` 的 plug 時間超過了某個時間限制,會定期 unplug。 換言之,子系統 unplug 的總數等於 plug 總次數減去定期 unplug 的次數。 下半部分則展示了當 unplug 發生時,`request_queue` 中的平均 I/O 數量。右半部分特別獨立出因 timeout 而 unplug 時的數據。 ``` ==================== Active Requests At Q Information ==================== DEV | Avg Reqs @ Q ---------- | ------------- (259, 3) | 0.8 ``` 分析 Block I/O Scheduler 時,一個重要的考慮點是了解 Scheduler 需要處理的 request 數量。這裡提供的數據顯示了處理傳入的 request 時,Scheduler 平均保留了多少個。 ``` ==================== I/O Active Period Information ==================== DEV | # Live Avg. Act Avg. !Act % Live ---------- | ---------- ------------- ------------- ------ (259, 3) | 6 0.000148146 0.000491539 26.56 (259, 1) | 0 0.000000000 0.000000000 0.00 ---------- | ---------- ------------- ------------- ------ Total Sys | 6 0.000148146 0.000491539 26.56 ``` 這節的數據展示了每個裝置的 "I/O activity": 意指底層裝置驅動程式和裝置有至少一個 I/O 需要處理的時間段。 * `# Live`: 發生 active 時間段的總次數 * `Avg. Act`: 每個 active period 的平均時長 * `Avg. !Act`: 每個 non-active period 的平均時長 * `% Live`: 驅動程式/裝置處於 active 的總時間比例 ``` # Total System # Total System : q activity 0.000161592 0.0 0.000161592 0.4 0.002798116 0.4 0.002798116 0.0 # Total System : c activity 0.000241946 0.5 0.000241946 0.9 0.000241946 0.9 0.000241946 0.5 18446744073.709518433 0.5 18446744073.709518433 0.9 18446744073.709518433 0.9 18446744073.709518433 0.5 # Per device # 259,3 : q activity 0.000161592 1.0 0.000161592 1.4 0.002798116 1.4 0.002798116 1.0 # 259,3 : c activity 0.000241946 1.5 0.000241946 1.9 0.000241946 1.9 0.000241946 1.5 18446744073.709518433 1.5 18446744073.709518433 1.9 18446744073.709518433 1.9 18446744073.709518433 1.5 # Per process # blktrace : q activity # blktrace : c activity 0.000241946 2.5 0.000241946 2.9 0.000241946 2.9 0.000241946 2.5 # jbd2 : q activity 0.000161592 3.0 0.000161592 3.4 0.002798116 3.4 0.002798116 3.0 # jbd2 : c activity # kernel : q activity # kernel : c activity 0.000013446 4.5 0.000013446 4.9 0.000132848 4.9 0.000132848 4.5 18446744073.709518433 4.5 18446744073.709518433 4.9 18446744073.709518433 4.9 18446744073.709518433 4.5 ``` 最後的部分展示了系統/裝置/程序觀察到的 "Activity",以易於 xmgrace6 繪圖工具處理的格式紀錄。 資訊被拆分成多個 data points,包含兩行資料。第一行是代表時間的 X 軸值(以秒為單位),第二行則是代表是否有 activity 的 Y 軸值。Y 軸僅會存在兩種數值: 代表沒有 activity 的低值以及有 activity 的高值。之所以每個觀察角度的 Y 軸數值有不同,是為了在繪圖時避免重疊,以利於觀察。 以上表的 "Total System : q activity" 為例,該輸出代表在 0.000161592 開始出現 activity,並於 0.002798116 時 activity 歸為零。 ### Detail Data 在使用 btt 時如果加上 `-A` 選項,可以得到更多額外的資訊。 ``` ==================== Per Process ==================== Q2C MIN AVG MAX N --------------- ------------- ------------- ------------- ----------- jbd2 0.000032966 0.000138166 0.001372781 30 kworker 0.004075599 0.004075599 0.004075599 1 ``` 包含區分 Process 的分析。會顯示各個 process 在 Block I/O 的不同階段的耗時(不僅只有 Q2C,上表僅擷取部分示範)。 必須留意的是,顯示的 process 可能與原始請求 I/O 的 process 無關。例如,如果某個應用程式正在執行 Buffer I/O,那麼實際提交的 I/O 很可能來自管理 page buffer 的 daemon(例如 pdflush 或 kjournald)。 ``` ==================== Per Device ==================== Q2Q MIN AVG MAX N --------------- ------------- ------------- ------------- ----------- (259, 3) 0.000000808 0.022403921 0.669235380 30 ``` 也有區分 Device 在各階段耗時的分析。 ``` ==================== Q2D Histogram ==================== DEV | <.005 <.010 <.025 <.050 <.075 <.100 <.250 <.500 < 1.0 >=1.0 --------- | ===== ===== ===== ===== ===== ===== ===== ===== ===== =====· (259, 3) | 100.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0· ========== | ===== ===== ===== ===== ===== ===== ===== ===== ===== =====· AVG | 100.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0· ``` 另外還有 Q2D 時間分佈的直方圖。 ## Plot ``` btt -i blktrace.bin -z q2d_latency btt -i blktrace.bin -l d2c_latency btt -i blktrace.bin -q q2c_latency ``` ## iowatcher 透過 `iowatcher` 我們可以將 I/O 的資訊視覺化出來,這可以讓 I/O 的分析更一眼明瞭。 ``` $ iowatcher -t blktrace.bin -o result.svg ``` ## iosnoop [perf-tools/iosnoop](https://github.com/brendangregg/perf-tools/blob/master/iosnoop) ## Reference * [blktrace分析IO](https://bean-li.github.io/blktrace-to-report/) * [分析IO的工具 ioprof](https://bean-li.github.io/ioprof/) * [Linux Block Driver - 5](https://oliveryang.net/2016/10/linux-block-driver-basic-5/) * [btt User Guide](https://allstar.jhuapl.edu/repo/p1/amd64/blktrace/doc/btt.pdf)