一、擁塞控制的相關算法
早期的TCP協議只有基於窗口的流控(flow control)機制而沒有擁塞控制機制,因而易導致網絡擁塞。1988年Jacobson針對TCP在網絡擁塞控制方面的不足,提出了“慢啟動(Slow Start)”和“擁塞避免(Congestion Avoidance)”算法。1990年Jacobson又做了兩個修正。在這二十來年的發展過程中,與擁塞控制相關的有四個比較重要的版本:TCP Tahoe、TCP Reno、TCP NewReno和TCP SACK。TCP Tahoe是早期的TCP版本,它包括了3個最基本的算法-“慢啟動”、“擁塞避免”和“快速重傳(Fast Retransmit)”,但是在Tahoe版本中對於超時重傳和快速重傳的處理相同,一旦發生重傳就會開始慢啟動過程。TCP Reno則在TCP Tahoe基礎上增加了“快速恢復(Fast Recovery)”算法,針對快速重傳作出特殊處理,避免了網絡擁塞不嚴重時采用“慢啟動”算法而造成過度減小發送窗口尺寸的現象。TCP NewReno對TCP Reno中的“快速恢復”算法進行了修正,它考慮了一個發送窗口內多個數據包丟失的情況。在Reno版中,發送端收到一個新的ack number后就退出“快速恢復” 階段,而在NewReno版中,只有當所有的數據包都被確認后才退出“快速恢復”階段。TCP SACK關注的也是一個窗口內多個數據包丟失的情況,它避免了之前版本的TCP重傳一個窗口內所有數據包的情況,包括那些已經被接收端正確接收的數據包, 而只是重傳那些被丟棄的數據包。傳統的TCP擁塞控制算法主要就由慢啟動、擁塞避免、快速重傳、快速恢復這4個基礎算法組成,這四個基礎算法在RFC5681規范中進行了描述。
后續我們將會分別對這些擁塞控制相關的算法做介紹,在介紹這些擁塞控制的相關算法之前我們先介紹一下擁塞控制中的數據包守恆原則和linux中擁塞控制的背景知識,以方便后面進行更進一步的介紹。
另外需要注意一下reno這個詞在不同語境下的不同含義,它可以指reno這個TCP版本,也可以指原始BSD版本中的reno擁塞控制算法,也可以指目前linux中實現的reno擁塞控制算法。linux中的reno擁塞控制算法可以看成原始BSD中的reno擁塞控制算法的一個演進變種。后面我們的wireshark示例也會以linux中的reno為例子。
二、數據包守恆原則
如果一個TCP連接處於一個穩定傳輸狀態,當發送端接收到一個Good ACK(Ack Number 大於當前發送端接收到最大的Ack Number的ACK反饋叫做Good ACK)的時候表示接收端已經收到了一個或者多個數據包,即這些數據包已經離開網絡到達對端,因此Good ACK可以看成是指示發送端可以發送新的數據包到網絡中了。這樣在穩定傳輸狀態下的TCP在發送端與接收端之間的網絡中數據包的個數是穩定的(SMSS大小數據包),這種現象就叫做“數據包守恆”的原則(conservation of packets principle)。如下圖所示,在發送端與接收端的鏈路中(即Pb數據包所在的管道)數據包的個數應該是穩定的(SMSS大小數據包)。下圖中深色部分表示數據包,在發送端短而粗的數據包在網絡中傳輸的時候會被伸展成細而長的數據包,這種設施叫做漏斗(funnel)。通過Good Ack指示發送端發送新數據的這種關系叫做self-clocking,因為是由Ack觸發,所以這種關系也可以叫做Ack clocking。

