Debug 網(wǎng)絡(luò)質(zhì)量的時(shí)候,我們一般會(huì)關(guān)注兩個(gè)因素:延遲和吞吐量(帶寬)。延遲比較好驗(yàn)證,Ping 一下或者 mtr[1] 一下就能看出來(lái)。這篇文章分享一個(gè) debug 吞吐量的辦法。
看重吞吐量的場(chǎng)景一般是所謂的長(zhǎng)肥管道(Long Fat Networks, LFN, rfc7323[2]). 比如下載大文件。吞吐量沒(méi)有達(dá)到網(wǎng)絡(luò)的上限,主要可能受 3 個(gè)方面的影響:發(fā)送端出現(xiàn)了瓶頸、接收端出現(xiàn)了瓶頸、中間的網(wǎng)絡(luò)層出現(xiàn)了瓶頸。
發(fā)送端出現(xiàn)瓶頸一般的情況是 buffer 不夠大,因?yàn)榘l(fā)送的過(guò)程是,應(yīng)用調(diào)用 syscall,將要發(fā)送的數(shù)據(jù)放到 buffer 里面,然后由系統(tǒng)負(fù)責(zé)發(fā)送出去。如果 buffer 滿(mǎn)了,那么應(yīng)用會(huì)阻塞住(如果使用 block 的 API 的話(huà)),直到 buffer 可用了再繼續(xù) write,生產(chǎn)者和消費(fèi)者模式。
發(fā)送端出現(xiàn)瓶頸一般都比較好排查,甚至通過(guò)應(yīng)用的日志看何時(shí)阻塞住了即可。大部分情況都是第 2,3 種情況,比較難以排查。這種情況發(fā)生在,發(fā)送端的應(yīng)用已經(jīng)將內(nèi)容寫(xiě)入到了系統(tǒng)的 buffer 中,但是系統(tǒng)并沒(méi)有很快的發(fā)送出去。
TCP 為了優(yōu)化傳輸效率(注意這里的傳輸效率,并不是單純某一個(gè) TCP 連接的傳輸效率,而是整體網(wǎng)絡(luò)的效率),會(huì)保護(hù)接收端,發(fā)送的數(shù)據(jù)不會(huì)超過(guò)接收端的 buffer 大小 (Flow control)。數(shù)據(jù)發(fā)送到接受端,也是和上面介紹的過(guò)程類(lèi)似,kernel 先負(fù)責(zé)收好包放到 buffer 中,然后上層應(yīng)用程序處理這個(gè) buffer 中的內(nèi)容,如果接收端的 buffer 過(guò)小,那么很容易出現(xiàn)瓶頸,即應(yīng)用程序還沒(méi)來(lái)得及處理就被填滿(mǎn)了。那么如果數(shù)據(jù)繼續(xù)發(fā)過(guò)來(lái),buffer 存不下,接收端只能丟棄。
保護(hù)網(wǎng)絡(luò),發(fā)送的數(shù)據(jù)不會(huì) overwhelming 網(wǎng)絡(luò) (Congestion Control, 擁塞控制), 如果中間的網(wǎng)絡(luò)出現(xiàn)瓶頸,會(huì)導(dǎo)致長(zhǎng)肥管道的吞吐不理想;
對(duì)于接收端的保護(hù),在兩邊連接建立的時(shí)候,會(huì)協(xié)商好接收端的 buffer 大小 (receiver window size, rwnd), 并且在后續(xù)的發(fā)送中,接收端也會(huì)在每一個(gè) ack 回包中報(bào)告自己剩余和接受的 window 大小。這樣,發(fā)送端在發(fā)送的時(shí)候會(huì)保證不會(huì)發(fā)送超過(guò)接收端 buffer 大小的數(shù)據(jù)。(意思是,發(fā)送端需要負(fù)責(zé),receiver 沒(méi)有 ack 的總數(shù),不會(huì)超過(guò) receiver 的 buffer.)
對(duì)于網(wǎng)絡(luò)的保護(hù),原理也是維護(hù)一個(gè) Window,叫做 Congestion window,擁塞窗口,cwnd, 這個(gè)窗口就是當(dāng)前網(wǎng)絡(luò)的限制,發(fā)送端不會(huì)發(fā)送超過(guò)這個(gè)窗口的容量(沒(méi)有 ack 的總數(shù)不會(huì)超過(guò) cwnd)。
怎么找到這個(gè) cwnd 的值呢?
這個(gè)就是關(guān)鍵了,默認(rèn)的算法是 cubic, 也有其他算法可以使用,比如 Google 的 BBR
主要的邏輯是,慢啟動(dòng)(Slow start), 發(fā)送數(shù)據(jù)來(lái)測(cè)試,如果能正確收到 receiver 那邊的 ack,說(shuō)明當(dāng)前網(wǎng)絡(luò)能容納這個(gè)吞吐,將 cwnd x 2,然后繼續(xù)測(cè)試。直到下面一種情況發(fā)生:
發(fā)送的包沒(méi)有收到 ACK
cwnd 已經(jīng)等于 rwnd 了
第 2 點(diǎn)很好理解,說(shuō)明網(wǎng)絡(luò)吞吐并不是一個(gè)瓶頸,瓶頸是在接收端的 buffer 不夠大。cwnd 不能超過(guò) rwnd,不然會(huì) overload 接收端。
對(duì)于第 1 點(diǎn),本質(zhì)上,發(fā)送端是用丟包來(lái)檢測(cè)網(wǎng)絡(luò)狀況的,如果沒(méi)有發(fā)生丟包,表示一切正常,如果發(fā)生丟包,說(shuō)明網(wǎng)絡(luò)處理不了這個(gè)發(fā)送速度,這時(shí)候發(fā)送端會(huì)直接將 cwnd 減半。
但實(shí)際造成第 1 點(diǎn)的情況并不一定是網(wǎng)絡(luò)吞吐瓶頸,而可能是以下幾種情況:
網(wǎng)絡(luò)達(dá)到了瓶頸
網(wǎng)絡(luò)質(zhì)量問(wèn)題丟包
中間網(wǎng)絡(luò)設(shè)備延遲了包的送達(dá),導(dǎo)致發(fā)送端沒(méi)有在預(yù)期時(shí)間內(nèi)收到 ACK
2 和 3 原因都會(huì)造成 cwnd 下降,無(wú)法充分利用網(wǎng)絡(luò)吞吐。
以上就是基本的原理,下面介紹如何定位這種問(wèn)題。
rwnd 查看方式
這個(gè) window size 直接就在 TCP header 里面,抓下來(lái)就能看這個(gè)字段。
但是真正的 window size 需要乘以 factor, factor 是在 TCP 握手節(jié)點(diǎn)通過(guò) TCP Options 協(xié)商的。所以如果分析一條 TCP 連接的 window size,必須抓到握手階段的包,不然就不可以知道協(xié)商的 factor 是多少。
cwnd 查看方式
Congestion control 是發(fā)送端通過(guò)算法得到的一個(gè)動(dòng)態(tài)變量,會(huì)試試調(diào)整,并不會(huì)體現(xiàn)在協(xié)議的傳輸數(shù)據(jù)中。所以要看這個(gè),必須在發(fā)送端的機(jī)器上看。
在 Linux 中可以使用 ss -i 選項(xiàng)將 TCP 連接的參數(shù)都打印出來(lái)。
這里展示的單位是 TCP MSS.即實(shí)際大小是 1460bytes * 10.
Wireshark 分析
Wireshark 提供了非常使用的統(tǒng)計(jì)功能,可以讓你一眼就能看出當(dāng)前的瓶頸是發(fā)生在了哪里。但是第一次打開(kāi)這個(gè)圖我不會(huì)看,一臉懵逼,也沒(méi)查到資料要怎么看。好在我同事會(huì),他把我教會(huì)了,我在這里記錄一下,把你也教會(huì)。
首先,打開(kāi)的方式如下:
然后你會(huì)看到如下的圖。
首先需要明確,tcptrace 的圖表示的是單方向的數(shù)據(jù)發(fā)送,因?yàn)?tcp 是雙工協(xié)議,兩邊都能發(fā)送數(shù)據(jù)。其中最上面寫(xiě)了你當(dāng)前在看的圖數(shù)據(jù)是從 10.0.0.1 發(fā)送到 192.168.0.1 的,然后按右下角的按鈕可以切換看的方向。
X 軸表示的是時(shí)間,很好理解。
然后理解一下 Y 軸表示的 Sequence Number, 就是 TCP 包中的 Sequence Number,這個(gè)很關(guān)鍵。圖中所有的數(shù)據(jù),都是以 Sequence Number 為準(zhǔn)的。
所以,你如果看到如上圖所示,那么說(shuō)明你看反了,因?yàn)閿?shù)據(jù)的 Sequence Number 并沒(méi)有增加過(guò),說(shuō)明幾乎沒(méi)有發(fā)送過(guò)數(shù)據(jù),需要點(diǎn)擊 Switch Direction。
這就對(duì)了,可以看到我們傳輸?shù)?Sequence Number 在隨著時(shí)間增加而增加。
這里面有 3 條線(xiàn),含義如下:
除此之外,另外還有兩種線(xiàn):
需要始終記住的是 Y 軸是 Sequence Number,紅色的線(xiàn)表示 SACK 的線(xiàn)表示這一段 Sequence Number 我已經(jīng)收到了,然后配合黃色線(xiàn)表示 ACK 過(guò)的 Sequence Number,那么發(fā)送端就會(huì)知道,在中間這段空擋,包丟了,紅色線(xiàn)和黃色線(xiàn)縱向的空白,是沒(méi)有被 ACK 的包。所以,需要重新傳輸。而藍(lán)色的線(xiàn)就是表示又重新傳輸了一遍。
學(xué)會(huì)了看這些圖,我們可以認(rèn)識(shí)幾種常見(jiàn)的 pattern:
丟包
很多紅色 SACK,說(shuō)明接收端那邊重復(fù)在說(shuō):中間有一個(gè)包我沒(méi)有收到,中間有一個(gè)包我沒(méi)有收到。
吞吐受到接收 window size 限制
從這個(gè)圖可以看出,黃色的線(xiàn)(接收端一 ACK)一上升,藍(lán)色就跟著上升(發(fā)送端就開(kāi)始發(fā)),直到填滿(mǎn)綠色的線(xiàn)(window size)。說(shuō)明網(wǎng)絡(luò)并不是瓶頸,可以調(diào)大接收端的 buffer size.
吞吐受到網(wǎng)絡(luò)質(zhì)量限制
從這張圖中可以看出,接收端的 window size 遠(yuǎn)遠(yuǎn)不是瓶頸,還有很多空閑。
放大可以看出,中間有很多丟包和重傳,并且每次只發(fā)送一點(diǎn)點(diǎn)數(shù)據(jù),這說(shuō)明很有可能是 cwnd 太小了,受到了擁塞控制算法的限制。