NVMe Protocol
為什麼 NVMe 取代了 AHCI
把一顆 NVMe SSD 插進主機板的 M.2 插槽,打開 CrystalDiskMark 跑個測試,sequential read 跑出 7000 MB/s。你的舊 SATA SSD 同樣的測試只有 550 MB/s。但如果你把兩顆 SSD 拆開來看,裡面的 NAND flash 顆粒可能是同一家廠商、同一個製程生產的。效能差了十幾倍,不是因為 flash 本身快了十幾倍,而是因為連接 flash 和 CPU 之間的協定和介面完全不同。舊的那套叫 AHCI,是為 HDD 時代設計的;新的叫 NVMe,是專門為 flash storage 從零開始設計的。
在 NVMe 出現之前,SSD 使用的介面是 AHCI(Advanced Host Controller Interface)。AHCI 最初是為 HDD 設計的,它只有一個 command queue,深度最多 32 個 command。
HDD 的 random I/O latency 大約 10ms,所以一個 queue 深度 32 就綽綽有餘:32 個 pending command x 每個 10ms 延遲,意味著 HDD 每秒最多也就處理幾百個 IOPS,一個 queue 足以餵飽它。
但 SSD 的 random read latency 只有大約 100 microseconds,理論 IOPS 可以到幾十萬甚至上百萬。一個深度 32 的 queue 根本不夠用。而且,因為只有一個 queue,多個 CPU core 要發 I/O 就必須搶同一個 lock。在 8 核、16 核甚至更多核心的現代 CPU 上,這個 lock contention 本身就成為嚴重的效能瓶頸。
除了 queue 數量的問題,AHCI 協定本身也跑在 SATA bus 上,SATA 3.0 的 bandwidth 上限是 600 MB/s。就算 SSD 本身能做到 3 GB/s 的 sequential read,被 SATA 一卡就只剩 600 MB/s。
NVMe 直接跑在 PCIe bus 上(PCIe 4.0 x4 的 bandwidth 約 8 GB/s),支援最多 65535 個 queue,每個 queue 深度可達 65536 個 command。這兩個數字和 AHCI 的「1 個 queue、32 個 command」相比,差距是三個數量級。
NVMe 的 Multi-Queue 架構
You can find some illustration here
NVMe 的核心設計是 Submission Queue(SQ) 和 Completion Queue(CQ) 的配對機制。一對 SQ/CQ 叫做一個 queue pair。整個 I/O 流程如下:
提交階段 (submission):軟體(通常是 OS 的 NVMe driver)把一個 I/O command 寫入 SQ 的下一個空位。這個 command 包含操作類型(read/write)、LBA(logical block address)、資料長度、以及 DMA buffer 的 physical address(用 PRP 或 SGL 格式)。寫完之後,軟體更新 SQ 的 doorbell register,這是一個 memory-mapped I/O register,寫入它就相當於告訴 NVMe controller:「我放了新的 command 進去,請處理」。
處理階段:NVMe controller 看到 doorbell 被更新後,從 SQ 裡取出 command,執行 I/O 操作(內部可能涉及 FTL 查表、NAND read/write 等等)。
完成階段 (completion):Controller 完成 I/O 後,把結果(成功或失敗、以及相關資訊)寫入對應的 CQ 的下一個空位。然後根據配置,可能送一個 interrupt 通知 CPU。軟體收到通知後,去 CQ 裡讀取完成結果,處理完畢後更新 CQ 的 head doorbell,告訴 controller「我已經消費了這些完成結果,你可以覆蓋掉了」。
SQ 和 CQ 本身就是一段 host memory 裡面的 ring buffer。Controller 透過 DMA 來讀取 SQ 裡的 command、以及寫入 CQ 裡的 completion entry。軟體和硬體各自維護自己的 head/tail pointer,透過 doorbell register 來同步。
Per-Core Queue Pair 避免 Lock Contention
NVMe 支援大量 queue pair 的目的不只是為了增加總 queue depth。更重要的設計意圖是:讓每個 CPU core 擁有自己專屬的 SQ/CQ pair。
為什麼這很重要?想像一個繁忙的資料庫 server,16 個 core 同時在處理不同使用者的查詢,每個查詢都需要讀取 SSD 上的資料。在 AHCI 的年代,只有一個 queue,多個 core 要發 I/O 就必須拿鎖。假設你有 16 個 core,每個 core 每秒要發 5 萬個 I/O command,那這把鎖每秒要被 contend 80 萬次。鎖的 overhead 包括 atomic operation(至少幾十個 nanoseconds)和 cache line bouncing(在不同 core 之間同步鎖的 cache line 也要幾十個 nanoseconds)。在高 IOPS 場景下,光是鎖的 overhead 就可能佔到 I/O latency 的 10-20%。
NVMe 的 per-core queue pair 設計讓每個 core 只操作自己的 SQ 和 CQ,完全不需要和其他 core 協調。Core 0 寫 SQ0、讀 CQ0;Core 1 寫 SQ1、讀 CQ1。沒有共享的資料結構,也就沒有鎖、沒有 cache line bouncing、沒有 contention。
Linux 的 NVMe driver 在初始化的時候,會為系統中的每個 CPU core(或者每個 NUMA node)建立一組 queue pair,然後在 I/O path 中根據 current CPU 選擇對應的 queue pair。這個設計和 NVMe 硬體的 multi-queue 架構完美搭配。
實測數字:一顆高階 NVMe SSD 搭配 per-core queue pair,在 16 核系統上可以達到接近線性的 IOPS scaling。也就是說 16 核的 IOPS 接近單核的 16 倍。如果用單一 queue 加鎖的方式,通常到 4-8 核就開始 saturate 了。
NVMe Namespace
NVMe 引入了 namespace 的概念來管理裝置上的儲存空間。Namespace 是 NVMe 裝置中的一個邏輯單元,類似於 disk partition,但更靈活。
每個 namespace 有自己的 LBA(Logical Block Address)空間,也就是說 namespace 1 的 LBA 0 和 namespace 2 的 LBA 0 是完全獨立的。一顆 NVMe SSD 可以被切成多個 namespace,每個 namespace 可以有不同的 block size、不同的 capacity,甚至不同的 protection 設定。
Namespace 常見的應用場景有幾種。第一,在 multi-tenant 的雲端環境中,一顆 physical SSD 可以被劃分成多個 namespace,分配給不同的 virtual machine,每個 VM 看到的是一個獨立的 NVMe 裝置。第二,在效能隔離的場景中,不同的 namespace 可以對應到裝置內部不同的 NAND channel 或 die,從而在物理層面實現 I/O 隔離。第三,NVMe 標準支援每個 namespace 獨立的格式化操作(format),你可以在不影響其他 namespace 的情況下重新格式化某一個 namespace。
不過大部分消費級 NVMe SSD 預設只有一個 namespace,涵蓋整顆裝置的全部容量。Namespace 的多分割特性主要在企業級 SSD 和資料中心場景下才會用到。
自問自答
試著用自己的話回答以下問題。如果卡住了,回去重讀對應的段落。
-
AHCI 的單一 queue 設計為什麼在 HDD 時代不是問題,但到了 SSD 時代卻成為嚴重瓶頸?
-
為什麼 per-core queue pair 的設計可以避免 lock contention?如果改用「所有 core 共享一個 queue,加一把 spinlock」的方式,在 16 核、每核每秒 5 萬 IOPS 的情境下會有什麼 overhead?
-
NVMe namespace 和傳統的 disk partition 有什麼相同和不同之處?namespace 在什麼場景下特別有用?