AI 訓練磁碟 I/O 瓶頸排查:從 fio Benchmark 到 GCP Hyperdisk 優化實踐

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=32numjobs=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 的情況下:

\[Max\ IOPS = \frac{1}{0.002} = 500\ IOPS\]

即使你把佇列深度拉到 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):使用 libaioio_uring,讓多個讀取請求同時進行
  • 多 Worker 平行讀取:PyTorch DataLoader(num_workers=N) 或 TensorFlow tf.data pipeline 的 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 根本無法同時發多個請求。

瓶頸判斷流程

  1. 磁碟層:IOPS 遠低於購買的額度,但延遲正常 → 佇列深度不夠,磁碟在「等工作」
  2. VM 頻寬上限:每種 VM 規格都有自己的磁碟頻寬天花板,就算 disk 買了再多 IOPS,VM 本身吃不下也沒用 → 去查你的 machine type 的 disk performance limits3
  3. 應用程式層:磁碟指標看起來正常,但 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 環境磁碟效能評估的影響。

參考資料

Eason Cao
Eason Cao Eason is an engineer working at FANNG and living in Europe. He was accredited as AWS Professional Solution Architect, AWS Professional DevOps Engineer and CNCF Certified Kubernetes Administrator. He started his Kubernetes journey in 2017 and enjoys solving real-world business problems.
comments powered by Disqus