掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網(wǎng)交流
為什么這么設計(Why's THE Design)是一系列關于計算機領域中程序設計決策的文章,我們在這個系列的每一篇文章中都會提出一個具體的問題并從不同的角度討論這種設計的優(yōu)缺點、對具體實現(xiàn)造成的影響。

創(chuàng)新互聯(lián)主營蒲縣網(wǎng)站建設的網(wǎng)絡公司,主營網(wǎng)站建設方案,重慶APP開發(fā)公司,蒲縣h5成都小程序開發(fā)搭建,蒲縣網(wǎng)站營銷推廣歡迎蒲縣等地區(qū)企業(yè)咨詢
TCP 協(xié)議可以說是今天互聯(lián)網(wǎng)的基石,作為可靠的傳輸協(xié)議,在今天幾乎所有的數(shù)據(jù)都會通過 TCP 協(xié)議傳輸,然而 TCP 在設計之初沒有考慮到現(xiàn)今復雜的網(wǎng)絡環(huán)境,當你在地鐵上或者火車上被斷斷續(xù)續(xù)的網(wǎng)絡折磨時,你可能都不知道這一切可能都是 TCP 協(xié)議造成的。本文會分析 TCP 協(xié)議為什么在弱網(wǎng)環(huán)境下有嚴重的性能問題[^1]。
底層的數(shù)據(jù)傳輸協(xié)議在設計時必須要對帶寬的利用率和通信延遲進行權衡和取舍,所以想要解決實際生產(chǎn)中的全部問題是不可能的,TCP 選擇了充分利用帶寬,為流量而設計,期望在盡可能短的時間內(nèi)傳輸更多的數(shù)據(jù)[^2]。
在網(wǎng)絡通信中,從發(fā)送方發(fā)出數(shù)據(jù)開始到收到來自接收方的確認的時間被叫做往返時延(Round-Trip Time,RTT)。
弱網(wǎng)環(huán)境是丟包率較高的特殊場景,TCP 在類似場景中的表現(xiàn)很差,當 RTT 為 30ms 時,一旦丟包率達到了 2%,TCP 的吞吐量就會下降 89.9%[^3],從下面的表中我們可以看出丟包對 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 |
本文將分析在弱網(wǎng)環(huán)境下(丟包率高)影響 TCP 性能的三個原因:
在上述的三個原因中,擁塞控制算法是導致 TCP 在弱網(wǎng)環(huán)境下有著較差表現(xiàn)的首要原因,三次握手和累計應答兩者的影響依次遞減,但是也加劇了 TCP 的性能問題。
擁塞控制
TCP 擁塞控制算法是互聯(lián)網(wǎng)上主要的擁塞控制措施,它使用一套基于線増積減(Additive increase/multiplicative decrease,AIMD)的網(wǎng)絡擁塞控制方法來控制擁塞[^4],也是造成 TCP 性能問題的主要原因。
第一次發(fā)現(xiàn)的互聯(lián)網(wǎng)擁塞崩潰是在 1986 年,NSFnet 階段一的骨干網(wǎng)的處理能力從 32,000bit/s 降到了 40bit/s,該骨干網(wǎng)的處理能力直到 1987 和 1988 年,TCP 協(xié)議實現(xiàn)了擁塞控制之后才得到解決[^5]。正是因為發(fā)生過網(wǎng)絡阻塞造成的崩潰,所以 TCP 的擁塞控制算法就認為只要發(fā)生了丟包當前網(wǎng)絡就發(fā)生了擁堵,從這一假設出發(fā),TCP 就使用了慢啟動和線增積減[^6]的機制實現(xiàn)擁塞控制。
圖 1 - TCP 的擁塞控制機制
每一個 TCP 連接都會維護一個擁塞控制窗口(Congestion Window),擁塞控制窗口的作用有兩個:
除了擁塞窗口大?。╟wnd)之外,TCP 連接的雙方都有接收窗口大?。╮wnd),在 TCP 連接建立之初,發(fā)送方和接收方都不清楚對方的接收窗口大小,所以通信雙方需要一套動態(tài)的估算機制改變數(shù)據(jù)傳輸?shù)乃俣龋?TCP 三次握手期間,通信雙方會通過 ACK 消息通知對方自己的接收窗口大小,接收窗口大小一般是帶寬延遲乘積(Bandwidth-delay product, BDP)決定的[^7],不過在這里我們就不展開介紹了。
客戶端能夠同時傳輸?shù)淖畲髷?shù)據(jù)段的數(shù)量是接收窗口大小和擁塞窗口大小的最小值,即 min(rwnd, cwnd)。TCP 連接的初始擁塞窗口大小是一個比較小的值,在 Linux 中是由 TCP_INIT_CWND 定義的[^8]:
- /* TCP initial congestion window as per rfc6928 */
- #define TCP_INIT_CWND10
初始擁塞控制窗口的大小從出現(xiàn)之后被多次修改,幾個名為 Increasing TCP's Initial Window 的 RFC 文檔:RFC2414[^9]、RFC3390[^10] 和 RFC6928[^11] 分別增加了 initcwnd 的值以適應不斷提高的網(wǎng)絡傳輸速度和帶寬。
圖 2 - TCP 擁塞控制算法的線増積減
如上圖所示,TCP 連接發(fā)送方的擁塞控制窗口大小會根據(jù)接收方的響應而變化:
如果 TCP 連接剛剛建立,由于 Linux 系統(tǒng)的默認設置,客戶端能夠同時發(fā)送 10 個數(shù)據(jù)段,假設我們網(wǎng)絡的帶寬是 10M,RTT 是 40ms,每個數(shù)據(jù)段的大小是 1460 字節(jié),那么使用 BDP 計算的通信雙方窗口大小上限應該是 35,這樣才能充分利用網(wǎng)絡的帶寬:
然而擁塞控制窗口的大小從 10 漲到 35 需要 2RTT 的時間,具體的過程如下:
從 TCP 三次握手建立連接到擁塞控制窗口大小達到假定網(wǎng)絡狀況的最大值 35 需要 3.5RTT 的時間,即 140ms,這是一個比較長的時間了。
早期互聯(lián)網(wǎng)的大多數(shù)計算設備都通過有線網(wǎng)絡連接,出現(xiàn)網(wǎng)絡不穩(wěn)定的可能性也比較低,所以 TCP 協(xié)議的設計者認為丟包意味著網(wǎng)絡出現(xiàn)擁塞,一旦發(fā)生丟包,客戶端瘋狂重試就可能導致互聯(lián)網(wǎng)的擁塞崩潰,所以發(fā)明了擁塞控制算法來解決該問題。
但是如今的網(wǎng)絡環(huán)境更加復雜,無線網(wǎng)絡的引入導致部分場景下的網(wǎng)絡不穩(wěn)定成了常態(tài),所以丟包并不一定意味著網(wǎng)絡擁堵,如果使用更加激進的策略傳輸數(shù)據(jù),在一些場景下會得到更好的效果。
三次握手
TCP 使用三次握手建立連接應該是全世界所有工程師都十分了解的知識點,三次握手的主要目的是避免歷史錯誤連接的建立并讓通信的雙方確定初始序列號[^12],然而三次握手的成本相當高,在不丟包的情況下,它需要建立 TCP 連接的雙方進行三次通信。
圖 3 - 常見的 TCP 三次握手
如果我們要從北京訪問上海的服務器,由于北京到上海的直線距離約為 1000 多公里,而光速是目前通信速度的極限,所以 RTT 一定會大于 6.7ms:
然而因為光在光纖中不是直線傳播的,真正的傳輸速度會比光速慢 ~31%[^13],而且數(shù)據(jù)需要在各種網(wǎng)絡設備之間來回跳轉(zhuǎn),所以很難達到理論的極限值。在生產(chǎn)環(huán)境中從北京到上海的 RTT 大概在 40ms 左右,所以 TCP 建立連接所需要最短時間也需要 60ms(1.5RTT)。
在網(wǎng)絡環(huán)境較差的地鐵、車站等場景中,因為丟包率較高,客戶端很難與服務端快速完成三次通信并建立 TCP 連接。當客戶端長時間沒有收到服務端的響應時,只能不斷發(fā)起重試,隨著請求次數(shù)逐漸增加,訪問的延遲也會越來越高。
由于大多數(shù)的 HTTP 請求都不會攜帶大量的數(shù)據(jù),未被壓縮的請求和響應頭大小在 ~200B 到 2KB 左右,而 TCP 三次握手帶來的額外開銷是 222 字節(jié),其中以太網(wǎng)數(shù)據(jù)幀占 3 * 14 = 42 字節(jié),IP 數(shù)據(jù)幀占 3 * 20 = 60 字節(jié),TCP 數(shù)據(jù)幀占 120 字節(jié):
圖 4 - TCP 三次握手的額外開銷
雖然 TCP 不會為每一個發(fā)出的數(shù)據(jù)段建立連接,但是三次握手建立連接需要的成本還是相當高,不僅需要額外增加 1.5RTT 的網(wǎng)絡延時,還需要增加 222 字節(jié)的額外開銷,所以在弱網(wǎng)環(huán)境下,通過三次握手建立連接會加劇 TCP 的性能問題。
重傳機制
TCP 傳輸?shù)目煽啃允峭ㄟ^序列號和接收方的 ACK 來保證的,當 TCP 傳輸一個數(shù)據(jù)段時,它會將該數(shù)據(jù)段的副本放到重傳隊列上并開啟計時器[^14]:
TCP 的 ACK 機制可能會導致發(fā)送方重新傳輸接收方已經(jīng)收到了數(shù)據(jù)段。TCP 中的 ACK 消息表示該消息之前的全部消息都已經(jīng)被成功接收和處理,例如:
這種 ACK 的方式在實現(xiàn)上比較簡單,更容易保證消息的順序性,但是在以下情況可能會導致發(fā)送方重傳已經(jīng)接收的數(shù)據(jù):
圖 5 - TCP 的重傳策略
如上圖所示,接收方已經(jīng)收到了序號為 2-5 的數(shù)據(jù),但是由于 TCP ACK 的語義是當前數(shù)據(jù)段前的全部數(shù)據(jù)段都已經(jīng)被接收和處理,所以接收方無法發(fā)送 ACK 消息,由于發(fā)送方?jīng)]有收到 ACK,所有數(shù)據(jù)段對應的計時器就會超時并重新傳輸數(shù)據(jù)。在丟包較為嚴重的網(wǎng)絡下,這種重傳機制會造成大量的帶寬浪費。
總結
TCP 協(xié)議的一些設計在今天來看雖然仍然具有巨大的價值,但是并不能適用于所有場景。為了解決 TCP 的性能問題,目前業(yè)界有兩種解決方案:
由于 TCP 協(xié)議在操作系統(tǒng)內(nèi)核中,不利于協(xié)議的更新,所以第一種方案目前發(fā)展的更好,HTTP/3 就使用了 QUIC 作為傳輸協(xié)議[^18]。我們在這里重新回顧一下導致 TCP 性能問題的三個重要原因:
TCP 協(xié)議作為互聯(lián)網(wǎng)數(shù)據(jù)傳輸?shù)幕梢哉f是當之無愧,雖然它確實在應對特殊場景時有些問題,但是它的設計思想有著非常多的借鑒意義并值得我們學習。
到最后,我們還是來看一些比較開放的相關問題,有興趣的讀者可以仔細思考一下下面的問題:

我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網(wǎng)交流