TCP 協議能夠說是今天互聯網的基石,做爲可靠的傳輸協議,在今天幾乎全部的數據都會經過 TCP 協議傳輸,然而 TCP 在設計之初沒有考慮到現今複雜的網絡環境,當你在地鐵上或者火車上被斷斷續續的網絡折磨時,你可能都不知道這一切可能都是 TCP 協議形成的。本文會分析 TCP 協議爲何在弱網環境下有嚴重的性能問題[^1]。算法
底層的數據傳輸協議在設計時必需要對帶寬的利用率和通訊延遲進行權衡和取捨,因此想要解決實際生產中的所有問題是不可能的,TCP 選擇了充分利用帶寬,爲流量而設計,指望在儘量短的時間內傳輸更多的數據[^2]。服務器
在網絡通訊中,從發送方發出數據開始到收到來自接收方的確認的時間被叫作往返時延(Round-Trip Time,RTT)。網絡
弱網環境是丟包率較高的特殊場景,TCP 在相似場景中的表現不好,當 RTT 爲 30ms 時,一旦丟包率達到了 2%,TCP 的吞吐量就會降低 89.9%[^3],從下面的表中咱們能夠看出丟包對 TCP 的吞吐量極其顯著的影響:
tcp
RTT | TCP 吞吐量 | TCP 吞吐量(2% 丟包率) |
---|---|---|
0 ms | 93.5 Mbps | 3.72 Mbps |
30 ms | 16.2 Mbps | 1.63 Mbps |
60 ms | 8.7 Mbps | 1.33 Mbps |
90 ms | 5.32 Mbps | 0.85 Mbps |
本文將分析在弱網環境下(丟包率高)影響 TCP 性能的三個緣由:性能
TCP 的擁塞控制算法會在丟包時主動下降吞吐量;學習
TCP 的三次握手增長了數據傳輸的延遲和額外開銷;大數據
TCP 的累計應答機制致使了數據段的傳輸;優化
在上述的三個緣由中,擁塞控制算法是致使 TCP 在弱網環境下有着較差表現的首要緣由,三次握手和累計應答二者的影響依次遞減,可是也加重了 TCP 的性能問題。spa
TCP 擁塞控制算法是互聯網上主要的擁塞控制措施,它使用一套基於線増積減(Additive increase/multiplicative decrease,AIMD)的網絡擁塞控制方法來控制擁塞[^4],也是形成 TCP 性能問題的主要緣由。操作系統
第一次發現的互聯網擁塞崩潰是在 1986 年,NSFnet 階段一的骨幹網的處理能力從 32,000bit/s 降到了 40bit/s,該骨幹網的處理能力直到 1987 和 1988 年,TCP 協議實現了擁塞控制以後才獲得解決[^5]。正是由於發生過網絡阻塞形成的崩潰,因此 TCP 的擁塞控制算法就認爲只要發生了丟包當前網絡就發生了擁堵,從這一假設出發,TCP 就使用了慢啓動和線增積減[^6]的機制實現擁塞控制。
tcp-congestion-control
圖 1 - TCP 的擁塞控制機制
每個 TCP 鏈接都會維護一個擁塞控制窗口(Congestion Window),擁塞控制窗口的做用有兩個:
防止發送方向接收方發送了太多數據,致使接收方沒法處理;
防止 TCP 鏈接的任意一方向網絡中發送大量數據,致使網絡擁塞崩潰;
除了擁塞窗口大小(cwnd)以外,TCP 鏈接的雙方都有接收窗口大小(rwnd),在 TCP 鏈接創建之初,發送方和接收方都不清楚對方的接收窗口大小,因此通訊雙方須要一套動態的估算機制改變數據傳輸的速度,在 TCP 三次握手期間,通訊雙方會經過 ACK 消息通知對方本身的接收窗口大小,接收窗口大小通常是帶寬延遲乘積(Bandwidth-delay product, BDP)決定的[^7],不過在這裏咱們就不展開介紹了。
客戶端可以同時傳輸的最大數據段的數量是接收窗口大小和擁塞窗口大小的最小值,即 min(rwnd, cwnd)
。TCP 鏈接的初始擁塞窗口大小是一個比較小的值,在 Linux 中是由 TCP_INIT_CWND
定義的[^8]:
初始擁塞控制窗口的大小從出現以後被屢次修改,幾個名爲 Increasing TCP's Initial Window 的 RFC 文檔:RFC2414[^9]、RFC3390[^10] 和 RFC6928[^11] 分別增長了 initcwnd
的值以適應不斷提升的網絡傳輸速度和帶寬。
tcp-congestion-window
圖 2 - TCP 擁塞控制窗口的線増積減
如上圖所示,TCP 鏈接發送方的擁塞控制窗口大小會根據接收方的響應而變化:
線性增加:當發送方收到了接收方的 ACK 時,擁塞窗口大小會加一;
積式減小:當發送方發送的數據包丟包時,擁塞控制窗口會減半;
若是 TCP 鏈接剛剛創建,因爲 Linux 系統的默認設置,客戶端可以同時發送 10 個數據段,假設咱們網絡的帶寬是 10M,RTT 是 40ms,每一個數據段的大小是 1460 字節,那麼使用 BDP 計算的通訊雙方窗口大小上限應該是 35,這樣才能充分利用網絡的帶寬:
然而擁塞控制窗口的大小從 10 漲到 35 須要 2RTT 的時間,具體的過程以下:
發送方向接收方發送 initcwnd = 10
個數據段(消耗 0.5RTT);
接收方接收到 10 個數據段後向發送方發送 ACK(消耗 0.5RTT);
發送方接收到發送方的 ACK,擁塞控制窗口大小因爲 10 個數據段的成功發送 +10,當前擁塞控制窗口大小達到 20;
發送方向接收方發送 20 個數據段(消耗 0.5RTT);
接收方接收到 20 個數據段後向發送方發送 ACK(消耗 0.5RTT);
發送方接收到發送方的 ACK,擁塞控制窗口大小因爲 20 個數據段的成功發送 +20,當前擁塞控制窗口大小達到 40;
從 TCP 三次握手創建鏈接到擁塞控制窗口大小達到假定網絡情況的最大值 35 須要 3.5RTT 的時間,即 140ms,這是一個比較長的時間了。
早期互聯網的大多數計算設備都經過有線網絡鏈接,出現網絡不穩定的可能性也比較低,因此 TCP 協議的設計者認爲丟包意味着網絡出現擁塞,一旦發生丟包,客戶端瘋狂重試就可能致使互聯網的擁塞崩潰,因此發明了擁塞控制算法來解決該問題。
可是現在的網絡環境更加複雜,無線網絡的引入致使部分場景下的網絡不穩定成了常態,因此丟包並不必定意味着網絡擁堵,若是使用更加激進的策略傳輸數據,在一些場景下會獲得更好的效果。
TCP 使用三次握手創建鏈接應該是全世界全部工程師都十分了解的知識點,三次握手的主要目的是避免歷史錯誤鏈接的創建並讓通訊的雙方肯定初始序列號[^12],然而三次握手的成本至關高,在不丟包的狀況下,它須要創建 TCP 鏈接的雙方進行三次通訊。
basic-3-way-handshake
圖 3 - 常見的 TCP 三次握手
若是咱們要從北京訪問上海的服務器,因爲北京到上海的直線距離約爲 1000 多千米,而光速是目前通訊速度的極限,因此 RTT 必定會大於 6.7ms:
然而由於光在光纖中不是直線傳播的,真正的傳輸速度會比光速慢 ~31%[^13],並且數據須要在各類網絡設備之間來回跳轉,因此很難達到理論的極限值。在生產環境中從北京到上海的 RTT 大概在 40ms 左右,因此 TCP 創建鏈接所須要最短期也須要 60ms(1.5RTT)。
在網絡環境較差的地鐵、車站等場景中,由於丟包率較高,客戶端很難與服務端快速完成三次通訊並創建 TCP 鏈接。當客戶端長時間沒有收到服務端的響應時,只能不斷髮起重試,隨着請求次數逐漸增長,訪問的延遲也會愈來愈高。
因爲大多數的 HTTP 請求都不會攜帶大量的數據,未被壓縮的請求和響應頭大小在 ~200B 到 2KB 左右,而 TCP 三次握手帶來的額外開銷是 222 字節,其中以太網數據幀佔 3 * 14 = 42
字節,IP 數據幀佔 3 * 20 = 60
字節,TCP 數據幀佔 120 字節:
tcp-three-way-handshake-overhead
圖 4 - TCP 三次握手的額外開銷
雖然 TCP 不會爲每個發出的數據段創建鏈接,可是三次握手創建鏈接須要的成本仍是至關高,不只須要額外增長 1.5RTT 的網絡延時,還須要增長 222 字節的額外開銷,因此在弱網環境下,經過三次握手創建鏈接會加重 TCP 的性能問題。
TCP 傳輸的可靠性是經過序列號和接收方的 ACK 來保證的,當 TCP 傳輸一個數據段時,它會將該數據段的副本放到重傳隊列上並開啓計時器[^14]:
若是發送方收到了該數據段對應的 ACK 響應,當前數據段就會從重傳隊列中刪除;
若是發送方在計時器到期之間都沒有收到該數據段對應的 ACK,就會從新發送當前數據段;
TCP 的 ACK 機制可能會致使發送方從新傳輸接收方已經收到了數據段。TCP 中的 ACK 消息表示該消息以前的所有消息都已經被成功接收和處理,例如:
發送方向接收方發送了序號爲 1-10 的消息;
接收方向發送方發送 ACK 8 響應;
發送方認爲序號爲 1-8 的消息已經被成功接收;
這種 ACK 的方式在實現上比較簡單,更容易保證消息的順序性,可是在如下狀況可能會致使發送方重傳已經接收的數據:
tcp-retransmission-al
圖 5 - TCP 的重傳策略
如上圖所示,接收方已經收到了序號爲 2-5 的數據,可是因爲 TCP ACK 的語義是當前數據段前的所有數據段都已經被接收和處理,因此接收方沒法發送 ACK 消息,因爲發送方沒有收到 ACK,全部數據段對應的計時器就會超時並從新傳輸數據。在丟包較爲嚴重的網絡下,這種重傳機制會形成大量的帶寬浪費。
TCP 協議的一些設計在今天來看雖然仍然具備巨大的價值,可是並不能適用於全部場景。爲了解決 TCP 的性能問題,目前業界有兩種解決方案:
使用 UDP 構建性能更加優異、更靈活的傳輸協議,例如:QUIC[^15] 等;
經過不一樣的手段優化 TCP 協議的性能,例如:選擇性 ACK(Selective ACK, SACK)[^16],TCP 快開啓(TCP Fast Open, TFO)[^17];
因爲 TCP 協議在操做系統內核中,不利於協議的更新,因此第一種方案目前發展的更好,HTTP/3 就使用了 QUIC 做爲傳輸協議[^18]。咱們在這裏從新回顧一下致使 TCP 性能問題的三個重要緣由:
TCP 的擁塞控制在發生丟包時會進行退讓,減小可以發送的數據段數量,可是丟包並不必定意味着網絡擁塞,更多的多是網絡情況較差;
TCP 的三次握手帶來了額外開銷,這些開銷不僅包括須要傳輸更多的數據,還增長了首次傳輸數據的網絡延遲;
TCP 的重傳機制在數據包丟失時可能會從新傳輸已經成功接收的數據段,形成帶寬的浪費;
TCP 協議做爲互聯網數據傳輸的基石能夠說是當之無愧,雖然它確實在應對特殊場景時有些問題,可是它的設計思想有着很是多的借鑑意義並值得咱們學習。
到最後,咱們仍是來看一些比較開放的相關問題,有興趣的讀者能夠仔細思考一下下面的問題:
QUIC 協議是可否保證丟包率較高時的傳輸性能?
除了 SACK 和 TFO 以外還有哪些手段能夠優化 TCP 的性能?