天天看點

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

前言

網上許多部落格針對增大 TCP 半連接配接隊列和全連接配接隊列的方式如下:

  • 增大 TCP 半連接配接隊列的方式是增大 /proc/sys/net/ipv4/tcp_max_syn_backlog;
  • 增大 TCP 全連接配接隊列的方式是增大 listen() 函數中的 backlog;

這裡先跟大家說下,上面的方式都是不準确的。

“你怎麼知道不準确?”

很簡單呀,因為我做了實驗和看了 TCP 協定棧的核心源碼,發現要增大這兩個隊列長度,不是簡簡單單增大某一個參數就可以的。

接下來,就會以實戰 + 源碼分析,帶大家解密 TCP 半連接配接隊列和全連接配接隊列。

“源碼分析,那不是勸退嗎?我們搞 Java 的看不懂呀”

放心,本文的源碼分析不會涉及很深的知識,因為都被我删減了,你隻需要會條件判斷語句 if、左移右移操作符、加減法等基本文法,就可以看懂。

另外,不僅有源碼分析,還會介紹 Linux 排查半連接配接隊列和全連接配接隊列的指令。

“哦?似乎很有看頭,那我姑且看一下吧!”

行,沒有被勸退的小夥伴,值得鼓勵,下面這圖是本文的提綱:

本文提綱

正文

什麼是 TCP 半連接配接隊列和全連接配接隊列?

在 TCP 三次握手的時候,Linux 核心會維護兩個隊列,分别是:

  • 半連接配接隊列,也稱 SYN 隊列;
  • 全連接配接隊列,也稱 accepet 隊列;

服務端收到用戶端發起的 SYN 請求後,核心會把該連接配接存儲到半連接配接隊列,并向用戶端響應 SYN+ACK,接着用戶端會傳回 ACK,服務端收到第三次握手的 ACK 後,核心會把連接配接從半連接配接隊列移除,然後建立新的完全的連接配接,并将其添加到 accept 隊列,等待程序調用 accept 函數時把連接配接取出來。

半連接配接隊列與全連接配接隊列

不管是半連接配接隊列還是全連接配接隊列,都有最大長度限制,超過限制時,核心會直接丢棄,或傳回 RST 包。

實戰 - TCP 全連接配接隊列溢出

如何知道應用程式的 TCP 全連接配接隊列大小?

在服務端可以使用

ss

指令,來檢視 TCP 全連接配接隊列的情況:

但需要注意的是

ss

指令擷取的

Recv-Q/Send-Q

在「LISTEN 狀态」和「非 LISTEN 狀态」所表達的含義是不同的。從下面的核心代碼可以看出差別:

在「LISTEN 狀态」時,

Recv-Q/Send-Q

表示的含義如下:

  • Recv-Q:目前全連接配接隊列的大小,也就是目前已完成三次握手并等待服務端

    accept()

    的 TCP 連接配接;
  • Send-Q:目前全連接配接最大隊列長度,上面的輸出結果說明監聽 8088 端口的 TCP 服務,最大全連接配接長度為 128;

在「非 LISTEN 狀态」時,

Recv-Q/Send-Q

  • Recv-Q:已收到但未被應用程序讀取的位元組數;
  • Send-Q:已發送但未收到确認的位元組數;
如何模拟 TCP 全連接配接隊列溢出的場景?
TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

測試環境

實驗環境:

  • 用戶端和服務端都是 CentOs 6.5 ,Linux 核心版本 2.6.32
  • 服務端 IP 192.168.3.200,用戶端 IP 192.168.3.100
  • 服務端是 Nginx 服務,端口為 8088

這裡先介紹下

wrk

工具,它是一款簡單的 HTTP 壓測工具,它能夠在單機多核 CPU 的條件下,使用系統自帶的高性能 I/O 機制,通過多線程和事件模式,對目标機器産生大量的負載。

本次模拟實驗就使用

wrk

工具來壓力測試服務端,發起大量的請求,一起看看服務端 TCP 全連接配接隊列滿了會發生什麼?有什麼觀察名額?

用戶端執行

wrk

指令對服務端發起壓力測試,并發 3 萬個連接配接:

ss

指令,來檢視目前 TCP 全連接配接隊列的情況:

其間共執行了兩次 ss 指令,從上面的輸出結果,可以發現目前 TCP 全連接配接隊列上升到了 129 大小,超過了最大 TCP 全連接配接隊列。

