網絡性能優化參數關系解讀 | TCP Nagle / TCP_NODELAY / TCP_QUICKACK / TCP_CORK

注:本文為 “網路性能優化” 相關文章合輯

未整理去重。

如有內容異常,請看原文。


TCP_NODELAY 詳解

lenky0401 發表于 2012-08-25 16:40

在網絡擁塞控制領域,Nagle 算法(Nagle algorithm)是一個非常著名的算法,其名稱來源于發明者 John Nagle。1984 年,John Nagle 首次提出該算法,旨在解決福特汽車公司網絡中的擁塞問題(RFC 896)。具體問題描述如下:如果應用程序每次僅產生 1 個字節的數據,并以網絡數據包的形式發送到遠端服務器,那么很容易導致網絡因過多的數據包而過載。例如,在使用 Telnet 連接到遠程服務器時,每次擊鍵操作會生成 1 個字節的數據并發送一個數據包。在這種情況下,一個僅包含 1 個字節有效數據的數據包,卻需要攜帶 40 個字節的包頭(即 IP 頭 20 字節 + TCP 頭 20 字節),有效載荷(payload)利用率極低。這種情況被稱為“愚蠢窗口癥候群”(Silly Window Syndrome)。對于輕負載網絡,這種狀況或許可以接受,但對于重負載網絡,極易導致擁塞甚至癱瘓。

為解決上述問題,Nagle 算法的核心改進如下:如果發送端多次發送包含少量字符的數據包(長度小于 MSS 的數據包稱為小包,長度等于 MSS 的數據包稱為大包),則發送端會先發送第一個小包,而將后續到達的少量字符數據緩存起來,直到收到接收端對前一個數據包的 ACK 確認,或者當前字符屬于緊急數據,或者積攢的數據量達到一定數量(例如緩存的數據已達到數據包的最大長度),才會將這些數據組成一個較大的數據包發送出去。

以下是 Linux 內核中相關的代碼實現:

/* Filename : \linux-3.4.4\net\ipv4\tcp_output.c */
/* Return 0, if packet can be sent now without violation Nagle's rules:* 1. It is full sized.* 2. Or it contains FIN. (already checked by caller)* 3. Or TCP_CORK is not set, and TCP_NODELAY is set.* 4. Or TCP_CORK is not set, and all sent packets are ACKed.*  With Minshall's modification: all sent small packets are ACKed.*/
static inline int tcp_nagle_check (const struct tcp_sock *tp,const struct sk_buff *skb,unsigned mss_now, int nonagle)
{return skb->len < mss_now &&((nonagle & TCP_NAGLE_CORK) ||(!nonagle && tp->packets_out && tcp_minshall_check (tp)));
}
/* Return non-zero if the Nagle test allows this packet to be sent now. */
static inline int tcp_nagle_test (const struct tcp_sock *tp, const struct sk_buff *skb,unsigned int cur_mss, int nonagle)
{/* Nagle rule does not apply to frames, which sit in the middle of the write_queue (they have no chances to get new data).* This is implemented in the callers, where they modify the 'nonagle' argument based upon the location of SKB in the send queue.*/if (nonagle & TCP_NAGLE_PUSH)return 1;/* Don't use the nagle rule for urgent data (or for the final FIN).* Nagle can be ignored during F-RTO too (see RFC413).*/if (tcp_urg_mode (tp) || (tp->frto_counter == 2) ||(TCP_SKB_CB (skb)->tcp_flags & TCPHDR_FIN))return 1;if (!tcp_nagle_check (tp, skb, cur_mss, nonagle))return 1;return 0;
}

從代碼中可以看出,tcp_nagle_test() 函數首先檢查是否設置了 TCP_NAGLE_PUSH 標志。如果設置了該標志(例如主動禁止 Nagle 算法,或者明確是連接的最后一個數據包),則立即返回 1,表示可以發送數據包。接下來,代碼處理特殊數據包,如緊急數據包、帶 FIN 標志的結束包以及帶 F-RTO 標志的包。最后,調用 tcp_nagle_check() 函數進行判斷。如果該函數返回 1,則表示數據包不立即發送。具體邏輯為:如果數據包長度小于當前 MSS,并且滿足以下條件之一,則緩存數據而不立即發送:

  1. 已主動加塞或明確標識后續還有數據(內核表示為 MSG_MORE)。
  2. 啟用了 Nagle 算法,并且存在未被 ACK 確認的已發送數據包。

img

上圖左側展示了未開啟 Nagle 算法的情況,客戶端應用層下傳的數據包被立即發送到網絡中,而不管數據包的大小如何。右側圖展示了開啟 Nagle 算法后的情況,在未收到服務器對第一個數據包的 ACK 確認之前,客戶端應用層下傳的數據包被緩存起來,直到收到 ACK 確認后才發送。這樣,總包數由原來的 3 個減少為 2 個,網絡負載降低,同時客戶端和服務器需要處理的數據包數量也減少,從而降低了 CPU 等資源的消耗。

雖然 Nagle 算法在某些場景下能夠提高網絡利用率、降低包處理主機資源消耗,但在某些場景下卻弊大于利。這需要引入另一個概念:延遲確認(Delayed ACK)。延遲確認是提高網絡利用率的另一種優化機制,它針對的是 ACK 確認包。在 TCP 協議中,正常情況下,接收端會對收到的每一個數據包向發送端發送一個 ACK 確認包。而延遲確認機制則是將 ACK 延后發送,使其與數據包或窗口更新通知包一起發送。

img

上圖左側展示了一般情況,右側圖展示了延遲確認機制中的兩種情況:通過反向數據攜帶 ACK 和超時發送 ACK。根據 RFC 1122,ACK 的最大超時時間為 500 毫秒,但在實際實現中,最大超時時間通常為 200 毫秒。例如,在 Linux 3.4.4 中,TCP_DELACK_MAX 宏定義了該超時最大值:

/* Filename : \linux-3.4.4\include\net\tcp.h */
#define TCP_DELACK_MAX ((unsigned)(HZ/5)) /* maximal time to delay before sending an ACK */

當 Nagle 算法與延遲確認機制相互作用時,可能會導致問題。例如,發送端有一段數據要發送給接收端,數據長度不足以組成一個大包。根據 Nagle 算法,發送端會等待接收端對第一個數據包的 ACK 確認,或者等待應用層傳下更多數據。然而,接收端由于延遲確認機制的作用,不會立即發送 ACK,而是等待。如果接收端在等待超時后才發送 ACK,那么發送端的第二個數據包就需要等待 200 毫秒才能發送。在 HTTP 等單向數據傳輸的應用中,這種情況尤為常見,可能導致時延顯著增加。

為解決上述問題,Minshall 對 Nagle 算法進行了改進,相關描述可在文檔中找到,Linux 內核中也已應用了這種改進。以下是改進后的代碼:

/* Filename : \linux-3.4.4\net\ipv4\tcp_output.c */
/* Minshall's variant of the Nagle send check. */
static inline int tcp_minshall_check (const struct tcp_sock *tp)
{return after (tp->snd_sml, tp->snd_una) &&!after (tp->snd_sml, tp->snd_nxt);
}

該函數的實現基于以下字段的含義(RFC 793、RFC 1122):

  • tp->snd_nxt:下一個待發送的字節序號。
  • tp->snd_una:下一個待確認的字節序號。如果其值等于 tp->snd_nxt,則表示所有已發送數據均已得到確認。
  • tp->snd_sml:最近一個已發送的小包的最后一個字節序號(不一定是已確認的)。

改進的核心思想是:在判斷當前包是否可發送時,僅檢查最近一個小包是否已確認。如果已確認(即 tcp_minshall_check(tp) 返回假),則表示可以發送;否則延遲等待。這種改進縮短了延遲,提高了帶寬利用率。

