TCP系列54—擁塞控制—17、AQM及ECN


一、概述

ECN的相關內容是在RFC3168中定義的,這里我簡單描述一下RFC3168涉及的主要內容。

1、AQM和RED

        目前TCP中多數的擁塞控制算法都是通過緩慢增加擁塞窗口直到檢測到丟包來進行慢啟動的,這就會導致數據包在路由器緩存隊列堆積,當路由器沒有復雜的調度和緩存管理策略的時候,路由器一般簡單的按照先進先出(FIFO)方式處理數據包,並在緩存隊列滿的時候就會丟棄新數據包(drop tail),這種FIFO/drop tail的路由器稱為passive路由器,會導致多個TCP流同時檢測到丟包,削減擁塞窗口,並進行對應的數據包重傳流程。而active的路由器則會有相對高級的調度和隊列緩存策略,這種路由器用來管理緩存隊列的方法就稱為AQM(active queue management)機制。路由器的AQM機制則會在路由器隊列滿之前探測到擁塞,並提供一個擁塞指示。AQM可以使用丟包或者本文后面要介紹的IP頭中的Congestion Experienced (CE) codepoint來指示擁塞,這樣就削減了丟包重傳的影響,降低了網絡延遲。之所以把CE指示放到IP頭中是因為多數路由器對IP頭的處理效率要高於對IP選項的處理效率。

        Random Early Detection (RED)則是AQM機制中用來探測擁塞和控制擁塞標記的一種方法。RED中有兩個門限一個是minthresh,另外一個是maxthresh,當平均隊列長度小於minthresh的時候,這個數據包總是會被接收處理,當平均隊列長度超過maxthresh的時候,這個數據包總是會被用來指示擁塞(可能通過丟包或者設置CE來指示擁塞),當平均隊列長度位於二者之間的時候,則會有一定的概率這個數據包被用來指示擁塞。RED算法是很多用在路由器和交換機中類似變種的基礎,例如思科的WRED。

2、ECN

       ECN(Explicit Congestion Notification)則是在AQM機制的基礎上,路由器顯式指示TCP發生擁塞的的一種機制,中文一般稱呼為顯式擁塞通告或者顯式擁塞通知。之前我們介紹的TCP的擁塞控制的相關特性都是假設TCP端與端之間的鏈路為一個黑盒,使用丟包來作為網絡擁塞的指示,在丟包后進行重傳,並開始慢啟動或者快速恢復等過程。但是有些交互式操作例如網頁瀏覽或者音視頻傳輸等應用對於丟包和時延很敏感,因此傳統的基於丟包檢測擁塞的方法會使得這類應用的體驗變差。如果傳輸層也支持ECN功能,那么可以在IP報文頭中設置一個ECT(ECN-Capable Transport)指示,當中間路由器的RED算法檢測到某個數據包應該用來指示擁塞的時候,如果這個數據包的ECT指示有效,那么就可以把這個數據包標記為CE,接着當接收端TCP收到這個數據包的時候,如果發現CE標志有效,那么就可以在隨后的ACK報文的TCP頭中設置ECN-Echo標志位來擁塞指示,發送端接收到這個擁塞指示的時候就可以對網絡擁塞作出對應的響應,並在隨后的數據包中把TCP頭中的CWR標志為置位,接收端收到CWR指示的時候就會知道發送端已經收到並處理ECN-Echo標志,隨后的ACK報文則不再繼續設置ECN-Echo標志(注意pure ACK是不可靠傳輸的,因此接收端需要一直發送ECN-Echo直到收到發送端的CWR指示)。TCP發送端在收到ECN-Echo指示后一般擁塞狀態會切換到CWR,之前介紹過CWR是一個與Recovery狀態類似的狀態。

        因為一些向后兼容的問題,目前部分系統對ECN的設置是默認關閉的,因此RFC7514提出了一個新的顯示擁塞指示機制——RECN(Really Explicit Congestion Notification),RECN通過ICMP報文來顯式的指示擁塞。本系列以介紹TCP為主,RECN相關協議格式請參考RFC7514。

3、協議格式

