关于 linux io_uring 性能测试 及其 实现原理的一些探索
文章目錄
- 先看看性能
- AIO 的基本實現
- io_ring 使用
- io_uring 基本接口
- liburing 的使用
- io_uring 非poll 模式下 的實現
- io_uring poll模式下的實現
- io_uring 在 rocksdb 中的應用
- 總結
- 參考
先看看性能
io_uring 需要內核版本在5.1 及以上才支持,liburing的編譯安裝 很簡單,直接clone 官方的代碼,sudo make && sudo make install 就好了,本文是在內核5.12版本 上測試的。
在描述io_uring 的性能之前,我們先直接看一組實測數據:
這組數據是在3D XPoint 介質的硬盤 : optane-5800上測試的,optane5800 能夠提供(randread100% 150w/s , randwrite 100% 150w/s)的性能。
| Direct | Depth | Jobs | Qps | Disk Width | Latency(avg,99,995,9999 us) | |
|---|---|---|---|---|---|---|
| AIO - RandRead(4K) | 1 | 1 | 1 | 11.2w/s | 436M | 8:9:10:18 |
| AIO - RandRead(4K) | 1 | 32 | 1 | 29w/s | 1.1G | 109:112:112:115 |
| AIO - RandRead(4K) | 1 | 128 | 1 | 29w/s | 1.1G | 439:441:445:465 |
| AIO - RandRead(4K) | 1 | 256 | 1 | 29w/s | 1.1G | 881:889:898:938 |
| URING-RandRead(4K) | 1 | 1 | 1 | 11.2w/s | 437M | 8:8:9:18 |
| URING-RandRead(4K) | 1 | 32 | 1 | 30.1w/s | 1.1G | 106:108:108:110 |
| URING-RandRead(4K) | 1 | 128 | 1 | 30.1w/s | 1.1G | 426:429:429:498 |
| URING-RandRead(4K) | 1 | 256 | 1 | 30w/s | 1.1G | 854:857:857:1004 |
| URING-RandRead(4K) sq_poll | 1 | 1 | 1 | 13.5w/s | 527M | 7:7:8:18 |
| URING-RandRead(4K) sq_poll | 1 | 32 | 1 | 49.4w/s | 1.9G | 64:70:72:83 |
| URING-RandRead(4K) sq_poll | 1 | 128 | 1 | 49.4w/s | 1.9G | 259:281:281:314 |
| URING-RandRead(4K) sq_poll | 1 | 256 | 1 | 49.1w/s | 1.9G | 521:545:545:652 |
| URING-RandRead(4K) sq_poll | 0 | 1 | 1 | 9.8w/s | 383M | 8:11:12:20 |
| URING-RandRead(4K) sq_poll | 0 | 32 | 1 | 26w/s | 0.99G | 121:174:178:1158 |
| URING-RandRead(4K) sq_poll | 0 | 128 | 1 | 25.1w/s | 976M | 507:742:750:116917 |
進行測試的fio 腳本如下:
# aio
[global]
ioengine=libaio
direct=0
randrepeat=1
threads=8
runtime=15
time_based
size=1G
directory=../test-data
group_reporting
[read256B-rand]
bs=4096
rw=randread
numjobs=1
iodepth=128# io_uring
[global]
ioengine=io_uring
sqthread_poll=1 #開啟io_uring sq_poll模式
direct=1
randrepeat=1
threads=8
runtime=15
time_based
size=1G
directory=../test-data
group_reporting
[read256B-rand]
bs=4096
rw=randread
numjobs=1
iodepth=128
通過上面的測試,我們能夠得到如下幾個結論:
- 這種高隊列深度的測試下,可以看到io_uring 在開啟sq_poll之后的性能 相比于aio 的高隊列深度的處理能力好接近一倍;
- 在較低隊列深度 以及不開啟 sq_poll 模式的情況下,io_uring 整體沒有太大的優勢,或者說一樣的性能。
- 在buffer I/O (direct=0) 下,io_uring 也不會有太大的優勢,因為都得通過 os-cache 來操作。
需要注意的是,如果aio和io_uring 在高并發下(jobs 的數目不斷增加),都是可以達到當前磁盤的性能瓶頸的。
AIO 的基本實現
那有這樣的測試現象,我們可能會有一些疑問,就這性能?我們在nvme上做軟件,希望發揮的是整個磁盤的性能,而不是比拼誰的隊列深度大,誰的優勢更大。。。 我用aio 做batch 也能達到磁盤的性能瓶頸,為什么要選擇 對于數據庫/存儲 領域來說 好像“如日中天”的io_uring呢。
我們先來看看aio 的大體實現,沒有涉及到源代碼。
aio 主要提供了三個系統調用:
io_setup初始化一些內核態的數據結構io_submit用于用戶態提交io 請求io_getevents用于io 請求處理完成之后的io 收割
大體的IO調度過程如下:
- io_setup 完成一些內核數據結構的初始化(包括內核中的 aio 調度隊列,aio_ring_info 的ring-buffer緩沖區)
- 用戶態構造一批io請求,通過io_submit 拷貝請求到內核態io 隊列(文件系統之上,上圖沒有體現出來)之后返回到用戶態。
- 內核態繼續通過內核i/o 棧處理io請求,處理完成之后 通過 aio_complete 函數將處理完成的請求放入到 aio_ring_info,每一個io請求是一個io_event。
- 用戶態通過 io_getevents 系統調用 從 aio_ring_info(ring-buffer) 的head 拿處理完成的io_event,如果head==tail,則表示這個ring-buffer是空的。拿到之后,將拿到的io_event 一批從內核態拷貝到用戶態。
如果單純看 誰能將磁盤性能完整發揮出來,那毋庸置疑,大家都可以;那為什么做存儲的對io_uring 的出現如此熱衷呢?我們就得結合實際的應用場景來看看兩者之間的差異了:
-
使用AIO的話,請求調度都需要直接由通用塊層來調度處理,所以需要
O_DIRECT標記。這就意味著,使用AIO的應用都無法享受os cache,這對與存儲應用來說并不友好,cache都得自己來維護,而且顯然沒有os page-cache性能以及穩定性有優勢。而使用io_uring 則沒有這樣的限制,當然,io_uring在 buffer I/O下顯然沒有太大的優勢。
-
延時上的開銷。AIO 提交用戶請求的時候 通過
io_submit調用,收割用戶請求的時候通過io_getevents,正常應用的時候每一個請求都意味著至少兩次系統調用(I/O提交和I/O收割),而對于io_uring來說,I/O 提交和I/O收割都可以 offload 給內核。這樣相比于AIO 來說,io_uring能夠極大得減少 系統調用引入的上下文切換。 -
io_uring 能夠支持針對submit queue的polling,啟動一個內核線程進行polling,加速請求的提交和收割;對于aio來說,這里就沒有這樣的機制。
總的來說,io_uring 能夠保證上層應用 對系統資源(cache)正常使用的同時 ,降低應用 下發的請求延時和CPU的開銷,在單實例高隊深下,能夠顯著優于同等隊深下的AIO性能。
io_ring 使用
io_uring 基本接口
io_uring的用戶態API 提供了三個系統調用,io_uring_setup,io_uring_enter,io_uring_register。
-
int io_uring_setup(u32 entries, struct io_uring_params *p);這個接口 用于創建 擁有entries個請求的 提交隊列(SQ) 和 完成隊列(CQ),并且返回給用戶一個fd。這個fd可以用做在同一個uring實例上 用戶空間和內核空間共享sq和cq 隊列,這樣能夠避免在請求完成時不需要從完成隊列拷貝數據到用戶態了。io_uring_params主要是根據用戶的配置來設置uring 實例的創建行為。包括 單不限于開啟IORING_SETUP_IOPOLL和IORING_SETUP_SQPOLL兩種 poll 模式。 后面會細說。 -
int io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args);這個接口主要用于注冊用戶態和內核態共享的緩沖區,即將 setup 返回的fd中的數據結構 映射到共享內存,從而進一步減少用戶I/O 提交到uring 隊列中的開銷。
-
int io_uring_enter(unsigned int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t *sig);這個接口既能夠提交 新的I/O請求 ,又能夠支持I/O收割。
liburing 的使用
可以從上面的幾個系統調用能夠簡單看到 用戶在自主使用這三個系統調用來調度 I/O請求時 還是比較麻煩的,像io_uring_setup 之后的fd,我們用戶層想要使用創建好的sq/cq ,則需要自主進行mmap,并且維護用戶態的sq/cq 數據結構,并在后續的 enter 中自主進行用戶態的sq 的填充。這個過程相對來說還是比較麻煩的。更不要說用三個系統調用中數十個的flags的靈活配置,如果全部結合起來,對于剛接觸io_uring的用戶來說還是需要較大的學習成本。
比如,我想啟動io_uring,并初始化好用戶態的sq/cq 數據結構,就需要寫下面這一些代碼:
int app_setup_uring(struct submitter *s) {struct app_io_sq_ring *sring = &s->sq_ring;struct app_io_cq_ring *cring = &s->cq_ring;struct io_uring_params p;void *sq_ptr, *cq_ptr;/** We need to pass in the io_uring_params structure to the io_uring_setup()* call zeroed out. We could set any flags if we need to, but for this* example, we don't.* */memset(&p, 0, sizeof(p));s->ring_fd = io_uring_setup(QUEUE_DEPTH, &p);if (s->ring_fd < 0) {perror("io_uring_setup");return 1;}/** io_uring communication happens via 2 shared kernel-user space ring buffers,* which can be jointly mapped with a single mmap() call in recent kernels.* While the completion queue is directly manipulated, the submission queue* has an indirection array in between. We map that in as well.* */int sring_sz = p.sq_off.array + p.sq_entries * sizeof(unsigned);int cring_sz = p.cq_off.cqes + p.cq_entries * sizeof(struct io_uring_cqe);/* In kernel version 5.4 and above, it is possible to map the submission and* completion buffers with a single mmap() call. Rather than check for kernel* versions, the recommended way is to just check the features field of the* io_uring_params structure, which is a bit mask. If the* IORING_FEAT_SINGLE_MMAP is set, then we can do away with the second mmap()* call to map the completion ring.* */if (p.features & IORING_FEAT_SINGLE_MMAP) {if (cring_sz > sring_sz) {sring_sz = cring_sz;}cring_sz = sring_sz;}/* Map in the submission and completion queue ring buffers.* Older kernels only map in the submission queue, though.* */sq_ptr = mmap(0, sring_sz, PROT_READ | PROT_WRITE,MAP_SHARED | MAP_POPULATE,s->ring_fd, IORING_OFF_SQ_RING);if (sq_ptr == MAP_FAILED) {perror("mmap");return 1;}if (p.features & IORING_FEAT_SINGLE_MMAP) {cq_ptr = sq_ptr;} else {/* Map in the completion queue ring buffer in older kernels separately */// 放置內存被page faultcq_ptr = mmap(0, cring_sz, PROT_READ | PROT_WRITE,MAP_SHARED | MAP_POPULATE,s->ring_fd, IORING_OFF_CQ_RING);if (cq_ptr == MAP_FAILED) {perror("mmap");return 1;}}/* Save useful fields in a global app_io_sq_ring struct for later* easy reference */sring->head = sq_ptr + p.sq_off.head;sring->tail = sq_ptr + p.sq_off.tail;sring->ring_mask = sq_ptr + p.sq_off.ring_mask;sring->ring_entries = sq_ptr + p.sq_off.ring_entries;sring->flags = sq_ptr + p.sq_off.flags;sring->array = sq_ptr + p.sq_off.array;/* Map in the submission queue entries array */s->sqes = mmap(0, p.sq_entries * sizeof(struct io_uring_sqe),PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,s->ring_fd, IORING_OFF_SQES);if (s->sqes == MAP_FAILED) {perror("mmap");return 1;}/* Save useful fields in a global app_io_cq_ring struct for later* easy reference */cring->head = cq_ptr + p.cq_off.head;cring->tail = cq_ptr + p.cq_off.tail;cring->ring_mask = cq_ptr + p.cq_off.ring_mask;cring->ring_entries = cq_ptr + p.cq_off.ring_entries;cring->cqes = cq_ptr + p.cq_off.cqes;return 0;
}
所以Jens Axboe 將三個系統調用做了一個封裝,形成了liburing,在這里面我想要初始化一個uring實例,并完成用戶態的數據結構的映射,只需要調用下面io_uring_queue_init 這個接口:
struct io_uring ring;struct io_uring_params p = { };int ret;ret = io_uring_queue_init(IORING_MAX_ENTRIES, &ring, IORING_SETUP_IOPOLL);
關于liburing的使用,可以看下面這個100行的小案例:
大體的功能就是利用io_uring 去讀一個用戶輸入的文件,每次讀請求的大小是4K,讀完整個文件結束。
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "liburing.h"#define QD 4int main(int argc, char *argv[])
{struct io_uring ring;int i, fd, ret, pending, done;struct io_uring_sqe *sqe;struct io_uring_cqe *cqe;struct iovec *iovecs;struct stat sb;ssize_t fsize;off_t offset;void *buf;if (argc < 2) {printf("%s: file\n", argv[0]);return 1;}// 初始化io_uring,并拿到初始化的結果,0是成功的,小于0 是失敗的ret = io_uring_queue_init(QD, &ring, 0);if (ret < 0) {fprintf(stderr, "queue_init: %s\n", strerror(-ret));return 1;}// 打開用戶輸入的文件fd = open(argv[1], O_RDONLY | O_DIRECT);if (fd < 0) {perror("open");return 1;}// 將文件屬性放在sb中,主要是獲取文件的大小if (fstat(fd, &sb) < 0) {perror("fstat");return 1;}// 拆分成 設置的 io_uring支持的最大隊列深度 個請求,4個fsize = 0;iovecs = calloc(QD, sizeof(struct iovec));for (i = 0; i < QD; i++) {if (posix_memalign(&buf, 4096, 4096))return 1;iovecs[i].iov_base = buf;iovecs[i].iov_len = 4096;fsize += 4096;}// 構造請求,并存放在 seq中offset = 0;i = 0;do {sqe = io_uring_get_sqe(&ring);if (!sqe)break;io_uring_prep_readv(sqe, fd, &iovecs[i], 1, offset);offset += iovecs[i].iov_len;i++;if (offset > sb.st_size)break;} while (1);// 提交請求sqe 中的請求到內核ret = io_uring_submit(&ring);if (ret < 0) {fprintf(stderr, "io_uring_submit: %s\n", strerror(-ret));return 1;} else if (ret != i) {fprintf(stderr, "io_uring_submit submitted less %d\n", ret);return 1;}done = 0;pending = ret;fsize = 0;// 等待內核處理完所有的請求,并由用戶態拿到cqe,表示請求處理完成for (i = 0; i < pending; i++) {ret = io_uring_wait_cqe(&ring, &cqe);if (ret < 0) {fprintf(stderr, "io_uring_wait_cqe: %s\n", strerror(-ret));return 1;}done++;ret = 0;if (cqe->res != 4096 && cqe->res + fsize != sb.st_size) {fprintf(stderr, "ret=%d, wanted 4096\n", cqe->res);ret = 1;}fsize += cqe->res;io_uring_cqe_seen(&ring, cqe);if (ret)break;}// 最后輸出 提交的請求的個數(4k),完成請求的個數,總共處理的請求大小printf("Submitted=%d, completed=%d, bytes=%lu\n", pending, done,(unsigned long) fsize);close(fd);io_uring_queue_exit(&ring);return 0;
}
編譯: gcc -O2 -D_GNU_SOURCE -o io_uring-test io_uring-test.c -luring
運行: ./io_uring-test test-file.txt
io_uring 非poll 模式下 的實現
接下來記錄一下io_uring的實現,來填之前說到的一些小坑,當然…這里描述的內容也是站在前人的肩膀 以及 自己經過一些測試驗證總體來看的。
io_uring 能夠支持其他多種I/O相關的請求:
- 文件I/O:read, write, remove, update, link,unlink, fadivse, allocate, rename, fsync等
- 網絡I/O:send, recv, socket, connet, accept等
- 進程間通信:pipe
- …
還是以 上面案例中 io_uring 處理read 請求為例, 通過io_uring_prep_readv 來填充之前已經創建好的sqe。
static inline void io_uring_prep_readv(struct io_uring_sqe *sqe, int fd,const struct iovec *iovecs,unsigned nr_vecs, __u64 offset)
{// 調度讀請求,將構造好的iovecs 中的內容填充到sqe中。io_uring_prep_rw(IORING_OP_READV, sqe, fd, iovecs, nr_vecs, offset);
}static inline void io_uring_prep_rw(int op, struct io_uring_sqe *sqe, int fd,const void *addr, unsigned len,__u64 offset)
{sqe->opcode = (__u8) op;...sqe->fd = fd;sqe->off = offset;sqe->addr = (unsigned long) addr;sqe->len = len;...sqe->__pad2[0] = sqe->__pad2[1] = 0;
}
那我們需要先回到最開始的io_uring_setup 以及 后續的mmap setup返回的結果 之后 用戶態和內核態共享的數據結構內容。
數據結構 在內存中的分布 如上圖:
-
io_uring_setup之后,會將內核中創建好的一塊內存區域 用fd標識 以及各個數據結構在這個內存區域中的偏移量存放在io_uring_params中, 通過mmap 來將這部分內存區域的數據結構映射到用用戶空間。其中
io_uring_params中的 關鍵數據結構如下:struct io_uring_params {__u32 sq_entries; // sq 隊列的個數__u32 cq_entries; // cq 隊列的個數__u32 flags; // setup設置的一些標識,比如是否開啟內核的io_poll 或者 sq_poll等__u32 sq_thread_cpu; // 設置sq_poll 模式下 輪詢的cpu 編號__u32 sq_thread_idle; __u32 features;__u32 wq_fd;__u32 resv[3];struct io_sqring_offsets sq_off; // sq的偏移量struct io_cqring_offsets cq_off; // cq的偏移量 }; -
Mmap 之后的內存形態就是上圖中的數據結構形態,mmap的過程就是填充用戶態可訪問的sq/cq。
-
SQ ,submission queue,保存用戶空間提交的請求的地址,實際的用戶請求會存放在
io_uring_sqe的sqes中。struct io_uring_sq {unsigned *khead;unsigned *ktail;...struct io_uring_sqe *sqes; // 較為復雜的數據結構,保存請求的實際內容unsigned sqe_head;unsigned sqe_tail;... };用戶空間的sq更新會追加到SQ 的隊尾部,內核空間消費 SQ 時則會消費隊頭。
-
CQ, complete queue,保存內核空間完成請求的地址,實際的完成請求的數據會存放在
io_uring_cqe的cqes中。struct io_uring_cq {unsigned *khead;unsigned *ktail;...struct io_uring_cqe *cqes;... };內核完成IO 收割之后會將請求填充到cqes 中,并更新cq 的隊尾,用戶空間則會從cq的隊頭消費 處理完成的請求。
-
-
在前面的read 案例代碼中,調用的
liburing的函數io_uring_get_sqe就是在用戶空間更新sq的隊尾部。struct io_uring_sqe *io_uring_get_sqe(struct io_uring *ring) {struct io_uring_sq *sq = &ring->sq;unsigned int head = io_uring_smp_load_acquire(sq->khead);unsigned int next = sq->sqe_tail + 1;struct io_uring_sqe *sqe = NULL;// 當前sq的 tail 和 head之間的容量滿足sq的大小,則將當前請求的填充到sqe中// 并更新sq 的隊尾,向上移動if (next - head <= *sq->kring_entries) {sqe = &sq->sqes[sq->sqe_tail & *sq->kring_mask];sq->sqe_tail = next;}return sqe; }后續,內核處理完成之后,用戶空間從cq中獲取 處理完成的請求時則會調用
io_uring_wait_cqe_nr進行收割。
io_uring 中的ring就是 上圖中的io 鏈路,從sq隊尾進入,最后請求從cq 隊頭出來,整個鏈路就是一個環形(ring)。而sq和cq在數據結構上被存放在了
io_uring中。加了uring 中的u猜測是指用戶態(userspace)可訪問的,目的是好的,不過讀起來的單詞諧音就讓一些人略微尷尬(urine。。。)
非poll 模式下的內核火焰圖調用棧如下:
io_uring poll模式下的實現
我們在最開始的性能測試過程中可以看到在開啟 poll 之后,io_uring的性能才能顯著提高。
我們從前面 io_uring 內存分布圖 中可以看到在內核調度兩個隊列請求的過程中 可以通過異步輪詢的方式進行調度的,也就是io_uring的 poll模式。
io_uring 在io_uring_setup的時候可以通過設置flag 來開啟poll模式,io-uring 支持兩種方式poll模式。
-
IORING_SETUP_IOPOLL,這種方式是由nvme 驅動支持的 io_poll。即用戶態通過io_uring_enter提交請求到內核的文件讀寫隊列中即可,nvme驅動會不斷得輪詢文件讀寫隊列進行io消費,同時用戶態在設置IORING_ENTER_GETEVENTS得flag之后,還需要不斷得調用io_uring_enter通過io_iopoll_check調用內核接口查看 nvme的io_poll 是否完成任務調度,從而進行填充 cqes。如果使用nvme驅動,則需要單獨開啟io_poll 才能真正讓 IORING_SETUP_IOPOLL 配置生效。
開啟的話,直接嘗試 root 用戶操作:echo 1 > /sys/block/nvme2n1/queue/io_poll,成功則表示開啟。
如果出現bash: echo: write error: Invalid argument ,則表示當前nvme驅動還不支持,需要通過驅動層打開這個配置才行,可以嘗試執行如下步驟:
如果執行之前,通過modinfo nvme 查看當前設備是否有nvme驅動失敗,則需要先編譯當前內核版本的nvme驅動才行,否則下面的操作沒有nvme驅動都是無法進行的。
- umount fs , 卸載磁盤上掛載的文件系統
- echo 1 > /sys/block/nvme0n1/device/device/remove , 將設備從當前服務器移除
- rmmod nvme
- modprobe nvme poll_queues=1, 重新加載nvme驅動,來支持io_poll的隊列深度為1
- echo 1 > /sys/bus/pci/rescan ,重新將磁盤加載回來
-
IORING_SETUP_SQPOLL,這種模式的poll則是我們fio測試下的sqthread_poll開啟的配置。開啟之后io_uring會啟動一個內核線程,用來輪詢submit queue,從而達到不需要系統調用的參與就可以提交請求。用戶請求在用戶空間提交到SQ 之后,這個內核線程處于喚醒狀態時會不斷得輪詢SQ,也就可以立即捕獲到這次請求。(我們前面的案例中會先在用戶空間構造指定數量的SQ放到ring-buffer中,再由io_uring_enter一起提交到內核),這個時候有了sq_thread 的輪詢,只要用戶空間提交到SQ,內核就能夠捕獲到并進行處理。如果sq_thread 長時間捕獲不到請求,則會進入休眠狀態,需要通過調用io_uring_enter系統調用,并設置IORING_SQ_NEED_WAKEUP來喚醒sq_thread。
大體的調度方式如下圖:
這種sq_thread 內核對SQ的輪詢模式能夠極大得減少請求在submit queue中的排隊時間,同時減少了
io_uring_enter系統調用的開銷。
開啟sq_thread之后的輪詢模式可以看到 用戶提交請求 對CPU消耗僅僅只占用了一小部分的cpu。
io_uring 在 rocksdb 中的應用
Rocksdb 針對io_uring的調用大體類似前面提到的使用liburing 接口實現的一個read 文件的案例,同樣是調用io_uring_prep_readv 來實現對文件的讀寫。
Io_uring 的特性決定了在I/O層 的批量讀才能體現它的優勢,所以rocksdb 將io_uring集成到了 MultiGet 中的 MultiRead 接口之中。
需要注意的是 rocksdb 設置的 io_uring的SQ 隊列深度大小是256,且setup的時候并沒有開啟sq_poll模式,而是默認開啟io_poll,即flag是0;如果想要開啟sq_poll模式,則需要變更這個接口的flags配置,比如將0設置為IORING_SETUP_SQPOLL,然后重新編譯源代碼即可。
inline struct io_uring* CreateIOUring() {struct io_uring* new_io_uring = new struct io_uring;int ret = io_uring_queue_init(kIoUringDepth, new_io_uring, 0);if (ret) {delete new_io_uring;new_io_uring = nullptr;}return new_io_uring;
}
大家在使用db_bench測試io_uring的時候 如果不變更rocksdb這里的io_uring_queue_init接口的話,需要保證自己的磁盤支持io_poll模式,也就是通過上一節說的那種查看/修改 nvme 驅動配置來支持io_poll。
在io_poll模式下,對MultiGet的接口測試性能數據大概如下:
我的環境不支持io_poll,大體收益應該和fio的poll模式下的性能收益差不了太多
圖片來自官方
db_bench的配置可以使用,直接用rocksdb的master, CMakeList.txt 默認會開啟io_uring:
生成數據:
./db_bench_uring \--benchmarks=fillrandom,stats \--num=3000000000 \--threads=32 \--db=./db \--wal_dir=./db \--duration=3600 \-report_interval_seconds=1 \--stats_interval_seconds=10 \--key_size=16 \--value_size=128 \--max_write_buffer_number=16 \-max_background_compactions=32 \-max_background_flushes=7 \-subcompactions=8 \-compression_type=none \
io_uring 測試MultiGet,不使用block_cache:
./db_bench_uring \--benchmarks=multireadrandom,stats \--num=3000000000 \--threads=32 \--db=./db \ --wal_dir=./db \--duration=3600 \-report_interval_seconds=1 \--stats_interval_seconds=10 \--key_size=16 \--value_size=128 \-compression_type=none \-cache_size=0 \-use_existing_db=1 \-batch_size=256 \ # 每次MultiGet的 請求總個數-multiread_batched=true \ # 使用 MultiGet 的新的API,支持MultiRead,否則就是逐個Get-multiread_stride=0 # 指定MultiGet 時生成的key之間的跨度,本來是連續的隨機key,現在可以讓上一個隨機key和下一個隨機key之間間隔指定的長度。
總結
總的來說,io_uring能夠在內核的各個組件都能夠正常運行的基礎上進一步提升了性能,提升的部分包括 減少系統調用的開銷,減少內核上下文的開銷,以及支持io_poll和sq_poll 這樣的高速輪詢處理機制。而且相比于libaio 僅能夠使用direct-io來調度,那這個限制本身就對存儲應用軟件不夠友好了。
可見的未來,存儲系統是內核的直接用戶,隨著未來硬件介質的超高速發展,互聯網應用對存儲系統的高性能需求就會反作用于內核,那內核的一些I/O鏈路的性能也需要不斷得跟進提升,然而每一項on-linux kernel的更改都因為內核精密復雜高要求 的 標準都會比普通的應用復雜很多,io_uring 能夠合入5系內核的upstream,那顯然證明了其未來的發展潛力 以及 內核社區 對其潛力的認可。
參考
1. https://kernel.dk/io_uring.pdf
2. https://zhuanlan.zhihu.com/p/380726590
3. https://developers.mattermost.com/blog/hands-on-iouring-go/
總結
以上是生活随笔為你收集整理的关于 linux io_uring 性能测试 及其 实现原理的一些探索的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 每个版本倚天屠龙记里的明教山洞拍摄地,是
- 下一篇: 求一个qq网名女生