0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

io_uring內(nèi)核各個組件的性能

科技綠洲 ? 來源:Linux開發(fā)架構(gòu)之路 ? 作者:Linux開發(fā)架構(gòu)之路 ? 2023-11-10 11:46 ? 次閱讀

先看看性能

io_uring 需要內(nèi)核版本在5.1 及以上才支持,liburing的編譯安裝 很簡單,直接clone 官方的代碼,sudo make && sudo make install 就好了,本文是在內(nèi)核5.12版本 上測試的。

在描述io_uring 的性能之前,我們先直接看一組實測數(shù)據(jù):

這組數(shù)據(jù)是在3D XPoint 介質(zhì)的硬盤 :optane-5800上測試的,optane5800 能夠提供(randread100% 150w/s , randwrite 100% 150w/s)的性能。

圖片

進行測試的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

通過上面的測試,我們能夠得到如下幾個結(jié)論:

  1. 這種高隊列深度的測試下,可以看到io_uring 在開啟sq_poll之后的性能 相比于aio 的高隊列深度的處理能力好接近一倍;
  2. 在較低隊列深度 以及不開啟 sq_poll 模式的情況下,io_uring 整體沒有太大的優(yōu)勢,或者說一樣的性能。
  3. 在buffer I/O (direct=0) 下,io_uring 也不會有太大的優(yōu)勢,因為都得通過 os-cache 來操作。

需要注意的是,如果aio和io_uring 在高并發(fā)下(jobs 的數(shù)目不斷增加),都是可以達到當前磁盤的性能瓶頸的。

AIO 的基本實現(xiàn)

那有這樣的測試現(xiàn)象,我們可能會有一些疑問,就這性能?我們在nvme上做軟件,希望發(fā)揮的是整個磁盤的性能,而不是比拼誰的隊列深度大,誰的優(yōu)勢更大。。。我用aio 做batch 也能達到磁盤的性能瓶頸,為什么要選擇 對于數(shù)據(jù)庫/存儲 領(lǐng)域來說 好像“如日中天”的io_uring呢。

我們先來看看aio 的大體實現(xiàn),沒有涉及到源代碼。aio 主要提供了三個系統(tǒng)調(diào)用:

  • io_setup 初始化一些內(nèi)核態(tài)的數(shù)據(jù)結(jié)構(gòu)
  • io_submit 用于用戶態(tài)提交io 請求
  • io_getevents 用于io 請求處理完成之后的io 收割

圖片

大體的IO調(diào)度過程如下:

  1. io_setup 完成一些內(nèi)核數(shù)據(jù)結(jié)構(gòu)的初始化(包括內(nèi)核中的 aio 調(diào)度隊列,aio_ring_info 的ring-buffer緩沖區(qū))
  2. 用戶態(tài)構(gòu)造一批io請求,通過io_submit 拷貝請求到內(nèi)核態(tài)io 隊列(文件系統(tǒng)之上,上圖沒有體現(xiàn)出來)之后返回到用戶態(tài)。
  3. 內(nèi)核態(tài)繼續(xù)通過內(nèi)核i/o 棧處理io請求,處理完成之后 通過 aio_complete 函數(shù)將處理完成的請求放入到 aio_ring_info,每一個io請求是一個io_event。
  4. 用戶態(tài)通過 io_getevents 系統(tǒng)調(diào)用 從 aio_ring_info(ring-buffer) 的head 拿處理完成的io_event,如果head==tail,則表示這個ring-buffer是空的。拿到之后,將拿到的io_event 一批從內(nèi)核態(tài)拷貝到用戶態(tài)。

如果單純看 誰能將磁盤性能完整發(fā)揮出來,那毋庸置疑,大家都可以;那為什么做存儲的對io_uring 的出現(xiàn)如此熱衷呢?我們就得結(jié)合實際的應用場景來看看兩者之間的差異了:

  1. 使用AIO的話,請求調(diào)度都需要直接由通用塊層來調(diào)度處理,所以需要O_DIRECT標記。這就意味著,使用AIO的應用都無法享受os cache,這對與存儲應用來說并不友好,cache都得自己來維護,而且顯然沒有os page-cache性能以及穩(wěn)定性有優(yōu)勢。而使用io_uring 則沒有這樣的限制,當然,io_uring在 buffer I/O下顯然沒有太大的優(yōu)勢。
  2. 延時上的開銷。AIO 提交用戶請求的時候 通過io_submit調(diào)用,收割用戶請求的時候通過io_getevents,正常應用的時候每一個請求都意味著至少兩次系統(tǒng)調(diào)用(I/O提交和I/O收割),而對于io_uring來說,I/O 提交和I/O收割都可以 offload 給內(nèi)核。這樣相比于AIO 來說,io_uring能夠極大得減少 系統(tǒng)調(diào)用引入的上下文切換。
  3. io_uring 能夠支持針對submit queue的polling,啟動一個內(nèi)核線程進行polling,加速請求的提交和收割;對于aio來說,這里就沒有這樣的機制。