IP頭中有個ECN field,上文提到的CE和ECT的格式如下。

從上圖可以看到ECT有兩種場景,ECT(0)和ECT(1)都表示發送端傳輸層支持ECN,按照RFC3168協議section18.1.1和section20的描述,ECT(1)是一個nonce,可以用來檢驗路由器是否會擦出CE指示,ECT(1)也曾打算用作其他指示,但是綜合對比后還是涉及用來作為nonce了。

而上文中提到的TCP頭中的ECN-Echo標志位即為ECE標志位,TCP頭中的ECE標志位和CWR標志位請參考前面介紹TCP頭的相關文章。

4、linux相關

linux中的TCP只使用ECT(0)來指示傳輸層支持ECN。在/proc/sys/net/ipv4目錄下有兩個設置參數與ECN相關:

tcp_ecn:0表示關閉ECN功能,既不會初始化也不會接受ECN,1表示主動連接和被動連接時候都會嘗試使能ECN,2表示主動連接時候不會使能ECN,被動連接的時候會嘗試使能ECN

tcp_ecn_fallback:這個參數設置為非0時,如果內核偵測到ECN的錯誤行為,就會關閉ECN功能。 這個參數實際上是控制后向兼容的一個參數,TCP建立連接的時候需要進行ECN協商過程,SYN報文中需要同時設置CWR和ECE標志位,如果tcp_ecn_fallback設置為非0,那么重傳SYN報文的時候就會取消CWR和ECE標志的設置。

關於Linux中ECN的實現還有幾點需要說明

  1. 在IP路由表中也可以設置ECN的特性使能情況,我們后面會通過示例演示。

  2. linux設置使用DCTCP擁塞控制算法的時候也會使能ECN功能。DCTCP是斯坦福和微軟一起開發的一個使用RED和ECN的擁塞控制算法,可以有效的降低了緩存隊列的占用。

  3. 協議要求一個發送窗口內(或者RTT內),發送端應該對ECE只響應一次,這個在linux中是通過high_seq狀態變量實現的,當TCP進入CWR狀態的時候,在次收到ECE標志,不會在重新削減ssthresh,當收到的報文中ack number大於high_seq時候,TCP退出CWR狀態切換到Open狀態。后面會有示例

  4. 協議要求發送端削減cwnd的時候(例如由於快速重傳、RTO超時重傳等原因),需要在接下來第一個新數據包中設置TCP頭中的CWR標志。對於linux來說因為采用PRR的cwnd更新算法,因此實際上是相當於削減ssthresh后,需要在接下來第一個新數據包中設置TCP頭中的CWR標志,請參考下面的示例

  5. 按照RFC3168 section6.1.1,如果要使用ECN功能,需要TCP在建立連接的時候進行協商,這里不做文件介紹了,直接通過后面的示例演示

  6. SYN cookie場景下,Linux TCP需要通過SYN-ACK報文中TSopt選項的TSval中第5比特位保存是否使能ECN的信息,因此SYN cookie下如果沒有協商成功TSopt選項也不會也不會使能ECN。

     

二、wireshark示例

RFC3168指定在TCP數據報文中支持ECN,但是在TCP控制報文(TCP SYN, TCP SYN/ACK, pure ACKs, Window probes)和重傳報文中不支持ECN,對於RST和FIN報文,RFC3168並沒有明確描述。預期后續將會進一步擴大ECN的使用范圍,下面示例的描述是以Linux實現和RFC3168為基礎的,使用的是Reno擁塞控制算法未考慮DCTCP這類特殊的擁塞控制算法。

1、ECN協商成功

設置tcp_ecn=1,使得主動連接和被動連接都會嘗試使用ECN,建立連接並發送數據后,TCP交互如下面wireshark所示

其中No1報文的IP頭如下所示,ECN列顯示的內容就是就對應下面IP頭中高亮的部分

No1報文的TCP頭如下所示,其中CWR列和ECN-Echo列即對應下圖TCP頭紅色框中的兩個標志位。注意No1數據包的Info列中顯示的ECN標志實際上是指TCP頭中的ECN-Echo標志位,即ECE標志位。