例如,對于前面提到的場景,第一個數據包是大包,因此無論其對應的 ACK 是否收到,都不會影響對第二個數據包的發送判斷。由于所有小包均已確認(實際上是因為沒有發送過小包),第二個數據包可以直接發送而無需等待。

傳統 Nagle 算法是一種“包 - 停 - 等”協議,在未收到前一個包的確認前不會發送第二個包。而改進的 Nagle 算法是一種折中處理:如果未確認的不是小包,則可以發送第二個包。這種改進保證了在同一個往返時間(RTT)內,網絡上只有一個當前連接的小包。然而,在某些特殊情況下,改進的 Nagle 算法可能會產生不利影響。例如,當 3 個數據塊相繼到達且后續沒有其他數據時,傳統 Nagle 算法僅產生一個小包,而改進的 Nagle 算法可能會產生 2 個小包(第二個小包是由于延遲等待超時產生的)。盡管如此,這種影響并不大,因此可以認為這是一種折中處理。

img

TCP 中的 Nagle 算法默認是啟用的,但并不適用于所有場景。對于 telnet 或 rlogin 等遠程登錄應用,Nagle 算法較為適用(原本就是為此設計的),但在某些應用場景下,需要關閉該算法。例如,在處理 HTTP 持久連接(Keep-Alive)時,可能會出現奇數包和結束小包問題。具體來說,當已有奇數個包發出,并且還有一個結束小包(不是帶 FIN 標志的包,而是 HTTP 請求或響應的結束包)等待發送時,就會出現該問題。以下是一個包含 3 個包和 1 個結束小包的發包示例:

img

最后一個結束小包包含了整個響應數據的最后一些數據。如果當前 HTTP 是非持久連接,則在連接關閉時,最后一個小包會立即發送,不會出現問題。然而,如果當前 HTTP 是持久連接(非 pipelining 處理,僅 HTTP 1.1 支持,并且目前部分舊版瀏覽器尚不支持,nginx 對 pipelining 的支持較弱),那么由于最后一個小包受到 Nagle 算法的影響,無法及時發送。具體原因是:客戶端在未結束上一個請求前不會發送新的請求數據,導致無法攜帶 ACK 而延遲確認,進而服務器未收到客戶端對上一個小包的確認,導致最后一個小包無法發送。這會導致第 n 次請求/響應未能結束,從而客戶端第 n+1 次的請求數據無法發出。

img

為解決該問題,nginx 會主動關閉 Nagle 算法。以下是相關代碼:

/* Filename : \linux-3.4.4\net\ipv4\tcp_output.c */
static void
ngx_http_set_keepalive (ngx_http_request_t *r)
{...if (tcp_nodelay&& clcf->tcp_nodelay&& c->tcp_nodelay == NGX_TCP_NODELAY_UNSET){ngx_log_debug0 (NGX_LOG_DEBUG_HTTP, c->log, 0, "tcp_nodelay");if (setsockopt (c->fd, IPPROTO_TCP, TCP_NODELAY,(const void *) &tcp_nodelay, sizeof (int))== -1){...}c->tcp_nodelay = NGX_TCP_NODELAY_SET;}
}

當 nginx 執行到該函數時,表示當前連接是持久連接。如果滿足以下條件,則對套接字設置 TCP_NODELAY,禁止 Nagle 算法:

  1. 局部變量 tcp_nodelay 用于標記 TCP_CORK 選項,由配置指令 tcp_nopush 指定,默認為 off。
  2. clcf->tcp_nodelay 對應 TCP_NODELAY 選項的配置指令 tcp_nodelay 的配置值,默認為 1。
  3. c->tcp_nodelay 用于標記當前是否已對該套接字設置 TCP_NODELAY 選項,第一次執行時通常為 NGX_TCP_NODELAY_UNSET

因此,如果條件滿足,則設置 TCP_NODELAY,使最后的響應數據能夠立即發送,從而解決上述問題。


TCP/IP 詳解 – Nagle 算法和 TCP_NODELAY

魚思故淵 于 2014-04-19 14:38:38 發布

在客戶端持續向服務器發送小數據時,接收到響應的時間可能會很長。這可能是由于 TCP_NODELAY 的原因。現在基本可以確定,問題出在 Nagle 算法上。

在 TCP/IP 協議中,無論發送多少數據,都需要在數據前加上協議頭,同時接收方需要發送 ACK 以確認收到數據。為了盡可能利用網絡帶寬,TCP 希望每次都能發送足夠大的數據塊。Nagle 算法正是為了實現這一點,避免網絡中充斥著許多小數據塊。

Nagle 算法的基本定義是:任意時刻,最多只能有一個未被確認的小段。所謂“小段”,指的是小于 MSS 尺寸的數據塊;所謂“未被確認”,是指一個數據塊發送出去后,沒有收到對方發送的 ACK 確認該數據已收到。

例如,客戶端調用 socket 的 write 操作將一個 int 型數據(稱為 A 塊)寫入網絡中。由于此時連接是空閑的(即沒有未被確認的小段),因此該 int 型數據會立即發送到服務器端。接著,客戶端又調用 write 操作寫入“/r/n”(簡稱 B 塊),此時 A 塊的 ACK 尚未返回,因此可以認為已經存在一個未被確認的小段。于是,B 塊不會立即發送,而是等待 A 塊的 ACK 收到(大約 40 毫秒后),B 塊才會被發送。整個過程如下圖所示:

這里還隱藏了一個問題:為什么 A 塊數據的 ACK 需要 40 毫秒才收到?這是因為 TCP/IP 中不僅有 Nagle 算法,還有一個 ACK 延遲機制。當服務器端收到數據后,不會立即向客戶端發送 ACK,而是將 ACK 的發送延遲一段時間(假設為 t),希望在 t 時間內服務器端會向客戶端發送應答數據,這樣 ACK 就可以和應答數據一起發送。這解釋了為什么“/r/n”(B 塊)總是在 A 塊之后 40 毫秒才發出。

如果覺得 Nagle 算法影響了性能,可以通過 設置 TCP_NODELAY 禁用它。當然,更合理的方案是 使用一次大數據的寫操作,而不是多次小數據的寫操作


Linux 下 TCP 延遲確認(Delayed Ack)機制導致的時延問題分析

3035597182 2015-04-26 23:03:33

案例

在進行 Server 壓力測試時發現,客戶端不斷向服務器發送請求,并接收服務器的響應。然而,在接收服務器響應的過程中,會出現 recv 阻塞 40 毫秒的情況。查看服務器端日志,服務器處理每個請求的時間均在 2 毫秒以內,并已將響應發送給客戶端。

產生原因

產生該問題的原因是 TCP 的延遲確認(Delayed Ack)機制。服務器端調用 send 向客戶端發送響應時,send 只是將數據存入 TCP 發送緩沖區。TCP 協議棧是否會發送該數據包,還需考慮 Nagle 算法。

解決辦法

在 TCP 中,recv 到數據后,調用一次 setsockopt 函數,設置 TCP_QUICKACK。例如:

setsockopt (fd, IPPROTO_TCP, TCP_QUICKACK, (int*){1}, sizeof (int));

產生問題原因的詳細分析

1. 延遲確認機制及作用

在《TCP/IP 詳解卷一:協議》第 19 章中,詳細描述了 TCP 在處理交互數據流(Interactive Data Flow)時,采用了延遲確認機制和 Nagle 算法來減少小分組數目。

2. TCP 的延遲確認機制為什么會導致 recv 延時?

僅 TCP 的延遲確認機制本身,并不會導致請求延時(因為 recv 系統調用并不需要等待 ACK 包發出去才能返回)。一般來說,只有當該機制與 Nagle 算法或擁塞控制(如慢啟動或擁塞避免)混合作用時,才可能會導致時延增加。

3. 延遲確認機制與 Nagle 算法

