案例:TCP Window Full 是導致異地拷貝速度低的原因嗎?也是在公有云服務的時候,有個客戶有這么一個需求,就是要把文件從北京機房拷貝到上海機房。但是他們發現傳輸速度比較慢,就做了抓包。在查看抓包文件的時候,發現 Wireshark 有很多 TCP Window Full 這樣的提示,不明白這些是否跟速度慢有關系,于是找我們來協助分析。
解讀 Expert Information 我們先要了解一下抓包文件的整體狀況。怎么看呢?當然是看 Expert Information 了:
它確實提醒我們,有 69 個 Warning 級別報文,它們的問題是 TCP Window specified by the receiver is now completely Full。在展開進一步排查之前,我們先對這個信息做一下解讀。
TCP Window:前面我介紹了 TCP 的三種窗口,分別是發送窗口、接收窗口、擁塞窗口。那么這里說的是哪個窗口呢?一般說到 TCP Window,如果沒有特別指明,就是指接收窗口。specified by the receiver:這也很明確,這個窗口是接收方的,其實就是佐證了這個窗口就是接收窗口。is now completely Full:窗口滿了,這又怎么理解呢?你還記得在上節課里學過的在途數據,也就是 Bytes in flight 嗎?當在途數據的大小等于接收窗口的大小時,這個窗口就是“滿了”。好了,這個信息解讀完畢,一句話說就是:發生了 69 次在途數據等于接收窗口的情況。
接下來我們看看 TCP Window Full 具體是個什么樣子。比如我們選中 224 號報文,主界面也自動定位到了這個報文:
我們可以看到,除了 224 報文,也確實有很多其他報文也報了 TCP Window Full 的警告信息。
解讀 TCP Window Full
TCP Window Full 這個信息非常直接明了,就是說“接收窗口滿了”。不過,你可別以為這個信息是 TCP 報文里的某個字段。其實,它只是 Wireshark 通過分析得出的信息。你有沒有注意到,TCP Window Full 前后是有方括號的。一般來說,Wireshark 自己分析得到的信息,都會用方括號括起來,而 TCP 報文本身的字段,是不會帶這種方括號的。我們來看一個截圖:
上面是 224 號報文的 TCP 詳情,里面有不少信息也帶上了方括號,比如其中的 [Bytes in flight: 112000],這也是解讀出來的,而且它跟 Window Full 關系很大。
前面提到過,在途數據(或者叫在途字節數,Bytes in flight)等于接收窗口大小的時候,Wireshark 就會解讀為 TCP Window Full 了。不過,如果你在上圖中找一下 Window size,會發現它是 19200,而不是 Bytes in flight 的 112000,這又是為什么呢?
這是因為,我們把發送方和接收方的接收窗口搞混啦。這里你需要搞清楚:如果說在途數據的發送方是 A,接收方是 B,那么這里 Window Full 的窗口,是 B 的接收窗口,而不是 A 的接收窗口。上圖是 A 的報文,自然沒有我們要找的 B 的接收窗口信息了,那怎么找到 B 的接收窗口呢?
因為這次通信是 SCP 文件傳輸,那么 A 就是客戶端,它的端口是 38979;B 就是服務端,它的端口是 22。我們的具體做法是:在抓包文件里,找到 B(也就是源端口為 22)的報文,而且應該是這個 TCP Window Full 報文之前的最近的一個,在這個報文里就有 B 的最近的接收窗口值。
單純用文字,可能未必容易理解,我給你畫了一張示意圖:
上圖中,我還是用 A 指代發送端,用 B 指代接收端。當 A 的在途數據跟 B 的接收窗口大小相等時,Wireshark 就會判斷出,這個接收窗口滿了,這意味著:A 無法再從自己的發送緩沖區把數據發送出來了。只有當 B 回復 ACK,確認了 n 字節的數據后,A 才有可能發送最多 n 字節的數據(如果緩沖區有足夠多的待發數據的話)。
讓我們回到 Wireshark 窗口,找到離這個 TCP Window Full 最近的,從源端口 22 發送來的報文。我們發現,它就是下圖這個 222 號報文:
可以看到,這個報文的接收窗口就是 112000,正好等于前面 224 號報文的 Bytes in flight 的 112000 字節。所以,我把前面的示意圖改進一下供你參考。這里面的信息比較多,建議你耐心多花幾分鐘時間來充分理解其中的機制:
整個過程是這樣的:
B 發送了報文 222 給 A,其中帶有 B 自己的接收窗口 112000 字節。由于這是一個純的確認報文,所以沒有 TCP 載荷,也沒有在途數據。
報文抵達 A 端,進入 A 的接收緩沖區。
A 從 222 號報文中得知,B 現在的接收窗口是 112000 字節,由于發送緩沖區有足夠多的待發的數據,A 選擇用滿這個接收窗口,也就是連續發送 112000 字節。
A 把這 112000 字節的數據發送出來,成為報文 224,其中還帶有 A 自己的接收窗口值 19200 字節,不過,由于這次主要是 A 向 B 傳送數據,所以 B 發給 A 的基本都是純確認報文,這些報文的載荷都是 0。極端情況下,即使 A 的接收窗口為 0,只要 B 回復的報文沒有載荷,它們也是可以持續通信的。
224 報文抵達 B 端,正好填滿 B 的接收窗口 112000 字節。Wireshark 分別從 222 報文中讀取到 B 的接收窗口值,從 224 報文中讀取到在途字節數,由于兩者相等,所以 Wiresahrk 提示 TCP Window Full。而這個信息是被 Wiresahrk 展示在 224 報文中的。
自己驗證 TCP Window Full
對于在途數據,既然 Wireshark 可以解讀出來,那只要理解了 TCP 的原理,我們同樣可以自己來計算,這不僅可以考查我們對 TCP 知識的掌握程度,同時對日常排查也有幫助。有這么多好處,你是不是躍躍欲試了呢?不過在開始之前,我們要先理解一個新的概念。
下個序列號
下個序列號,也就是 Next Sequence Number,縮寫是 NextSeq。它是指當前 TCP 段的結尾字節的位置,但不包含這個結尾字節本身。很顯然,下個序列號的值就是當前序列號加上當前 TCP 段的長度,也就是 NextSeq = Seq + Len。
這也不難理解,因為 TCP 字節流是連續的,那么既然 Seq + Len 是這個報文的數據截止點,自然也是下一個報文的起始點,你可以參考這個示意圖:
在 Wireshark 里,我們也可以找到 NextSeq 這個解讀值,比如下圖這樣:
明白了 NextSeq,我們來看如何手工驗證 TCP Window Full。比如,還是分析 224 號報文的這次 TCP Window Full,我們可以這么做,來驗證一下在途數據是否真的是 112000 字節。
首先,跟上面的步驟類似,我們要找到 222 號報文。在這個報文里,服務端(源端口 22)告訴客戶端:“我確認你發送來的 198854 字節的數據”。我們先把這個數字記為 X。
然后,我們查看 224 號報文里,客戶端發送的數據到了哪個位置:
我們可以在 224 號報文的 TCP 詳情頁,看到 Next sequence number: 310854,而這個數字,就是客戶端發送的數據的最新的位置。我們把這個數字記為 Y。當然,你也可以像前面說的那樣,把 Seq 和 Len 加起來,也就是 308054 + 2800,得到的自然也是 310854。
最后,我們做一個最簡單的減法:Y - X = 310854 - 198854 = 112000!這正是前面說的在途數據的大小。
恭喜你,你已經學會了如何手工計算在途數據的方法,這也意味著你對 TCP 的了解又更深入了一點。你可以這么來總結計算在途數據的方法:
Bytes_in_flight = latest_nextSeq - latest_ack_from_receiver
不過,你會不會覺得,雖然這個計算方法對理解窗口有幫助,但是既然 Wireshark 會給我們提示,那這種計算也主要是自我練習而已,應該不會真的用得上吧?
這還真不好說。因為,Wireshark 在不少場景下并不會給你提示。比如,在接收窗口接近滿但又不是完全滿的時候,哪怕是離窗口滿只差 1 個字節,Wireshark 也不會提示 TCP Window Full 了。但是,在途數據都已經逼近接收窗口的 99.9% 了,你還覺得這個肯定沒有問題,或者一定沒有隱患嗎?
要知道,這種臨界狀況也很可能跟問題根因有關。那么你掌握了這個方法,就可以把排查做得更徹底了。或者,如果你想預防性能瓶頸,那么提前找到這種窗口臨界滿的狀況,也是有益的。
到這里,我們可以回答開始時候的問題了:為什么上節課里,沒有看到 TCP Window Full 這種提示呢?我們看一下當時的報文狀況吧。
在 2239.067477,接收端的確認號為 7105632:
然后在 2239.209712,接收端的確認號為 7169872:
這兩個報文的時間跨度正好是 141ms 左右,也就是這次傳輸里面的往返時間。在這個往返時間里,接收端確認了多少數據呢?是 7169872-7105632=64240,也就是 64KB。這個就是 99.9% 逼近 TCP Window Full 了,但是因為還差小幾十個字節,所以 Wireshark 并沒有提示 TCP Window Full!
你可能還想追問:那為什么不把這剩余的 0.1% 的窗口“榨干”,非要留一點呢?我們看一下當時的接收窗口和在途數據的具體情況,就以上面選擇的 5864 報文附近為例:
接收端(源端口為 22)的接收窗口為 65728,發送端(源端口為 59159)的在途數據為 65700,兩者相差只有 28 字節。對于發送端來說,沒有必要為了這區區 28 字節再發送一個小報文了,等接收窗口空余出多一點的空間后再動身不遲。
如果你還沒看過我前面的《TCP SEGMENT》,可能會對上面這些信息感到疑惑,建議先去看完,再來看這一篇,效果更好。
TCP Window Full 對傳輸的影響
好了,現在我們已經對 TCP Window Full 做了充分的分析,而且也明白了:這就是接收端的接收窗口小于發送端的發送能力而出現的狀況。我們也很容易得出推論:瓶頸在接收端,TCP Window Full 也確實會影響傳輸速度。
春節剛過,你可能對高速公路上的狀況也感受深刻吧!很多路段出現了堵車,這就相當于 TCP Window Full,更多的車輛上不了高速了,只好堵在外面。如果高速公路的路更寬、車速更快,那么就相當于接收窗口變得更大,車輛就能進更多,也就相當于 Bytes in flight 更大了。這么說來,TCP 流量控制和高速車流控制這兩個領域也有不少共通之處,說不定雙方都互有借鑒呢。
回到客戶這次的案例。我們看看,這次的傳輸速度是多少呢?在上節課里,我介紹了在 Wireshark 里查看 TCP 傳輸速度的兩種方法。比如,我們現在用 I/O Graph 來看一下:
補充:如果你的 I/O Graph 顯示的不是這種圖,那需要像圖中這樣:選中 All Bytes 指標;Y 軸的單位選為 Bytes。
這個圖不能說不對,但柱子比較粗,看起來不是很精確。這是因為,它默認是以 1 秒為間隔而計算的速度。但是 TCP 傳輸中途,很可能每過幾毫秒都有所變化,所以,如果我們要看更加精細的圖,可以調整一下粒度,把 Interval 從 1sec 改為 100ms,看看會怎么樣:
這樣看起來精確了很多。
補充:如果你用 Wireshark 查看到的 I/O Graph 跟我這里的不同,那可以對比一下 Interval 和 SMA period 這兩個配置是否跟圖中的一致。
這里有個小的注意點:因為我們選擇的間隔是 100ms,所以 Y 軸的數字就是 Bytes/100ms,換算成 Bytes/s 的話,要把 Y 軸的數字再乘以 10。從圖上看,在一開始的 8 秒幾乎沒有數據傳輸發生,從第 8 秒開始速度上到了 400KB/s(就是圖上的 40K*10)左右,一直到結束都大致維持在 300~400KB/s 這個區間里。
繼續深挖窗口
一般來說,接收窗口、擁塞窗口、發送窗口,這些都不是一上來就是一個很大的值的,而是跟汽車起步階段類似,逐步跑起來的。那么這就產生了一個很有意思的話題:這些窗口之間都是怎么協調的呢?直觀上感覺,無論哪個更快了,另外兩個就要受影響。
我們很容易理解,假設起始值相同,如果接收窗口增長的速度小于擁塞窗口的增長速度,那么接收窗口就成了瓶頸;反過來說,擁塞窗口增速更小,那么它就成了瓶頸。
當接收窗口成為瓶頸的時候,很容易就出現這里的 TCP Window Full 的現象。不過,我們這么多討論都是基于文字,如果有更加直觀的方式,讓我們理解這個現象就更好了。這里,我們就可以再學一個 Wireshark 的小工具:TCP Stream Graphs 里面的 Window Scaling。
我們還是打開 Wireshark 的 Statistics 下拉菜單,找到 TCP Stream Graphs,在二級菜單中,選擇 Window Scaling:
這時候就能看到 Windows Scaling 的趨勢圖了:
我就直接給你把關鍵信息標注出來了。這里主要是兩個關鍵點。
數據流的方向要找對:比如這次傳輸是從客戶端向 SSH 服務端發送數據,所以要確認這是從一個高端口向 22 端口發送數據的流向。如果搞反了,那圖就變成了 SSH 服務端回復的 ACK 報文了,不是你要分析的傳輸速度了。定位 TCP Window Full:在這里,Receive Window 是“階梯”式的,每次變化后會保持在一個“平臺”一小段時間,那么這時候 Bytes Out(發送的數據,也就是 Bytes in flight)就有可能觸及這個“平臺”,每次真的碰上的時候,就是一次 TCP Window Full。我們可以看一個例子。圖中的藍線代表 Bytes Out,綠線代表 Receive Window。你可以像我這樣,在這幾個“平臺”區域,找到藍線和綠線的匯合點,然后在這些點上點擊鼠標左鍵,就能定位到 TCP Window Full 事件了。
上圖中,我用鼠標放大了一個“平臺”,然后選中了一個 Receive Window 和 Bytes Out 重合的點,它是 1200 號報文,主窗口也自動定位到了這個報文,果然它也是一次 TCP Window Full。
驗證傳輸公式
前面我們推導出了 TCP 傳輸的核心公式:速度 = 窗口 / 往返時間。既然當前案例里 TCP Window Full 的時候,Bytes in flight 跟接收窗口相等,那么在這個公式里的窗口,是否就是 Bytes in flight 呢?我們來驗證一下。
還是在 Wireshark 窗口里,我們要添加這么幾個自定義列,以便進行數據比對:
Acknowledgement Number:確認號 Next Sequence Number:下個序列號 Caculated Window Size:計算后的接收窗口 Bytes in flight:在途字節數 另外,因為我們要集中檢查發送端的 Bytes in flight,就需要把源端口 38979 的報文過濾出來,這樣就不會被另一個方向的報文給干擾了。
在這里,我們看到的 Bytes in flight 是 112000 字節左右。從右邊滾動條的位置來看,這是在傳輸過程的初期。讓我們滾動到中間和后期,看看這些在途字節數是多少:
中期這里的 Calculated Window Size 明顯增大了,到了 445312 字節。再看看后半程:
最后階段已經達到 863800 字節。綜合這三個階段來看,折中值差不多在 400KB 左右,我們把它除以 RTT 0.029 秒,得到的是 400KB/0.029s=13790KB/s。顯然,這個數值遠超過前面 I/O Graph 里看到的 300~400KB/s。這是怎么回事呢?難道我們的核心公式是錯的嗎?
不知道你有沒有考慮到這個問題:Bytes in flight 是指真的一直在網絡上兩頭不著嗎?一般來說,數據到了接收端,接收端就發送 ACK 確認這部分數據,然后 TCP Window 就往下降了。比如 ACK 300 字節,那么 TCP Window 就又空出來 300 字節,也就是發送端又可以新發送 300 字節了。
像圖上這種情況:
B 通知 A:“我的接收窗口是 1000”;A 向 B 發送了 1000 字節,此時 B 的接收窗口滿;B 向 A 確認了 300 字節的數據,自身的接收窗口也擴大為 300 字節;A 的在途字節數也從 1000 字節變成 700 字節,因為剛剛有 300 字節被 B 確認了。圖中的 t1 到 t4 表示時間點。t2 到 t4 就是一次往返的時間,在這個往返時間內,被傳輸的數據是 1000 字節嗎?不是。因為被確認的只有 300 字節,所以傳輸完的也只有這 300 字節,速度也就不是 1000/RTT,而是 300/RTT!我們可以把核心公式做一下改進,變成下面這個:
velocity = acked_data/RTT
我們再用改進后的公式來計算這次的速度。我們可以選擇傳輸中間偏后面一點的報文來做分析。比如下圖中,我們選擇 1337 號和 1357 號報文為起始和截止點,計算 NextSeq 的差值,還有時間的差值,然后兩個差值相除。
33600/0.094 = 357KB/s。是不是很接近 I/O Graph 的值了?看來這樣計算才是正確的!
那為什么在前面文章里我們就可以用 窗口 / 往返時間 來計算速度,而且數值也很準呢?而這種方法用到這里就完全不對呢?
這是因為前面文章的案例,在途數據一旦到了接收端,都被及時確認了。而當前這個案例里面并沒有這樣。也就是說,這次的案例,出現了“滯留”現象。
還是 1337 到 1357 號報文,我們去掉了過濾器 tcp.srcport eq 38979,這樣就展示了雙向報文。可以看到,服務端(B 端)在這段時間內,只確認了 22400 字節(1495254 - 1472854),而同樣時刻的在途數據,卻一直維持在一個比較高的數字,在 660KB 上下。所以,真正完成了傳輸的數據量,是前者 22400B,而不是“虛浮”的 660KB。
那你可能又要問了:既然已經確認了 22400 字節,為什么客戶端的在途字節數還是沒有變化呢?
這是因為,客戶端被確認了 22400 字節的數據,馬上又把這個尺寸的數據發送出去了,事實上就維持了這個在途字節數的尺寸。
我可以再做一個比喻幫助你理解這個現象。我們如果去銀行的一個窗口(這可不是 TCP 窗口)排隊辦業務,現在排隊人數為 10 人,相當于 Bytes in flight 為 10。每分鐘都有一個人能完成業務辦理,原以為隊列會減小為 9 人,結果每當有一個人出來,保安就喊:“下一個!”于是就立刻又補進來一個人,所以隊伍還是維持在 10 人這么長。
那么,窗口的業務員的辦理速度是多少呢?顯然不是 10 人 / 分鐘,而是 1 人 / 分鐘了。這樣是不是理解起來容易多了?而前面文章的情況,相當于這里的“每次就處理一個人”,所以處理速度就是 1 人 / 分鐘,也就可以用“速度 = 窗口 / 往返時間”來計算了。
審核編輯:湯梓紅
-
Window
+關注
關注
0文章
82瀏覽量
37305 -
TCP
+關注
關注
8文章
1378瀏覽量
79302 -
窗口
+關注
關注
0文章
66瀏覽量
10898 -
Full
+關注
關注
0文章
7瀏覽量
9757 -
文件傳輸
+關注
關注
0文章
37瀏覽量
8329
原文標題:解讀 TCP Window Full
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運維】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論