接着我們說一下ECN的協商過程,在No1這個SYN報文中,需要設置CWR和ECE標志位有效,這種類型的SYN報文協議稱為ECN-setup SYN packet,其他類型的SYN報文稱為non-ECN-setup   SYN packet。在SYN-ACK報文中需要設置ECE標志為有效,並把CWR標志位設置為0,這種類似的SYN-ACK報文,協議稱為ECN-setup SYN-ACK packet,其他類型的SYN-ACK報文稱為non-ECN-setup SYN-ACK packet。ECN-setup SYN packet和ECN-setup SYN-ACK packet報文進行三次握手即表示ECN協商成功。協商成功后,隨后的TCP數據報文才可以設置ECT。

同時注意上面TCP包系列中,在SYN報文、SYN-ACK報文、pure ACK報文中ECN列都是Not-ECT表示對應的數據包不支持ECN功能。而在No4和No6這兩個實際傳輸了數據包的報文中ECN列都是ECT(0),表示傳輸層支持ECN功能,並且這個數據包可以使用ECN功能。

2、ECN協商失敗

下面演示一下路由表設置使能ECN特性,並演示一下ECN協商失敗的處理,同樣在執行上面的示例前如下設置相關參數

  1. #設置與127.0.0.1的主動連接和被動連接都嘗試使用ECN功能
  2. root@Inspiron:/proc/sys/net/ipv4# ip route change local 127.0.0.1 dev lo feature ecn congctl reno
  3. #查詢路由表中127.0.0.1和127.0.0.2的相關設置
  4. root@Inspiron:/proc/sys/net/ipv4# ip route show table local 127.0.0.1
  5. local127.0.0.1 dev lo  scope host  features ecn congctl reno
  6. root@Inspiron:/proc/sys/net/ipv4# ip route show table local 127.0.0.2
  7. local127.0.0.2 dev lo  scope host  initcwnd 3 congctl reno
  8. #全局關閉ECN功能  但是由於路由表的設置與127.0.0.1協商ECN的時候還會嘗試使能ECN
  9. root@Inspiron:/proc/sys/net/ipv4# echo 0 > tcp_ecn

No1:雖然全局設置tcp_ecn=0關閉了ECN功能,但是路由表中設置了與127.0.0.1的連接都會嘗試協商使能ECN,因此No1中設置了CWR和ECE標志位,是一個ECN-setup SYN packet報文。

No2:全局關閉了ECN功能,而且路由表中127.0.0.2的路由並沒有設置使能ECN,因此SYN-ACK中並不會設置ECE,No2是一個non-ECN-setup SYN-ACK packet。

從No2可以看出這個TCP連接協商ECN失敗,因此隨后的No4和No6這兩數據包報文都沒有設置ECT(0),即沒有使能ECN。

3、ECN下的擁塞控制處理

接下來我們把關注點移動到ECN下Linux的擁塞處理上,看一下Linux在ECN下的擁塞控制狀態切換,相關狀態變量的更新。首先把路由表設置成如下所示

  1. root@Inspiron:/proc/sys/net/ipv4# ip route show table all 127.0.0.2
  2. local127.0.0.2 dev lo  table local  scope host  ssthresh lock 50 initcwnd 3 features ecn congctl reno

業務場景:server端與client建立連接后,休眠1000ms,然后以3ms為間隔連續write寫入15個數據包,每個數據包的大小為50bytes,其中第六次寫入的數據包即No11模擬在傳輸過程中被RED標記為CE,client對server端的每個數據包都會回復一個ACK確認包。最終如下圖所示,其中IP頭中ECN標志位為ECT(0)的數據包都被我標記為青綠色了。TCP頭中的ECE和CWR標志可以從Info列查看

No1-No21:這個是連接建立和慢啟動過程,從圖中可以看到No1-No3協商了ECN功能。使能ECN后慢啟動過程並無差異這里不再贅述。最終server端在發出No21報文后,ssthresh=50, cwnd=8, packets_out=8, sacked_out=0,  lost_out=0, retrans_out=0,server端處於Open狀態。

