在上一篇博客(EMQX 性能調(diào)優(yōu):最大連接與文件描述符),我們深入研究了?MQTT 連接與文件描述符之間的關(guān)系,介紹了如何修改文件描述符相關(guān)的內(nèi)核參數(shù)來(lái)突破默認(rèn)的最大連接數(shù)量限制。
但你可能會(huì)發(fā)現(xiàn),在某些情況下,即便當(dāng)前服務(wù)端的 MQTT 連接總數(shù)并未達(dá)到文件描述符限制,客戶端的連接請(qǐng)求仍然失敗。當(dāng)你運(yùn)行以下命令,你將看到以下 Overflowed 和 SYN Dropped 計(jì)數(shù)在不斷增加:
$ watch -d 'netstat -s | grep -i "listen"' 2091 times the listen queue of a socket overflowed 3418 SYNs to LISTEN sockets dropped
本文將介紹導(dǎo)致這一現(xiàn)象的原因以及如何通過(guò)內(nèi)核參數(shù)調(diào)優(yōu)來(lái)解決此問(wèn)題。
SYN 隊(duì)列與 Accept 隊(duì)列
前文提到的這兩個(gè)計(jì)數(shù),通常意味著 SYN 隊(duì)列和 Accept 隊(duì)列發(fā)生了溢出,一旦溢出,客戶端的正常連接就會(huì)受到影響。
所以首先,我們需要了解 SYN 隊(duì)列和 Accept 隊(duì)列分別是什么?
MQTT 作為構(gòu)建在 TCP 之上的應(yīng)用層協(xié)議,我們總是必須先建立 TCP 連接,然后才能發(fā)送?MQTT 協(xié)議的 CONNECT 報(bào)文。建立 TCP 連接,需要經(jīng)過(guò)三次握手:
- 客戶端向服務(wù)器發(fā)送 SYN(同步)報(bào)文,表示要建立連接。
- 服務(wù)器用 SYN-ACK(同步確認(rèn))報(bào)文進(jìn)行響應(yīng),表示愿意建立連接。
- 客戶端回復(fù) ACK(Acknowledge)報(bào)文,表示已收到 SYN-ACK 報(bào)文,連接已建立。
由于網(wǎng)絡(luò)延遲的存在,從服務(wù)端發(fā)出 SYN-ACK 報(bào)文到收到客戶端回復(fù)的 ACK 報(bào)文總是需要一段時(shí)間。在這段時(shí)間內(nèi),服務(wù)端需要暫存本次連接的關(guān)鍵信息,例如 TCP 四元組、MSS(Maximum Segment Size,最大分段大?。┖痛翱诳s放因子(Window Scale)。所以,Linux 內(nèi)核維護(hù)了一個(gè) SYN 隊(duì)列來(lái)存放這些連接信息。
當(dāng)服務(wù)端收到客戶端回復(fù)的 ACK 報(bào)文,連接建立完成,就會(huì)將連接從 SYN 隊(duì)列取出,然后放入同樣由內(nèi)核維護(hù)的 Accept 隊(duì)列,直到上層應(yīng)用調(diào)用?accept()
?將連接從 Accept 隊(duì)列中取出。Accept 隊(duì)列在這里的主要作用是解耦了網(wǎng)絡(luò)層和應(yīng)用層,使 TCP 三次握手和實(shí)際的數(shù)據(jù)傳輸可以并行進(jìn)行,有效提高連接請(qǐng)求的處理效率。
但服務(wù)端的資源是有限的,不管是 SYN 隊(duì)列還是 Accept 隊(duì)列,它們都有著最大長(zhǎng)度限制。如果客戶端一直只發(fā)送 SYN 報(bào)文而不回復(fù)最后的 ACK 報(bào)文,或者服務(wù)端的應(yīng)用程序沒(méi)有及時(shí)地調(diào)用?accept()
,那么這兩個(gè)隊(duì)列就會(huì)有溢出的風(fēng)險(xiǎn)。
SYN 隊(duì)列溢出時(shí)將發(fā)生什么?
服務(wù)端在 SYN 隊(duì)列溢出時(shí)的行為,主要由?net.ipv4.tcp_syncookies
?選項(xiàng)決定。
由于 SYN 隊(duì)列的長(zhǎng)度總是有限的,所以一些攻擊者會(huì)嘗試采用發(fā)送大量 SYN 報(bào)文的方式來(lái)對(duì)服務(wù)端發(fā)起攻擊,企圖耗盡服務(wù)端的 SYN 隊(duì)列來(lái)阻止合法連接的建立,這也就是我們常說(shuō)的 SYN 泛洪攻擊 (SYN Flood Attack)。
SYN Cookie 機(jī)制被設(shè)計(jì)用于解決這一問(wèn)題。簡(jiǎn)單來(lái)說(shuō),當(dāng)啟用這一機(jī)制后,Linux 在收到 SYN 報(bào)文時(shí)將基于時(shí)間戳、四元組等信息計(jì)算出一個(gè) Cookie,然后作為 SYN-ACK 報(bào)文的序列號(hào)后返回給客戶端??蛻舳嗽?ACK 中將序列號(hào)加一返回,服務(wù)端只需要減一就可以逆推出原始的 Cookie。因此,服務(wù)端不需要再將連接請(qǐng)求放入 SYN 隊(duì)列。
但 SYN Cookie 機(jī)制也存在一些弊端:
- Cookie 的計(jì)算并未包含 SACK(Selective Acknowledgment,選擇性確認(rèn)) 和 Window Scale (窗口縮放因子)這些 TCP 選項(xiàng),啟用 SYN Cookie 后,服務(wù)端將不會(huì)保存這些選項(xiàng),所以這些功能將無(wú)法使用。雖然從 Linux 內(nèi)核 v2.6.26 開(kāi)始,我們可以啟用 TCP Timestamps 選項(xiàng)(
net.ipv4.tcp_timestamps
),借助 32 Bit 時(shí)間戳的低 6 bit 來(lái)存放這些 TCP 選項(xiàng),但 TCP Timestamps 需要客戶端和服務(wù)端的共同支持才會(huì)真正啟用。 - 用于生成 Cookie 的 Hash 計(jì)算增加了服務(wù)端的負(fù)載。
所以目前?net.ipv4.tcp_syncookies
?選項(xiàng)一共有三個(gè)可取值:
-
net.ipv4.tcp_syncookies = 0
,表示關(guān)閉 SYN Cookie 機(jī)制,如果 SYN 隊(duì)列已滿,那么新到的 SYN 報(bào)文將被丟棄。 -
net.ipv4.tcp_syncookies = 1
,表示 SYN Cookie 機(jī)制僅在 SYN 隊(duì)列滿時(shí)才正式啟用。 -
net.ipv4.tcp_syncookies = 2
,表示無(wú)條件啟用 SYN Cookie 機(jī)制。
不同類型和版本的操作系統(tǒng)中,net.ipv4.tcp_syncookies
?的默認(rèn)值可能不同,你可以運(yùn)行以下命令查看當(dāng)前值:
sysctl -n net.ipv4.tcp_syncookies
考慮到 SYN Cookie 可能帶來(lái)的副作用,通常我們建議僅在 SYN 隊(duì)列滿時(shí)才啟用 SYN Cookie(將?net.ipv4.tcp_syncookies
?設(shè)置為 1),優(yōu)先盡可能地增加 SYN 隊(duì)列最大長(zhǎng)度。運(yùn)行以下命令以修改此選項(xiàng):
sysctl -w net.ipv4.tcp_syncookies=1
RTT 對(duì) SYN 隊(duì)列的影響
連接請(qǐng)求在 SYN 隊(duì)列中停留的時(shí)間,基本等同于服務(wù)端發(fā)出 SYN-ACK 報(bào)文到收到客戶端返回的 ACK 報(bào)文的時(shí)間。換句話說(shuō),它完全取決于報(bào)文在客戶端與服務(wù)端之間的往返時(shí)間(Round Trip Time,RTT)。
RTT 越長(zhǎng),那么連接請(qǐng)求將越容易占滿 SYN 隊(duì)列。假設(shè) RTT 為 200 ms,SYN 隊(duì)列最大長(zhǎng)度為 512,那么只要每秒向服務(wù)端發(fā)起連接請(qǐng)求的客戶端數(shù)量超過(guò) 2560 個(gè)就會(huì)造成 SYN 隊(duì)列溢出。
查看當(dāng)前 SYN 隊(duì)列大小
Linux 沒(méi)有提供相應(yīng)的內(nèi)核參數(shù)供我們直接查看當(dāng)前 SYN 隊(duì)列大小,但通過(guò)前文我們可以知道 SYN 隊(duì)列中的連接都處于 SYN-RECEIVED 狀態(tài),因此我們可以借助 netstat 命令統(tǒng)計(jì) SYN-RECEIVED 狀態(tài)的連接數(shù)量來(lái)間接獲得當(dāng)前 SYN 隊(duì)列的大?。?/p>
sudo netstat -antp | grep SYN_RECV | wc -l
如何確認(rèn) SYN 隊(duì)列發(fā)生溢出?
當(dāng) SYN 報(bào)文因?yàn)?SYN 隊(duì)列滿而被丟棄時(shí),服務(wù)端中的以下計(jì)數(shù)會(huì)相應(yīng)增加:
$ netstat -s | grep "LISTEN" <Number> SYNs to LISTEN sockets dropped
不過(guò) SYN 報(bào)文也可能因?yàn)?Accept 隊(duì)列滿而被丟棄,所以還需要結(jié)合 Accept 隊(duì)列的情況綜合判斷。
需要注意的是,在 CentOS 中,即便啟用了 SYN Cookie 機(jī)制,服務(wù)端已經(jīng)不會(huì)再丟棄 SYN 報(bào)文,此 SYN 丟棄計(jì)數(shù)仍然可能增加:
- SYN Cookie 設(shè)置為 1 時(shí),如果 SYN 隊(duì)列已滿,那么新到的 SYN 報(bào)文仍然會(huì)使此計(jì)數(shù)增加。這可以幫助我們?cè)u(píng)估是否需要增加 SYN 隊(duì)列的最大長(zhǎng)度。
- SYN Cookie 設(shè)置為 2 時(shí),此計(jì)數(shù)不存在任何意義。
Accept 隊(duì)列溢出時(shí)將發(fā)生什么?
服務(wù)端在 Accept 隊(duì)列溢出時(shí)的行為,主要由?net.ipv4.tcp_abort_on_overflow
?選項(xiàng)決定。
通常情況下,此選項(xiàng)的默認(rèn)值為 0,即當(dāng) Accept 隊(duì)列溢出時(shí),服務(wù)端將直接丟棄第三次握手的 ACK 報(bào)文,并視作從未收到該 ACK 報(bào)文,因此服務(wù)端將重傳 SYN-ACK 報(bào)文,最大重傳次數(shù)由?net.ipv4.tcp_synack_retries
?選項(xiàng)決定。
雖然服務(wù)端丟棄了 ACK 報(bào)文,但是對(duì)客戶端來(lái)說(shuō),三次握手已經(jīng)完成,所以它可以發(fā)送后續(xù)的應(yīng)用數(shù)據(jù)。不過(guò)這些攜帶了應(yīng)用數(shù)據(jù)的 PSH 也會(huì)和 ACK 報(bào)文一樣被服務(wù)端直接丟棄。由于收不到響應(yīng),客戶端將不斷地重傳 PSH 報(bào)文,PSH 報(bào)文的最大重傳次數(shù)由?net.ipv4.tcp_retries2
?選項(xiàng)決定。
將?net.ipv4.tcp_abort_on_overflow
?設(shè)置為 0 的好處是如果在 SYN-ACK 或 PSH 報(bào)文達(dá)到最大重傳次數(shù)前,上層應(yīng)用及時(shí)地取出連接使 Accept 隊(duì)列出現(xiàn)空位,那么連接可以直接恢復(fù),這更有利于應(yīng)對(duì)突發(fā)流量。但如果 Accept 隊(duì)列過(guò)短,導(dǎo)致客戶端和服務(wù)端過(guò)早地重傳報(bào)文,也會(huì)浪費(fèi)流量以及降低連接效率。
相反,如果將?net.ipv4.tcp_abort_on_overflow
?設(shè)置為 1,那么服務(wù)端將在 Accept 隊(duì)列溢出時(shí)直接向客戶端返回 RST 報(bào)文來(lái)關(guān)閉連接。
所以通常我們建議將?net.ipv4.tcp_abort_on_overflow
?設(shè)置為 0,除非你確信服務(wù)端在短時(shí)間內(nèi)無(wú)法從繁忙中恢復(fù)并且希望盡快通知客戶端。
不同類型和版本的操作系統(tǒng)在這方面的行為可能不同。以上行為在 CentOS 中得到驗(yàn)證,但 Ubuntu 似乎采用了另一種行為模式,并且更改?
net.ipv4.tcp_abort_on_overflow
?似乎并不會(huì)改變 Ubuntu 的行為。如果你對(duì)此感興趣,我們?cè)?這里?提供了用于模擬 Accept 隊(duì)列溢出的示例代碼和操作步驟,你可以在自己的環(huán)境中自行試驗(yàn)。
另外,當(dāng) Accept 隊(duì)列溢出時(shí),即使啟用了 SYN Cookie 機(jī)制,服務(wù)端也不會(huì)再接受新的連接請(qǐng)求,即到達(dá)服務(wù)端的 SYN 報(bào)文將被直接丟棄,這會(huì)使得 SYN 丟棄計(jì)數(shù)增加。
查看當(dāng)前 Accept 隊(duì)列大小
我們可以使用?ss
?命令來(lái)查看當(dāng)前 Accept 隊(duì)列的情況。對(duì)于監(jiān)聽(tīng)狀態(tài)的套接字,ss
?命令獲得的第二列 Recv-Q 表示當(dāng)前 Accept 隊(duì)列的大小,第二列 Send-Q 則表示 Accept 隊(duì)列的最大長(zhǎng)度:
$ ss -lnt LISTEN 0 1024 *:1883 *:*
如何確認(rèn) Accept 隊(duì)列發(fā)生溢出?
每當(dāng)服務(wù)端因?yàn)?Accept 隊(duì)列溢出而丟棄報(bào)文時(shí),不管是第一次握手的 SYN 報(bào)文,還是第三次握手的 ACK 報(bào)文,又或者是 PSH 報(bào)文,服務(wù)端中的以下計(jì)數(shù)都會(huì)相應(yīng)加 1:
$ netstat -s | grep "overflowed" <Number> times the listen queue of a socket overflowed
因此我們可以通過(guò)觀察這個(gè)計(jì)數(shù)是否增長(zhǎng)來(lái)判斷 Accept 隊(duì)列是否發(fā)生溢出。
如何增加 SYN 隊(duì)列和 Accept 隊(duì)列的大???
Accept 隊(duì)列比較簡(jiǎn)單,它的最大長(zhǎng)度由監(jiān)聽(tīng)函數(shù)(例如?listen(fd, backlog)
)中的?backlog
?參數(shù)和?net.core.somaxconn
?這個(gè) Linux 內(nèi)核參數(shù)決定。Linux 總是取?backlog
?和?net.core.somaxconn
?中對(duì)的較小值作為 Accept 隊(duì)列的最大長(zhǎng)度。
在 EMQX 中,我們可以為每個(gè)監(jiān)聽(tīng)器都單獨(dú)設(shè)置?backlog
,以默認(rèn)的 TCP 監(jiān)聽(tīng)器為例,我們只需要在?emqx.conf
?中添加以下配置即可:
listeners.tcp.default { tcp_options { backlog = 1024 } }
如果想要修改其他監(jiān)聽(tīng)器的?backlog
,只需要使用對(duì)應(yīng)的協(xié)議名和監(jiān)聽(tīng)器名稱即可:
listeners.[tcp | ssl | ws | wss | quic].<Listener Name> { tcp_options { backlog = 1024 } }
SYN 隊(duì)列略為復(fù)雜,它的最大長(zhǎng)度并不由某個(gè)內(nèi)核參數(shù)直接決定,而是受到?net.ipv4.tcp_max_syn_backlog
、net.core.somaxconn
?等參數(shù)的綜合影響。在不同的操作系統(tǒng)中,這些參數(shù)的效果還會(huì)有所差異。在 CentOS 中,SYN 隊(duì)列的最大長(zhǎng)度有著以下計(jì)算公式:
Max SYN Queue Size = roundup_pow_of_two( max(min(somaxconn, backlog, sysctl_max_syn_backlog), 8) + 1)
roundup_pow_of_two(Num)
?表示將 Num 向上取整到 2 的冪。例如,當(dāng) Num 為 6,7 或 8 時(shí),roundup_pow_of_two(Num)
?總是返回 8。
因此,如果我們將?somaxconn
?設(shè)置為 64,tcp_max_syn_backlog
?設(shè)置為 128,而?listen()
?函數(shù)的?backlog
?設(shè)置為 256 時(shí),那么在 CentOS 中最終 SYN 隊(duì)列的最大長(zhǎng)度將是 256。
而在 Ubuntu 中,SYN 隊(duì)列的長(zhǎng)度必須小于?Accept 隊(duì)列的最大長(zhǎng)度,并且小于等于?0.75 倍的?net.ipv4.tcp_max_syn_backlog
。我們可以轉(zhuǎn)換為以下公式:
Max SYN Queue Size = min(min(somaxconn, backlog), 0.75 * tcp_max_syn_backlog + 1)
如果我們將?somaxconn
?設(shè)置為 64,tcp_max_syn_backlog
?設(shè)置為 512,而?backlog
?設(shè)置為 256 時(shí),Accept 隊(duì)列的最大長(zhǎng)度為 64,小于 0.75 倍的?tcp_max_syn_backlog
,所以此時(shí) SYN 隊(duì)列的最大長(zhǎng)度為 64。
如果我們將?somaxconn
?設(shè)置為 1024,tcp_max_syn_backlog
?設(shè)置為 256,backlog
?設(shè)置為 512 時(shí),Accept 隊(duì)列的最大長(zhǎng)度為 512,大于?tcp_max_syn_backlog
,所以此時(shí) SYN 隊(duì)列的最大長(zhǎng)度為 193。
驗(yàn)證 SYN 隊(duì)列最大長(zhǎng)度
我們可以通過(guò)以下方式來(lái)驗(yàn)證 SYN 隊(duì)列的最大長(zhǎng)度:
首先我們需要一個(gè)簡(jiǎn)單的 TCP 服務(wù)端,它監(jiān)聽(tīng) 12345 端口,但從不調(diào)用?accept()
?從 Accept 隊(duì)列中獲取連接。注意將 backlog 修改為你期望的值:
import socket import time def start_server(host, port, backlog): server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.bind((host, port)) server_socket.listen(backlog) print(f'Server is listening on {host}:{port}') while True: time.sleep(3) if __name__ == '__main__': start_server('0.0.0.0', 12345, 256)
然后我們需要在服務(wù)端進(jìn)行以下操作:
# 如果你使用的是 Ubuntu,那么需要關(guān)閉 SYN Cookie,以便接下來(lái)可以看到計(jì)數(shù)變化 echo 0 > /proc/sys/net/ipv4/tcp_syncookies # 將 somaxconn 和 tcp_max_syn_backlog 設(shè)置為你期望的值 echo 64 > /proc/sys/net/core/somaxconn echo 512 > /proc/sys/net/ipv4/tcp_max_syn_backlog # 為服務(wù)端的 eth0 接口設(shè)置 200ms 的網(wǎng)絡(luò)延遲,使 SYN 隊(duì)列更容易溢出。 # # 當(dāng)你不再需要此延遲時(shí),你可以運(yùn)行以下命令刪除它: # sudo tc qdisc delete dev eth0 root sudo tc qdisc add dev eth0 root netem delay 200ms
完成以上設(shè)置后,運(yùn)行以下命令啟動(dòng) TCP 服務(wù)器:
python3 ./server.py
另起一個(gè)終端窗口,運(yùn)行以下命令用于觀察 SYN 隊(duì)列是否溢出:
watch -n 1 -d 'netstat -s | grep -i "listen"
在客戶端中安裝?hping3
?工具:
apt-get install hping3
運(yùn)行以下命令以指定速率發(fā)送指定數(shù)量的 SYN 報(bào)文:
# -S,發(fā)送 SYN 報(bào)文 # -p <Port>,指定端口 # -c <Count>,發(fā)送報(bào)文的數(shù)量 # -i u100, 以 100us 的間隔發(fā)送報(bào)文 hping3 -S -p 12345 -c 65 -i u100 <Your Hostname>
如果你使用的是 Ubuntu,并且?somaxconn
?等參數(shù)的值與以上示例保持一致,那么在運(yùn)行 hping3 命令后,你將看到 SYN 丟棄計(jì)數(shù)加 1,因?yàn)榇藭r(shí) SYN 隊(duì)列最大長(zhǎng)度為 64。
令改動(dòng)永久生效
echo 64 > /proc/sys/net/core/somaxconn
?和?sysctl -w net.core.somaxconn=64
?都只是臨時(shí)性的改動(dòng),一旦用戶注銷或者系統(tǒng)重啟,我們改動(dòng)就會(huì)失效。如果我們確認(rèn)當(dāng)前值滿足最終期望,那么可以將它們寫(xiě)入?/etc/sysctl.conf
:
net.core.somaxconn = 4096 net.ipv4.tcp_max_syn_backlog = 4096
然后運(yùn)行?sysctl -p
?使改動(dòng)立即永久生效。
總結(jié)
在現(xiàn)代操作系統(tǒng)中,?net.ipv4.tcp_syncookies
?通常默認(rèn)為 1,net.ipv4.tcp_abort_on_overflow
?則通常默認(rèn)為 0,所以除了 Accept 隊(duì)列溢出導(dǎo)致服務(wù)端拒絕后續(xù)連接請(qǐng)求以外,我們很難直接觀察到客戶端連接失敗的情況。
當(dāng) SYN Cookie 機(jī)制生效時(shí),雖然 SYN 報(bào)文不會(huì)再被丟棄,但 TCP 的部分功能可能會(huì)受到限制,并且服務(wù)端的負(fù)載會(huì)相應(yīng)增加。特別在物聯(lián)網(wǎng)、車(chē)聯(lián)網(wǎng)這類 RTT 較高的場(chǎng)景中,SYN 隊(duì)列會(huì)更加容易溢出。所以及時(shí)關(guān)注 SYNs Dropped 計(jì)數(shù)并調(diào)整 SYN 隊(duì)列大小是非常有必要的。
Accept 隊(duì)列通常在服務(wù)端繁忙時(shí)更容易溢出,tcp_abort_on_overflow
?等于 0 的情況下,較短的 Accept 隊(duì)列可能會(huì)使客戶端和服務(wù)端過(guò)早地進(jìn)入報(bào)文重傳,反而增加網(wǎng)絡(luò)負(fù)載。如果 Accept 隊(duì)列不斷溢出,但服務(wù)端的 CPU 并未飽和,那么可以適當(dāng)增大 Accept 隊(duì)列。
另外,將 SYN 隊(duì)列和 Accept 隊(duì)列設(shè)置為一個(gè)非常非常大的值并不是一件好事。在遭受泛洪攻擊或者突發(fā)流量導(dǎo)致服務(wù)端繁忙時(shí),對(duì)這兩個(gè)隊(duì)列施加合理的大小限制反而是對(duì)服務(wù)端的保護(hù)。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-827782.html
在后續(xù)的博客中,我們將繼續(xù)帶來(lái)更多 Linux 系統(tǒng)中影響 EMQX 性能表現(xiàn)的內(nèi)核參數(shù)的優(yōu)化指南。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-827782.html
到了這里,關(guān)于EMQX 性能調(diào)優(yōu):TCP SYN 隊(duì)列與 Accept 隊(duì)列的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!