本次和大家聊一下TCP性能優化。
TCP全稱為Transmission Control Protocol,每一個IT人士對TCP都有一定了解。TCP協議屬於底層協議,對於大部分研發人員來説,這是透明的,無需關心TCP的實現與細節。
不過如果想做深入的性能優化,TCP是繞不過去的一環。要講TCP性能優化,必須先回顧一下TCP的一些細節。讓我們先來看一下TCP的首部格式
TCP報文段的首部格式
TCP報文段首部的前20個字節是固定的,後面有4n字節是根據需要而增加的選項(n是整數)。因此TCP首部的最小長度是20字節。
- 序號:字段值指的是本報文段所發送的數據的第一個字節的序號
- 確認號:是期望收到對方下一個報文段的第一個數據字節的序號。若確認號為= N,則表明:到序號N-1為止的所有數據都已正確收到
- ACK: 僅當ACK = 1時確認號字段才有效,當ACK = 0時確認號無效。TCP規定,在連接建立後所有的傳送的報文段都必須把ACK置為1
- SYN: 在連接建立時用來同步序號。當SYN=1而ACK=0時,表明這是一個連接請求報文段。對方若同意建立連接,則應在響應的報文段中使SYN=1和ACK=1,因此SYN置為1就表示這是一個連接請求或連接接受報文
- 窗口:窗口字段明確指出了現在允許對方發送的數據量。窗口值經常在動態變化。
- 選項:
- 最大報文段長度MSS:
- 以太網Ethernet最大的數據幀是1518字節。以太網幀的幀頭14字節和幀尾CRC校驗4字節(共佔18字節),剩下承載上層協議的地方也就是Data域最大就只剩1500字節. 這個值我們就把它稱之為MTU。
- 為了達到最佳的傳輸效能TCP協議在建立連接的時候通常要協商雙方的MSS值,這個值TCP協議在實現的時候往往用MTU值代替,MSS一般在1420~1460,1460是由1500 - 20(IP頭)- 20/60(TCP頭)計算出的。
- 窗口擴大選項:TCP首部中窗口字段長度是16位,因此最大的窗口大小為64K字節。可以將窗口最大值增大到2^(16+14)-1=2^30-1
- 最大報文段長度MSS:
三次握手
原理
所有TCP連接一開始都要經過三次握手,如下圖所示:
- SYN: 客户端選擇一個隨機序列號x,併發送一個SYN分組,其中可能還包括其他TCP標誌和選項。
- SYN ACK: 服務器給x加1,並選擇自己的一個隨機序列號y,追加自己的標誌和選項,然後返回響應。
- ACK: 客户端給x和y加1併發送握手期間的最後一個ACK分組。
上面的內容我們在書上看過多次,這次我們用wireshark抓包看一下詳情:
本機ip為192.168.1.102,服務器ip為122.51.162
sync
sync ack
ack
三次握手完成後,客户端與服務器之間就可以通信了。
這個啟動通信的過程適用於所有TCP連接,因此對所有使用TCP的應用具有非常大的性能影響,因為每次傳輸應用數據之前,都必須經歷一次完整的往返。
優化
三次握手帶來的延遲使得每創建一個新TCP連接都要付出很大代價。而這也決定了提高TCP應用性能的關鍵,在於想辦法重用連接。
TCP快速打開
TFO(TCP fast open)允許服務器和客户端在連接建立握手階段交換數據,從而使應用節省了一個RTT的時延。
但是TFO會引起一些問題,因此協議要求TCP實現必須默認禁止TFO。需要在某個服務端口上啟用TFO功能的時候需要應用程序顯示啟用。
設置:sysctl -n net.ipv4.tcp_fastopen = 0x203
限制:並不能解決所有問題,它雖然有助於減少三次握手的往返時間,但卻只能在某些情況下有效,如隨同SYN分組一起發送的數據淨荷有最大尺寸限制、只能發送某些類型的HTTP請求,以及由於依賴加密cookie,只能應用於重複的連接。
效果:經過流量分析和網絡模擬,谷歌研究人員發現TFO平均可以降低HTTP事務網絡延遲15%、整個頁面加載時間10%以上。在某些延遲很長的情況下,降低幅度甚至可達40%。
盡最大可能重用已經建立的TCP連接
長鏈接(Keep-Alive)
Keep-Alive,HTTP 1.1 之後默認開啟,指在一個 TCP 連接中可以持續發送多份數據而不會斷開連接
Keep-Alive能夠實現,需要服務端支持:
Httpd守護進程,如nginx需要設置keepalive_timeout
- keepalive_timeout=0:建立tcp連接 + 傳送http請求 + 執行時間 + 傳送http響應 + 關閉tcp連接 + 2MSL
- keepalive_timeout>0:建立tcp連接 + (最後一個響應時間 – 第一個請求時間) + 關閉tcp連接 + 2MSL
另外TCP自身也有Keep-Alive,是檢測TCP連接狀況的保鮮機制
-
net.ipv4.tcpkeepalivetime:表示TCP鏈接在多少秒之後沒有數據報文傳輸啟動探測報文
-
net.ipv4.tcpkeepaliveintvl:前一個探測報文和後一個探測報文之間的時間間隔
-
net.ipv4.tcpkeepaliveprobes:探測的次數
負載均衡
基本原理:客户端(如:ClientA)與負載均衡設備之間進行三次握手併發送 HTTP 請求。負載均衡設備收到請求後,會檢測服務器是否存在空閒的長鏈接,如果不存在,服務器將建立一個新連接。當 HTTP 請求響應完成後,客户端與負載均衡設備協商關閉連接,而負載均衡則保持與服務器之間的這個連接。當有其他客户端(如:ClientB)需要發送 HTTP 請求時,負載均衡設備會直接向服務器之間保持的這個空閒連接發送 HTTP 請求,避免來由於新建 TCP 連接造成的延時和服務器資源耗費。
接收窗口rwnd
流量控制是一種預防發送端過多向接收端發送數據的機制。否則,接收端可能因為忙碌、負載重或緩衝區容量有限而無法處理。為實現流量控制,
TCP連接的每一方都要通吿自己的接收窗口(rwnd),其中包含能夠保存數據的緩衝區空間大小信息。
第一次建立連接時,兩端都會使用自身系統的默認設置來發送rwnd。每個ACK分組都會攜帶相應的最新rwnd值,以便兩端動態調整數據流速,使之適應發送端和接收端的容量及處理能力。
最初的TCP規範分配給通吿窗口大小的字段是16位的,這相當於設定了發送端和接收端窗口的最大值(2的16次方即65 535字節)。為解決這個問題,RFC 1323提供了“TCP窗口縮放”(TCPWindow Scaling)選項,可以把接收窗口大小由65 535字節提高到1G字節!
縮放TCP窗口是在三次握手期間完成的,其中有一個值表示在將來的ACK中左移16位窗口字段的位數。
優化
客户端與服務器之間最大可以傳輸數據量取rwnd和cwnd變量中的最小值。
開啟窗口縮放
開啟窗口縮放,能使接收窗口大小從2^16升級到2^30,可以獲得更好的傳輸性能。
查看:sysctl net.ipv4.tcp_window_scaling
設置:sysctl -w net.ipv4.tcp_window_scaling=1
效果:比起不開啟窗口縮放,能夠充分利用帶寬
這裏講述一下帶寬延遲積。BDP(Bandwidth-delay product,帶寬延遲積)數據鏈路的容量與其端到端延遲的乘積。這個結果就是任意時刻處於在途未確認狀態的最大數據量。
發送端或接收端無論誰被迫頻繁地停止等待之前分組的ACK,都會造成數據缺口,從而必然限制連接的最大吞吐量。
無論實際或通吿的帶寬是多大,窗口過小都會限制連接的吞吐量。
知道往返時間和兩端的實際帶寬也可以計算最優窗口大小。這一次我們假設往返時間為100 ms,發送端的可用帶寬為10 Mbps,接收端則為100 Mbps+。還假設兩端之間沒有網絡擁塞,我們的目標就是充分利用客户端的10 Mbps帶寬:
窗口至少需要122.1 KB才能充分利用10 Mbps帶寬!如果沒“窗口縮放,TCP接收窗口最大隻有64 KB,無論網絡性能有多好,永遠無法充分利用帶寬。
慢啟動與擁塞避免
接收窗口對性能很重要,但擁塞窗口比接收窗口更重要。
客户端與服務器之間最大可以傳輸(未經ACK確認的)數據量取rwnd和cwnd變量中的最小值,而一開始的cwnd很小,通過慢啟動算法不斷增大。
慢啟動和擁塞避免的算法有很多,這裏使用Tahoe版本的TCP版本進行展示,這個也是帶有擁塞控制功能的第一個TCP版本,使用的擁塞避免算法為AIMD(Multiplicative Decrease and Additive Increase,倍減加增)。
- SS:Slow Start,慢啟動階段。TCP 剛開始傳輸的時候,速度是慢慢漲起來的,除非遇到丟包,否則速度會一直指數性增長。
- CA:Congestion Avoid,擁塞避免階段。當擁塞窗口大於ssthresh後, CWND增長速度會下降,不再像 SS 那樣指數增,而是線性增。
- 超時:當數據發送方感知到丟包時,會記錄此時的 CWND,並計算合理的 ssthresh 值,一般ssthresh會置為超時時CWND的一半,發送端會驟降 CWND 到最初始的狀態,當 CWND 重新由小至大增長,直到 sshtresh 時,不再 SS 而是 CA
服務器會有一個默認cwnd初始值。最初,cwnd的值只有1個TCP段。1999年4月,RFC 2581將其增加到了4個TCP段。2013年4月,RFC 6928再次將其提高到10個TCP段。
計算題
問題:cwnd大小達到N所需的時間
解:
下面我們就來看一個例子,假設:
• 客户端和服務器的接收窗口為65535字節(64 KB);
• 初始的擁塞窗口:4段(RFC 2581);
• 往返時間是56 ms(倫敦到紐約);
這個例子説明網絡正常情況下,要達到最大傳輸量,需要224ms。因為慢啟動限制了可用的吞吐量,而這對於小文件傳輸非常不利,因為擁塞控制尚處於slowstart階段,傳輸就完畢了。
優化
確保cwnd大小為10
查看:
-
寫腳本
probe kernel.function("tcp_init_cwnd").return
{
printf("tcp_init_cwnd return: %d\n", $return)
}
-
把服務器內核升級到最新版本(Linux:3.2+)
增大TCP的初始擁塞窗口
設置:在內核中增加一個控制initcwnd的proc參數,/proc/sys/net/ipv4/tcp_initcwnd。該方法對所有的TCP連接有效。
限制:初始擁塞窗口不能設置特別大,否則會導致交換節點的緩衝區被填滿,多出來的分組必須刪掉,相應的主機會在網絡中製造越來越多的數據報副本,使得整個網絡陷入癱瘓。行業內各大cdn廠商都調整過init_cwnd值,普遍取值在10-20之間
效果:
禁用慢啟動重啟
名詞解釋:SSR(Slow-Start Restart,慢啟動重啟)會在連接空閒一定時間後重置連接的擁塞窗口。
原因:在連接空閒的同時,網絡狀況也可能發生了變化,為了避免擁塞,理應將擁塞窗口重置回“安全的”默認值。
查看: sysctl net.ipv4.tcp_slow_start_after_idle
設置: sysctl -w net.ipv4.tcp_slow_start_after_idle=0
效果:對於那些會出現突發空閒的長週期TCP連接(比如HTTP的keep-alive連接)有很大的影響,具體提升性能根據網絡性能和數據量大小不同而不同
更改擁塞避免算法
擁塞控制算法對TCP性能影響很大,除了上面提到的AIMD算法,還有眾多其他算法。
PRR(Proportional Rate Reduction,比例降速)就是RFC 6937規定的一個新算法,其目標就是改進丟包後的恢復速度。
效果:根據谷歌的測量,實現新算法後,因丟包造成的平均連接延遲減少了3%~10%。
設置:升級服務器。PRR現在是Linux 3.2+內核默認的擁塞預防算法。
減少傳輸數據量
方案:
-
減少傳輸宂餘數據
-
壓縮要傳輸的數據:gzip、protobuf、webp等
-
再快也快不過什麼也不用發送
減少往返時間
方案:
- 多機房部署服務器
- 使用CDN
隊首阻塞
隊首(HOL,Head of Line)阻塞:如果中途有一個分組沒能到達接收端,那麼後續分組必須保存在接收端的TCP緩衝區,等待丟失的分組重發併到達接收端。這一切都發生在TCP層,應用程序對TCP重發和緩衝區中排隊的分組一無所知,必須等待分組全部到達才能訪問數據。在此之前,應用程序只能在通過套接字讀數據時感覺到延遲交付。
優點:應用程序不用關心分組重排和重組,從而讓代碼保持簡潔。
缺點:分組到達時間會存在無法預知的延遲變化。這個時間變化通常被稱為抖動,也是影響應用程序性能的一個主要因素。
優化
UDP
無法優化,這是TCP的基礎邏輯,目前沒有優化的可能。
無需按序交付數據或能夠處理分組丟失的應用程序,以及對延遲或抖動要求很高的應用程序,最好選擇UDP等協議。
一般的音頻或者遊戲等應用,可以選擇使用UDP協議
總結
針對TCP的優化建議
- 服務器配置調優
- 服務器使用最新版本
- 增大TCP的初始擁塞窗口
- 慢啟動重啟
- 窗口縮放(RFC 1323)
- TCP快速打開
- 可用ss命令或sysctl -a | grep tcp查看相關配置
- 應用程序行為調優
- 再快也快不過什麼也不用發送,能少發就少發
- 我們不能讓數據傳輸得更快,但可以讓它們傳輸的距離更短
- 重用TCP連接是提升性能的關鍵
- 性能檢查清單
- 把服務器內核升級到最新版本(Linux:3.2+);
- 確保cwnd大小為10;
- 禁用空閒後的慢啟動;
- 確保啟動窗口縮放;
- 減少傳輸宂餘數據;
- 壓縮要傳輸的數據;
- 把服務器放到離用户近的地方以減少往返時間;
- 盡最大可能重用已經建立的TCP連接。
資料
- Web權威性能指南
- TCP 滑動窗口 與窗口縮放因子
- TCP的滑動窗口與擁塞窗口
- Web 性能優化 - TCP
- 就是要你懂TCP--性能優化大全
- TCP報文段的首部格式
- TCP Socket通信詳細過程
- TCP三次握手以及SYN,ACK,Seq的不詳細解釋
- Wireshark數據包分析
- Wireshark網絡分析就這麼簡單
- TCP-fastopen(TFO)
- TCP系列40—擁塞控制—3、慢啟動和擁塞避免概述
- TCP系列41—擁塞控制—4、Linux中的慢啟動和擁塞避免(一)
- HTTP Keep-Alive是什麼?如何工作?(理解TCP生命週期)
- nginx - KeepAlive詳細解釋
最後
大家如果喜歡我的文章,可以關注我的公眾號(程序員麻辣燙)
往期文章回顧:
技術
- TCP性能優化
- 限流實現1
- Redis實現分佈式鎖
- Golang源碼BUG追查
- 事務原子性、一致性、持久性的實現原理
- CDN請求過程詳解
- 記博客服務被壓垮的歷程
- 常用緩存技巧
- 如何高效對接第三方支付
- Gin框架簡潔版
- InnoDB鎖與事務簡析
讀書筆記
思考