contributed by < StevenChen8759
>
linux2020
khttpd
中的 htstress.c
效能量測並改善./htstress.c
,並輸入特定 URL 時,會得到下列錯誤:/
,透過 gdb
追蹤錯誤發生點,可發現是由字串處理不當所引起,如下所示:strchr()
函式在未找到字元 /
時會回傳空指標,而接續的判斷式並未檢查是否回傳空指標就進行記憶體操作,造成 Segmentation Fault
,我們新增 if
判斷式以避免存取空指標。http://127.0.0.1:8081/
測試,結果正常。但改以 URL http://127.0.0.1:8081
測試,仍然有 Segmentation Fault,於是我們再次透過 gdb
檢查出錯的地方:strlen
的呼叫引起 (參考第 14 行的 gdb 指令執行結果),參考 Linux 的 strlen reference,會針對傳入的指標進行記憶體操作,以取得字串的長度,但參考網路上諸多文章討論, strlen
並不會檢查傳入的指標是否為 NULL
,參考 GDB 操作,因為 rq
為 NULL
,造成 strlen(rq)
執行錯誤,因此我們亦須在呼叫 strlen
前,檢查傳入的指標 rq
是否為空。 ( Ref_1 Ref_2 )待整理…
Steven HH ChenTue, May 5, 2020 6:47 PM
htstress
中,利用 Linux 的系統呼叫 GETOPT(3)
實作參數解析的方法,我們在 sehttpd
中亦採用此方法實現參數解析,雖然程式碼較為複雜,但未來在增加輸入參數上較為容易。switch
的實作改以效能較差 computed goto
實作即可。此處仍以閱讀較直覺的 switch
實現。http://127.0.0.1:8081/
與 http://127.0.0.1:8081/xxx/
作為輸入,透過 htstress
量測執行效能,參考下列結果:http://127.0.0.1:8081/xxx/
而言,server 端接受要求的路徑沒有對應檔案,因此回覆 HTTP 404 錯誤碼,參考下列的網頁 Response:但值得我們觀察的是,在輸入正確的 URL 時,Request 處理效能可達每秒 7134.143 個 request,但在輸入錯誤的 URL 時,其Request 處理效能卻只有每秒 31.567 個 request,明顯有改善的空間。
我們著手分析造成錯誤 URL 輸入造成效能降低的因素。在 TCP socket 接收由 client 傳來的 HTTP Request 後的,會依序進行下列處理流程:
比對我們透過 htstress
所觀察到的效能降低的原因,主要是在剖析 URI 時的差異,我們設定程式輸出 debug 的訊息,可發現其等待時間竟長達 500 ms,參考下方圖片的輸出:
我們逐步追蹤程式在剖析 URI 時的行為,參考 http.c 可發現 404 Not Found
訊息由函式 do_error()
產生,並將訊息回傳給 Client 端,參考下列程式片段:
continue
的作用主要在於重新讀取 Socket 的 File Descriptor,主要是在 Non-blocking Socket 尚未傳輸完成 ( EAGAIN
) 時,程式能夠即時地重新聽取 File Descriptor,當 Socket的傳輸資料抵達時,Server 可立即針對資料進行處理,縮短資料抵達與資料開始處理的時間差。sendfile
系統呼叫實現 zero-copy
原有的資料傳遞方式:
mmap
將檔案讀取至記憶體空間write()
將資料寫入 (Socket) file descriptor
將檔案回傳 Client。上述的資料流牽涉到兩次大量記憶體操作,一次是由檔案讀取至記憶體,一次是執行讀取資料寫入(複製)至 Socket Write Buffer,且期間牽涉函式 writen()
呼叫,在 writen()
中頻繁的分支 (branch) 亦可能降低部分效能。參考下圖的資料流:
我們改以 sendfile
系統呼叫替代上述的作法。資料傳遞的方式如下:
(Read) File Descriptor
sendfile()
操作 (Read) File Descriptor
讀取檔案。sendfile()
將讀取的資料操作 (Socket) File Descriptor
寫入,將檔案同時回傳 Client。因為 sendfile()
可直接將檔案讀取至至 Socket Write Buffer,因此可減少一次大量記憶體操作,且免去呼叫 writen()
函式與其內部的系統呼叫 write()
與迴圈等待的分支 (branch) 成本。除此之外,因為檔案是直接複製至 Socket Write Buffer,假設兩個操作是使用單一個 Thread,這樣的操作可使兩個動作並行,因此可達成效能的改善。參考下圖的資料流:
若使用兩個 Thread 操作可使兩個動作達成平行,理論上效能更佳,但須考量同步問題與同步化需付出的成本。參考 Producer-Consumer Problem 解決 Buffer 的讀寫問題。
效能成本分析上,應再額外考量User Mode
與Kernel Mode
的切換成本,較為精確。
Steven HH ChenTue, May 5, 2020 6:12 PM
sendfile
的程式實現片段htstress
測量結果 - mmap()
方法htstress
測量結果 - sendfile()
方法htstress
測試兩種實現,可發現 sendfile()
方法有較佳的效能,但仍有改善空間。EAGAIN
錯誤處理 (2020/05/07 新增):在利用 sendfile()
後效能提升,加上後續整合 computed goto
…等效能改善方法,同一時間內可並行 or 平行的連線因而大量增加,在 sendfile()
大量地向 Socket Write Buffer
寫入資料的情況下,可能會使 Write Buffer
被填滿,此時因資料暫時無法寫入並回傳給 Client,且 Socket
被設定為 non-blocking mode
,因此得到 EAGAIN
錯誤。Note: 對照 Non-Blocking Socket Read 的操作,在 Socket Read Buffer 為空的時候進行 Read,因為無資料可供讀取,亦會產生
EAGAIN
的錯誤。
sendfile
針對參數 offset
的說明:file offset
的問題,我們僅需重新呼叫 sendfile()
即可繼續將已開啟檔案中未傳輸的部分完成 Socket Write Buffer 寫入。non-blocking I/O
的設計精神。除此之外,重新執行 sendfile()
的機制亦須設計完善,避免造成資料回傳不全。