Nagle 算法的規則如下(可參考 tcp_output.c 文件中 tcp_nagle_check 函數的注釋):

1). 如果包長度達到 MSS,則允許發送。
2). 如果該包含有 FIN,則允許發送。
3). 如果設置了 TCP_NODELAY 選項,則允許發送。
4). 如果未設置 TCP_CORK 選項,并且所有已發送的包均已被確認,或者所有已發送的小數據包(包長度小于 MSS)均已被確認,則允許發送。

對于規則 4),一個 TCP 連接上最多只能有一個未被確認的小數據包。如果某個小分組的確認被延遲(如案例中的 40 毫秒),那么后續小分組的發送也會相應延遲。也就是說,延遲確認不僅影響被延遲確認的那個數據包,還會影響后續的所有應答包。

4. 關于 TCP_NODELAYTCP_CORK 選項

TCP_CORK 選項與 TCP_NODELAY 一樣,用于控制 Nagle 化。

  1. 打開 TCP_NODELAY 選項,則無論數據包多么小,都會立即發送(不考慮擁塞窗口)。

  2. 如果將 TCP 連接比喻為一個管道,那么 TCP_CORK 選項的作用就像一個塞子。設置 TCP_CORK 選項,就是用塞子塞住管道;取消 TCP_CORK 選項,就是將塞子拔掉。當 TCP_CORK 選項被設置時,TCP 連接不會發送任何小包,只有當數據量達到 MSS 時,才會發送。通常在數據傳輸完成后,需要取消該選項,以便讓不夠 MSS 大小的包能夠及時發送。

5. 為什么 TCP_QUICKACK 需要在每次 recv 后重新設置?

因為 TCP_QUICKACK 不是永久的,所以在每次 recv 數據后,應該重新設置。


神秘的 40 毫秒延遲與 TCP_NODELAY

posted on 2017-03-18 12:31 wajika

在編寫 HTTP Server 并使用 ab 進行性能測試時,出現了一個困擾了幾天的問題:神秘的 40 毫秒延遲。

1. 現象

首先,使用 ab 不加 -k 選項進行測試:

[~/dev/personal/breeze]$ /usr/sbin/ab -c 1 -n 10 http://127.0.0.1:8000/styles/shThemeRDark.css

測試結果顯示,響應時間不超過 1 毫秒。然而,一旦加上 -k 選項啟用 HTTP Keep-Alive,結果就變成了這樣:

[~/dev/personal/breeze]$ /usr/sbin/ab -k -c 1 -n 10 http://127.0.0.1:8000/styles/shThemeRDark.css

響應時間變成了 36 毫秒。使用 strace 工具跟蹤,發現是讀取下一個請求之前的 epoll_wait 花了 40 毫秒才返回。這意味著要么是客戶端等待了 40 毫秒才發送請求,要么是之前寫入的響應數據過了 40 毫秒才到達客戶端。由于 ab 作為壓力測試工具不可能故意延遲發送請求,因此問題出在響應數據的延遲上。

2. 背后的原因

40 毫秒的延遲是由于 TCP 協議中的 Nagle 算法和延遲確認機制共同作用的結果。Nagle 算法旨在提高帶寬利用率,通過合并小的 TCP 包來避免過多的小報文浪費帶寬。如果開啟了該算法(默認情況下),協議棧會累積數據,直到滿足以下條件之一才發送:

  1. 積累的數據量達到最大的 TCP Segment Size。
  2. 收到一個 ACK。

TCP 延遲確認機制也是為了類似的目的而設計的,它會延遲 ACK 包的發送,以便協議棧有機會合并多個 ACK,從而提高網絡性能。如果一個 TCP 連接的一端啟用了 Nagle 算法,而另一端啟用了延遲確認機制,且發送的數據包較小,則可能會出現以下情況:發送端等待接收端對上一個數據包的 ACK 才發送當前數據包,而接收端則延遲了 ACK 的發送。因此,當前數據包也會被延遲。延遲確認機制有一個超時機制,默認超時時間為 40 毫秒。

3. 為什么只有 Write-Write-Read 時才會出問題

維基百科上有一段偽代碼介紹了 Nagle 算法:

if there is new data to sendif the window size >= MSS and available data is >= MSSsend complete MSS segment nowelseif there is unconfirmed data still in the pipeenqueue data in the buffer until an acknowledge is receivedelsesend data immediatelyend ifend if
end if

從偽代碼可以看出,當待發送的數據小于 MSS 時(外層的 else 分支),還需判斷是否有未確認的數據。只有當管道中存在未確認的數據時,才會將數據緩存起來等待 ACK。因此,發送端發送的第一個 write 不會被緩存,而是立即發送(進入內層的 else 分支)。此時,接收端收到數據后,由于期待更多數據,不會立即發送 ACK。根據延遲確認機制,ACK 會被延遲。當發送端發送第二個數據包時,由于隊列中存在未確認的數據包,會進入內層 if 的 then 分支,該數據包會被緩存起來。此時,發送端等待接收端的 ACK,而接收端則延遲發送 ACK,直到超時(40 毫秒),ACK 才被發送回去,發送端緩存的數據包才會被發送。

如果采用 write-read-write-read 模式,則不會出現問題。因為第一個 write 不會被緩存,會立即到達接收端。接收端處理完后發送結果,并將 ACK 與數據一起發送回去,無需延遲確認,因此不會導致任何問題。

4. 解決方案

4.1 優化協議

連續 write 小數據包然后 read 是一種不好的網絡編程模式。這種連續的 write 應該在應用層合并成一次 write。然而,如果程序難以進行這樣的優化,可以采用以下方法。

4.2 開啟 TCP_NODELAY

簡單來說,該選項的作用是禁用 Nagle 算法。禁用后,就不會出現由該算法引起的問題。在 UNIX C 中,可以使用 setsockopt 來實現:

static void _set_tcp_nodelay (int fd)
{int enable = 1;setsockopt (fd, IPPROTO_TCP, TCP_NODELAY, (void*)&enable, sizeof (enable));
}

在 Java 中,Socket 對象有一個 setTcpNoDelay 方法,直接設置為 true 即可。據我所知,Nginx 默認開啟了這個選項,這為我提供了一些安慰:既然 Nginx 都這么做了,我也可以先默認開啟 TCP_NODELAY


Linux 下 TCP 延遲確認(Delayed Ack)機制導致的時延問題分析

潘安群 修改于 2017-06-19 19:29:42

案例一

同事隨手寫了一個壓力測試程序,其邏輯為:每秒鐘先連續發送 N 個 132 字節的包,然后連續接收 N 個由后臺服務回顯回來的 132 字節包。代碼簡化如下:

char sndBuf [132];
char rcvBuf [132];
while (1) {for (int i = 0; i < N; i++) {send (fd, sndBuf, sizeof (sndBuf), 0);...}for (int i = 0; i < N; i++) {recv (fd, rcvBuf, sizeof (rcvBuf), 0);...}sleep (1);
}

在實際測試中發現,當 N 大于等于 3 時,從第 2 秒開始,每次第三個 recv 調用總會阻塞 40 毫秒左右。然而,在分析服務器端日志時,發現所有請求在服務器端的處理時間均在 2 毫秒以下。

定位過程如下:首先嘗試使用 strace 跟蹤客戶端進程,但奇怪的是,一旦 strace attach 上進程,所有收發都正常,不會出現阻塞現象。退出 strace 后,問題重現。經同事提醒,可能是 strace 改變了程序或系統的某些設置。于是使用 tcpdump 抓包分析,發現服務器端在回顯應答包后,客戶端并沒有立即對該數據進行 ACK 確認,而是等待了近 40 毫秒后才確認。經過 Google 并查閱《TCP/IP 詳解卷一:協議》,得知這是 TCP 的延遲確認(Delayed Ack)機制。