總的來說,io_uring 能夠保證上層應用 對系統(tǒng)資源(cache)正常使用的同時 ,降低應用 下發(fā)的請求延時和CPU的開銷,在單實例高隊深下,能夠顯著優(yōu)于同等隊深下的AIO性能。

io_ring 使用

io_uring 基本接口

io_uring的用戶態(tài)API 提供了三個系統(tǒng)調(diào)用,io_uring_setup,io_uring_enter,io_uring_register。

  • int io_uring_setup(u32 entries, struct io_uring_params *p); 這個接口 用于創(chuàng)建 擁有 entries 個請求的 提交隊列(SQ) 和 完成隊列(CQ),并且返回給用戶一個fd。這個fd可以用做在同一個uring實例上 用戶空間和內(nèi)核空間共享sq和cq 隊列,這樣能夠避免在請求完成時不需要從完成隊列拷貝數(shù)據(jù)到用戶態(tài)了。io_uring_params 主要是根據(jù)用戶的配置來設置uring 實例的創(chuàng)建行為。包括 單不限于開啟 IORING_SETUP_IOPOLL 和 IORING_SETUP_SQPOLL 兩種 poll 模式。后面會細說。
  • int io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args);這個接口主要用于注冊用戶態(tài)和內(nèi)核態(tài)共享的緩沖區(qū),即將 setup 返回的fd中的數(shù)據(jù)結(jié)構(gòu) 映射到共享內(nèi)存,從而進一步減少用戶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 的使用

可以從上面的幾個系統(tǒng)調(diào)用能夠簡單看到 用戶在自主使用這三個系統(tǒng)調(diào)用來調(diào)度 I/O請求時 還是比較麻煩的,像io_uring_setup 之后的fd,我們用戶層想要使用創(chuàng)建好的sq/cq ,則需要自主進行mmap,并且維護用戶態(tài)的sq/cq 數(shù)據(jù)結(jié)構(gòu),并在后續(xù)的 enter 中自主進行用戶態(tài)的sq 的填充。這個過程相對來說還是比較麻煩的。更不要說用三個系統(tǒng)調(diào)用中數(shù)十個的flags的靈活配置,如果全部結(jié)合起來,對于剛接觸io_uring的用戶來說還是需要較大的學習成本。

比如,我想啟動io_uring,并初始化好用戶態(tài)的sq/cq 數(shù)據(jù)結(jié)構(gòu),就需要寫下面這一些代碼:

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 */
// 放置內(nèi)存被page fault
cq_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將三個系統(tǒng)調(diào)用做了一個封裝,形成了liburing,在這里面我想要初始化一個uring實例,并完成用戶態(tài)的數(shù)據(jù)結(jié)構(gòu)的映射,只需要調(diào)用下面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);

關(guān)于liburing的使用,可以看下面這個100行的小案例:

大體的功能就是利用io_uring 去讀一個用戶輸入的文件,每次讀請求的大小是4K,讀完整個文件結(jié)束。

#include
#include
#include
#include
#include
#include
#include
#include "liburing.h"

#define QD 4

