# 2022q1 seHTTPd 改進 contributed by < `haoyu0970624763` > > [作業說明](https://hackmd.io/@sysprog/linux2022-sehttpd) > [GitHub](https://github.com/haoyu0970624763/sehttpd) ### 壓力測試 * [apache bench](https://httpd.apache.org/docs/current/programs/ab.html) * 用法: `ab -n 100000 -c 500 -k http://127.0.0.1:8081/` * 無法有效反映出多執行緒的特性 * [htstress](https://github.com/sysprog21/khttpd/blob/master/htstress.c) * 用法: * 編譯: `gcc -std=gnu11 -Wall -Wextra -Werror -o htstress htstress.c -lpthread` * 使用: `./htstress -n 10000 -c 100 -t 8 localhost:8081` * 使用 epoll 觸發、可反映多執行緒的特性 * [wrk](https://github.com/wg/wrk) * 只支援 HTTP 通訊協定的連線請求(如 get, post 等),若要執行 get 之外的 HTTP 請求,需要使用者自行撰寫 lua 腳本 * 尚有 [httperf](https://github.com/httperf/httperf) , [weighttp](https://github.com/lighttpd/weighttp) 等壓力測試工具等 ### 使用 computed goto 改寫 switch-case 在 [Computed goto for efficient dispatch tables](https://eli.thegreenplace.net/2012/07/12/computed-goto-for-efficient-dispatch-tables) 一文中提及, goto 在執行效率上比 switch-case 要更好,主要可以從 2 個層面探討: * switch-case 會做 bounds checking , 檢查是否有 default case 的狀況,即使程式中的 switch 沒有寫到 default case,編譯期間仍會產生強制檢查的程式,這是為了符合 C99 的標準 > If no converted case constant expression matches and there is no default label, no part of the switch body is executed. * 另一部份則是 branch prediction branch prediction 的部份可參考老師之前撰寫的 [switch-背後的-goto-和實作考量](https://hackmd.io/@sysprog/c-control-flow?type=view#switch-%E8%83%8C%E5%BE%8C%E7%9A%84-goto-%E5%92%8C%E5%AF%A6%E4%BD%9C%E8%80%83%E9%87%8F) 我們透過 [Labels as Values](https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html),在 http_parser.c 的兩個函式中加入 conditions 作為 label address table 讓 state 進行查表,並透過 goto 跳躍到對應的程式碼。 以 `http_parse_request_body` conditions 為範例 ``` const void *conditions[] = { &&c_start, &&c_key, &&c_spaces_before_colon, &&c_spaces_after_colon, &&c_value, &&c_cr, &&c_crlf, &&c_crlfcr, }; ``` 接著用 goto 取代 switch ``` goto *conditions[state]; ``` 我用 perf 觀察原本的程式碼 以及 使用 computed goto 改寫後的效率 switch 版本的觀測紀錄如下 ``` $ sudo perf stat ./sehttpd -p 8081 $ ./htstress -n 100000 -c 1 -t 4 http:/127.0.0.1:8081/ requests: 100000 good requests: 100000 [100%] bad requests: 0 [0%] socket errors: 0 [0%] seconds: 4.626 requests/sec: 21617.794 Performance counter stats for './sehttpd -p 8081': 4,551.69 msec task-clock # 0.498 CPUs utilized 458 context-switches # 100.622 /sec 36 cpu-migrations # 7.909 /sec 100,070 page-faults # 21.985 K/sec 14,861,152,979 cycles # 3.265 GHz 10,629,920,230 instructions # 0.72 insn per cycle 2,043,158,393 branches # 448.879 M/sec 25,004,139 branch-misses # 1.22% of all branches ``` goto 版本的觀測紀錄如下 ``` $ sudo perf stat ./sehttpd -p 8081 $ ./htstress -n 100000 -c 1 -t 4 http://127.0.0.1:8081/ requests: 100000 good requests: 100000 [100%] bad requests: 0 [0%] socket errors: 0 [0%] seconds: 4.150 requests/sec: 24094.580 Performance counter stats for './sehttpd -p 8081': 4,093.64 msec task-clock # 0.207 CPUs utilized 359 context-switches # 87.697 /sec 34 cpu-migrations # 8.306 /sec 100,071 page-faults # 24.445 K/sec 13,730,441,736 cycles # 3.354 GHz 10,613,940,571 instructions # 0.77 insn per cycle 2,038,720,358 branches # 498.021 M/sec 24,397,318 branch-misses # 1.20% of all branches ``` 可以看出在 computed goto 版本中的執行的 cycles 少於 switch 版本的 且 branch miss rate 也略少於 switch 版本 在改寫的過程中有參照 [oscarshiang](https://hackmd.io/@oscarshiang/linux_sehttpd#2020q1-Homework6-sehttpd) 的共筆,不過在測量的過程中我沒辦法觀測出 branch miss rate 有明顯的改善,我更傾向在這個實驗中 goto 之所以效能更好的原因在於減少了`bounds checking` #### 程式碼更動 更改 buffer size 從 8124 更改為 8192 , 變成 2 的冪 , 我們就可以透過跟 (MAX_BUF-1) 做 bitwise-AND , 可取得 0~8191的範圍來取代 % 的運算 , 因為 & 運算比 % 來的更有效率 原本 ```c p = (uint8_t *) &r->buf[pi % MAX_BUF]; ``` 更改成 ```c p = (uint8_t *) &r->buf[pi & (MAX_BUF - 1)]; ``` :::warning 可將 `pi % MAX_BUF` 改為 `pi & (MAX_BUF - 1)`,當 `MAX_BUF` 為 2 的冪 詳見: [你所不知道的 C 語言: bitwise 操作](https://hackmd.io/@sysprog/c-bitwise) :notes: jserv ::: ### epoll 工作模式 節錄自 [epoll](https://man7.org/linux/man-pages/man7/epoll.7.html) 中 >monitoring multiple file descriptors to see if I/O is possible on any of them epoll 用來監聽 I/O 事件是否準備好了 , 而 epoll 又可分成 * Level-triggered * 當 file descriptors 準備就緒時 , kernel 透過 epoll 發送通知告知我們已就緒,接著就可以進行IO操作, 當我們長時間沒有進行操作時 , kernel 會繼續發送通知給我們 * socket 在 readable/writable 狀態時,epoll_wait會返回該socket * Edge-triggered * 當 file descriptors 從還沒準備好轉變成準備好時,kernel 通過 epoll 發送通知告知我們,並且不再發送第二次通知 以 [test_epoll_lt_and_et](https://github.com/Manistein/test_epoll_lt_and_et) 舉例說明可能會講的更清楚 在客户端输入aabbcc,客户端發送包含 aabbcc 内容的數據包到服務端,在服務端一次可讀取2個字元的情況下 * lt 模式下, 服務端先讀取 aa 並輸出 , epoll_wait 可返回 socket 繼續讀取資料 , 讀取 bb並輸出 , 依此類推 * et 模式下, 服務端先讀取 aa 並輸出 , epoll_wait 只有等到新的數據到達時才會被喚醒 , bbcc 會遺留在服務端的 buffer 我們可以看出 1. lt 模式下的操作更為直覺與簡單 2. et 模式下可能隱含較多的 bug 此外, et 模式的效能會比 lt 來的更好 , 原因在於當 kernel 發送通知後 , 會把該事件從 ready list 中刪除 , 而 lt 模式則不會從 ready list 中刪除該事件 , 當 list中有越來越多事件時 , epoll 下次要通知用戶時 , 搜尋 list 的時間會增長 兩種模式各有其優缺點 , 在高效的 web server 中 , 我認為採用 edge-triggered 的方式可能會更合適 ### timer web server 需要同時間處理多項請求 , 要優先處理哪個請求就變成一個重要的課題 , 而常見的作法之一是先處理最快到期的請求 在 [mainloop.c](https://github.com/haoyu0970624763/sehttpd/blob/master/src/mainloop.c) 中 server 接收請求後會先呼叫 [timer.c](https://github.com/haoyu0970624763/sehttpd/blob/master/src/timer.c) 中的`handle_expired_timers` 函式處理已經超出期限但還沒處理的請求 , 接著會呼叫 `add_timer` 函式將新的請求加入 queue 中 而 `add_timer` 中根據 `node->key = current_msec + timeout` (`current_msec` 表示當前的時間 , `timeout` 代表處理請求的最大延遲時間),顯而易見的可以發現 node->key 值代表的是此請求處理的期限 我們想取得最快到期的請求就可以透過 priority queue(MIN_HEAP)的方式獲取 , 也就是 `timer.c` 中的 `prio_queue_min` 函式