[轉(zhuǎn)] 再探Linux下的TCP延遲確認機制

轉(zhuǎn)載:http://blog.csdn.net/sctq8888/article/details/7398967
案例一:某同事隨手寫個壓力測試程序,其實現(xiàn)邏輯為:每秒鐘先連續(xù)發(fā)N個132字節(jié)的包,然后連續(xù)收N個由后臺服務(wù)回顯回來的132字節(jié)包。其代碼大致如下:

 1: char sndBuf[132];
 2: char rcvBuf[132];
 3: while (1) {
 4:  for (int i = 0; i < N; i++){
 5:  send(fd, sndBuf, sizeof(sndBuf), 0);
 6:  ...
 7:  }
 8:  for (int i = 0; i < N; i++) {
 9:  recv(fd, rcvBuf, sizeof(rcvBuf), 0);
 10:  ...
 11:  }
 12:  sleep(1);
 13: }

在實際測試中發(fā)現(xiàn),當N大于等于3的情況,第2秒之后,每次第三個recv調(diào)用,總會阻塞40毫秒左右,但在分析Server端日志時,發(fā)現(xiàn)所有請求在Server端處理時耗均在2ms以下。
當時的具體定位過程如下:先試圖用strace跟蹤客戶端進程,但奇怪的是:一旦strace attach上進程,所有收發(fā)又都正常,不會有阻塞現(xiàn)象,一旦退出strace,問題重現(xiàn)。經(jīng)同事提醒,很可能是strace改變了程序或系統(tǒng)的某些東西(這個問題現(xiàn)在也還沒搞清楚),于是再用tcpdump抓包分析,發(fā)現(xiàn)Server后端在回現(xiàn)應(yīng)答包后,Client端并沒有立即對該數(shù)據(jù)進行ACK確認,而是等待了近40毫秒后才確認。經(jīng)過Google,并查閱《TCP/IP詳解卷一:協(xié)議》得知,此即TCP的延遲確認(Delayed Ack)機制。
其解決辦法如下:在recv系統(tǒng)調(diào)用后,調(diào)用一次setsockopt函數(shù),設(shè)置TCP_QUICKACK。最終代碼如下:

 1: char sndBuf[132];
 2: char rcvBuf[132];
 3: while (1) {
 4:  for (int i = 0; i < N; i++) {
 5:  send(fd, sndBuf, 132, 0);
 6:  ...
 7:  }
 8:  for (int i = 0; i < N; i++) {
 9:  recv(fd, rcvBuf, 132, 0);
 10:  setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, (int[]){1}, sizeof(int));
 11:  }
 12:  sleep(1);
 13: }

案例二:在營銷平臺內(nèi)存化CDKEY版本做性能測試時,發(fā)現(xiàn)請求時耗分布異常:90%的請求均在2ms以內(nèi),而10%左右時耗始終在38-42ms之間,這是一個很有規(guī)律的數(shù)字:40ms。因為之前經(jīng)歷過案例一,所以猜測同樣是因為延遲確認機制引起的時耗問題,經(jīng)過簡單的抓包驗證后,通過設(shè)置TCP_QUICKACK選項,得以解決時延問題。

延遲確認機制
在《TCP/IP詳解卷一:協(xié)議》第19章對其進行原理進行了詳細描述:TCP在處理交互數(shù)據(jù)流(即Interactive Data Flow,區(qū)別于Bulk Data Flow,即成塊數(shù)據(jù)流,典型的交互數(shù)據(jù)流如telnet、rlogin等)時,采用了Delayed Ack機制以及Nagle算法來減少小分組數(shù)目。
書上已經(jīng)對這兩種機制的原理講的很清晰,這里不再做復(fù)述。本文后續(xù)部分將通過分析TCP/IP在Linux下的實現(xiàn),來解釋一下TCP的延遲確認機制。
1、為什么TCP延遲確認會導致延遲?
其實僅有延遲確認機制,是不會導致請求延遲的(初以為是必須等到ACK包發(fā)出去,recv系統(tǒng)調(diào)用才會返回)。一般來說,只有當該機制與Nagle算法或擁塞控制(慢啟動或擁塞避免)混合作用時,才可能會導致時耗增長。我們下面來詳細看看是如何相互作用的:
延遲確認與Nagle算法
我們先看看Nagle算法的規(guī)則(可參考tcp_output.c文件里tcp_nagle_check函數(shù)注釋):
1)如果包長度達到MSS,則允許發(fā)送;
2)如果該包含有FIN,則允許發(fā)送;
3)設(shè)置了TCP_NODELAY選項,則允許發(fā)送;
4)未設(shè)置TCP_CORK選項時,若所有發(fā)出去的包均被確認,或所有發(fā)出去的小數(shù)據(jù)包(包長度小于MSS)均被確認,則允許發(fā)送。
對于規(guī)則4),就是說要求一個TCP連接上最多只能有一個未被確認的小數(shù)據(jù)包,在該分組的確認到達之前,不能發(fā)送其他的小數(shù)據(jù)包。如果某個小分組的確認被延遲了(案例中的40ms),那么后續(xù)小分組的發(fā)送就會相應(yīng)的延遲。也就是說延遲確認影響的并不是被延遲確認的那個數(shù)據(jù)包,而是后續(xù)的應(yīng)答包。

