TCP系列43—擁塞控制—6、Congestion Window Validation(CWV)


一、概述

在RFC2861中,區分了TCP連接數據傳輸的三種狀態

network-limited:TCP的數據傳輸受限於擁塞窗口而不能發送更多的數據

application-limited:TCP的數據傳輸速率受限與應用層的數據寫入速率,並沒有到達擁塞窗口上限,有些文檔也稱呼這種場景為data-limited

idle:發送端沒有額外的數據等待發送,當數據發送間隔超過一個RTO的時候就認為是ilde態。

之前我們介紹慢啟動和擁塞避免的過程都是基於conservation of packets principle和Ack clocking建立的,cwnd代表了對網絡擁塞狀態的一個評估,很明顯擁塞控制要根據ACK來更新cwnd的前提條件是,當前的數據發送速率真實的反映了cwnd的狀況,也就是說當前傳輸狀態是network-limited。想象一個場景,假如tcp隔了很長時間沒有發送數據包,即進入idle,那么當前真實的網絡擁塞狀態很可能就會與cwnd反映的網絡狀況有差距。而application-limited的場景下,受限數據的ACK報文還可能把cwnd增長到一個異常大的值,顯然是不合理的。

基於上面提到的這個問題,RFC2861引入了擁塞窗口校驗(CWV,Congestion Window Validation)算法,協議中給出的偽代碼如下,其中tcpnow表示獲取當前時間,T_last表示上次數據發送的時間,T_prev表示TCP從network-limited切換到application-limited狀態的時間點,W_used表示程序實際使用的窗口大小。

 
 
 
         
  1. Initially:
  2.       T_last = tcpnow, T_prev = tcpnow, W_used = 0
  3.   After sending a data segment:
  4.       If tcpnow - T_last >= RTO
  5.           (The sender has been idle.)
  6.           ssthresh =  max(ssthresh, 3*cwnd/4)
  7.           For i=1  To (tcpnow - T_last)/RTO
  8.               win =  min(cwnd, receiver's declared max window)
  9.               cwnd =  max(win/2, MSS)
  10.           T_prev = tcpnow
  11.           W_used = 0
  12.       T_last = tcpnow
  13.       If window is full
  14.           T_prev = tcpnow
  15.           W_used = 0
  16.       Else
  17.           If no more data is available to send
  18.               W_used =  max(W_used, amount of unacknowledged data)
  19.               If tcpnow - T_prev >= RTO
  20.                   (The sender has been application-limited.)
  21.                   ssthresh =  max(ssthresh, 3*cwnd/4)
  22.                   win =  min(cwnd, receiver's declared max window)
  23.                   cwnd = (win + W_used)/2
  24.                   T_prev = tcpnow
  25.                   W_used = 0

可以看到基本思路是:當TCP idle超過一個RTO時更新ssthresh =  max(ssthresh, 3*cwnd/4),cwnd 每隔一個RTO更新為 max(win/2, MSS)。當TCP處於application-limited超過一個RTO的時候,更新ssthresh =  max(ssthresh, 3*cwnd/4),cwnd = (win + W_used)/2。

CWV是在RFC2861引入的,目前RFC2861已經被RFC7661取代,RFC7661中提出了一個new CWV的算法,不在區分application-limited和idle兩種狀態,統稱為Rate-Limited,RFC7661對於RFC2861的評價是It had the  correct motivation but the wrong approach to solving this problem。翻譯過來就是說提出RFC2861 CWV算法的人雖然腦子笨了點沒能解決好問題但是動機還是好的。雖然有new CWV的patch,但目前(2016.9)最新的linux內核代碼仍然是使用的RFC2861。因此本篇也以介紹RFC2861的CWV算法為主(實際上對於RFC7661我也沒看完,只是看了協議前面的基本介紹)。

linux中CWV功能受到/proc/sys/net/ipv4/tcp_slow_start_after_idle參數的控制,這個參數設置為非0的時候打開CWV功能,默認是打開的。linux中使用is_cwnd_limited標記當前為network-limited的狀態,is_cwnd_limited可以看成是每個rtt更新一次。linux在實現上實際與RFC2861有些差異,例如協議偽代碼中If window is full這個條件,在慢啟動階段,linux會判斷如果上一個窗口發出的數據中packets_out的最大值(max_packets_out)超過了cwnd的一半,就認為窗口是滿的不會按照application-limited來更新ssthresh和cwnd,如果不是慢啟動階段則直接根據is_cwnd_limited來判斷上一個窗口是否滿,窗口或者說rtt的維護與RTO計算中rtt_seq狀態變量的維護類似,同樣是使用snd.una和snd.nxt來更新的。 對於W_used是以packets_out來更新的。對於ilde態的處理是在應用層write操作時候進行判斷的。

上面這些差異是可以理解的,但是還有一處差異就是,linux在application-limited場景下,只有擁塞狀態處於Open狀態的時候才會更新ssthresh和cwnd,但是另外一方面Open狀態下每次收到ack number反饋確認了新的數據包的時候又會更新T_prev,這樣在linux中基本很難滿足tcpnow - T_prev >= RTO這個條件,這就導致linux在application-limited場景下的處理與RFC2861相差甚遠,一般只會觸發idle場景處理,而不能進入application-limited場景。相關代碼在git上已經找不到最初的修改記錄了,具體原因也就不清楚。但是好在reno擁塞控制算法中當TCP發送端處於application-limited狀態時候並不會更新cwnd。有可能是因為在application-limited場景下,直接在具體擁塞控制算法中控制不更新cwnd比原始的RFC2861 CWV算法效果更好吧。

二、wireshark示例

同樣在測試進行前我們如下設置,使得server端與127.0.0.2的連接,初始cwnd=2,初始ssthresh=8,擁塞控制算法選擇reno。關閉tso、gso功能

 
 
 
         
  1. ******@Inspiron:~$ sudo ip route add local 127.0.0.2 dev lo congctl reno initcwnd 2 ssthresh lock 8      #參考本系列destination metric文章
  2. ******@Inspiron:~$ sudo ethtool -K lo tso off gso off  #關閉tso gso以方便觀察cwnd變化

此處的兩個示例重點關注cwnd和ssthresh的變化,因為沒有SACK及重傳等干擾,sacked_out、lost_out、retrans_out中間變量一直為0,對於下面示例的場景linux內部計算的in_flight = packets_out - ( sacked_out + lost_out) + retrans_out =packets_out,與wireshark中in_fligth列是一致的,因此下面不在詳細解釋這些中間變量。對於有SACK及重傳時這些狀態變量的變化慶請參考前面文章擁塞控制的綜合示例。而is_cwnd_limited、max_packets_out變量的更新示例只會給出更新后的結果,更新的過程涉及到比較多的其他變量,即使貼上內核代碼也需要大量的篇幅來解釋清楚,因為本文不再詳細介紹。感興趣的可以自行對照本示例去學習內核代碼。我會在補充說明中給出幾個與下面示例相關聯的關鍵函數。讀者因此可能很難理解清楚下面的wireshark示例,但是重點觀察兩個宏觀的現象就行了,一個是application-limited狀態下reno不會更新cwnd,另外一個是ilde時間超過RTO后,CWV會更新cwnd和ssthresh。不必過於糾結更新的細節。

1、application-limited狀態linux的處理以及idle后cwnd和ssthresh的更新

client與server端建立連接后先發送一個請求報文,然后server端內核正常回復ACK,另外server應用層在建立與client的連接后休眠50ms,然后發送4個數據包,每個數據包大小為50bytes間隔為5ms,接着以60ms為間隔,連續發送6個數據包,每個數據包的大小為50bytes,構造RFC2861中的application-limited的場景,接着server端休眠300ms,構造RFC2861的idle場景,最后server端再以5ms為間隔,連續發送15個數據包,每個數據包大小為50bytes。client端對於每個server端的報文都回復一個ACK,client與server端的rtt為50ms。

No1-No3:client與server通過三次握手建立連接,連接建立后根據路由表配置,初始化cwnd=2,ssthresh=8。連接建立后進入慢啟動流程

No4-No5:client端發送請求,server端回復ACK報文

No6-No7:server端開始以5ms的間隔寫入報文,可以看到受限cwnd,只能發出No6和No7兩個報文。其余寫入的報文只能暫時緩存在內核中。注意server端在發出No6和No7兩個數據包后,發現當前in_flight的報文個數以cwnd是一致的,因此更新is_cwnd_limited=1,表示當前是處於network-limited狀態。

No8-No10:server端正在進行慢啟動,收到一個ACK后cwnd=cwnd+1=3,此時可以額外發出兩個報文了。No10發出后更新max_packets_out=3。

No11:server端收到No11后,更新cwnd=cwnd+1=4,但是server端第一階段以5ms間隔僅僅寫入了4個數據包,然后休眠60s后在繼續寫入的,因此此時已經沒有額外的數據可以發送了,所以server端在收到No11后並沒有立即發出新的數據包。

No12:server端開始以60ms的間隔寫入數據,No12是第一個寫入的數據包,從這時候起,server端開始進入RFC2861所說的application-limited狀態。

No13-No15:這幾個報文是之前server端發出報文的ACK報文,注意收到No13報文后更新cwnd=cwnd+1=5,收到No14后更新cwnd=cwnd+1=6。收到No15后,reno擁塞控制算法發現當前處於慢啟動階段,而且cwnd=6>=2*max_packets_out=6,因此認為當前處於application-limited狀態,雖然收到了ACK報文,但是不再更新cwnd。

No16-No25:server端發出No16的時候會更新max_packets_out=1,后面收到No17這個ACK報文的時候,同樣會因為cwnd=6>=2*max_packets_out=2,reno擁塞控制算法並不更新cwnd。后面reno收到No19、No21、No23、No25對應的ACK報文時候同樣是因為這個原因而不能更新cwnd。但是如上所說No17、No19、No21、No23、No25這些報文都會更新偽代碼中T_prev這個變量,因此linux每次發送數據的時候If tcpnow - T_prev >= RTO這個判斷條件都很難滿足,因此也就不會進入application-limited下cwnd和ssthresh的更新流程了。因此在收到No25之后,cwnd=6,ssthresh=8。


No26:注意No26與No24之間的間隔大約為300ms。server端的應用層在進行write操作的時候,發現當前數據發送的時間間隔已經超過了RTO(從server端程序可以獲取當前RTO為252ms),因此更新ssthresh =  max(ssthresh, 3*cwnd/4)=8,cwnd =  max(win/2, MSS)=3,實際linux更新cwnd的時候還要cwnd大於等於路由表中配置的2。

接着server端以5ms為間隔連續寫入15個數據包

No27-No28:可以看到發出No28后,server端不能在額外發出新的數據,此時in_flight為150bytes,正好對應cwnd=3。

No29-No43:接着server端進入慢啟動流程,可以看到直到發出No43后,in_flight=400bytes,對應cwnd=8,此時ssthresh=8,因此隨后應該進入擁塞避免階段

No44-No49:進入擁塞避免階段,每收到一個ACK報文,cwnd_cnt=cwnd_cnt+1,直到收到No48后,cwnd_cnt=3

No50-No57:server端陸續收到其余的ACK報文,其中在收到No54報文的時候,cwnd_cnt增長到8,因此更新cwnd=cwnd+1=9,並重置cwnd_cnt=0。最終收到No57報文后,ssthresh=8,cwn=9,cwnd_cnt=3

2、構造application-limited下更新cwnd、ssthresh流程場景

在上一個示例中我們已經看到了在RFC2861表述的application-limited場景下,linux並不會進入更新cwnd和ssthresh的application-limited代碼流程,下面我們人為根據代碼構造一個這樣的場景。在應用層數據受限的情況下,我們前面介紹過linux會在數據真實發送時刻判斷是否進入更新cwnd和ssthresh的application-limited流程,而idle態的判斷則是在應用層write操作的時候進行的。因為linux會在收到確認新數據包的ACK報文的時候更新T_prev,因此如果要在數據實際發出的時候滿足tcpnow - T_prev >= RTO這個條件,數據應用層write時刻不滿足tcpnow - T_last >= RTO才行。讀者可能會想到通過前面介紹的nagle算法或者cork算法,使得應用層的write操作與tcp層真實的發送操作隔離。但是這兩個都行不通的,原因是Nagle算法是ACK觸發,收到ACK會更新T_prev,因此不會滿足tcpnow - T_prev >= RTO。而cork算法數據包的超時發送是persist timer定時器觸發的,這個定時器觸發的數據包發送流程不會經過linux中更新cwnd和ssthresh中的application-limited流程。

最終構造的場景如下,client端對於server端的每個數據包都會觸發ACK回復,server端的No26數據包是間隔5ms后收到的ACK確認報文,而server端的其余數據包都是在發出50ms后收到的ACK報文。可以看到No26與No28間隔時間大約為240ms,低於RTO時間,因此不會判斷進入tcpnow - T_last >= RTO的流程,而No27和No29之間間隔275ms,高於RTO時間,因此會進入更新cwnd和ssthresh的application-limited流程。

在No29之前,ssthresh=8,cwnd=8,cwnd_cnt=4,注意server端在收到No27報文的時候判斷並沒有處於network-limited階段,因此reno並不會更新cwnd_cnt。收到No29之后更新ssthresh =  max(ssthresh, 3*cwnd/4)=8,cwnd = (win + W_used)/2=(8+2)/2=5,cwnd_cnt不變。更新后ssthresh、cwnd、cwnd_cnt的值也可以從后面慢啟動和擁塞避免的流程看出來。注意的是在No35到No43慢啟動的過程中cwnd_cnt的值並沒有清空。在No44-No50擁塞避免階段,cwnd_cnt從4增長到8,收到No50后更新cwnd=cwnd+1=9,重置cwnd_cnt=0;




補充說明:

1、RFC7661 new CWV的patch可以參考 https://github.com/rsecchi/newcwv

2、https://riteproject.eu/resources/new-cwv/

3、is_cwnd_limited等變量的更新請參考tcp_cwnd_validate、tcp_cwnd_application_limited、tcp_is_cwnd_limited

4、idle的處理請參考tcp_slow_start_after_idle_check

5、第二版的TCPIP詳解中對於application-limit的解釋正好是錯誤的,application-limited是指受限應用層而沒有更多的數據可以發送,從協議給出的偽代碼示例中也可以看到這一點。







免責聲明!

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



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