---
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
###