1 00:44:37.878027 IP 172.25.38.135.44792 > 172.25.81.16.9877: S 3512052379:3512052379(0) win 5840 <mss 1448,wscale 7> 
2 00:44:37.878045 IP 172.25.81.16.9877 > 172.25.38.135.44792: S 3581620571:3581620571(0) ack 3512052380 win 5792 <mss 1460,wscale 2> 
3 00:44:37.879080 IP 172.25.38.135.44792 > 172.25.81.16.9877: . ack 1 win 46 
 ...... 
4 00:44:38.885325 IP 172.25.38.135.44792 > 172.25.81.16.9877: P 1321:1453(132) ack 1321 win 86 
5 00:44:38.886037 IP 172.25.81.16.9877 > 172.25.38.135.44792: P 1321:1453(132) ack 1453 win 2310 
6 00:44:38.887174 IP 172.25.38.135.44792 > 172.25.81.16.9877: P 1453:2641(1188) ack 1453 win 102 
7 00:44:38.887888 IP 172.25.81.16.9877 > 172.25.38.135.44792: P 1453:2476(1023) ack 2641 win 2904 
8 00:44:38.925270 IP 172.25.38.135.44792 > 172.25.81.16.9877: . ack 2476 win 118 
9 00:44:38.925276 IP 172.25.81.16.9877 > 172.25.38.135.44792: P 2476:2641(165) ack 2641 win 2904 
10 00:44:38.926328 IP 172.25.38.135.44792 > 172.25.81.16.9877: . ack 2641 win 134

從上面的tcpdump抓包分析看,第8個包是延遲確認的,而第9個包的數(shù)據(jù),在Server端(172.25.81.16)雖然早就已放到TCP發(fā)送緩沖區(qū)里面(應(yīng)用層調(diào)用的send已經(jīng)返回)了,但按照Nagle算法,第9個包需要等到第個7包(小于MSS)的ACK到達后才能發(fā)出。
延遲確認與擁塞控制
我們先利用TCP_NODELAY選項關(guān)閉Nagle算法,再來分析延遲確認與TCP擁塞控制是如何互相作用的。
慢啟動:TCP的發(fā)送方維護一個擁塞窗口,記為cwnd。TCP連接建立是,該值初始化為1個報文段,每收到一個ACK,該值就增加1個報文段。發(fā)送方取擁塞窗口與通告窗口(與滑動窗口機制對應(yīng))中的最小值作為發(fā)送上限(擁塞窗口是發(fā)送方使用的流控,而通告窗口則是接收方使用的流控)。發(fā)送方開始發(fā)送1個報文段,收到ACK后,cwnd從1增加到2,即可以發(fā)送2個報文段,當收到這兩個報文段的ACK后,cwnd就增加為4,即指數(shù)增長:例如第一個RTT內(nèi),發(fā)送一個包,并收到其ACK,cwnd增加1,而第二個RTT內(nèi),可以發(fā)送兩個包,并收到對應(yīng)的兩個ACK,則cwnd每收到一個ACK就增加1,最終變?yōu)?,實現(xiàn)了指數(shù)增長。
在Linux實現(xiàn)里,并不是每收到一個ACK包,cwnd就增加1,如果在收到ACK時,并沒有其他數(shù)據(jù)包在等待被ACK,則不增加。
本人使用案例1的測試代碼,在實際測試中,cwnd從初始值2開始,最終保持3個報文段的值,tcpdump結(jié)果如下:

