TCP 簡介
因特網有兩個核心協議: IP 和 TCP。 IP,即 Internet Protocol(因特網協議),負責聯網主機之間的路由選擇和定址; TCP,即 Transmission Control Protocol(傳輸控制協議),負責在不可靠的傳輸通道之上提供可靠的抽象層。 TCP/IP 也常被稱為“因特網協議套件”。
我們都知道有 IPv4 和 IPv6,那 IPv1~3 和 IPv5 呢?IPv4 中的 4 表示 TCP/IP 協議的第 4個版本,釋出於 1981 年 9 月。最初的 TCP/IP 建議中同時包含兩個協議,但標準草案第 4 版將這兩個協議分開,使之各自成為獨立的 RFC。實際上, IPv4 中的 v4 只是表明了它與 TCP 前 3 個版本的承繼關係,之前並沒有單獨的 IPv1、 IPv2 或 IPv3 協議。1994 年,當工作組著手製定 Internet Protocol next generation(IPng)需要一個新版本號時, v5 已經被分配給了另一個試驗性協議 Internet Stream Protocol(ST)。但ST 一直沒有什麼進展,這也是我們為什麼很少聽說它的原因。結果 TCP/IP 的下一版本就成了 IPv6。
三次握手
所有 TCP 連線一開始都要經過三次握手,客戶端與伺服器在交換應用資料之前,必須就起始分組序列號,以及其他一些連線相關的細節達成一致。出於安全考慮,序列號由兩端隨機生成。
三次握手的步驟:
- SYN。客戶端選擇一個隨機序列號 x,併傳送一個 SYN 分組,其中可能還包括其他 TCP 標誌和選項。
- SYN ACK。伺服器給 x 加 1,並選擇自己的一個隨機序列號 y,追加自己的標誌和選項,然後返回響應。
- ACK。客戶端給 x 和 y 加 1 併傳送握手期間的最後一個 ACK 分組
三次握手帶來的延遲使得每建立一個新 TCP 連線都要付出很大代價,而這也決定了提高 TCP 應用效能的關鍵,在於想辦法重用連線。
擁塞預防及控制
參考《TCP/IP 詳解》,可知 TCP 保證可靠傳輸的機制有:
- 分割資料塊。應用資料被分割成 TCP 認為最適合傳送的資料塊
- 自適應的超時及重傳策略。TCP 在傳送時會設定一個定時器,如果時間到了還沒有收到確認,它就重傳資料
- 停止等待。TCP 每傳送完一個分組,就會停止傳送,等待對方的確認,只有對方確認之後,才傳送下一個分組
- 檢驗和。如果首部的檢驗和出錯,那麼 TCP 會重新丟棄該報文段,並要求傳送端重新傳送
- 重新排序。IP 資料包的到達順序可能會亂,因此 TCP 需要對收到的資料進行重新排序
- 丟棄重複的資料。IP 資料包可能會重複,因此 TCP 的接收端需要對重複的資料進行丟棄處理
- 流量控制。
- 擁塞控制。
下面詳細介紹流量控制及擁塞控制。
流量控制
流量控制是一種預防傳送端過多向接收端傳送資料的機制。否則,接收端可能因為忙碌、負載重或緩衝區既定而無法處理。為實現流量控制, TCP 連線的每一方都要通告自己的接收視窗(rwnd),其中包含能夠儲存資料的緩衝區空間大小資訊。
如果其中一端跟不上資料傳輸,那它可以向傳送端通告一個較小的視窗。假如視窗為零,則意味著必須由應用層先清空緩衝區,才能再接收剩餘資料。這個過程貫穿於每個 TCP 連線的整個生命週期:每個 ACK 分組都會攜帶相應的最新 rwnd 值,以便兩端動態調整資料流速,使之適應傳送端和接收端的容量及處理能力。
最初的 TCP 規範分配給通告視窗大小的欄位是 16 位的,這相當於設定了傳送端和接收端視窗的最大值(2^16 即 65 535 位元組)。結果,在這個限制內經常無法獲得最優效能,特別是在那些“頻寬延遲積”(下面會介紹)很高的網路中。為解決這個問題, RFC 1323 提供了 **TCP 視窗縮放(TCP Window Scaling) ** 選項,可以把接收視窗大小由 65 535 位元組提高到 1G 位元組!縮放 TCP 視窗是在三次握手期間完成的,其中有一個值表示在將來的 ACK 中左移 16 位視窗欄位的位數。今天, TCP 視窗縮放機制在所有主要平臺上都是預設啟用的。不過,中間節點和路由器可以重寫,甚至完全去掉這個選項。如果你的伺服器或客戶端的連線不能完全利用現有頻寬,那往往該先查一查視窗大小。在 Linux 中,可以通過如下命令檢查和啟用視窗縮放選項:
$> sysctl net.ipv4.tcp_window_scaling
$> sysctl -w net.ipv4.tcp_window_scaling=1
複製程式碼
慢啟動
儘管流量控制確實可以防止傳送端向接收端過多傳送資料,但卻沒有機制預防任何一端向潛在網路過多傳送資料。換句話說,傳送端和接收端在連線建立之初,誰也不知道可用頻寬是多少,因此需要一個估算機制,然後還要根據網路中不斷變化的條件而動態改變速度。
解決這個問題的演算法有:慢啟動、擁塞預防、快速重傳和快速恢復。
慢啟動演算法的設計思路是這樣的:伺服器通過 TCP 連線初始化一個新的**擁塞視窗(cwnd)**變數,將其值設定為一個系統設定的保守值(在 Linux 中就是 initcwnd)。客戶端與伺服器之間最大可以傳輸(未經 ACK 確認的)資料量取 rwnd 和 cwnd 變數中的最小值。然後在分組被確認後增大視窗大小,慢慢地啟動(下圖前半部分)——慢啟動中的“慢”指的不是視窗增長的速度慢,而是因為要增長到適合當前頻寬的視窗大小,需要多次 TCP 通訊往返,這個過程帶來了較大的時間消耗。
為了說明這個過程,這裡先介紹兩個概念:
- MTU,Maximum Transmit Unit,最大傳輸單元,即物理介面(資料鏈路層、IP 層)提供給其上層最大一次傳輸資料的大小,預設為 1500 Byte
- MSS,Maximum Segment Size ,最大TCP分段大小,預設為 1500 - 20(TCP 首部)- 20(IP 首部) = 1460 位元組
並假設有如下條件:
- 客戶端和伺服器的接收視窗為 65 535 位元組(64 KB)
- 初始的擁塞視窗: 4 段(MSS)
- 往返時間是 56 ms(倫敦到紐約)
計算可知,要達到 64 KB 的限制,需要把擁塞視窗大小增加到 45 段,而這需要 224 ms:
也就是說,要達到客戶端與伺服器之間 64 KB 的吞吐量,需要 4 次往返,幾百 ms 的延遲!至於客戶端與伺服器之間實際的連線速率是不是在 Mbit/s 級別,絲毫不影響這個結果。這就是慢啟動。
慢啟動導致客戶端與伺服器之間經過幾百 ms 才能達到接近最大速度的問題,對於大型流式下載服務的影響倒不顯著,因為慢啟動的時間可以分攤到整個傳輸週期內消化掉。可是,對於很多 HTTP 連線,特別是一些短暫、突發的連線而言,常常會出現還沒有達到最大視窗請求就被終止的情況。換句話說,很多 Web 應用的效能經常受到伺服器與客戶端之間往返時間的制約。因為慢啟動限制了可用的吞吐量,而這對於小檔案傳輸非常不利。
因此,把伺服器的初始 cwnd 值增大到 RFC 6928 新規定的 10 段(IW10),是提升使用者體驗以及所有 TCP 應用效能的最簡單方式。好訊息是,很多作業系統已經更新了核心,採用了增大後的值。
另外,除了調節新連線的傳輸速度, TCP 還實現了 SSR(Slow-Start Restart,慢啟動重啟)機制。這種機制會在連線空閒一定時間後重置連線的擁塞視窗。道理很簡單,在連線空閒的同時,網路狀況也可能發生了變化,為了避免擁塞,理應將擁塞視窗重置回“安全的”預設值。毫無疑問, SSR 對於那些會出現突發空閒的長週期 TCP 連線(比如 HTTP 的 keep-alive 連線)有很大的影響。因此,建議在伺服器上禁用 SSR。
擁塞預防
擁塞預防演算法,其實就是上圖(圖2-3)的後半部分。
慢啟動以保守的視窗初始化連線,隨後的每次往返都會成倍提高傳輸的資料量,直到超過接收端的流量控制視窗,或者有分組丟失為止,此時擁塞預防演算法介入。擁塞預防演算法把丟包作為網路擁塞的標誌,即路徑中某個連線或路由器已經擁堵了,以至於必須採取刪包措施。因此,必須調整視窗小,以避免造成更多的包丟失,從而保證網路暢通。重置擁塞視窗後,擁塞預防機制按照自己的演算法來增大視窗以儘量避免丟包。某個時刻,可能又會有包丟失,於是這個過程再從頭開始。
確定丟包恢復的最優方式並不容易。如果太激進,那麼間歇性的丟包就會對整個連線的吞吐量造成很大影響。而如果不夠快,那麼還會繼續造成更多分組丟失。
最初, TCP 使用 AIMD( Multiplicative Decrease and Additive Increase,倍減加增)演算法,即發生丟包時,先將擁塞視窗減半,然後每次往返再緩慢地給視窗增加一個固定的值。不過,很多時候 AIMD 演算法太過保守,因此又有了新的演算法。
PRR( Proportional Rate Reduction,比例降速)就是 RFC 6937 規定的一個新演算法,其目標就是改進丟包後的恢復速度。改進效果如何呢?根據谷歌的測量,實現新演算法後,因丟包造成的平均連線延遲減少了 3%~10%。
快速重傳和快速恢復
簡單介紹一下這兩個演算法:如果一連串收到3個或3個以上的重複ACK,就非常可能是一個報文段丟失了。於是我們就重傳丟失的資料包文段,而無需等待超時定時器溢位。這就是快速重傳演算法。接下來執行的不是慢啟動演算法而是擁塞避免演算法。這就是快速恢復演算法。
頻寬延遲積
BDP(Bandwidth-delay product,頻寬延遲積):資料鏈路的容量與其端到端延遲的乘積。
在 TCP 通訊中,傳送端或接收端無論誰被迫頻繁地停止等待之前分組的 ACK,都會造成資料缺口,從而必然限制連線的最大吞吐量。為解決這個問題,應該讓視窗足夠大,以保證任何一端都能在 ACK 返回前持續傳送資料。只有傳輸不中斷,才能保證最大吞吐量。而最優視窗大小取決於往返時間!無論實際或通告的頻寬是多大,視窗過小都會限制連線的吞吐量。
那麼,流量控制視窗( rwnd)和擁塞控制視窗( cwnd)的值多大合適呢?實際上,計算過程很簡單。首先,假設 cwnd 和 rwnd 的最小值為 16 KB,往返時間為 100 ms,那麼:
因此,不管傳送端和接收端的實際頻寬多大,這個 TCP 連線的資料傳輸速率不會超過 1.31Mbit/s !想提高吞吐量,要麼增大最小視窗值,要麼減少往返時間。
類似地,知道往返時間和兩端的實際頻寬也可以計算最優視窗大小。這一次我們假設往返時間不變(還是 100 ms),傳送端的可用頻寬為 10 Mbit/s,接收端則為100 Mbit/s+。還假設兩端之間沒有網路擁塞,我們的目標就是充分利用客戶端的 10Mbit/s 頻寬:
因此,視窗至少需要 122.1 KB(這個值就是頻寬延遲積) 才能充分利用 10 Mbit/s 頻寬!
隊首阻塞
每個 TCP 分組都會帶著一個唯一的序列號被髮出,而所有分組必須按順序傳送到接收端。如果中途有一個分組沒能到達接收端,那麼後續分組必須儲存在接收端的 TCP 緩衝區,等待丟失的分組重發併到達接收端。這一切都發生在 TCP 層,應用程式對 TCP 重發和緩衝區中排隊的分組一無所知,必須等待分組全部到達才能訪問資料。在此之前,應用程式只能在通過套接字讀資料時感覺到延遲交付。這種效應稱為** TCP 的隊首阻塞**。
隊首阻塞造成的延遲可以讓我們的應用程式不用關心分組重排和重組,從而讓程式碼保持簡潔。然而,程式碼簡潔也要付出代價,那就是分組到達時間會存在無法預知的延遲變化。這個時間變化通常被稱為抖動,也是影響應用程式效能的一個主要因素
有些應用程式可能並不需要可靠的交付或者不需要按順序交付。比如,每個分組都是獨立的訊息,那麼按順序交付就沒有任何必要。而且,如果每個訊息都會覆蓋之前的訊息,那麼可靠交付同樣也沒有必要了。無需按序交付資料或能夠處理分組丟失的應用程式,以及對延遲或抖動要求很高的應用程式,最好選擇 UDP 等協議。
針對 TCP 的優化建議
由上文可知,TCP 的核心原理及其影響有:
- TCP 三次握手增加了整整一次往返時間
- TCP 慢啟動將被應用到每個新連線
- TCP 流量及擁塞控制會影響所有連線的吞吐量
- TCP 的吞吐量由當前擁塞視窗大小控制
現代高速網路中 TCP 連線的資料傳輸速度,往往會受到接收端和傳送端之間往返時間的限制。儘管頻寬不斷增長,但延遲依舊受限於光速,而且已經限定在了其最大值的一個很小的常數因子之內。因此,大多數情況下,TCP 的瓶頸都是延遲,而非頻寬。
伺服器配置調優
TCP 的最佳實踐以及影響其效能的底層演算法一直在與時俱進,而且大多數變化都只在最新核心中才有實現。因此,讓你的伺服器跟上時代是優化傳送端和接收端 TCP 棧的首要措施。此外,可以採取下列措施配置伺服器:
- 增大TCP的初始擁塞視窗
- 禁用慢啟動重啟
- 啟用視窗縮放
- TCP 快速開啟(TFO)
應用程式行為調優
- 消除不必要的資料傳輸。比如,減少下載不必要的資源,或者通過壓縮演算法把要傳送的位元數降到最低。
- 部署 CDN。通過在不同的地區部署伺服器(比如,使用 CDN),把資料放到接近客戶端的地方,可以減少網路往返的延遲,從而顯著提升 TCP 效能。
- 儘可能重用已經建立的 TCP 連線,把慢啟動和其他擁塞控制機制的影響降到最低
效能檢查清單
- 把伺服器核心升級到最新版本
- 增大TCP的初始擁塞視窗(cwnd.大小
- 禁用慢啟動重啟
- 啟用視窗縮放
- 減少傳輸冗餘資料
- 壓縮要傳輸的資料
- 把伺服器放到離使用者近的地方以減少往返時間
- 盡最大可能重用已經建立的 TCP 連線
參考:《Web 效能權威指南》