在正文開始之前,我們先來看一段對話(演繹版本)
Jens Axboe :Linus,我有個好東西,你瞅瞅?
Linus:啥玩意兒,不是已經有 aio 了么,為啥又來一套,你咋不去好好修 aio 的問題。aio 還有 balabala 問題沒有修呢。Jens Axboe :我這個 DIAO 啊,balabalaLinus :看你誠意這麼足,那行吧,我先收到我的 tree 下,不 push out 出去,讓我測測先。
【不一會兒】
Linus :你這 IO 引用計數寫的辣雞,一看就有問題,去改吧。……
背景
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 77K
io_uring 2 0 8.2usec 183K
io_uring 4 0 8.4usec 383K
io_uring 8 0 13.3usec 449K
libaio 1 0 9.7usec 74K
libaio 2 0 8.5usec 181K
libaio 4 0 8.5usec 373K
libaio 8 0 15.4usec 402K
io_uring 1 1 6.1usec 139K
io_uring 2 1 6.1usec 272K
io_uring 4 1 6.3usec 519K
io_uring 8 1 11.5usec 592K
spdk 1 1 6.1usec 151K
spdk 2 1 6.2usec 293K
spdk 4 1 6.7usec 536K
spdk 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 1620K
libaio 128 0 608K
spdk 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_setup
426 io_uring_enter
427 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)數組。