在 Linux 中,有兩種用於高效非同步磁碟 I/O 的介面:傳統的 AIO (libaio) 和較新的 io_uring (liburing)。眾所周知,io_uring 的效能優於 AIO。許多論文都證明了這一點,核心開發者也經常提到 io_uring 在初始發布後效能有了顯著提升。然而,要找到具體的數字來顯示這種效能如何在不同核心版本中演變,卻出奇地困難。大多數基準測試僅在單一核心上比較 API,使得歷史圖景變得模糊不清。

在 YDB,我們不斷尋找提升資料庫效能的方法。我們的生產伺服器通常運行 Linux 5.4、5.15 或 6.6,並且我們正朝著 6.12 邁進,這讓我們感到好奇:核心版本本身對非同步 I/O 效能的影響有多大?因此,我們決定使用 fio 自己進行測量。以下是隨機 4K 寫入的結果:

從上圖可以看出,關鍵發現是:

io_uring 不僅擊敗了 libaio,而且其效能在新核心版本上也有顯著提升。然而,其中存在一些微妙的陷阱。在此過程中,我們調查了看似核心退化的現象,並發現真正的原因是 Intel IOMMU 在不同版本之間預設啟用。在發現這一點之前,我們對新核心版本中 libaio 和 io_uring 的 IOPS 約下降 30% 感到震驚。

這是故事的簡短版本。細節如下。

我們在具有以下配置的裸機上進行了實驗:

Linux 核心 5.4.161c 是我們自訂的內部核心。它包含發行版補丁,但我們不預期其中任何一個會影響非同步 I/O 效能。核心 5.15.0–164 來自 Ubuntu。其餘的是 Ubuntu 主線核心。

NVMe 裝置連接到 NUMA 節點 0。所有基準測試都在固定到 NUMA 節點 0 的 CPU 核心 0–16 的 cgroup 中執行,記憶體分配綁定到相同的 NUMA 節點。CPU 調速器設定為 performance。

為了比較 libaio 和 io_uring,我們使用了一個基於 fio 基準測試工具的腳本。該腳本運行一系列 fio 命令,同時改變 iodepth 並在不同的 I/O 引擎之間切換。我們測量原始區塊裝置效能。

實驗中使用的基礎 fio 命令範例如下:

使用的引擎和引擎參數如下:

額外的 io_uring 優化(例如,註冊緩衝區)故意不在範圍內;我們僅比較核心之間的等效配置。

基準測試的目標是比較 I/O 機制,因此在每次運行之前,我們會刷新裝置狀態。這是使用 blkdiscard 完成的,它會重置 NVMe 裝置並將其恢復到最佳效能狀態。

工作負載使用隨機 4K 寫入,這會對核心中的 I/O 提交路徑造成壓力,並常用於評估非同步 I/O 效能。

對於每個引擎 + 引擎參數 + iodepth 組合:

為了避免熱效應和裝置端節流:

每次運行都從 10 秒的預熱開始,然後是 1 分鐘的測量窗口。這個持續時間通常足以比較 I/O 引擎,同時避免由於內部垃圾回收或背景維護導致的 NVMe 效能下降。

對於每次運行,我們都會收集 IOPS 和延遲統計數據(包括百分位數)。最大 IOPS 只是圖片的一部分:檢查低 I/O 深度的延遲也很重要。

所有基準測試都在相同的硬體上執行,使用相同的 fio 版本和配置,除非另有說明,僅更改核心版本和 I/O 引擎。

首先,讓我們再次看看不同核心和 I/O 機制達到的最大 IOPS。

為了完成畫面,讓我們專注於 Linux 6.18.18(我們使用的最新穩定核心)上的 io_uring 與 libaio 的比較。首先,這是僅顯示 6.18.18 結果的最大 IOPS 圖:

接下來,讓我們檢查 IOPS 如何隨著 iodepth 的增加而擴展。下圖顯示了帶有鬍鬚的最小值和最大值,中位數顯示為一個點。

下面是 iodepth 範圍 1–8 的縮放視圖:

io_uring 在整個 iodepth 範圍內始終優於 libaio。

最後,讓我們檢查延遲行為。下圖顯示了隨著 iodepth 增加的延遲百分位數:

下面是 iodepth 範圍 1–16 的縮放視圖:

以下是佇列深度 1、4 和 16 的延遲分佈:

同樣,io_uring 在整個範圍內始終提供比 libaio 更低的延遲。

正如我們之前提到的,我們的調查並不像看起來那麼直接。讓我們分享一下這個故事,一個快樂的結局。

我們從 Linux 核心 5.4、5.15 和 6.6 開始。為了完整起見,我們還測試了 7.0-rc3。這就是我們首次注意到 libaio 和 io_uring 的 IOPS 下降了 30%。起初,這似乎並不令人驚訝:畢竟,這是一個 rc 核心。7.0-rc2 中也有一個已知的退化,可能會延續到 rc3。為了驗證這一點,我們回退到 6.18.18 並看到了相同的顯著效能下降。

在那時,我們的結果看起來像這樣:

我們觀察到 5.4 和 5.15 之間有輕微的退化(僅限 libaio 和非輪詢 io_uring),以及在 6.6.15 和 6.6.20 之間出現更嚴重的下降。

