AIO 的新歸宿:io_uring - 儲存人的福音

背景

Linus 和 Jens 在討論的,就是 Linux Kernel 即將在 5。1 版本加入一個重大 feature:io_uring。

對做儲存的來說,這是一個大事情,值得普大喜奔,廣而告之。libaio 即將埋入黃土,io_uring 拔地而起。

一句話總結 io_uring 就是:一套全新的 syscall,一套全新的 async API,更高的效能,更好的相容性,來迎接高 IOPS,高吞吐量的未來。

先看一下效能資料(資料來自 Jens Axboe)。

4k randread,3D Xpoint 盤:

Interface QD Polled Latency IOPS——————————————————————————————————————io_uring 1 0 9。5usec 77Kio_uring 2 0 8。2usec 183Kio_uring 4 0 8。4usec 383Kio_uring 8 0 13。3usec 449Klibaio 1 0 9。7usec 74Klibaio 2 0 8。5usec 181Klibaio 4 0 8。5usec 373Klibaio 8 0 15。4usec 402Kio_uring 1 1 6。1usec 139Kio_uring 2 1 6。1usec 272K io_uring 4 1 6。3usec 519Kio_uring 8 1 11。5usec 592Kspdk 1 1 6。1usec 151Kspdk 2 1 6。2usec 293Kspdk 4 1 6。7usec 536Kspdk 8 1 12。6usec 586K

io_uring vs libaio,在非 polling 模式下,io_uring 效能提升不到 10%,好像並沒有什麼了不起的地方。

然而 io_uring 提供了 polling 模式。在 polling 模式下,io_uring 和 SPDK 的效能非常接近,特別是高 QueueDepth 下,io_uring 有趕超的架勢,同時完爆 libaio。

測試 per-core,4k randread 多裝置下的最高 IOPS 能力:

Interface QD Polled IOPS——————————————————————————————————————io_uring 128 1 1620Klibaio 128 0 608Kspdk 128 1 1739K

最近幾年一直流行 kernel bypass,從網路到儲存,各個領域開花,核心在效能方面被各種詬病。io_uring 出現以後,算是扳回一局。

io_uring 有如此出眾的效能,主要來源於以下幾個方面:

使用者態和核心態共享提交佇列(submission queue)和完成佇列(completion queue)

IO 提交和收割可以 offload 給 Kernel,且提交和完成不需要經過系統呼叫(system call)

支援 Block 層的 Polling 模式

透過提前註冊使用者態記憶體地址,減少地址對映的開銷

不僅如此,io_uring 還可以完美支援 buffered IO,而 libaio 對於 buffered IO 的支援則一直是被詬病的地方。

io_uring

io_uring 提供了一套新的系統呼叫,應用程式可以使用兩個佇列,Submission Queue(SQ) 和 Completion Queue(CQ) 來和 Kernel 進行通訊。這種方式類似 RDMA 或者 NVMe 的方式,可以高效處理 IO。

syscall 425 io_uring_setup426 io_uring_enter427 io_uring_register

io_uring 準備階段

io_uring_setup 需要兩個引數,entries 和 io_uring_params。

其中 entries,代表 queue depth。

io_uring_params 的定義如下。

struct io_uring_params { __u32 sq_entries; __u32 cq_entries; __u32 flags; __u32 sq_thread_cpu; __u32 sq_thread_idle; __u32 resv[5]; struct io_sqring_offsets sq_off; struct io_cqring_offsets cq_off;};struct io_sqring_offsets { __u32 head; __u32 tail; __u32 ring_mask; __u32 ring_entries; __u32 flags; __u32 dropped; __u32 array; __u32 resv1; __u64 resv2;};struct io_cqring_offsets { __u32 head; __u32 tail; __u32 ring_mask; __u32 ring_entries; __u32 overflow; __u32 cqes; __u64 resv[2];};

其中,flags、sq_thread_cpu、sq_thread_idle 屬於輸入引數,用於定義 io_uring 在核心中的行為。其他引數屬於輸出引數,由核心負責設定。

