TCP半連接隊列和全連接隊列滿了,怎麼破

TCP半連接隊列和全連接隊列滿了,怎麼破

  作者|小林coding

  來源|小林coding

  責編|王曉曼

  前言

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

  增大TCP半連接隊列方式是增大tcp_max_syn_backlog;

  增大TCP全連接隊列方式是增大listen()函數中的backlog;

  這裏先跟大家説下,上面的方式都是不準確的。

  “你怎麼知道不準確?”

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

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

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

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

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

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

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

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

  半連接隊列,也稱SYN隊列;

  全連接隊列,也稱accepet隊列;

  服務端收到客户端發起的SYN請求後,內核會把該連接存儲到半連接隊列,並向客户端響應SYN+ACK,接着客户端會返回ACK,服務端收到第三次握手的ACK後,內核會把連接從半連接隊列移除,然後創建新的完全的連接,並將其添加到accept隊列,等待進程調用accept函數時把連接取出來。

TCP半連接隊列和全連接隊列滿了,怎麼破

  半連接隊列與全連接隊列

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

  實戰-TCP全連接隊列溢出

  1、如何知道應用程序的TCP全連接隊列大小?

  在服務端可以使用ss命令,來查看TCP全連接隊列的情況:

  但需要注意的是ss命令獲取的Recv-Q/Send-Q在「LISTEN狀態」和「非LISTEN狀態」所表達的含義是不同的。從下面的內核代碼可以看出區別:

TCP半連接隊列和全連接隊列滿了,怎麼破

  在「LISTEN狀態」時,Recv-Q/Send-Q表示的含義如下:

  Recv-Q:當前全連接隊列的大小,也就是當前已完成三次握手並等待服務端accept()的TCP連接個數;

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

  在「非LISTEN狀態」時,Recv-Q/Send-Q表示的含義如下:

  Recv-Q:已收到但未被應用進程讀取的字節數;

  Send-Q:已發送但未收到確認的字節數;

  2、如何模擬TCP全連接隊列溢出的場景?

TCP半連接隊列和全連接隊列滿了,怎麼破

  測試環境

  實驗環境:

  客户端和服務端都是CentOs6.5,Linux內核版本2.6.32

  服務端IP192.168.3.200,客户端IP192.168.3.100

  服務端是Nginx服務,端口為8088

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

  本次模擬實驗就使用wrk工具來壓力測試服務端,發起大量的請求,一起看看服務端TCP全連接隊列滿了會發生什麼?有什麼觀察指標?

  客户端執行wrk命令對服務端發起壓力測試,併發3萬個連接:

TCP半連接隊列和全連接隊列滿了,怎麼破

  在服務端可以使用ss命令,來查看當前TCP全連接隊列的情況:

TCP半連接隊列和全連接隊列滿了,怎麼破

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

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

TCP半連接隊列和全連接隊列滿了,怎麼破

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

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

TCP半連接隊列和全連接隊列滿了,怎麼破

  全連接隊列溢出

  3、全連接隊列滿了,就只會丟棄連接嗎?

  實際上,丟棄連接只是Linux的默認行為,我們還可以選擇向客户端發送RST復位報文,告訴客户端連接已經建立失敗。

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

  0:表示如果全連接隊列滿了,那麼server扔掉client發過來的ack;

  1:表示如果全連接隊列滿了,那麼server發送一個reset包給client,表示廢掉這個握手過程和這個連接;

  如果要想知道客户端連接不上服務端,是不是服務端TCP全連接隊列滿的原因,那麼可以把tcp_abort_on_overflow設置為1,這時如果在客户端異常中可以看到很多connectionresetbypeer的錯誤,那麼就可以證明是由於服務端TCP全連接隊列溢出的問題。

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

  舉個例子,當TCP全連接隊列滿導致服務器丟掉了ACK,與此同時,客户端的連接狀態卻是ESTABLISHED,進程就在建立好的連接上發送請求。只要服務器沒有為請求回覆ACK,請求就會被多次重發。如果服務器上的進程只是短暫的繁忙造成accept隊列滿,那麼當TCP全連接隊列有空位時,再次接收到的請求報文由於含有ACK,仍然會觸發服務器端成功建立連接。

  所以,tcp_abort_on_overflow設為0可以提高連接建立的成功率,只有你非常肯定TCP全連接隊列會長期溢出時,才能設置為1以儘快通知客户端。

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

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

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

TCP半連接隊列和全連接隊列滿了,怎麼破

  somaxconn是Linux內核的參數,默認值是128,可以通過/proc/sys/net/core/somaxconn來設置其值;

  backlog是listen(intsockfd,intbacklog)函數中的backlog大小,Nginx默認值是511,可以通過修改配置文件設置其長度;

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

  somaxconn是默認值128;

  Nginx的backlog是默認值511

  所以測試環境的TCP全連接隊列最大值為min(128,511),也就是128,可以執行ss命令查看:

  現在我們重新壓測,把TCP全連接隊列搞大,把somaxconn設置成5000:

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

TCP半連接隊列和全連接隊列滿了,怎麼破

  最後要重啓Nginx服務,因為只有重新調用listen()函數,TCP全連接隊列才會重新初始化。

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

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

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

  客户端同樣以3萬個連接併發發送請求給服務端:

TCP半連接隊列和全連接隊列滿了,怎麼破

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

TCP半連接隊列和全連接隊列滿了,怎麼破

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

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

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

  實戰-TCP半連接隊列溢出

  1、如何查看TCP半連接隊列長度?

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

  但是我們可以抓住TCP半連接的特點,就是服務端處於SYN_RECV狀態的TCP連接,就是在TCP半連接隊列。

  於是,我們可以使用如下命令計算當前TCP半連接隊列長度:

TCP半連接隊列和全連接隊列滿了,怎麼破

  2、如何模擬TCP半連接隊列溢出場景?

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

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

TCP半連接隊列和全連接隊列滿了,怎麼破

  測試環境

  實驗環境:

  客户端和服務端都是CentOs6.5,Linux內核版本2.6.32

  服務端IP192.168.3.200,客户端IP192.168.3.100

  服務端是Nginx服務,端口為8088

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

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

TCP半連接隊列和全連接隊列滿了,怎麼破

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

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

TCP半連接隊列和全連接隊列滿了,怎麼破

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

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

  很遺憾,半連接隊列的大小並不單單隻跟tcp_max_syn_backlog有關係。

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

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

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

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

TCP半連接隊列和全連接隊列滿了,怎麼破

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

TCP半連接隊列和全連接隊列滿了,怎麼破

  如果半連接隊列滿了,並且沒有開啓tcp_syncookies,則會丟棄;

  若全連接隊列滿了,且沒有重傳SYN+ACK包的連接請求多於1個,則會丟棄;

  如果沒有開啓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有關係。

  在Linux2.6.32內核版本,它們之間的關係,總體可以概況為:

TCP半連接隊列和全連接隊列滿了,怎麼破

  當max_syn_backlog>min(somaxconn,backlog)時,半連接隊列最大值max_qlen_log=min(somaxconn,backlog)*2;

  當max_syn_backlog

  5、半連接隊列最大值max_qlen_log就表示服務端處於SYN_REVC狀態的最大個數嗎?

  依然很遺憾,並不是。

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

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

  如果半連接隊列滿了,並且沒有開啓tcp_syncookies,則會丟棄;

  若全連接隊列滿了,且沒有重傳SYN+ACK包的連接請求多於1個,則會丟棄;

  如果沒有開啓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:

TCP半連接隊列和全連接隊列滿了,怎麼破

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

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

  可以發現,服務端處於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狀態的最大個數就是「理論半連接隊列最大值」;

  6、每個Linux內核版本「理論」半連接最大值計算方式會不同。

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

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

TCP半連接隊列和全連接隊列滿了,怎麼破

  7、如果SYN半連接隊列已滿,只能丟棄連接嗎?

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

  syncookies是這麼做的:服務器根據當前狀態計算出一個值,放在己方發出的SYN+ACK報文中發出,當客户端返回ACK報文時,取出該值驗證,如果合法,就認為連接建立成功,如下圖所示。

TCP半連接隊列和全連接隊列滿了,怎麼破

  開啓syncookies功能

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

  0值,表示關閉該功能;

  1值,表示僅當SYN半連接隊列放不下時,再啓用它;

  2值,表示無條件開啓功能;

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

  8、如何防禦SYN攻擊?

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

  增大半連接隊列;

  開啓tcp_syncookies功能;

  減少SYN+ACK重傳次數。

  (1)方式一:增大半連接隊列

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

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

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

TCP半連接隊列和全連接隊列滿了,怎麼破

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

  (2)方式二:開啓tcp_syncookies功能

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

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

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

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

  參考:

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

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

版權聲明:本文源自 網絡, 於,由 楠木軒 整理發佈,共 7569 字。

轉載請註明: TCP半連接隊列和全連接隊列滿了,怎麼破 - 楠木軒