成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

網絡 通信技術
某次大促值班 ing,對系統穩定性有著充分信心、心態穩如老狗的筆者突然收到上游反饋有萬分幾的概率請求我們 endpoint 會出現 Connection timeout 。

[[431611]]

本文轉載自微信公眾號「云巔論劍」,作者黃剛。轉載本文請聯系云巔論劍公眾號。

前言

某次大促值班 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 命令可以查看到全連接隊列的信息

  1. # -n 不解析服務名稱 
  2. # -t 只顯示 tcp sockets 
  3. # -l 顯示正在監聽(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 狀態的 socket

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

對于非 LISTEN 狀態的 socket

  • Recv-Q:已收到但未被應用程序讀取的字節數
  • Send-Q:已發送但未收到確認的字節數

相關內核代碼:

  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 狀態是 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 狀態不是 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);    // 已收到但未被應用程序讀取的字節數 
  14.     r->idiag_wqueue = READ_ONCE(tp->write_seq) - tp->snd_una;   // 已發送但未收到確認的字節數 
  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 

上面輸出的數值是累計值,分別表示有多少 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 文件中的值。

相關內核代碼:

  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; 

實驗

服務端 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.   } 

在測試環境查看 somaxconn 的值為 128

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

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

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

全連接隊列最大長度為 128

現在更新 somaxconn 值為 1024,再重新啟動服務端。

1、更新 /etc/sysctl.conf 文件,該文件為內核參數配置文件

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

2、執行 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 

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

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

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

全連接隊列溢出

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

實驗物料

服務端 server 代碼

  1. // server 端監聽 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 端并發請求 10 次 server 端,成功建立 tcp 連接后向 server 端發送數據 
  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、執行 sysctl -p 使配置生效

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

實驗結果

客戶端日志輸出

  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 

服務端 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)) 

抓包結果

對客戶端、服務端抓包后,發現出現了三種情況,分別是:

  • 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
  1. State      Recv-Q Send-Q    Local Address:Port         Peer Address:Port 
  2. LISTEN     6      5         [::]:8888                  [::]:* 

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

經過多次實驗發現,能夠進入全連接隊列的 Socket 最大數量始終比配置的全連接隊列最大長度 + 1。

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

相關內核代碼:

  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 發送 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 的大小。

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

  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: 將參數向上取整到最小的 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 

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

  • 調用 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),其他配置采用默認值,來計算下半連接隊列的最大長度

  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 端發送 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 內核判斷的是當前半連接隊列長度是否 >= 全連接隊列最大長度。

相關內核代碼:

  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; 

我們假設如下參數,來計算下當 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

將相關參數的配置更新

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

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

客戶端 Client 發起 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 

查看服務端 Server 8888端口處于 SYN_RECV 狀態的 socket 最大個數:

  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 

實驗結果符合預期,當半連接隊列長度增長至 96 后,后續 SYN 請求就會觸發 Drop。

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

理論上:

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

將相關參數的配置更新

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

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

客戶端 Client 發起 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 

查看服務端 Server 8888端口處于 SYN_RECV 狀態的 socket 最大個數:

  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 

實驗結果符合預期,當半連接隊列長度增長至 128 后,后續 SYN 請求就會觸發 Drop

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

理論上:

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

將相關參數的配置更新

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

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

客戶端 Client 發起 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 

查看服務端 Server 8888端口處于 SYN_RECV 狀態的 socket 最大個數:

  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 

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

繼續做實驗,將 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 

發起 SYN Flood 攻擊后,查看服務端 Server 8888端口處于 SYN_RECV 狀態的 socket 最大個數:

  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,當半連接隊列長度 > 全連接最大長度時,就會觸發 DROP SYN 請求。

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

理論上:

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

將相關參數的配置更新

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

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

客戶端 Client 發起 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 

查看服務端 Server 8888端口處于 SYN_RECV 狀態的 socket 最大個數:

  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 

實驗結果符合預期,當半連接隊列長度增長至 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

 

責任編輯:武曉燕 來源: 云巔論劍
相關推薦

2019-09-16 09:29:01

TCP全連接隊列半連接隊列

2015-04-23 18:46:38

TCPTCP協議

2023-04-06 07:53:56

Redis連接問題K8s

2018-07-05 14:25:01

TCP握手原理

2024-01-19 19:22:45

TCPTIME_WAIT

2020-10-14 14:31:37

LinuxTCP連接

2021-03-17 09:51:31

網絡編程TCP網絡協議

2019-11-17 22:11:11

TCPSYN隊列Accept隊列

2020-01-18 14:11:13

數據庫線程技術

2020-02-17 10:10:43

TCP三次握手四次揮手

2021-11-23 21:21:07

線上排查服務

2020-11-16 07:19:17

線上函數性能

2021-12-12 18:12:13

Hbase線上問題

2010-07-07 10:45:22

TCP UDP協議

2020-08-24 07:34:39

網絡超時請求

2023-11-29 12:12:24

Oceanbase數據庫

2021-10-14 20:33:16

TCP連接關閉

2012-07-02 13:26:28

電線連接

2014-08-22 09:10:46

2020-10-21 08:17:11

隊列數據
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 在线三级网址 | 精品免费 | 天天干狠狠 | 久久中文网 | 午夜网站视频 | 亚洲黄色av网站 | 亚洲精品女人久久久 | 天堂在线免费视频 | 日韩av免费在线观看 | 中文字幕一级 | 欧美精品1区2区 | 日本久久精品 | 久久国产秒| 国产欧美一区二区三区久久人妖 | 国产精品视频久久久 | 国产一区91精品张津瑜 | 国产三级精品视频 | 中文字幕免费 | 久久91av| 国产精品视频一区二区三区 | 日本中文字幕一区 | 欧洲视频一区二区 | 国产日韩欧美激情 | 色婷婷亚洲一区二区三区 | 亚洲欧美一区二区三区视频 | 国产成人精品久久久 | 亚洲欧美在线视频 | 欧美日韩网站 | 国产精品免费一区二区三区四区 | 91 视频网站 | 亚洲最色视频 | 亚洲欧美一区二区三区1000 | 欧美激情综合 | 欧美性久久 | 在线日韩欧美 | 欧美婷婷 | 午夜国产在线 | 久久av一区二区三区 | www.99re| 人妖无码| 精品国产91 |