一、Linux中的慢啟動和擁塞避免
Linux中采用了Google論文的建議把IW初始化成了10了。在linux中一般有三種場景會觸發慢啟動過程
-
1、連接初始建立發送數據的時候,此時cwnd初始化為10,ssthresh初始化為0x7fffffff,因此會觸發慢啟動。但是當路由表中有對應的設置的時候,cwnd和ssthresh會被路由表中的設置的值覆蓋,有可能連接建立后直接進入擁塞避免階段。
2、RTO超時進入Loss狀態后,此時cwnd初始化為1,ssthresh的值會調用具體擁塞控制算法的回調函數(ssthresh函數)來設置,如reno算法中ssthresh = max(snd_cwnd/2, 2U)。
3、TCP連接idle時間在一個RTO以上時候,會重新初始化cwnd。這種場景下的處理我們留到后面CWV的文章中進行介紹。
在reno算法中的慢啟動過程與前面文章概述的慢啟動流程類似,接收的ack number每確認一個報文,就會更新cwnd=cwnd+1。而在reno的擁塞避免過程中,會維護一個cwnd_cnt的變量,初始值為0,當cwnd>=ssthresh的時候,進入擁塞避免后,接收到的ack number每確認一個報文的時候,就會更新cwnd_cnt=cwnd_cnt+1,如果cwnd_cnt>=cwnd,那么就會更新cwnd=cwnd+1,cwnd_cnt=cwnd_cnt-cwnd。在reno算法中只有TCP發送端處於network-limited狀態下才會根據慢啟動和擁塞避免來更新ssthresh和cwnd,關於network-limited狀態的介紹,以及非network-limited狀態下的擁塞控制,請參考后面的CWV相關文章。另外packets_out、 sacked_out、 lost_out、 retrans_out、 in_flight這些狀態變量的說明請參考前面的文章。
至於linux中對於延遲ACK、stretched ACK、ABC、ACK壓縮等的處理我們會在后面的示例中加以演示介紹。建議在讀本篇示例前先把前面兩篇文章讀完。
二、wireshark示例
為了演示方便我們把server端的tcp連接的初始擁塞窗口設置為3,並把擁塞控制算法設置為reno。同時我們關閉網卡的tso和gso功能這樣wireshark才能看到網卡最終發出去的報文。另外在下面的wireshark截圖中為了方便觀察SACK信息,info列中不在顯示TSopt選項的信息,並不是報文不帶有TSopt選項了。
******@Inspiron:~$ sudo ip route add local 127.0.0.2 dev lo congctl reno initcwnd 3 #相關設置慶參考本系列destination metric相關文章
******@Inspiron:~$ ip route show table all | grep 127.0.0.2
local 127.0.0.2 dev lo table local scope host initcwnd 3 congctl reno
******@Inspiron:~$ sudo ethtool -K lo tso off gso off #關閉tso gso以方便觀察cwnd變化
1、慢啟動、擁塞避免與RTO超時、FACK、TLP綜合示例
如下圖所示client為127.0.0.2:10000,server為127.0.0.1:9877,client端與server端時延為50ms,client端使用quickack,對於收到的server端的每個數據包都會回復一個ACK確認包。下圖可以看成是server端的wireshark抓包。server端在連接建立后先休眠1s,然后以5ms為發送間隔連續發送20個數據包,每個數據包的大小為50bytes,加上TSopt選項的大小后正好為client端的mss大小。
No1-No3:TCP連接建立,client端SYN報文中的MSS設置為62,扣除12bytes的TSopt選項的大小后,正好為50byte,此時server端處於Open狀態,初始cwnd為路由表中配置的3,初始化ssthresh為2147483647(十六進制為0x7fffffff),packets_out、sacked_out、lost_out、retrans_out、cwnd_cnt等變量都會初始化為0。
No4-No5:client發送一個22bytes大小的請求報文,server端回復一個ACK確認報文。
No6-No8:server端以5ms為間隔連續寫入20次50bytes大小的數據,可以看到寫入的前三個報文都順利發送出去了。從wireshark中可以看到No8報文發出時間為1.061,server端休眠5ms后,第四個報文寫入時間應該大約為1.066,但是可以從wireshark中看到在1.066這個時間點上server端並沒有發出報文。原因就是受到擁塞控制的限制。在發送完No8報文后in_flight = packets_out - ( sacked_out + lost_out) + retrans_out = 3-(0+0)+0=3,可以看到in_flight的大小已經達到用擁塞窗口的大小,因此不能額外發送新的數據了。
No9-No11:client對No6報文的ACK確認包No9經過大約50ms后到達server端,server端收到No9確認包后,cwnd=cwnd+1=4,而此時只有No7和No8兩個報文還沒有收到ACK確認包因此packets_out=2,in_flight=2-(0+0)+0=2,in_flight比cwnd小2,因此收到No9確認包后,可以發出NO10和No11兩個新的數據包了
No12-No14、No15-No17、No18-No20、No21-No23、No24-No26、No27-No29這個過程與No9-No11類似,cwnd在每收到一個ACK報文后自增1,in_flight則可以從最新發出的報文和最近收到的ack number計算出來。最終server端在發出No29這個tcp報文后,cwnd=10,in_flight=10,可以看到此時同樣是由於擁塞控制限制而不能發出新的數據包。
client端對於No17之后的報文直接丟棄,而且不回復ACK確認包,以觸發server端RTO超時,觀察RTO超時后server端的行為。
No30:可以看到這個報文與No29報文之間的時間差大約為100ms。server端與client端的RTT大約為50ms,想起來這個報文是什么包了吧。這個就是TLP的loss probe報文,server端在發出No29報文的時候會啟動TLP定時器,定時間隔為2*RTT。TLP超時后發現還有未發出的新數據,因此選擇了發送新的數據而不是重傳No29報文。No30發出后,cwnd=10,in_flight=11,可以看到此時in_flight已經超越了cwnd的限制,如之前介紹TLP時候所說,TLP的發送不受擁塞控制的限制。TLP的內容前面不少文章都有過實例詳解,詳細請參考之前的文章。
No31:在No30報文發出后,啟動RTO定時器,從server端程序可以看到此時server端的RTO為252ms。最終RTO超時后,server端重傳了第一個還未收到ACK確認包的報文。可以看到No31和No30之間的時間差正好為252ms。如上所說,在RTO超時后,ssthresh的值由具體的擁塞控制算法的回調函數確定,在我們使用的reno算法中,ssthresh=max(cwnd/2, 2)=5。而cwnd固定設置為1。在RTO超時后linux還會把所有發出去還未被ack number或者SACK確認的數據包標記為lost。此時packets_out=lost_out=11,在發出No31報文后retrans_out=1,則in_flight = packets_out - ( sacked_out + lost_out) + retrans_out=1,正好與cwnd相同,因此發出No31重傳包后,不能在發出其他的重傳報文。
No32、No33:同樣是兩個RTO超時發出的報文,在同一個數據包多次連續RTO超時進行指數回退的過程中ssthreah的值並不會改變,但是每次RTO超時都會把cwnd初始化為1,把cwnd_cnt初始化為0。但是如果另外的其他數據包觸發RTO超時,那么ssthresh的值則會初始化為max(cwnd/2, 2)。注意是其他的數據包觸發了RTO超時才會初始化ssthresh,如果是我們之前介紹的慢啟動重傳(SlowStartRetrans)並不會更新ssthresh。
No34-No36:server端在收到No34ACK確認包后,更新packets_out=packets_out-1=10,lost_out=lost_out-1=10,retrans_out=retrans_out-1=0,in_flight = packets_out - ( sacked_out + lost_out) + retrans_out=0,而此時cwnd為1,因此允許發出一個額外的數據包,接着進入慢啟動重傳流程,重傳No35護具包后retrans_out=1,in_flight也變為1,此時受限與擁塞窗口cwnd而不能在重傳其他數據包,因此退出慢啟動重傳。接着因為收到了新的ACK報文會使用具體的擁塞控制算法更新擁塞窗口,在reno的回調函數中更新cwnd=cwnd+1=2,ssthreah不變仍為5,更新擁塞窗口后會嘗試發送之前未發送的新數據,因此又發出了一個新的數據包No36,更新packets_out為11。注意這里的順序是先進行重傳然后進行擁塞窗口的更新,最后嘗試發送新的未發送數據。這是一個linux通常的處理順序,但是有些場景下可能會先更新擁塞窗口然后進行重傳,最后嘗試發送新數據,例如收到包含有SACK信息的old ACK的時候(old ACK是指ack number在SND.UNA之前)。
No37-No39:這個過程與No34-No36類似,收到ack報文后重傳一個數據包,然后更新cwnd,接着發出一個新數據包。注意No39系列號范圍為(951,1000),我們一共寫入了20個50bytes大小的數據包,因此這個數據已經是最后的一個數據包了。后面已經沒有待發送的新數據了。在No39之后packets_out=11,lost_out=9,retrans_out=1,cwnd=3,in_flight = packets_out - ( sacked_out + lost_out) + retrans_out=3,可以看到正好是cwnd的大小。注意這里的No38重傳就是慢啟動重傳,並沒有觸發RTO超時,因此不會更新ssthresh。注意我們之前介紹過TCP只會維護一個RTO定時器,Seq=351的數據包RTO超時后,觸發的No35超時重傳,但是隨后No38是慢啟動重傳,位於recovery point之前。詳細內容參考前面重傳部分的文章。
No40-No41:No40是對No36的ACK回復報文,帶有一個SACK信息通知server已經收到No36報文,server端收到No40后更新sacked_out=1,in_flight = packets_out - ( sacked_out + lost_out) + retrans_out=11-(1+9)+1=2,而此時cwnd為3,因此可以在額外重傳一個No41報文,雖然wireshark顯示No41為TCP Out-Of-Order,但No41實際上就是一個慢啟動重傳,此時更新retrans_out=2。因為No40這個ACK報文的ack number並沒有確認收到新的數據包,因此cwnd也不能進行更新。
No42-No43:server端收到No38的ACK確認包后,retrans_out=retrans_out-1=1,packets_out=packets_out-1=10,lost_out=lost_out-1=8,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=10-(1+8)+1=2,因此又可以額外重傳一個No43報文了,重傳完畢后更新retrans_out=retrans_out+1=2,接着reno更新cwnd=cwnd+1=4,然后嘗試發送新的未發送數據,上面我們已經說了No39已經是最后一個數據包,已經沒有新的未發送數據了,因此嘗試發送新的未發送數據失敗。注意這時候in_flight=3,cwnd=4,此時擁塞窗口實際上還允許額外發送一個TCP報文的。
No44-No46:從No44的SACK信息可以看到,No44實際上是No39的ACK確認包,server端收到No44后,更新sacked_out=sacked_out+1=2,然后嘗試進行慢啟動重傳,此時in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=10-(2+8)+2=2,cwnd=4因此可以重傳No45、No46兩個報文,更新retrans_out=retrans_out+2=4,No44的ack number並沒有確認新的數據包,因此重傳完后並不會更新cwnd。
No47-No48:No47是No41的ACK確認包,server端在收到這個數據包后,更新retrans_out=retrans_out-1=3,packets_out=packets_out-1=9,lost_out=lost_out-1=7,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=9-(2+7)+3=3,而此時cwnd=4,因此可以重傳一個TCP報文No48,然后更新retrans_out=retrans_out+1=4,接着reno更新cwnd=cwnd+1=5。注意此時的ssthresh=5,因此這次cwnd更新后,TCP將從慢啟動進入到擁塞避免。
No49-No51:No49是No43的ACK確認包,server端收到No49報文后更新retrans_out=retrans_out-1=3,packets_out=packets_out-1=8,lost_out=lost_out-1=6,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=8-(2+6)+3=3,而此時cwnd=5,因此TCP可以額外發出兩個新的重傳包No50和No51,然后更新retrans_out=retrans_out+2=5,接着reno更新cwnd_cnt=cwnd_cnt+1=1,因此cwnd_cnt<cwnd,不滿足擁塞避免更新cwnd的條件,因此cwnd不更新。
No52-No53:server端收到No52后,更新retrans_out=retrans_out-1=4,packets_out=packets_out-1=7,lost_out=lost_out-1=5,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=7-(2+5)+4=4,而此時cwnd=6,因此可以額外的重傳兩個數據包出去,但是此時只剩下最后一個數據包等待重傳了,因此server端只發出了No53重傳包,然后更新retrans_out=retrans_out+1=5,接着reno更新cwnd_cnt=cwnd_cnt+1=2,cwnd不變。
No54:No54是對應No46的ACK確認包,server端收到No54后,更新retrans_out=retrans_out-1=4,packets_out=packets_out-1=6,lost_out=lost_out-1=4,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=6-(2+4)+4=4,接着reno更新cwnd_cnt=cwnd_cnt+1=3,cwnd不變。
No55:No55是對應No48的ACK確認包,server端收到No55后,更新retrans_out=retrans_out-1=3,packets_out=packets_out-1=5,lost_out=lost_out-1=3,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=5-(2+3)+3=3,接着reno更新cwnd_cnt=cwnd_cnt+1=4,cwnd不變。
No56:No56是對應No50的ACK確認包,server端收到No56后,更新retrans_out=retrans_out-1=2,packets_out=packets_out-1=4,lost_out=lost_out-1=2,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=4-(2+2)+2=2,接着reno更新cwnd_cnt=cwnd_cnt+1=5,注意這個時候cwnd_cnt已經滿足擁塞避免更新cwnd的cwnd_cnt>=cwnd條件,因此reno更新cwnd=cwnd+1=6,cwnd_cnt=0。
No57:No57是對應No51的ACK確認包,server端收到No57后,更新retrans_out=retrans_out-1=1,packets_out=packets_out-1=3,lost_out=lost_out-1=1,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=3-(2+1)+1=1,接着reno更新cwnd_cnt=cwnd_cnt+1=1,cwnd不變。
No58:No58是數據傳輸過程中的最后一個報文,是對No53包的ACK確認,注意No58相對於No57,ack number新確認了三個數據包,其中包含No53、另外還有SACK塊中的兩個數據包即No36和No39。server端收到這個報文后會更新retrans_out=retrans_out-1=0,packets_out=packets_out-3=0,lost_out=lost_out-1=0,sacked_out=sacked_out-2=0,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=0,接着reno更新cwnd_cnt=cwnd_cnt+3=4,cwnd不變。
補充說明:
1、https://developers.google.com/speed/articles/tcp_initcwnd_paper.pdf
2、linux中三種慢啟動場景對應的cwnd初始化處理代碼tcp_init_sock、 tcp_enter_loss、 tcp_init_metrics、 tcp_slow_start_after_idle_check