自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從一次線上問題說起,詳解 TCP 半連接隊列、全連接隊列

網(wǎng)絡(luò) 通信技術(shù)
某次大促值班 ing,對系統(tǒng)穩(wěn)定性有著充分信心、心態(tài)穩(wěn)如老狗的筆者突然收到上游反饋有萬分幾的概率請求我們 endpoint 會出現(xiàn) Connection timeout 。

[[431611]]

本文轉(zhuǎn)載自微信公眾號「云巔論劍」,作者黃剛。轉(zhuǎn)載本文請聯(lián)系云巔論劍公眾號。

前言

某次大促值班 ing,對系統(tǒng)穩(wěn)定性有著充分信心、心態(tài)穩(wěn)如老狗的筆者突然收到上游反饋有萬分幾的概率請求我們 endpoint 會出現(xiàn) Connection timeout 。此時系統(tǒng)側(cè)的 apiserver 集群水位在 40%,離極限水位還有著很大的距離,當時通過緊急擴容 apiserver 集群后錯誤率降為了 0。事后進行了詳細的問題排查,定位分析到問題根因出現(xiàn)在系統(tǒng)連接隊列被打滿導(dǎo)致,之前筆者對 TCP 半連接隊列、全連接隊列不太了解,只依稀記得 《TCP/IP 詳解》中好像有好像提到過這兩個名詞。

目前網(wǎng)上相關(guān)資料都比較零散,并且有些是過時或錯誤的結(jié)論,筆者在調(diào)查問題時踩了很多坑。痛定思痛,筆者查閱了大量資料并做了眾多實驗進行驗證,梳理了這篇 TCP 半連接隊列、全連接詳解,當你細心閱讀完這篇文章后相信你可以對 TCP 半連接隊列、全連接隊列有更充分的認識。

本篇文章將結(jié)合理論知識、內(nèi)核代碼、操作實驗為你呈現(xiàn)如下內(nèi)容:

  • 半連接隊列、全連接隊列介紹
  • 常用命令介紹
  • 全連接隊列實戰(zhàn) —— 最大長度控制、全連接隊列溢出實驗、實驗結(jié)果分析...
  • 半連接隊列實戰(zhàn) —— 最大長度控制、半連接隊列溢出實驗、實驗結(jié)果分析...
  • ...

半連接隊列、全連接隊列

在 TCP 三次握手的過程中,Linux 內(nèi)核會維護兩個隊列,分別是:

  • 半連接隊列 (SYN Queue)
  • 全連接隊列 (Accept Queue)

正常的 TCP 三次握手過程:

1、Client 端向 Server 端發(fā)送 SYN 發(fā)起握手,Client 端進入 SYN_SENT 狀態(tài)

2、Server 端收到 Client 端的 SYN 請求后,Server 端進入 SYN_RECV 狀態(tài),此時內(nèi)核會將連接存儲到半連接隊列(SYN Queue),并向 Client 端回復(fù) SYN+ACK

3、Client 端收到 Server 端的 SYN+ACK 后,Client 端回復(fù) ACK 并進入 ESTABLISHED 狀態(tài)

4、Server 端收到 Client 端的 ACK 后,內(nèi)核將連接從半連接隊列(SYN Queue)中取出,添加到全連接隊列(Accept Queue),Server 端進入 ESTABLISHED 狀態(tài)

5、Server 端應(yīng)用進程調(diào)用 accept 函數(shù)時,將連接從全連接隊列(Accept Queue)中取出

半連接隊列和全連接隊列都有長度大小限制,超過限制時內(nèi)核會將連接 Drop 丟棄或者返回 RST 包。

相關(guān)指標查看

ss 命令