No22-No23:No22報文是No11報文的確認包,首先更新packets_out=7,注意這里No22這個ACK報文中ECE標志位有效(ECE標志位在wireshark的Info列顯示為ECN標志位),server端在收到這個ECE有效的確認包后,擁塞狀態從Open切換到CWR,並且初始化ssthresh=max(cwnd/2,2)=4,high_seq=651, prior_cwnd=8。接着使用PRR算法更新cwnd,更新prr_delivered=1,此時in_flight=7,delta=ssthresh-in_flight=-3<0,接着sndcnt = (ssthresh * prr_delivered + prior_cwnd - 1)/prior_cwnd - prr_out = (4*1+7)/8-0=1,sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0))=max(1,0)=1,因此最終更新cwnd = in_flight + sndcnt = 7+1=8。可以看到此時擁塞窗口允許發出一個新的數據包,CWR狀態下沒有數據包被標記為lost,因此不會嘗試重傳之前的數據包,最終發出No23這個新數據包,注意No23這個數據包響應了No22的ECE,設置了CWR標志位有效。發出No23后,ssthresh=4, cwnd=8, packets_out=8, sacked_out=0,  lost_out=0, retrans_out=0,prr_delivered=1,prr_out=1,server端處於CWR狀態。

No24-No26:這個過程與之前介紹過多次的Recovery狀態下的cwnd更新過程類似,這里僅簡單介紹一下。server端在收到No24后,計算sndcnt = (ssthresh * prr_delivered + prior_cwnd - 1)/prior_cwnd - prr_out = (4*2+7)/8-1=0,sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0))=max(0,0)=0,因此最終更新cwnd = in_flight + sndcnt = 7+0=7。此時擁塞窗口cwnd不允許發出數據包。server端在收到No25后,計算sndcnt = (ssthresh * prr_delivered + prior_cwnd - 1)/prior_cwnd - prr_out = (4*3+7)/8-1=1,sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0))=max(1,0)=1,因此最終更新cwnd = in_flight + sndcnt = 6+1=7。此時擁塞窗口允許發出一個數據包,即對應No26,No26中不再標記CWR標志位。發出No26后,ssthresh=4, cwnd=7, packets_out=7, sacked_out=0,  lost_out=0, retrans_out=0,prr_delivered=3,prr_out=2,server端處於CWR狀態。

No27-No31:cwnd更新過程與之前的Recovery狀態處理類似,最終處理完No31后,ssthresh=4, cwnd=3, packets_out=2, sacked_out=0,  lost_out=0, retrans_out=0,prr_delivered=8,prr_out=2,server端處於CWR狀態。

No32:No32是client對No23的確認包,server首先更新packets_out=1,因為No23中CWR有效,client在收到No23這個報文后,再次回復ACK的時候就不會在設置ECE標志為了,可以從Info列看到從No32開始,client的ACK確認包不再有ECN標志(wireshark中Info列的ECN標志就是TCP頭中的ECE標志)。No32的Ack=701>high_seq,server端TCP切換到Open狀態,更新cwnd=ssthresh=4。接着進入reno的擁塞避免過程更新更cwnd_cnt=1。

No33:最終server端處理完No33后,ssthresh=4, cwnd=4, cwnd_cnt=2, packets_out=0, sacked_out=0,  lost_out=0, retrans_out=0, server端處於Open狀態。

4、CWR狀態被Recovery狀態打斷

本示例路由表的設置與示例3一致

業務場景:本示例業務場景與示例3基本一致,但是有兩個不同點,一個是server端在休眠1000ms后,以3ms為間隔連續write寫入16個數據包,另外一個不同點是No14報文在傳輸過程中丟失,觸發server端Recovery狀態打斷CWR狀態,並進行快速重傳。下面我們看一下Recovery狀態打斷CWR狀態的處理。

No1-No24:這部分的處理與上面的示例類似,不再重復,處理完No24后,ssthresh=4, cwnd=7, packets_out=7, sacked_out=0,  lost_out=0, retrans_out=0,prr_delivered=2,prr_out=1,server端處於CWR狀態。