解決辦法如下:在 recv 系統調用后,調用一次 setsockopt 函數,設置 TCP_QUICKACK。最終代碼如下:

char sndBuf [132];
char rcvBuf [132];
while (1) {for (int i = 0; i < N; i++) {send (fd, sndBuf, 132, 0);...}for (int i = 0; i < N; i++) {recv (fd, rcvBuf, 132, 0);setsockopt (fd, IPPROTO_TCP, TCP_QUICKACK, (int []){1}, sizeof (int));}sleep (1);
}

案例二

在營銷平臺內存化 CDKEY 版本進行性能測試時,發現請求時延分布異常:90% 的請求時延在 2 毫秒以內,而 10% 的請求時延始終在 38-42 毫秒之間。這是一個非常有規律的數字:40 毫秒。由于之前經歷過案例一,因此猜測這也是由延遲確認機制引起的時延問題。經過簡單的抓包驗證后,通過設置 TCP_QUICKACK 選項,成功解決了時延問題。

延遲確認機制

在《TCP/IP 詳解卷一:協議》第 19 章中,詳細描述了 TCP 在處理交互數據流(Interactive Data Flow)時,采用了延遲確認機制和 Nagle 算法來減少小分組數目。

1. 為什么 TCP 延遲確認會導致延遲?

實際上,僅延遲確認機制本身并不會導致請求延遲(最初以為必須等到 ACK 包發出去,recv 系統調用才會返回)。一般來說,只有當該機制與 Nagle 算法或擁塞控制(如慢啟動或擁塞避免)混合作用時,才可能會導致時延增加。接下來詳細分析它們是如何相互作用的。

延遲確認與 Nagle 算法

Nagle 算法的規則如下(可參考 tcp_output.c 文件中 tcp_nagle_check 函數的注釋):

  1. 如果包長度達到 MSS,則允許發送。
  2. 如果該包含有 FIN,則允許發送。
  3. 如果設置了 TCP_NODELAY 選項,則允許發送。
  4. 如果未設置 TCP_CORK 選項,并且所有已發送的包均已被確認,或者所有已發送的小數據包(包長度小于 MSS)均已被確認,則允許發送。

對于規則 4),一個 TCP 連接上最多只能有一個未被確認的小數據包。如果某個小分組的確認被延遲(如案例中的 40 毫秒),那么后續小分組的發送也會相應延遲。也就是說,延遲確認影響的并不是被延遲確認的那個數據包,而是后續的應答包。

延遲確認與擁塞控制

我們先利用 TCP_NODELAY 選項關閉 Nagle 算法,再來分析延遲確認與 TCP 擁塞控制是如何相互作用的。

慢啟動

TCP 的發送方維護一個擁塞窗口(cwnd)。TCP 連接建立時,該值初始化為 1 個報文段。每收到一個 ACK,該值就增加 1 個報文段。發送方取擁塞窗口與通告窗口(與滑動窗口機制對應)中的最小值作為發送上限。發送方開始發送 1 個報文段,收到 ACK 后,cwnd 從 1 增加到 2,即可以發送 2 個報文段。當收到這兩個報文段的 ACK 后,cwnd 就增加為 4,即指數增長。例如,在第一個 RTT 內,發送一個包,并收到其 ACK,cwnd 增加 1。在第二個 RTT 內,可以發送兩個包,并收到對應的兩個 ACK,則 cwnd 每收到一個 ACK 就增加 1,最終變為 4,實現了指數增長。

在 Linux 實現中,并不是每收到一個 ACK 包,cwnd 就增加 1。如果在收到 ACK 時,并沒有其他數據包在等待被 ACK,則不增加。

2. 為什么是 40 毫秒?這個時間能否調整?

在 RedHat 的官方文檔中,有如下說明:某些應用在發送小報文時,可能會因為 TCP 的延遲確認機制而產生延遲。其默認值為 40 毫秒。可以通過修改 tcp_delack_min,調整系統級別的最小延遲確認時間。例如:

# echo 1 > /proc/sys/net/ipv4/tcp_delack_min

這表示期望設置最小的延遲確認超時時間為 1 毫秒。然而,在 Slackware 和 SUSE 系統下,均未找到該選項,這意味著在這些系統中,40 毫秒的最小值無法通過配置調整。

linux-2.6.39.1/net/tcp.h 中,有如下宏定義:

#define TCP_DELACK_MIN ((unsigned)(HZ/25)) /* minimal time to delay before sending an ACK */

Linux 內核每隔固定周期會發出 timer interrupt (IRQ 0),HZ 用于定義每秒的 timer interrupts 次數。例如,HZ 為 1000,表示每秒有 1000 次 timer interrupts。HZ 可在編譯內核時設置。在現有服務器上運行的系統中,HZ 值均為 250。

因此,最小的延遲確認時間為 40 毫秒。TCP 連接的延遲確認時間通常初始化為最小值 40 毫秒,隨后根據連接的重傳超時時間(RTO)、上次收到數據包與本次接收數據包的時間間隔等參數進行調整。具體的調整算法可以參考 linux-2.6.39.1/net/ipv4/tcp_input.c 中的 tcp_event_data_recv 函數。

3. 為什么 TCP_QUICKACK 需要在每次調用 recv 后重新設置?

在 man 7 tcp 中,有如下說明:

TCP_QUICKACK
Enable quickack mode if set or disable quickack mode if cleared. In quickack mode, acks are sent immediately, rather than delayed if needed in accordance to normal TCP operation. This flag is not permanent, it only enables a switch to or from quickack mode. Subsequent operation of the TCP protocol will once again enter/leave quickack mode depending on internal protocol processing and factors such as delayed ack timeouts occurring and data transfer. This option should not be used in code intended to be portable.

手冊中明確指出 TCP_QUICKACK 不是永久的。其具體實現如下:

case TCP_QUICKACK:if (!val) {icsk->icsk_ack.pingpong = 1;} else {icsk->icsk_ack.pingpong = 0;if ((1 << sk->sk_state) &(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT) &&inet_csk_ack_scheduled (sk)) {icsk->icsk_ack.pending |= ICSK_ACK_PUSHED;tcp_cleanup_rbuf (sk, 1);if (!(val & 1))icsk->icsk_ack.pingpong = 1;}}break;

Linux 下的 socket 有一個 pingpong 屬性,用于表明當前連接是否為交互數據流。如果其值為 1,則表示為交互數據流,會使用延遲確認機制。然而,pingpong 的值是動態變化的。例如,當 TCP 連接要發送一個數據包時,會執行如下函數(linux-2.6.39.1/net/ipv4/tcp_output.c,Line 156):

/* Congestion state accounting after a packet has been sent. */
static void tcp_event_data_sent (struct tcp_sock *tp, struct sk_buff *skb, struct sock *sk)
{...tp->lsndtime = now;/* If it is a reply for ato after last received* packet, enter pingpong mode.*/if ((u32)(now - icsk->icsk_ack.lrcvtime) < icsk->icsk_ack.ato)icsk->icsk_ack.pingpong = 1;
}

最后兩行代碼說明:如果當前時間與最近一次接收數據包的時間間隔小于計算的延遲確認超時時間,則重新進入交互數據流模式。也可以這樣理解:延遲確認機制被確認有效時,會自動進入交互式。

通過以上分析可知,TCP_QUICKACK 選項需要在每次調用 recv 后重新設置

4. 為什么不是所有包都延遲確認?

在 TCP 實現中,使用 tcp_in_quickack_mode 函數(linux-2.6.39.1/net/ipv4/tcp_input.c,Line 197)來判斷是否需要立即發送 ACK。其函數實現如下:

/* Send ACKs quickly, if "quick" count is not exhausted* and the session is not interactive.*/
static inline int tcp_in_quickack_mode (const struct sock *sk)
{const struct inet_connection_sock *icsk = inet_csk (sk);return icsk->icsk_ack.quick && !icsk->icsk_ack.pingpong;
}