后續要介紹的慢啟動和擁塞避免算法就是基於上面介紹的conservation of packets principle和Ack clocking建立的。
三、linux中擁塞控制的背景知識
1、擁塞控制狀態機
在Linux的TCP實現中,使用了狀態機來處理擁塞控制,這個狀態機器共有五種狀態,分別如下
Open:這是TCP的正常狀態沒有dup ACK之類的可疑事件,新收到的數據包處理上一般使用快速路徑處理。TCP連接初始建立的時候就處於Open狀態。當TCP收到good ACK的時候,就會依據慢啟動或者擁塞避免算法調整cwnd。good ACK是指確認了新數據的ACK報文。
Disorder:這個狀態是一個和Open狀態很類似的狀態,在收到SACK或者dup ACK的時候會進入到Disorder狀態,他會把一些處理移動到慢速路徑。在這個狀態下cwnd並會如open態一樣進行調整,但是因為這個狀態下大部分收到的是ack number相同的dup ACK,因此很多場景下cwnd不會變,我們后面會給出一個Disorder狀態下cwnd調整的示例。一般每個新收到的ack報文都會觸發新數據的傳輸,此時TCP遵守包守恆原則。
CWR(Congestion Window Reduced):擁塞窗口因為一些擁塞指示(congestion notification)事件而變小,這些擁塞指示事件可以是ECN、local device congestion。當Linux收到擁塞指示事件的時候並不會一次把cwnd減小到指定值,而是每隔一個ACK報文把cwnd減1,直到窗口大小變為原來的一半。CWR狀態可以被下面的Recovery狀態或者Loss狀態打斷。
Recovery:當TCP收到足夠數量的dup ACK的時候就會觸發快速重傳,此時就會進入Recovery狀態。類似CWR狀態,在Recovery狀態下cwnd每隔一個ACK報文就會減小1,一般當cwnd降低到原來擁塞窗口的一半的時候就停止減小,但是Recovery狀態下其實cwnd也可以降低到ssthresh以下,后面我們會有示例介紹。在Recovery狀態下cwnd並不會增加。當新收到的ack number大於等於進入Recovery狀態時的SND.UNA(還記得之前文章中我們稱呼這個點為recovery point吧,另外本句大於等於的描述實際上不太准確,后面會給出多個示例)后,TCP發送端進入Open狀態。RTO超時的Loss狀態也可以打斷Recovery狀態。
Loss:當發生RTO超時、SACK reneging、PMTUD收到ICMP指示更小的MTU並需要進行重傳的時候會進入這個狀態。我們之前通過wireshark示例看到過SACK reneging的時候實際上最終觸發的也是RTO超時。當RTO超時后,所有發出去的數據包都會被標記為已丟失,cwnd設置為1,發送端開始使用慢啟動算法增加cwnd。Loss狀態並不能直接被Disorder、CWR、Recovery狀態打斷。只有收到大於等於recovery point的ack number之后,Loss狀態才能切換到Open狀態。
我們不會詳細的介紹這些狀態機的狀態切換,但是我們會在后面的一些典型場景下結合wireshark示例來介紹這些狀態中cwnd的變化和狀態之間的切換。
2、in_flight的計算
協議的中對於擁塞控制一般都是基於字節的描述,即cwnd表示允許TCP發送多少bytes的數據,而Linux中的擁塞控制是基於數據包做的實現,cwnd表示允許TCP發送多少個數據包。因此對於in_flight,Linux也是以數據包維護的,注意我這里說的in_flight並不是RFC5681中的FlightSize,而是RFC6675中的Pipe。RFC5681是擁塞控制的一個基本協議,而RFC6675則是SACK增強版本的擁塞控制,自然linux需要采用RFC6675中的方案。Pipe表示TCP發送端對於還在網絡中傳輸的數據量的估算。in_flight表示目前還在網絡中傳輸的數據包的個數。linux中以in_flight狀態變量表示in_flight,進行擁塞控制的時候,如果in_flight>=cwnd的,就表示擁塞窗口不允許在額外發送數據包了。
如果對於下面的in_flight計算過程還是不懂那么請參考RFC6675中的SetPipe ()過程。另外如果要進一步理解linux擁塞控制的代碼實現,強烈建議一定要讀懂RFC6675。
in_flight = packets_out - left_out + retrans_out
packets_out:表示在SND.NXT和SND.UNA之間一共發送出去了多少個數據包
retrans_out:是重傳的TCP報文的個數
left_out:表示已經離開網絡但是沒有被ack number確認接收的,它包含兩部分
left_out = sacked_out + lost_out
sacked_out:這個表示亂序到達接收端的數據包,因此沒有被ack number確認,當使能SACK的時候,該變量就是SACK反饋確認的數據包的個數,當沒有使能SACK的時候,該變量為接收到的dup ACK的個數。我們之前介紹快速重傳的時候說過,接收端收到亂序數據包的時候會立即反饋ACK,因此可以使用dup ACK來估計亂序到達接收端的數據包的總數
lost_out:被網絡丟失的數據包的個數,這個變量在快速重傳場景下是啟發式估計的,正是不同的估計方式區分了不同的算法,目前linux有兩種方法來估計lost_out
FACK:lost_out = fackets_out - sacked_out 從而 left_out = fackets_out,即在FACK下,所有在ack number和 most forward SACK之間的數據包,如果沒有被SACK確認接收那么就被認定為在網絡傳輸中發生丟失。FACK的相關信息請參考前面重傳部分文章的介紹
NewReno:當擁塞控制進入Recovery狀態的時候,假設一個數據包發生了丟失,當收到一個partial ACK的時候,則假設又有額外一個數據包發生了丟失。
而在RTO超時場景下,TCP進入Loss狀態,linux則會認為所有發出去的數據包都丟失了,因此lost_out = packets_out,in_flight = retrans_out。
Linux進行擁塞控制判斷是否允許發送TCP報文的時候就是根據in_flight是否小於cwnd來判斷的。當in_flight<cwnd的時候說明擁塞控制允許發送這個報文,否則擁塞控制不允許發送這個報文。而流量控制中對於awnd的判斷,則是根據當前報文最后一個byte的數據對應的系列號是否超過對端的接收窗口來判斷的。
3、linux中擁塞控制的配置
這里僅介紹如何設置及選擇TCP連接的擁塞控制算法,后面文章涉及到其他的擁塞控制的參數配置的時候在具體介紹。在我使用的ubuntu16.04的linux4.4內核中,內核TCP初始化的時候,會默認注冊一個reno擁塞控制,其余的擁塞控制算法在編譯內核的時候則可以通過編譯選項自行配置編譯為內核模塊或內建到內核中或者直接移除,ubuntu16.04的默認編譯選項如下所示,可以看到除了內核中默認的reno擁塞控制算法外,ubuntu16.04還會把cubic擁塞控制算法內建到內核中,並且把cubic擁塞控制算法設置為默認的擁塞控制算法了。而其他的擁塞控制算法則是以模塊的形式編譯的,默認並不會加載到內核中。不同的擁塞控制算法一般都會有慢啟動、擁塞避免等過程,但是在具體調整cwnd和ssthresh的方法可能會有差異。如前面文章所說本系列中將會着重介紹基於丟包的擁塞檢測的擁塞控制算法。后面的大部分wireshark擁塞控制示例都會使用reno算法演示。
******@Inspiron:/boot$ grep -i tcp_cong config-4.4.13+
CONFIG_TCP_CONG_ADVANCED=y
CONFIG_TCP_CONG_BIC=m
CONFIG_TCP_CONG_CUBIC=y
CONFIG_TCP_CONG_WESTWOOD=m
CONFIG_TCP_CONG_HTCP=m
CONFIG_TCP_CONG_HSTCP=m
CONFIG_TCP_CONG_HYBLA=m
CONFIG_TCP_CONG_VEGAS=m
CONFIG_TCP_CONG_SCALABLE=m
CONFIG_TCP_CONG_LP=m
CONFIG_TCP_CONG_VENO=m
CONFIG_TCP_CONG_YEAH=m
CONFIG_TCP_CONG_ILLINOIS=m
CONFIG_TCP_CONG_DCTCP=m
CONFIG_TCP_CONG_CDG=m
CONFIG_DEFAULT_TCP_CONG="cubic"
在/proc/sys/net/ipv4目錄下與擁塞控制算法的選擇直接相關的有三個
******@Inspiron:/boot$ more /proc/sys/net/ipv4/*cong*
::::::::::::::
/proc/sys/net/ipv4/tcp_allowed_congestion_control
::::::::::::::
cubic reno
::::::::::::::
/proc/sys/net/ipv4/tcp_available_congestion_control
::::::::::::::
cubic reno
::::::::::::::
/proc/sys/net/ipv4/tcp_congestion_control
::::::::::::::
cubic
其中tcp_available_congestion_control表示當前可用的擁塞控制算法,可以看到有一個內核默認包含的reno擁塞控制算法外,還有一個編譯內核時候內建到內核中的cubic擁塞控制算法。tcp_allowed_congestion_control參數設置了對於非特權進程的允許使用的擁塞控制算法,tcp_allowed_congestion_control參數是tcp_available_congestion_control的一個子集。tcp_congestion_control 參數設置了對於新建tcp連接的默認擁塞控制算法,也可以使用socket選項TCP_CONGESTION來對每個TCP連接獨立設置擁塞控制算法。
對於編譯為模塊的TCP擁塞控制算法,可以通過下面的方式加載
******@Inspiron:/boot$ sudo insmod /lib/modules/4.4.13+/kernel/net/ipv4/tcp_bic.ko
[sudo] lybxin 的密碼:
******@Inspiron:/boot$ more /proc/sys/net/ipv4/*cong*
::::::::::::::
/proc/sys/net/ipv4/tcp_allowed_congestion_control
::::::::::::::
cubic reno
::::::::::::::
/proc/sys/net/ipv4/tcp_available_congestion_control
::::::::::::::
cubic reno bic
::::::::::::::
/proc/sys/net/ipv4/tcp_congestion_control
::::::::::::::
cubic
******@Inspiron:/boot$ sudo rmmod tcp_bic
******@Inspiron:/boot$ more /proc/sys/net/ipv4/*cong*
::::::::::::::
/proc/sys/net/ipv4/tcp_allowed_congestion_control
::::::::::::::
cubic reno
::::::::::::::
/proc/sys/net/ipv4/tcp_available_congestion_control
::::::::::::::
cubic reno
::::::::::::::
/proc/sys/net/ipv4/tcp_congestion_control
::::::::::::::
cubic
另外還可以通過路由表來設置針對某一destination的擁塞控制算法,如下所示將所有與127.0.0.2的tcp連接的擁塞控制算法設置為reno,通過路由表設置TCP相關的參數詳見后面destination metric相關的文章。
******@Inspiron:~$ sudo ip route add local 127.0.0.2 dev lo congctl reno
******@Inspiron:~$ ip route show table all | grep 127.0.0.2
local 127.0.0.2 dev lo table local scope host congctl reno
補充說明:
1、linux中FlightSize的計算請參考tcp_packets_in_flight
2、https://www.cs.helsinki.fi/research/iwtcp/papers/linuxtcp.pdf
3、https://people.cs.clemson.edu/~westall/853/linuxtcp.pdf
4、https://wiki.aalto.fi/display/OsProtocols/TCP+Congestion+Control
5、TCP/IP Architecture, Design and Implementation in Linux
6、關於TCP各種擁塞控制版本的介紹,建議閱讀一下http://download.csdn.net/detail/xiaoran815/5509267