1 16:46:14.288604 IP 172.16.1.3.1913 > 172.16.1.2.20001: S 1324697951:1324697951(0) win 5840 <mss 1460,wscale 2> 
2 16:46:14.289549 IP 172.16.1.2.20001 > 172.16.1.3.1913: S 2866427156:2866427156(0) ack 1324697952 win 5792 <mss 1460,wscale 2> 
3 16:46:14.288690 IP 172.16.1.3.1913 > 172.16.1.2.20001: . ack 1 win 1460 
...... 
4 16:46:15.327493 IP 172.16.1.3.1913 > 172.16.1.2.20001: P 1321:1453(132) ack 1321 win 4140 
5 16:46:15.329749 IP 172.16.1.2.20001 > 172.16.1.3.1913: P 1321:1453(132) ack 1453 win 2904 
6 16:46:15.330001 IP 172.16.1.3.1913 > 172.16.1.2.20001: P 1453:2641(1188) ack 1453 win 4140 
7 16:46:15.333629 IP 172.16.1.2.20001 > 172.16.1.3.1913: P 1453:1585(132) ack 2641 win 3498 
8 16:46:15.337629 IP 172.16.1.2.20001 > 172.16.1.3.1913: P 1585:1717(132) ack 2641 win 3498 
9 16:46:15.340035 IP 172.16.1.2.20001 > 172.16.1.3.1913: P 1717:1849(132) ack 2641 win 3498 
10 16:46:15.371416 IP 172.16.1.3.1913 > 172.16.1.2.20001: . ack 1849 win 4140 
11 16:46:15.371461 IP 172.16.1.2.20001 > 172.16.1.3.1913: P 1849:2641(792) ack 2641 win 3498 
12 16:46:15.371581 IP 172.16.1.3.1913 > 172.16.1.2.20001: . ack 2641 win 4536

上表中的包,是在設(shè)置TCP_NODELAY,且cwnd已經(jīng)增長到3的情況,第7、8、9發(fā)出后,受限于擁塞窗口大小,即使此時TCP緩沖區(qū)有數(shù)據(jù)可以發(fā)送亦不能繼續(xù)發(fā)送,即第11個包必須等到第10個包到達后,才能發(fā)出,而第10個包明顯有一個40ms的延遲。
注:通過getsockopt的TCP_INFO選項(man 7 tcp)可以查看TCP連接的詳細信息,例如當前擁塞窗口大小,MSS等。
2、為什么是40ms?這個時間能不能調(diào)整呢?
首先在redhat的官方文檔中,有如下說明:
一些應(yīng)用在發(fā)送小的報文時,可能會因為TCP的Delayed Ack機制,導致一定的延遲。其值默認為40ms??梢酝ㄟ^修改tcp_delack_min,調(diào)整系統(tǒng)級別的最小延遲確認時間。例如:
# echo 1 > /proc/sys/net/ipv4/tcp_delack_min
即是期望設(shè)置最小的延遲確認超時時間為1ms。
不過在slackware和suse系統(tǒng)下,均未找到這個選項,也就是說40ms這個最小值,在這兩個系統(tǒng)下,是無法通過配置調(diào)整的。
linux-2.6.39.1/net/tcp.h下有如下一個宏定義:
1: /* minimal time to delay before sending an ACK */
2: #define TCP_DELACK_MIN ((unsigned)(HZ/25))

注:Linux內(nèi)核每隔固定周期會發(fā)出timer interrupt(IRQ 0),HZ是用來定義每秒有幾次timer interrupts的。舉例來說,HZ為1000,代表每秒有1000次timer interrupts。HZ可在編譯內(nèi)核時設(shè)置。在我們現(xiàn)有服務(wù)器上跑的系統(tǒng),HZ值均為250。
以此可知,最小的延遲確認時間為40ms。
TCP連接的延遲確認時間一般初始化為最小值40ms,隨后根據(jù)連接的重傳超時時間(RTO)、上次收到數(shù)據(jù)包與本次接收數(shù)據(jù)包的時間間隔等參數(shù)進行不斷調(diào)整。具體調(diào)整算法,可以參考linux-2.6.39.1/net/ipv4/tcp_input.c, Line 564的tcp_event_data_recv函數(shù)。
3、為什么TCP_QUICKACK需要在每次調(diào)用recv后重新設(shè)置?
在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不是永久的。那么其具體實現(xiàn)是如何的呢?參考setsockopt函數(shù)關(guān)于TCP_QUICKACK選項的實現(xiàn):

 1: case TCP_QUICKACK:
 2:  if (!val) {
 3:  icsk->icsk_ack.pingpong = 1;
 4:  } else {
 5:  icsk->icsk_ack.pingpong = 0;
 6:  if ((1 << sk->sk_state) &
 7:  (TCPF_ESTABLISHED | TCPF_CLOSE_WAIT) &&
 8:  inet_csk_ack_scheduled(sk)) {
 9:  icsk->icsk_ack.pending |= ICSK_ACK_PUSHED;
 10:  tcp_cleanup_rbuf(sk, 1);
 11:  if (!(val & 1))
 12:  icsk->icsk_ack.pingpong = 1;
 13:  }
 14:  }
 15:  break;

其實linux下socket有一個pingpong屬性來表明當前鏈接是否為交互數(shù)據(jù)流,如其值為1,則表明為交互數(shù)據(jù)流,會使用延遲確認機制。但是pingpong這個值是會動態(tài)變化的。例如TCP鏈接在要發(fā)送一個數(shù)據(jù)包時,會執(zhí)行如下函數(shù)(linux-2.6.39.1/net/ipv4/tcp_output.c, Line 156):

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