要滿足以下兩個條件才能進入 quickack 模式:

  1. pingpong 被設置為 0。
  2. 快速確認數(quick)必須為非 0。

關于 pingpong 的值,前面已有描述。而 quick 屬性的代碼注釋為:scheduled number of quick acks,即快速確認的包數量。每次進入 quickack 模式時,quick 被初始化為接收窗口除以 2 倍 MSS 值(linux-2.6.39.1/net/ipv4/tcp_input.c,Line 174)。每次發送一個 ACK 包時,quick 就減 1。

5. 關于 TCP_CORK 選項

TCP_CORK 選項與 TCP_NODELAY 一樣,用于控制 Nagle 化。

  1. 打開 TCP_NODELAY 選項,則意味著無論數據包多么小,都會立即發送(不考慮擁塞窗口)。
  2. 如果將 TCP 連接比喻為一個管道,那么 TCP_CORK 選項的作用就像一個塞子。設置 TCP_CORK 選項,就是用塞子塞住管道;取消 TCP_CORK 選項,就是將塞子拔掉。例如,以下代碼:
int on = 1;
setsockopt (sockfd, SOL_TCP, TCP_CORK, &on, sizeof (on)); // 設置 TCP_CORK
write (sockfd, ...);    // 例如,HTTP 頭
sendfile (sockfd, ...); // 例如,HTTP 正文
on = 0;
setsockopt (sockfd, SOL_TCP, TCP_CORK, &on, sizeof (on)); // 取消 TCP_CORK

TCP_CORK 選項被設置時,TCP 連接不會發送任何小包,只有當數據量達到 MSS 時,才會發送。當數據傳輸完成時,通常需要取消該選項,以便讓不夠 MSS 大小的包能夠及時發送。如果應用程序確定可以一起發送多個數據集合(例如 HTTP 響應的頭和正文),建議設置 TCP_CORK 選項,這樣在這些數據之間不存在延遲。為了提升性能和吞吐量,Web Server 和文件服務器通常會使用該選項。

著名的高性能 Web 服務器 Nginx,在使用 sendfile 模式時,可以通過將 nginx.conf 配置文件中的 tcp_nopush 配置為 on 來打開 TCP_CORK 選項。TCP_NOPUSHTCP_CORK 功能類似,只不過 NOPUSH 是 BSD 下的實現,而 CORK 是 Linux 下的實現。此外,Nginx 為了減少系統調用,追求性能極致,針對短連接(一般傳送完數據后,立即主動關閉連接,對于 Keep-Alive 的 HTTP 持久連接除外),程序并不通過 setsockopt 調用取消 TCP_CORK 選項,因為關閉連接會自動取消 TCP_CORK 選項,并將剩余數據發送出去。


套接字 socket 選項 TCP_NODELAYTCP_CORKTCP_QUICKACK

冬生0 已于 2022-03-02 14:15:28 修改

一、簡介

  • TCP_NODELAY:關閉 Nagle 算法,控制數據的發送。Nagle 算法規定,如果包大于 MSS(Max Segment Size)或含有 FIN,則立即發送;否則放入緩沖區,等待已發送的包被確認后再發送。該算法可以減少網絡中的小包數量,降低 IP 頭部在網絡中的比重,從而提升網絡性能。
  • TCP_CORK:設置后不會發送任何小包(小于 MSS),除非超時 200 毫秒。
  • TCP_QUICKACK(自 Linux 2.4.4 起可用):設置后會立即發送確認 ACK,而不是延遲發送 ACK。如果未開啟,則延遲確認,使得協議有機會合并 ACK,提高網絡利用率。默認超時確認時間為 40 毫秒,該系統值可配置。

二、測試

測試方法:服務器 IP 為 192.168.x.5,客戶端 IP 為 192.168.x.7。服務器端接受客戶端連接,但不做任何處理;客戶端與服務器建立連接后,連續發送 10 個字符。

  1. 服務器端關閉 QUICKACK,客戶端關閉 NODELAY(即會延遲發送):服務器關閉了 QUICKACK,因此會等待超時后再確認,或等到有數據發送時順帶發送確認 ACK。但由于服務器沒有數據發送,因此會等待 40 毫秒超時。由于客戶端沒有收到服務器端的確認 ACK,且 nodelay 關閉(即開啟了延遲發送),因此會等待收到服務器的 ACK 后才繼續發送剩余數據。

  2. 服務器端開啟 QUICKACK,客戶端開啟 NODELAY(立即發送):服務器端收到一個數據后立即發送確認給客戶端。

  3. 服務器端關閉 QUICKACK,客戶端開啟 NODELAY(立即發送):客戶端不等待確認,連續發送 10 個字符。服務器端在 10 毫秒后發送 ACK。疑問:為什么是 10 毫秒后發送 ACK?

  4. 服務器端開啟 QUICKACK,客戶端關閉 NODELAY(即會延遲發送):服務器收到數據后立即發送確認給客戶端,客戶端將剩余的 9 個字符合并發送。

  5. 服務器端關閉 QUICKACK,客戶端開啟 TCP_CORK:客戶端發送 10 個字符,遠小于 MSS,因此會等待 200 毫秒超時后發送。

三、總結

延遲與帶寬利用率不可兼得。如果需要降低延遲,則應開啟 QUICKACKNODELAY;否則關閉兩者,以提高帶寬利用率。

四、附 demo

