# seHTTPd + io_uring 開發紀錄 > * [sehttpd 原始程式碼 epoll](https://github.com/sysprog21/sehttpd) > * [修改過的原始程式碼 io_uring](https://github.com/zzzxxx00019/sehttpd-IO_URING) --- ## io_uring 系統呼叫 > [Efficient IO with io_uring](https://kernel.dk/io_uring.pdf) > [Efficient IO with io_uring 內容解讀](https://hackmd.io/@ptzling310/ByOfdvzsv#liburing-library) ### 基本架構 ![](https://i.imgur.com/uT7r1Zj.png) * `SQ` ( `submission queue` ) 內存放 `submission event` ,代表等待 `I/O` 狀態的事件 * `CQ` (`complete queue` ) 內存放 `complete event` ,代表執行完 `I/O` 操作的事件 * 核心內部以 `polling` 的方式去檢查 `SQ` 內每個 `event` 的狀態 * 一旦 `sqe` 操作完成,將完成事件 ( `cqe` )加入 `CQ->tail` ### 設計流程 ( 以 socket 為例 ) > [Sample Code : socket-rw.c](https://github.com/axboe/liburing/blob/master/test/socket-rw.c) > [io_uring sample web server](https://github.com/shuveb/io_uring-by-example/blob/master/05_webserver_liburing/main.c) * 建立一個 `socket file descriptor` ,並將 `sqe` 的狀態設為準備 `accept` 並加入 `SQ` * 等待 `CQ` 中,執行完 `accept` 的 `cqe` ,代表成功 `accept` 一個新的連線 * 確認 `accept` 成功後,將 `sqe` 狀態設為準備 `read` 封包資訊,並加入 `SQ` 等待執行 * 等待 `CQ` 中,完成 `read` 的 `cqe` ,再根據不同的資訊做出不同的回應 * 將回應內容以 `write` 的方式回傳給 `client` ,將 `sqe` 狀態設為 `write` ,送出回應封包 * 程式大致設計流程為下 : ```c= io_uring socket example { sockfd = listen() ; add_accept_sqe(ring, sockfd); while(1) { wait_cqe(ring, cqe); switch(cqe->complete_case) { case accept_done: /* accept done, so next step is read request */ add_read_sqe(ring, clientfd); case read_done: /* read done, so next step is write our response */ add_write_sqe(ring, clientfd); case write_done: /* socket done, close the connect */ close(clientfd); } seen_cqe(ring, cqe); } } ``` --- ### SQE Timeout Delete > [IOSQE_IO_LINK 說明](https://hackmd.io/@ptzling310/ByOfdvzsv#IOURING_OP_LINK_TIMEOUT-amp-IOSQE_IO_LINK-%E7%B4%80%E9%8C%84) > [Sample Code : read-write.c](https://github.com/axboe/liburing/blob/516280a50fbcc4e6330ec20f6c93128caff510c4/test/read-write.c) * 在 `io_uring` 系統呼叫下, `I/O operation` 以 `block` 的狀態在 `kernel` 等待執行,以 `socket` 的 `read()` 為例,若 client 已關閉連線, server 中對這個 client 的狀態仍然維持在等待資料讀取,造成後續連線的錯誤 * 為避免上述問題的出現,必須給每個 `sqe` 設定 `timeout delete` 的機制,當經過設定時間內,尚未執行動作,就將這個 `event` 強迫結束 根據 `IOSQE_IO_LINK Flag` 說明: > When this flag is specified, it forms a link with the next SQE in the submission ring. That next SQE will not be started before this one completes. This, in effect, forms a chain of SQEs, which can be arbitrarily long. The tail of the chain is denoted by the first SQE that does not have this flag set. This flag has no effect on previous SQE submissions, nor does it impact SQEs that are outside of the chain tail. This means that multiple chains can be executing in parallel, or chains and individual SQEs. Only members inside the chain are serialized. A chain of SQEs will be broken, if any request in that chain ends in error. io_uring considers any unexpected result an error. This means that, eg, a short read will also terminate the remainder of the chain. If a chain of SQE links is broken, the remaining unstarted part of the chain will be terminated and completed with -ECANCELED as the error code. Available since 5.3. * 透過以 `IOSQE_IO_LINK` 的方式,將 `read sqe` 與 `timer sqe` 相互連結,一旦其中一方完成動作,兩個連結在一起的 `sqe` 都會被設定為完成,並將其設定為 `cqe` ,表示事件完成 * `IOSQE_IO_LINK Flag` 在 `Linux kernel 5.3` 以上才可使用 --- ### SQE Buffer Select > [io_uring : SubmissionEntryFlags](https://tchaloupka.github.io/during/during.io_uring.SubmissionEntryFlags.html) > [Sample Code : io_uring-echo-server](https://github.com/frevib/io_uring-echo-server/blob/master/io_uring_echo_server.c) * 以 `socket sever` 來說,在大量 `client` 同時連線下, `server` 若在分配 `data buffer` 上浪費過多時間,將造成服務器效能的降低 * 在 `io_uring` 使用 `Buffer Select` ,將 `client` 的資料寫入預先分配的 `buffer` ,當 `server` 需要使用到 `client data` 並進行回應時,再去從 `buffer` 讀取資料 * 將分配 `buffer` 的動作視為一個 `SQE` ,當系統擁有處理其他事件的資源時,再來處理空間分配的問題 根據 `IOSQE_BUFFER_SELECT Flag` 說明: > With IORING_OP_PROVIDE_BUFFERS, an application can register buffers to use for any request. The request then sets IOSQE_BUFFER_SELECT in the sqe, and a given group ID in sqe->buf_group. When the fd becomes ready, a free buffer from the specified group is selected. If none are available, the request is terminated with -ENOBUFS. If successful, the CQE on completion will contain the buffer ID chosen in the cqe->flags member, encoded as: > >(buffer_id << IORING_CQE_BUFFER_SHIFT) | IORING_CQE_F_BUFFER; * `read` 或 `recv` 讀取的內容,將會暫時存放於 `buffer` ,並將 `buffer ID` 紀錄在 `cqe->flags` 中,透過將 `cqe->flags` 向右移動 `IORING_CQE_BUFFER_SHIFT bits` 即可得到 `buffer ID` * `IOSQE_BUFFER_SELECT Flag` 在 `Linux kernel 5.7` 以上才可使用 --- ## Web Server 開發過程 > [Github](https://github.com/zzzxxx00019/sehttpd) ### sendfile 原始程式碼: ```c= int srcfd = open(filename, O_RDONLY, 0); assert(srcfd > 2 && "open error"); char *srcaddr = mmap(NULL, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0); assert(srcaddr != (void *) -1 && "mmap error"); close(srcfd); writen(fd, srcaddr, filesize); munmap(srcaddr, filesize); ``` * 原本的方法是透過 `fd` 去讀取 `filename` 內容,再透過 [`mmap`](https://man7.org/linux/man-pages/man2/mmap.2.html) 將內容映射到虛擬記憶體,最後再使用 `write` 將內容寫入 `sockfd` `sendfile` 改寫: ```c= int srcfd = open(filename, O_RDONLY, 0); assert(srcfd > 2 && "open error"); n = sendfile(fd, srcfd, 0, filesize); assert(n == filesize && "sendfile"); close(srcfd); ``` * 將 `srcfd` 的內容直接在 `kernel` 複製到 `sockfd` ,省略將 `kernel buffer` 複製到 `user space` 的效能浪費 --- ### memory pool > [Malloc segmentation fault](https://stackoverflow.com/questions/22051294/malloc-segmentation-fault?fbclid=IwAR3zllKtFLtfTMrTYGFTJhRid5gv9lfxm4vLZBQb2-WWJ-Ee4vMXvu8_Yqo) > [Memory Pool](https://en.wikipedia.org/wiki/Memory_pool) * 不斷使用 `malloc` 來動態分配記憶體在程式長期執行或短期間大量呼叫,容易造成記憶體區段錯誤`segmentation fault` * 在實作 `io_uring` 過程,需反覆使用 `http_request_t` 這個結構紀錄 `event` 狀況,透過 `memory pool` 以分配回收的方式,給予系統 `request` 位址,使用完成後,再將 `request` 回收,供下一次執行時需要分配 #### http_request_t memory pool 實作 `parameters` ```c= #define PoolLength Queue_Depth #define BitmapSize PoolLength/32 uint32_t bitmap[BitmapSize]; http_request_t *pool_ptr; ``` * `PoolLength` 代表將預先分配多少個 `http_request_t` 的結構型態記憶體空間,考慮到每個 `event` 都需要 `request` 去記錄狀態,因此數量必須等同於 `Queue_Depth` * 利用 `bitmap` 的方式去記錄每個 `request` 的使用狀態,每個 `bitset` 可記錄 `32` 個 `request` ,因此 `BitmapSize` 必須等同於 `PoolLenght/32` * `pool_ptr` 紀錄分配記憶體後,回傳的 `address` `initilize memory pool` ```c= int init_memorypool() { pool_ptr = calloc(PoolLength ,sizeof(http_request_t)); for (int i=0 ; i <PoolLength ; i++) { if(! &pool_ptr[i]) { printf("Memory %d calloc fail\n",i); exit(1); } (&pool_ptr[i])->pool_id = i ; } return 0; } ``` * 使用 `calloc` 預先分配結構記憶體, `calloc` 適合用來分配複雜結構型態的記憶體空間,回傳數個 `block` 如同陣列,方便程式撰寫 `get request` ```c= inline http_request_t *get_request() { int pos; uint32_t bitset ; for (int i = 0 ; i < BitmapSize ; i++) { bitset = bitmap[i]; if(!(bitset ^ 0xffffffff)) continue; for(int k = 0 ; k < 32 ; k++) { if (!((bitset >> k) & 0x1)) { bitmap[i] ^= (0x1 << k); pos = 32*i + k; return &pool_ptr[pos]; } } } return NULL; } ``` * 計算 `bitmap` 中,回傳首個不為 `1` 的位置,代表該 `request` 尚未被使用 * 透過 `inline` 展開迴圈,降低 `jump` 開銷 `free request` ```c= int free_request(http_request_t *req) { int pos = req->pool_id; bitmap[pos/32] ^= (0x1 << (pos%32)); return 0; } ``` * 將 `bitmap` 中,該 `request` 的位置設為 `0` ,代表下個使用者可使用 --- ### Computed goto 在 `http_parser.c` 中,兩個 function(`http_parse_request_line`、`http_parse_request_body`)使用了 switch,為了改善效能,改用 `computed goto` 。 1. 先修改 `Makefile` 加入 `CFLAGS += -fno-gcse -fno-crossjumping`。 2. 建立 dispatch table,紀錄各 label 的位址。 ```c staticc const void* dispatch_talbe[]={ &&s_start, &&s_method, &&s_spaces_before_uri, &&s_after_slash_in_uri, &&s_http, &&s_http_H, &&s_http_HT, &&s_http_HTT, &&s_http_HTTP, &&s_first_major_digit, &&s_major_digit, &&s_first_minor_digit, &&s_minor_digit, &&s_spaces_after_digit, &&s_almost_done }; ``` 3. 設定 `DISPATCH()`,每 label 執行完後要再 goto 到下一個 label。 ```c #define DISPATCH(){ \ pi++; \ if(pi >= r->last){ \ goto END;} \ p = (uint8_t *) &r->buf[pi% MAX_BUF]; \ ch = *p; \ goto *dispatch_table[state]; \ } ``` --- ### custom absolute value function 在 `http_process_if_modified_since` 中使用了,`fabs(x)` 來求 `time_diff` 的絕對值,我們可以額外在寫一個 function 來完成此事: ```c void absf(double *x) { int a = 0x1; char little_end = *((char *) &a); *(((int *) x) + little_end) &= 0x7fffffff; } ``` 主要是把 floating point 的 sign 都設為 `0` 即可,在這裡利用 `little_end` 來判斷所在的系統是採用 little endian 或 big endian。 - 參考:[浮點數運算和定點數操作](https://hackmd.io/@NwaynhhKTK6joWHmoUxc7Q/H1SVhbETQ?type=view) --- ### io_uring * 主要使用 `liburing` 提供的 `API` 去實作 * 為 [`Dec 19, 2020`](https://github.com/axboe/liburing/tree/f020d23cb0dc70cc301b1e489aa28ef727bec1a1) 更新版本 * 為使 `liburing.h` 成功編譯,需在編譯過程加入 `-luring` ,避免出現找不到 `library` 的錯誤 `Makefile` ```diff= OBJS = \ + src/memory_pool.o \ + src/uring.o \ - src/timer.o \ src/http.o \ src/http_parser.o \ src/http_request.o \ src/mainloop.o deps += $(OBJS:%.o=%.o.d) $(TARGET): $(OBJS) $(VECHO) " LD\t$@\n" - $(Q)$(CC) -o $@ $^ $(LDFLAGS) + $(Q)$(CC) -o $@ $^ $(LDFLAGS) -luring ``` --- * 將 `socket` 的主要三個 `I/O operation` 以 `io_uring` 方式實作,並分別包裝成 `function` * 預計使用這些 `function` 取代原本的 `accept` 、 `read` 、 `write` `io_uring` 實作 `Web Server` 流程圖: ![](https://i.imgur.com/EdflL9R.png) --- * 在 `http.h` 中,針對 `io_uring` 所需,新增了一些參數,並將原本的 `buffer` 改以指標的方式,直接指向 `io_uring` 所分配給該動作的緩衝 `buffer` ```diff= typedef struct { void *root; int fd; - int epfd; - char buf[MAX_BUF]; /* ring buffer */ + char *buf; size_t pos, last; int state; void *request_start; int method; void *uri_start, *uri_end; int http_major, http_minor; void *request_end; struct list_head list; /* store http header */ void *cur_header_key_start, *cur_header_key_end; void *cur_header_value_start, *cur_header_value_end; - void *timer; + bool keep_alive; + int bid ; + int event_type ; + int pool_id ; } http_request_t; ``` * `bid` 為存取所分配到的 `buffer id` * `event_type` 為針對不同動作,紀錄動作形式的參數 --- `add accept request` ```c= void add_accept(struct io_uring *ring, int fd, struct sockaddr *client_addr, socklen_t *client_len, http_request_t *req) { struct io_uring_sqe *sqe = io_uring_get_sqe(ring); io_uring_prep_accept(sqe, fd, client_addr, client_len, 0); io_uring_sqe_set_flags(sqe, 0); req->event_type = accept; req->fd = fd; io_uring_sqe_set_data(sqe, req); } ``` * 當 `accept` 完成, `cqe->res` 預計回傳 `client fd` --- `add read request` ```c= void add_read_request(http_request_t *request) { int clientfd = request->fd ; struct io_uring_sqe *sqe = io_uring_get_sqe(&ring) ; io_uring_prep_recv(sqe, clientfd, NULL, MAX_MESSAGE_LEN, 0); io_uring_sqe_set_flags(sqe, (IOSQE_BUFFER_SELECT | IOSQE_IO_LINK) ); sqe->buf_group = group_id; request->event_type = read ; io_uring_sqe_set_data(sqe, request); struct __kernel_timespec ts; msec_to_ts(&ts, TIMEOUT_MSEC); sqe = io_uring_get_sqe(&ring); io_uring_prep_link_timeout(sqe, &ts, 0); http_request_t *timeout_req = get_request(); timeout_req->event_type = uring_timer ; io_uring_sqe_set_data(sqe, timeout_req); io_uring_submit(&ring); } ``` * 以 `io_uring_prep_recv` 實作讀取 `client fd` ,並使用 `select buffer` 機制 * 當 `recv` 完成, `cqe->res` 預計回傳成功讀取幾 `bytes` 的資料 * 使用 `io_uring_prep_link_timeout` 實作 `SQE Timeout Delete` 機制,避免佔用服務器空間與排除使用者中斷所帶來的錯誤 --- `add write request` ```c= void add_write_request(int fd, void *usrbuf, size_t n, http_request_t *r) { char *bufp = usrbuf; struct io_uring_sqe *sqe = io_uring_get_sqe(&ring) ; http_request_t *request = r; request->event_type = write ; unsigned long len = strlen(bufp); io_uring_prep_send(sqe, fd, bufp, len, 0); io_uring_sqe_set_flags(sqe, IOSQE_IO_LINK); io_uring_sqe_set_data(sqe, request); struct __kernel_timespec ts; msec_to_ts(&ts, TIMEOUT_MSEC); sqe = io_uring_get_sqe(&ring); io_uring_prep_link_timeout(sqe, &ts, 0); http_request_t *timeout_req = get_request(); timeout_req->event_type = uring_timer ; io_uring_sqe_set_data(sqe, timeout_req); io_uring_submit(&ring); } ``` * 以 `io_uring_prep_send` 實作將資料寫入 `client fd` 的動作 * 當 `send` 動作完成,預計回傳成功寫入 `client fd` 的 `data bytes` * 當 `send` 完成, `cqe->res` 預計回傳成功寫入幾 `bytes` 的資料 * 以 `io_uring_prep_link_timeout` 實作 `SQE Timeout delete` 機制,偵測資料未成功寫入的錯誤 :::info 在 `writev(2)` 中,有特別提到該動作是一個 atomic 的操作, 也就是在寫入這個完成前並不會被 interrupt。 而在 `write(2)` 中,有特別強調最後寫入的資料數可會比預期寫入的少, 所以才需要加入偵錯機制。若使用 `writev(2)` 中,就不用。 [write(2)](https://man7.org/linux/man-pages/man2/writev.2.html) > The number of bytes written may be less than count if. [writev(2)](https://man7.org/linux/man-pages/man2/writev.2.html) > readv() and writev() are **atomic**: the data written by writev() is written as a single block that is not intermingled with output from writes in other processes. ::: :::spoiler `writen` 擁有偵錯機制,判斷 `usrbuf` 是否成功全數寫入 `fd` ```c= static ssize_t writen(int fd, void *usrbuf, size_t n) { ssize_t nwritten; char *bufp = usrbuf; for (size_t nleft = n; nleft > 0; nleft -= nwritten) { if ((nwritten = write(fd, bufp, nleft)) <= 0) { if (errno == EINTR) /* interrupted by sig handler return */ nwritten = 0; /* and call write() again */ else { log_err("errno == %d\n", errno); return -1; /* errrno set by write() */ } } bufp += nwritten; } return n; } ``` ::: --- `add provide buffer request` ```c= void add_provide_buf(int bid) { struct io_uring_sqe *sqe = io_uring_get_sqe(&ring); io_uring_prep_provide_buffers(sqe, bufs[bid], MAX_MESSAGE_LEN, 1, group_id, bid); io_uring_sqe_set_flags(sqe, 0); http_request_t *req = get_request(); assert(req && "malloc fault"); req->event_type = prov_buf; io_uring_sqe_set_data(sqe, req); } ``` * 告知 `io_uring` 有哪些 `buffer` 可用來作為資料緩衝區 --- * 在 `io_uring_loop` 針對不同的 `cqe->event_type` ( 代表已完成事件 ),去新增下個 `sqe` ( 待做事件 ) ```c= while (1) { submit_and_wait(); struct io_uring_cqe *cqe; unsigned head; unsigned count = 0; io_uring_for_each_cqe(ring, head, cqe) { ++count; http_request_t *cqe_req = io_uring_cqe_get_data(cqe); int type = cqe_req->event_type; if (type == accept) { add_accept(ring, listenfd, (struct sockaddr *) &client_addr, &client_len, cqe_req); int clientfd = cqe->res; if (clientfd >= 0) { http_request_t *request = get_request(); init_http_request(request, clientfd, WEBROOT); add_read_request(request); } } else if (type == read) { int read_bytes = cqe->res; if (read_bytes <= 0) { int ret = http_close_conn(cqe_req); assert(ret == 0 && "http_close_conn"); } else { cqe_req->bid = (cqe->flags >> IORING_CQE_BUFFER_SHIFT); do_request(cqe_req, read_bytes); } } else if (type == write) { add_provide_buf(cqe_req->bid); int write_bytes = cqe->res; if (write_bytes <= 0) { int ret = http_close_conn(cqe_req); assert(ret == 0 && "http_close_conn"); } else { if (cqe_req->keep_alive == false) http_close_conn(cqe_req); else add_read_request(cqe_req); } } else if (type == prov_buf) { free_request(cqe_req); } else if (type == uring_timer) { free_request(cqe_req); } if (count > 4096) break; } uring_cq_advance(count); } ``` * `accept` * 表示完成 `accept` 的動作 * 新增 `accept request` 提供下個 `client` 的連線 * 建立一個 `request` 儲存這個 `client` 的連線狀態 * 新增 `read request` ,讀取 `client fd` 的內容 * `read` * 表示完成 `read` 的動作 * 若回傳的成功讀取 `bytes` 數低於或等於 `0` ,視為錯誤或事件超時,直接關閉該使用者連線 * 呼叫 `do_request` ,針對 `client fd` 給予回應 * `write` * 表示完成 `write` 的動作 * 若連線狀態非 `keep_alive` ,完成 `write` 動作後關閉連線 * 重新回到對該 `client fd` 預備 `read` 的狀態,並將原本讀取 `client` 所使用到的 `buffer` 空間釋放回 `io_uring` 的 `select buffer` 機制 * `prov_buf` * 表示完成 `select buffer` 空間分配的動作 * `uring_timer` * 表示連結的 `event` 完成動作,或是 `timeout` 觸發 * 當 `timeout` 觸發,連結的 `event` 將強制中斷,並回傳 `cqe->res` * 當 `recv` 或 `send` 因 `timeout` 觸發中斷,預計該 `event` 回傳的 `result` 將小於 `0` ,視為使用者已超時未使用 `sever` 服務,系統設計將會把 `client fd` 關閉 * 當累積處理 `CQE` 數量達 `4096` ,告知 `io_uring` 並清除已完成事項,避免 `ring` 無法提供空間供 `SQE` 使用 --- * 在 `http.c` 中,取消迴圈讀取 `client fd` 的方式 `do_request` ```diff= +r->buf = get_bufs(r->bid); +r->pos = 0; +r->last = n; -char *plast = &r->buf[r->last % MAX_BUF]; -size_t remain_size = - MIN(MAX_BUF - (r->last - r->pos) - 1, MAX_BUF - r->last % MAX_BUF); -int n = read(fd, plast, remain_size); -assert(r->last - r->pos < MAX_BUF && "request buffer overflow!"); -if (n == 0) /* EOF */ - goto err; -if (n < 0) { - if (errno != EAGAIN) { - log_err("read err, and errno = %d", errno); - goto err; - } - break; -} -r->last += n; -assert(r->last - r->pos < MAX_BUF && "request buffer overflow!"); ``` * 將原本的 `r->buf` 改為 `char` 指標的型態,直接指向 `io_uring` 所分配的 `buffer` 位址 ```diff= +if (!out->keep_alive) + r->keep_alive = false; -if (!out->keep_alive) { - debug("no keep_alive! ready to close"); - free(out); - goto close; -} ``` * 若連線方式非 `keep_alive` ,該 `request` 把 `keep_alive` 的 `flag` 設為 `false` ,隨後將連線中斷,因 `io_uring` 使用該 `request` 作為 `user_data` ,若直接在這裡關閉連線並歸還 `request` 至 `memry pool` ,會發生 `cqe` 的 `user data` 已歸還的問題,因此該動作拉至 `main loop` 執行 ```diff= void handle_request(void *ptr, int n) { ... - for (;;) { - ... - int n = read(fd, plast, remain_size); - ... - } ... ``` * 在 `epoll` 中,系統會告知 `fd` 的 `I/O` 準備就緒,但使用者可能一次無法完全 `read` 內容,故在舊有的程式中,以迴圈檢查是否仍有內容可以 `read` ,且 fd 的狀態被設為 non_blocking ,故當 `n < 0` 且 `n == EAGAIN` 表示目前無內容可以讀取,直接 `break` 掉迴圈 * 若使用 `io_uring` ,每個 `sqe` 都 `block` 住等待 `I/O` 就緒,因此實作部分是直接再新增一個 `read request` ,若尚有內容未完成 `read` ,系統會馬上再執行一次 `read` ,反之,則 `block` 等待下次的 `I/O` 就緒 --- * `server_static` 主要負責對 `client` 做回應的動作 * 使用 `add_write_request` 取代原本的 `writen` ```diff= +add_write_request(header, r); -size_t n = (size_t) writen(fd, header, strlen(header)); ``` --- ## Epoll vs. io_uring 效能比較 * 實體電腦安裝 `ubuntu 20.04.1 LTS` 作業系統作為 `Server` * 使用 `Linux Kernel = 5.8.0-38-generic` 支援 `io_uring` 的 `select buffer flag` * 隔離 `CPU` 進行測試,透過 `taskset` 把行程安排給特定 `CPU` 核心 ### 測試結果說明 詳細內容請參考 [Epoll vs. io_uring 效能測試與比較](https://hackmd.io/@shanvia/B1Ds1vlAD) 。 * 使用 `ApacheBench` 進行壓力測試,針對不同的同時連線數量進行連線評估 * 在連線狀態為 `keep-alive` 的情況下,隨著同時連線數量的增加,在 `requests per second` 的評分上, `io_uring` 逐漸與 `epoll` 拉開差距 * 在測試數量為 `100000` 的壓力測試上,在 `failed requests` 的評分上, `io_uring` 表現明顯較 `epoll` 出色許多, `epoll` 的 `failed requests` 數量維持在 `1000` 上下,失敗率約為 `1%` ,相較於 `io_uring` ,並未出現 `failed request` 的狀況,即系統未偵測 `I/O Error` 而關閉連線 * 在連線狀態為 `no keep-alive` 的情況下,在 `requests per second` 表現上, `io_uring` 明顯優於 `epoll` ,代表從 `accept` 、 `read` 、 `write` 到 `close` 一連串的 `I/O` 處理上, `io_uring` 明顯有較好的表現,值得注意的是,隨著同時連線數量的上升 `io_uring` 的評分卻是逐漸降低,因為在任務排程上多半被 `accept` 動作佔據,而非優先處理 `read/write` 而導致效能下降 * 在 `do_request` 測試上,測試每次使用 `do_request function` 所耗費的時間 * 在實作改動上,透過 `io_uring` 提供的 `select buffer` 方式,透過預先分配的 `buffer` 取代舊有的 `ring buffer` 方式,在 `read operation` 都先將內容放置 `select buffer` ,再透過指標告知使用的是哪個 `buffer` ,降低大量連線進入,大量資料讀取的衝擊;而在 `switch` 方面上,以 `computed goto` 的方式取代,預期將降低 `branch-misses` 的發生 * 測試結果顯示, `io_uring` 版本在 `do_request` 消耗時間,明顯低於 `epoll` ,但其實所需時間微觀來看,差距並不明顯,主要影響因素還是在兩系統呼叫方法上的差別 * 在實際效能測試上,使用 `perf_event` 作為工具 * 透過 `ApacheBench` 作為連線輸入工具,參數為 `ab -n 100000 -c 1000 -k` * 實作過程使用 `memory pool` 取代反覆 `malloc` 所帶來的系統衝擊,在 `page-faults` 擁有明顯的改善 * 在 `CPU` 使用率上, `io_uring` 明顯低於 `epoll` ,而在其他評比項目, `io_uring` 所需的系統資源也較 `epoll` 減少許多 --- ## 已知問題 * ~~使用 `ApacheBench` 進行壓力測試時,在大量且眾多同時連線的測試上,程式停止回應~~ * 目前系統已可通過連線數量為 `100000` ,且同時連線數量為 `1000` 之壓力測試 :::warning TODO: 詳述測試方式,並善用原有的測試程式,提供可重現問題的測試場景 :notes: jserv :::