# KTCP contributed by < `csotaku0926` > ## 自我檢查清單 - [ ] 如何測試網頁伺服器的效能,針對多核處理器場景調整 古典方法可用 `ab` ([Apache bending tool](https://httpd.apache.org/docs/current/programs/ab.html)) 這項工具進行伺服器的壓力測試 例如測量 `sehttpd` 伺服器 就以下指令來說 ```shell $ ab -n 10000 -c 500 -k http://127.0.0.1:8081/ ``` - `-n` : 在 benchmarking 階段發送 10000 條請求, - `-c` : 在同一時間發送的請求數量,concurrecny - `-k` : 開啟 HTTP "Keep Alive" 設置,也就是在同一 HTTP session 執行多個請求 部份測量結果數據: ``` Time taken for tests: 0.905 seconds Complete requests: 10000 Failed requests: 0 Keep-Alive requests: 10000 Total transferred: 4180000 bytes HTML transferred: 2410000 bytes Requests per second: 11053.58 [#/sec] (mean) Time per request: 45.234 [ms] (mean) Time per request: 0.090 [ms] (mean, across all concurrent requests) Transfer rate: 4512.11 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.8 0 6 Processing: 2 44 10.2 44 61 Waiting: 0 2 1.5 1 9 Total: 5 44 9.4 44 61 ``` 但 `ab` 無法反映多執行緒特性(自身已消耗單核 100% 運算量) 所以使用 [wrk](https://github.com/wg/wrk) 這項工具,可以針對多核場景測量 ```shell $ wrk -t8 -c400 -d30s http://127.0.0.1:8081 ``` 這項工具允許開啟多個執行緒,可依據測試端的 CPU 數量進行調整 ``` Running 2s test @ http://127.0.0.1:8081 8 threads and 500 connections Thread Stats Avg Stdev Max +/- Stdev Latency 7.23ms 0.97ms 15.18ms 91.45% Req/Sec 8.54k 0.89k 18.95k 95.00% 135956 requests in 2.03s, 47.84MB read Requests/sec: 67050.39 Transfer/sec: 23.60MB ``` 注意到利用 `ab` 以及 `wrk` 所測量出的 transfer rate 相差二十幾個 MBytes 也可以使用 `htstress` 工具 另外,經過長時間的開啟 `sehttpd`,出現以下錯誤訊息 ``` [ERROR] (src/http.c:32: errno: Broken pipe) errno == 32 ``` - [ ] 研讀 [透過 eBPF 觀察作業系統行為](https://hackmd.io/@sysprog/linux-ebpf),如何用 eBPF 測量 kthread / CMWQ 關鍵操作的執行成本? > [eBPF簡單介紹](https://hackmd.io/@RinHizakura/S1DGq8ebw#%E9%80%8F%E9%81%8E-libbpf-%E6%92%B0%E5%AF%AB-BPF-%E7%A8%8B%E5%BC%8F%E7%A2%BC) 動態追蹤允許「非侵入」的方式,不需更動內部系統的運作,可以獲取需要的資訊 傳統的封包過濾,需要將位於核心的封包傳進 (當然一開始封包是由網卡接收) 使用者空間 (user space),而 BPF 的核心概念為讓使用者透過額外的過濾程式告訴核心,應該過濾哪些封包。 好處顯而易見,可以在封包一進入核心空間就進行過濾,避免無用封包進入網路堆疊 (network stack) 到應用層 以網路封包的獲取為例,`tcpdump` 將透過 `libpcap` 轉譯後的濾包條件,送給位於核心的 BPF 模組,再由其將符合條件的封包送回 `tcpdump` eBPF 允許使用者以高效率的方式,撰寫程式附加於 Linux 核心內部,以達到事件監聽,追蹤系統呼叫等功能 - [ ] kthread 的執行成本是什麼?? CMWQ 呢? 當我們提到測量 kthread 執行成本,我們的關注點在於其執行花費時間 ,可以使用 eBPF 測量 ### khttpd 效能測量 觀察測試的 BPF 程式碼: ```c #include <uapi/linux/ptrace.h> BPF_HASH(start, u64); int BPF_kprobe(struct pt_regs *ctx) { u64 ts = bpf_ktime_get_ns(); bpf_trace_printk("in %llu\\n",ts); return 0; } int BPF_kretprobe(struct pt_regs *ctx) { u64 ts = bpf_ktime_get_ns(); bpf_trace_printk("out %llu\\n",ts); return 0; } ``` 其中 `BPF_HASH(start, u64)` ([ref](https://android.googlesource.com/platform/external/bcc/+/refs/heads/android10-c2f2-s1-release/docs/reference_guide.md#2-bpf_hash)) 創建一個名叫 `start` 的雜湊表,他的 key 是 `struct request*` 形式,value (這裡為 timestamp) 形式則是 `u64` `kprobe` 允許使用者自行定義 callback function,並動態將探針插入大多核心函式與模組 `kretprobe` 則是用來取得 `kprobe` 的回傳值 在 callback 函式中,`bpf_trace_printk` 將輸出 log 儲存於 /sys/kernel/debug/tracing/trace_pipe 可以用 python `bcc.BPF` module 的 `trace_print` 取得結果 問題是,`kthread_run` 並不是列於 `/proc/kallsyms` 中的符號之一,而是巨集 [include/linux/kthread.h](https://elixir.bootlin.com/linux/latest/source/include/linux/kthread.h#L51) 中的定義 ```c #define kthread_run(threadfn, data, namefmt, ...) \ ({ \ struct task_struct *__k \ = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \ if (!IS_ERR(__k)) \ wake_up_process(__k); \ __k; \ }) ``` 因此,在測量 `khttpd` 中 `kthread_run` 時間成本時,需要額外添加一個 function wrapper `my_kthread_wrapper` 包裝起來 但是 eBPF 應該只能測量系統呼叫,(如 `/proc/kallsyms` 列舉的呼叫),要怎麼測量這個 wrapper ? 目前無法測量 `my_kthread_wrapper` , 但是位於 `khttpd` 的執行緒函式 `http_server_worker` 卻可以被測量到 ```c int my_kthread_wrapper(struct socket *socket, struct task_struct *worker) { // printk(KERN_INFO "my_kthread_wrapper called"); worker = kthread_run(http_server_worker, socket, KBUILD_MODNAME); return IS_ERR(worker); } int http_server_daemon(void *arg) { ... // kthread_run(http_server_worker, socket, KBUILD_MODNAME); if (my_kthread_wrapper(socket, worker)) { pr_err("can't create more worker process\n"); continue; } } return 0; } ``` 測量的 BCC python code ```python b = BPF(text=code) b.attach_kprobe(event="http_server_worker", fn_name="BPF_kprobe") b.attach_kretprobe(event="http_server_worker", fn_name="BPF_kretprobe") while True: res = b.trace_fields() print(res[5].decode()) ``` 根據 vax-r 同學的想法,可能是編譯器優化,導致 wrapper 被無視,直接執行裡面的程式 初步解決方案是將 wrapper 內的功能寫的更多一點,還需要涉及記憶體配置,`ftrace` 才能捕捉到 後續的 `my_kthread_wrapper` 如下,這次就可以捕捉到了 > [commit 537ca0a](https://github.com/csotaku0926/khttpd/commit/537ca0a8071f0f9ee2959b0b4673f0a5a5504382) ```diff struct task_struct *my_kthread_wrapper(struct socket *socket) { // dummy kmalloc + char* buf = kmalloc(1, GFP_KERNEL); + if (!buf) { + pr_err("kmalloc\n"); + return NULL; + } + kfree(buf); // real code return kthread_run(http_server_worker, socket, KBUILD_MODNAME); } ``` 完成 eBPF 時間測量後,應用 [gnuplot](https://hackmd.io/@sysprog/Skwp-alOg) 繪圖 ![image](https://hackmd.io/_uploads/By3wGPxX0.png) `kthread_run` 的建立成本大多落在 200 us 以下 ### `kecho` 的 `kthread` 與 CMWQ 效能比較 壓力測試程式碼: ```shell $ ./htstress http://127.0.0.1:8081/ -c 1 -t 4 -n 100000 ``` `kthread_based` 的版本在收到連線請求後才會建立 `kthread` 而 CMWQ 中的 workqueue 可以根據任務執行狀況安排執行緒,並且採用 thread pool 的概念,預先建立好執行緒 測量過程為先透過 `make` 編譯出核心檔 (.ko) 再用 `./bench` 進行壓力測試 最後用 `gnuplot` 繪圖 根據 [官方文件](https://www.kernel.org/doc/html/v4.10/core-api/workqueue.html), > WQ_UNBOUND Work items queued to an unbound wq are served by the special worker-pools which host workers which are not bound to any specific CPU. `WQ_UNBOUND` 會讓 worker 不與特定 CPU 綁定 (bond) 為了測試 locality 效能,將參數 `bench` 設為真 - `user_echo_server` (kthread-based) ![image](https://hackmd.io/_uploads/rkQ6dDgQ0.png) - `kecho` (CMWQ-based) - `bench=true` ![image](https://hackmd.io/_uploads/SyDUmqeQ0.png) - `bench=false` ![image](https://hackmd.io/_uploads/B1dRQcxXA.png) 反而是 `bench=false` 看起來比較穩定,大多數資料都集中在一處 ## 作業要求 ### khttpd - [ ] 引入 CMWQ,分析效能表現並提出改進方案 根據 [改進功能與效能](https://hackmd.io/@sysprog/linux2024-ktcp/%2F%40sysprog%2Flinux2024-ktcp-c) 一文 ,CMWQ 版本的實作得益於 locality 以及事先準備的執行緒 面對大量連線時,CMWQ 的優勢較 kthread-based 還要突出 首先在 init_module 處配置 workqueue - [ ] 利用 Ftrace 找出 `khttpd` 核心模組效能瓶頸,以及該如何設計相關實驗學習。 > 搭配閱讀《Demystifying the Linux CPU Scheduler》第 6 章 [Ftrace](https://docs.kernel.org/trace/ftrace.html) 是個位於核心的動態追蹤工具,可用於追蹤函式、事件等 可藉由寫入 `/sys/kernel/debug/tracing` 內的檔案來設定 `ftrace` 例如,透過 `available_filter_functions` 列出 `khttpd` 核心程式中可以被追蹤的函式: ```shell $ sudo insmod khttpd.ko $ sudo cat /sys/kernel/debug/tracing/available_filter_functions | grep khttpd parse_url_char [khttpd] http_message_needs_eof [khttpd] http_should_keep_alive [khttpd] http_parser_execute [khttpd] http_method_str [khttpd] http_status_str [khttpd] ... ``` 接著嘗試追蹤 `http_server_worker` 函式 - `current_tracer` : 設定或顯示目前使用的 tracer ,如: `function`、`function_graph` - `set_ftrace_filter` : 指定要追蹤的函式 (只會追蹤他們) - `set_graph_function` : 指定要顯示呼叫關係的函式 - `tracing_on` : 設定或顯示使用的 tracer 是否寫入到 ring buffer - `max_graph_depth` : function graph tracer 最大追蹤深度 (呼叫 kernel function 數量) - `trace` : 紀錄追蹤輸出結果 ```sh #!/bin/bash TRACE_DIR=/sys/kernel/debug/tracing TARGET=http_server_worker # clear file echo 0 > $TRACE_DIR/tracing_on echo > $TRACE_DIR/set_graph_function echo > $TRACE_DIR/set_ftrace_filter echo nop > $TRACE_DIR/current_tracer echo > $TRACE_DIR/trace # settings echo function_graph > $TRACE_DIR/current_tracer echo 3 > $TRACE_DIR/max_trace_depth echo $TARGET > $TRACE_DIR/set_graph_function # execute echo 1 > $TRACE_DIR/tracing_on ../htstress http://127.0.0.1:8081/ -n 2000 echo 0 > $TRACE_DIR/tracing_on # output file cat $TRACE_DIR/trace > trace.txt ``` 最後,追蹤結果會在 `$TRACE_DIR/trace` 裡面 可以看到整個 `http_server_worker` 函式在各個內部函式所耗費的時間 以下節錄: ``` # tracer: function_graph # # CPU DURATION FUNCTION CALLS # | | | | | | | 7) | http_server_worker [khttpd]() { 7) | kernel_sigaction() { 7) 0.138 us | _raw_spin_lock_irq(); 7) 0.111 us | _raw_spin_unlock_irq(); 7) 0.694 us | } 7) | kernel_sigaction() { 7) 0.090 us | _raw_spin_lock_irq(); 7) 0.087 us | _raw_spin_unlock_irq(); 7) 0.418 us | } 7) | kmalloc_trace() { 7) 0.490 us | __kmem_cache_alloc_node(); 7) 0.873 us | } 7) 0.097 us | http_parser_init [khttpd](); 7) 0.089 us | kthread_should_stop(); 7) | http_server_recv.constprop.0 [khttpd]() { 7) 2.440 us | kernel_recvmsg(); 7) 2.611 us | } 7) | kernel_sock_shutdown() { 7) + 34.380 us | inet_shutdown(); 7) + 34.707 us | } 7) | sock_release() { 7) 3.229 us | inet_release(); 7) 0.097 us | module_put(); 7) 3.064 us | iput(); 7) 6.840 us | } 7) | kfree() { 7) 0.186 us | __kmem_cache_free(); 7) 0.491 us | } 7) + 48.728 us | } ... ``` ## 雜記 `kecho` 是 Linux 核心模組的 TCP 伺服器 telnet 是 application-layer ,使用 TCP/IP 與遠端伺服器溝通的協議 seHTTPd 是高效網路伺服器 `khttpd` 與 `kecho` 在掛載階段時,差異在後者使用 CMWQ 函式 `alloc_workqueue` 在 `open_listen_socket` 中,進行 socket 與 TCP 連線相關設定: `TCP_NODELAY` 是關閉 Nagle's 算法 `TCP_CORK` 為了將零碎資料彙整為完整封包後再發送 可以發現伺服器本身,以及每個連線都會以 `kthread` 創建新的執行緒 建立 socket 後,呼叫 `kthread_run` 並執行函式 `http_server_daemon` 與 `kecho` 邏輯相似 首先利用 `allow_signal` 登記要接收的 `SIGKILL` , `SIGTERM` 再來以 `kthread_should_stop` 判斷是否中止負責執行 `http_server_daemon` 的執行緒 使用函式 `kernel_accept` 接收連線,若成功建立則使用 `kthread_run` 建立新的執行緒執行 `kthread_worker` `kthread_worker` 函式執行以下行為: 1. 設定 callback 函式:這部份是用來送出回應客戶的資料 2. 進入迴圈,同樣以 `kthread_should_stop` 判斷中止與否 3. 接收客戶端傳來的資料 4. 使用 `http_parser_execute` 解讀並傳給客戶 5. 最後釋放記憶體 至於 `kecho` 則是透過 `create_work` , `queue_work` 這種使用 `struct work_item` 的方式處理連線,以取代 `khttpd` 中`kthread_run` 建立連線的方式 ### 測量效能的方式 - `./htstress` : http server 壓力測試 - `./htstress http://127.0.0.1:8081 -t 3 -c 20 -n 200000` - 每秒處理多少要求 - `perf` : 分析 user program ,或核心程式碼中各函式呼叫佔多少時間 - [教學](https://wiki.csie.ncku.edu.tw/embedded/perf-tutorial) - eBPF : python BCC module 或 透過 C code 編譯 - [教學](https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main) - `ftrace` : 核心內部呼叫函式追蹤 - [man](https://docs.kernel.org/trace/ftrace.html)