版權聲明:本文由潘安羣原創文章,轉載請註明出處:
文章原文連接:https://www.qcloud.com/community/article/105linux
來源:騰雲閣 https://www.qcloud.com/communitynginx
案例一:同事隨手寫個壓力測試程序,其實現邏輯爲:每秒鐘先連續發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毫秒左右,但在分析Server端日誌時,發現全部請求在Server端處理時耗均在2ms如下。後端
當時的具體定位過程以下:先試圖用strace跟蹤客戶端進程,但奇怪的是:一旦strace attach
上進程,全部收發又都正常,不會有阻塞現象,一旦退出strace,問題重現。經同事提醒,極可能是strace改變了程序或系統的某些東西(這個問題如今也還沒搞清楚),因而再用tcpdump抓包分析,發現Server後端在回現應答包後,Client端並無當即對該數據進行ACK確認,而是等待了近40毫秒後才確認。通過Google,並查閱《TCP/IP詳解卷一:協議》得知,此即TCP的延遲確認(Delayed Ack)機制。服務器
其解決辦法以下:在recv系統調用後,調用一次setsockopt
函數,設置TCP_QUICKACK
。最終代碼以下:session
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%的請求均在2ms之內,而10%左右時耗始終在38-42ms之間,這是一個頗有規律的數字:40ms。由於以前經歷過案例一,因此猜想一樣是由於延遲確認機制引發的時耗問題,通過簡單的抓包驗證後,經過設置TCP_QUICKACK
選項,得以解決時延問題。socket
在《TCP/IP詳解卷一:協議》第19章對其進行原理進行了詳細描述:TCP在處理交互數據流(即Interactive Data Flow
,區別於Bulk Data Flow
,即成塊數據流,典型的交互數據流如telnet、rlogin等)時,採用了Delayed Ack機制以及Nagle算法來減小小分組數目。tcp
書上已經對這兩種機制的原理講的很清晰,這裏再也不作複述。本文後續部分將經過分析TCP/IP在Linux下的實現,來解釋一下TCP的延遲確認機制。函數
其實僅有延遲確認機制,是不會致使請求延遲的(初覺得是必須等到ACK包發出去,recv系統調用纔會返回)。通常來講,只有當該機制與Nagle算法或擁塞控制(慢啓動或擁塞避免)混合做用時,纔可能會致使時耗增加。咱們下面來詳細看看是如何相互做用的:性能
咱們先看看Nagle算法的規則(可參考tcp_output.c文件裏tcp_nagle_check函數註釋):
1)若是包長度達到MSS,則容許發送;
2)若是該包含有FIN,則容許發送;
3)設置了TCP_NODELAY選項,則容許發送;
4)未設置TCP_CORK選項時,若全部發出去的包均被確認,或全部發出去的小數據包(包長度小於MSS)均被確認,則容許發送。
對於規則4),就是說要求一個TCP鏈接上最多隻能有一個未被確認的小數據包,在該分組的確認到達以前,不能發送其餘的小數據包。若是某個小分組的確認被延遲了(案例中的40ms),那麼後續小分組的發送就會相應的延遲。也就是說延遲確認影響的並非被延遲確認的那個數據包,而是後續的應答包。
1 00:44:37.878027 IP 171.24.38.136.44792 > 175.24.11.18.9877: S 3512052379:3512052379(0) win 5840 <mss 1448,wscale 7> 2 00:44:37.878045 IP 175.24.11.18.9877 > 171.24.38.136.44792: S 3581620571:3581620571(0) ack 3512052380 win 5792 <mss 1460,wscale 2> 3 00:44:37.879080 IP 171.24.38.136.44792 > 175.24.11.18.9877: . ack 1 win 46 ...... 4 00:44:38.885325 IP 171.24.38.136.44792 > 175.24.11.18.9877: P 1321:1453(132) ack 1321 win 86 5 00:44:38.886037 IP 175.24.11.18.9877 > 171.24.38.136.44792: P 1321:1453(132) ack 1453 win 2310 6 00:44:38.887174 IP 171.24.38.136.44792 > 175.24.11.18.9877: P 1453:2641(1188) ack 1453 win 102 7 00:44:38.887888 IP 175.24.11.18.9877 > 171.24.38.136.44792: P 1453:2476(1023) ack 2641 win 2904 8 00:44:38.925270 IP 171.24.38.136.44792 > 175.24.11.18.9877: . ack 2476 win 118 9 00:44:38.925276 IP 175.24.11.18.9877 > 171.24.38.136.44792: P 2476:2641(165) ack 2641 win 2904 10 00:44:38.926328 IP 171.24.38.136.44792 > 175.24.11.18.9877: . ack 2641 win 134
從上面的tcpdump抓包分析看,第8個包是延遲確認的,而第9個包的數據,在Server端(175.24.11.18)雖然早就已放到TCP發送緩衝區裏面(應用層調用的send已經返回)了,但按照Nagle算法,第9個包須要等到第個7包(小於MSS)的ACK到達後才能發出。
咱們先利用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,則不增長。
本人使用案例1的測試代碼,在實際測試中,cwnd從初始值2開始,最終保持3個報文段的值,tcpdump結果以下:
1 16:46:14.288604 IP 178.14.5.3.1913 > 178.14.5.4.20001: S 1324697951:1324697951(0) win 5840 <mss 1460,wscale 2> 2 16:46:14.289549 IP 178.14.5.4.20001 > 178.14.5.3.1913: S 2866427156:2866427156(0) ack 1324697952 win 5792 <mss 1460,wscale 2> 3 16:46:14.288690 IP 178.14.5.3.1913 > 178.14.5.4.20001: . ack 1 win 1460 ...... 4 16:46:15.327493 IP 178.14.5.3.1913 > 178.14.5.4.20001: P 1321:1453(132) ack 1321 win 4140 5 16:46:15.329749 IP 178.14.5.4.20001 > 178.14.5.3.1913: P 1321:1453(132) ack 1453 win 2904 6 16:46:15.330001 IP 178.14.5.3.1913 > 178.14.5.4.20001: P 1453:2641(1188) ack 1453 win 4140 7 16:46:15.333629 IP 178.14.5.4.20001 > 178.14.5.3.1913: P 1453:1585(132) ack 2641 win 3498 8 16:46:15.337629 IP 178.14.5.4.20001 > 178.14.5.3.1913: P 1585:1717(132) ack 2641 win 3498 9 16:46:15.340035 IP 178.14.5.4.20001 > 178.14.5.3.1913: P 1717:1849(132) ack 2641 win 3498 10 16:46:15.371416 IP 178.14.5.3.1913 > 178.14.5.4.20001: . ack 1849 win 4140 11 16:46:15.371461 IP 178.14.5.4.20001 > 178.14.5.3.1913: P 1849:2641(792) ack 2641 win 3498 12 16:46:15.371581 IP 178.14.5.3.1913 > 178.14.5.4.20001: . ack 2641 win 4536
上表中的包,是在設置TCP_NODELAY,且cwnd已經增加到3的狀況,第七、八、9發出後,受限於擁塞窗口大小,即便此時TCP緩衝區有數據能夠發送亦不能繼續發送,即第11個包必須等到第10個包到達後,才能發出,而第10個包明顯有一個40ms的延遲。
注:經過getsockopt的TCP_INFO選項(man 7 tcp)能夠查看TCP鏈接的詳細信息,例如當前擁塞窗口大小,MSS等。
首先在redhat的官方文檔中,有以下說明:
一些應用在發送小的報文時,可能會由於TCP的Delayed Ack機制,致使必定的延遲。其值默認爲40ms。能夠經過修改tcp_delack_min,調整系統級別的最小延遲確認時間。例如:
# echo 1 > /proc/sys/net/ipv4/tcpdelackmin
便是指望設置最小的延遲確認超時時間爲1ms。
不過在slackware和suse系統下,均未找到這個選項,也就是說40ms這個最小值,在這兩個系統下,是沒法經過配置調整的。
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。
以此可知,最小的延遲確認時間爲40ms。
TCP鏈接的延遲確認時間通常初始化爲最小值40ms,隨後根據鏈接的重傳超時時間(RTO)、上次收到數據包與本次接收數據包的時間間隔等參數進行不斷調整。具體調整算法,能夠參考linux-2.6.39.1/net/ipv4/tcp_input.c, Line 564
的tcp_event_data_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
不是永久的。那麼其具體實現是如何的呢?參考setsockopt
函數關於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後從新設置的。
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模式:
pingpong被設置爲0。
快速確認數(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。
TCP_CORK
選項與TCP_NODELAY
同樣,是控制Nagle化的。
打開TCP_NODELAY
選項,則意味着不管數據包是多麼的小,都當即發送(不考慮擁塞窗口)。
若是將TCP鏈接比喻爲一個管道,那TCP_CORK
選項的做用就像一個塞子。設置TCP_CORK
選項,就是用塞子塞住管道,而取消TCP_CORK
選項,就是將塞子拔掉。例以下面這段代碼:
int on = 1; setsockopt(sockfd, SOL_TCP, TCP_CORK, &on, sizeof(on)); //set TCP_CORK write(sockfd, ...); //e.g., http header sendfile(sockfd, ...); //e.g., http body on = 0; setsockopt(sockfd, SOL_TCP, TCP_CORK, &on, sizeof(on)); //unset TCP_CORK
當TCP_CORK
選項被設置時,TCP連接不會發送任何的小包,即只有當數據量達到MSS時,纔會被髮送。當數據傳輸完成時,一般須要取消該選項,以便被塞住,可是又不夠MSS大小的包能及時發出去。若是應用程序肯定能一塊兒發送多個數據集合(例如HTTP響應的頭和正文),建議設置TCP_CORK
選項,這樣在這些數據之間不存在延遲。爲提高性能及吞吐量,Web Server、文件服務器這一類通常會使用該選項。
著名的高性能Web服務器Nginx,在使用sendfile模式的狀況下,能夠設置打開TCP_CORK選項:將nginx.conf配置文件裏的tcp_nopush
配置爲on。(TCP_NOPUSH
與TCP_CORK
兩個選項實現功能相似,只不過NOPUSH是BSD下的實現,而CORK是Linux下的實現)。另外Nginx爲了減小系統調用,追求性能極致,針對短鏈接(通常傳送完數據後,當即主動關閉鏈接,對於Keep-Alive的HTTP持久鏈接除外),程序並不經過setsockopt
調用取消TCP_CORK選項,由於關閉鏈接會自動取消TCP_CORK選項,將剩餘數據發出。