Try   HackMD

2020q1 Homework6 (sehttpd)

contributed by < StevenChen8759 >

tags: linux2020

作業說明
作業繳交區
GitHub

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
引入 khttpd 中的 htstress.c 效能量測並改善

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
URL 解析錯誤修正

  • 在 shell 執行 ./htstress.c ,並輸入特定 URL 時,會得到下列錯誤:
$ ./htstress -n 10000 -c 4 -t 4 -o 5000 http://127.0.0.1:8081/
0 requests
1000 requests
2000 requests
3000 requests
4000 requests
5000 requests
6000 requests
7000 requests
8000 requests
9000 requests

requests:      10000
good requests: 10000 [100%]
bad requests:  0 [0%]
socker errors: 0 [0%]
seconds:       1.153
requests/sec:  8669.598

$ ./htstress -n 10000 -c 4 -t 4 -o 5000 http://127.0.0.1:8081
Segmentation fault (core dumped)
  • 上列兩個指令所要求的 URL 只差在 port 後方是否有字元 / ,透過 gdb 追蹤錯誤發生點,可發現是由字串處理不當所引起,如下所示:
$ gdb -q ./htstress
Reading symbols from ./htstress...done.
(gdb) r -n 10000 -c 4 -t 4 -o 5000 http://127.0.0.1:8081
Starting program: /home/stch/linux2020/sehttpd/htstress -n 10000 -c 4 -t 4 -o 5000 http://127.0.0.1:8081
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Program received signal SIGSEGV, Segmentation fault.
0x000000000040216c in main (argc=10, argv=0x7fffffffde08) at htstress.c:457
457	        if (*rq == '/') {
set listsize 30
(gdb) list 457
442	    node = s;
443	
444	    char *rq = strpbrk(s, ":/");
445	    if (!rq)
446	        rq = "/";
447	    else if (*rq == '/') {
448	        node = strndup(s, rq - s);
449	        if (!node) {
450	            perror("node = strndup(s, rq - s)");
451	            exit(EXIT_FAILURE);
452	        }
453	    } else if (*rq == ':') {
454	        *rq++ = 0;
455	        port = rq;
456	        rq = strchr(rq, '/');
457	        if (*rq == '/') {
458	            port = strndup(port, rq - port);
459	            if (!port) {
460	                perror("port = strndup(rq, rq - port)");
461	                exit(EXIT_FAILURE);
462	            }
463	        } else
464	            rq = "/";
465	    }
466	
467	    if (strnlen(udaddr, sizeof(ssun->sun_path) - 1) == 0) {
468	        int j = getaddrinfo(node, port, &hints, &result);
469	        if (j) {
  • 因為位於 456 行的 strchr() 函式在未找到字元 / 時會回傳空指標,而接續的判斷式並未檢查是否回傳空指標就進行記憶體操作,造成 Segmentation Fault,我們新增 if 判斷式以避免存取空指標。
  • 在修改之後,我們以 URL http://127.0.0.1:8081/ 測試,結果正常。但改以 URL http://127.0.0.1:8081 測試,仍然有 Segmentation Fault,於是我們再次透過 gdb 檢查出錯的地方:
gdb -q ./htstress
Reading symbols from ./htstress...done.
(gdb) r -n 10000 -c 4 -t 4 -o 5000 http://127.0.0.1:8081
Starting program: /home/stch/linux2020/sehttpd/htstress -n 10000 -c 4 -t 4 -o 5000 http://127.0.0.1:8081
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Program received signal SIGSEGV, Segmentation fault.
strlen () at ../sysdeps/x86_64/strlen.S:106
106	../sysdeps/x86_64/strlen.S: No such file or directory.
(gdb) backtrace
#0  strlen () at ../sysdeps/x86_64/strlen.S:106
#1  0x000000000040244f in main (argc=10, argv=0x7fffffffde08) at htstress.c:517
(gdb) br htstress.c:517
Breakpoint 6 at 0x402443: file htstress.c, line 517.
(gdb) r -n 10000 -c 4 -t 4 -o 5000 http://127.0.0.1:8081
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/stch/linux2020/sehttpd/htstress -n 10000 -c 4 -t 4 -o 5000 http://127.0.0.1:8081
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 6, main (argc=10, argv=0x7fffffffde08) at htstress.c:517
517	    outbufsize = strlen(rq) + sizeof(HTTP_REQUEST_FMT) + strlen(host);
(gdb) p rq 
$1 = 0x0
(gdb) p host
$2 = 0x7fffffffe22d "127.0.0.1"
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
strlen () at ../sysdeps/x86_64/strlen.S:106
106	../sysdeps/x86_64/strlen.S: No such file or directory.
(gdb) 
  • 此次的執行問題是由 strlen 的呼叫引起 (參考第 14 行的 gdb 指令執行結果),參考 Linux 的 strlen reference,會針對傳入的指標進行記憶體操作,以取得字串的長度,但參考網路上諸多文章討論, strlen 並不會檢查傳入的指標是否為 NULL,參考 GDB 操作,因為 rqNULL,造成 strlen(rq) 執行錯誤,因此我們亦須在呼叫 strlen 前,檢查傳入的指標 rq 是否為空。 ( Ref_1 Ref_2 )
  • 經過修改後,即可正常執行,參考下列輸出:
./htstress -n 100 -c 4 -t 4 -o 5000 http://127.0.0.1:8081
0 requests
10 requests
20 requests
30 requests
40 requests
50 requests
60 requests
70 requests
80 requests
90 requests

requests:      100
good requests: 100 [100%]
bad requests:  0 [0%]
socker errors: 0 [0%]
seconds:       3.522
requests/sec:  28.394

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
改善效能量測的機制

  • 我們首先比較下列量測工具的特色,作為改善量測方法的參考。
  • htstress 之效能量測工具特點:
    • 可調整測試 function 的重複量測次數 ( -n 參數輸入)
    • 可調整測試 function 的同時連線數 ( -c 參數輸入)
    • 可調整同一時間運行測試 fucntion 的 Thread 數 ( -t 參數輸入)
  • wrk 之效能量測工具特點:
    • 可調整測試 function 的同時連線數 ( -c 參數輸入)
    • 可調整同一時間運行測試 fucntion 的 Thread 數 ( -t 參數輸入)
    • 可調整測試 function 重複執行的間隔 ( -d 參數輸入)
  • httperf 之效能量測工具特點:
    • 支援 https 協定下的測試 (編譯時已包含相關的加密套件)
    • 可調整測試的 Request 數與 Request Rate (request/sec),並可依據需求調整 request timeout。

待整理
Steven HH ChenTue, May 5, 2020 6:47 PM

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
改善 sehttpd 的實作

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
實作由 CMD 輸入指定 PORT 與 WEBROOT 兩個參數的功能

  • 參考 htstress 中,利用 Linux 的系統呼叫 GETOPT(3) 實作參數解析的方法,我們在 sehttpd 中亦採用此方法實現參數解析,雖然程式碼較為複雜,但未來在增加輸入參數上較為容易。
  • 因參數解析只在程式啟動時運作,若未來需針對 Server 的啟動時間成本進行最佳化,再將 switch 的實作改以效能較差 computed goto 實作即可。此處仍以閱讀較直覺的 switch 實現。
  • 詳細實作請參考實作完成的程式碼 - mainloop.c

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
HTTP 404/403 錯誤處理的效能改善

  • 我們分別以 URL http://127.0.0.1:8081/http://127.0.0.1:8081/xxx/ 作為輸入,透過 htstress 量測執行效能,參考下列結果:
$ ./htstress -n 1000 -c 4 -t 4 http://127.0.0.1:8081/
0 requests
100 requests
200 requests
300 requests
400 requests
500 requests
600 requests
700 requests
800 requests
900 requests

requests:      1000
good requests: 1000 [100%]
bad requests:  0 [0%]
socker errors: 0 [0%]
seconds:       0.140
requests/sec:  7134.143
$ ./htstress -n 1000 -c 4 -t 4 http://127.0.0.1:8081/xxx/
0 requests
100 requests
200 requests
300 requests
400 requests
500 requests
600 requests
700 requests
800 requests
900 requests

requests:      1000
good requests: 0 [0%]
bad requests:  1000 [100%]
socker errors: 0 [0%]
seconds:       31.679
requests/sec:  31.567
  • 對於 URL http://127.0.0.1:8081/xxx/ 而言,server 端接受要求的路徑沒有對應檔案,因此回覆 HTTP 404 錯誤碼,參考下列的網頁 Response:
<html><title>Server Error</title><body>
404: Not Found
<p>Can't find the file: ./www/xxx/index.html
</p><hr><em>web server</em>
</body></html>
  • 但值得我們觀察的是,在輸入正確的 URL 時,Request 處理效能可達每秒 7134.143 個 request,但在輸入錯誤的 URL 時,其Request 處理效能卻只有每秒 31.567 個 request,明顯有改善的空間。

  • 我們著手分析造成錯誤 URL 輸入造成效能降低的因素。在 TCP socket 接收由 client 傳來的 HTTP Request 後的,會依序進行下列處理流程:

    • 讀取 Socket Buffer 資料 (recv)
    • 剖析讀取的資料 - HTTP Line
    • 剖析讀取的資料 - HTTP Body
    • 剖析讀取的資料 - 統一資源識別器 (URI)
    • 讀取 client 要求的檔案並 Response
  • 比對我們透過 htstress 所觀察到的效能降低的原因,主要是在剖析 URI 時的差異,我們設定程式輸出 debug 的訊息,可發現其等待時間竟長達 500 ms,參考下方圖片的輸出:

    500ms_waiting

  • 我們逐步追蹤程式在剖析 URI 時的行為,參考 http.c 可發現 404 Not Found 訊息由函式 do_error() 產生,並將訊息回傳給 Client 端,參考下列程式片段:

parse_uri(r->uri_start, r->uri_end - r->uri_start, filename); struct stat sbuf; if (stat(filename, &sbuf) < 0) { do_error(fd, filename, "404", "Not Found", "Can't find the file"); continue; } if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) { do_error(fd, filename, "403", "Forbidden", "Can't read the file"); continue; }
  • 其中,continue 的作用主要在於重新讀取 Socket 的 File Descriptor,主要是在 Non-blocking Socket 尚未傳輸完成 ( EAGAIN ) 時,程式能夠即時地重新聽取 File Descriptor,當 Socket的傳輸資料抵達時,Server 可立即針對資料進行處理,縮短資料抵達與資料開始處理的時間差。
  • 但是在 Client 的 Request 已經有錯的狀況下,即可針對原本的 Connection 做出一些處理,此處我們選擇直接將 Connection 關閉,並將資源釋放給接續的連線,參考下列程式片段:
parse_uri(r->uri_start, r->uri_end - r->uri_start, filename); struct stat sbuf; if (stat(filename, &sbuf) < 0) { do_error(fd, filename, "404", "Not Found", "Can't find the file"); // 404 not found, close connection free(out); debug("404 Not Found, close connection"); goto close; } if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) { do_error(fd, filename, "403", "Forbidden", "Can't read the file"); // 403 forbidden, close connection free(out); debug("403 Forbidden, close connection"); goto close; }
close: /* TODO: handle the timeout raised by inactive connections */ rc = http_close_conn(r); assert(rc == 0 && "do_request: http_close_conn");
  • 經過效能量測,輸入錯誤 URL 的處理效能已有改善,參考下列量測結果:
$ ./htstress -n 1 -c 1 -t 1 http://127.0.0.1:8081/index.htm

requests:      1
good requests: 0 [0%]
bad requests:  1 [100%]
socker errors: 0 [0%]
seconds:       0.000
requests/sec:  4065.041

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
sendfile 系統呼叫實現 zero-copy

  • 原有的資料傳遞方式:

    • 利用 mmap 將檔案讀取至記憶體空間
    • 透過 write() 將資料寫入 (Socket) file descriptor 將檔案回傳 Client。
  • 上述的資料流牽涉到兩次大量記憶體操作,一次是由檔案讀取至記憶體,一次是執行讀取資料寫入(複製)至 Socket Write Buffer,且期間牽涉函式 writen() 呼叫,在 writen() 中頻繁的分支 (branch) 亦可能降低部分效能。參考下圖的資料流:

    data_flow_mmap

  • 我們改以 sendfile 系統呼叫替代上述的作法。資料傳遞的方式如下:

    • 先將讀取檔案開啟,得到操作該檔案的 (Read) File Descriptor
    • 透過 sendfile() 操作 (Read) File Descriptor 讀取檔案。
    • 在此同時,sendfile() 將讀取的資料操作 (Socket) File Descriptor 寫入,將檔案同時回傳 Client。
  • 因為 sendfile() 可直接將檔案讀取至至 Socket Write Buffer,因此可減少一次大量記憶體操作,且免去呼叫 writen() 函式與其內部的系統呼叫 write() 與迴圈等待的分支 (branch) 成本。除此之外,因為檔案是直接複製至 Socket Write Buffer,假設兩個操作是使用單一個 Thread,這樣的操作可使兩個動作並行,因此可達成效能的改善。參考下圖的資料流:

    data_flow_sendfile

若使用兩個 Thread 操作可使兩個動作達成平行,理論上效能更佳,但須考量同步問題與同步化需付出的成本。參考 Producer-Consumer Problem 解決 Buffer 的讀寫問題。
效能成本分析上,應再額外考量 User ModeKernel Mode 的切換成本,較為精確。
Steven HH ChenTue, May 5, 2020 6:12 PM

  • sendfile 的程式實現片段
int srcfd = open(filename, O_RDONLY, 0); assert(srcfd > 2 && "open error"); /* TODO: make sendfile() error handling function complete */ int retn = sendfile(fd, srcfd, NULL, filesize); if (retn != 0) { printf("sendfile error, errno: %d", errno); assert("sendfile error"); } close(srcfd);
  • htstress 測量結果 - mmap() 方法
$ make strperf
$ make strperf
./htstress -n 100000 -c 4 -t 4 http://127.0.0.1:8081

...

requests:      100000
good requests: 100000 [100%]
bad requests:  0 [0%]
socker errors: 0 [0%]
seconds:       8.311
requests/sec:  12032.173

  • htstress 測量結果 - sendfile() 方法
$ make strperf
./htstress -n 100000 -c 4 -t 4 http://127.0.0.1:8081

...

requests:      100000
good requests: 100000 [100%]
bad requests:  0 [0%]
socker errors: 0 [0%]
seconds:       7.069
requests/sec:  14146.625
  • 透過 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 的說明:
If offset is NULL, then data will be read from in_fd starting at the
       file offset, and the file offset will be updated by the call.
  • 基於上述說明,我們可以不用考慮檔案在傳送給 Client 時,需重新設定 file offset 的問題,我們僅需重新呼叫 sendfile() 即可繼續將已開啟檔案中未傳輸的部分完成 Socket Write Buffer 寫入。
  • 我們以下列的程式碼實現 EAGAIN 錯誤的處理,並考量分支次數的最小化,以達成較佳的效能:
int retn; sendfile_again: retn = sendfile(fd, srcfd, NULL, filesize); // Process with sendfile error if (retn != 0) { // Socket Write Buffer Full, Call sendfile again... if (errno == EAGAIN) goto sendfile_again; /* TODO: make sendfile() error handling function complete */ /* TODO: make socket error handling function complete */ // printf("sendfile error, errno: %d\n", errno); // assert(retn==0 && "sendfile error"); } close(srcfd);
  • 上述程式仍有改進空間,以下列出接下來的修正方向:
    • 加強 sendfile() 與 socket write 系統呼叫的錯誤處理機制,使能處理的錯誤更加全面。
    • 將 sendfile() 的重試機制整合 Timer,設計適當地延遲機制,在延遲期間可執行其他任務 (如: accept()、recv()等),強化各個任務之間的並行 (Concurreny) 程度,以符合 non-blocking I/O 的設計精神。除此之外,重新執行 sendfile() 的機制亦須設計完善,避免造成資料回傳不全。
    • 參考 lwan-io-wrappers 的 sendfile() 重試處理機制。