(點選上方公眾號,可快速關註)
來源:辰洋 ,
tinylcy.me/2017/如何實現一個Web-Server/
最近重構了去年造的一個輪子 Vino。Vino 旨在實現一個輕量並且能夠保證效能的 Web Server,僅關註 Web Server 的本質部分。在重構過程中,Vino 借鑒了許多優秀開源專案的思想,如 Nginx、Mongoose 和 Webbench。因此,對比上一個版本的 Vino,現在的 Vino 不僅效能得到提升,而且設計也更為優雅、健壯 :D。
本文將會對 Vino 目前所具備的關鍵特性進行闡述,並總結開發過程中的一點心得。
單執行緒 + Non-Blocking
Vino 整體採用了基於事件驅動的單執行緒 + Non-Blocking 模型。採用單執行緒模型,避免了系統分配多執行緒及執行緒之間通訊的開銷,同時降低了記憶體的耗用。由於採用了單執行緒模型,為了更好的提高執行緒利用率,Vino 將預設 Blocking 的 I/O 設定為 Non-Blocking I/O,即在執行緒讀/寫資料的過程中,如果緩衝區為空/緩衝區滿,執行緒不會阻塞,而是立即傳回,並設定 errno。
Vino 最初的靈感來源於 Computer Systems: A Programmer’s Perspective 一書講述網路程式設計時實現的一個簡單的 Web Server,每到來一個請求,Web Server 都會 fork 一個行程去處理。顯然,在高併發的場景下,這種模型是不合理的。每次 fork 行程會帶來巨大的開銷,並且系統中行程的數量是有限的。同時,伴隨多行程帶來的行程排程的開銷也不可小覷,CPU 會花費大量的時間用於決定呼叫哪一個行程。行程排程引發的行程背景關係之間的切換,也需要耗費相當大的資源。
很容易聯想到採用多執行緒模型來替代多行程模型,相比於多行程模型,多執行緒模型佔用的系統資源會大大降低,但是本質上並沒有減小執行緒排程帶來的開銷。為了減小由執行緒排程導致的開銷,我們可以採用執行緒池模型,即固定執行緒的數量,但是問題依舊存在:因為 Linux 預設 I/O 是阻塞(Blocking)的,如果執行緒池中所有的執行緒同時阻塞於正在處理的請求,那麼新到來的請求就沒有執行緒去處理了。因此,如果我們用 Non-Blocking 的 I/O 替換預設的 Blocking I/O,執行緒將不會阻塞於資料的讀寫,問題便可得到解決。
HTTP Keep-Alive
Vino 支援 HTTP 長連線(Persistent Connections),即多個請求可以復用同一個 TCP 連線,以此減少由 TCP 建立/斷開連線所帶來的效能開銷。每到來一個請求,Vino 會對請求進行解析,判斷請求頭中是否存在 Connection: keep-alive 請求頭。如果存在,在處理完一個請求後會保持連線,並對資料緩衝區(用於儲存請求內容,響應內容)及狀態標記進行重置,否則,關閉連線。
關於 HTTP Keep-Alive 的優勢,RFC 2616 有著更完善的總結,取用如下。
-
By opening and closing fewer TCP connections, CPU time is saved in routers and hosts (clients, servers, proxies, gateways, tunnels, or caches), and memory used for TCP protocol control blocks can be saved in hosts.
-
HTTP requests and responses can be pipelined on a connection. Pipelining allows a client to make multiple requests without waiting for each response, allowing a single TCP connection to be used much more efficiently, with much lower elapsed time.
-
Network congestion is reduced by reducing the number of packets caused by TCP opens, and by allowing TCP sufficient time to determine the congestion state of the network.
-
Latency on subsequent requests is reduced since there is no time spent in TCP’s connection opening handshake.
-
HTTP can evolve more gracefully, since errors can be reported without the penalty of closing the TCP connection. Clients using future versions of HTTP might optimistically try a new feature, but if communicating with an older server, retry with old semantics after an error is reported.
定時器 Timer
如果一個請求在建立連線後遲遲沒有傳送資料,或者對方突然斷電,應該如何處理?我們需要實現定時器來處理超時的請求。Vino 定時器的實現參考了 Nginx 的設計,Nginx 使用一顆紅黑樹來儲存各個定時事件,每次事件迴圈時從紅黑樹中不斷找出最小(早)的事件,如果超時則觸發超時處理。為了簡化實現,在 Vino 中,我實現了一個小頂堆來儲存定時事件,如果被處理的定時事件同時支援長連線,那麼在該請求處理完畢後會更新該請求對應的定時器,也就是重新計時。定時器相關程式碼見 vn_event_timer.h 和 vn_event_timer.c。
HTTP Parser
由於網路的不確定性,我們並不能保證一次就能讀取所有的請求資料。因此,對於每一個請求,我們都會開闢一段緩衝區用於儲存已經讀取到的資料。同時,我們需要同時對讀取到的資料進行解析,以保證讀取到的資料都是合理的資料,例如,假設目前緩衝區內的資料為 GET /index.html HTT,那麼下一次讀取到的字元必須為 P,否則,應立即檢測出當前請求是一個異常的請求,並主動關閉當前的連線。
基於以上分析,我們需要實現一個 HTTP 狀態機(Parser)來維持當前的解析狀態,Vino 狀態機的實現參考了 Nginx 的設計,並對 Nginx 的實現做了簡化。HTTP Parser 相關程式碼見 vn_http_parse.h 和 vn_http_parse.c。
Memory Pool
我們一般使用 malloc/calloc/free 來分配/釋放記憶體,但是這些函式對於一些需要長時間執行的程式來說會有一些弊端。頻繁使用這些函式分配和釋放記憶體,會導致記憶體碎片,不容易讓系統直接回收記憶體。典型的例子就是大併發頻繁分配和回收記憶體,會導致行程的記憶體產生碎片,並且不會立馬被系統回收。
使用記憶體池分配記憶體,可以在一定程度上提升記憶體分配的效率,不需要每次都呼叫 malloc/calloc 函式。同時,使用記憶體池使得記憶體管理更加簡單。在 Vino 中,針對每一個請求,Vino 都會為其分配一或多個記憶體池(各個記憶體池形成一個單連結串列),在請求處理完畢後,一併釋放所有的記憶體。
Vino 記憶體池的實現依舊參考了 Nginx 的實現,並做了簡化,Memory Pool 相關程式碼見 vn_palloc.h 和 vn_palloc.c。
其他
在開發 Vino 的過程中,還有許多需要考慮和權衡的地方。響應請求時,如果使用者請求的是一個很大的檔案,導致寫緩衝區滿,我們如何更好的設計響應緩衝區?如何更高效的設計底層資料結構(如字串、連結串列、小頂堆等)?如何更優雅的解析命令列引數?如何對特定訊號進行處理?如何更健壯的處理錯誤資訊?當程式碼的數量達到一定程度後,如何更快的定位異常程式碼?
Vino 的開發 & 重構暫時告一段落,原始碼放在了 GitHub 上。當然,Vino 還有許多不足之處,以及未實現的特性。
-
僅支援 HTTP GET 方法,暫不支援其他 HTTP method。
-
暫不支援動態請求的處理。
-
支援的 HTTP/1.1 特性有限。
-
…
寫這篇文章,希望對初學者有所幫助。
參考
[1] Vino, https://github.com/tinylcy/vino.
[2] Computer Systems: A Programmer’s Perspective, http://csapp.cs.cmu.edu.
[3] Advanced Programming in the UNIX Environment (3rd Edition), https://www.amazon.ca/Advanced-Programming-UNIX-Environment-3rd/dp/0321637739.
[4] Unix Network Programming, Volume 1, https://www.amazon.ca/Unix-Network-Programming-Sockets-Networking/dp/0131411551.
[5] Nginx, https://github.com/nginx/nginx.
[6] Mongoose, https://github.com/cesanta/mongoose.
[7] Web Bench, http://home.tiscali.cz/~cz210552/webbench.html.
[8] Zaver, https://github.com/zyearn/zaver.
[9] RFC 2616, https://tools.ietf.org/html/rfc2616.
[10] How to use epoll? A complete example in C, https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c .
看完本文有收穫?請轉發分享給更多人
關註「ImportNew」,看技術乾貨