大家好,我是小富~
前言
之前有個小夥伴在技術交流羣裏諮詢過一個問題,我當時還給提供了點排查思路,是個典型的八股文轉實戰分析的案例,我覺得挺有意思,趁着中午休息簡單整理出來和大家分享下,有不嚴謹的地方歡迎大家指出。
問題分析
我們先來看看他的問題,下邊是他在羣裏對這個問題的描述,我大致的總結了一下。
他們有很多的 IOT 設備與服務端建立連接,當增加設備併發請求變多,TCP
連接數在接近1024個時,可用TCP
連接數會降到200左右並且無法建立新連接,而且分析應用服務的GC和內存情況均未發現異常。
從他的描述中我提取了幾個關鍵值,1024
、200
、無法建立新連接
。
看到這幾個數值,直覺告訴我大概率是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
[[email protected] ~]# 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
[[email protected] ~]# 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
[[email protected] ~]# ss -lnt | grep 8888
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 200 100 :::8888 :::*
如果發生了全連接隊列溢出,我們可以通過netstat -s
命令查詢溢出的累計次數,若這個times
持續的增長,那就說明正在發生溢出。
[[email protected] ~]# netstat -s | grep overflowed
7102 times the listen queue of a socket overflowed #全連接隊列溢出的次數
拒絕策略
在全連接隊列已滿的情況,Linux提供了不同的策略去處理後續的請求,默認是直接丟棄,也可以通過tcp_abort_on_overflow
配置來更改策略,其值 0 和 1 表示不同的策略,默認配置 0。
# 查看策略
[[email protected] ~]# 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
配置。
[[email protected] core]# sysctl -a | grep net.core.somaxconn
net.core.somaxconn = 128
[[email protected] core]# sysctl -w net.core.somaxconn=1024
net.core.somaxconn = 1024
[[email protected] core]# sysctl -a | grep net.core.somaxconn
net.core.somaxconn = 1024
如果服務端處理請求的速度跟不上連接請求的到達速度,隊列可能會被快速填滿,導致連接超時或丟失。應該及時增加隊列大小,以避免連接請求被拒絕或超時。
增大該參數的值雖然可以增加隊列的容量,但是也會佔用更多的內存資源。一般來說,建議將全連接隊列的大小設置爲服務器處理能力的兩倍左右。
半連接隊列
隊列信息
上邊TCP三次握手過程中,我們知道服務端SYN_RECV
狀態的TCP連接存放在半連接隊列,所以直接執行如下命令查看半連接隊列長度。
[roo[email protected] ~] netstat -natp | grep SYN_RECV | wc -l
1111
隊列溢出
半連接隊列溢出最常見的場景就是,客戶端沒有及時向服務端回ACK
,使得服務端有大量處於SYN_RECV
狀態的連接,導致半連接隊列被佔滿,得不到ACK
響應半連接隊列中的 TCP 連接無法移動全連接隊列,以至於後續的SYN
請求無法創建。這也是一種常見的DDos攻擊方式。
查看TCP半連接隊列溢出情況,可以執行netstat -s
命令,SYNs to LISTEN
前的數值表示溢出的次數,如果反覆查詢幾次數值持續增加,那就說明半連接隊列正在溢出。
[[email protected] ~]# 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
來調大半連接隊列長度。
[[email protected] ~]# echo 2048 > /proc/sys/net/ipv4/tcp_max_syn_backlog
爲什麼建議
看完上邊對兩個隊列的粗略介紹,相信大家也能大致明白,爲啥我會直接建議他去調大隊列了。
因爲從他的描述中提到了兩個關鍵值,TCP連接數增加至1024個時,可用連接數會降至200以內,一般centos
系統全連接隊列長度一般默認 128,半連接隊列默認長度 1024。所以隊列溢出可以作爲第一嫌疑對象。
全連接隊列默認大小 128
[[email protected] core]# sysctl -a | grep net.core.somaxconn
net.core.somaxconn = 128
半連接隊列默認大小 1024
[[email protected] ~]# cat /proc/sys/net/ipv4/tcp_max_syn_backlog
1024
總結
簡單分享了一點TCP全連接隊列、半連接隊列的相關內容,講的比較淺顯,如果有不嚴謹的地方歡迎留言指正,畢竟還是個老菜鳥。
全連接隊列、半連接隊列溢出是比較常見,但又容易被忽視的問題,往往上線會遺忘這兩個配置,一旦發生溢出,從CPU
、線程狀態
、內存
看起來都比較正常,偏偏連接數上不去。
定期對系統壓測是可以暴露出更多問題的,不過話又說回來,就像我和小夥伴聊的一樣,即便測試環境程序跑的在穩定,到了線上環境也總會出現各種奇奇怪怪的問題。
我是小富,下期見~
技術交流,公衆號:程序員小富
本文收錄在 Springboot-Notebook 面試錦集