contributed by <LanKuDot
>, <ktvexe
>,<judy66jo
>, <JonSyuGithub
>
直播錄影
judy66jo
JonSyuGithub
LanKuDot
、ktvexe
11/18 如何分析 server 效能 / youtube link ; Nov 18, 2016
server-framework 是一個以 C99 撰寫、具備 物件導向 程式設計風格的伺服器開發框架。
程式碼特色
部份會介紹C語言物件導向程式設計風格,或是參考「你所不知道的 C 語言」:物件導向設計篇。劉亮谷
就是將資料與實作包在一塊兒LanKuDot
由三個部份組成-
epoll
管理 server 所有 file descriptor 的狀態,並負責派送工作給 async.c
主程式為 httpd.c
。
一個最簡單的 Web Server 之功能包含下列三個步驟:
步驟一 : 接收瀏覽器所傳來的網址。
步驟二 : 取出相對應的檔案。
步驟三 : 將檔案內容傳回給瀏覽器。
在這個接收與傳回的過程中,所有的資訊都必須遵照固定的格式,規範這個接收/傳送格式的協定,稱為超文字傳送協定(Hyper Text Transfer Protocol),簡稱為 HTTP 協定。
HTTP 協定格式的基礎,乃是建構在網址 URL 上的傳輸方式,早期只能用來傳送簡單的 HTML 檔案,後來經擴充後也可以傳送其他類型的檔案,包含 影像、動畫、簡報、Word 文件等。
- server要處理client的連線,必須先建立socket,若要討論socket就比較涉及七層,因為socket是屬於TCP/IP的interface,之中就包含很多實作,例如:create、listen、connect、accept、send、read和write等等,像是java中就提供三種不同形態的sockets,其中簡略的介紹,可於恐龍書中Communication in Client-Server system閱讀。
- 上圖與影片中所提及的
連接port
其實嚴格來說也算是socket programming的一部份,所以不另行介紹,但是要注意的是listen時的行為。劉亮谷
Async.create()
建立 thread pool,準備處理來自 reactor 的工作
Async.worker_thread_cycle()
listen()
執行後Async.wait()
: 等待所有 thread 跳出 Async.worker_thread_cycle()
迴圈。並處理完剩下的工作
以上皆實作在
protocol-server.c:srv_listen()
LanKuDot
之後整合起來吧,因為正在看影片,要即時作筆記LanKuDot
Epoll
system call:偵測放進 epoll
的 task 有沒有 ready,如果有就放到 kernel ready queuectrl+c
來呼叫對應的中止函式,使整個系統可以正確停止。async_run
:將 task 放到 work queue 中,並喚醒 threadworker_thread_cycle
:從 work queue 取得 task,並 consume 掉。
read()
是 blocking call。sched_yield()
:讓完成工作的 thread,釋放掉 CPU 的資源,讓其他 thread 可以運作。perform_tasks()
處理剩餘的工作set_fd_polling()
,將 request 送到 epoll,並設定輪巡的間隔reactor_review()
:偵測 epoll queue 有多少任務要執行 (macro _WAIT_FOR_EVENTS
為 epoll_wait()
),並將任務推到 work queue 中srv_cycle_core()
監控系統及 client 連線的狀態
srv_cycle_core()
也是其中一個 task,執行完後會自己推回 work queueprotocal.c
中的 bind_server_socket()
去建立 server 的 listening portstruct addrinfo
紀錄 socket 所需要的資訊socket()
:create file descriptor for socketbind()
:bind socket to portlisten()
:設定 socket 可以接受的最大連線數accept()
:取得連進來的 client 專屬的 file desciptorepoll
system call:與 select()
和 poll()
不同的是在 ET mode 下只有在 fd 為 ready state 才會有反應reactor.c
使用 epoll()
監控async.c
使用 pipe()
取得 work queue 的工作signal()
signal re-direction,改變 signal 原本的運作timeout
設定會影響效能,但又不能只透過縮短 timeout 時間來加強效能server.c
實作 server kernel 的行為url.py
處理reactor 是一種 design pattern,他的概念是「同時」處理多個請求並回應,在 server-framework 用 epoll 方式實作。
昆憶哥,Design Patterns 我實在不懂,不知道 "reactor 是一種design pattern" 這句話對不對,wikipedia 有提到,我就自己揣摩了劉亮谷
跟據 wikipedia 提到 design pattern 是一種解決常見問題的解法。design pattern 不會提供最後形式 (如:source code),而是類似 template 的存在。所以這樣解釋是對的。LanKuDot
同場加映-design pattern
design pattern是對軟體設計中普遍存在(反覆出現)的各種問題,所提出的解決方案。不直接用來完成程式碼的編寫,而是描述在各種不同情況下,要怎麼解決問題的一種方案。例如:Abstract factory、Factory method、Reactor。
物件導向設計模式通常以類別或物件來描述其中的關係和相互作用,但不涉及用來完成應用程式的特定類別或物件。
原文:Object-oriented design patterns typically show relationships and interactions between classes or objects, without specifying the final application classes or objects that are involved.
看不懂"但不涉及用來完成應用程式的特定類別或物件。"這句話的意思,昆憶哥救一下。劉亮谷
就像我上面提到的,他只是告訴你解法,但不會規定 source code 怎麼實作,反正只要能達成目的就好。LanKuDot
並非所有的軟體模式都是設計模式,設計模式特指軟體「設計」層次上的問題。還有其它非設計模式的模式,如架構模式(software architecture patterns)。同時,演算法不能算是一種設計模式,因為演算法主要是用來解決計算上的問題,而非設計上的問題。
當 thread 數量兩個以上將會 thread dispatching ,即使沒有 request 時 worker_thread_cycle() 內的 read(async->pipe.in, &sig_buf, 1) 仍然沒辦法 block 住 thread ? JonSyuGithub
思考這樣的 API 設計有沒有不足?或者值得做 code refactoring 之處 jserv
IPC: pipe
struct Server
儲存 server 屬性與資料管理的 data structure
繼承的實現:將母 strcut 放在子 strcut 的第一個 member,透過 pointer 的 casting,可以將子 struct 視為母 struct 使用。因為母 struct 在第一個 member,當 pointer 指向子 struct 時,同時也指向母 struct。
示範程式:
struct FDTask
struct GroupTask
struct __SERVER_API__
用來 access struct Server
object 的 helper function,定義對於 struct Server
object 的操作。也因此 struct __SERVER_API__
宣告成 global const object:
struct Server
的 definition 是宣告在 protocaol-server.c
中,代表其 member 是 private
的,只能透過 struct __SERVER_API_
去 access (public member function)
__SERVER_API_
大部分的函式的第一個參數都是指向 struct Server
物件的指標。
物件導向概念的實現:
取得或設定 struct Server
的 private member
reactor
與 settings
允許外部直接存取 private 的 member
對應的 handler
使用 static variable flim
來紀錄允許使用的最大 file descriptor 數量,當該值非零時,會直接回傳該值。
透過預估每個 connection 會使用到的記憶體空間 (stack 與 heap) 與該 process 可以取得的記憶體空間,以計算可以容納多少 conenction。
實作細節
getrlimit()
與 setrlimit()
取得指定的 resource 限制。設定以下 resource 的最大值:RLIMIT_NOFILE
:該 process 可以開啟的最大 file descriptor 數量。將 rlim_cur
設為與 rlim_max
一樣,並取得設定後的值。如果設定後的 rlim_cur
比 _SC_OPEN_MAX - 1
或 OPEN_MAX - 1
大的話,就更新 flim
的值作為可以開啟的最大 fd 數。
RLIMIT_STACK
:process stack size in bytes。
flim
* 4096 bytes 的空間call stack、thread 自己的 stack 應該也要考慮? 代表不太可能在配置的 stack 中開好開滿到自己想要的 connection 數量LanKuDot
flim
* 4096 bytes 的 stack size,設置後取得真正可以配置的 fd 數量,存到 flim
。RLIMIT_DATA
:process data segment size。
Data segment 包含 initialized variables, uninitizlized variables 及 heap。 C programming memory layoutLanKuDot
struct ServerSettings
的資料是否有效,否則會使用預設值:
protocol
:不能為 NULL
,否則直接回傳錯誤FIXME 註紀要輸出 warning message,用
assert
會不會太暴力?LanKuDot
太暴力的意思是希望只 warning ,但不 suspend 掉嗎? 亮谷
不過如果這是無法透過預設來處理的話,直接強制關閉也是可以LanKuDot
port
:8000timeout
:5threads
:1processes
:1strcut Server
protocol-server.c:648
srv_capacity()
計算最大連線數,以此建立所需的資料數量struct Server
object 紀錄 server 的資訊,及儲存上一步所建立的資料的 pointersrv_fd
:透過 bind_server_socket()
取得lock
, task_lock
:透過 pthread_mutex_init()
初始化task_pool
, group_task_pool
:設為空 pollserver_map[i]
:存自己的 reference (類似 this
)buffer_map[i]
:透過 Buffer.create()
取得protocol_map[i]
, busy[i]
, idle[i]
, tout[i]
, udata_map[i]
:設為 0探討 epoll, pipe, signals 等系統呼叫
的 SIGNAL
一節settings.processes
fork process
srv.root_pid
紀錄 root process 之 pidServer.read
或 Server.write
) 才會執行,以取代 read()
或 write()
再者來聊聊 struct Protocol
與 struct ServerSettings
在 protocol-server.h
中,有段 macro start_server
,這邊用到技巧稱為 Variadic Macros ,這種宣告方式允許 macro 像 function call 一樣能有多個參數。
可參考:Variadic Macros - The C Preprocessor - GCC - GNU
所以從上方 macro 可以得知 start_server
的參數為 ServerSettings
的 member 。
固接下來開始來研究 ServerSettings
中的 member 。
char *port
顧名思義是用來設定 socket 時所使用的 port ,在 protocol-server.c
中 srv_listen()
會將其 default 設成8080。本來想來介紹一下 port 8080 的用途,但我發現查完資料後還是不太理解,大致內容為 8080 與 80 port 為負責http traffic 。 亮谷
char *address
同理為設定 IP address 用,他的使用在 protocol.c
中 bind_server_socket()
,他使用到 linux system call getaddrinfo()
。
getaddrinfo()
為給定 host IP 與 port/http ,他會回傳 addrinfo
structures 其中已填好 Internet address 的資訊, 可用於 bind()
、connect()
。
可參考:Linux Programmer's Manual GETADDRINFO(3)
threads
、processes
、timeout
在前面影片回顧有做過整理,分別是用來設定 thread pool 的 thread 數量、並行的 process 數量、 worker thread 每次要等待的時間。
除了上方的 member data 外,在 server-framework
中多次提及的以 function pointer 實作物件導向,所以接下來分析各 function pointer 指向之功能。
struct __SERVER_API__
struct Server *
有些寫 server_pt
來指向 struct Server
物件。雖然有 typedef
,但統一使用一種會比較好count()
的 argument service
只指定要查詢的 protocol,或許用 service_protocol
比較好run_after()
與 run_every()
的 comment 不佳
run_after()
:run a task after milliseconds
msrun_every()
:run a task every millisecons
and repeat repetations
timesstruct Protocol
struct ServerSettings
monitoring multiple file descriptors to see if I/O is possible on any of them.
The epoll event distribution interface is able to behave both as edge-triggered (ET) and as level-triggered (LT).
on_signal()
;忽略 SIGPIPE 訊號2016q1 Homework #3中提到以ab -c 32 -n 100 http://localhost:8080/
測試 server frameworks 效能,看到apache docs 中的描述:
ab is a tool for benchmarking your Apache Hypertext Transfer Protocol (HTTP) server.
-c concurrency
Number of multiple requests to perform at a time. Default is one request at a time.
-n requests
Number of requests to perform for the benchmarking session. The default is to just perform a single request which usually leads to non-representative benchmarking results.
所以說,這邊的測試方式是,對 Web Server 送出100個 request ,且一次送出32次 request。
看到結果:
Web server-
Benchmark-
問題描述:
為什麼 httpd 可以用 ab 測出結果,但是 test-reactor 在 ab test 的時候卻會 timeout?
如果是測 test-reactor 的話會出現這個訊息:
Benchmarking 127.0.0.1 (be patient)...apr_pollset_poll: The timeout specified has expired (70007)
原因探討:
epoll 本身沒有 socket 的 timeout, 讓 socket 接上去關不起來,新增一個 list 存放每個 fd 的 time,然後在 epoll_wait return 0的時候去檢查有沒有 timeout, timeout 的話就把 socket 關掉。這樣做之後 ab 就有結果了。
可是這樣做就捨棄 epoll 的好處了,變成還要去維護一個 timeout 的 list,每次都要去 iteration 一次,跟 select 與 poll 相去不遠矣。
除此之外的作法還有,想想是否是每次 epoll 的回傳都要檢查 timeout list,或是新增一個 timer,定時去檢查就好 (後面就會看到了)
或是想辦法讓 ab 自己去關閉這個 socket 的連線,當然需要靠我們提供資訊 (Content-Length),client 端才知道這次的 request 已經回復完畢
apache bench的問題
by < Broken Performance Tools >
wrk是一個modern HTTP 效能分析工具,重點是他可以用於single multi-core CPU上,不過有看到資料說wrk只能執行於Unix like的系統上,這點我還沒有進行確認,僅先進行紀錄。
附上README片段:
wrk is a modern HTTP benchmarking tool capable of generating significant load when run on a single multi-core CPU. It combines a multithreaded design with scalable event notification systems such as epoll and kqueue.劉亮谷
這個問題雖然不難理解,不過很難想像解決的辦法,畢竟任何的benchmark tool一定也具有本身的overhead。劉亮谷
In http.c
:
效能:
不太確定上面你們想怎麼排版
所以就麻煩自行更改囉~
課程助教
結果:
In protocol-server.c
由於上方實驗可發現,timeout 會影響效能,所以在 trace protocol-server.c
時想到,理論上這次實驗中資料的傳輸應該是遠比 timeout 所設定的時間還要都來的短,既然如此在資料傳輸結束時關閉 connection 應該能提升效能。
TODO:驗證正確性亮谷
效能:
會發現 Requests per second 很浮動,可以發現這個作法效能受到資料傳輸速度的影響極大,所以應該不是好的改善方式。
傳輸完就關掉的話,重新連線會不會有 latency 呢? 有沒有方法可以確認這次傳輸是最後一筆資料。LanKuDot
In reactor.c
這部份做了什麼修改呢?課程助教
結果:
struct Async
tasks
作為 work queue,儘管 memberthreads
的註解寫著 thread pool 但並沒有實質用處。struct thpool threads
取代原本的 pthread_t threads
:Async.run
產生新的工作。srv_cycle_core
,我們額外以一個執行緒來執行: