AI 訓練磁碟 I/O 瓶頸排查:從 fio Benchmark 到 GCP Hyperdisk 優化實踐
你有沒有遇過這種情況:在 GCP 上開了一台 VM,掛上 Hyperdisk Balanced,花錢買了 160,000 IOPS 的效能額度(provisioned IOPS)1,信心滿滿地跑了一輪 fio 壓測,結果 IOPS 只有少少的 ~2,000?
真相往往令人意外:問題可能不在 Hyperdisk,而在測試方法。更精確地說,fio 的預設參數(特別是 iodepth,也就是同時發出幾個 I/O 請求)根本無法充分發揮「透過網路連接的遠端磁碟」的效能。
這篇文章會從原理開始,一步步拆解以下幾個核心概念:
- Queue Depth 與 Little’s Law:為什麼「同時發出幾個 I/O 請求」決定了你能跑出多少 IOPS
- fio 參數調校:哪些預設值會讓測試結果嚴重失真,以及正確的壓測指令該怎麼寫
- AI Workload 的磁碟優化:從訓練到推論,不同階段對磁碟的需求有什麼不同,以及如何在 GKE 環境中正確配置 Hyperdisk
一個實際問題描述
你在 GCP 上開了一台 a3-megagpu-8g VM,掛上 Hyperdisk Balanced,並且在建立磁碟時把 provisioned IOPS 直接拉到上限 160,000 IOPS。照理說,跑個 fio 隨機讀取 IOPS 壓測,應該能看到接近 160k 的數字。
但實際上,測出來卻只有 read 約 24k IOPS,看起來像是「我花錢買的 IOPS 不見了」。
範例 fio 指令
這裡用常見的 4K random read 來測 IOPS,並直接對 raw device 測試:
fio --name=randread-iops-first-try \
--filename=/dev/nvme0n1 \
--ioengine=libaio \
--direct=1 \
--rw=randread \
--bs=4k \
--iodepth=32 \
--numjobs=1 \
--runtime=30 \
--time_based \
--group_reporting
實際輸出(read 約 24k IOPS)
randread-iops-first-try: (g=0): rw=randread, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=32
fio-3.35
Starting 1 process
Jobs: 1 (f=1): [r(1)][100.0%][r=96.0MiB/s][r=24.6k IOPS][eta 00m:00s]
randread-iops-first-try: (groupid=0, jobs=1): err= 0: pid=12345: Sun Mar 29 15:54:16 2026
read: IOPS=24.1k, BW=94.1MiB/s (98.7MB/s)(2823MiB/30001msec)
slat (nsec): min=1200, max=29000, avg=4100.32, stdev=900.11
clat (usec): min=850, max=4200, avg=1310.45, stdev=210.37
lat (usec): min=860, max=4210, avg=1314.62, stdev=211.02
clat percentiles (usec):
| 1.00th=[ 980], 5.00th=[ 1057], 10.00th=[ 1123], 50.00th=[ 1303],
| 90.00th=[ 1565], 95.00th=[ 1696], 99.00th=[ 2057], 99.90th=[ 2933]
cpu : usr=1.10%, sys=6.80%, ctx=18012, majf=0, minf=15
IO depths : 1=0.1%, 2=0.1%, 4=0.2%, 8=0.3%, 16=0.6%, 32=98.7%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
issued rwts: total=722944,0,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=32
Run status group 0 (all jobs):
READ: bw=94.1MiB/s (98.7MB/s), 94.1MiB/s-94.1MiB/s (98.7MB/s-98.7MB/s), io=2823MiB (2960MB), run=30001-30001msec
Disk stats (read/write):
nvme0n1: ios=722944/0, merge=0/0, ticks=913000/0, in_queue=913000, util=99.20%
這個輸出最關鍵的訊息是:雖然磁碟買到 160k IOPS,但在 iodepth=32、numjobs=1 的設定下,讀取只跑到約 24k IOPS。接下來會用 Queue Depth 與 Little’s Law 解釋「為什麼這是預期中的結果」。
Hyperdisk 基礎概念
Hyperdisk vs Persistent Disk
GCP 的雲端磁碟(block storage)經歷了幾代演進。傳統的 Persistent Disk(PD)效能跟磁碟容量綁死,想要更高 IOPS,就得開更大的 disk。這在很多場景下既浪費又不靈活。
Hyperdisk 是 GCP 推薦的新一代雲端磁碟,資料不會因為 VM 關機而消失(也就是所謂的「持久性儲存」)2,最大差異在於:
- IOPS、讀寫速度(throughput)、容量(capacity)可以分開購買:你可以開一個只有 100 GiB 的 disk,但單獨買 160k IOPS 的效能
- 效能不再跟容量綁死:按需分配,成本更可控
- 支援 Storage Pool:大規模部署時可以把多個 disk 的資源放進同一個「池子」統一管理
Hyperdisk 類型比較
| 類型 | 最大 IOPS | 最大 Throughput | 延遲 | 典型場景 |
|---|---|---|---|---|
| Hyperdisk Balanced | 160,000 | 2,400 MiB/s | < 1ms | 一般用途、Web app、中型資料庫 |
| Hyperdisk Extreme | 350,000 | 5,000 MiB/s | < 1ms | 高效能資料庫、高頻交易(OLTP)、對延遲敏感的應用 |
| Hyperdisk ML | 最高 19,200,000 | 最高 1,200,000 MiB/s | < 1ms | AI 推論、模型載入(支援多達 2,500 台 VM 同時唯讀掛載) |
| Hyperdisk Throughput | 最高 9,600 | 最高 2,400 MiB/s | 較高 | 資料分析、大容量冷資料儲存 |
補充:以上為每個 volume 的規格上限,單位統一為 MiB/s(GCP 官方用法)1。Hyperdisk Extreme 的 throughput 由 IOPS 衍生(每 1,000 IOPS = 250 MiB/s),無法單獨設定。Hyperdisk ML 和 Throughput 的 IOPS 則由 throughput 衍生。Hyperdisk ML 的超高上限是為多 VM 唯讀共享設計,單台 VM 能用到的 throughput 取決於機型(例如 a3-megagpu-8g 上限為 4,800 MiB/s)3。
透過網路連接的磁碟,延遲是關鍵
這是最重要的概念:Hyperdisk 不是插在主機板上的本地 SSD,而是透過網路連接的遠端磁碟(Network-Attached Storage)4。每一次讀寫都要經過網路往返,平均延遲大約在 1~2 毫秒(ms) 的等級。
1~2ms 聽起來很快,但它直接決定了你需要「同時排多少個 I/O 請求」才能填滿你買的 IOPS 額度,這就是接下來要討論的核心問題。Hyperdisk 的效能不是「接上就能用滿」的。你必須主動製造足夠的並行 I/O 請求,才能把花錢買的 IOPS 額度真正跑出來。
為什麼 fio 預設參數測不到 Provisioned IOPS
Queue Depth(佇列深度)與 Little’s Law
要理解這個問題,得先認識一個排隊理論的經典公式:Little’s Law。簡單來說,就像餐廳出餐一樣:如果廚房每道菜要 2 分鐘,你想要每分鐘出 100 道菜,那你就得同時有 200 道菜在廚房裡排隊。套到磁碟 I/O 上:
\[Throughput = \frac{Queue\ Depth}{Latency}\]換句話說:
\[Required\ Queue\ Depth = Desired\ IOPS \times Latency\]假設 Hyperdisk 的 average I/O latency 是 2ms(0.002 秒),你想達到 160,000 IOPS:
\[Required\ QD = 160{,}000 \times 0.002 = 320\]這意味著你必須同時維持 320 個正在進行中的 I/O 請求,才能把 160k IOPS 全部跑滿。
預設 iodepth=1 的瓶頸
fio 的預設 iodepth(佇列深度)是 15:也就是一次只送出一個 I/O 請求,等它完成才送下一個。在每次要等 2ms 的情況下:
即使你把佇列深度拉到 iodepth=32,以 2ms 延遲估算理論上也只能達到 ~16,000 IOPS(前面範例的實測延遲約 1.3ms,所以 32 ÷ 0.0013 ≈ 24k,數字是吻合的)。這就是為什麼測試結果遠低於預期的原因:排隊的人不夠多,廚房(磁碟)當然閒著。這不是磁碟的問題,是測試方法的問題。
GCP 推薦的 Queue Depth 對照表
下表整理自 GCP 對 Hyperdisk 的最佳化建議4:
| 目標 IOPS | 建議 Queue Depth | 備註 |
|---|---|---|
| 500 | 1 | 預設就夠 |
| 16,000 | 32 | 一般 PD 等級 |
| 64,000 | 128 | 中等 Hyperdisk |
| 160,000 | 320 | Hyperdisk Balanced 上限 |
| 350,000 | 640+ | Hyperdisk Extreme |
其他常見的參數優化
I/O Size(每次讀寫的資料大小)
- 測 IOPS 時用
bs=4k(每次只讀寫 4KB,這樣才能衝高「次數」) - 測讀寫速度時用
bs=1M(每次讀寫 1MB,衝高「總傳輸量」) - 如果每次讀寫太大,可能還沒達到 IOPS 上限,就先被傳輸頻寬卡住了
direct=1(繞過作業系統的快取)
- 必須設定
direct=1,讓資料直接寫入/讀取磁碟,不經過 OS 的記憶體快取(page cache)6 - 否則你測的其實是記憶體速度,不是真正的磁碟速度
- GCP 官方文件明確要求壓測時使用
direct=1
ioengine=libaio(非同步 I/O 引擎)
libaio是 Linux 原生的非同步 I/O 引擎,屬於建議壓測雲端磁碟的首選,它可以「同時發出很多個請求,不用一個一個等」5- fio 預設的
psync引擎是同步模式,一次只能處理一個 I/O,使用這種模式,很可能就算你設了高iodepth也沒辦法打滿磁碟效能5 io_uring是比較新的非同步引擎,效能接近libaio,但要注意你的 Linux kernel 版本是否支援
優化的 fio 壓測指令範例
注意:務必使用 raw device(如 /dev/sdb)而非 mounted filesystem 來測試6。如果必須在 filesystem 上測試,使用 --filename=/mnt/test/fio-test 搭配 --size=100G 先建立測試檔案,並確認 filesystem 沒有額外 overhead(如 journaling)干擾結果。
Random Read IOPS 測試
iodepth=256 × numjobs=4 = 1024 (queue depth),足以驅動 160k+ IOPS。GCP 官方在 Hyperdisk benchmark 文件中,對 random read IOPS 明確建議至少 iodepth=256,並提供多 job 的 fio 範例7。
fio --name=rand-read-iops \
--filename=/dev/sdb \
--ioengine=libaio \
--direct=1 \
--rw=randread \
--bs=4k \
--iodepth=256 \
--numjobs=4 \
--runtime=60 \
--time_based \
--group_reporting
Sequential Read Throughput 測試
這個測試的目標是把 MiB/s(吞吐量) 跑滿,所以 block size 會放大到 1M,並用多個 jobs 來吃滿 VM 端的網路與磁碟併發能力。你會看到 IOPS 可能不高,但 BW 會顯著上升。
幾個常見調整方向:
- 如果 BW 上不去但 latency 很低,通常是 jobs / iodepth 不夠,把
numjobs提高到 8 或 16 再試。 - 如果 BW 上不去且 CPU sys 很高,可能是 I/O 引擎或路徑額外開銷,可改用
ioengine=io_uring(kernel 支援時)或檢查是否在 filesystem 上測。 - 如果 BW 達到某個上限就卡住,通常是 VM 的磁碟吞吐上限,這時候再加 iodepth 可能不會有顯著效果。
下面是一個 sequential read throughput 指令範例:
fio --name=seq-read-throughput \
--filename=/dev/sdb \
--ioengine=libaio \
--direct=1 \
--rw=read \
--bs=1M \
--iodepth=64 \
--numjobs=4 \
--runtime=60 \
--time_based \
--group_reporting
Mixed Read/Write 測試
很多真實 workload 不會是純讀或純寫,而是混合讀寫(例如:資料庫、Feature Store、訓練過程中一邊讀資料一邊寫 checkpoint、或是 inference 服務一邊載入權重一邊寫入快取)。
這類測試的關鍵是:
- 先定義讀寫比例:例如 70% read / 30% write
- 維持足夠併發:不然一樣會被 latency 鎖死,看起來像 IOPS 很低
- 留意 latency 分佈:混合 I/O 時,寫入通常會拉高整體延遲,進而影響讀取 IOPS
安全提醒:下面這個範例使用 rw=randrw 直接對 /dev/sdb 進行真實寫入。如果你拿它跑在正式磁碟上,會覆蓋既有資料。這個指令只應該用在專門拿來測試的空白磁碟上;若你要測既有 filesystem,請改用測試檔案路徑而不是 raw device。
下面是一個 70/30 的 4K 隨機混合讀寫範例(可依需求調整 rwmixread):
fio --name=mixed-rw \
--filename=/dev/sdb \
--ioengine=libaio \
--direct=1 \
--rw=randrw \
--rwmixread=70 \
--bs=4k \
--iodepth=128 \
--numjobs=4 \
--runtime=60 \
--time_based \
--group_reporting
AI Workload 的磁碟效能優化
AI 訓練場景
AI 訓練對磁碟的需求主要有兩大類:
| 訓練階段工作 | I/O 型態與需求 | 建議 Hyperdisk | fio 模擬和實作 |
|---|---|---|---|
| Checkpoint 寫入 | 大量、偏連續寫入,重點是 throughput(MiB/s) | Balanced 或 Extreme(搭配足夠的 provisioned throughput) | bs=1M • 較高 iodepth 以拉滿寫入頻寬 |
| 載入訓練資料 | 偏隨機讀取,重點是 IOPS | Balanced 或 Extreme(視目標 IOPS/成本) | 提高 DataLoader worker 數以形成足夠併發,並避免同步 I/O 成為瓶頸 |
AI 推論場景
載入模型權重
- 推論服務啟動時需要一次性把整個模型讀進記憶體(幾 GB 到幾百 GB)
- Hyperdisk ML 就是為這個場景設計的:多台 VM 可以同時「唯讀」掛載同一塊 disk,不用每台都複製一份8
- 非常適合推論服務的水平擴展架構:一份模型磁碟被多個推論節點共用
- 在 GKE 中透過 CSI driver 原生支援
ReadOnlyMany存取模式8
如何選擇 Hyperdisk 類型
| 場景 | 考慮 Hyperdisk 類型 | 原因 |
|---|---|---|
| 一般訓練資料儲存 | Balanced | IOPS 和傳輸速度平衡,價格合理 |
| 高頻存檔 + 大量資料載入 | Extreme | 需要超過 160k IOPS 或超過 2.4 GiB/s 的傳輸速度 |
| 推論服務(模型部署) | ML | 多台機器唯讀共享同一塊 disk |
| 大規模冷資料儲存 | Throughput | 大容量、高傳輸速度、低成本 |
應用程式層的優化
- 提前預讀(Prefetch):在 training loop 中提前載入下一批資料,不要等 GPU 空閒才去讀 disk
- 非同步 I/O(Async I/O):使用
libaio或io_uring,讓多個讀取請求同時進行 - 多 Worker 平行讀取:PyTorch
DataLoader(num_workers=N)或 TensorFlowtf.datapipeline 的 parallel map,每個 worker 都在幫你排隊讀資料 - 記憶體映射檔案(mmap):對於大型 dataset,用
mmap讓 OS 自動管理哪些資料留在記憶體,但注意隨機讀取時可能因為頻繁「換頁」(page fault)而產生額外開銷
除錯與監控
使用 iostat 觀察實際狀況
iostat -xdmt /dev/sdb 1
重點觀察的欄位:
r/s/w/s:每秒實際完成的讀/寫次數(也就是實際 IOPS)await:每個 I/O 請求的平均等待時間(毫秒)aqu-sz:平均佇列深度——如果這個數字遠低於你設定的iodepth,代表 fio 沒有成功同時發出那麼多請求%util:裝置使用率(注意:對遠端網路磁碟來說,這個數字可能不太準確)
如果 aqu-sz 遠低於你設定的 iodepth,通常是因為 ioengine 沒設成非同步模式(如 libaio),導致 fio 根本無法同時發多個請求。
瓶頸判斷流程
- 磁碟層:IOPS 遠低於購買的額度,但延遲正常 → 佇列深度不夠,磁碟在「等工作」
- VM 頻寬上限:每種 VM 規格都有自己的磁碟頻寬天花板,就算 disk 買了再多 IOPS,VM 本身吃不下也沒用 → 去查你的 machine type 的 disk performance limits3
- 應用程式層:磁碟指標看起來正常,但 app 還是覺得慢 → 可能是程式本身的讀寫方式有問題(用了同步 I/O、單執行緒讀取、緩衝區太小等)
結語
在整理整篇文章的過程中,我歸納出各家雲端廠商提供的磁碟效能,並不是「接上就能用滿」。通常需要在應用程式端主動製造足夠的並行 I/O 請求,才能把花錢買的 IOPS 額度真正跑出來。
Little’s Law 把這件事講得很直覺:需要的佇列深度 = 目標 IOPS × 平均延遲。以學術理論總結,它解釋了為什麼 fio 預設的 iodepth=1 跑出來的數字會跟實際買的額度差這麼多,不是磁碟不行,很可能是在支援並行處理的系統中,沒有同時送出夠多請求4。
延伸到 AI workload,這個原則同樣適用:訓練需要寫入 checkpoint,也需要隨機讀取資料;推論則需要快速載入模型權重,並讓多台機器共享同一份磁碟。每個場景對 IOPS 與 throughput 的需求不同;一旦選錯 Hyperdisk 類型或參數設定不當,瓶頸就會直接轉移到磁碟 I/O 指標與資料處理速度上。
本篇以實際使用 fio 在雲端環境觀察到的磁碟效能瓶頸為例,進一步延伸討論其背後機制與應用層面的優化方向,期望能提供一些參考,並協助理解 Queue depth 對 GCP 環境磁碟效能評估的影響。
參考資料