TCP 三次握手,給我長臉了噢

大家好,我是小富~

前言

之前有個小夥伴在技術交流羣裏諮詢過一個問題,我當時還給提供了點排查思路,是個典型的八股文轉實戰分析的案例,我覺得挺有意思,趁着中午休息簡單整理出來和大家分享下,有不嚴謹的地方歡迎大家指出。

問題分析

我們先來看看他的問題,下邊是他在羣裏對這個問題的描述,我大致的總結了一下。

他們有很多的 IOT 設備與服務端建立連接,當增加設備併發請求變多,TCP連接數在接近1024個時,可用TCP連接數會降到200左右並且無法建立新連接,而且分析應用服務的GC和內存情況均未發現異常。

從他的描述中我提取了幾個關鍵值,1024200無法建立新連接

看到這幾個數值,直覺告訴我大概率是TCP請求溢出了,我給的建議是先直接調大全連接隊列半連接隊列的閥值試一下效果。

那爲什麼我會給出這個建議?

半連接隊列和全連接隊列又是個啥玩意?

弄明白這些回顧下TCP的三次握手流程,一切就迎刃而解了~

回顧TCP

TCP三次握手,熟悉吧,面試八股裏經常全文背誦的題目。

話不多說先上一張圖,看明白TCP連接的整個過程。

TCP三次握手

第一步:客戶端發起SYN_SEND連接請求,服務端收到客戶端發起的SYN請求後,會先將連接請求放入半連接隊列;

第二步:服務端向客戶端響應SYN+ACK

第三步:客戶端會返回ACK確認,服務端收到第三次握手的 ACK 後標識連接成功。如果這時全連接隊列沒滿,內核會把連接從半連接隊列移除,創建新的連接並將其添加到全連接隊列,等待客戶端調用accept()方法將連接取出來使用;

TCP協議三次握手的過程,Linux內核維護了兩個隊列,SYN半連接隊列和accepet全連接隊列。即然叫隊列,那就存在隊列被壓滿的時候,這種情況我們稱之爲隊列溢出

當半連接隊列或全連接隊列滿了時,服務器都無法接收新的連接請求,從而導致客戶端無法建立連接。

全連接隊列

隊列信息

全連接隊列溢出時,首先要查看全連接隊列的狀態,服務端通常使用 ss 命令即可查看,ss 命令獲取的數據又分爲 LISTEN狀態 和 非LISTEN兩種狀態下,通常只看LISTEN狀態數據就可以。

LISTEN狀態

Recv-Q:當前全連接隊列的大小,表示上圖中已完成三次握手等待可用的 TCP 連接個數;

Send-Q:全連接最大隊列長度,如上監聽8888端口的TCP連接最大全連接長度爲128;

# -l 顯示正在Listener 的socket
# -n 不解析服務名稱
# -t 只顯示tcp
[root@VM-4-14-centos ~]#  ss -lnt | grep 8888
State  Recv-Q Send-Q  Local Address:Port   Peer Address:Port
LISTEN     0   100       :::8888                  :::*               

非LISTEN 狀態下Recv-Q、Send-Q字段含義有所不同

Recv-Q:已收到但未被應用進程讀取的字節數;

Send-Q:已發送但未收到確認的字節數;

# -n 不解析服務名稱
# -t 只顯示tcp
[root@VM-4-14-centos ~]#  ss -nt | grep 8888
State  Recv-Q Send-Q  Local Address:Port   Peer Address:Port
ESTAB     0   100       :::8888                  :::*               

隊列溢出

一般在請求量過大,全連接隊列設置過小會發生全連接隊列溢出,也就是LISTEN狀態下 Send-Q < Recv-Q 的情況。接收到的請求數大於TCP全連接隊列的最大長度,後續的請求將被服務端丟棄,客戶端無法創建新連接

# -l 顯示正在Listener 的socket
# -n 不解析服務名稱
# -t 只顯示tcp
[root@VM-4-14-centos ~]#  ss -lnt | grep 8888
State  Recv-Q Send-Q  Local Address:Port   Peer Address:Port
LISTEN     200   100       :::8888                  :::*               

如果發生了全連接隊列溢出,我們可以通過netstat -s命令查詢溢出的累計次數,若這個times持續的增長,那就說明正在發生溢出。

[root@VM-4-14-centos ~]# netstat -s | grep overflowed
  7102 times the listen queue of a socket overflowed #全連接隊列溢出的次數

拒絕策略

在全連接隊列已滿的情況,Linux提供了不同的策略去處理後續的請求,默認是直接丟棄,也可以通過tcp_abort_on_overflow配置來更改策略,其值 0 和 1 表示不同的策略,默認配置 0。

# 查看策略
[root@VM-4-14-centos ~]# cat /proc/sys/net/ipv4/tcp_abort_on_overflow
0

tcp_abort_on_overflow = 0:全連接隊列已滿時,服務端直接丟棄客戶端發送的 ACK,此時服務端仍然是 SYN_RCVD 狀態,在該狀態下服務端會重試幾次向客戶端推送 SYN + ACK

