一、概述
FACK下的重傳我們在之前的重傳部分已經進行了介紹,這里簡單介紹一下隨着FACK提出的擁塞控制算法的改進及隨后的進一步改進。
從我們之前介紹的RFC2582和RFC5681中可以看到,快速恢復下當探測到丟包的時候,會設置ssthresh = max (FlightSize / 2, 2*MSS)、 cwnd=ssthresh+3*MSS,隨后發送端收到dup ACK的時候進行cwnd的inflate過程,發送端需要收到大約一半的dup ACK后,才能允許發送新數據,這意味着發送端需要等待大約RTT/2的時間后才能發送新數據,然后隨后的RTT/2時間內每收到一個dup ACK就發送一個新數據包。為了避免初始的RTT/2等待並把發送過程平穩化,Mathis等人在1996年Mathis提出FACK的同時提出了overdamping和rampdown,隨后作者把這兩個算法統一並改進為rate halving,然后作者又給rate halving加了一些門限參數,提出了一個RHBP(Rate-Halving with Bounding Parameters)算法。RHBP的基本思路就是在快速恢復階段的第一個RTT內發送端每收到兩個dup ACK就允許發出一個數據包,從而避免了初始的RTT/2等待並使得發包過程更為平穩。可以看到RHBP假設了一個50%的窗口削減,但是有些最新的擁塞控制算法對於擁塞窗口的削減可能小於50%,因而rate halving會過度削減窗口,針對其存在的類似問題,Mathis又提出了PRR(Proportional Rate Reduction)算法。這個算法在RFC6937中定義,在快速恢復下prr_delivered和prr_out這兩個狀態變量就是PRR狀態變量,RFC6937中定義的PRR算法就是對應我們之前介紹的SACK關閉時候快速恢復下cwnd的更新過程。因此PRR算法下cwnd的具體更新流程請參考前文。
二、wireshark示例
在執行本示例前如下設置initcwnd、ssthresh和reno擁塞控制算法。同時設置tcp_recovery=0關閉RACK重傳,RACK下的快速恢復除了對lost數據包的標記外其余並無太大並無差異。之前文章我們提到過linux中使用fackets_out狀態變量保存FACK信息,這個變量表示當前通過SACK塊確認的最靠前的數據包的個數。通過下面示例來進一步理解這個狀態變量吧。
******@Inspiron:~$ sudo ip route add local 127.0.0.2 dev lo congctl reno initcwnd 12 ssthresh lock 30 #參考本系列destination metric文章
******@Inspiron:~$ sudo ethtool -K lo tso off gso off #關閉tso gso以方便觀察cwnd變化
1、FACK下的快速恢復
在介紹本示例前如果讀者不熟悉linux的TCP代碼,那一定要先看前面的文章。
業務場景:server端與client建立連接后休眠1000ms,接着連續發出12個數據包,每個數據包的大小為50bytes,發送間隔為3ms,在第12數據包write寫入后,立即進行一次write操作寫入(15*50)bytes。server端的發送緩存設置足夠大,因此write操作不會因為發送緩存不足而休眠。server發出的數據包中有5個數據包在傳輸過程中丟失,如下圖所示,我高亮標示出來的No8、No9、No10、No11、No19這五個數據包傳輸丟失。client對收到的每個數據包都會進行ACK回復確認。
No1-No23:這些數據包的處理與前面SACK下的快速恢復示例1的處理相同,因此不再重復介紹。發出No23后,packets_out=14,sacked_out=0, lost_out=0, retrans_out=0,fackets_out=0,prr_delivered=0, prr_out=0,ssthresh=30, cwnd=14,server端TCP處於Open狀態。
No24-No25:No8、No9、No10、No11這四個數據包丟失,client收到亂序的No12后回復No24確認包,No24通過SACK確認了No12數據包。client按序接收的數據包到No7為止,亂序接收了No12,因此更新fackets_out=12-7=5,表示server端當前接收到的最靠前的數據包No12與No7之間差了5個數據包(包括No12本身)。同樣會更新sacked_out=1。此時dupthresh=3,在打開FACK的情況下,server端會判斷如果fackets_out>dupthresh,就表示需要進行快速重傳(請參考前面FACK重傳的介紹文章),因此server端從Open狀態直接切換到Recovery狀態,注意這個切換中間是不經過Disorder狀態的,進入Recovery狀態的時候更新prior_ssthresh=max(ssthresh,3/4*cwnd)=30, ssthresh=max(cwnd/2,2)=7, prior_cwnd=cwnd=14, prr_delivered=0, prr_out=0,high_seq=801。因為fackets_out-dupthresh=2,因此server端TCP會把系列號范圍(101,151)、(151,201)這兩個數據包標記為lost,更新lost_out=2。接着進入Recovery狀態下更新cwnd的流程,更新prr_delivered=1,此時in_flight= packets_out - ( sacked_out + lost_out) + retrans_out = 14-(1+2)+0=11,delta=sshresh-in_flight=7-11=-4<0,因此sndcnt = (ssthresh * prr_delivered + prior_cwnd - 1)/prior_cwnd - prr_out =(7*1+14-1)/14-0=1, sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0)) = max(1, 1)=1,因而更新cwnd = in_flight + sndcnt = 12。接着server端進入快速重傳流程,此時雖然有兩個數據包被標記為lost但是因為cwnd的限制只能重傳一個數據包,即No25,然后更新retrans_out=1, prr_out=1。隨后server端嘗試發送新的數據包同樣由於cwnd限制未能發出。
No26:server端收到No26后,No26通過SACK塊新確認了一個數據包,因此更新sacked_out=2,而No26通過SACK確認的最靠前的數據包為(351,401),與系列號Ack=101相差6個數據包,因此更新fackets_out=6,fackets_out-dupthresh=3,因此從Ack=101開始的三個數據包可以標記為lost,(101,151)和(151,201)這兩個數據包在之前已經標記為lost了,因此本次只是額外標記(201,251)為lost,更新lost_out=3。接着進入cwnd的更新,prr_delivered=2,in_flight=14-(2+3)+1=10,delta=-3,sndcnt=(7*2+14-1)/14-1=0,sndcnt=max(0,0)=0,因此cwnd=10。擁塞控制不允許發出新的數據包,故而接下來的快速重傳和新數據發送嘗試都沒有發出數據包。
No27-No28:server端收到No27后更新sacked_out=3, fackets_out=7,lost_out=4,prr_delivered=3,in_flight=14-(3+4)+1=8,delta=-1,sndcnt=(7*3+14-1)/14-1=1,sndcnt=max(1,0)=1,因此cwnd=9。此時擁塞控制允許發出一個數據包,因此隨后的快速重傳流程發出了No28數據包,並更新prr_out=2,retrans_out=2。
No29:server端收到No29后更新sacked_out=4, fackets_out=8, 此時fackets_out-dupthresh=5,也就意味着從Ack=101開始的5個數據包應該被標記為lost,但是第5個數據包的系列號為(301,351),這個數據包已經被SACK確認了,因此不會被標記為lost狀態,lost_out因而不會更新。接着進入更新cwnd的流程,prr_delivered=4,in_flight=14-(4+4)+2=8,delta=-1<0,sndcnt=(7*4+14-1)/14-2=0,sndcnt=max(0,0)=0,因此cwnd=8,此時擁塞控制不允許發出數據包。
No30:server收到No30后,sacked_out=5,fackets_out=9,lost_out=4保持不變原因同上,prr_delivered=5,in_flight=14-(5+4)+2=7, delta=0, 此時delta不再小於0,因此sndcnt的更新流程也發生了變化,sndcnt = min(delta, newly_acked_sacked)=min(0,1)=0, sndcnt=max(0,0)=0,因此cwnd=7,此時同樣不允許發出新的數據包。
No31-No32:server端收到No31后,sacked_out=6,fackets_out=10,lost_out=4,prr_delivered=6,in_flight=14-(6+4)+2=6, delta=1, sndcnt=min(1,1)=1, sndcnt=max(1,0)=1,因此cwnd=7,此時擁塞窗口允許發出一個數據包,接着server端快速重傳No32數據包並更新prr_out=3,retrans_out=3。
No33-No34:這組數據包的處理與No31-No32類似,server收到No33后,sacked_out=7,注意No33這個ACK確認包的SACK塊相比No31新確認數據包(651,701),701這個系列號相比No31的601系列號向前滑動了兩個數據包的大小,因而更新fackets_out=fackets_out+2=12, lost_out=4, prr_delivered=7, in_flight=14-(7+4)+3=6, delta=1, sndcnt=min(1,1)=1, sndcnt=max(1,0)=1,因此cwnd=7,此時擁塞窗口允許發出一個數據包,接着server端快速重傳No34數據包並更新prr_out=4,retrans_out=4。
No35-No36:server收到No35后,sacked_out=8,fackets_out=13,lost_out=4,prr_delivered=8,in_flight=14-(8+4)+4=6, delta=1, sndcnt=min(1,1)=1, sndcnt=max(1,0)=1,因此cwnd=7,此時擁塞窗口允許發出一個數據包,此時server端被標記為lost的數據包已全部重傳了,沒有額外的數據包等待重傳,因此server端發出新數據,對應No36,發出No36后packets_out=15,prr_out=5。
No37-No38:No37的SACK新確認了一個數據包,更新sacked_out=9,fackets_out=14,此時fackets_out-dupthresh=11,而從Ack=101開始的第11個數據包為(601,651),這個數據包是沒有被SACK確認的,因此TCP會把這個數據包標記為lost,進而更新lost_out=5。接着進行cwnd的更新操作,prr_delivered=9, in_flight=15-(9+5)+4=5, delta=2, sndcnt=min(2,1)=1, sndcnt=max(1,0)=1,因此cwnd=6,可以看到此時cwnd在Recovery狀態下又降低到了ssthresh下面。此時cwnd允許發出一個數據包,因此發出No38重傳包,更新retrans_out=5,prr_out=6。
No39-No41:client收到No25重傳包后,回復No39確認包,No39的SACK信息沒有發生變化,但是ack number新確認了一個之前標記為lost的數據包,server收到No39后更新packets_out=14,fackets_out=13,lost_out=4,retrans_out=4,因為SACK塊信息沒有發生變化,因此sacked_out不變,此時in_flight=14-(9+4)+4=5,prr_delivered=10, delta=2>0,因為這個No39這個ACK報文確認了新數據,而且沒有被RACK做標記,因此sndcnt = min(delta, max(prr_delivered - prr_out,newly_acked_sacked) + 1)=min(2,max(10-6,1)+1)=2,sndcnt=max(2,0)=2,cwnd=7,注意到了吧在Recovery狀態下,cwnd也可能會向上調整的,此時擁塞窗口允許發出兩個新的數據包,即對應No40和No41,並更新packets_out=16,prr_out=8。
No42-No43:server端收到No42后,更新packets_out=15,fackets_out=12,lost_out=3,retrans_out=3,sacked_out不變仍為9。此時in_flight=15-(9+3)+3=6,prr_delivered=11,delta=1>0,sndcnt=min(1,max(11-8,1)+1)=1,snd=max(1,0)=1,cwnd=6+1=7。此時擁塞控制允許TCP發出一個數據包,即對應No43,然后更新packets_out=16,prr_out=9。
No44-No45:這組數據包的處理與No42-No43相同,發出No45后,packets_out=16, fackets_out=11,lost_out=2,retrans_out=2, sacked_out=9, prr_delivered=12,cwnd=7,prr_out=10。
No46-No47:client收到No34這個重傳包后,之前由No8、No9、No10、No11四個數據包丟包形成hole得以填上,可以看到No46的因此少了一個(301,601)的SACK塊,這個SACK塊中共包含6個數據包,因此更新sacked_out=3。No46的Ack=601,相比No44新確認了7個數據包,因此更新packets_out=9, fackets_out=4,這7個數據包(251,301)是之前被標記為lost的數據包並進行了重傳,因此更新lost_out=1,retrans_out=1。此時in_flight=9-(3+1)+1=6, prr_delivered=13, delta=1>0, sndcnt=min(1,max(13-10,1)+1)=1,cwnd=7,此時擁塞控制允許發出一個數據包即No47,然后更新packets_out=10,prr_out=11。
No48-No49:No48這個dup ACK對應No36數據包,server收到No48后,更新sacked_out=4,fackets_out=5,in_flight=10-(4+1)+1=6, prr_delivered=14, delta=1>0, sndcnt=min(1,max(14-11,1)+1)=1,cwnd=7,此時擁塞控制允許發出一個數據包即No49,然后更新packets_out=11,prr_out=12。
No50-No51:client端收到No38的重傳后,最后一個由No19形成的hole也被填充上了,因此No50中不在帶有SACK信息,更新sacked_out=0, fackets_out=0,No50的Ack=851新確認了5個數據包,更新packets_out=6,其中4個是之前被SACK確認的,1個是被標記為lost並進行了重傳的,因此更新lost_out=0, retrans_out=0,注意No50的Ack=851>high_seq=801,因此此時server端的TCP進入Open狀態,更新cwnd=ssthresh=7,即此時TCP處於reno的擁塞避免過程中,更新cwnd_cnt=5,此時in_flight=6,因此擁塞控制允許TCP發出一個數據包即No51,發出后更新packets_out=7。
No52-No53:server端收到No52后更新packets_out=6,cwnd_cnt=6,cwnd=7,TCP發出No53數據包后更新packets_out=7。
No54-No56:server收到No54后更新packets_out=6,cwnd_cnt=7,此時cwnd_cnt/cwnd=1,因此更新cwnd=8,cwnd_cnt=0。此時擁塞控制允許TCP發出兩個數據包No55和No56,並更新packets_out=8。此時server端write寫入的數據都已經發出去了。
No57-No64:server端依次收到client端的ACK確認包,最終發出No64后packets_out=0, fackets_out=0,lost_out=0,retrans_out=0, sacked_out=0, cwnd=9(server端收到No59后reno的擁塞避免又更新cwnd=cwnd+1=9),ssthresh=7,cwnd_cnt=0。
最后依然給出系列號的時序圖,不過因為這個圖中不能顯示處SACK的信息,所以看起來不像SACK關閉場景下那么直觀了
補充說明:
1、https://tools.ietf.org/html/draft-mathis-tcp-ratehalving-00
2、https://tools.ietf.org/html/rfc6937
3、http://conferences.sigcomm.org/sigcomm/1996/papers/mathis.pdf