--- tags: linux2022 --- # 2022q1: 高效網頁伺服器 contributed by < `blueskyson` > > [GitHub](https://github.com/blueskyson/sehttpd/tree/blueskyson) 探討從無到有打造 Linux 平台的高效能網頁伺服器,涵蓋是否該將伺服器實作於 Linux 核心內部、並行處理、I/O 模型、epoll、Reactor pattern,和 Web 伺服器在事件驅動架構的考量。 > [專題列表](https://hackmd.io/@sysprog/linux2022-projects) 相關資訊: - [sehttpd 作業說明](https://hackmd.io/@sysprog/linux2022-sehttpd) - [sysprog21/timeout](https://github.com/sysprog21/timeout): Tickless Hierarchical Timing Wheel - [2020 年開發紀錄](https://hackmd.io/@jwang0306/final-project) / [GitHub](https://hackmd.io/@jwang0306/sehttpd) - [2021 年開發紀錄](https://hackmd.io/@XDEv11/sehttpd-project) - [高效 Web 伺服器開發](https://hackmd.io/@sysprog/fast-web-server) ## 測試環境 這兩年來我所習慣的開發環境為 Ubuntu20.04,但在此次作業中使用 [bcc](https://github.com/iovisor/bcc) 的 eBPF 時,不管使用編譯好的工具或是編譯原始碼都遇到諸多問題,所以選擇在 Arch Linux 上開發。 ``` $ cat /proc/version Linux version 5.18.0-arch1-1 (linux@archlinux) (gcc (GCC) 12.1.0, GNU ld (GNU Binutils) 2.38) #1 SMP PREEMPT_DYNAMIC $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian Address sizes: 39 bits physical, 48 bits virtual CPU(s): 12 On-line CPU(s) list: 0-11 Thread(s) per core: 2 Core(s) per socket: 6 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 165 Model name: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz Stepping: 2 CPU MHz: 2600.000 CPU max MHz: 5000.0000 CPU min MHz: 800.0000 BogoMIPS: 5199.98 Virtualization: VT-x L1d cache: 192 KiB L1i cache: 192 KiB L2 cache: 1.5 MiB L3 cache: 12 MiB NUMA node0 CPU(s): 0-11 ``` ### 相依套件 ```python # dev tools $ sudo pacman -S bcc bcc-tools python-bcc linux-header # git hooks $ sudo pacman -S cppcheck clang aspell colordiff # benchmark tools $ yay -S apache-tools $ git clone https://github.com/sysprog21/sehttpd ``` ## 效能測量環境 隔離 cpu0 到 cpu3,這四個 cpu 只會執行 Read-copy-update,以及藉由 `taskset` 來把 `sehttpd` 固定在這四個 cpu 執行,藉此讓 `sehttpd` 被其他 process 搶佔的機會降到最低。 其餘 linux 系統調校還有抑制 ASLR、關閉 intel turbo mode 使得 cpu 頻率維持穩定。 ### 隔離特定 cpu 最常見的方法為修改 `/etc/default/grub`,使其包含以下 `GRUB_CMDLINE_LINUX="isolcpus=0-3"` 再執行 `update-grub` 來達成目的。 但是我的 arch linux 一直無法成功修改 isolcpu 這個參數,所以我用另一種方式。首先在開機時進到 grub 選單,選取將要進入的作業系統,還不要按 `enter`: ![](https://i.imgur.com/GoUjTVX.jpg) 按下 `e` 編輯開機參數: ![](https://i.imgur.com/IsXWQeq.jpg) 在倒數第二行的最後面加入 `isolcpus=0-3`: ``` fi linux ... quiet isolcpus=0-3 initrd ... ``` 按下 `ctrl x` 或 `F10` 開機,用 taskset 檢查 cpu 0 到 3 是否已經被 isolate: ``` $ taskset -p 1 pid 1's current affinity mask: ff0 ``` ### 排除其餘干擾因素 1. 抑制 [address space layout randomization](https://en.wikipedia.org/wiki/Address_space_layout_randomization) (ASLR) ``` $ sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space" ``` 2. 設定 scaling_governor 為 performance: ```bash # performance.sh for i in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor do echo performance > ${i} done ``` 執行腳本 ``` $ sudo sh performance.sh ``` 3. 針對 Intel 處理器,關閉 turbo mode: ``` $ sudo sh -c "echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo" ``` ## 簡介 `epoll` 與 seHTTPd 的架構 `epoll` 是 `poll` 的變體,允許單一個執行緒值監看多個 file descriptor 的事件,在使用者監看的事件發生時回傳以通知使用者。事件通知的時機又分為 level trigger 和 edge trigger,在 edge trigger 模式中,只有事件觸發,或是 timeout 時 `epoll_wait` 才會回傳;在 level trigger 模式,`epoll_wait` 在事件狀態未變更前都會回傳。 sehttpd 是採用 edge trigger,在 mainloop.c 的關鍵程式碼如下: ```c int main(int argc, char **argv) { // ... int listenfd = open_listenfd(cfg->port); // open server's port int epfd = epoll_create1(0); // init epoll http_request_t *request = malloc(sizeof(http_request_t)); struct epoll_event event = { .data.ptr = request, .events = EPOLLIN | EPOLLET, // use edge trigger }; epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event); // watch listenfd // ... while (1) { int n = epoll_wait(epfd, events, MAXEVENTS, time); // event for (int i = 0; i < n; i++) { http_request_t *r = events[i].data.ptr; int fd = r->fd; if (listenfd == fd) { /* handle incomming connections */ int infd = accept(listenfd, (struct sockaddr *) &clientaddr, &inlen); // ... init_http_request(request, infd, epfd, WEBROOT); event.data.ptr = request; event.events = EPOLLIN | EPOLLET | EPOLLONESHOT; epoll_ctl(epfd, EPOLL_CTL_ADD, infd, &event); } else { /* response to a request */ // ... do_request(events[i].data.ptr); } } } } ``` ## 修正 seHTTPd 的缺失 將 seHTTPd 執行後,可以透過 `ab` 進行壓力測試,以下會同時開啟 1000 個連線,總共 50000 次連線連到 `localhost:8081`: ``` $ ab -n 50000 -c 1000 -k http://127.0.0.1:8081/ ``` 只要短時間連線次數夠多,會顯示錯誤訊息: ``` [ERROR] (src/http.c:253: errno: Resource temporarily unavailable) rc != 0 ``` ### 找到錯誤的回傳點 首先 `rc` 為 `http_parse_request_line(r)` 的回傳值,為了知道 `rc` 確切的值,我將 http.c 第 253 行的的 `log_err` 改寫為: ```c log_err("rc == %d", rc); ``` 如此一來透過 `ab` 測試所顯示的錯誤訊息變為: ``` [ERROR] (src/http.c:253: errno: Resource temporarily unavailable) rc == 10 ``` 從 http.h 中可以找到 `HTTP_PARSER_INVALID_METHOD` 所對應的值即為 `10`: ```c enum http_parser_retcode { HTTP_PARSER_INVALID_METHOD = 10, HTTP_PARSER_INVALID_REQUEST, HTTP_PARSER_INVALID_HEADER }; ``` 在 http_parser.c 的第 54 行和第 92 行都會回傳此值: ```c=54 if ((ch < 'A' || ch > 'Z') && ch != '_') return HTTP_PARSER_INVALID_METHOD; ``` ### 觀察 `read` 回傳值 我猜測標記 `r->buf` 的起始點 `r->pos` 指到錯誤的位置,或是超出陣列範圍(這個猜測是錯的,真正原因是下方黃底句子)。所以我將 http.c 的第 231 行的 `read` 下方印出 `read` 和 `r->pos` 的回傳值,檢查在什麼情況下會出問題: ```c=231 int n = read(fd, plast, remain_size); printf("n = %d, r->pos = %d\n", n, r->pos); ``` 此時再執行測試,錯誤訊息變為: ```= Web server started. n = 106, r->pos = 0 n = -1, r->pos = 106 n = 106, r->pos = 106 n = -1, r->pos = 212 ... n = -1, r->pos = 8056 n = 68, r->pos = 8056 n = 38, r->pos = 8124 [ERROR] (src/http.c:254: errno: Resource temporarily unavailable) rc == 10 n = 106, r->pos = 0 n = 106, r->pos = 106 n = 106, r->pos = 212 ``` 由上述輸出可以得知,藉由 `ab` 發送的 http request 大小總共為 106 byte,而上面第 10、11 行發生錯誤的情況為:`r->pos` 距離 `MAX_BUF` (8124) 只剩 68 個 byte,所以沒辦法一次讀取 106 byte,下一次讀取 file descriptor 時,讀進來的是同一個 request 剩餘的 38 byte。然而原始==程式將這 38 byte 當作下一個 request 的 head 來解析==,理所當然會 parse error! 注意到 `read` 的回傳值 `n` 等於 `-1` 是正常現象,代表 `EAGAIN`,在 non-blocking I/O 代表資料尚未準備就緒,稍後再讀取一次。 ### 修正 ring buffer 缺失 我的想法是,如果 `n` 等於 `remain_size`,代表 buffer 讀到尾端,很有可能還有存在 `fd` 的資料尚未被讀取,所以要再讀取剩餘的資料,複製到 `r->buf` 的頭端。 在 http.c 的第 231 行後面插入以下程式碼: ```c=231 int n = read(fd, plast, remain_size); if (n == remain_size) { int n_again = read(fd, r->buf, MAX_BUF - n); // read again if (n_again > 0) n += n_again; } ``` 對應的修改在 [5217ef7](https://github.com/blueskyson/sehttpd/commit/5217ef7422b989f884dd37e505a516c8089fea2a)。 ## 完成 seHTTPd 的 TODO ###