重試次數取決於tcp_synack_retries配置,重試次數超過此配置後後,服務端不在重傳,此時客戶端發送數據,服務端直接向客戶端回覆RST復位報文,告知客戶端本次建立連接已失敗。

RST: 連接 reset 重置消息,用於連接的異常關閉。常用場景例如:服務端接收不存在端口的連接請求;客戶端或者服務端異常,無法繼續正常的連接處理,發送 RST 終止連接操作;長期未收到對方確認報文,經過一定時間或者重傳嘗試後,發送 RST 終止連接。

[root@VM-4-14-centos ~]# cat /proc/sys/net/ipv4/tcp_synack_retries
0

tcp_abort_on_overflow = 1:全連接隊列已滿時,服務端直接丟棄客戶端發送的 ACK,直接向客戶端回覆RST復位報文,告知客戶端本次連接終止,客戶端會報錯提示connection reset by peer

隊列調整

解決全連接隊列溢出我們可以通過調整TCP參數來控制全連接隊列的大小,全連接隊列的大小取決於 backlog 和 somaxconn 兩個參數。

這裏需要注意一下,兩個參數要同時調整,因爲取的兩者中最小值min(backlog,somaxconn),經常發生只挑調大其中一個另一個值很小導致不生效的情況。

backlog 是在socket 創建的時候 Listen() 函數傳入的參數,例如我們也可以在 Nginx 配置中指定 backlog 的大小。

server {
   listen 8888 default backlog = 200
   server_name fire100.top
   .....
}

somaxconn 是個 OS 級別的參數,默認值是 128,可以通過修改 net.core.somaxconn 配置。

[root@localhost core]# sysctl -a | grep net.core.somaxconn
net.core.somaxconn = 128
[root@localhost core]# sysctl -w net.core.somaxconn=1024
net.core.somaxconn = 1024
[root@localhost core]# sysctl -a | grep net.core.somaxconn
net.core.somaxconn = 1024

如果服務端處理請求的速度跟不上連接請求的到達速度,隊列可能會被快速填滿,導致連接超時或丟失。應該及時增加隊列大小,以避免連接請求被拒絕或超時。

增大該參數的值雖然可以增加隊列的容量,但是也會佔用更多的內存資源。一般來說,建議將全連接隊列的大小設置爲服務器處理能力的兩倍左右

半連接隊列

隊列信息

上邊TCP三次握手過程中,我們知道服務端SYN_RECV狀態的TCP連接存放在半連接隊列,所以直接執行如下命令查看半連接隊列長度。

[root@VM-4-14-centos ~]  netstat -natp | grep SYN_RECV | wc -l
1111

隊列溢出

半連接隊列溢出最常見的場景就是,客戶端沒有及時向服務端回ACK,使得服務端有大量處於SYN_RECV狀態的連接,導致半連接隊列被佔滿,得不到ACK響應半連接隊列中的 TCP 連接無法移動全連接隊列,以至於後續的SYN請求無法創建。這也是一種常見的DDos攻擊方式。

查看TCP半連接隊列溢出情況,可以執行netstat -s命令,SYNs to LISTEN前的數值表示溢出的次數,如果反覆查詢幾次數值持續增加,那就說明半連接隊列正在溢出。

[root@VM-4-14-centos ~]# netstat -s | egrep “listen|LISTEN”
1606 times the listen queue of a socket overflowed
1606 SYNs to LISTEN sockets ignored

隊列調整

可以修改 Linux 內核配置 /proc/sys/net/ipv4/tcp_max_syn_backlog來調大半連接隊列長度。

[root@VM-4-14-centos ~]# echo 2048 > /proc/sys/net/ipv4/tcp_max_syn_backlog

爲什麼建議

看完上邊對兩個隊列的粗略介紹,相信大家也能大致明白,爲啥我會直接建議他去調大隊列了。

因爲從他的描述中提到了兩個關鍵值,TCP連接數增加至1024個時,可用連接數會降至200以內,一般centos系統全連接隊列長度一般默認 128,半連接隊列默認長度 1024。所以隊列溢出可以作爲第一嫌疑對象。

全連接隊列默認大小 128

[root@localhost core]# sysctl -a | grep net.core.somaxconn
net.core.somaxconn = 128

半連接隊列默認大小 1024

[root@iZ2ze3ifc44ezdiif8jhf7Z ~]# cat /proc/sys/net/ipv4/tcp_max_syn_backlog
1024

總結

簡單分享了一點TCP全連接隊列、半連接隊列的相關內容,講的比較淺顯,如果有不嚴謹的地方歡迎留言指正,畢竟還是個老菜鳥。

全連接隊列、半連接隊列溢出是比較常見,但又容易被忽視的問題,往往上線會遺忘這兩個配置,一旦發生溢出,從CPU線程狀態內存看起來都比較正常,偏偏連接數上不去。

定期對系統壓測是可以暴露出更多問題的,不過話又說回來,就像我和小夥伴聊的一樣,即便測試環境程序跑的在穩定,到了線上環境也總會出現各種奇奇怪怪的問題。

我是小富,下期見~

技術交流,公衆號:程序員小富

本文收錄在 Springboot-Notebook 面試錦集

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章