1、概述html
ECN的相關內容是在RFC3168中定義的,這裏我簡單描述一下RFC3168涉及的主要內容。linux
一、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選項的處理效率。windows
Random Early Detection (RED)則是AQM機制中用來探測擁塞和控制擁塞標記的一種方法。RED中有兩個門限一個是minthresh,另一個是maxthresh,當平均隊列長度小於minthresh的時候,這個數據包老是會被接收處理,當平均隊列長度超過maxthresh的時候,這個數據包老是會被用來指示擁塞(可能經過丟包或者設置CE來指示擁塞),當平均隊列長度位於兩者之間的時候,則會有必定的機率這個數據包被用來指示擁塞。RED算法是不少用在路由器和交換機中相似變種的基礎,例如思科的WRED。緩存
二、ECNcookie
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。併發
三、協議格式dom
IP頭中有個ECN field,上文提到的CE和ECT的格式以下。tcp
從上圖能夠看到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頭的相關文章。
四、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的實現還有幾點須要說明
在IP路由表中也能夠設置ECN的特性使能狀況,咱們後面會經過示例演示。
linux設置使用DCTCP擁塞控制算法的時候也會使能ECN功能。DCTCP是斯坦福和微軟一塊兒開發的一個使用RED和ECN的擁塞控制算法,能夠有效的下降了緩存隊列的佔用。
協議要求一個發送窗口內(或者RTT內),發送端應該對ECE只響應一次,這個在linux中是經過high_seq狀態變量實現的,當TCP進入CWR狀態的時候,在次收到ECE標誌,不會在從新削減ssthresh,當收到的報文中ack number大於high_seq時候,TCP退出CWR狀態切換到Open狀態。後面會有示例
協議要求發送端削減cwnd的時候(例如因爲快速重傳、RTO超時重傳等緣由),須要在接下來第一個新數據包中設置TCP頭中的CWR標誌。對於linux來講由於採用PRR的cwnd更新算法,所以其實是至關於削減ssthresh後,須要在接下來第一個新數據包中設置TCP頭中的CWR標誌,請參考下面的示例
按照RFC3168 section6.1.1,若是要使用ECN功能,須要TCP在創建鏈接的時候進行協商,這裏不作文件介紹了,直接經過後面的示例演示
SYN cookie場景下,Linux TCP須要經過SYN-ACK報文中TSopt選項的TSval中第5比特位保存是否使能ECN的信息,所以SYN cookie下若是沒有協商成功TSopt選項也不會也不會使能ECN。
2、wireshark示例
RFC3168指定在TCP數據報文中支持ECN,可是在TCP控制報文(TCP SYN, TCP SYN/ACK, pure ACKs, Window probes)和重傳報文中不支持ECN,對於RST和FIN報文,RFC3168並無明確描述。預期後續將會進一步擴大ECN的使用範圍,下面示例的描述是以Linux實現和RFC3168爲基礎的,使用的是Reno擁塞控制算法未考慮DCTCP這類特殊的擁塞控制算法。
一、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功能。
二、ECN協商失敗
下面演示一下路由表設置使能ECN特性,並演示一下ECN協商失敗的處理,一樣在執行上面的示例前以下設置相關參數
#設置與127.0.0.1的主動鏈接和被動鏈接都嘗試使用ECN功能
root@Inspiron:/proc/sys/net/ipv4# ip route change local 127.0.0.1 dev lo feature ecn congctl reno
#查詢路由表中127.0.0.1和127.0.0.2的相關設置
root@Inspiron:/proc/sys/net/ipv4# ip route show table local 127.0.0.1
local127.0.0.1 dev lo scope host features ecn congctl reno
root@Inspiron:/proc/sys/net/ipv4# ip route show table local 127.0.0.2
local127.0.0.2 dev lo scope host initcwnd 3 congctl reno
#全局關閉ECN功能 可是因爲路由表的設置與127.0.0.1協商ECN的時候還會嘗試使能ECN
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。
三、ECN下的擁塞控制處理
接下來咱們把關注點移動到ECN下Linux的擁塞處理上,看一下Linux在ECN下的擁塞控制狀態切換,相關狀態變量的更新。首先把路由表設置成以下所示
root@Inspiron:/proc/sys/net/ipv4# ip route show table all 127.0.0.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狀態。
四、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狀態。
五、ECN下ssthresh削減後下一個發送的新數據包須要設置CWR標誌位
最後咱們再來看一下ECN協商使能狀況下,dup ACK觸發快速重傳,Open->Disorder->Recovery狀態切換場景下,快速重傳後發送的第一個新數據包中CWR標誌爲使能,以下圖紅色高亮的No18數據包所示。那麼爲何上一個示例中快速重傳後的No32這個新數據包沒有設置CWR標記位呢?緣由是上一個示例是從CWR狀態切換到Recovery狀態的,在切換到Recovery狀態時,並無削減ssthresh,所以快速重傳後的第一個新數據包並不會標記CWR標誌位。本示例是一個簡單的SACK下快速重傳/快速恢復的過程,再也不逐包解釋相關狀態變量的更新變化狀況,感興趣的請參考前文。
補充說明:
一、容許ECN在SYNs, Pure ACKs, Window probes, FINs, RSTs and retransmissions中使用https://datatracker.ietf.org/doc/draft-bagnulo-tcpm-generalized-ecn/
二、RECN https://datatracker.ietf.org/doc/rfc7514/?include_text=1
三、ECN規範文檔 https://datatracker.ietf.org/doc/rfc3168/?include_text=1
四、AQM機制 https://datatracker.ietf.org/doc/rfc7567/?include_text=1
五、RED相關狀態變量和操做規則能夠參考http://www.mathcs.emory.edu/~cheung/Courses/558-old/Syllabus/90-NS/RED.html
六、windows設置 netsh int tcp set global ecncapability=enabled 未驗證
七、MAC設置 net.inet.tcp.ecn_initiate_out和net.inet.tcp.ecn_negotiate_in 未驗證