Server 代碼

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <netdb.h>
#include <errno.h>
#include <syslog.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <signal.h>
#include <sys/wait.h>#define BUFLEN 1024int main (int argc, char** argv)
{int listenfd, connfd;socklen_t clilen;struct sockaddr_in cliaddr, servaddr;listenfd = socket (AF_INET, SOCK_STREAM, 0);memset (&cliaddr, 0, sizeof (cliaddr));memset (&servaddr, 0, sizeof (servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl (INADDR_ANY);servaddr.sin_port = htons (9000);bind (listenfd, (struct sockaddr*)&servaddr, sizeof (servaddr));listen (listenfd, 128);printf ("Listening on port 9000...\n");clilen = sizeof (cliaddr);connfd = accept (listenfd, (struct sockaddr*)&cliaddr, &clilen);if (connfd <= 0) {perror ("accept error\n");exit (1);}int quick = 1;setsockopt (connfd, IPPROTO_TCP, TCP_QUICKACK, &quick, sizeof (int));sleep (10);close (connfd);return 0;
}

Client 代碼

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <errno.h>
#include <syslog.h>
#include <netinet/tcp.h>
#include <sys/socket.h>#define BUFLEN 1024int main (int argc, char** argv)
{int sockfd;struct sockaddr_in servaddr;if (argc != 2) {printf ("Usage: %s <server_addr>\n", argv[0]);exit (1);}sockfd = socket (AF_INET, SOCK_STREAM, 0);memset (&servaddr, 0, sizeof (servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons (9000);if (inet_pton (AF_INET, argv[1], &servaddr.sin_addr) <= 0) {printf ("Invalid address: %s\n", argv[1]);return 1;}if (connect (sockfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) != 0) {printf ("Failed to connect to server %s, errno: %d\n", argv[1], errno);return 1;}int rc = 0;int nodelay = 1;rc = setsockopt (sockfd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof (int));int cork = 1;rc = setsockopt (sockfd, IPPROTO_TCP, TCP_CORK, &cork, sizeof (int));printf ("Connected and options set.\n");char sendline [2] = {0};sendline [0] = 'a';int n = 0;for (int i = 0; i < 10; i++)n += write (sockfd, sendline, strlen (sendline));printf ("%d bytes sent\n", n);if (read (sockfd, sendline, 2) <= 0) {printf ("Server terminated\n");exit (1);}return 0;
}

【計算機網絡】Socket 的 TCP_NODELAY 選項與 Nagle 算法

morris131 于 2024-01-19 09:44:36 發布

TCP_NODELAY 是一個套接字選項,用于控制 TCP 套接字的延遲行為。當 TCP_NODELAY 選項被啟用(即設置為 true)時,會禁用 Nagle 算法,從而實現 TCP 套接字的無延遲傳輸。這意味著每次發送數據時都會立即發送,不會等待緩沖區的填充或等待確認。

TCP_NODELAY 選項的演示

Socket 服務端代碼

package com.morris.socket;import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;/*** Socket 服務端,演示 TCP_NODELAY** @see java.net.SocketOptions*/
public class TcpNoDelayServerDemo {public static void main (String [] args) throws IOException {ServerSocket serverSocket = new ServerSocket (8090);while (true) {Socket client = serverSocket.accept ();System.out.println ("Accepted connection from: " + client.getRemoteSocketAddress ());new Thread (() -> {try {InputStream inputStream = client.getInputStream ();byte [] buffer = new byte [1024];while (true) {int len = inputStream.read (buffer);if (len == -1) {System.out.println ("Connection closed by: " + client.getRemoteSocketAddress ());inputStream.close ();client.close ();break;} else {System.out.print ("Received: " + new String (buffer, 0, len));}}} catch (IOException e) {e.printStackTrace ();}}).start ();}}
}

Socket 客戶端代碼

package com.morris.socket;import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;/*** Socket 客戶端,演示 TCP_NODELAY** @see java.net.SocketOptions*/
public class TcpNoDelayClientDemo {public static void main (String [] args) throws IOException {Socket socket = new Socket ("192.168.1.11", 8099);socket.setTcpNoDelay (true);OutputStream outputStream = socket.getOutputStream ();for (int i = 0; i < 10; i++) {outputStream.write ((i + "\n").getBytes (StandardCharsets.UTF_8));outputStream.flush ();}outputStream.close ();socket.close ();}
}

默認情況下(即 TcpNoDelayfalse,開啟 Nagle 算法)的運行結果:

Accepted connection from: /192.168.1.10:4760
Received: 0
Received: 1
2
3
4
5
6
7
8
9

客戶端發送了 10 個報文,而服務端只收到了 2 個。客戶端在發送報文的過程中進行了合并。

以下是 tcpdump 抓到的服務端的報文,從中也可以清晰地看到,客戶端往服務端發送了 2 個數據報文(Flags 為 PSH):

$ tcpdump tcp port 8099
19:37:53.914138 IP 192.168.1.10.4760 > 192.168.1.11.8099: Flags [S], seq 3193331081, win 64240, options [mss 1452,nop,wscale 8,nop,nop,sackOK], length 0
19:37:53.914182 IP 192.168.1.11.8099 > 192.168.1.10.4760: Flags [S.], seq 2693275116, ack 3193331082, win 29200, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
19:37:53.925761 IP 192.168.1.10.4760 > 192.168.1.11.8099: Flags [.], ack 1, win 516, length 0
19:37:53.927131 IP 192.168.1.10.4760 > 192.168.1.11.8099: Flags [P.], seq 1:3, ack 1, win 516, length 2
19:37:53.927172 IP 192.168.1.11.8099 > 192.168.1.10.4760: Flags [.], ack 3, win 229, length 0
19:37:53.927977 IP 192.168.1.10.4760 > 192.168.1.11.8099: Flags [FP.], seq 3:21, ack 1, win 516, length 18
19:37:53.928363 IP 192.168.1.11.8099 > 192.168.1.10.4760: Flags [F.], seq 1, ack 22, win 229, length 0
19:37:53.940733 IP 192.168.1.10.4760 > 192.168.1.11.8099: Flags [.], ack 2, win 516, length 0

TcpNoDelay 設置為 true 后(即禁用 Nagle 算法)的運行結果:

Accepted connection from: /192.168.1.10:3307
Received: 0
Received: 1
Received: 2
Received: 3
4
Received: 5
6
Received: 7
Received: 8
9

客戶端發送了 10 個報文,而服務端收到了 7 個。客戶端在發送報文的過程中允許小的數據包立即發送,盡量不合并。

以下是 tcpdump 抓到的服務端的報文,從中也可以清晰地看到,客戶端往服務端發送了 10 個數據報文(Flags 為 PSH):

$ tcpdump tcp port 8099
19:39:07.953002 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [S], seq 2087425409, win 64240, options [mss 1452,nop,wscale 8,nop,nop,sackOK], length 0
19:39:07.953071 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [S.], seq 1383068300, ack 2087425410, win 29200, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
19:39:07.961935 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [.], ack 1, win 516, length 0
19:39:07.963806 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 1:3, ack 1, win 516, length 2
19:39:07.963808 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 3:5, ack 1, win 516, length 2
19:39:07.963858 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 3, win 229, length 0
19:39:07.963867 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 5, win 229, length 0
19:39:07.963903 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 5:7, ack 1, win 516, length 2
19:39:07.963925 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 7, win 229, length 0
19:39:07.963904 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 7:9, ack 1, win 516, length 2
19:39:07.963933 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 9, win 229, length 0
19:39:07.963904 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 9:11, ack 1, win 516, length 2
19:39:07.963939 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 11, win 229, length 0
19:39:07.964809 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 11:13, ack 1, win 516, length 2
19:39:07.964810 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 13:15, ack 1, win 516, length 2
19:39:07.964811 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 15:17, ack 1, win 516, length 2
19:39:07.964812 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 17:19, ack 1, win 516, length 2
19:39:07.964813 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 19:21, ack 1, win 516, length 2
19:39:07.964861 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 13, win 229, length 0
19:39:07.964868 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 15, win 229, length 0
19:39:07.964871 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 17, win 229, length 0
19:39:07.964874 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 19, win 229, length 0
19:39:07.964878 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 21, win 229, length 0
19:39:07.964923 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [F.], seq 21, ack 1, win 516, length 0
19:39:07.965196 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [F.], seq 1, ack 22, win 229, length 0
19:39:07.974934 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [.], ack 2, win 516, length 0

Nagle 算法

TCP 協議是網絡編程中最重要的協議之一。TCP 協議將上層的數據附上 TCP 報頭等信息,封裝成一個個報文段(segment),然后交由下層網絡層去處理。TCP 協議定義了 TCP 報文段的結構,如下圖所示:

img

可以看出,TCP 每個報文段的首部大小至少是 20 字節。因此,如果用戶數據僅為 1 字節,再加上網絡層 IP 包頭 20 字節,則整個 IP 數據包的大小為 41 字節。那么整個 IP 數據包的負荷率為 1/41。這顯然是不劃算的,會降低網絡的傳輸效率。當網絡中充斥著這種 IP 數據包時,整個網絡幾乎都在傳輸一些無用的包頭信息,這種問題被稱為小包問題。特別是在 Telnet 協議中,當用戶遠程登錄到一個主機時,他的每一次鍵盤敲擊實際上都會產生一個攜帶用戶數據量小的數據包,這是典型的小包問題。

為了解決這種問題,出現了 Nagle 算法。該算法由 John Nagle 為解決實際過程中的小包問題而發明。其思想是將多個即將發送的小段用戶數據緩存并合并成一個大段數據,一次性發送出去。特別地,只要發送者還沒有收到前一次發送的 TCP 報文段的 ACK(即連接中還存在未回執 ACK 的 TCP 報文段),發送方就應該一直緩存數據,直到數據達到可以發送的大小,然后再統一合并到一起發送出去。如果收到上一次發送的 TCP 報文段的 ACK,則立即發送緩存的數據。

與之相呼應的還有一個網絡優化機制,稱為 TCP 延遲確認。這是針對接收方的機制。由于 ACK 包屬于有效數據較少的小包,因此延遲確認機制會導致接收方將多個收到數據包的 ACK 打包成一個回復包返回給發送方。這樣可以避免過多的只包含 ACK 的 TCP 報文段導致網絡額外開銷(前面提到的小包問題)。延遲確認機制有一個超時機制,即當收到每一個 TCP 報文段后,如果該 TCP 報文段的 ACK 超過一定時間還未發送,就會啟動超時機制,立刻將該 ACK 發送出去。因此,延遲確認機制可能會帶來 500 毫秒的 ACK 延遲確認時間。

延遲確認機制和 Nagle 算法幾乎是在同一時期提出的,但由不同的團隊提出。這兩種機制在某種程度上確實對網絡傳輸進行了優化,在通常的協議棧實現中,這兩種機制是默認開啟的。

然而,這兩種機制結合起來時,可能會產生一些負面的影響,導致應用程序的性能下降。

TCP_NODELAY 使用注意事項

Nagle 算法應用于發送端。簡而言之,對于發送端而言:

  • 當第一次發送數據時,不需要等待,即使是 1 字節的小包也會立即發送。
  • 后續發送數據時,需要累積數據包,直到滿足以下條件之一才會繼續發送數據:
    • 數據包達到最大段大小 MSS。
    • 接收端收到之前數據包的確認 ACK。

MTU(Maximum Transmission Unit)是指在網絡通信中能夠傳輸的最大數據包大小。MSS(Maximum Segment Size)通常小于等于 MTU,因為數據包中還包含 TCP/IP 協議頭部的額外開銷。一般情況下,IPv4 網絡中的 MTU 大小為 1500 字節,而 IPv6 網絡中的 MTU 大小為 1280 字節。

可以通過網卡信息查看 MTU 的大小:

$ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
inet 172.24.104.61  netmask 255.255.240.0  broadcast 172.24.111.255
inet6 fe80::215:5dff:fe6f:5634  prefixlen 64  scopeid 0x20<link>
ether 00:15:5d:6f:56:34  txqueuelen 1000  (Ethernet)
RX packets 213794  bytes 293979318 (293.9 MB)
RX errors 0  dropped 0  overruns 0  frame 0
TX packets 106646  bytes 8514354 (8.5 MB)
TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

禁用 Nagle 算法可以降低延遲,適用于實時性要求較高的應用,如實時音視頻傳輸或實時游戲。然而,禁用 Nagle 算法可能會導致更頻繁的網絡傳輸,增加網絡開銷。因此,在傳輸大量小數據包的情況下,禁用 Nagle 算法可能會降低網絡效率。

TCP_NODELAY 選項通常在創建套接字后設置。對于已經建立的連接,可能需要重新創建套接字并設置選項。

需要根據具體的需求和場景來決定是否使用 TCP_NODELAY 選項,權衡延遲和網絡效率之間的關系。


TCP Nagle 算法:網絡優化的雙刃劍

Linux編程用C 發布于 2024-02-23 21:13?IP 屬地四川

在網絡通信的世界里,TCP(傳輸控制協議)是確保數據可靠傳輸的基石。然而,為了進一步提高網絡效率,TCP 引入了一系列優化算法,其中 Nagle 算法就是其中之一。今天,我們將探討 Nagle 算法的原理、作用 以及它在現代網絡中的應用。

Nagle 算法的起源

Nagle 算法由 John Nagle 在 1984 年提出,旨在解決 TCP/IP 網絡中的小分組問題。在廣域網(WAN)中,頻繁發送小數據包可能會導致網絡擁塞,因為每個小數據包都需要單獨的確認(ACK),這增加了網絡的負載。Nagle 算法通過合并小數據包來減少這種擁塞。

Nagle 算法的工作原理

Nagle 算法的核心思想是:在一個 TCP 連接上,最多只能有一個未被確認的小數據包(小于 MSS,即最大報文段大小)在網絡中。算法的邏輯如下:

  • 如果有數據要發送,且可用窗口大小大于等于 MSS,或者數據包含 FIN 標志(表示連接即將關閉),則立即發送數據。
  • 如果有未確認的數據,且新數據可以與未確認的數據合并(即新數據的大小加上未確認數據的大小小于 MSS),則將新數據添加到未確認數據的末尾。
  • 如果新數據不能與未確認數據合并,且未確認數據已收到 ACK,那么發送未確認數據和新數據。
  • 如果新數據不能與未確認數據合并,且未確認數據未收到 ACK,那么等待 ACK 到達后再發送新數據。

Nagle 算法的優勢與劣勢

優勢

  • 減少網絡擁塞:通過合并小數據包,減少了網絡中的數據包數量,降低了擁塞的可能性。
  • 提高網絡效率:在低速網絡中,Nagle 算法可以顯著提高傳輸效率。

劣勢

  • 增加延遲:在交互式應用中,Nagle 算法可能導致顯著的延遲,因為它等待 ACK 或合并數據包。
  • 不適用于實時應用:對于需要快速響應的應用(如在線游戲、實時視頻流),Nagle 算法可能會影響用戶體驗。

如何調整 Nagle 算法

在某些情況下,我們可能需要關閉 Nagle 算法以減少延遲。這可以通過設置 TCP_NODELAY 套接字選項來實現。在編程時,可以通過以下代碼片段來禁用 Nagle 算法:

int noDelay = 1;
setsockopt (socket, IPPROTO_TCP, TCP_NODELAY, (char *)&noDelay, sizeof (noDelay));

結語

Nagle 算法是 TCP 協議中的一個重要優化。它在提高網絡效率的同時,也可能帶來延遲問題。在設計網絡應用時,開發者需要根據應用的特性和網絡環境來決定是否使用 Nagle 算法。通過合理配置,我們可以在保證數據傳輸可靠性的同時,優化用戶體驗。


via:

  • TCP_NODELAY 詳解 - C/C+±Chinaunix
    http://bbs.chinaunix.net/thread-3767363-1-1.html

  • TCP/IP 詳解 --nagle 算法和 TCP_NODELAY_nagle 和 nodelay-CSDN 博客
    https://blog.csdn.net/yusiguyuan/article/details/24109845

  • Linux 下 TCP 延遲確認(Delayed Ack)機制導致的時延問題分析_產生原因 (延遲確認機制和 Nagle 算法及_行者無疆_新浪博客
    https://blog.sina.com.cn/s/blog_b4ef897e0102vgch.html

  • 神秘的 40 毫秒延遲與 TCP_NODELAY - wajika - 博客園
    https://www.cnblogs.com/wajika/p/6573028.html

  • Linux 下 TCP 延遲確認 (Delayed Ack) 機制導致的時延問題分析 - 騰訊云開發者社區 - 騰訊云
    https://cloud.tencent.com/developer/article/1004356

  • 套接字 socket 選項 TCP_NODELAY、TCP_CORK 與 TCP_QUICKACK_socket nodelay-CSDN 博客
    https://blog.csdn.net/WEI_GW2012/article/details/123213084

  • 【計算機網絡】Socket 的 TCP_NODELAY 選項與 Nagle 算法_tcp nodelay-CSDN 博客
    https://blog.csdn.net/u022812849/article/details/135689740

  • TCP Nagle 算法:網絡優化的雙刃劍 - 知乎
    https://zhuanlan.zhihu.com/p/683650018

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/900449.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/900449.shtml
英文地址,請注明出處:http://en.pswp.cn/news/900449.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

玄機-應急響應-webshell查殺

題目要求&#xff1a; 要求獲取四個flag webshell查殺&#xff1a; 常見的webshell&#xff1a; PHP: eval(), system(), exec(), shell_exec(), passthru(), assert(), base64_decode() ASP: Execute(), Eval(), CreateObject() JSP: Runtime.getRuntime().exec() websh…

docker存儲卷及dockers容器源碼部署httpd

1. COW機制 Docker鏡像由多個只讀層疊加而成,啟動容器時,Docker會加載只讀鏡像層并在鏡像棧頂部添加一個讀寫層。 如果運行中的容器修改了現有的一個已經存在的文件,那么該文件將會從讀寫層下面的只讀層復制到讀寫層,該文件的只讀版本依然存在,只是已經被讀寫層中該文件…

PyTorch中卷積層torch.nn.Conv2d

在 PyTorch 中&#xff0c;卷積層主要由 torch.nn.Conv1d、torch.nn.Conv2d 和 torch.nn.Conv3d 實現&#xff0c;分別對應一維、二維和三維卷積操作。以下是詳細說明&#xff1a; 1. 二維卷積 (Conv2d) - 最常用 import torch.nn as nn# 基本參數 conv nn.Conv2d(in_channe…

從 ZStack 獲取物理機與云主機信息并導出 Excel 文件

文章目錄 從 ZStack 獲取物理機與云主機信息并導出 Excel 文件環境zstack 官網客戶端封裝講解 獲取物理機信息講解 獲取云主機信息并關聯物理機講解 導出數據到 Excel 文件講解 運行主程序講解 總結最終文檔效果完整代碼 從 ZStack 獲取物理機與云主機信息并導出 Excel 文件 在…

5.好事多磨 -- TCP網絡連接Ⅱ

前言 第4章節通過回聲服務示例講解了TCP服務器端/客戶端的實現方法。但這僅是從編程角度的學習&#xff0c;我們尚未詳細討論TCP的工作原理。因此&#xff0c;將詳細講解TCP中必要的理論知識&#xff0c;還將給出第4章節客戶端問題的解決方案。 一、回聲客戶端完美實現 第4章…

sql server數據庫可疑修復

sql server數據庫可疑修復 從上圖可以看到數據庫nchrdb顯示可疑&#xff0c;導致原因為NC系統在增加公共薪資項目的時候&#xff0c;擴展字段報錯了&#xff0c;第一次遇到這種情況&#xff0c;折騰了很久終于解決&#xff0c;記下解決方案&#xff1a; 1&#xff0c;將SQL數據…

Flutter之頁面布局二

目錄&#xff1a; 1、列表布局1.1、基礎列表1.2、水平滑動的列表1.3、網格列表1.3、不同列表項的列表1.4、包含間隔的列表1.6、長列表 2、滾動2.1、浮動的頂欄2.2、平衡錯位滾動 1、列表布局 1.1、基礎列表 import package:flutter/material.dart;void main() > runApp(con…

ARM------硬件程序開發

硬件程序開發流程 相關硬件的工作原理 理解硬件的工作原理&#xff0c;明確硬件的功能和用途。 硬件連接 將硬件設備正確連接到開發板上。 編寫程序 根據硬件功能編寫相應的程序代碼。 調試驗證 通過調試工具驗證程序的正確性&#xff0c;確保硬件功能正常。 控制LED的…

《QT從基礎到進階·七十四》Qt+C++開發一個python編譯器,能夠編寫,運行python程序改進版

1、概述 源碼放在文章末尾 根據上一篇文章回顧下利用QtC實現了一個簡易的python編譯器&#xff0c;類似pycharm或vsCode這樣的編譯器&#xff0c;該python編譯器目前實現了如下功能&#xff1a; &#xff08;1&#xff09;支持編寫python程序 &#xff08;2&#xff09;編寫代…

Winform MQTT客戶端連接方式

項目中使用到Winform的數據轉發服務&#xff0c;所以記錄下使用到的方法。 一.創建單例模板 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace ConsoleApp.Scripts {public class SingleTon&…

Windows強制刪除任何你想刪除的文件和文件夾

Windows強制刪除任何你想刪除的文件和文件夾 本教程適用于 Windows 10/11 系統&#xff0c;工具和命令均經過驗證。 為什么刪除會失敗&#xff1f; 權限不足&#xff1a;文件或文件夾可能需要管理員權限才能刪除。文件被占用&#xff1a;某個程序正在使用目標文件&#xff0c…

Jmeter如何使用MD5進行加密?

在軟件測試中&#xff0c;使用 JMeter 對數據進行 MD5 加密是一項常見需求&#xff0c;尤其是在模擬用戶登錄等涉及密碼加密的場景時。下面詳細介紹在 JMeter 里運用 MD5 加密的具體步驟。 1. 添加 BeanShell 預處理器 JMeter 本身沒有直接的 MD5 加密功能&#xff0c;但可以…

4-c語言中的數據類型

一.C 語?中的常量 1.生活中的數據 整數&#xff1a; 100,200,300,400,500 小數: 11.11 22.22 33.33 字母&#xff1a; a&#xff0c;b&#xff0c;c&#xff0c;d A&#xff0c;B&#xff0c;C&#xff0c;D 在 C 語?中我們把字?叫做字符. 字符?單引號引?。例如A’ 單詞…

中鈞科技通過數字賦能,編織“數字互聯網”助力數字化進程!

時間飛逝轉眼間2025年已過去四分之一&#xff0c;作為一名95后回顧當下的生活&#xff0c;忍不住感慨10年和現在的對比。發現現在的手機支付、網上掛號、APP打車、在線學習、網絡訂餐、線上協同辦公都以逐漸成為人們生活、工作的常態。也正是在這樣的常態背景下&#xff0c;加快…

AI重塑云基礎設施,亞馬遜云科技打造AI定制版IaaS“樣板房”

AI正在徹底重塑云基礎設施。 IDC最新《2025年IDC MarketScape&#xff1a;全球公有云基礎設施即服務&#xff08;IaaS&#xff09;報告》顯示&#xff0c;AI正在通過多種方式重塑云基礎設施&#xff0c;公有云IaaS有望繼續保持快速增長&#xff0c;預計2025年全球IaaS的整體規…

高效深度學習lecture01

lecture01 零樣本學習(Zero-Shot Learning, ZSL)&#xff1a; 模型可以在沒有見過某種特定任務或類別的訓練樣本的情況下&#xff0c;直接完成對應的任務 利用知識遷移 模型在一個任務上訓練時學到的知識&#xff0c;能夠遷移到其他任務上比如&#xff0c;模型知道“狗”和“…

使用 iPerf 測試內網兩臺機器之間的傳輸速度

在現代網絡管理中&#xff0c;確保內部網絡&#xff08;內網&#xff09;的高效運行是至關重要的。為了評估和優化網絡性能&#xff0c;我們需要一種可靠的方法來測試內網中不同設備間的傳輸速率。iPerf 作為一款廣泛使用的工具&#xff0c;能夠幫助我們準確測量兩個節點之間的…

視頻設備軌跡回放平臺EasyCVR如何搭建公共娛樂場所遠程視頻監控系統

一、背景介紹 由于KTV、酒吧、足療店等服務場所人員流動頻繁、環境復雜&#xff0c;一直是治安管理的重點區域。為有效打擊 “黃賭毒”、打架斗毆、尋釁滋事等違法犯罪的活動&#xff0c;打造安全有序的娛樂消費環境&#xff0c;我國相關部門將加大對這類場所的清查與管控力度…

vue進度條組件

<div class"global-mask" v-if"isProgress"><div class"contentBox"><div class"progresstitie">數據加載中請稍后</div><el-progress class"progressStyle" :color"customColor" tex…

Css:如何解決絕對定位子元素內容被父級元素overflow:hidden屬性剪裁

一、問題描述 今天小伙伴提了一個bug&#xff0c;在點擊列表項的“…”按鈕應該出現的懸浮菜單顯示不完整&#xff1a; 二、問題排查 一般這種問題&#xff0c;是由于懸浮菜單采用的是絕對定位&#xff0c;而父級采用了overflow:hidden屬性。但需要注意的是&#xff0c;這里的…