當超過了 TCP 最大全連接配接隊列,服務端則會丢掉後續進來的 TCP 連接配接,丢掉的 TCP 連接配接的個數會被統計起來,我們可以使用 netstat -s 指令來檢視:

上面看到的 41150 times ,表示全連接配接隊列溢出的次數,注意這個是累計值。可以隔幾秒鐘執行下,如果這個數字一直在增加的話肯定全連接配接隊列偶爾滿了。

從上面的模拟結果,可以得知,當服務端并發處理大量請求時,如果 TCP 全連接配接隊列過小,就容易溢出。發生 TCP 全連接配接隊溢出的時候,後續的請求就會被丢棄,這樣就會出現服務端請求數量上不去的現象。

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

全連接配接隊列溢出

Linux 有個參數可以指定當 TCP 全連接配接隊列滿了會使用什麼政策來回應用戶端。

實際上,丢棄連接配接隻是 Linux 的預設行為,我們還可以選擇向用戶端發送 RST 複位封包,告訴用戶端連接配接已經建立失敗。

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

tcp_abort_on_overflow 共有兩個值分别是 0 和 1,其分别表示:

  • 0 :如果全連接配接隊列滿了,那麼 server 扔掉 client 發過來的 ack ;
  • 1 :如果全連接配接隊列滿了,server 發送一個

    reset

    包給 client,表示廢掉這個握手過程和這個連接配接;

如果要想知道用戶端連接配接不上服務端,是不是服務端 TCP 全連接配接隊列滿的原因,那麼可以把 tcp_abort_on_overflow 設定為 1,這時如果在用戶端異常中可以看到很多

connection reset by peer

的錯誤,那麼就可以證明是由于服務端 TCP 全連接配接隊列溢出的問題。

通常情況下,應當把 tcp_abort_on_overflow 設定為 0,因為這樣更有利于應對突發流量。

舉個例子,當 TCP 全連接配接隊列滿導緻伺服器丢掉了 ACK,與此同時,用戶端的連接配接狀态卻是 ESTABLISHED,程序就在建立好的連接配接上發送請求。隻要伺服器沒有為請求回複 ACK,請求就會被多次重發。如果伺服器上的程序隻是短暫的繁忙造成 accept 隊列滿,那麼當 TCP 全連接配接隊列有空位時,再次接收到的請求封包由于含有 ACK,仍然會觸發伺服器端成功建立連接配接。

是以,tcp_abort_on_overflow 設為 0 可以提高連接配接建立的成功率,隻有你非常肯定 TCP 全連接配接隊列會長期溢出時,才能設定為 1 以盡快通知用戶端。

如何增大 TCP 全連接配接隊列呢?

是的,當發現 TCP 全連接配接隊列發生溢出的時候,我們就需要增大該隊列的大小,以便可以應對用戶端大量的請求。

TCP 全連接配接隊列的最大值取決于 somaxconn 和 backlog 之間的最小值,也就是 min(somaxconn, backlog)。從下面的 Linux 核心代碼可以得知:

  • somaxconn

    是 Linux 核心的參數,預設值是 128,可以通過

    /proc/sys/net/core/somaxconn

    來設定其值;
  • backlog

    listen(int sockfd, int backlog)

    函數中的 backlog 大小,Nginx 預設值是 511,可以通過修改配置檔案設定其長度;

前面模拟測試中,我的測試環境:

  • somaxconn 是預設值 128;
  • Nginx 的 backlog 是預設值 511

是以測試環境的 TCP 全連接配接隊列最大值為 min(128, 511),也就是

128

,可以執行

ss

指令檢視:

現在我們重新壓測,把 TCP 全連接配接隊列搞大,把

somaxconn

設定成 5000:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

接着把 Nginx 的 backlog 也同樣設定成 5000:

最後要重新開機 Nginx 服務,因為隻有重新調用

listen()

函數 TCP 全連接配接隊列才會重新初始化。

重新開機完後 Nginx 服務後,服務端執行 ss 指令,檢視 TCP 全連接配接隊列大小:

從執行結果,可以發現 TCP 全連接配接最大值為 5000。

增大 TCP 全連接配接隊列後,繼續壓測

用戶端同樣以 3 萬個連接配接并發發送請求給服務端:

服務端執行

ss

指令,檢視 TCP 全連接配接隊列使用情況:

從上面的執行結果,可以發現全連接配接隊列使用增長的很快,但是一直都沒有超過最大值,是以就不會溢出,那麼

netstat -s

就不會有 TCP 全連接配接隊列溢出個數的顯示:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

說明 TCP 全連接配接隊列最大值從 128 增大到 5000 後,服務端抗住了 3 萬連接配接并發請求,也沒有發生全連接配接隊列溢出的現象了。

如果持續不斷地有連接配接因為 TCP 全連接配接隊列溢出被丢棄,就應該調大 backlog 以及 somaxconn 參數。

實戰 - TCP 半連接配接隊列溢出

如何檢視 TCP 半連接配接隊列長度?

很遺憾,TCP 半連接配接隊列長度的長度,沒有像全連接配接隊列那樣可以用 ss 指令檢視。

但是我們可以抓住 TCP 半連接配接的特點,就是服務端處于

SYN_RECV

狀态的 TCP 連接配接,就是 TCP 半連接配接隊列。

于是,我們可以使用如下指令計算目前 TCP 半連接配接隊列長度:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?
如何模拟 TCP 半連接配接隊列溢出場景?

模拟 TCP 半連接配接溢出場景不難,實際上就是對服務端一直發送 TCP SYN 包,但是不回第三次握手 ACK,這樣就會使得服務端有大量的處于

SYN_RECV

狀态的 TCP 連接配接。

這其實也就是所謂的 SYN 洪泛、SYN 攻擊、DDos 攻擊。

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

注意:本次模拟實驗是沒有開啟 tcp_syncookies,關于 tcp_syncookies 的作用,後續會說明。

本次實驗使用

hping3

工具模拟 SYN 攻擊:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

當服務端受到 SYN 攻擊後,連接配接服務端 ssh 就會斷開了,無法再連上。隻能在服務端主機上執行檢視目前 TCP 半連接配接隊列大小:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

同時,還可以通過 netstat -s 觀察半連接配接隊列溢出的情況:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

上面輸出的數值是累計值,表示共有多少個 TCP 連接配接因為半連接配接隊列溢出而被丢棄。隔幾秒執行幾次,如果有上升的趨勢,說明目前存在半連接配接隊列溢出的現象。

大部分人都說 tcp_max_syn_backlog 是指定半連接配接隊列的大小,是真的嗎?

很遺憾,半連接配接隊列的大小并不單單隻跟

tcp_max_syn_backlog

有關系。

上面模拟 SYN 攻擊場景時,服務端的 tcp_max_syn_backlog 的預設值如下:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

但是在測試的時候發現,服務端最多隻有 256 個半連接配接隊列,而不是 512,是以半連接配接隊列的最大長度不一定由 tcp_max_syn_backlog 值決定的。

接下來,走進 Linux 核心的源碼,來分析 TCP 半連接配接隊列的最大值是如何決定的。

TCP 第一次握手(收到 SYN 包)的 Linux 核心代碼如下,其中縮減了大量的代碼,隻需要重點關注 TCP 半連接配接隊列溢出的處理邏輯:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

從源碼中,我可以得出共有三個條件因隊列長度的關系而被丢棄的:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?
  1. 如果半連接配接隊列滿了,并且沒有開啟 tcp_syncookies,則會丢棄;
  2. 若全連接配接隊列滿了,且沒有重傳 SYN+ACK 包的連接配接請求多于 1 個,則會丢棄;
  3. 如果沒有開啟 tcp_syncookies,并且 max_syn_backlog 減去 目前半連接配接隊列長度小于 (max_syn_backlog >> 2),則會丢棄;

關于 tcp_syncookies 的設定,後面在詳細說明,可以先給大家說一下,開啟 tcp_syncookies 是緩解 SYN 攻擊其中一個手段。

接下來,我們繼續跟一下檢測半連接配接隊列是否滿的函數 inet_csk_reqsk_queue_is_full 和 檢測全連接配接隊列是否滿的函數 sk_acceptq_is_full :

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

從上面源碼,可以得知:

  • 全連接配接隊列的最大值是

    sk_max_ack_backlog

    變量,sk_max_ack_backlog 實際上是在 listen() 源碼裡指定的,也就是 min(somaxconn, backlog);
  • 半連接配接隊列的最大值是

    max_qlen_log

    變量,max_qlen_log 是在哪指定的呢?現在暫時還不知道,我們繼續跟進;

我們繼續跟進代碼,看一下是哪裡初始化了半連接配接隊列的最大值 max_qlen_log:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

從上面的代碼中,我們可以算出 max_qlen_log 是 8,于是代入到 檢測半連接配接隊列是否滿的函數 reqsk_queue_is_full :

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

也就是

qlen >> 8

什麼時候為 1 就代表半連接配接隊列滿了。這計算這不難,很明顯是當 qlen 為 256 時,

256 >> 8 = 1

至此,總算知道為什麼上面模拟測試 SYN 攻擊的時候,服務端處于

SYN_RECV

連接配接最大隻有 256 個。

可見,半連接配接隊列最大值不是單單由 max_syn_backlog 決定,還跟 somaxconn 和 backlog 有關系。

在 Linux 2.6.32 核心版本,它們之間的關系,總體可以概況為:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?
  • 當 max_syn_backlog > min(somaxconn, backlog) 時, 半連接配接隊列最大值 max_qlen_log = min(somaxconn, backlog) * 2;
  • 當 max_syn_backlog < min(somaxconn, backlog) 時, 半連接配接隊列最大值 max_qlen_log = max_syn_backlog * 2;
半連接配接隊列最大值 max_qlen_log 就表示服務端處于 SYN_REVC 狀态的最大個數嗎?

依然很遺憾,并不是。

max_qlen_log 是理論半連接配接隊列最大值,并不一定代表服務端處于 SYN_REVC 狀态的最大個數。

在前面我們在分析 TCP 第一次握手(收到 SYN 包)時會被丢棄的三種條件:

假設條件 1 目前半連接配接隊列的長度 「沒有超過」理論的半連接配接隊列最大值 max_qlen_log,那麼如果條件 3 成立,則依然會丢棄 SYN 包,也就會使得服務端處于 SYN_REVC 狀态的最大個數不會是理論值 max_qlen_log。

似乎很難了解,我們繼續接着做實驗,實驗見真知。

服務端環境如下:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

配置完後,服務端要重新開機 Nginx,因為全連接配接隊列最大值和半連接配接隊列最大值是在 listen() 函數初始化。

根據前面的源碼分析,我們可以計算出半連接配接隊列 max_qlen_log 的最大值為 256:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

用戶端執行 hping3 發起 SYN 攻擊:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

服務端執行如下指令,檢視處于 SYN_RECV 狀态的最大個數:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

可以發現,服務端處于 SYN_RECV 狀态的最大個數并不是 max_qlen_log 變量的值。

這就是前面所說的原因:如果目前半連接配接隊列的長度 「沒有超過」理論半連接配接隊列最大值 max_qlen_log,那麼如果條件 3 成立,則依然會丢棄 SYN 包,也就會使得服務端處于 SYN_REVC 狀态的最大個數不會是理論值 max_qlen_log。

我們來分析一波條件 3 :

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

從上面的分析,可以得知如果觸發「目前半連接配接隊列長度 > 192」條件,TCP 第一次握手的 SYN 包是會被丢棄的。

在前面我們測試的結果,服務端處于 SYN_RECV 狀态的最大個數是 193,正好是觸發了條件 3,是以處于 SYN_RECV 狀态的個數還沒到「理論半連接配接隊列最大值 256」,就已經把 SYN 包丢棄了。

是以,服務端處于 SYN_RECV 狀态的最大個數分為如下兩種情況:

  • 如果「目前半連接配接隊列」沒超過「理論半連接配接隊列最大值」,但是超過 max_syn_backlog - (max_syn_backlog >> 2),那麼處于 SYN_RECV 狀态的最大個數就是 max_syn_backlog - (max_syn_backlog >> 2);
  • 如果「目前半連接配接隊列」超過「理論半連接配接隊列最大值」,那麼處于 SYN_RECV 狀态的最大個數就是「理論半連接配接隊列最大值」;
每個 Linux 核心版本「理論」半連接配接最大值計算方式會不同。

在上面我們是針對 Linux 2.6.32 版本分析的「理論」半連接配接最大值的算法,可能每個版本有些不同。

比如在 Linux 5.0.0 的時候,「理論」半連接配接最大值就是全連接配接隊列最大值,但依然還是有隊列溢出的三個條件:

如果 SYN 半連接配接隊列已滿,隻能丢棄連接配接嗎?

并不是這樣,開啟 syncookies 功能就可以在不使用 SYN 半連接配接隊列的情況下成功建立連接配接,在前面我們源碼分析也可以看到這點,當開啟了 syncookies 功能就不會丢棄連接配接。

