歡迎去gitbook(https://legacy.gitbook.com/@rogerzhu/)看到完整版。
如果對和程序員有關的計算機網絡知識,和對計算機網絡方面的編程有興趣,雖然說現在這種“看不見”的東西真正能在實用中遇到的機會不多,但是我始終覺得無論計算機的語言,熱點方向怎么變化,作為一個程序員,很多基本的知識都應該有所了解。而當時在網上搜索資料的時候,這方面的資料真的是少的可憐,所以,我有幸前兩年接觸了這方面的知識,我覺得我應該把我知道的記錄下來,雖然寫的不一定很好,但是希望能給需要幫助的人多個參考。我的計划是用半年時間來寫完這一系列文章,這個標題也是我對太多速成文章的一種態度,好了,廢話不再多扯了,下面是其中的一節內容,更多內容可以去gitbook上找到。
在TCP中,超時重傳機制是和應答確認機制一樣組成TCP可靠傳輸的關鍵設計。而超時重傳機制中最最重要的就是超時計時器的時間選擇的了,很明顯,在工程上,在數據發送的過程中,如果用一個固定的值一直作為超時計時器的時長是非常不經濟也非常不准確的方法,所以這一篇就來說說TCP中的超時計時器的設計哲學。
太短不行,太長也不行
超時超時,首先你得定義什么是正常的時間,才能知道有沒有超過正常的時間。先假設一個非常理想的環境,這個環境理想到和以前很多物理題一樣,不考慮摩擦力。我們假設網絡很通暢且速率穩定,而且處理包的速度忽略不計,這樣一個包發送到對端的時間永遠都是一樣的,將這個時間記為t。那么很明顯,如果超過兩倍的t還沒有收到對端的回復,我們就可以肯定超時了。所以在這種情況下超時計時器只要設置的比兩倍t長就行了。只要過了這個時間,發送端就會重新發送這個包。
那么這個時間是不是越長越好呢?答案很明顯不是,因為太長會人為的減少通信的速率,對於通信這種有時候一點點速率的提高都讓人欣喜若狂了,如果你還人為的浪費時間那真是暴殄天物了。
那么如果這個時間設置的太短會怎樣呢?在這個理想情況下就是小於2倍的t,這會導致太多不必要的重傳。也許ACK正在路上,你卻錯誤的認為是丟失了,那么網絡中就會增加很多本來就不必要的包。
而且,要知道,現實的網絡環境是十分復雜多變的,有時候可能突然的抽風,有時候可能突然的又很順暢,所以說如果只用一個一直不變的時間作為重傳計時器的時長是完全不現實,不可用的。所以很多計算重傳時間的算法就被設計出來。
調的一手好參數
TCP把一個包從發送端發送出去到接收到這個包的回復這段時間稱之為RTT,學名round-trip time。如果在一個包發送出去以后,超過了RTT還沒有接受到回復確認,那么很明顯,這個包超時了。如果你還記得前面的關於的PING那一篇,里面就有一個time指示了來回時間,但是這個是ICMP的來回時間,和TCP的這個RTT是完全不一樣的概念。通過wireshark你可以看到每一次的RTT的值。

這個RTT的計算很簡單,只要把收到確認包的時間減去發送包的時間就得到了這個答案。
現在開始對於重傳計時的第一次思考,上面說了這樣一個來回就說明包是成功的接收了並且沒有發生任何異常,那么可不可以簡單的用這個值作為標准來作為判斷超時的依據呢?也就是如果超過了0.285s沒有收到ack就開始重傳,很明顯,不能。原因是這個RTT是過去完成時了,是上一次成功的時候的時間,和下一次網絡會不會突然抽風,還是會突然變得更通暢沒有太大的必然關系,最愚蠢的一種思維就是簡單的用過去代表未來。但是數學給我們提供了一種用已知大概去逼近未知的方法,那就是用概率統計的思維。所以最簡單的一個辦法是用過去的幾次平均值來作為這一次重傳計時器的時長,畢竟這是初中學過的理論。不過這個方法明顯太過於幼稚,缺乏靈活的控制,所以說,第一次設計的嘗試就出現了。
為了能夠用更加靈活的方法來估算出重傳時間,一個叫SRTT的概念被引進,SRTT學名是Smoothed RTT。估算重傳時間(以后稱之為RTO,Retransmission Timeout)的算法如下:
SRTT = (α * SRTT) + ( (1 -α) *RTT)
其中這個奇妙莫測的阿爾法取值在0.8到0.9之間,為什么這樣取,我也不知道,我至今也沒有找到原因。對於這第一個公式,具體實際中的做法是這樣的,首先采樣幾次RTT的值,然后在第一次迭代的時候SRTT的初始值為RTT,后面就是根據每次計算出來的SRTT來計算就行了。這個公式有個你應該比較熟悉的中文名字,叫做加權移動平均。
在計算出SRTT之后,就使用這個值來計算我們需要的RTO,其方法如下:
RTO = min[UBOUND, max[LBOUND,(β * SRTT)]]
這其中UBOUND是一個上限時間,比如1分鍾,LBOUND是一個下限時間,比如1秒鍾,β,哈哈,又是一個神奇的參數,取值在1.3到2.0之間,叫做延遲方差因子,到底取啥,為什么取這個值,我,還是不知道。
這個方法有什么問題呢?問題就在這個RTT的計算上,前面說過RTT的計算是接收到ACK的時間和包發送出去的時間的差值,在正常情況下還好,如果是在采樣的過程中發生了重傳,那么到減去的時間是第一次發送的時間還是重傳發送的時間呢?
如果是減去第一次發送的時間,那么很明顯,這個RTT計算大了。那你可能會說了,從直觀上說,用第二次發送的時間計算才是合理的。但是有一種情況,假設本來應該到達的ACK不是丟失了,只是延遲到達了,也就是說你剛重傳,這個迷路的ACK就到了,那么你用這個時間減去第二次發送的時間,明顯就小了。
這個時候兩個叫Phil Karn和Craig Partridge的人就針對這個問題提出了一個算法,其解決方案十分簡單,既然重傳情況這么復雜,那么在采用RTT的時候直接忽略重傳不就行了。你先收起你的吐槽說尼瑪這樣我要早出生幾年也能想出這個辦法啊,人家論文里還寫了很多其他的東西,這個只是其中之一,而且這個算法也有很大的問題,Karn針對這個問題還提出了一個可行的解決方案,至少在工程上有了個可行的路子。
這個問題是什么呢?假設在某個時間,網絡極度的抽風,突然由快變得很慢,導致所有的包都要重傳。這下好了,因為前面一直很通暢,所以必然RTO很小,那么你又說重傳的包不參與RTT的采樣,這下完了,RTO永遠不會更新,只會不斷的重傳,情況會越來越糟。而Karn針對這個提出了一個解決方案,只要重傳,那么RTO就翻倍,這樣就保證了在極端情況下不會導致越來越糟。
Karn的算法解決了初代算法的問題並且有了個可行的方案,但是RTO粗暴翻倍的做法感覺還是比較浪費。所以,在一年之后又有兩個人Jacobson 和 Karels 針對這種加權移動平均的算法對RTT波動handle能力不強的弊端做了修正。其原理是用最新采樣的RTT和平滑過的SRTT的差距來作為另一個影響因子。
SRTT = SRTT + α * (RTT - SRTT)
DevRTT = (1-β) * DevRTT + β *(|RTT - SRTT|)
RTO = μ * SRTT + δ * DevRTT
這三個公式就是現在TCP協議中真正運用的算法,關於這些參數,α是取0.125,β是0.25,μ 是1,δ是4,這就是linux中的取值,至於為什么,沒有人知道,但是在實際效果中,果真就很有效,在編程過程中,我們稱這種玄學叫做調的一手好參數。