案例一:同僚随手寫個壓力測試程式,其實作邏輯為:每秒鐘先連續發N個132位元組的包,然後連續收N個由背景服務回顯回來的132位元組包。其代碼簡化如下:
在實際測試中發現,當N大于等于3的情況,第2秒之後,每次第三個recv調用,總會阻塞40毫秒左右,但在分析Server端日志時,發現所有請求在Server端處理時耗均在2ms以下。
當時的具體定位過程如下:先試圖用strace跟蹤用戶端程序,但奇怪的是:一旦<code>strace attach</code>上程序,所有收發又都正常,不會有阻塞現象,一旦退出strace,問題重制。經同僚提醒,很可能是strace改變了程式或系統的某些東西(這個問題現在也還沒搞清楚),于是再用tcpdump抓包分析,發現Server後端在回現應答包後,Client端并沒有立即對該資料進行ACK确認,而是等待了近40毫秒後才确認。經過Google,并查閱《TCP/IP詳解卷一:協定》得知,此即TCP的延遲确認(Delayed Ack)機制。
其解決辦法如下:在recv系統調用後,調用一次<code>setsockopt</code>函數,設定<code>TCP_QUICKACK</code>。最終代碼如下:
案例二:在營銷平台記憶體化CDKEY版本做性能測試時,發現請求時耗分布異常:90%的請求均在2ms以内,而10%左右時耗始終在38-42ms之間,這是一個很有規律的數字:40ms。因為之前經曆過案例一,是以猜測同樣是因為延遲确認機制引起的時耗問題,經過簡單的抓包驗證後,通過設定<code>TCP_QUICKACK</code>選項,得以解決時延問題。
在《TCP/IP詳解卷一:協定》第19章對其進行原理進行了較長的描述:TCP在處理互動資料流(即<code>Interactive Data Flow</code>,差別于<code>Bulk Data Flow</code>,即成塊資料流,典型的互動資料流如telnet、rlogin等)時,采用了Delayed Ack機制以及Nagle算法來減少小分組數目。
書上已經對這兩種機制的原理講的很清晰,這裡不再做複述。本文後續部分将通過分析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),那麼後續小分組的發送就會相應的延遲。也就是說延遲确認影響的并不是被延遲确認的那個資料包,而是後續的應答包。
從上面的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結果如下:
上表中的包,是在設定TCP_NODELAY,且cwnd已經增長到3的情況,第7、8、9發出後,受限于擁塞視窗大小,即使此時TCP緩沖區有資料可以發送亦不能繼續發送,即第11個包必須等到第10個包到達後,才能發出,而第10個包明顯有一個40ms的延遲。
注:通過getsockopt的TCP_INFO選項(man 7 tcp)可以檢視TCP連接配接的詳細資訊,例如目前擁塞視窗大小,MSS等。
首先在redhat的官方文檔中,有如下說明:
一些應用在發送小的封包時,可能會因為TCP的Delayed Ack機制,導緻一定的延遲。其值預設為40ms。可以通過修改tcp_delack_min,調整系統級别的最小延遲确認時間。例如:
<code># echo 1 > /proc/sys/net/ipv4/tcpdelackmin</code>
即是期望設定最小的延遲确認逾時時間為1ms。
不過在slackware和suse系統下,均未找到這個選項,也就是說40ms這個最小值,在這兩個系統下,是無法通過配置調整的。
<code>linux-2.6.39.1/net/tcp.h</code>下有如下一個宏定義:
<code>#define TCP_DELACK_MIN ((unsigned)(HZ/25)) /* minimal time to delay before sending an ACK */</code>
注:Linux核心每隔固定周期會發出<code>timer interrupt(IRQ 0)</code>,HZ是用來定義每秒有幾次<code>timer interrupts</code>的。舉例來說,HZ為1000,代表每秒有1000次<code>timer interrupts</code>。HZ可在編譯核心時設定。在我們現有伺服器上跑的系統,HZ值均為250。
以此可知,最小的延遲确認時間為40ms。
TCP連接配接的延遲确認時間一般初始化為最小值40ms,随後根據連接配接的重傳逾時時間(RTO)、上次收到資料包與本次接收資料包的時間間隔等參數進行不斷調整。具體調整算法,可以參考<code>linux-2.6.39.1/net/ipv4/tcp_input.c, Line 564</code>的<code>tcp_event_data_recv</code>函數。
在man 7 tcp中,有如下說明:
手冊中明确描述<code>TCP_QUICKACK</code>不是永久的。那麼其具體實作是如何的呢?參考<code>setsockopt</code>函數關于<code>TCP_QUICKACK</code>選項的實作:
其實linux下socket有一個pingpong屬性來表明目前連結是否為互動資料流,如其值為1,則表明為互動資料流,會使用延遲确認機制。但是pingpong這個值是會動态變化的。例如TCP連結在要發送一個資料包時,會執行如下函數(<code>linux-2.6.39.1/net/ipv4/tcp_output.c</code>, Line 156):
最後兩行代碼說明:如果目前時間與最近一次接受資料包的時間間隔小于計算的延遲确認逾時時間,則重新進入互動資料流模式。也可以這麼了解:延遲确認機制被确認有效時,會自動進入互動式。
通過以上分析可知,TCP_QUICKACK選項是需要在每次調用recv後重新設定的。
TCP實作裡,用<code>tcp_in_quickack_mode(linux-2.6.39.1/net/ipv4/tcp_input.c, Line 197)</code>這個函數來判斷是否需要立即發送ACK。其函數實作如下:
要求滿足兩個條件才能算是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。
<code>TCP_CORK</code>選項與<code>TCP_NODELAY</code>一樣,是控制Nagle化的。
打開<code>TCP_NODELAY</code>選項,則意味着無論資料包是多麼的小,都立即發送(不考慮擁塞視窗)。
如果将TCP連接配接比喻為一個管道,那<code>TCP_CORK</code>選項的作用就像一個塞子。設定<code>TCP_CORK</code>選項,就是用塞子塞住管道,而取消<code>TCP_CORK</code>選項,就是将塞子拔掉。例如下面這段代碼:
當<code>TCP_CORK</code>選項被設定時,TCP連結不會發送任何的小包,即隻有當資料量達到MSS時,才會被發送。當資料傳輸完成時,通常需要取消該選項,以便被塞住,但是又不夠MSS大小的包能及時發出去。如果應用程式确定能一起發送多個資料集合(例如HTTP響應的頭和正文),建議設定<code>TCP_CORK</code>選項,這樣在這些資料之間不存在延遲。為提升性能及吞吐量,Web Server、檔案伺服器這一類一般會使用該選項。
著名的高性能Web伺服器Nginx,在使用sendfile模式的情況下,可以設定打開TCP_CORK選項:将nginx.conf配置檔案裡的<code>tcp_nopush</code>配置為on。(<code>TCP_NOPUSH</code>與<code>TCP_CORK</code>兩個選項實作功能類似,隻不過NOPUSH是BSD下的實作,而CORK是Linux下的實作)。另外Nginx為了減少系統調用,追求性能極緻,針對短連接配接(一般傳送完資料後,立即主動關閉連接配接,對于Keep-Alive的HTTP持久連接配接除外),程式并不通過<code>setsockopt</code>調用取消TCP_CORK選項,因為關閉連接配接會自動取消TCP_CORK選項,将剩餘資料發出。
相關推薦