No25-No26:之前我們介紹各種快速重傳場景收到dup ACK后,都是從Open切換到Disorder狀態,但是如果TCP之前處於CWR狀態,收到dup ACK的時候並不會切換到Disorder狀態,而是繼續停留在CWR狀態。CWR狀態下收到dup ACK時候,cwnd仍然按照PRR流程更新。因此收到No25后,先更新sacked_out=1。 接着進入cwnd更新流程,更新prr_delivered=3,計算sndcnt = (ssthresh * prr_delivered + prior_cwnd - 1)/prior_cwnd - prr_out = (4*3+7)/8-1=1,sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0))=max(1,0)=1,因此最終更新cwnd = in_flight + sndcnt = 6+1=7。此時擁塞窗口允許發出一個數據包,即對應No26,No26中不再標記CWR標志位。發出No26后,ssthresh=4, cwnd=7, packets_out=8, sacked_out=1,  lost_out=0, retrans_out=0,prr_delivered=3,prr_out=2,server端處於CWR狀態。

No27-No29:No27和No28這兩個數據包依然按照PRR算法更新。注意server端在收到No28數據包的時候,sacked_out=3,已經被SACK確認的數據包到達門限dupthresh,因此server端會從CWR狀態切換為Recovery狀態,更新high_seq=751,把No14報文標記為lost狀態,並更新lost_out=1,設置fast_rexmit=1,這樣PRR更新cwnd的時候就可以確保擁塞窗口至少允許發出一個重傳報文。注意從CWR狀態切換為Recovery狀態的時候並不會重新削減ssthresh。server收到No28報文時候,更新prr_delivered=5,計算delta=ssthresh-in_flight=0。 sndcnt = min(delta, max(prr_delivered - prr_out,newly_acked_sacked) + 1)=min(0,max(5-3,1)+1)=0,sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0))=max(0,1)=1,因此最終更新cwnd = in_flight + sndcnt = 4+1=5。接着進行快速重傳,即No29報文,更新retrans_out=1,prr_out=3。

No30-No36:這個過程與SACK打開場景下的快速恢復類似,最終server在處理完No36后,ssthresh=4, cwnd=6,cwnd_cnt=1, packets_out=0, sacked_out=0,  lost_out=0, retrans_out=0,prr_delivered=9,prr_out=4,server端處於Open狀態。

5、ECN下ssthresh削減后下一個發送的新數據包需要設置CWR標志位

最后我們再來看一下ECN協商使能情況下,dup ACK觸發快速重傳,Open->Disorder->Recovery狀態切換場景下,快速重傳后發送的第一個新數據包中CWR標志為使能,如下圖紅色高亮的No18數據包所示。那么為什么上一個示例中快速重傳后的No32這個新數據包沒有設置CWR標記位呢?原因是上一個示例是從CWR狀態切換到Recovery狀態的,在切換到Recovery狀態時,並沒有削減ssthresh,因此快速重傳后的第一個新數據包並不會標記CWR標志位。本示例是一個簡單的SACK下快速重傳/快速恢復的過程,不再逐包解釋相關狀態變量的更新變化情況,感興趣的請參考前文。

 

補充說明:

1、允許ECN在SYNs, Pure ACKs, Window   probes, FINs, RSTs and retransmissions中使用https://datatracker.ietf.org/doc/draft-bagnulo-tcpm-generalized-ecn/

2、RECN https://datatracker.ietf.org/doc/rfc7514/?include_text=1

3、ECN規范文檔 https://datatracker.ietf.org/doc/rfc3168/?include_text=1

4、AQM機制 https://datatracker.ietf.org/doc/rfc7567/?include_text=1

5、RED相關狀態變量和操作規則可以參考http://www.mathcs.emory.edu/~cheung/Courses/558-old/Syllabus/90-NS/RED.html

6、windows設置 netsh int tcp set global ecncapability=enabled 未驗證

7、MAC設置 net.inet.tcp.ecn_initiate_out和net.inet.tcp.ecn_negotiate_in 未驗證

8、DCTCP http://simula.stanford.edu/~alizade/Site/DCTCP.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM