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

每日英語,天天進步一點點:


前言

網上許多博客針對增大 TCP 半鏈接隊列和全鏈接隊列的方式以下:html

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

這裏先跟你們說下,上面的方式都是不許確的。算法

「你怎麼知道不許確?」

很簡單呀,由於我作了實驗和看了 TCP 協議棧的內核源碼,發現要增大這兩個隊列長度,不是簡簡單單增大某一個參數就能夠的。服務器

接下來,就會以實戰 + 源碼分析,帶你們解密 TCP 半鏈接隊列和全鏈接隊列。cookie

「源碼分析,那不是勸退嗎?咱們搞 Java 的看不懂呀」

放心,本文的源碼分析不會涉及很深的知識,由於都被我刪減了,你只須要會條件判斷語句 if、左移右移操做符、加減法等基本語法,就能夠看懂。多線程

另外,不只有源碼分析,還會介紹 Linux 排查半鏈接隊列和全鏈接隊列的命令。併發

「哦?彷佛頗有看頭,那我姑且看一下吧!」

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

本文提綱


正文

什麼是 TCP 半鏈接隊列和全鏈接隊列?

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

  • 半鏈接隊列,也稱 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 全鏈接隊列溢出的場景?

測試環境

實驗環境:

  • 客戶端和服務端都是 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 全鏈接隊溢出的時候,後續的請求就會被丟棄,這樣就會出現服務端請求數量上不去的現象。

全鏈接隊列溢出

Linux 有個參數能夠指定當 TCP 全鏈接隊列滿了會使用什麼策略來回應客戶端。

實際上,丟棄鏈接只是 Linux 的默認行爲,咱們還能夠選擇向客戶端發送 RST 復位報文,告訴客戶端鏈接已經創建失敗。

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 來設置其值;
  • backloglisten(int sockfd, int backlog) 函數中的 backlog 大小,Nginx 默認值是 511,能夠經過修改配置文件設置其長度;

前面模擬測試中,個人測試環境:

  • somaxconn 是默認值 128;
  • Nginx 的 backlog 是默認值 511

因此測試環境的 TCP 全鏈接隊列最大值爲 min(128, 511),也就是 128,能夠執行 ss 命令查看:

如今咱們從新壓測,把 TCP 全鏈接隊列搞大,把 somaxconn 設置成 5000:

接着把 Nginx 的 backlog 也一樣設置成 5000:

最後要重啓 Nginx 服務,由於只有從新調用 listen() 函數 TCP 全鏈接隊列纔會從新初始化。

重啓完後 Nginx 服務後,服務端執行 ss 命令,查看 TCP 全鏈接隊列大小:

從執行結果,能夠發現 TCP 全鏈接最大值爲 5000。

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

客戶端一樣以 3 萬個鏈接併發發送請求給服務端:

服務端執行 ss 命令,查看 TCP 全鏈接隊列使用狀況:

從上面的執行結果,能夠發現全鏈接隊列使用增加的很快,可是一直都沒有超過最大值,因此就不會溢出,那麼 netstat -s 就不會有 TCP 全鏈接隊列溢出個數的顯示:

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

若是持續不斷地有鏈接由於 TCP 全鏈接隊列溢出被丟棄,就應該調大 backlog 以及 somaxconn 參數。


實戰 - TCP 半鏈接隊列溢出

如何查看 TCP 半鏈接隊列長度?

很遺憾,TCP 半鏈接隊列長度的長度,沒有像全鏈接隊列那樣能夠用 ss 命令查看。

可是咱們能夠抓住 TCP 半鏈接的特色,就是服務端處於 SYN_RECV 狀態的 TCP 鏈接,就是在 TCP 半鏈接隊列。

因而,咱們可使用以下命令計算當前 TCP 半鏈接隊列長度:

如何模擬 TCP 半鏈接隊列溢出場景?

模擬 TCP 半鏈接溢出場景不難,實際上就是對服務端一直髮送 TCP SYN 包,可是不回第三次握手 ACK,這樣就會使得服務端有大量的處於 SYN_RECV 狀態的 TCP 鏈接。

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

測試環境

實驗環境:

  • 客戶端和服務端都是 CentOs 6.5 ,Linux 內核版本 2.6.32
  • 服務端 IP 192.168.3.200,客戶端 IP 192.168.3.100
  • 服務端是 Nginx 服務,端口爲 8088

注意:本次模擬實驗是沒有開啓 tcp_syncookies,關於 tcp_syncookies 的做用,後續會說明。

本次實驗使用 hping3 工具模擬 SYN 攻擊:

當服務端受到 SYN 攻擊後,鏈接服務端 ssh 就會斷開了,沒法再連上。只能在服務端主機上執行查看當前 TCP 半鏈接隊列大小:

同時,還能夠經過 netstat -s 觀察半鏈接隊列溢出的狀況:

上面輸出的數值是累計值,表示共有多少個 TCP 鏈接由於半鏈接隊列溢出而被丟棄。隔幾秒執行幾回,若是有上升的趨勢,說明當前存在半鏈接隊列溢出的現象

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

很遺憾,半鏈接隊列的大小並不僅僅只跟 tcp_max_syn_backlog 有關係。

上面模擬 SYN 攻擊場景時,服務端的 tcp_max_syn_backlog 的默認值以下:

可是在測試的時候發現,服務端最多隻有 256 個半鏈接隊列,而不是 512,因此半鏈接隊列的最大長度不必定由 tcp_max_syn_backlog 值決定的

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

TCP 第一次握手(收到 SYN 包)的 Linux 內核代碼以下,其中縮減了大量的代碼,只須要重點關注 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 :

從上面源碼,能夠得知:

  • 鏈接隊列的最大值是 sk_max_ack_backlog 變量,sk_max_ack_backlog 其實是在 listen() 源碼裏指定的,也就是 min(somaxconn, backlog)
  • 鏈接隊列的最大值是 max_qlen_log 變量,max_qlen_log 是在哪指定的呢?如今暫時還不知道,咱們繼續跟進;

咱們繼續跟進代碼,看一下是哪裏初始化了半鏈接隊列的最大值 max_qlen_log:

從上面的代碼中,咱們能夠算出 max_qlen_log 是 8,因而代入到 檢測半鏈接隊列是否滿的函數 reqsk_queue_is_full :

也就是 qlen >> 8 何時爲 1 就表明半鏈接隊列滿了。這計算這不難,很明顯是當 qlen 爲 256 時,256 >> 8 = 1

至此,總算知道爲何上面模擬測試 SYN 攻擊的時候,服務端處於 SYN_RECV 鏈接最大隻有 256 個。

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

在 Linux 2.6.32 內核版本,它們之間的關係,整體能夠概況爲:

  • 當 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. 若是半鏈接隊列滿了,而且沒有開啓 tcp_syncookies,則會丟棄;
  2. 若全鏈接隊列滿了,且沒有重傳 SYN+ACK 包的鏈接請求多於 1 個,則會丟棄;
  3. 若是沒有開啓 tcp_syncookies,而且 max_syn_backlog 減去 當前半鏈接隊列長度小於 (max_syn_backlog >> 2),則會丟棄;

假設條件 1 當前半鏈接隊列的長度 「沒有超過」理論的半鏈接隊列最大值 max_qlen_log,那麼若是條件 3 成立,則依然會丟棄 SYN 包,也就會使得服務端處於 SYN_REVC 狀態的最大個數不會是理論值 max_qlen_log。

彷佛很難理解,咱們繼續接着作實驗,實驗見真知。

服務端環境以下:

配置完後,服務端要重啓 Nginx,由於全鏈接隊列最大和半鏈接隊列最大值是在 listen() 函數初始化。

根據前面的源碼分析,咱們能夠計算出半鏈接隊列 max_qlen_log 的最大值爲 256:

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

服務端執行以下命令,查看處於 SYN_RECV 狀態的最大個數:

能夠發現,服務端處於 SYN_RECV 狀態的最大個數並非 max_qlen_log 變量的值。

這就是前面所說的緣由:若是當前半鏈接隊列的長度 「沒有超過」理論半鏈接隊列最大值 max_qlen_log,那麼若是條件 3 成立,則依然會丟棄 SYN 包,也就會使得服務端處於 SYN_REVC 狀態的最大個數不會是理論值 max_qlen_log。

咱們來分析一波條件 3 :

從上面的分析,能夠得知若是觸發「當前半鏈接隊列長度 > 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 報文時,取出該值驗證,若是合法,就認爲鏈接創建成功,以下圖所示。

開啓 syncookies 功能

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

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

那麼在應對 SYN 攻擊時,只須要設置爲 1 便可:

如何防護 SYN 攻擊?

這裏給出幾種防護 SYN 攻擊的方法:

  • 增大半鏈接隊列;
  • 開啓 tcp_syncookies 功能
  • 減小 SYN+ACK 重傳次數

方式一:增大半鏈接隊列

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

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

增大 backlog 的方式,每一個 Web 服務都不一樣,好比 Nginx 增大 backlog 的方法以下:

最後,改變了如上這些參數後,要重啓 Nginx 服務,由於半鏈接隊列和全鏈接隊列都是在 listen() 初始化的。

方式二:開啓 tcp_syncookies 功能

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

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

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

那麼針對 SYN 攻擊的場景,咱們能夠減小 SYN+ACK 的重傳次數,以加快處於 SYN_REVC 狀態的 TCP 鏈接斷開。


巨人的肩膀

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

[2] https://www.cnblogs.com/zengk...

[3] https://blog.cloudflare.com/s...


小林是專爲你們圖解的工具人,Goodbye,咱們下次見!

相關文章
相關標籤/搜索