在 io_setup 返回的時候,核心已經初始化好了 SQ 和 CQ,此外,還有核心還提供了一個 Submission Queue Entries(SQEs)陣列。

AIO 的新歸宿:io_uring - 儲存人的福音

之所以額外採用了一個數組儲存 SQEs,是為了方便透過 RingBuffer 提交記憶體上不連續的請求。SQ 和 CQ 中每個節點儲存的都是 SQEs 陣列的偏移量,而不是實際的請求,實際的請求只儲存在 SQEs 陣列中。這樣在提交請求時,就可以批次提交一組 SQEs 上不連續的請求。

但由於 SQ,CQ,SQEs 是在核心中分配的,所以使用者態程式並不能直接訪問。io_setup 的返回值是一個 fd,應用程式使用這個 fd 進行 mmap,和 kernel 共享一塊記憶體。

這塊記憶體共分為三個區域,分別是 SQ,CQ,SQEs。kernel 返回的 io_sqring_offset 和 io_cqring_offset 分別描述了 SQ 和 CQ 的指標在 mmap 中的 offset。而 SQEs 則直接對應了 mmap 中的 SQEs 區域。

mmap 的時候需要傳入 MAP_POPULATE 引數,以防止記憶體被 page fault。

IO 提交

IO 提交的做法是找到一個空閒的 SQE,根據請求設定 SQE,並將這個 SQE 的索引放到 SQ 中。SQ 是一個典型的 RingBuffer,有 head,tail 兩個成員,如果 head == tail,意味著佇列為空。SQE 設定完成後,需要修改 SQ 的 tail,以表示向 RingBuffer 中插入一個請求。

當所有請求都加入 SQ 後,就可以使用 :

int io_uring_enter(unsigned int fd, u32 to_submit, u32 min_complete, u32 flags);

來提交 IO 請求。

io_uring_enter 被呼叫後會陷入到核心,核心將 SQ 中的請求提交給 Block 層。to_submit 表示一次提交多少個 IO。

如果 flags 設定了 IORING_ENTER_GETEVENTS,並且 min_complete > 0,那麼這個系統呼叫會同時處理 IO 收割。這個系統呼叫會一直 block,直到 min_complete 個 IO 已經完成。

這個流程貌似和 libaio 沒有什麼區別,IO 提交的過程中依然會產生系統呼叫。

但 io_uring 的精髓在於,提供了 submission offload 模式,使得提交過程完全不需要進行系統呼叫。

如果在呼叫 io_uring_setup 時設定了 IORING_SETUP_SQPOLL 的 flag,核心會額外啟動一個核心執行緒,我們稱作 SQ 執行緒。這個核心執行緒可以執行在某個指定的 core 上(透過 sq_thread_cpu 配置)。這個核心執行緒會不停的 Poll SQ,除非在一段時間內沒有 Poll 到任何請求(透過 sq_thread_idle 配置),才會被掛起。

AIO 的新歸宿:io_uring - 儲存人的福音

當程式在使用者態設定完 SQE,並透過修改 SQ 的 tail 完成一次插入時,如果此時 SQ 執行緒處於喚醒狀態,那麼可以立刻捕獲到這次提交,這樣就避免了使用者程式呼叫 io_uring_enter 這個系統呼叫。如果 SQ 執行緒處於休眠狀態,則需要透過呼叫 io_uring_enter,並使用 IORING_SQ_NEED_WAKEUP 引數,來喚醒 SQ 執行緒。使用者態可以透過 sqring 的 flags 變數獲取 SQ 執行緒的狀態。

IO 收割

當 IO 完成時,核心負責將完成 IO 在 SQEs 中的 index 放到 CQ 中。由於 IO 在提交的時候可以順便返回完成的 IO,所以收割 IO 不需要額外系統呼叫。這是跟 libaio 比較大的不同,省去了一次系統呼叫。

如果使用了 IORING_SETUP_SQPOLL 引數,IO 收割也不需要系統呼叫的參與。由於核心和使用者態共享記憶體,所以收割的時候,使用者態遍歷 [cring->head, cring->tail) 區間,這是已經完成的 IO 佇列,然後找到相應的 CQE 並進行處理,最後移動 head 指標到 tail,IO 收割就到此結束了。