最后兩行代碼說明:如果當前時間與最近一次接受數(shù)據(jù)包的時間間隔小于計算的延遲確認超時時間,則重新進入交互數(shù)據(jù)流模式。也可以這么理解:延遲確認機制被確認有效時,會自動進入交互式。
通過以上分析可知,TCP_QUICKACK選項是需要在每次調(diào)用recv后重新設(shè)置的。
4、為什么不是所有包都延遲確認?
TCP實現(xiàn)里,用tcp_in_quickack_mode(linux-2.6.39.1/net/ipv4/tcp_input.c, Line 197)這個函數(shù)來判斷是否需要立即發(fā)送ACK。其函數(shù)實現(xiàn)如下:

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

要求滿足兩個條件才能算是quickack模式:
1、pingpong被設(shè)置為0。
2、快速確認數(shù)(quick)必須為非0。
關(guān)于pingpong這個值,在前面有描述。而quick這個屬性其代碼中的注釋為:scheduled number of quick acks,即快速確認的包數(shù)量,每次進入quickack模式,quick被初始化為接收窗口除以2倍MSS值(linux-2.6.39.1/net/ipv4/tcp_input.c, Line 174),每次發(fā)送一個ACK包,quick即被減1。
5、關(guān)于TCP_CORK選項
TCP_CORK選項與TCP_NODELAY一樣,是控制Nagle化的。
1、打開TCP_NODELAY選項,則意味著無論數(shù)據(jù)包是多么的小,都立即發(fā)送(不考慮擁塞窗口)。
2、如果將TCP連接比喻為一個管道,那TCP_CORK選項的作用就像一個塞子。設(shè)置TCP_CORK選項,就是用塞子塞住管道,而取消TCP_CORK選項,就是將塞子拔掉。例如下面這段代碼:

 1: int on = 1;
 2: setsockopt(sockfd, SOL_TCP, TCP_CORK, &on, sizeof(on)); //set TCP_CORK
 3: write(sockfd, ...); //e.g., http header
 4: sendfile(sockfd, ...); //e.g., http body
 5: on = 0;
 6: setsockopt(sockfd, SOL_TCP, TCP_CORK, &on, sizeof(on)); //unset TCP_CORK

當TCP_CORK選項被設(shè)置時,TCP鏈接不會發(fā)送任何的小包,即只有當數(shù)據(jù)量達到MSS時,才會被發(fā)送。當數(shù)據(jù)傳輸完成時,通常需要取消該選項,以便被塞住,但是又不夠MSS大小的包能及時發(fā)出去。為提升性能及吞吐量,Web Server、文件服務(wù)器這一類一般會使用該選項。
著名的高性能Web服務(wù)器Nginx,在使用sendfile模式的情況下,可以設(shè)置打開TCP_CORK選項:將nginx.conf配置文件里的tcp_nopush配置為on。(TCP_NOPUSH與TCP_CORK兩個選項實現(xiàn)功能類似,只不過NOPUSH是FreeBSD下的實現(xiàn),而CORK是Linux下的選項)。另外Nginx為了減少系統(tǒng)調(diào)用,追求性能極致,針對短連接(一般傳送完數(shù)據(jù)后,立即主動關(guān)閉連接,對于Keep-Alive的HTTP持久連接除外),程序并不通過setsockopt調(diào)用取消TCP_CORK選項,因為關(guān)閉連接會自動取消TCP_CORK選項,將剩余數(shù)據(jù)發(fā)出。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.這篇文章不是本人原創(chuàng)的,只是個人為了對這部分知識做一個整理和系統(tǒng)的輸出而編輯成的,在此鄭重地向本文所引用文章的...
    SOMCENT閱讀 13,365評論 6 174
  • 個人認為,Goodboy1881先生的TCP /IP 協(xié)議詳解學習博客系列博客是一部非常精彩的學習筆記,這雖然只是...
    貳零壹柒_fc10閱讀 5,195評論 0 8
  • 21.1 引言 TCP提供可靠的運輸層。它使用的方法之一就是確認從另一端收到的數(shù)據(jù)。但數(shù)據(jù)和確認都有可能會丟失。T...
    張芳濤閱讀 3,377評論 0 8
  • 我們用websocket和http來研究一下TCP/IP協(xié)議的一些特性,在上一篇文章《https連接的前幾毫秒發(fā)生...
    極樂君閱讀 2,093評論 1 6
  • 我們用websocket和http來研究一下TCP/IP協(xié)議的一些特性,在上一篇文章《https連接的前幾毫秒發(fā)生...
    啾啾噠閱讀 657評論 0 1

友情鏈接更多精彩內(nèi)容