通過 ss 命令可以查看到全連接隊列的信息

  1. # -n 不解析服務(wù)名稱 
  2. # -t 只顯示 tcp sockets 
  3. # -l 顯示正在監(jiān)聽(LISTEN)的 sockets 
  4.  
  5. $ ss -lnt 
  6. State      Recv-Q Send-Q    Local Address:Port         Peer Address:Port 
  7. LISTEN     0      128       [::]:2380                  [::]:* 
  8. LISTEN     0      128       [::]:80                    [::]:* 
  9. LISTEN     0      128       [::]:8080                  [::]:* 
  10. LISTEN     0      128       [::]:8090                  [::]:* 
  11.  
  12. $ ss -nt 
  13. State      Recv-Q Send-Q    Local Address:Port         Peer Address:Port 
  14. ESTAB      0      0         [::ffff:33.9.95.134]:80                   [::ffff:33.51.103.59]:47452 
  15. ESTAB      0      536       [::ffff:33.9.95.134]:80                  [::ffff:33.43.108.144]:37656 
  16. ESTAB      0      0         [::ffff:33.9.95.134]:80                   [::ffff:33.51.103.59]:38130 
  17. ESTAB      0      536       [::ffff:33.9.95.134]:80                   [::ffff:33.51.103.59]:38280 
  18. ESTAB      0      0         [::ffff:33.9.95.134]:80                   [:: 

對于 LISTEN 狀態(tài)的 socket

  • Recv-Q:當前全連接隊列的大小,即已完成三次握手等待應(yīng)用程序 accept() 的 TCP 鏈接
  • Send-Q:全連接隊列的最大長度,即全連接隊列的大小

對于非 LISTEN 狀態(tài)的 socket

  • Recv-Q:已收到但未被應(yīng)用程序讀取的字節(jié)數(shù)
  • Send-Q:已發(fā)送但未收到確認的字節(jié)數(shù)

相關(guān)內(nèi)核代碼:

  1. // https://github.com/torvalds/linux/blob/master/net/ipv4/tcp_diag.c 
  2. static void tcp_diag_get_info(struct sock *sk, struct inet_diag_msg *r, 
  3.             void *_info) 
  4.   struct tcp_info *info = _info; 
  5.  
  6.   if (inet_sk_state_load(sk) == TCP_LISTEN) { // socket 狀態(tài)是 LISTEN 時 
  7.     r->idiag_rqueue = READ_ONCE(sk->sk_ack_backlog);  // 當前全連接隊列大小 
  8.     r->idiag_wqueue = READ_ONCE(sk->sk_max_ack_backlog); // 全連接隊列最大長度 
  9.   } else if (sk->sk_type == SOCK_STREAM) {    // socket 狀態(tài)不是 LISTEN 時 
  10.     const struct tcp_sock *tp = tcp_sk(sk); 
  11.  
  12.     r->idiag_rqueue = max_t(int, READ_ONCE(tp->rcv_nxt) - 
  13.                READ_ONCE(tp->copied_seq), 0);    // 已收到但未被應(yīng)用程序讀取的字節(jié)數(shù) 
  14.     r->idiag_wqueue = READ_ONCE(tp->write_seq) - tp->snd_una;   // 已發(fā)送但未收到確認的字節(jié)數(shù) 
  15.   } 
  16.   if (info) 
  17.     tcp_get_info(sk, info); 

netstat 命令

通過 netstat -s 命令可以查看 TCP 半連接隊列、全連接隊列的溢出情況

  1. $ netstat -s | grep -i "listen" 
  2.     189088 times the listen queue of a socket overflowed 
  3.     30140232 SYNs to LISTEN sockets dropped 

上面輸出的數(shù)值是累計值,分別表示有多少 TCP socket 鏈接因為全連接隊列、半連接隊列滿了而被丟棄

  • 189088 times the listen queue of a socket overflowed 代表有 189088 次全連接隊列溢出
  • 30140232 SYNs to LISTEN sockets dropped 代表有 30140232 次半連接隊列溢出

在排查線上問題時,如果一段時間內(nèi)相關(guān)數(shù)值一直在上升,則表明半連接隊列、全連接隊列有溢出情況

實戰(zhàn) —— 全連接隊列

全連接隊列最大長度控制

TCP 全連接隊列的最大長度由 min(somaxconn, backlog) 控制,其中:

  • somaxconn 是 Linux 內(nèi)核參數(shù),由 /proc/sys/net/core/somaxconn 指定
  • backlog 是 TCP 協(xié)議中 listen 函數(shù)的參數(shù)之一,即 int listen(int sockfd, int backlog) 函數(shù)中的 backlog 大小。在 Golang 中,listen 的 backlog 參數(shù)使用的是 /proc/sys/net/core/somaxconn 文件中的值。

相關(guān)內(nèi)核代碼:

  1. // https://github.com/torvalds/linux/blob/master/net/socket.c 
  2.  
  3. /* 
  4.  *  Perform a listen. Basically, we allow the protocol to do anything 
  5.  *  necessary for a listen, and if that works, we mark the socket as 
  6.  *  ready for listening. 
  7.  */ 
  8. int __sys_listen(int fd, int backlog) 
  9.   struct socket *sock; 
  10.   int err, fput_needed; 
  11.   int somaxconn; 
  12.  
  13.   sock = sockfd_lookup_light(fd, &err, &fput_needed); 
  14.   if (sock) { 
  15.     somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;  // /proc/sys/net/core/somaxconn 
  16.     if ((unsigned int)backlog > somaxconn) 
  17.       backlog = somaxconn;   // TCP 全連接隊列最大長度 min(somaxconn, backlog) 
  18.  
  19.     err = security_socket_listen(sock, backlog); 
  20.     if (!err) 
  21.       err = sock->ops->listen(sock, backlog); 
  22.  
  23.     fput_light(sock->file, fput_needed); 
  24.   } 
  25.   return err; 

實驗

服務(wù)端 server 代碼

  1. package main 
  2.  
  3. import ( 
  4.   "log" 
  5.   "net" 
  6.   "time" 
  7.  
  8. func main() { 
  9.   l, err := net.Listen("tcp"":8888"
  10.   if err != nil { 
  11.     log.Printf("failed to listen due to %v", err) 
  12.   } 
  13.   defer l.Close() 
  14.   log.Println("listen :8888 success"
  15.  
  16.   for { 
  17.     time.Sleep(time.Second * 100) 
  18.   } 

在測試環(huán)境查看 somaxconn 的值為 128

  1. $ cat /proc/sys/net/core/somaxconn 
  2. 128 

啟動服務(wù)端,通過 ss -lnt | grep :8888 確認全連接隊列大小

  1. LISTEN     0      128       [::]:8888                  [::]:* 

全連接隊列最大長度為 128

現(xiàn)在更新 somaxconn 值為 1024,再重新啟動服務(wù)端。

1、更新 /etc/sysctl.conf 文件,該文件為內(nèi)核參數(shù)配置文件

a.新增一行 net.core.somaxconn=1024

2、執(zhí)行 sysctl -p 使配置生效

  1. $ sudo sysctl -p 
  2. net.core.somaxconn = 1024 

3、檢查 /proc/sys/net/core/somaxconn 文件,確認 somaxconn 為更新后的 1024

  1. $ cat /proc/sys/net/core/somaxconn 
  2. 1024 

重新啟動服務(wù)端, 通過 ss -lnt | grep :8888 確認全連接隊列大小

  1. $ ss -lnt | grep 8888 
  2. LISTEN     0      1024      [::]:8888                  [::]:* 

可以看到,現(xiàn)在全鏈接隊列最大長度為 1024,成功更新。

全連接隊列溢出

下面來驗證下全連接隊列溢出會發(fā)生什么情況,可以通過讓服務(wù)端應(yīng)用只負責 Listen 對應(yīng)端口而不執(zhí)行 accept() TCP 連接,使 TCP 全連接隊列溢出。

實驗物料

服務(wù)端 server 代碼

  1. // server 端監(jiān)聽 8888 tcp 端口 
  2.  
  3. package main 
  4.  
  5. import ( 
  6.   "log" 
  7.   "net" 
  8.   "time" 
  9.  
  10. func main() { 
  11.   l, err := net.Listen("tcp"":8888"
  12.   if err != nil { 
  13.     log.Printf("failed to listen due to %v", err) 
  14.   } 
  15.   defer l.Close() 
  16.   log.Println("listen :8888 success"
  17.  
  18.   for { 
  19.     time.Sleep(time.Second * 100) 
  20.   } 

客戶端 client 代碼

  1. // client 端并發(fā)請求 10 次 server 端,成功建立 tcp 連接后向 server 端發(fā)送數(shù)據(jù) 
  2. package main 
  3.  
  4. import ( 
  5.   "context" 
  6.   "log" 
  7.   "net" 
  8.   "os" 
  9.   "os/signal" 
  10.   "sync" 
  11.   "syscall" 
  12.   "time" 
  13.  
  14. var wg sync.WaitGroup 
  15.  
  16. func establishConn(ctx context.Context, i int) { 
  17.   defer wg.Done() 
  18.   conn, err := net.DialTimeout("tcp"":8888"time.Second*5) 
  19.   if err != nil { 
  20.     log.Printf("%d, dial error: %v", i, err) 
  21.     return 
  22.   } 
  23.   log.Printf("%d, dial success", i) 
  24.   _, err = conn.Write([]byte("hello world")) 
  25.   if err != nil { 
  26.     log.Printf("%d, send error: %v", i, err) 
  27.     return 
  28.   } 
  29.   select { 
  30.   case <-ctx.Done(): 
  31.     log.Printf("%d, dail close", i) 
  32.   } 
  33.  
  34. func main() { 
  35.   ctx, cancel := context.WithCancel(context.Background()) 
  36.   for i := 0; i < 10; i++ { 
  37.     wg.Add(1) 
  38.     go establishConn(ctx, i) 
  39.   } 
  40.  
  41.   go func() { 
  42.     sc := make(chan os.Signal, 1) 
  43.     signal.Notify(sc, syscall.SIGINT) 
  44.     select { 
  45.     case <-sc: 
  46.       cancel() 
  47.     } 
  48.   }() 
  49.  
  50.   wg.Wait() 
  51.   log.Printf("client exit"

為了方便實驗,將 somaxconn 全連接隊列最大長度更新為 5:

1、更新 /etc/sysctl.conf 文件,將 net.core.somaxconn 更新為 5

2、執(zhí)行 sysctl -p 使配置生效

  1. $ sudo sysctl -p 
  2. net.core.somaxconn = 5 

實驗結(jié)果

客戶端日志輸出

  1. 2021/10/11 17:24:48 8, dial success 
  2. 2021/10/11 17:24:48 3, dial success 
  3. 2021/10/11 17:24:48 4, dial success 
  4. 2021/10/11 17:24:48 6, dial success 
  5. 2021/10/11 17:24:48 5, dial success 
  6. 2021/10/11 17:24:48 2, dial success 
  7. 2021/10/11 17:24:48 1, dial success 
  8. 2021/10/11 17:24:48 0, dial success 
  9. 2021/10/11 17:24:48 7, dial success 
  10. 2021/10/11 17:24:53 9, dial error: dial tcp 33.9.192.157:8888: i/o timeout 

客戶端 socket 情況

  1. tcp        0      0 33.9.192.155:40372      33.9.192.157:8888       ESTABLISHED 
  2. tcp        0      0 33.9.192.155:40376      33.9.192.157:8888       ESTABLISHED 
  3. tcp        0      0 33.9.192.155:40370      33.9.192.157:8888       ESTABLISHED 
  4. tcp        0      0 33.9.192.155:40366      33.9.192.157:8888       ESTABLISHED 
  5. tcp        0      0 33.9.192.155:40374      33.9.192.157:8888       ESTABLISHED 
  6. tcp        0      0 33.9.192.155:40368      33.9.192.157:8888       ESTABLISHED 

服務(wù)端 socket 情況

  1. tcp6      11      0 33.9.192.157:8888       33.9.192.155:40376      ESTABLISHED 
  2. tcp6      11      0 33.9.192.157:8888       33.9.192.155:40370      ESTABLISHED 
  3. tcp6      11      0 33.9.192.157:8888       33.9.192.155:40368      ESTABLISHED 
  4. tcp6      11      0 33.9.192.157:8888       33.9.192.155:40372      ESTABLISHED 
  5. tcp6      11      0 33.9.192.157:8888       33.9.192.155:40374      ESTABLISHED 
  6. tcp6      11      0 33.9.192.157:8888       33.9.192.155:40366      ESTABLISHED 
  7.  
  8. tcp    LISTEN     6      5      [::]:8888               [::]:*                   users:(("main",pid=84244,fd=3)) 

抓包結(jié)果

對客戶端、服務(wù)端抓包后,發(fā)現(xiàn)出現(xiàn)了三種情況,分別是:

  • client 成功與 server 端建立 tcp socket 連接,發(fā)送數(shù)據(jù)成功
  • client 認為成功與 server 端建立 tcp socket 連接,發(fā)送數(shù)據(jù)失敗,一直在 RETRY;server 端認為 tcp 連接未建立,一直在發(fā)送 SYN+ACK
  • client 向 server 發(fā)送 SYN 未得到響應(yīng),一直在 RETRY

全連接隊列實驗結(jié)果分析

上述實驗結(jié)果出現(xiàn)了三種情況,我們分別對抓包內(nèi)容進行分析

情況一:Client 成功與 Server 端建立 tcp socket 鏈接,發(fā)送數(shù)據(jù)成功

上圖可以看到如下請求:

  • Client 端向 Server 端發(fā)送 SYN 發(fā)起握手
  • Server 端收到 Client 端 SYN 后,向 Client 端回復(fù) SYN+ACK,socket 連接存儲到半連接隊列(SYN Queue)
  • Client 端收到 Server 端 SYN+ACK 后,向 Server 端回復(fù) ACK,Client 端進入 ESTABLISHED 狀態(tài)
  • Server 端收到 Client 端 ACK 后,進入 ESTABLISHED 狀態(tài),socket 連接存儲到全連接隊列(Accept Queue)
  • Client 端向 Server 端發(fā)送數(shù)據(jù) [PSH, ACK],Server 端確認接收到數(shù)據(jù) [ACK]

這種情況就是正常的請求,即全連接隊列、半連接隊列未滿,client 成功與 server 建立了 tcp 鏈接,并成功發(fā)送數(shù)據(jù)。

情況二:Client 認為成功與 Server 端建立 tcp socket 連接,后續(xù)發(fā)送數(shù)據(jù)失敗,持續(xù) RETRY;Server 端認為 TCP 連接未建立,一直在發(fā)送SYN+ACK

上圖可以看到如下請求:

  • Client 端向 Server 端發(fā)送 SYN 發(fā)起握手
  • Server 端收到 Client 端 SYN 后,向 Client 端回復(fù) SYN+ACK,socket 連接存儲到半連接隊列(SYN Queue)
  • Client 端收到 Server 端 SYN+ACK 后,向 Server 端回復(fù) ACK,Client 端進入 ESTABLISHED狀態(tài)(重要:此時僅僅是 Client 端認為 tcp 連接建立成功)
  • 由于 Client 端認為 TCP 連接已經(jīng)建立完成,所以向 Server 端發(fā)送數(shù)據(jù) [PSH,ACK],但是一直未收到 Server 端的確認 ACK,所以一直在 RETRY
  • Server 端一直在 RETRY 發(fā)送 SYN+ACK

為什么會出現(xiàn)上述情況?Server 端為什么一直在 RETRY 發(fā)送 SYN+ACK?Server 端不是已經(jīng)收到了 Client 端的 ACK 確認了嗎?

上述情況是由于 Server 端 socket 連接進入了半連接隊列,在收到 Client 端 ACK 后,本應(yīng)將 socket 連接存儲到全連接隊列,但是全連接隊列已滿,所以 Server 端 DROP 了該 ACK 請求。

之所以 Server 端一直在 RETRY 發(fā)送 SYN+ACK,是因為 DROP 了 client 端的 ACK 請求,所以 socket 連接仍舊在半連接隊列中,等待 Client 端回復(fù) ACK。

tcp_abort_on_overflow 參數(shù)控制

全連接隊列滿DROP 請求是默認行為,可以通過設(shè)置 /proc/sys/net/ipv4/tcp_abort_on_overflow 使 Server 端在全連接隊列滿時,向 Client 端發(fā)送 RST 報文。

tcp_abort_on_overflow 有兩種可選值:

  • 0:如果全連接隊列滿了,Server 端 DROP Client 端回復(fù)的 ACK
  • 1:如果全連接隊列滿了,Server 端向 Client 端發(fā)送 RST 報文,終止 TCP socket 鏈接 (TODO:后續(xù)有時間補充下該實驗)

為什么實驗結(jié)果中當前全連接隊列大小 > 全連接隊列最大長度配置?

上述結(jié)果中可以看到 Listen 狀態(tài)的 socket 鏈接:

  • Recv-Q 當前全連接隊列的大小是 6
  • Send-Q 全連接隊列最大長度是 5
  1. State      Recv-Q Send-Q    Local Address:Port         Peer Address:Port 
  2. LISTEN     6      5         [::]:8888                  [::]:* 

為什么全連接隊列大小 > 全連接隊列最大長度配置呢?

經(jīng)過多次實驗發(fā)現(xiàn),能夠進入全連接隊列的 Socket 最大數(shù)量始終比配置的全連接隊列最大長度 + 1。

結(jié)合其他文章以及內(nèi)核代碼,發(fā)現(xiàn)內(nèi)核在判斷全連接隊列是否滿的情況下,使用的是 > 而非 >= (具體是為什么沒有找到相關(guān)資源 : ) )。

相關(guān)內(nèi)核代碼:

  1. /* Note: If you think the test should be: 
  2.  *  return READ_ONCE(sk->sk_ack_backlog) >= READ_ONCE(sk->sk_max_ack_backlog); 
  3.  * Then please take a look at commit 64a146513f8f ("[NET]: Revert incorrect accept queue backlog changes."
  4.  */ 
  5. static inline bool sk_acceptq_is_full(const struct sock *sk) 
  6.   return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog); 

情況三:Client 向 Server 發(fā)送 SYN 未得到相應(yīng),一直在 RETRY

圖片上圖可以看到如下請求:

  • Client 端向 Server 端發(fā)送 SYN 發(fā)起握手,未得到 Server 回應(yīng),一直在 RETRY

(這種情況涉及到半連接隊列,這里先給上述情況發(fā)生的原因結(jié)論,具體內(nèi)容將在下文半連接隊列中展開。)

發(fā)生上述情況的原因由以下兩方面導(dǎo)致:

1、開啟了 /proc/sys/net/ipv4/tcp_syncookies 功能

2、全連接隊列滿了

實戰(zhàn) —— 半連接隊列

半連接隊列最大長度控制

翻閱了很多博文,查找關(guān)于半連接隊列最大長度控制的相關(guān)內(nèi)容,大多含糊其辭或不準確,經(jīng)過不懈努力,最終找到了比較確切的內(nèi)容(相關(guān)博文鏈接在附錄中)。

很多博文中說半連接隊列最大長度由 /proc/sys/net/ipv4/tcp_max_syn_backlog 參數(shù)指定,實際上只有在 linux 內(nèi)核版本小于 2.6.20 時,半連接隊列才等于 backlog 的大小。

這塊的源碼比較復(fù)雜,這里給一下大體的計算方式,詳細的內(nèi)容可以參考附錄中的相關(guān)博文。半連接隊列長度的計算過程:

  1. backlog = min(somaxconn, backlog) 
  2. nr_table_entries = backlog 
  3. nr_table_entries = min(backlog, sysctl_max_syn_backlog) 
  4. nr_table_entries = max(nr_table_entries, 8) 
  5. // roundup_pow_of_two: 將參數(shù)向上取整到最小的 2^n,注意這里存在一個 +1 
  6. nr_table_entries = roundup_pow_of_two(nr_table_entries + 1) 
  7. max_qlen_log = max(3, log2(nr_table_entries)) 
  8. max_queue_length = 2^max_qlen_log 

可以看到,半連接隊列的長度由三個參數(shù)指定:

  • 調(diào)用 listen 時,傳入的 backlog
  • /proc/sys/net/core/somaxconn 默認值為 128
  • /proc/sys/net/ipv4/tcp_max_syn_backlog 默認值為 1024

我們假設(shè) listen 傳入的 backlog = 128 (Golang 中調(diào)用 listen 時傳遞的 backlog 參數(shù)使用的是 /proc/sys/net/core/somaxconn),其他配置采用默認值,來計算下半連接隊列的最大長度

  1. backlog = min(somaxconn, backlog) = min(128, 128) = 128 
  2. nr_table_entries = backlog = 128 
  3. nr_table_entries = min(backlog, sysctl_max_syn_backlog) = min(128, 1024) = 128 
  4. nr_table_entries = max(nr_table_entries, 8) = max(128, 8) = 128 
  5. nr_table_entries = roundup_pow_of_two(nr_table_entries + 1) = 256 
  6. max_qlen_log = max(3, log2(nr_table_entries)) = max(3, 8) = 8 
  7. max_queue_length = 2^max_qlen_log = 2^8 = 256 

可以得到半隊列大小是 256。

判斷是否 Drop SYN 請求

當 Client 端向 Server 端發(fā)送 SYN 報文后,Server 端會將該 socket 連接存儲到半連接隊列(SYN Queue),如果 Server 端判斷半連接隊列滿了則會將連接 Drop 丟棄。

那么 Server 端是如何判斷半連接隊列是否滿的呢?除了上面一小節(jié)提到的半連接隊列最大長度控制外,還和 /proc/sys/net/ipv4/tcp_syncookies 參數(shù)有關(guān)。(tcp_syncookies 的作用是為了防止 SYN Flood 攻擊的,下文會給出相關(guān)鏈接介紹)

流程圖

判斷是否 Drop SYN 請求的流程圖:

上圖是整理了多份資料后,整理出來的判斷是否 Drop SYN 請求的流程圖。

注意:第一個判斷條件 「當前半連接隊列是否已超過半連接隊列最大長度」在不同內(nèi)核版本中的判斷不一樣,Linux4.19.91 內(nèi)核判斷的是當前半連接隊列長度是否 >= 全連接隊列最大長度。

相關(guān)內(nèi)核代碼:

  1. static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk) 
  2.   return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog; 

我們假設(shè)如下參數(shù),來計算下當 Client 端只發(fā)送 SYN 包,理論上 Server 端何時會 Drop SYN 請求:

  • 調(diào)用 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 請求,就會觸發(fā) Drop SYN 請求

當 /proc/sys/net/ipv4/tcp_syncookies 值為 1 時

1.計算出的半連接隊列最大長度為 256

2.由于開啟了 tcp_syncookies

  • 當全連接隊列未滿時,永遠不會 Drop 請求 (注意:經(jīng)實驗發(fā)現(xiàn)這個理論是錯誤的,實驗發(fā)現(xiàn)只要半連接隊列的大小 > 全連接隊列最大長度就會觸發(fā) Drop SYN 請求)
  • 當全連接隊列滿了后,即全連接隊列大小到 1024 后,就會觸發(fā) Drop SYN 請求

PS:/proc/sys/net/ipv4/tcp_syncookies 的取值還可以為 2,筆者沒有詳細實驗。

回顧全連接隊列實驗結(jié)果

在上文全連接隊列實驗中,有一類實驗結(jié)果是:client 向 Server 發(fā)送 SYN 未得到響應(yīng),一直在 RETRY。

發(fā)生上述情況的原因由以下兩方面導(dǎo)致:

1. 開啟了 /proc/sys/net/ipv4/tcp_syncookies 功能

2. 全連接隊列滿了

半連接隊列溢出實驗

上文我們已經(jīng)知道如何計算理論上半連接隊列何時會溢出,下面我們來具體實驗下

(Golang 調(diào)用 listen 時傳入的 backlog 值為 somaxconn)

實驗一:syncookies=0,somaxconn=1024,tcp_max_syn_backlog=128

理論上:

  • 計算出的半連接隊列最大長度為 256
  • 當半連接隊列長度增長至 96 后,后續(xù) SYN 請求就會觸發(fā) Drop

將相關(guān)參數(shù)的配置更新

  1. $ sudo sysctl -p 
  2. net.core.somaxconn = 1024 
  3. net.ipv4.tcp_max_syn_backlog = 128 
  4. net.ipv4.tcp_syncookies = 0 

啟動服務(wù)端 Server 監(jiān)聽 8888 端口(代碼參考全連接隊列實驗物料)

客戶端 Client 發(fā)起 SYN Flood 攻擊:

  1. $ sudo hping3 -S 33.9.192.157 -p 8888 --flood 
  2. HPING 33.9.192.157 (eth0 33.9.192.157): S set, 40 headers + 0 data bytes 
  3. hping in flood mode, no replies will be shown 

查看服務(wù)端 Server 8888端口處于 SYN_RECV 狀態(tài)的 socket 最大個數(shù):

  1. [zechen.hg@function-compute033009192157.na63 /home/zechen.hg] 
  2. $ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l 
  3. 96 
  4.  
  5. [zechen.hg@function-compute033009192157.na63 /home/zechen.hg] 
  6. $ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l 
  7. 96 

實驗結(jié)果符合預(yù)期,當半連接隊列長度增長至 96 后,后續(xù) SYN 請求就會觸發(fā) Drop。

實驗二:syncookies = 0,somaxconn=128,tcp_max_syn_backlog=512

理論上:

  • 計算出的半連接隊列最大長度為 256,由于筆者實驗機器上的內(nèi)核版本是 4.19.91,所以當半連接隊列長度 >= 全連接隊列最大長度時,內(nèi)核就認為半連接隊列溢出了
  • 所以當半連接隊列長度增長至 128 后,后續(xù) SYN 請求就會觸發(fā) DROP

將相關(guān)參數(shù)的配置更新

  1. $ sudo sysctl -p 
  2. net.core.somaxconn = 128 
  3. net.ipv4.tcp_max_syn_backlog = 512 
  4. net.ipv4.tcp_syncookies = 0 

啟動服務(wù)端 Server 監(jiān)聽 8888 端口(代碼參考全連接隊列實驗物料)

客戶端 Client 發(fā)起 SYN Flood 攻擊:

  1. $ sudo hping3 -S 33.9.192.157 -p 8888 --flood 
  2. HPING 33.9.192.157 (eth0 33.9.192.157): S set, 40 headers + 0 data bytes 
  3. hping in flood mode, no replies will be shown 

查看服務(wù)端 Server 8888端口處于 SYN_RECV 狀態(tài)的 socket 最大個數(shù):

  1. [zechen.hg@function-compute033009192157.na63 /home/zechen.hg] 
  2. $ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l 
  3. 128 
  4.  
  5. [zechen.hg@function-compute033009192157.na63 /home/zechen.hg] 
  6. $ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l 
  7. 128 

實驗結(jié)果符合預(yù)期,當半連接隊列長度增長至 128 后,后續(xù) SYN 請求就會觸發(fā) Drop

實驗三:syncookies = 1,somaxconn=128,tcp_max_syn_backlog=512

理論上:

  • 當全連接隊列未滿,syncookies = 1,理論上 SYN 請求永遠不會被 Drop

將相關(guān)參數(shù)的配置更新

  1. $ sudo sysctl -p 
  2. net.core.somaxconn = 128 
  3. net.ipv4.tcp_max_syn_backlog = 512 
  4. net.ipv4.tcp_syncookies = 1 

啟動服務(wù)端 Server 監(jiān)聽 8888 端口(代碼參考全連接隊列實驗物料)

客戶端 Client 發(fā)起 SYN Flood 攻擊:

  1. $ sudo hping3 -S 33.9.192.157 -p 8888 --flood 
  2. HPING 33.9.192.157 (eth0 33.9.192.157): S set, 40 headers + 0 data bytes 
  3. hping in flood mode, no replies will be shown 

查看服務(wù)端 Server 8888端口處于 SYN_RECV 狀態(tài)的 socket 最大個數(shù):

  1. [zechen.hg@function-compute033009192157.na63 /home/zechen.hg] 
  2. $ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l 
  3. 128 
  4.  
  5. [zechen.hg@function-compute033009192157.na63 /home/zechen.hg] 
  6. $ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l 
  7. 128 

實驗發(fā)現(xiàn)即使syncookies=1,當半連接隊列長度 > 全連接隊列最大長度時,就會觸發(fā) DROP SYN 請求!!!(TODO:有時間閱讀下相關(guān)內(nèi)核源碼,再分析下)

繼續(xù)做實驗,將 somaxconn 更新為 5

  1. $ sudo sysctl -p 
  2. net.core.somaxconn = 5 
  3. net.ipv4.tcp_max_syn_backlog = 512 
  4. net.ipv4.tcp_syncookies = 1 

發(fā)起 SYN Flood 攻擊后,查看服務(wù)端 Server 8888端口處于 SYN_RECV 狀態(tài)的 socket 最大個數(shù):

  1. [zechen.hg@function-compute033009192157.na63 /home/zechen.hg] 
  2. $ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l 
  3.  
  4. [zechen.hg@function-compute033009192157.na63 /home/zechen.hg] 
  5. $ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l 

確實 即使 syncookies=1,當半連接隊列長度 > 全連接最大長度時,就會觸發(fā) DROP SYN 請求。

實驗四:syncookies = 1,somaxconn=256,tcp_max_syn_backlog=128

理論上:

  • 當半連接隊列大小到 256 后,后觸發(fā) DROP SYN 請求

將相關(guān)參數(shù)的配置更新

  1. $ sudo sysctl -p 
  2. net.core.somaxconn = 256 
  3. net.ipv4.tcp_max_syn_backlog = 128 
  4. net.ipv4.tcp_syncookies = 1 

啟動服務(wù)端 Server 監(jiān)聽 8888 端口(代碼參考全連接隊列實驗物料)。

客戶端 Client 發(fā)起 SYN Flood 攻擊:

  1. $ sudo hping3 -S 33.9.192.157 -p 8888 --flood 
  2. HPING 33.9.192.157 (eth0 33.9.192.157): S set, 40 headers + 0 data bytes 
  3. hping in flood mode, no replies will be shown 

查看服務(wù)端 Server 8888端口處于 SYN_RECV 狀態(tài)的 socket 最大個數(shù):

  1. [zechen.hg@function-compute033009192157.na63 /home/zechen.hg] 
  2. $ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l 
  3. 256 
  4.  
  5. [zechen.hg@function-compute033009192157.na63 /home/zechen.hg] 
  6. $ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l 
  7. 256 

實驗結(jié)果符合預(yù)期,當半連接隊列長度增長至 256 后,后續(xù) SYN 請求就會觸發(fā) Drop。

回顧線上問題

再回顧值班時遇到的 Connection timeout 問題,當時相關(guān)系統(tǒng)參數(shù)配置為:

  • net.core.somaxconn = 128
  • net.ipv4.tcp_max_syn_backlog = 512
  • net.ipv4.tcp_syncookies = 1
  • net.ipv4.tcp_abort_on_overflow = 0

所以出現(xiàn) Connection timeout 有兩種可能情況:

1、半連接隊列未滿,全連接隊列滿,Client 端向 Server 端發(fā)起 SYN 被 DROP (參考全連接隊列實驗結(jié)果情況三分析、半連接隊列溢出實驗情況三)

2、全連接隊列未滿,半連接隊列大小超過全鏈接隊列最大長度(參考半連接隊列溢出實驗情況三、半連接隊列溢出實驗情況四)

問題的最快修復(fù)方式是將 net.core.somaxconn 調(diào)大,以及 net.ipv4.tcp_abort_on_overflow 設(shè)置為 1,net.ipv4.tcp_abort_on_overflow 設(shè)置為 1 是為了讓 client fail fast。

總結(jié)

半連接隊列溢出、全連接隊列溢出這類問題很容易被忽略,同時這類問題又很致命。當半連接隊列、全連接隊列溢出時 Server 端,從監(jiān)控上來看系統(tǒng) cpu 水位、內(nèi)存水位、網(wǎng)絡(luò)連接數(shù)等一切正常,然而卻會持續(xù)影響 Client 端業(yè)務(wù)請求。對于高負載上游使用短連接的情況,出現(xiàn)這類問題的可能性更大。

本文詳細梳理了 TCP 半連接隊列、全連接隊列的理論知識,同時結(jié)合 Linux 相關(guān)內(nèi)核代碼以及詳細的動手實驗,講解了 TCP 半連接隊列、全連接隊列的相關(guān)原理、溢出判斷、問題分析等內(nèi)容,希望大家在閱讀后可以對 TCP 半連接隊列、全連接隊列有更充分的認識。

PS:可以去線上檢查下服務(wù)器的相關(guān)參數(shù)喲~

附錄

這里羅列下相關(guān)參考博文資料:

Linux 源碼

  • https://github.com/torvalds/linux

Linux 詭異的半連接隊列長度

  • https://www.cnblogs.com/zengkefu/p/5606696.html

TCP 半連接隊列和全連接隊列滿了會發(fā)生什么

  • 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

 

責任編輯:武曉燕 來源: 云巔論劍
相關(guān)推薦

2019-09-16 09:29:01

TCP全連接隊列半連接隊列

2015-04-23 18:46:38

TCPTCP協(xié)議

2023-04-06 07:53:56

Redis連接問題K8s

2018-07-05 14:25:01

TCP握手原理

2020-10-14 14:31:37

LinuxTCP連接

2024-01-19 19:22:45

TCPTIME_WAIT

2021-03-17 09:51:31

網(wǎng)絡(luò)編程TCP網(wǎng)絡(luò)協(xié)議

2019-11-17 22:11:11

TCPSYN隊列Accept隊列

2020-01-18 14:11:13

數(shù)據(jù)庫線程技術(shù)

2020-02-17 10:10:43

TCP三次握手四次揮手

2021-11-23 21:21:07

線上排查服務(wù)

2020-11-16 07:19:17

線上函數(shù)性能

2021-12-12 18:12:13

Hbase線上問題

2010-07-07 10:45:22

TCP UDP協(xié)議

2020-08-24 07:34:39

網(wǎng)絡(luò)超時請求

2021-10-14 20:33:16

TCP連接關(guān)閉

2023-11-29 12:12:24

Oceanbase數(shù)據(jù)庫

2012-07-02 13:26:28

電線連接

2014-08-22 09:10:46

2020-10-21 08:17:11

隊列數(shù)據(jù)
點贊
收藏

51CTO技術(shù)棧公眾號