由於提交和收割的時候需要訪問共享記憶體的 head,tail 指標,所以需要使用 rmb/wmb 記憶體屏障操作確保時序。

所以在最理想的情況下,IO 提交和收割都不需要使用系統呼叫。

其它高階特性

io_uring 支援還支援以下特性。

IORING_REGISTER_FILES

這個的用途是避免每次 IO 對檔案做 fget/fput 操作,當批次 IO 的時候,這組原子操作可以避免掉。

IORING_SETUP_IOPOLL

這個功能讓核心採用 Polling 的模式收割 Block 層的請求。當沒有使用 SQ 執行緒時,io_uring_enter 函式會主動的 Poll,以檢查提交給 Block 層的請求是否已經完成,而不是掛起,並等待 Block 層完成後再被喚醒。使用 SQ 執行緒時也是同理。

透過 perf 可以看到,當使用 IOPOLL 時,88% 的 CPU 時間花費在呼叫 blkdev_iopoll 和 blk_poll 上。

AIO 的新歸宿:io_uring - 儲存人的福音

IORING_REGISTER_BUFFERS

如果應用提交到核心的虛擬記憶體地址是固定的,那麼可以提前完成虛擬地址到物理 pages 的對映,避免在 IO 路徑上進行轉換,從而最佳化效能。用法是,在 setup io_uring 之後,呼叫 io_uring_register,傳遞 IORING_REGISTER_BUFFERS 作為 opcode,引數是一個指向 iovec 的陣列,表示這些地址需要 map 到核心。在做 IO 的時候,使用帶 FIXED 版本的opcode(IORING_OP_READ_FIXED /IORING_OP_WRITE_FIXED)來操作 IO 即可。

核心在處理 IORING_REGISTER_BUFFERS 時,提前使用 get_user_pages 來獲得 userspace 虛擬地址對應的物理 pages。在做 IO 的時候,如果提交的虛擬地址曾經被註冊過,那麼就免去了虛擬地址到 pages 的轉換。

下面是兩個版本的 perf 資料。

帶 fixed buffer:

AIO 的新歸宿:io_uring - 儲存人的福音

不帶 fixed buffer:

AIO 的新歸宿:io_uring - 儲存人的福音

可以明顯看到,提前 map pages,可以減少 iov_iter_get_pages 7% 的 CPU 時間消耗。

關於名字

取名一直是一個老大難的問題,io_uring 這個名字,有點意思,社群有人吐槽,看起來像 io urine(*不是一個很好的詞*),太歡樂了。

有人說可以叫做 aio_ring,io_ring,ring_io。

總結

io_uring 的介面雖然簡單,但操作起來有些複雜,需要手動 mmap 來對映記憶體。可以看到,io_uring 是完全為效能而生的新一代 native async IO 模型,比 libaio 高階不少。透過全新的設計,共享記憶體,IO 過程不需要系統呼叫,由核心完成 IO 的提交, 以及 IO completion polling 機制,實現了高IOPS,高 Bandwidth。相比 kernel bypass,這種 native 的方式顯得友好一些。

當然,不可否認,aio 也在與時俱進。自從 kernel 2。5 進入 upstream 以來,aio 一直都沒有實現完整。 aio 對 Direct IO 支援的很好,但是其他的 IO 型別支援的不完善。嘗試使用其他型別的 IO,例如 buffered IO,可能導致同步的行為。polling 也是一個方向,最近 aio 的 polling 機制已經實現,感興趣的可以嘗試一下。

參考

https://lore。kernel。org/linux-block/20190116175003。17880-1-axboe@kernel。dk/

https://lwn。net/ml/linux-fsdevel/20190109160036。GK6310@bombadil。infradead。org/

http://git。kernel。dk/cgit/fio/plain/t/io_uring。c

https://lore。kernel。org/linux-block/20190211190049。7888-14-axboe@kernel。dk/

https://lwn。net/Articles/743714/

io_uring_setup。2\man - liburing - io_uring library