主線 6.6.15 和 6.6.20 之間的差異很小:小的配置更改和略有不同的編譯器版本。不幸的是,我們最初低估了配置更改的影響,而是嘗試使用舊的 GCC 重建 6.6.20。

作為最後一個,有點絕望的步驟,我們禁用了 intel_iommu — 這被證明是退化的根本原因。進一步調查顯示,Ubuntu 從某些 5.15.x 核心開始啟用了 CONFIG_INTEL_IOMMU_DEFAULT_ON。在主線 Ubuntu 核心中,它似乎被切換了:在某個時候被禁用,然後在 6.6.15 和 6.6.20 之間重新啟用。

IOMMU(輸入/輸出記憶體管理單元)會翻譯裝置記憶體訪問並強制執行隔離,從而提高安全性,但有時會在 I/O 密集型工作負載中引入額外的開銷。它對於保護系統至關重要,可以防止裝置訪問任意記憶體,這對於虛擬化和不受信任的外圍裝置尤其重要。

根據我們的經驗,大多數資料庫管理系統運行在不需要嚴格 IOMMU 的受信任環境中。如果需要 PCI 直通或裝置隔離,並且不能禁用 IOMMU,則可以使用 iommu=pt 模式。在此配置中,對於大多數裝置,位址轉換實際上被繞過,從而減少了開銷,同時仍然為必要的隔離啟用 IOMMU。

我們沒有找到任何關於現代設定中 IOMMU 開銷的近期系統性評估。然而,有幾點值得注意的觀察。這裡的作者報告說,IOMMU 在 Ceph 叢集中成為了「核心級別的瓶頸」,禁用它帶來了「顯著的效能提升」。

另一個例子是著名的 VLDB 論文「現代 NVMe 儲存能做什麼,以及如何利用它:高性能儲存引擎的高性能 I/O」。為了充分利用多個 NVMe 裝置,作者在他們的設定中禁用了 IOMMU。

我們自己遇到這個問題的一個好處是,我們能夠直接測量其影響。以下是 Linux 6.6.80 上啟用 IOMMU 與禁用 IOMMU 的比較結果:

從安全角度來看,預設啟用 IOMMU 是一個合理的決定。但對於通常在受信任環境中運行其 DBMS 的資料庫開發者來說,這可能看起來非常像一個退化。責怪作業系統?我們是同道中人 :)

開玩笑說,讚揚核心開發者——非同步 I/O 的進步,特別是 io_uring,令人印象深刻。

我們想強調的還有一個有趣的注意事項。從 Linux 6.8 開始,配置為使用帶有 IOPOLL (–hipri) 的 io_uring 的 fio 開始出現以下錯誤:

事實證明,使用 IOPOLL 需要特定的 NVMe 驅動程式配置。步驟如下:

在發現這一點之前,我們已經在早期核心上進行了多次 IOPOLL 實驗。實際上,我們只觀察到「常規」io_uring 和 IOPOLL 之間有很小的差異,並且在將 SQPOLL 與 IOPOLL 結合時只有適度的增益。

正如 Jens Axboe 在此解釋的:「如果你沒有任何輪詢佇列,帶有 IOCB_HIPRI 的 preadv2 將是基於 IRQ 的,而不是輪詢的。io_uring 會提前用 -EOPNOTSUPP 告訴你這一點。」在舊核心或預設配置上使用 IOPOLL 時,這一點需要記住。因此,我們不得不對所有舊核心重新運行基準測試。

在這篇文章中,我們使用隨機 4K 寫入工作負載比較了 libaio 和 io_uring 在多個 Linux 核心版本中的效能。實驗得出了三個主要結論。

首先,io_uring 的效能在新核心版本上顯著提升。Linux 6.6 上最快的 io_uring 配置比舊核心版本快約 1.4 倍。對於運行舊核心版本的系統,僅升級核心就可能帶來顯著的儲存效能提升,即使沒有應用程式變更。

其次,io_uring 持續優於 libaio。即使是非輪詢、非批次的 io_uring,在所有測試的核心版本中,在 IOPS 和延遲方面都比 libaio 提供了更好的效能。在最佳配置下,io_uring 的 IOPS 比 libaio 高出 2 倍。在我們的實驗中,io_uring 已經將測試的 NVMe 裝置推向了其極限。在更快的儲存硬體上,它相對於 libaio 的優勢可能會更加明顯。

第三,我們觀察到 libaio 和非輪詢 io_uring 在核心版本 5.4 和 5.15 之間出現了效能退化。由於這種影響出現在多個 I/O 介面中,它很可能源於區塊層或 NVMe 驅動程式,而不是 I/O API 本身。輪詢 io_uring 模式的改進可能會部分掩蓋這些模式的退化。

同時,配置很重要。特別是,IOMMU 會顯著影響 I/O 效能,在比較不同核心版本之間的結果時應明確考慮。在我們的案例中,Intel IOMMU 在核心版本發布之間預設開啟,導致嚴重的效能退化,IOPS 下降高達 30%。

最後,除了 fio,我們還使用 YDB 的一個組件驗證了這些發現,並觀察到非常相似的結果,這表明這些結論適用於實際工作負載。這些見解也與其他依賴現代 Linux I/O 堆疊的資料庫系統相關,包括 PostgreSQL 等系統,其中 io_uring 的支援正在積極發展。