簡體   English   中英

大型 TCP 內核緩沖導致應用程序在 FIN 上失敗

[英]Large TCP kernel buffering cause application to fail on FIN

我想重新打開之前被錯誤歸類為網絡工程問題的問題,經過更多測試,我認為這對程序員來說是一個真正的問題。

因此,我的應用程序從服務器流式傳輸 mp3 文件。 我無法修改服務器。 客戶端根據需要從服務器讀取數據(160kbits/s)並將其饋送到 DAC。 讓我們使用一個 3.5MB 的文件。

當服務器發送完最后一個字節后,它關閉連接,所以它發送一個 FIN,這似乎是正常的做法。

問題是內核,尤其是在 Windows 上,似乎存儲了 1 到 3 MB 的數據,我假設 TCP 窗口大小已經完全打開。

幾秒鍾后,服務器發送了整個 3.5 MB 和大約 3MB 的內核緩沖區。 此時服務器已經發送了 FIN,它在適當的時候是 ACK。

從客戶端的角度來看,它繼續以 20kB 的塊讀取數據,並將在接下來的 3MB/20 ~= 150 秒內讀取數據,然后再看到 EOF。

同時,服務器處於 FIN_WAIT_2(而不是我最初寫的 TIME_WAIT,感謝Steffen糾正我。現在,像 Windows 這樣的操作系統似乎有一個半關閉的套接字計時器,它從發送 FIN 開始並且小到 120s,無論實際的 TCPWindowsize BTW)。 當然在 120s 之后它認為它應該已經收到了客戶端的 FIN,所以它發送了一個 RST。 該 RST 導致所有客戶端的內核緩沖區被丟棄並且應用程序失敗。

由於需要代碼,這里是:

int sock = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in addr;

addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_family = AF_INET;
addr.sin_port = htons(80);

int res = connect(sock, (const struct sockaddr*) & addr, sizeof(addr));

char* get = "GET /data-3 HTTP/1.0\n\r"
        "User-Agent: mine\n\r"
        "Host: localhost\n\r"
        "Connection: close\n\r"
        "\n\r\n\r";

bytes = send(sock, get, strlen(get), 0);
printf("send %d\n", bytes);

char *buf = malloc(20000);

while (1) {
    int n = recv(sock, buf, 20000, 0);
    if (n == 0) {
        printf(“normal eof at %d”, bytes);
        close(sock);
        break;
    }
    if (n < 0) {
        printf(“error at %d”, bytes);
        exit(1);
    }
    bytes += n;
    Sleep(n*1000/(160000/8));
}
free(buf);
closesocket(sock);

它可以用任何 HTTP 服務器進行測試。

我知道有一些解決方案可以通過在服務器關閉套接字之前與服務器握手(但服務器只是一個 HTTP 服務器)但是當緩沖區大於使用它們的時間時,內核級別的緩沖會導致系統性故障。

客戶端在吸收數據方面是完全實時的。 擁有更大的客戶端緩沖區或根本沒有緩沖區不會改變這個問題,這對我來說似乎是系統設計缺陷,除非有可能在應用程序級別而不是整個操作系統控制內核緩沖區,或者檢測到 FIN 接收recv() 的 EOF 之前的客戶端級別。 我試圖改變 SO_RCVBUF 但它似乎並沒有從邏輯上影響這種內核緩沖級別。

這是一次成功和一次失敗的交換的截圖

success
3684    381.383533  192.168.6.15    192.168.6.194   TCP 54  [TCP Retransmission] 9000 → 52422 [FIN, ACK] Seq=9305427 Ack=54 Win=262656 Len=0
3685    381.387417  192.168.6.194   192.168.6.15    TCP 60  52422 → 9000 [ACK] Seq=54 Ack=9305428 Win=131328 Len=0
3686    381.387417  192.168.6.194   192.168.6.15    TCP 60  52422 → 9000 [FIN, ACK] Seq=54 Ack=9305428 Win=131328 Len=0
3687    381.387526  192.168.6.15    192.168.6.194   TCP 54  9000 → 52422 [ACK] Seq=9305428 Ack=55 Win=262656 Len=0

failed
5375    508.721495  192.168.6.15    192.168.6.194   TCP 54  [TCP Retransmission] 9000 → 52436 [FIN, ACK] Seq=5584802 Ack=54 Win=262656 Len=0
5376    508.724054  192.168.6.194   192.168.6.15    TCP 60  52436 → 9000 [ACK] Seq=54 Ack=5584803 Win=961024 Len=0
6039    628.728483  192.168.6.15    192.168.6.194   TCP 54  9000 → 52436 [RST, ACK] Seq=5584803 Ack=54 Win=0 Len=0

這是我認為的原因,非常感謝 Steffen 讓我走上正軌。

  • 一個 mp3 文件在 160 kbits/s = 20 kB/s 時是 3.5 MB
  • 客戶端以 20kB/秒的確切要求速度讀取它,假設每秒 20kB 的一個 recv(),為簡單起見沒有預緩沖
  • 一些操作系統,如 Windows,可以有非常大的 TCP 內核緩沖區(大約 3MB 或更多)並且在快速連接的情況下,TCP 窗口大小是廣泛開放的
  • 在幾秒鍾內,整個文件被發送到客戶端,假設內核緩沖區中有大約 3MB
  • 就服務器而言,所有內容均已發送並確認,因此它執行 close()
  • close() 向客戶端發送 FIN,客戶端響應 ACK,服務器進入 FIN_WAIT_2 狀態
  • 但是,從客戶端的角度來看,在接下來的 150 秒內,所有 recv() 都會在看到 eof 之前進行大量讀取!
  • 所以客戶端不會執行 close() 因此不會發送 FIN
  • 服務器處於 FIN_WAIT_2 狀態,根據 TCP 規范,它應該永遠保持這種狀態
  • 現在,各種操作系統(至少是 Windows)在啟動 close() 時啟動一個類似於 TIME_WAIT(120 秒)的計時器,或者在接收到他們的 FIN 的 ACK 時,我不知道(實際上 Windows 有一個特定的注冊表項那,AFAIK)。 這是為了更積極地處理半封閉套接字。
  • 當然,在 120s 之后,服務器還沒有看到客戶端的 FIN 並發送一個 RST
  • 客戶端接收到 RST 並導致錯誤,TCP 緩沖區中的所有剩余數據將被丟棄和丟失
  • 當然,高比特率格式不會發生這種情況,因為客戶端消耗數據的速度足夠快,因此內核 TCP 緩沖區永遠不會空閑 120 秒,而當應用程序緩沖系統讀取所有數據時,低比特率可能不會發生這種情況。 它必須是比特率、文件大小和內核緩沖區的錯誤組合……因此它不會一直發生。

而已。 這可以用幾行代碼和每個 HTTP 服務器重現。 這可以爭論,但我認為這是一個系統性的操作系統問題。 現在,似乎可行的解決方案是將客戶端的接收緩沖區 (SO_RCVBUF) 強制降低到較低級別,這樣服務器幾乎沒有機會發送所有數據,並且數據在客戶端的內核緩沖區中停留的時間太長。 請注意,如果緩沖區為 20kB 並且客戶端以 1B/s 的速度使用它,這仍然會發生……因此我將其稱為系統故障。 現在我同意有些人會將其視為應用程序問題

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM