本文轉載自微信公眾號「云巔論劍」,作者黃剛。轉載本文請聯系云巔論劍公眾號。
前言
某次大促值班 ing,對系統穩定性有著充分信心、心態穩如老狗的筆者突然收到上游反饋有萬分幾的概率請求我們 endpoint 會出現 Connection timeout 。此時系統側的 apiserver 集群水位在 40%,離極限水位還有著很大的距離,當時通過緊急擴容 apiserver 集群后錯誤率降為了 0。事后進行了詳細的問題排查,定位分析到問題根因出現在系統連接隊列被打滿導致,之前筆者對 TCP 半連接隊列、全連接隊列不太了解,只依稀記得 《TCP/IP 詳解》中好像有好像提到過這兩個名詞。
目前網上相關資料都比較零散,并且有些是過時或錯誤的結論,筆者在調查問題時踩了很多坑。痛定思痛,筆者查閱了大量資料并做了眾多實驗進行驗證,梳理了這篇 TCP 半連接隊列、全連接詳解,當你細心閱讀完這篇文章后相信你可以對 TCP 半連接隊列、全連接隊列有更充分的認識。
本篇文章將結合理論知識、內核代碼、操作實驗為你呈現如下內容:
- 半連接隊列、全連接隊列介紹
- 常用命令介紹
- 全連接隊列實戰 —— 最大長度控制、全連接隊列溢出實驗、實驗結果分析...
- 半連接隊列實戰 —— 最大長度控制、半連接隊列溢出實驗、實驗結果分析...
- ...
半連接隊列、全連接隊列
在 TCP 三次握手的過程中,Linux 內核會維護兩個隊列,分別是:
- 半連接隊列 (SYN Queue)
- 全連接隊列 (Accept Queue)
正常的 TCP 三次握手過程:
1、Client 端向 Server 端發送 SYN 發起握手,Client 端進入 SYN_SENT 狀態
2、Server 端收到 Client 端的 SYN 請求后,Server 端進入 SYN_RECV 狀態,此時內核會將連接存儲到半連接隊列(SYN Queue),并向 Client 端回復 SYN+ACK
3、Client 端收到 Server 端的 SYN+ACK 后,Client 端回復 ACK 并進入 ESTABLISHED 狀態
4、Server 端收到 Client 端的 ACK 后,內核將連接從半連接隊列(SYN Queue)中取出,添加到全連接隊列(Accept Queue),Server 端進入 ESTABLISHED 狀態
5、Server 端應用進程調用 accept 函數時,將連接從全連接隊列(Accept Queue)中取出
半連接隊列和全連接隊列都有長度大小限制,超過限制時內核會將連接 Drop 丟棄或者返回 RST 包。
相關指標查看
ss 命令
通過 ss 命令可以查看到全連接隊列的信息
- #-n不解析服務名稱
- #-t只顯示tcpsockets
- #-l顯示正在監聽(LISTEN)的sockets
- $ss-lnt
- StateRecv-QSend-QLocalAddress:PortPeerAddress:Port
- LISTEN0128[::]:2380[::]:*
- LISTEN0128[::]:80[::]:*
- LISTEN0128[::]:8080[::]:*
- LISTEN0128[::]:8090[::]:*
- $ss-nt
- StateRecv-QSend-QLocalAddress:PortPeerAddress:Port
- ESTAB00[::ffff:33.9.95.134]:80[::ffff:33.51.103.59]:47452
- ESTAB0536[::ffff:33.9.95.134]:80[::ffff:33.43.108.144]:37656
- ESTAB00[::ffff:33.9.95.134]:80[::ffff:33.51.103.59]:38130
- ESTAB0536[::ffff:33.9.95.134]:80[::ffff:33.51.103.59]:38280
- ESTAB00[::ffff:33.9.95.134]:80[::
對于 LISTEN 狀態的 socket
- Recv-Q:當前全連接隊列的大小,即已完成三次握手等待應用程序 accept() 的 TCP 鏈接
- Send-Q:全連接隊列的最大長度,即全連接隊列的大小
對于非 LISTEN 狀態的 socket
- Recv-Q:已收到但未被應用程序讀取的字節數
- Send-Q:已發送但未收到確認的字節數
相關內核代碼:
- //https://github.com/torvalds/linux/blob/master/net/ipv4/tcp_diag.c
- staticvoidtcp_diag_get_info(structsock*sk,structinet_diag_msg*r,
- void*_info)
- {
- structtcp_info*info=_info;
- if(inet_sk_state_load(sk)==TCP_LISTEN){//socket狀態是LISTEN時
- r->idiag_rqueue=READ_ONCE(sk->sk_ack_backlog);//當前全連接隊列大小
- r->idiag_wqueue=READ_ONCE(sk->sk_max_ack_backlog);//全連接隊列最大長度
- }elseif(sk->sk_type==SOCK_STREAM){//socket狀態不是LISTEN時
- conststructtcp_sock*tp=tcp_sk(sk);
- r->idiag_rqueue=max_t(int,READ_ONCE(tp->rcv_nxt)-
- READ_ONCE(tp->copied_seq),0);//已收到但未被應用程序讀取的字節數
- r->idiag_wqueue=READ_ONCE(tp->write_seq)-tp->snd_una;//已發送但未收到確認的字節數
- }
- if(info)
- tcp_get_info(sk,info);
- }
netstat 命令
通過 netstat -s 命令可以查看 TCP 半連接隊列、全連接隊列的溢出情況
- $netstat-s|grep-i"listen"
- 189088timesthelistenqueueofasocketoverflowed
- 30140232SYNstoLISTENsocketsdropped
上面輸出的數值是累計值,分別表示有多少 TCP socket 鏈接因為全連接隊列、半連接隊列滿了而被丟棄
- 189088 times the listen queue of a socket overflowed 代表有 189088 次全連接隊列溢出
- 30140232 SYNs to LISTEN sockets dropped 代表有 30140232 次半連接隊列溢出
在排查線上問題時,如果一段時間內相關數值一直在上升,則表明半連接隊列、全連接隊列有溢出情況
實戰 —— 全連接隊列
全連接隊列最大長度控制
TCP 全連接隊列的最大長度由 min(somaxconn, backlog) 控制,其中:
- somaxconn 是 Linux 內核參數,由 /proc/sys/net/core/somaxconn 指定
- backlog 是 TCP 協議中 listen 函數的參數之一,即 int listen(int sockfd, int backlog) 函數中的 backlog 大小。在 Golang 中,listen 的 backlog 參數使用的是 /proc/sys/net/core/somaxconn 文件中的值。
相關內核代碼:
- //https://github.com/torvalds/linux/blob/master/net/socket.c
- /*
- *Performalisten.Basically,weallowtheprotocoltodoanything
- *necessaryforalisten,andifthatworks,wemarkthesocketas
- *readyforlistening.
- */
- int__sys_listen(intfd,intbacklog)
- {
- structsocket*sock;
- interr,fput_needed;
- intsomaxconn;
- sock=sockfd_lookup_light(fd,&err,&fput_needed);
- if(sock){
- somaxconn=sock_net(sock->sk)->core.sysctl_somaxconn;///proc/sys/net/core/somaxconn
- if((unsignedint)backlog>somaxconn)
- backlog=somaxconn;//TCP全連接隊列最大長度min(somaxconn,backlog)
- err=security_socket_listen(sock,backlog);
- if(!err)
- err=sock->ops->listen(sock,backlog);
- fput_light(sock->file,fput_needed);
- }
- returnerr;
- }
實驗
服務端 server 代碼
- packagemain
- import(
- "log"
- "net"
- "time"
- )
- funcmain(){
- l,err:=net.Listen("tcp",":8888")
- iferr!=nil{
- log.Printf("failedtolistendueto%v",err)
- }
- deferl.Close()
- log.Println("listen:8888success")
- for{
- time.Sleep(time.Second*100)
- }
- }
在測試環境查看 somaxconn 的值為 128
- $cat/proc/sys/net/core/somaxconn
- 128
啟動服務端,通過 ss -lnt | grep :8888 確認全連接隊列大小
- LISTEN0128[::]:8888[::]:*
全連接隊列最大長度為 128
現在更新 somaxconn 值為 1024,再重新啟動服務端。
1、更新 /etc/sysctl.conf 文件,該文件為內核參數配置文件
a.新增一行 net.core.somaxconn=1024
2、執行 sysctl -p 使配置生效
- $sudosysctl-p
- net.core.somaxconn=1024
3、檢查 /proc/sys/net/core/somaxconn 文件,確認 somaxconn 為更新后的 1024
- $cat/proc/sys/net/core/somaxconn
- 1024
重新啟動服務端, 通過 ss -lnt | grep :8888 確認全連接隊列大小
- $ss-lnt|grep8888
- LISTEN01024[::]:8888[::]:*
可以看到,現在全鏈接隊列最大長度為 1024,成功更新。
全連接隊列溢出
下面來驗證下全連接隊列溢出會發生什么情況,可以通過讓服務端應用只負責 Listen 對應端口而不執行 accept() TCP 連接,使 TCP 全連接隊列溢出。
實驗物料
服務端 server 代碼
- //server端監聽8888tcp端口
- packagemain
- import(
- "log"
- "net"
- "time"
- )
- funcmain(){
- l,err:=net.Listen("tcp",":8888")
- iferr!=nil{
- log.Printf("failedtolistendueto%v",err)
- }
- deferl.Close()
- log.Println("listen:8888success")
- for{
- time.Sleep(time.Second*100)
- }
- }
客戶端 client 代碼
- //client端并發請求10次server端,成功建立tcp連接后向server端發送數據
- packagemain
- import(
- "context"
- "log"
- "net"
- "os"
- "os/signal"
- "sync"
- "syscall"
- "time"
- )
- varwgsync.WaitGroup
- funcestablishConn(ctxcontext.Context,iint){
- deferwg.Done()
- conn,err:=net.DialTimeout("tcp",":8888",time.Second*5)
- iferr!=nil{
- log.Printf("%d,dialerror:%v",i,err)
- return
- }
- log.Printf("%d,dialsuccess",i)
- _,err=conn.Write([]byte("helloworld"))
- iferr!=nil{
- log.Printf("%d,senderror:%v",i,err)
- return
- }
- select{
- case<-ctx.Done():
- log.Printf("%d,dailclose",i)
- }
- }
- funcmain(){
- ctx,cancel:=context.WithCancel(context.Background())
- fori:=0;i<10;i++{
- wg.Add(1)
- goestablishConn(ctx,i)
- }
- gofunc(){
- sc:=make(chanos.Signal,1)
- signal.Notify(sc,syscall.SIGINT)
- select{
- case<-sc:
- cancel()
- }
- }()
- wg.Wait()
- log.Printf("clientexit")
- }
為了方便實驗,將 somaxconn 全連接隊列最大長度更新為 5:
1、更新 /etc/sysctl.conf 文件,將 net.core.somaxconn 更新為 5
2、執行 sysctl -p 使配置生效
- $sudosysctl-p
- net.core.somaxconn=5
實驗結果
客戶端日志輸出
- 2021/10/1117:24:488,dialsuccess
- 2021/10/1117:24:483,dialsuccess
- 2021/10/1117:24:484,dialsuccess
- 2021/10/1117:24:486,dialsuccess
- 2021/10/1117:24:485,dialsuccess
- 2021/10/1117:24:482,dialsuccess
- 2021/10/1117:24:481,dialsuccess
- 2021/10/1117:24:480,dialsuccess
- 2021/10/1117:24:487,dialsuccess
- 2021/10/1117:24:539,dialerror:dialtcp33.9.192.157:8888:i/otimeout
客戶端 socket 情況
- tcp0033.9.192.155:4037233.9.192.157:8888ESTABLISHED
- tcp0033.9.192.155:4037633.9.192.157:8888ESTABLISHED
- tcp0033.9.192.155:4037033.9.192.157:8888ESTABLISHED
- tcp0033.9.192.155:4036633.9.192.157:8888ESTABLISHED
- tcp0033.9.192.155:4037433.9.192.157:8888ESTABLISHED
- tcp0033.9.192.155:4036833.9.192.157:8888ESTABLISHED
服務端 socket 情況
- tcp611033.9.192.157:888833.9.192.155:40376ESTABLISHED
- tcp611033.9.192.157:888833.9.192.155:40370ESTABLISHED
- tcp611033.9.192.157:888833.9.192.155:40368ESTABLISHED
- tcp611033.9.192.157:888833.9.192.155:40372ESTABLISHED
- tcp611033.9.192.157:888833.9.192.155:40374ESTABLISHED
- tcp611033.9.192.157:888833.9.192.155:40366ESTABLISHED
- tcpLISTEN65[::]:8888[::]:*users:(("main",pid=84244,fd=3))
抓包結果
對客戶端、服務端抓包后,發現出現了三種情況,分別是:
- client 成功與 server 端建立 tcp socket 連接,發送數據成功
- client 認為成功與 server 端建立 tcp socket 連接,發送數據失敗,一直在 RETRY;server 端認為 tcp 連接未建立,一直在發送 SYN+ACK
- client 向 server 發送 SYN 未得到響應,一直在 RETRY
全連接隊列實驗結果分析
上述實驗結果出現了三種情況,我們分別對抓包內容進行分析
情況一:Client 成功與 Server 端建立 tcp socket 鏈接,發送數據成功
上圖可以看到如下請求:
- Client 端向 Server 端發送 SYN 發起握手
- Server 端收到 Client 端 SYN 后,向 Client 端回復 SYN+ACK,socket 連接存儲到半連接隊列(SYN Queue)
- Client 端收到 Server 端 SYN+ACK 后,向 Server 端回復 ACK,Client 端進入 ESTABLISHED 狀態
- Server 端收到 Client 端 ACK 后,進入 ESTABLISHED 狀態,socket 連接存儲到全連接隊列(Accept Queue)
- Client 端向 Server 端發送數據 [PSH, ACK],Server 端確認接收到數據 [ACK]
這種情況就是正常的請求,即全連接隊列、半連接隊列未滿,client 成功與 server 建立了 tcp 鏈接,并成功發送數據。
情況二:Client 認為成功與 Server 端建立 tcp socket 連接,后續發送數據失敗,持續 RETRY;Server 端認為 TCP 連接未建立,一直在發送SYN+ACK
上圖可以看到如下請求:
- Client 端向 Server 端發送 SYN 發起握手
- Server 端收到 Client 端 SYN 后,向 Client 端回復 SYN+ACK,socket 連接存儲到半連接隊列(SYN Queue)
- Client 端收到 Server 端 SYN+ACK 后,向 Server 端回復 ACK,Client 端進入 ESTABLISHED狀態(重要:此時僅僅是 Client 端認為 tcp 連接建立成功)
- 由于 Client 端認為 TCP 連接已經建立完成,所以向 Server 端發送數據 [PSH,ACK],但是一直未收到 Server 端的確認 ACK,所以一直在 RETRY
- Server 端一直在 RETRY 發送 SYN+ACK
為什么會出現上述情況?Server 端為什么一直在 RETRY 發送 SYN+ACK?Server 端不是已經收到了 Client 端的 ACK 確認了嗎?
上述情況是由于 Server 端 socket 連接進入了半連接隊列,在收到 Client 端 ACK 后,本應將 socket 連接存儲到全連接隊列,但是全連接隊列已滿,所以 Server 端 DROP 了該 ACK 請求。
之所以 Server 端一直在 RETRY 發送 SYN+ACK,是因為 DROP 了 client 端的 ACK 請求,所以 socket 連接仍舊在半連接隊列中,等待 Client 端回復 ACK。
tcp_abort_on_overflow 參數控制
全連接隊列滿DROP 請求是默認行為,可以通過設置 /proc/sys/net/ipv4/tcp_abort_on_overflow 使 Server 端在全連接隊列滿時,向 Client 端發送 RST 報文。
tcp_abort_on_overflow 有兩種可選值:
- 0:如果全連接隊列滿了,Server 端 DROP Client 端回復的 ACK
- 1:如果全連接隊列滿了,Server 端向 Client 端發送 RST 報文,終止 TCP socket 鏈接 (TODO:后續有時間補充下該實驗)
為什么實驗結果中當前全連接隊列大小 > 全連接隊列最大長度配置?
上述結果中可以看到 Listen 狀態的 socket 鏈接:
- Recv-Q 當前全連接隊列的大小是 6
- Send-Q 全連接隊列最大長度是 5
- StateRecv-QSend-QLocalAddress:PortPeerAddress:Port
- LISTEN65[::]:8888[::]:*
為什么全連接隊列大小 > 全連接隊列最大長度配置呢?
經過多次實驗發現,能夠進入全連接隊列的 Socket 最大數量始終比配置的全連接隊列最大長度 + 1。
結合其他文章以及內核代碼,發現內核在判斷全連接隊列是否滿的情況下,使用的是 > 而非 >= (具體是為什么沒有找到相關資源 : ) )。
相關內核代碼:
- /*Note:Ifyouthinkthetestshouldbe:
- *returnREAD_ONCE(sk->sk_ack_backlog)>=READ_ONCE(sk->sk_max_ack_backlog);
- *Thenpleasetakealookatcommit64a146513f8f("[NET]:Revertincorrectacceptqueuebacklogchanges.")
- */
- staticinlineboolsk_acceptq_is_full(conststructsock*sk)
- {
- returnREAD_ONCE(sk->sk_ack_backlog)>READ_ONCE(sk->sk_max_ack_backlog);
- }
情況三:Client 向 Server 發送 SYN 未得到相應,一直在 RETRY
圖片上圖可以看到如下請求:
- Client 端向 Server 端發送 SYN 發起握手,未得到 Server 回應,一直在 RETRY
(這種情況涉及到半連接隊列,這里先給上述情況發生的原因結論,具體內容將在下文半連接隊列中展開。)
發生上述情況的原因由以下兩方面導致:
1、開啟了 /proc/sys/net/ipv4/tcp_syncookies 功能
2、全連接隊列滿了
實戰 —— 半連接隊列
半連接隊列最大長度控制
翻閱了很多博文,查找關于半連接隊列最大長度控制的相關內容,大多含糊其辭或不準確,經過不懈努力,最終找到了比較確切的內容(相關博文鏈接在附錄中)。
很多博文中說半連接隊列最大長度由 /proc/sys/net/ipv4/tcp_max_syn_backlog 參數指定,實際上只有在 linux 內核版本小于 2.6.20 時,半連接隊列才等于 backlog 的大小。
這塊的源碼比較復雜,這里給一下大體的計算方式,詳細的內容可以參考附錄中的相關博文。半連接隊列長度的計算過程:
- backlog=min(somaxconn,backlog)
- nr_table_entries=backlog
- nr_table_entries=min(backlog,sysctl_max_syn_backlog)
- nr_table_entries=max(nr_table_entries,8)
- //roundup_pow_of_two:將參數向上取整到最小的2^n,注意這里存在一個+1
- nr_table_entries=roundup_pow_of_two(nr_table_entries+1)
- max_qlen_log=max(3,log2(nr_table_entries))
- max_queue_length=2^max_qlen_log
可以看到,半連接隊列的長度由三個參數指定:
- 調用 listen 時,傳入的 backlog
- /proc/sys/net/core/somaxconn 默認值為 128
- /proc/sys/net/ipv4/tcp_max_syn_backlog 默認值為 1024
我們假設 listen 傳入的 backlog = 128 (Golang 中調用 listen 時傳遞的 backlog 參數使用的是 /proc/sys/net/core/somaxconn),其他配置采用默認值,來計算下半連接隊列的最大長度
- backlog=min(somaxconn,backlog)=min(128,128)=128
- nr_table_entries=backlog=128
- nr_table_entries=min(backlog,sysctl_max_syn_backlog)=min(128,1024)=128
- nr_table_entries=max(nr_table_entries,8)=max(128,8)=128
- nr_table_entries=roundup_pow_of_two(nr_table_entries+1)=256
- max_qlen_log=max(3,log2(nr_table_entries))=max(3,8)=8
- max_queue_length=2^max_qlen_log=2^8=256
可以得到半隊列大小是 256。
判斷是否 Drop SYN 請求
當 Client 端向 Server 端發送 SYN 報文后,Server 端會將該 socket 連接存儲到半連接隊列(SYN Queue),如果 Server 端判斷半連接隊列滿了則會將連接 Drop 丟棄。
那么 Server 端是如何判斷半連接隊列是否滿的呢?除了上面一小節提到的半連接隊列最大長度控制外,還和 /proc/sys/net/ipv4/tcp_syncookies 參數有關。(tcp_syncookies 的作用是為了防止 SYN Flood 攻擊的,下文會給出相關鏈接介紹)
流程圖
判斷是否 Drop SYN 請求的流程圖:
上圖是整理了多份資料后,整理出來的判斷是否 Drop SYN 請求的流程圖。
注意:第一個判斷條件 「當前半連接隊列是否已超過半連接隊列最大長度」在不同內核版本中的判斷不一樣,Linux4.19.91 內核判斷的是當前半連接隊列長度是否 >= 全連接隊列最大長度。
相關內核代碼:
- staticinlineintinet_csk_reqsk_queue_is_full(conststructsock*sk)
- {
- returninet_csk_reqsk_queue_len(sk)>=sk->sk_max_ack_backlog;
- }
我們假設如下參數,來計算下當 Client 端只發送 SYN 包,理論上 Server 端何時會 Drop SYN 請求:
- 調用 listen 時傳入的 backlog = 1024
- /proc/sys/net/core/somaxconn 值為 1024
- /proc/sys/net/ipv4/tcp_max_syn_backlog 值為 128
當 /proc/sys/net/ipv4/tcp_syncookies 值為 0 時
- 計算出的半連接隊列最大長度為 256
- 當半連接隊列長度增長至 96 后,再新增 SYN 請求,就會觸發 Drop SYN 請求
當 /proc/sys/net/ipv4/tcp_syncookies 值為 1 時
1.計算出的半連接隊列最大長度為 256
2.由于開啟了 tcp_syncookies
- 當全連接隊列未滿時,永遠不會 Drop 請求 (注意:經實驗發現這個理論是錯誤的,實驗發現只要半連接隊列的大小 > 全連接隊列最大長度就會觸發 Drop SYN 請求)
- 當全連接隊列滿了后,即全連接隊列大小到 1024 后,就會觸發 Drop SYN 請求
PS:/proc/sys/net/ipv4/tcp_syncookies 的取值還可以為 2,筆者沒有詳細實驗。
回顧全連接隊列實驗結果
在上文全連接隊列實驗中,有一類實驗結果是:client 向 Server 發送 SYN 未得到響應,一直在 RETRY。
發生上述情況的原因由以下兩方面導致:
1. 開啟了 /proc/sys/net/ipv4/tcp_syncookies 功能
2. 全連接隊列滿了
半連接隊列溢出實驗
上文我們已經知道如何計算理論上半連接隊列何時會溢出,下面我們來具體實驗下
(Golang 調用 listen 時傳入的 backlog 值為 somaxconn)
實驗一:syncookies=0,somaxconn=1024,tcp_max_syn_backlog=128
理論上:
- 計算出的半連接隊列最大長度為 256
- 當半連接隊列長度增長至 96 后,后續 SYN 請求就會觸發 Drop
將相關參數的配置更新
- $sudosysctl-p
- net.core.somaxconn=1024
- net.ipv4.tcp_max_syn_backlog=128
- net.ipv4.tcp_syncookies=0
啟動服務端 Server 監聽 8888 端口(代碼參考全連接隊列實驗物料)
客戶端 Client 發起 SYN Flood 攻擊:
- $sudohping3-S33.9.192.157-p8888--flood
- HPING33.9.192.157(eth033.9.192.157):Sset,40headers+0databytes
- hpinginfloodmode,noreplieswillbeshown
查看服務端 Server 8888端口處于 SYN_RECV 狀態的 socket 最大個數:
- [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
- $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
- 96
- [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
- $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
- 96
實驗結果符合預期,當半連接隊列長度增長至 96 后,后續 SYN 請求就會觸發 Drop。
實驗二:syncookies = 0,somaxconn=128,tcp_max_syn_backlog=512
理論上:
- 計算出的半連接隊列最大長度為 256,由于筆者實驗機器上的內核版本是 4.19.91,所以當半連接隊列長度 >= 全連接隊列最大長度時,內核就認為半連接隊列溢出了
- 所以當半連接隊列長度增長至 128 后,后續 SYN 請求就會觸發 DROP
將相關參數的配置更新
- $sudosysctl-p
- net.core.somaxconn=128
- net.ipv4.tcp_max_syn_backlog=512
- net.ipv4.tcp_syncookies=0
啟動服務端 Server 監聽 8888 端口(代碼參考全連接隊列實驗物料)
客戶端 Client 發起 SYN Flood 攻擊:
- $sudohping3-S33.9.192.157-p8888--flood
- HPING33.9.192.157(eth033.9.192.157):Sset,40headers+0databytes
- hpinginfloodmode,noreplieswillbeshown
查看服務端 Server 8888端口處于 SYN_RECV 狀態的 socket 最大個數:
- [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
- $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
- 128
- [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
- $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
- 128
實驗結果符合預期,當半連接隊列長度增長至 128 后,后續 SYN 請求就會觸發 Drop
實驗三:syncookies = 1,somaxconn=128,tcp_max_syn_backlog=512
理論上:
- 當全連接隊列未滿,syncookies = 1,理論上 SYN 請求永遠不會被 Drop
將相關參數的配置更新
- $sudosysctl-p
- net.core.somaxconn=128
- net.ipv4.tcp_max_syn_backlog=512
- net.ipv4.tcp_syncookies=1
啟動服務端 Server 監聽 8888 端口(代碼參考全連接隊列實驗物料)
客戶端 Client 發起 SYN Flood 攻擊:
- $sudohping3-S33.9.192.157-p8888--flood
- HPING33.9.192.157(eth033.9.192.157):Sset,40headers+0databytes
- hpinginfloodmode,noreplieswillbeshown
查看服務端 Server 8888端口處于 SYN_RECV 狀態的 socket 最大個數:
- [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
- $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
- 128
- [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
- $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
- 128
實驗發現即使syncookies=1,當半連接隊列長度 > 全連接隊列最大長度時,就會觸發 DROP SYN 請求!!!(TODO:有時間閱讀下相關內核源碼,再分析下)
繼續做實驗,將 somaxconn 更新為 5
- $sudosysctl-p
- net.core.somaxconn=5
- net.ipv4.tcp_max_syn_backlog=512
- net.ipv4.tcp_syncookies=1
發起 SYN Flood 攻擊后,查看服務端 Server 8888端口處于 SYN_RECV 狀態的 socket 最大個數:
- [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
- $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
- 5
- [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
- $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
- 5
確實 即使 syncookies=1,當半連接隊列長度 > 全連接最大長度時,就會觸發 DROP SYN 請求。
實驗四:syncookies = 1,somaxconn=256,tcp_max_syn_backlog=128
理論上:
- 當半連接隊列大小到 256 后,后觸發 DROP SYN 請求
將相關參數的配置更新
- $sudosysctl-p
- net.core.somaxconn=256
- net.ipv4.tcp_max_syn_backlog=128
- net.ipv4.tcp_syncookies=1
啟動服務端 Server 監聽 8888 端口(代碼參考全連接隊列實驗物料)。
客戶端 Client 發起 SYN Flood 攻擊:
- $sudohping3-S33.9.192.157-p8888--flood
- HPING33.9.192.157(eth033.9.192.157):Sset,40headers+0databytes
- hpinginfloodmode,noreplieswillbeshown
查看服務端 Server 8888端口處于 SYN_RECV 狀態的 socket 最大個數:
- [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
- $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
- 256
- [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
- $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
- 256
實驗結果符合預期,當半連接隊列長度增長至 256 后,后續 SYN 請求就會觸發 Drop。
回顧線上問題
再回顧值班時遇到的 Connection timeout 問題,當時相關系統參數配置為:
- net.core.somaxconn = 128
- net.ipv4.tcp_max_syn_backlog = 512
- net.ipv4.tcp_syncookies = 1
- net.ipv4.tcp_abort_on_overflow = 0
所以出現 Connection timeout 有兩種可能情況:
1、半連接隊列未滿,全連接隊列滿,Client 端向 Server 端發起 SYN 被 DROP (參考全連接隊列實驗結果情況三分析、半連接隊列溢出實驗情況三)
2、全連接隊列未滿,半連接隊列大小超過全鏈接隊列最大長度(參考半連接隊列溢出實驗情況三、半連接隊列溢出實驗情況四)
問題的最快修復方式是將 net.core.somaxconn 調大,以及 net.ipv4.tcp_abort_on_overflow 設置為 1,net.ipv4.tcp_abort_on_overflow 設置為 1 是為了讓 client fail fast。
總結
半連接隊列溢出、全連接隊列溢出這類問題很容易被忽略,同時這類問題又很致命。當半連接隊列、全連接隊列溢出時 Server 端,從監控上來看系統 cpu 水位、內存水位、網絡連接數等一切正常,然而卻會持續影響 Client 端業務請求。對于高負載上游使用短連接的情況,出現這類問題的可能性更大。
本文詳細梳理了 TCP 半連接隊列、全連接隊列的理論知識,同時結合 Linux 相關內核代碼以及詳細的動手實驗,講解了 TCP 半連接隊列、全連接隊列的相關原理、溢出判斷、問題分析等內容,希望大家在閱讀后可以對 TCP 半連接隊列、全連接隊列有更充分的認識。
PS:可以去線上檢查下服務器的相關參數喲~
附錄
這里羅列下相關參考博文資料:
Linux 源碼
- https://github.com/torvalds/linux
Linux 詭異的半連接隊列長度
- https://www.cnblogs.com/zengkefu/p/5606696.html
TCP 半連接隊列和全連接隊列滿了會發生什么
- https://www.cnblogs.com/xiaolincoding/p/12995358.html
一次 HTTP connect-timeout 排查
- https://www.jianshu.com/p/3b9c4216b822
Connection Reset 排查
- https://cjting.me/2019/08/28/tcp-queue/
深入淺出 TCP 中的 SYN-Cookies
- https://segmentfault.com/a/1190000019292140
原文鏈接:https://mp.weixin.qq.com/s/YpSlU1yaowTs-pF6R43hMw