syncookies 是這麼做的:伺服器根據目前狀态計算出一個值,放在己方發出的 SYN+ACK 封包中發出,當用戶端傳回 ACK 封包時,取出該值驗證,如果合法,就認為連接配接建立成功,如下圖所示。

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

開啟 syncookies 功能

syncookies 參數主要有以下三個值:

  • 0 值,表示關閉該功能;
  • 1 值,表示僅當 SYN 半連接配接隊列放不下時,再啟用它;
  • 2 值,表示無條件開啟功能;

那麼在應對 SYN 攻擊時,隻需要設定為 1 即可:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?
如何防禦 SYN 攻擊?

這裡給出幾種防禦 SYN 攻擊的方法:

  • 增大半連接配接隊列;
  • 開啟 tcp_syncookies 功能
  • 減少 SYN+ACK 重傳次數

方式一:增大半連接配接隊列

在前面源碼和實驗中,得知要想增大半連接配接隊列,我們得知不能隻單純增大 tcp_max_syn_backlog 的值,還需一同增大 somaxconn 和 backlog,也就是增大全連接配接隊列。否則,隻單純增大 tcp_max_syn_backlog 是無效的。

增大 tcp_max_syn_backlog 和 somaxconn 的方法是修改 Linux 核心參數:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

增大 backlog 的方式,每個 Web 服務都不同,比如 Nginx 增大 backlog 的方法如下:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

最後,改變了如上這些參數後,要重新開機 Nginx 服務,因為半連接配接隊列和全連接配接隊列都是在 listen() 初始化的。

方式二:開啟 tcp_syncookies 功能

開啟 tcp_syncookies 功能的方式也很簡單,修改 Linux 核心參數:

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

方式三:減少 SYN+ACK 重傳次數

當服務端受到 SYN 攻擊時,就會有大量處于 SYN_REVC 狀态的 TCP 連接配接,處于這個狀态的 TCP 會重傳 SYN+ACK ,當重傳超過次數達到上限後,就會斷開連接配接。

那麼針對 SYN 攻擊的場景,我們可以減少 SYN+ACK 的重傳次數,以加快處于 SYN_REVC 狀态的 TCP 連接配接斷開。

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

巨人的肩膀

[1] 系統性能調優必知必會.陶輝.極客時間.

[2] https://www.cnblogs.com/zengkefu/p/5606696.html

[3] https://blog.cloudflare.com/syn-packet-handling-in-the-wild/

唠叨唠叨

本文是以 Linux 2.6.32 版本的核心用實驗 + 源碼的方式,給大家說明了 TCP 半連接配接隊列和全連接配接隊列,我們可以看到 TCP 半連接配接隊列「并不是」如網上說的那樣 tcp_max_syn_backlog 表示半連接配接隊列。

TCP 半連接配接隊列的大小對于不同的 Linux 核心版本會有不同的計算方式,是以并不要求大家要死記住本文計算 TCP 半連接配接隊列的大小。

重要的是要學會自我源碼分析,這樣不管碰到什麼版本的 Linux 核心,都不再怕了。

網上搜尋出來的資訊,并不一定針對你的系統,通過自我分析一波,你會更了解你目前使用的 Linux 核心版本!

小林是專為大家圖解的工具人,Goodbye,我們下次見!

TCP 半連接配接隊列和全連接配接隊列滿了會發生什麼?又該如何應對?

讀者問答

讀者問:“咦 我比較好奇部落客都是從哪裡學到這些知識的呀?書籍?視訊?還是多種參考資料”

你可以看我的參考文獻呀,知識點我主要是在極客專欄學的,實戰模拟實驗和源碼解析是自己瞎折騰出來的。

讀者問:“syncookies 啟用後就不需要半連結了?那請求的資料會存在哪裡?”

syncookies = 1 時,半連接配接隊列滿後,後續的請求就不會存放到半連接配接隊列了,而是在第二次握手的時候,服務端會計算一個 cookie 值,放入到 SYN +ACK 包中的序列号發給用戶端,用戶端收到後并回 ack ,服務端就會校驗連接配接是否合法,合法就直接把連接配接放入到全連接配接隊列。

關注公衆号:「小林coding」 ,回複「我要學習」即可免費獲得「伺服器 Linux C/C++ 」成長路程(書籍資料 + 思維導圖)

繼續閱讀