int 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: filen", argv[0]);
return 1;
}
// 初始化io_uring,并拿到初始化的結(jié)果,0是成功的,小于0 是失敗的
ret = io_uring_queue_init(QD, &ring, 0);
if (ret < 0) {
fprintf(stderr, "queue_init: %sn", 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;
}

// 構(gòu)造請求,并存放在 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 中的請求到內(nèi)核
ret = io_uring_submit(&ring);
if (ret < 0) {
fprintf(stderr, "io_uring_submit: %sn", strerror(-ret));
return 1;
} else if (ret != i) {
fprintf(stderr, "io_uring_submit submitted less %dn", ret);
return 1;
}

done = 0;
pending = ret;
fsize = 0;
// 等待內(nèi)核處理完所有的請求,并由用戶態(tài)拿到cqe,表示請求處理完成
for (i = 0; i < pending; i++) {
ret = io_uring_wait_cqe(&ring, &cqe);
if (ret < 0) {
fprintf(stderr, "io_uring_wait_cqe: %sn", strerror(-ret));
return 1;
}

done++;
ret = 0;
if (cqe->res != 4096 && cqe->res + fsize != sb.st_size) {
fprintf(stderr, "ret=%d, wanted 4096n", cqe->res);
ret = 1;
}
fsize += cqe->res;
io_uring_cqe_seen(&ring, cqe);
if (ret)
break;
}

// 最后輸出 提交的請求的個數(shù)(4k),完成請求的個數(shù),總共處理的請求大小
printf("Submitted=%d, completed=%d, bytes=%lun", 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 模式下 的實現(xiàn)

接下來記錄一下io_uring的實現(xiàn),來填之前說到的一些小坑,當然…這里描述的內(nèi)容也是站在前人的肩膀 以及 自己經(jīng)過一些測試驗證總體來看的。

io_uring 能夠支持其他多種I/O相關(guān)的請求:

  • 文件I/O:read, write, remove, update, link,unlink, fadivse, allocate, rename, fsync等
  • 網(wǎng)絡I/O:send, recv, socket, connet, accept等
  • 進程間通信:pipe

還是以 上面案例中 io_uring 處理read 請求為例, 通過io_uring_prep_readv來填充之前已經(jīng)創(chuàng)建好的sqe。

static inline void io_uring_prep_readv(struct io_uring_sqe *sqe, int fd,
const struct iovec *iovecs,
unsigned nr_vecs, __u64 offset)
{
// 調(diào)度讀請求,將構(gòu)造好的iovecs 中的內(nèi)容填充到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以及 后續(xù)的mmap setup返回的結(jié)果 之后 用戶態(tài)和內(nèi)核態(tài)共享的數(shù)據(jù)結(jié)構(gòu)內(nèi)容。

圖片

數(shù)據(jù)結(jié)構(gòu) 在內(nèi)存中的分布 如上圖:

1.io_uring_setup 之后,會將內(nèi)核中創(chuàng)建好的一塊內(nèi)存區(qū)域 用 fd標識 以及各個數(shù)據(jù)結(jié)構(gòu)在這個內(nèi)存區(qū)域中的偏移量存放在io_uring_params中, 通過mmap 來將這部分內(nèi)存區(qū)域的數(shù)據(jù)結(jié)構(gòu)映射到用用戶空間。

其中io_uring_params 中的 關(guān)鍵數(shù)據(jù)結(jié)構(gòu)如下:

struct io_uring_params {
__u32 sq_entries; // sq 隊列的個數(shù)
__u32 cq_entries; // cq 隊列的個數(shù)
__u32 flags; // setup設置的一些標識,比如是否開啟內(nèi)核的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的偏移量
};

2.Mmap 之后的內(nèi)存形態(tài)就是上圖中的數(shù)據(jù)結(jié)構(gòu)形態(tài),mmap的過程就是填充用戶態(tài)可訪問的sq/cq。

  • SQ ,submission queue,保存用戶空間提交的請求的地址,實際的用戶請求會存放在io_uring_sqe的sqes中。
struct io_uring_sq {
unsigned *khead;
unsigned *ktail;
...
struct io_uring_sqe *sqes; // 較為復雜的數(shù)據(jù)結(jié)構(gòu),保存請求的實際內(nèi)容
unsigned sqe_head;
unsigned sqe_tail;
...
};

用戶空間的sq更新會追加到SQ 的隊尾部,內(nèi)核空間消費 SQ 時則會消費隊頭。

  • CQ, complete queue,保存內(nèi)核空間完成請求的地址,實際的完成請求的數(shù)據(jù)會存放在io_uring_cqe的cqes中。
struct io_uring_cq {
unsigned *khead;
unsigned *ktail;
...
struct io_uring_cqe *cqes;
...
};

內(nèi)核完成IO 收割之后會將請求填充到cqes 中,并更新cq 的隊尾,用戶空間則會從cq的隊頭消費 處理完成的請求。

3.在前面的read 案例代碼中,調(diào)用的liburing 的函數(shù) 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;
}

后續(xù),內(nèi)核處理完成之后,用戶空間從cq中獲取 處理完成的請求時則會調(diào)用io_uring_wait_cqe_nr進行收割。

io_uring 中的ring就是 上圖中的io 鏈路,從sq隊尾進入,最后請求從cq 隊頭出來,整個鏈路就是一個環(huán)形(ring)。而sq和cq在數(shù)據(jù)結(jié)構(gòu)上被存放在了 io_uring 中。加了uring 中的u 猜測是指用戶態(tài)(userspace)可訪問的,目的是好的,不過讀起來的單詞諧音就讓一些人略微尷尬(urine。。。)

非poll 模式下的內(nèi)核火焰圖調(diào)用棧如下:

圖片

io_uring poll模式下的實現(xiàn)

我們在最開始的性能測試過程中可以看到在開啟 poll 之后,io_uring的性能才能顯著提高。

我們從前面 io_uring 內(nèi)存分布圖 中可以看到在內(nèi)核調(diào)度兩個隊列請求的過程中 可以通過異步輪詢的方式進行調(diào)度的,也就是io_uring的 poll模式。

io_uring 在io_uring_setup的時候可以通過設置flag 來開啟poll模式,io-uring 支持兩種方式poll模式。

  • IORING_SETUP_IOPOLL,這種方式是由nvme 驅(qū)動支持的 io_poll。即用戶態(tài)通過io_uring_enter提交請求到內(nèi)核的文件讀寫隊列中即可,nvme驅(qū)動會不斷得輪詢文件讀寫隊列進行io消費,同時用戶態(tài)在設置IORING_ENTER_GETEVENTS得flag之后,還需要不斷得調(diào)用io_uring_enter 通過io_iopoll_check 調(diào)用內(nèi)核接口查看 nvme的io_poll 是否完成任務調(diào)度,從而進行填充 cqes。

如果使用nvme驅(qū)動,則需要單獨開啟io_poll 才能真正讓 IORING_SETUP_IOPOLL 配置生效。開啟的話,直接嘗試 root 用戶操作:echo 1 > /sys/block/nvme2n1/queue/io_poll,成功則表示開啟。如果出現(xiàn)bash: echo: write error: Invalid argument ,則表示當前nvme驅(qū)動還不支持,需要通過驅(qū)動層打開這個配置才行,可以嘗試執(zhí)行如下步驟:如果執(zhí)行之前,通過modinfo nvme 查看當前設備是否有nvme驅(qū)動失敗,則需要先編譯當前內(nèi)核版本的nvme驅(qū)動才行,否則下面的操作沒有nvme驅(qū)動都是無法進行的。

  1. umount fs , 卸載磁盤上掛載的文件系統(tǒng)
  2. echo 1 > /sys/block/nvme0n1/device/device/remove , 將設備從當前服務器移除
  3. rmmod nvme
  4. modprobe nvme poll_queues=1, 重新加載nvme驅(qū)動,來支持io_poll的隊列深度為1
  5. echo 1 > /sys/bus/pci/rescan ,重新將磁盤加載回來
  • IORING_SETUP_SQPOLL,這種模式的poll則是我們fio測試下的 sqthread_poll開啟的配置。開啟之后io_uring會啟動一個內(nèi)核線程,用來輪詢submit queue,從而達到不需要系統(tǒng)調(diào)用的參與就可以提交請求。用戶請求在用戶空間提交到SQ 之后,這個內(nèi)核線程處于喚醒狀態(tài)時會不斷得輪詢SQ,也就可以立即捕獲到這次請求。(我們前面的案例中會先在用戶空間構(gòu)造指定數(shù)量的SQ放到ring-buffer中,再由io_uring_enter一起提交到內(nèi)核),這個時候有了sq_thread 的輪詢,只要用戶空間提交到SQ,內(nèi)核就能夠捕獲到并進行處理。如果sq_thread 長時間捕獲不到請求,則會進入休眠狀態(tài),需要通過調(diào)用io_uring_enter系統(tǒng)調(diào)用,并設置IORING_SQ_NEED_WAKEUP來喚醒sq_thread。

大體的調(diào)度方式如下圖:

圖片

這種sq_thread 內(nèi)核對SQ的輪詢模式能夠極大得減少請求在submit queue中的排隊時間,同時減少了io_uring_enter系統(tǒng)調(diào)用的開銷。

圖片

開啟sq_thread之后的輪詢模式可以看到 用戶提交請求 對CPU消耗僅僅只占用了一小部分的cpu。

io_uring 在 rocksdb 中的應用

Rocksdb 針對io_uring的調(diào)用大體類似前面提到的使用liburing 接口實現(xiàn)的一個read 文件的案例,同樣是調(diào)用io_uring_prep_readv 來實現(xiàn)對文件的讀寫。

Io_uring 的特性決定了在I/O層 的批量讀才能體現(xiàn)它的優(yōu)勢,所以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模式,也就是通過上一節(jié)說的那種查看/修改 nvme 驅(qū)動配置來支持io_poll。

在io_poll模式下,對MultiGet的接口測試性能數(shù)據(jù)大概如下:

我的環(huán)境不支持io_poll,大體收益應該和fio的poll模式下的性能收益差不了太多

圖片圖片來自官方

db_bench的配置可以使用,直接用rocksdb的master, CMakeList.txt 默認會開啟io_uring:

生成數(shù)據(jù):

./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的 請求總個數(shù)
-multiread_batched=true # 使用 MultiGet 的新的API,支持MultiRead,否則就是逐個Get
-multiread_stride=0 # 指定MultiGet 時生成的key之間的跨度,本來是連續(xù)的隨機key,現(xiàn)在可以讓上一個隨機key和下一個隨機key之間間隔指定的長度。

總結(jié)

總的來說,io_uring能夠在內(nèi)核的各個組件都能夠正常運行的基礎上進一步提升了性能,提升的部分包括 減少系統(tǒng)調(diào)用的開銷,減少內(nèi)核上下文的開銷,以及支持io_poll和sq_poll 這樣的高速輪詢處理機制。而且相比于libaio 僅能夠使用direct-io來調(diào)度,那這個限制本身就對存儲應用軟件不夠友好了。

可見的未來,存儲系統(tǒng)是內(nèi)核的直接用戶,隨著未來硬件介質(zhì)的超高速發(fā)展,互聯(lián)網(wǎng)應用對存儲系統(tǒng)的高性能需求就會反作用于內(nèi)核,那內(nèi)核的一些I/O鏈路的性能也需要不斷得跟進提升,然而每一項on-linux kernel的更改都因為內(nèi)核精密復雜高要求 的 標準都會比普通的應用復雜很多,io_uring 能夠合入5系內(nèi)核的upstream,那顯然證明了其未來的發(fā)展?jié)摿?以及 內(nèi)核社區(qū) 對其潛力的認可。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 硬盤
    +關(guān)注

    關(guān)注

    3

    文章

    1278

    瀏覽量

    57175
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4701

    瀏覽量

    68123
  • 編譯
    +關(guān)注

    關(guān)注

    0

    文章

    647

    瀏覽量

    32746
  • 組件
    +關(guān)注

    關(guān)注

    1

    文章

    500

    瀏覽量

    17777
收藏 人收藏

    評論

    相關(guān)推薦

    帶你深入探索okio組件的奧秘 提升IO效率

    三方組件庫上新了一批JS/eTS組件,其中就包括okio組件。okio是一個可應用于OpenAtom OpenHarmony(以下簡稱“OpenHarmony”)的高效IO庫,它依托于
    發(fā)表于 07-04 15:39 ?1024次閱讀

    如何將Runtime IO組件添加到現(xiàn)有項目?

    我最近在SPC5studio上下載了運行時IO組件的更新,但是,我似乎找不到從IDE gui導入向?qū)?b class='flag-5'>組件添加到現(xiàn)有項目或新項目的方法。以上來自于谷歌翻譯以下為原文I recently
    發(fā)表于 11-15 10:59

    可擴展的高性能RISC-V 內(nèi)核IP

    性能SiFive U8系列內(nèi)核IP的介紹開啟了現(xiàn)代SoC設計的新篇章。性能、面積效率和能效是芯片設計人員的關(guān)鍵指標,而SiFive U8系列在各個方面都表現(xiàn)搶眼。SiFive這種獨有
    發(fā)表于 08-13 15:14

    The Wirele Mea uring Sy tem of

    The Wirele Mea uring Sy tem of Temperature and Humidity and lt Application:介紹了利用無線收發(fā)元件構(gòu)成的無線溫濕度測量系統(tǒng)的設計方法。采用這種測量方案,不必敷設電纜,節(jié)省了費用和時間。
    發(fā)表于 03-24 09:13 ?13次下載

    IO系統(tǒng)衡量性能的幾個指標

    本系列文章試圖從基本概念開始對磁盤存儲相關(guān)的各種概念進行綜合歸納,讓大家能夠?qū)?b class='flag-5'>IO性能相關(guān)的基本概念,IO性能的監(jiān)控和調(diào)整有個比較全面的了解
    發(fā)表于 03-28 12:01 ?2661次閱讀

    IO多路復用的幾種實現(xiàn)機制的分析

    服務器端編程經(jīng)常需要構(gòu)造高性能IO模型,常見的IO模型有四種:同步和異步的概念描述的是用戶線程與內(nèi)核的交互方式:同步是指用戶線程發(fā)起IO
    發(fā)表于 03-07 11:40 ?5712次閱讀
    <b class='flag-5'>IO</b>多路復用的幾種實現(xiàn)機制的分析

    論述學習Linux內(nèi)核各個階段

    第三階段(回歸第一階段):你已經(jīng)工作了一段時間,寫了一些代碼,修復了一些bug,提交了一些patch,然后你重新回來迭代整體的知識框架,搞清楚各個子系統(tǒng)內(nèi)在的聯(lián)系。這階段你如果有興趣可以讀《深入
    的頭像 發(fā)表于 08-20 17:23 ?4954次閱讀

    io_uring 優(yōu)化 nginx,基于通用應用 nginx 的實戰(zhàn)

    引言 io_uring是Linux內(nèi)核在v5.1引入的一套異步IO接口,隨著其迅速發(fā)展,現(xiàn)在的io_uring已經(jīng)遠遠超過了純IO的范疇。從
    的頭像 發(fā)表于 10-10 16:19 ?3020次閱讀
    <b class='flag-5'>io_uring</b> 優(yōu)化 nginx,基于通用應用 nginx 的實戰(zhàn)

    Linux 6.0生命周期結(jié)束介紹

    在去年 10 月初,Linux 6.0 正式發(fā)布,當時新版本帶來了非常多的新特性 / 功能,如 F2FS 低內(nèi)存模式、在使用 XFS 和 io_uring 時的異步緩沖寫入、對 RISC-V 和 AArch64(ARM64)硬件架構(gòu)的改進,以及 Btrfs 和 OverlayFS 文件系統(tǒng)的新功能和改進等。
    的頭像 發(fā)表于 01-16 11:43 ?762次閱讀

    現(xiàn)代異步存儲訪問API探索:libaio、io_uring和SPDK

    最近的高性能存儲設備暴露了現(xiàn)有軟件棧的低效,因而催生了對I/O棧的改進。Linux內(nèi)核的最新API是io_uring。作者提供了第一個針對io_uring的深度研究,并且和libaio
    的頭像 發(fā)表于 06-27 10:54 ?910次閱讀
    現(xiàn)代異步存儲訪問API探索:libaio、<b class='flag-5'>io_uring</b>和SPDK

    信號驅(qū)動IO與異步IO的區(qū)別

    一. 談信號驅(qū)動IO (對比異步IO來看) 信號驅(qū)動IO 對比 異步 IO進行理解 信號驅(qū)動IO: 內(nèi)核
    的頭像 發(fā)表于 11-08 15:32 ?926次閱讀
    信號驅(qū)動<b class='flag-5'>IO</b>與異步<b class='flag-5'>IO</b>的區(qū)別

    linux異步io框架iouring應用

    Linux內(nèi)核5.1支持了新的異步IO框架iouring,由Block IO大神也即Fio作者Jens Axboe開發(fā),意在提供一套公用的網(wǎng)絡和磁盤異步IO,不過
    的頭像 發(fā)表于 11-08 15:39 ?587次閱讀
    linux異步<b class='flag-5'>io</b>框架iouring應用

    異步IO框架iouring介紹

    前言 Linux內(nèi)核5.1支持了新的異步IO框架iouring,由Block IO大神也即Fio作者Jens Axboe開發(fā),意在提供一套公用的網(wǎng)絡和磁盤異步IO,不過
    的頭像 發(fā)表于 11-09 09:30 ?1995次閱讀
    異步<b class='flag-5'>IO</b>框架iouring介紹

    如何去提高EtherCAT IO性能呢?

    進行EtherCAT IO性能優(yōu)化涉及多個方面,包括硬件選擇、網(wǎng)絡配置、軟件優(yōu)化和應用程序設計。
    的頭像 發(fā)表于 03-07 09:28 ?415次閱讀

    組件測試儀如何提高太陽能組件性能?

      JD-EL4太陽能組件測試儀在太陽能行業(yè)中扮演著至關(guān)重要的角色,不僅可以檢測組件的缺陷和問題,還可以幫助提高太陽能組件性能。以下是太陽能組件
    的頭像 發(fā)表于 05-21 17:13 ?576次閱讀