很多人常常對TCP優化有一種霧裡看花的感覺,實際上只要理解了TCP的執行方式就能掀開它的神祕面紗。Ilya Grigorik 在「High Performance Browser Networking」中做了很多細緻的描述,讓人讀起來醍醐灌頂,我大概總結了一下,以期更加通俗易懂。
流量控制
傳輸資料的時候,如果傳送方傳輸的資料量超過了接收方的處理能力,那麼接收方會出現丟包。為了避免出現此類問題,流量控制要求資料傳輸雙方在每次互動時宣告各自的接收視窗「rwnd」大小,用來表示自己最大能儲存多少資料,這主要是針對接收方而言的,通俗點兒說就是讓傳送方知道接收方能吃幾碗飯,如果視窗衰減到零,那麼就說明吃飽了,必須消化消化,如果硬撐的話說不定會大小便失禁,那就是丟包了。
接收方和傳送方的稱呼是相對的,如果站在使用者的角度看:當瀏覽網頁時,資料以下行為主,此時客戶端是接收方,服務端是傳送方;當上傳檔案時,資料以上行為主,此時客戶端是傳送方,服務端是接收方。
慢啟動
雖然流量控制可以避免傳送方過載接收方,但是卻無法避免過載網路,這是因為接收視窗「rwnd」只反映了伺服器個體的情況,卻無法反映網路整體的情況。
為了避免過載網路的問題,慢啟動引入了擁塞視窗「cwnd」的概念,用來表示傳送方在得到接收方確認前,最大允許傳輸的未經確認的資料。「cwnd」同「rwnd」相比不同的是:它只是傳送方的一個內部引數,無需通知給接收方,其初始值往往比較小,然後隨著資料包被接收方確認,視窗成倍擴大,有點類似於拳擊比賽,開始時不瞭解敵情,往往是次拳試探,慢慢心裡有底了,開始逐漸加大重拳進攻的力度。
在慢啟動的過程中,隨著「cwnd」的增加,可能會出現網路過載,其外在表現就是丟包,一旦出現此類問題,「cwnd」的大小會迅速衰減,以便網路能夠緩過來。
說明:網路中實際傳輸的未經確認的資料大小取決於「rwnd」和「cwnd」中的小值。
擁塞避免
從慢啟動的介紹中,我們能看到,傳送方通過對「cwnd」大小的控制,能夠避免網路過載,在此過程中,丟包與其說是一個網路問題,倒不如說是一種反饋機制,通過它我們可以感知到發生了網路擁塞,進而調整資料傳輸策略,實際上,這裡還有一個慢啟動閾值「ssthresh」的概念,如果「cwnd」小於「ssthresh」,那麼表示在慢啟動階段;如果「cwnd」大於「ssthresh」,那麼表示在擁塞避免階段,此時「cwnd」不再像慢啟動階段那樣呈指數級整整,而是趨向於線性增長,以期避免網路擁塞,此階段有多種演算法實現,通常保持預設即可,這裡就不一一說明了,有興趣的讀者可以自行查閱。
…
如何調整「rwnd」到一個合理值
有很多人都遇到過網路傳輸速度過慢的問題,比如說明明是百兆網路,其最大傳輸資料的理論值怎麼著也得有個十兆,但是實際情況卻相距甚遠,可能只有一兆。此類問題如果剔除奸商因素,多半是由於接收視窗「rwnd」設定不合理造成的。
實際上接收視窗「rwnd」的合理值取決於BDP的大小,也就是頻寬和延遲的乘積。假設頻寬是 100Mbps,延遲是 100ms,那麼計算過程如下:
1 |
BDP = 100Mbps * 100ms = (100 / 8) * (100 / 1000) = 1.25MB |
此問題下如果想最大限度提升吞度量,接收視窗「rwnd」的大小不應小於 1.25MB。說點引申的內容:TCP使用16位來記錄視窗大小,也就是說最大值是64KB,如果超過它,就需要使用tcp_window_scaling機制。參考:TCP Windows and Window Scaling。
Linux中通過配置核心引數裡接收緩衝的大小,進而可以控制接收視窗的大小:
1 2 |
shell> sysctl -a | grep mem net.ipv4.tcp_rmem = |
如果我們出於傳輸效能的考慮,設定了一個足夠大的緩衝,那麼當大量請求同時到達時,記憶體會不會爆掉?通常不會,因為Linux本身有一個緩衝大小自動調優的機制,視窗的實際大小會自動在最小值和最大值之間浮動,以期找到效能和資源的平衡點。
通過如下方式可以確認緩衝大小自動調優機制的狀態(0:關閉、1:開啟):
1 |
shell> sysctl -a | grep tcp_moderate_rcvbuf |
如果緩衝大小自動調優機制是關閉狀態,那麼就把緩衝的預設值設定為BDP;如果緩衝大小自動調優機制是開啟狀態,那麼就把緩衝的最大值設定為BDP。
實際上這裡還有一個細節問題是:緩衝裡除了儲存著傳輸的資料本身,還要預留一部分空間用來儲存TCP連線本身相關的資訊,換句話說,並不是所有空間都會被用來儲存資料,相應額外開銷的具體計算方法如下:
Buffer / 2^tcp_adv_win_scale
依照Linux核心版本的不同,net.ipv4.tcp_adv_win_scale 的值可能是 1 或者 2,如果為 1 的話,則表示二分之一的緩衝被用來做額外開銷,如果為 2 的話,則表示四分之一的緩衝被用來做額外開銷。按照這個邏輯,緩衝最終的合理值的具體計算方法如下:
BDP / (1 – 1 / 2^tcp_adv_win_scale)
此外,提醒一下延遲的測試方法,BDP中的延遲指的就是RTT,通常使用ping命令很容易就能得到它,但是如果 ICMP 被遮蔽,ping也就沒用了,此時可以試試 synack。
如何調整「cwnd」到一個合理值
一般來說「cwnd」的初始值取決於MSS的大小,計算方法如下:
min(4 * MSS, max(2 * MSS, 4380))
乙太網標準的MSS大小通常是1460,所以「cwnd」的初始值是3MSS。
當我們瀏覽視訊或者下載軟體的時候,「cwnd」初始值的影響並不明顯,這是因為傳輸的資料量比較大,時間比較長,相比之下,即便慢啟動階段「cwnd」初始值比較小,也會在相對很短的時間內加速到滿視窗,基本上可以忽略不計。
不過當我們瀏覽網頁的時候,情況就不一樣了,這是因為傳輸的資料量比較小,時間比較短,相比之下,如果慢啟動階段「cwnd」初始值比較小,那麼很可能還沒來得及加速到滿視窗,通訊就結束了。這就好比博爾特參加百米比賽,如果起跑慢的話,即便他的加速很快,也可能拿不到好成績,因為還沒等他完全跑起來,終點線已經到了。
舉例:假設網頁20KB,MSS大小1460B,如此說來整個網頁就是15MSS。
先讓我們看一下「cwnd」初始值比較小(等於4MSS)的時候會發生什麼:
再看一下「cwnd」初始值比較大(大於15MSS)的時候又會如何:
明顯可見,除去TCP握手和服務端處理,原本需要三次RTT才能完成的資料傳輸,當我們加大「cwnd」初始值之後,僅用了一次RTT就完成了,效率提升非常大。
推薦:大拿 mnot 寫了一個名叫 htracr 的工具,可以用來測試相關的影響。
既然加大「cwnd」初始值這麼好,那麼到底應該設定多大為好呢?Google在這方面做了大量的研究,權衡了效率和穩定性之後,最終給出的建議是10MSS。如果你的Linux版本不太舊的話,那麼可以通過如下方法來調整「cwnd」初始值:
1 2 3 |
shell> ip route | while read p; do ip route change $p initcwnd 10; done |
需要提醒的是片面的提升傳送端「cwnd」的大小並不一定有效,這是因為前面我們說過網路中實際傳輸的未經確認的資料大小取決於「rwnd」和「cwnd」中的小值,所以一旦接收方的「rwnd」比較小的話,會阻礙「cwnd」的發揮。
推薦:相關詳細的描述資訊請參考:Tuning initcwnd for optimum performance。
有時候我們可能想檢查一下目標伺服器的「cwnd」初始值設定,此時可以數包:
通過握手階段確認RTT為168,開始傳輸後得到第一個資料包的時間是409,加上RTT後就是577,從409到577之間有兩個資料包,所以「cwnd」初始值是2MSS。
需要額外說明的是,單純數包可能並不準確,因為網路卡可能會對包做點手腳,具體說明資訊請參考:Segmentation and Checksum Offloading: Turning Off with ethtool。
補充:有人寫了一個名叫 initcwnd_check 的指令碼,可以幫你檢查「cwnd」初始值。
…
實踐是檢驗真理的唯一標準,希望大家多動手,通過實驗來檢驗結果,推薦一篇不錯的文章:Impact of Bandwidth Delay Product on TCP Throughput,此外知乎上的討論也值得一看:為什麼多 TCP 連線分塊下載比單連線下載快,大家有貨的話也請告訴我。
如果覺得不錯,歡迎紅包支援!