Linux內(nèi)核5.1支持了新的異步IO框架iouring,由Block IO大神也即Fio作者Jens Axboe開發(fā),意在提供一套公用的網(wǎng)絡(luò)和磁盤異步IO,不過io_uring目前在磁盤方面要比網(wǎng)絡(luò)方面更加成熟。
目錄
背景簡介
熟悉Linux系統(tǒng)編程的同學(xué)都清楚,Linux并沒有提供完善的異步IO(網(wǎng)絡(luò)IO、磁盤IO)機制。
在網(wǎng)絡(luò)編程中,我們通常使用epoll IO多路復(fù)用來處理網(wǎng)絡(luò)IO,然而epoll也并不是異步網(wǎng)絡(luò)IO,僅僅是內(nèi)核提供了IO復(fù)用機制,epoll回調(diào)通知的是數(shù)據(jù)可以讀取或者寫入了,具體的讀寫操作仍然需要用戶去做,而不是內(nèi)核代替完成。
在存儲IO棧中,做存儲的同學(xué)大都使用過libaio,然而那是一個巨難用啊Linux AIO這個奇葩。首先只能在DIO下使用,用不了pagecache;其次用戶的數(shù)據(jù)地址空間起始地址和大小必須頁大小對齊;然后在submit_io時仍然可能因為文件系統(tǒng)、pagecache、sync發(fā)生阻塞,除此之外,我們在使用libaio的時候會設(shè)置io_depth的大小,還可能因為內(nèi)核的/sys/block/sda/queue/nr_requests(128)設(shè)置的過小而發(fā)生阻塞;而且libaio提供的sync命令關(guān)鍵還不起作用,想要sync數(shù)據(jù)還得依賴fsync/fdatasync,真的是心塞塞,libaio想說愛你不容易啊。
所以Linux迫切需要一個完善的異步機制。同時在Linux平臺上跑的大多數(shù)程序都是專用程序,并不需要內(nèi)核的大多數(shù)功能,而且這幾年也流行kernel bypass,intel也發(fā)起的用戶態(tài)IO DPDK、SPDK。但是這些用戶態(tài)IO API不統(tǒng)一,使用成本過高,所以內(nèi)核便推出了io_uring來統(tǒng)一網(wǎng)絡(luò)和磁盤的異步IO,提供一套統(tǒng)一完善的異步API,也支持異步、輪詢、無鎖、zero copy。真的是姍姍來遲啊,不過也算是在高性能IO方面也算是是扳回了一城。
io_uring
io_uring的設(shè)計目標(biāo)是提供一個統(tǒng)一、易用、可擴展、功能豐富、高效的網(wǎng)絡(luò)和磁盤系統(tǒng)接口。其高性能依賴于以下幾個方面:
- 用戶態(tài)和內(nèi)核態(tài)共享提交隊列(submission queue)和完成隊列(completion queue)。
- 用戶態(tài)支持Polling模式,不依賴硬件的中斷,通過調(diào)用IORING_ENTER_GETEVENTS不斷輪詢收割完成事件。
- 內(nèi)核態(tài)支持Polling模式,IO 提交和收割可以 offload 給 Kernel,且提交和完成不需要經(jīng)過系統(tǒng)調(diào)用(system call)。
- 在DirectIO下可以提前注冊用戶態(tài)內(nèi)存地址,減小地址映射的開銷。
系統(tǒng)API
io_uring提供了3個系統(tǒng)調(diào)用API,雖然只有3個,但是直接使用起來還是蠻復(fù)雜的。
- io_uring_setup
entries:queue depth,表示隊列深度。
io_uring_params:初始化時候的參數(shù)。
在io_uring_setup返回的時候就已經(jīng)初始化好了 SQ 和 CQ,此外,還有內(nèi)核還提供了一個 Submission Queue Entries(SQEs)數(shù)組。
之所以額外采用了一個數(shù)組保存 SQEs,是為了方便通過 RingBuffer 提交內(nèi)存上不連續(xù)的請求。SQ 和 CQ 中每個節(jié)點保存的都是 SQEs 數(shù)組的偏移量,而不是實際的請求,實際的請求只保存在 SQEs 數(shù)組中。這樣在提交請求時,就可以批量提交一組 SQEs 上不連續(xù)的請求。 但由于 SQ,CQ,SQEs 是在內(nèi)核中分配的,所以用戶態(tài)程序并不能直接訪問。io_setup 的返回值是一個 fd,應(yīng)用程序使用這個 fd 進行 mmap,和 kernel 共享一塊內(nèi)存。 這塊內(nèi)存共分為三個區(qū)域,分別是 SQ,CQ,SQEs。kernel 返回的 io_sqring_offset 和 io_cqring_offset 分別描述了 SQ 和 CQ 的指針在 mmap 中的 offset。而 SQEs 則直接對應(yīng)了 mmap 中的 SQEs 區(qū)域。 mmap 的時候需要傳入 MAP_POPULATE 參數(shù),以防止內(nèi)存被 page fault。
- io_uring_enter
io_uring_enter即可以提交io,也可以來收割完成的IO,一般IO完成時內(nèi)核會自動將SQE 的索引放入到CQ中,用戶可以遍歷CQ來處理完成的IO。
IO 提交的做法是找到一個空閑的 SQE,根據(jù)請求設(shè)置 SQE,并將這個 SQE 的索引放到 SQ 中。SQ 是一個典型的 RingBuffer,有 head,tail 兩個成員,如果 head == tail,意味著隊列為空。SQE 設(shè)置完成后,需要修改 SQ 的 tail,以表示向 RingBuffer 中插入一個請求。
io_uring_enter 被調(diào)用后會陷入到內(nèi)核,內(nèi)核將 SQ 中的請求提交給 Block 層。to_submit 表示一次提交多少個 IO。
如果 flags 設(shè)置了 IORING_ENTER_GETEVENTS,并且 min_complete > 0,那么這個系統(tǒng)調(diào)用會同時處理 IO 收割。這個系統(tǒng)調(diào)用會一直 block,直到 min_complete 個 IO 已經(jīng)完成。
這個流程貌似和 libaio 沒有什么區(qū)別,IO 提交的過程中依然會產(chǎn)生系統(tǒng)調(diào)用。
但 io_uring 的精髓在于,提供了 submission offload 模式,使得提交過程完全不需要進行系統(tǒng)調(diào)用。
如果在調(diào)用 io_uring_setup 時設(shè)置了 IORING_SETUP_SQPOLL 的 flag,內(nèi)核會額外啟動一個內(nèi)核線程,我們稱作 SQ 線程。這個內(nèi)核線程可以運行在某個指定的 core 上(通過 sq_thread_cpu 配置)。這個內(nèi)核線程會不停的 Poll SQ,除非在一段時間內(nèi)沒有 Poll 到任何請求(通過 sq_thread_idle 配置),才會被掛起。
當(dāng)程序在用戶態(tài)設(shè)置完 SQE,并通過修改 SQ 的 tail 完成一次插入時,如果此時 SQ 線程處于喚醒狀態(tài),那么可以立刻捕獲到這次提交,這樣就避免了用戶程序調(diào)用 io_uring_enter 這個系統(tǒng)調(diào)用。如果 SQ 線程處于休眠狀態(tài),則需要通過調(diào)用 io_uring_enter,并使用 IORING_SQ_NEED_WAKEUP 參數(shù),來喚醒 SQ 線程。用戶態(tài)可以通過 sqring 的 flags 變量獲取 SQ 線程的狀態(tài)。
if (IO_URING_READ_ONCE(*ring->sq.kflags) & IORING_SQ_NEED_WAKEUP) {
*flags |= IORING_ENTER_SQ_WAKEUP;
return true;
}
- io_uring_register
主要包含IORING_REGISTER_FILES、IORING_REGISTER_BUFFERS,在高級特性章節(jié)會描述。
liburing
我們知道io_uring雖然僅僅提供了3個系統(tǒng)API,但是想要用好還是有一定難度的,所提fio大神本人封裝了一個Liburing,簡化了io_uring的使用,通過使用liburing,我們很容易寫出異步IO程序。
代碼位置:github.com/axboe/liburi,在使用的時候目前仍然需要拉取代碼,自己編譯,估計之后將會融入內(nèi)核,在用戶程序中需要包含#include "liburing.h"。
列舉一些比較常用的封裝的API:github.com/axboe/liburi
extern int io_uring_queue_init(unsigned entries, struct io_uring *ring, unsigned flags);
// 非系統(tǒng)調(diào)用,清理io_uring
extern void io_uring_queue_exit(struct io_uring *ring);
// 非系統(tǒng)調(diào)用,獲取一個可用的 submit_queue_entry,用來提交IO
extern struct io_uring_sqe *io_uring_get_sqe(struct io_uring *ring);
// 非系統(tǒng)調(diào)用,準(zhǔn)備階段,和libaio封裝的io_prep_writev一樣
static inline void io_uring_prep_writev(struct io_uring_sqe *sqe, int fd,const struct iovec *iovecs, unsigned nr_vecs, off_t offset)
// 非系統(tǒng)調(diào)用,準(zhǔn)備階段,和libaio封裝的io_prep_readv一樣
static inline void io_uring_prep_readv(struct io_uring_sqe *sqe, int fd, const struct iovec *iovecs, unsigned nr_vecs, off_t offset)
// 非系統(tǒng)調(diào)用,把準(zhǔn)備階段準(zhǔn)備的data放進 submit_queue_entry
static inline void io_uring_sqe_set_data(struct io_uring_sqe *sqe, void *data)
// 非系統(tǒng)調(diào)用,設(shè)置submit_queue_entry的flag
static inline void io_uring_sqe_set_flags(struct io_uring_sqe *sqe, unsigned flags)
// 非系統(tǒng)調(diào)用,提交sq的entry,不會阻塞等到其完成,內(nèi)核在其完成后會自動將sqe的偏移信息加入到cq,在提交時需要加鎖
extern int io_uring_submit(struct io_uring *ring);
// 非系統(tǒng)調(diào)用,提交sq的entry,阻塞等到其完成,在提交時需要加鎖。
extern int io_uring_submit_and_wait(struct io_uring *ring, unsigned wait_nr);
// 非系統(tǒng)調(diào)用 宏定義,會遍歷cq從head到tail,來處理完成的IO
#define io_uring_for_each_cqe(ring, head, cqe)
// 非系統(tǒng)調(diào)用 遍歷時,可以獲取cqe的data
static inline void *io_uring_cqe_get_data(const struct io_uring_cqe *cqe)
// 非系統(tǒng)調(diào)用 遍歷完成時,需要調(diào)整head往后移nr
static inline void io_uring_cq_advance(struct io_uring *ring, unsigned nr)
高級特性
io_uring里面提供了polling機制:IORING_SETUP_IOPOLL可以讓內(nèi)核采用 Polling 的模式收割 Block 層的請求;IORING_SETUP_SQPOLL可以讓內(nèi)核新起線程輪詢提交sq的entry。
IORING_REGISTER_FILES
這個的用途是避免每次 IO 對文件做 fget/fput 操作,當(dāng)批量 IO 的時候,這組原子操作可以避免掉。
IORING_REGISTER_BUFFERS
如果應(yīng)用提交到內(nèi)核的虛擬內(nèi)存地址是固定的,那么可以提前完成虛擬地址到物理 pages 的映射,避免在 IO 路徑上進行轉(zhuǎn)換,從而優(yōu)化性能。用法是,在 setup io_uring 之后,調(diào)用 io_uring_register,傳遞 IORING_REGISTER_BUFFERS 作為 opcode,參數(shù)是一個指向 iovec 的數(shù)組,表示這些地址需要 map 到內(nèi)核。在做 IO 的時候,使用帶 FIXED 版本的opcode(IORING_OP_READ_FIXED /IORING_OP_WRITE_FIXED)來操作 IO 即可。
內(nèi)核在處理 IORING_REGISTER_BUFFERS 時,提前使用 get_user_pages 來獲得 userspace 虛擬地址對應(yīng)的物理 pages。在做 IO 的時候,如果提交的虛擬地址曾經(jīng)被注冊過,那么就免去了虛擬地址到 pages 的轉(zhuǎn)換。
IORING_SETUP_IOPOLL
這個功能讓內(nèi)核采用 Polling 的模式收割 Block 層的請求。當(dāng)沒有使用 SQ 線程時,io_uring_enter 函數(shù)會主動的 Poll,以檢查提交給 Block 層的請求是否已經(jīng)完成,而不是掛起,并等待 Block 層完成后再被喚醒。使用 SQ 線程時也是同理。
編程示例
通過liburing使用起來還是比較方便的,不用操心內(nèi)核的一些事情,簡直爽歪歪啊。具體可參考ceph:github.com/ceph/ceph/bl
- io_uring_queue_init 來初始化 io_uring。IORING_SETUP_IOPOLL / IORING_SETUP_SQPOLL。
- io_uring_submit 來提交 IO,在這個函數(shù)里面會判斷是否需要調(diào)用系統(tǒng)調(diào)用io_uring_enter。設(shè)置了IORING_SETUP_SQPOLL則不需要調(diào)用,沒有設(shè)置則需要用戶調(diào)用。
- io_uring_for_each_cqe 來收割完成的IO,這是一個for循環(huán)宏定義,后面直接跟 {} 就可以。
性能對比
intel團隊測試結(jié)果
可以看出來intel自己測試的結(jié)果表明延遲方面spdk比io_uring要低60%。使用了自己帶的perf的測試工具測的。
fio作者測試結(jié)果
4k randread,3D Xpoint 盤:
io_uring vs libaio,在非 polling 模式下,io_uring 性能提升不到 10%,好像并沒有什么了不起的地方。
然而 io_uring 提供了 polling 模式。在 polling 模式下,io_uring 和 SPDK 的性能非常接近,特別是高 QueueDepth 下,io_uring 有趕超的架勢,同時完爆 libaio。
模式對比
項目 | io_uring | spdk |
---|---|---|
驅(qū)動程序 | 內(nèi)核態(tài)驅(qū)動程序有鎖 | 用戶態(tài)驅(qū)動程序、無鎖、輪詢、線程綁定 |
run_to_completion | 非rtc模型,可能會有上下文切換? | rtc模型,單線程擼到底 |
內(nèi)存管理 | mmu、4k | 2MB大頁 |
提交任務(wù)有無鎖 | 無鎖 | 無鎖 |
系統(tǒng)調(diào)用 | 可有可無 | 無系統(tǒng)調(diào)用 |
用戶內(nèi)核態(tài)切換 | 輕量級的 | 無內(nèi)核切換 |
poll模型 | 可選 | polling |
線上應(yīng)用
目前發(fā)現(xiàn)已經(jīng)有幾個項目在做嘗試性的應(yīng)用:rocksdb、ceph、spdk、第三方適配(nginx、redis、echo_server)
rocksdb
rocksdb官方實現(xiàn)了PosixRandomAccessFile::MultiRead()使用io_uring。
除此之外,tikv擴展了一些實現(xiàn):openinx.github.io/ppt/i
- wal和sstbale的寫入使用io_uring,但是測完之后性能提升不明顯。
- compaction file write的時間降低了一半。
- 可用io_uring優(yōu)化的點:參考 Conclusion & Future work 章節(jié)。
spdk
SPDK與io_uring新異步IO機制,在其抽象的通用塊層加入了io_uring的支持。
ceph
ceph的io_uring主要使用在block_device,抽象出了統(tǒng)一的塊設(shè)備,直接操作裸設(shè)備,對上層提供統(tǒng)一的讀寫方法。
bluefs僅僅需要提供append only的寫入即可,不需要提供隨機寫,大大簡化了bluefs的實現(xiàn)。
第三方適配(nginx、redis、echo_server)
第三方io_uring適配(nginx、redis、echo_server)性能測試結(jié)果:
redis:
以下是 redis 在 event poll 和 io_uring 下的 qps 對比:
- 高負載情況下,io_uring 相比 event poll,吞吐提升 8%~11%。
- 開啟 sqpoll 時,吞吐提升 24%~32%。這里讀者可能會有個疑問,開啟 sqpoll 額外使用了一個 CPU,性能為什么才提升 30% 左右?那是因為 redis 運行時同步讀寫就消耗了 70% 以上的 CPU,而 sq_thread 只能使用一個 CPU 的能力,把讀寫工作交給 sq_thread 之后,理論上 QPS 最多能提升 40% 左右(1/0.7 - 1 = 0.42),再加上 sq_thread 還需要處理中斷以及本身的開銷,因此只能有 30% 左右的提升。
nginx:
- 單 worker 場景,當(dāng)連接數(shù)超過 500 時,QPS提升 20% 以上。
- connection 固定 1000,worker 數(shù)目在 8 以下時,QPS 有 20% 左右的提升。隨著 worker 數(shù)目增大,收益逐漸降低。
- 短連接場景,io uring 相對于 event poll 非但沒有提升,甚至在某些場景下有 5%~10% 的性能下降。究其原因,除了 io uring 框架本身帶來的開銷以外,還可能跟 io uring 編程模式下請求批量下發(fā)帶來的延遲有關(guān)。
-
Linux
+關(guān)注
關(guān)注
87文章
11310瀏覽量
209615 -
API
+關(guān)注
關(guān)注
2文章
1502瀏覽量
62078 -
框架
+關(guān)注
關(guān)注
0文章
403瀏覽量
17502 -
編程
+關(guān)注
關(guān)注
88文章
3616瀏覽量
93763 -
磁盤
+關(guān)注
關(guān)注
1文章
379瀏覽量
25209
發(fā)布評論請先 登錄
相關(guān)推薦
評論