從一次線上問(wèn)題說(shuō)起,詳解 TCP 半連接隊(duì)列、全連接隊(duì)列
本文轉(zhuǎn)載自微信公眾號(hào)「云巔論劍」,作者黃剛。轉(zhuǎn)載本文請(qǐng)聯(lián)系云巔論劍公眾號(hào)。
前言
某次大促值班 ing,對(duì)系統(tǒng)穩(wěn)定性有著充分信心、心態(tài)穩(wěn)如老狗的筆者突然收到上游反饋有萬(wàn)分幾的概率請(qǐng)求我們 endpoint 會(huì)出現(xiàn) Connection timeout 。此時(shí)系統(tǒng)側(cè)的 apiserver 集群水位在 40%,離極限水位還有著很大的距離,當(dāng)時(shí)通過(guò)緊急擴(kuò)容 apiserver 集群后錯(cuò)誤率降為了 0。事后進(jìn)行了詳細(xì)的問(wèn)題排查,定位分析到問(wèn)題根因出現(xiàn)在系統(tǒng)連接隊(duì)列被打滿導(dǎo)致,之前筆者對(duì) TCP 半連接隊(duì)列、全連接隊(duì)列不太了解,只依稀記得 《TCP/IP 詳解》中好像有好像提到過(guò)這兩個(gè)名詞。
目前網(wǎng)上相關(guān)資料都比較零散,并且有些是過(guò)時(shí)或錯(cuò)誤的結(jié)論,筆者在調(diào)查問(wèn)題時(shí)踩了很多坑。痛定思痛,筆者查閱了大量資料并做了眾多實(shí)驗(yàn)進(jìn)行驗(yàn)證,梳理了這篇 TCP 半連接隊(duì)列、全連接詳解,當(dāng)你細(xì)心閱讀完這篇文章后相信你可以對(duì) TCP 半連接隊(duì)列、全連接隊(duì)列有更充分的認(rèn)識(shí)。
本篇文章將結(jié)合理論知識(shí)、內(nèi)核代碼、操作實(shí)驗(yàn)為你呈現(xiàn)如下內(nèi)容:
- 半連接隊(duì)列、全連接隊(duì)列介紹
- 常用命令介紹
- 全連接隊(duì)列實(shí)戰(zhàn) —— 最大長(zhǎng)度控制、全連接隊(duì)列溢出實(shí)驗(yàn)、實(shí)驗(yàn)結(jié)果分析...
- 半連接隊(duì)列實(shí)戰(zhàn) —— 最大長(zhǎng)度控制、半連接隊(duì)列溢出實(shí)驗(yàn)、實(shí)驗(yàn)結(jié)果分析...
- ...
半連接隊(duì)列、全連接隊(duì)列
在 TCP 三次握手的過(guò)程中,Linux 內(nèi)核會(huì)維護(hù)兩個(gè)隊(duì)列,分別是:
- 半連接隊(duì)列 (SYN Queue)
- 全連接隊(duì)列 (Accept Queue)
正常的 TCP 三次握手過(guò)程:
1、Client 端向 Server 端發(fā)送 SYN 發(fā)起握手,Client 端進(jìn)入 SYN_SENT 狀態(tài)
2、Server 端收到 Client 端的 SYN 請(qǐng)求后,Server 端進(jìn)入 SYN_RECV 狀態(tài),此時(shí)內(nèi)核會(huì)將連接存儲(chǔ)到半連接隊(duì)列(SYN Queue),并向 Client 端回復(fù) SYN+ACK
3、Client 端收到 Server 端的 SYN+ACK 后,Client 端回復(fù) ACK 并進(jìn)入 ESTABLISHED 狀態(tài)
4、Server 端收到 Client 端的 ACK 后,內(nèi)核將連接從半連接隊(duì)列(SYN Queue)中取出,添加到全連接隊(duì)列(Accept Queue),Server 端進(jìn)入 ESTABLISHED 狀態(tài)
5、Server 端應(yīng)用進(jìn)程調(diào)用 accept 函數(shù)時(shí),將連接從全連接隊(duì)列(Accept Queue)中取出
半連接隊(duì)列和全連接隊(duì)列都有長(zhǎng)度大小限制,超過(guò)限制時(shí)內(nèi)核會(huì)將連接 Drop 丟棄或者返回 RST 包。
相關(guān)指標(biāo)查看
ss 命令
通過(guò) ss 命令可以查看到全連接隊(duì)列的信息
- # -n 不解析服務(wù)名稱
- # -t 只顯示 tcp sockets
- # -l 顯示正在監(jiān)聽(tīng)(LISTEN)的 sockets
- $ ss -lnt
- State Recv-Q Send-Q Local Address:Port Peer Address:Port
- LISTEN 0 128 [::]:2380 [::]:*
- LISTEN 0 128 [::]:80 [::]:*
- LISTEN 0 128 [::]:8080 [::]:*
- LISTEN 0 128 [::]:8090 [::]:*
- $ ss -nt
- State Recv-Q Send-Q Local Address:Port Peer Address:Port
- ESTAB 0 0 [::ffff:33.9.95.134]:80 [::ffff:33.51.103.59]:47452
- ESTAB 0 536 [::ffff:33.9.95.134]:80 [::ffff:33.43.108.144]:37656
- ESTAB 0 0 [::ffff:33.9.95.134]:80 [::ffff:33.51.103.59]:38130
- ESTAB 0 536 [::ffff:33.9.95.134]:80 [::ffff:33.51.103.59]:38280
- ESTAB 0 0 [::ffff:33.9.95.134]:80 [::
對(duì)于 LISTEN 狀態(tài)的 socket
- Recv-Q:當(dāng)前全連接隊(duì)列的大小,即已完成三次握手等待應(yīng)用程序 accept() 的 TCP 鏈接
- Send-Q:全連接隊(duì)列的最大長(zhǎng)度,即全連接隊(duì)列的大小
對(duì)于非 LISTEN 狀態(tài)的 socket
- Recv-Q:已收到但未被應(yīng)用程序讀取的字節(jié)數(shù)
- Send-Q:已發(fā)送但未收到確認(rèn)的字節(jié)數(shù)
相關(guān)內(nèi)核代碼:
- // https://github.com/torvalds/linux/blob/master/net/ipv4/tcp_diag.c
- static void tcp_diag_get_info(struct sock *sk, struct inet_diag_msg *r,
- void *_info)
- {
- struct tcp_info *info = _info;
- if (inet_sk_state_load(sk) == TCP_LISTEN) { // socket 狀態(tài)是 LISTEN 時(shí)
- r->idiag_rqueue = READ_ONCE(sk->sk_ack_backlog); // 當(dāng)前全連接隊(duì)列大小
- r->idiag_wqueue = READ_ONCE(sk->sk_max_ack_backlog); // 全連接隊(duì)列最大長(zhǎng)度
- } else if (sk->sk_type == SOCK_STREAM) { // socket 狀態(tài)不是 LISTEN 時(shí)
- const struct tcp_sock *tp = tcp_sk(sk);
- r->idiag_rqueue = max_t(int, READ_ONCE(tp->rcv_nxt) -
- READ_ONCE(tp->copied_seq), 0); // 已收到但未被應(yīng)用程序讀取的字節(jié)數(shù)
- r->idiag_wqueue = READ_ONCE(tp->write_seq) - tp->snd_una; // 已發(fā)送但未收到確認(rèn)的字節(jié)數(shù)
- }
- if (info)
- tcp_get_info(sk, info);
- }
netstat 命令
通過(guò) netstat -s 命令可以查看 TCP 半連接隊(duì)列、全連接隊(duì)列的溢出情況
- $ netstat -s | grep -i "listen"
- 189088 times the listen queue of a socket overflowed
- 30140232 SYNs to LISTEN sockets dropped
上面輸出的數(shù)值是累計(jì)值,分別表示有多少 TCP socket 鏈接因?yàn)槿B接隊(duì)列、半連接隊(duì)列滿了而被丟棄
- 189088 times the listen queue of a socket overflowed 代表有 189088 次全連接隊(duì)列溢出
- 30140232 SYNs to LISTEN sockets dropped 代表有 30140232 次半連接隊(duì)列溢出
在排查線上問(wèn)題時(shí),如果一段時(shí)間內(nèi)相關(guān)數(shù)值一直在上升,則表明半連接隊(duì)列、全連接隊(duì)列有溢出情況
實(shí)戰(zhàn) —— 全連接隊(duì)列
全連接隊(duì)列最大長(zhǎng)度控制
TCP 全連接隊(duì)列的最大長(zhǎng)度由 min(somaxconn, backlog) 控制,其中:
- somaxconn 是 Linux 內(nèi)核參數(shù),由 /proc/sys/net/core/somaxconn 指定
- backlog 是 TCP 協(xié)議中 listen 函數(shù)的參數(shù)之一,即 int listen(int sockfd, int backlog) 函數(shù)中的 backlog 大小。在 Golang 中,listen 的 backlog 參數(shù)使用的是 /proc/sys/net/core/somaxconn 文件中的值。
相關(guān)內(nèi)核代碼:
- // https://github.com/torvalds/linux/blob/master/net/socket.c
- /*
- * Perform a listen. Basically, we allow the protocol to do anything
- * necessary for a listen, and if that works, we mark the socket as
- * ready for listening.
- */
- int __sys_listen(int fd, int backlog)
- {
- struct socket *sock;
- int err, fput_needed;
- int somaxconn;
- sock = sockfd_lookup_light(fd, &err, &fput_needed);
- if (sock) {
- somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn; // /proc/sys/net/core/somaxconn
- if ((unsigned int)backlog > somaxconn)
- backlog = somaxconn; // TCP 全連接隊(duì)列最大長(zhǎng)度 min(somaxconn, backlog)
- err = security_socket_listen(sock, backlog);
- if (!err)
- err = sock->ops->listen(sock, backlog);
- fput_light(sock->file, fput_needed);
- }
- return err;
- }
實(shí)驗(yàn)
服務(wù)端 server 代碼
- package main
- import (
- "log"
- "net"
- "time"
- )
- func main() {
- l, err := net.Listen("tcp", ":8888")
- if err != nil {
- log.Printf("failed to listen due to %v", err)
- }
- defer l.Close()
- log.Println("listen :8888 success")
- for {
- time.Sleep(time.Second * 100)
- }
- }
在測(cè)試環(huán)境查看 somaxconn 的值為 128
- $ cat /proc/sys/net/core/somaxconn
- 128
啟動(dòng)服務(wù)端,通過(guò) ss -lnt | grep :8888 確認(rèn)全連接隊(duì)列大小
- LISTEN 0 128 [::]:8888 [::]:*
全連接隊(duì)列最大長(zhǎng)度為 128
現(xiàn)在更新 somaxconn 值為 1024,再重新啟動(dòng)服務(wù)端。
1、更新 /etc/sysctl.conf 文件,該文件為內(nèi)核參數(shù)配置文件
a.新增一行 net.core.somaxconn=1024
2、執(zhí)行 sysctl -p 使配置生效
- $ sudo sysctl -p
- net.core.somaxconn = 1024
3、檢查 /proc/sys/net/core/somaxconn 文件,確認(rèn) somaxconn 為更新后的 1024
- $ cat /proc/sys/net/core/somaxconn
- 1024
重新啟動(dòng)服務(wù)端, 通過(guò) ss -lnt | grep :8888 確認(rèn)全連接隊(duì)列大小
- $ ss -lnt | grep 8888
- LISTEN 0 1024 [::]:8888 [::]:*
可以看到,現(xiàn)在全鏈接隊(duì)列最大長(zhǎng)度為 1024,成功更新。
全連接隊(duì)列溢出
下面來(lái)驗(yàn)證下全連接隊(duì)列溢出會(huì)發(fā)生什么情況,可以通過(guò)讓服務(wù)端應(yīng)用只負(fù)責(zé) Listen 對(duì)應(yīng)端口而不執(zhí)行 accept() TCP 連接,使 TCP 全連接隊(duì)列溢出。
實(shí)驗(yàn)物料
服務(wù)端 server 代碼
- // server 端監(jiān)聽(tīng) 8888 tcp 端口
- package main
- import (
- "log"
- "net"
- "time"
- )
- func main() {
- l, err := net.Listen("tcp", ":8888")
- if err != nil {
- log.Printf("failed to listen due to %v", err)
- }
- defer l.Close()
- log.Println("listen :8888 success")
- for {
- time.Sleep(time.Second * 100)
- }
- }
客戶端 client 代碼
- // client 端并發(fā)請(qǐng)求 10 次 server 端,成功建立 tcp 連接后向 server 端發(fā)送數(shù)據(jù)
- package main
- import (
- "context"
- "log"
- "net"
- "os"
- "os/signal"
- "sync"
- "syscall"
- "time"
- )
- var wg sync.WaitGroup
- func establishConn(ctx context.Context, i int) {
- defer wg.Done()
- conn, err := net.DialTimeout("tcp", ":8888", time.Second*5)
- if err != nil {
- log.Printf("%d, dial error: %v", i, err)
- return
- }
- log.Printf("%d, dial success", i)
- _, err = conn.Write([]byte("hello world"))
- if err != nil {
- log.Printf("%d, send error: %v", i, err)
- return
- }
- select {
- case <-ctx.Done():
- log.Printf("%d, dail close", i)
- }
- }
- func main() {
- ctx, cancel := context.WithCancel(context.Background())
- for i := 0; i < 10; i++ {
- wg.Add(1)
- go establishConn(ctx, i)
- }
- go func() {
- sc := make(chan os.Signal, 1)
- signal.Notify(sc, syscall.SIGINT)
- select {
- case <-sc:
- cancel()
- }
- }()
- wg.Wait()
- log.Printf("client exit")
- }
為了方便實(shí)驗(yàn),將 somaxconn 全連接隊(duì)列最大長(zhǎng)度更新為 5:
1、更新 /etc/sysctl.conf 文件,將 net.core.somaxconn 更新為 5
2、執(zhí)行 sysctl -p 使配置生效
- $ sudo sysctl -p
- net.core.somaxconn = 5
實(shí)驗(yàn)結(jié)果
客戶端日志輸出
- 2021/10/11 17:24:48 8, dial success
- 2021/10/11 17:24:48 3, dial success
- 2021/10/11 17:24:48 4, dial success
- 2021/10/11 17:24:48 6, dial success
- 2021/10/11 17:24:48 5, dial success
- 2021/10/11 17:24:48 2, dial success
- 2021/10/11 17:24:48 1, dial success
- 2021/10/11 17:24:48 0, dial success
- 2021/10/11 17:24:48 7, dial success
- 2021/10/11 17:24:53 9, dial error: dial tcp 33.9.192.157:8888: i/o timeout
客戶端 socket 情況
- tcp 0 0 33.9.192.155:40372 33.9.192.157:8888 ESTABLISHED
- tcp 0 0 33.9.192.155:40376 33.9.192.157:8888 ESTABLISHED
- tcp 0 0 33.9.192.155:40370 33.9.192.157:8888 ESTABLISHED
- tcp 0 0 33.9.192.155:40366 33.9.192.157:8888 ESTABLISHED
- tcp 0 0 33.9.192.155:40374 33.9.192.157:8888 ESTABLISHED
- tcp 0 0 33.9.192.155:40368 33.9.192.157:8888 ESTABLISHED
服務(wù)端 socket 情況
- tcp6 11 0 33.9.192.157:8888 33.9.192.155:40376 ESTABLISHED
- tcp6 11 0 33.9.192.157:8888 33.9.192.155:40370 ESTABLISHED
- tcp6 11 0 33.9.192.157:8888 33.9.192.155:40368 ESTABLISHED
- tcp6 11 0 33.9.192.157:8888 33.9.192.155:40372 ESTABLISHED
- tcp6 11 0 33.9.192.157:8888 33.9.192.155:40374 ESTABLISHED
- tcp6 11 0 33.9.192.157:8888 33.9.192.155:40366 ESTABLISHED
- tcp LISTEN 6 5 [::]:8888 [::]:* users:(("main",pid=84244,fd=3))
抓包結(jié)果
對(duì)客戶端、服務(wù)端抓包后,發(fā)現(xiàn)出現(xiàn)了三種情況,分別是:
- client 成功與 server 端建立 tcp socket 連接,發(fā)送數(shù)據(jù)成功
- client 認(rèn)為成功與 server 端建立 tcp socket 連接,發(fā)送數(shù)據(jù)失敗,一直在 RETRY;server 端認(rèn)為 tcp 連接未建立,一直在發(fā)送 SYN+ACK
- client 向 server 發(fā)送 SYN 未得到響應(yīng),一直在 RETRY
全連接隊(duì)列實(shí)驗(yàn)結(jié)果分析
上述實(shí)驗(yàn)結(jié)果出現(xiàn)了三種情況,我們分別對(duì)抓包內(nèi)容進(jìn)行分析
情況一:Client 成功與 Server 端建立 tcp socket 鏈接,發(fā)送數(shù)據(jù)成功
上圖可以看到如下請(qǐng)求:
- Client 端向 Server 端發(fā)送 SYN 發(fā)起握手
- Server 端收到 Client 端 SYN 后,向 Client 端回復(fù) SYN+ACK,socket 連接存儲(chǔ)到半連接隊(duì)列(SYN Queue)
- Client 端收到 Server 端 SYN+ACK 后,向 Server 端回復(fù) ACK,Client 端進(jìn)入 ESTABLISHED 狀態(tài)
- Server 端收到 Client 端 ACK 后,進(jìn)入 ESTABLISHED 狀態(tài),socket 連接存儲(chǔ)到全連接隊(duì)列(Accept Queue)
- Client 端向 Server 端發(fā)送數(shù)據(jù) [PSH, ACK],Server 端確認(rèn)接收到數(shù)據(jù) [ACK]
這種情況就是正常的請(qǐng)求,即全連接隊(duì)列、半連接隊(duì)列未滿,client 成功與 server 建立了 tcp 鏈接,并成功發(fā)送數(shù)據(jù)。
情況二:Client 認(rèn)為成功與 Server 端建立 tcp socket 連接,后續(xù)發(fā)送數(shù)據(jù)失敗,持續(xù) RETRY;Server 端認(rèn)為 TCP 連接未建立,一直在發(fā)送SYN+ACK
上圖可以看到如下請(qǐng)求:
- Client 端向 Server 端發(fā)送 SYN 發(fā)起握手
- Server 端收到 Client 端 SYN 后,向 Client 端回復(fù) SYN+ACK,socket 連接存儲(chǔ)到半連接隊(duì)列(SYN Queue)
- Client 端收到 Server 端 SYN+ACK 后,向 Server 端回復(fù) ACK,Client 端進(jìn)入 ESTABLISHED狀態(tài)(重要:此時(shí)僅僅是 Client 端認(rèn)為 tcp 連接建立成功)
- 由于 Client 端認(rèn)為 TCP 連接已經(jīng)建立完成,所以向 Server 端發(fā)送數(shù)據(jù) [PSH,ACK],但是一直未收到 Server 端的確認(rèn) ACK,所以一直在 RETRY
- Server 端一直在 RETRY 發(fā)送 SYN+ACK
為什么會(huì)出現(xiàn)上述情況?Server 端為什么一直在 RETRY 發(fā)送 SYN+ACK?Server 端不是已經(jīng)收到了 Client 端的 ACK 確認(rèn)了嗎?
上述情況是由于 Server 端 socket 連接進(jìn)入了半連接隊(duì)列,在收到 Client 端 ACK 后,本應(yīng)將 socket 連接存儲(chǔ)到全連接隊(duì)列,但是全連接隊(duì)列已滿,所以 Server 端 DROP 了該 ACK 請(qǐng)求。
之所以 Server 端一直在 RETRY 發(fā)送 SYN+ACK,是因?yàn)?DROP 了 client 端的 ACK 請(qǐng)求,所以 socket 連接仍舊在半連接隊(duì)列中,等待 Client 端回復(fù) ACK。
tcp_abort_on_overflow 參數(shù)控制
全連接隊(duì)列滿DROP 請(qǐng)求是默認(rèn)行為,可以通過(guò)設(shè)置 /proc/sys/net/ipv4/tcp_abort_on_overflow 使 Server 端在全連接隊(duì)列滿時(shí),向 Client 端發(fā)送 RST 報(bào)文。
tcp_abort_on_overflow 有兩種可選值:
- 0:如果全連接隊(duì)列滿了,Server 端 DROP Client 端回復(fù)的 ACK
- 1:如果全連接隊(duì)列滿了,Server 端向 Client 端發(fā)送 RST 報(bào)文,終止 TCP socket 鏈接 (TODO:后續(xù)有時(shí)間補(bǔ)充下該實(shí)驗(yàn))
為什么實(shí)驗(yàn)結(jié)果中當(dāng)前全連接隊(duì)列大小 > 全連接隊(duì)列最大長(zhǎng)度配置?
上述結(jié)果中可以看到 Listen 狀態(tài)的 socket 鏈接:
- Recv-Q 當(dāng)前全連接隊(duì)列的大小是 6
- Send-Q 全連接隊(duì)列最大長(zhǎng)度是 5
- State Recv-Q Send-Q Local Address:Port Peer Address:Port
- LISTEN 6 5 [::]:8888 [::]:*
為什么全連接隊(duì)列大小 > 全連接隊(duì)列最大長(zhǎng)度配置呢?
經(jīng)過(guò)多次實(shí)驗(yàn)發(fā)現(xiàn),能夠進(jìn)入全連接隊(duì)列的 Socket 最大數(shù)量始終比配置的全連接隊(duì)列最大長(zhǎng)度 + 1。
結(jié)合其他文章以及內(nèi)核代碼,發(fā)現(xiàn)內(nèi)核在判斷全連接隊(duì)列是否滿的情況下,使用的是 > 而非 >= (具體是為什么沒(méi)有找到相關(guān)資源 : ) )。
相關(guān)內(nèi)核代碼:
- /* Note: If you think the test should be:
- * return READ_ONCE(sk->sk_ack_backlog) >= READ_ONCE(sk->sk_max_ack_backlog);
- * Then please take a look at commit 64a146513f8f ("[NET]: Revert incorrect accept queue backlog changes.")
- */
- static inline bool sk_acceptq_is_full(const struct sock *sk)
- {
- return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog);
- }
情況三:Client 向 Server 發(fā)送 SYN 未得到相應(yīng),一直在 RETRY
圖片上圖可以看到如下請(qǐng)求:
- Client 端向 Server 端發(fā)送 SYN 發(fā)起握手,未得到 Server 回應(yīng),一直在 RETRY
(這種情況涉及到半連接隊(duì)列,這里先給上述情況發(fā)生的原因結(jié)論,具體內(nèi)容將在下文半連接隊(duì)列中展開(kāi)。)
發(fā)生上述情況的原因由以下兩方面導(dǎo)致:
1、開(kāi)啟了 /proc/sys/net/ipv4/tcp_syncookies 功能
2、全連接隊(duì)列滿了
實(shí)戰(zhàn) —— 半連接隊(duì)列
半連接隊(duì)列最大長(zhǎng)度控制
翻閱了很多博文,查找關(guān)于半連接隊(duì)列最大長(zhǎng)度控制的相關(guān)內(nèi)容,大多含糊其辭或不準(zhǔn)確,經(jīng)過(guò)不懈努力,最終找到了比較確切的內(nèi)容(相關(guān)博文鏈接在附錄中)。
很多博文中說(shuō)半連接隊(duì)列最大長(zhǎng)度由 /proc/sys/net/ipv4/tcp_max_syn_backlog 參數(shù)指定,實(shí)際上只有在 linux 內(nèi)核版本小于 2.6.20 時(shí),半連接隊(duì)列才等于 backlog 的大小。
這塊的源碼比較復(fù)雜,這里給一下大體的計(jì)算方式,詳細(xì)的內(nèi)容可以參考附錄中的相關(guān)博文。半連接隊(duì)列長(zhǎng)度的計(jì)算過(guò)程:
- backlog = min(somaxconn, backlog)
- nr_table_entries = backlog
- nr_table_entries = min(backlog, sysctl_max_syn_backlog)
- nr_table_entries = max(nr_table_entries, 8)
- // roundup_pow_of_two: 將參數(shù)向上取整到最小的 2^n,注意這里存在一個(gè) +1
- nr_table_entries = roundup_pow_of_two(nr_table_entries + 1)
- max_qlen_log = max(3, log2(nr_table_entries))
- max_queue_length = 2^max_qlen_log
可以看到,半連接隊(duì)列的長(zhǎng)度由三個(gè)參數(shù)指定:
- 調(diào)用 listen 時(shí),傳入的 backlog
- /proc/sys/net/core/somaxconn 默認(rèn)值為 128
- /proc/sys/net/ipv4/tcp_max_syn_backlog 默認(rèn)值為 1024
我們假設(shè) listen 傳入的 backlog = 128 (Golang 中調(diào)用 listen 時(shí)傳遞的 backlog 參數(shù)使用的是 /proc/sys/net/core/somaxconn),其他配置采用默認(rèn)值,來(lái)計(jì)算下半連接隊(duì)列的最大長(zhǎng)度
- backlog = min(somaxconn, backlog) = min(128, 128) = 128
- nr_table_entries = backlog = 128
- nr_table_entries = min(backlog, sysctl_max_syn_backlog) = min(128, 1024) = 128
- nr_table_entries = max(nr_table_entries, 8) = max(128, 8) = 128
- nr_table_entries = roundup_pow_of_two(nr_table_entries + 1) = 256
- max_qlen_log = max(3, log2(nr_table_entries)) = max(3, 8) = 8
- max_queue_length = 2^max_qlen_log = 2^8 = 256
可以得到半隊(duì)列大小是 256。
判斷是否 Drop SYN 請(qǐng)求
當(dāng) Client 端向 Server 端發(fā)送 SYN 報(bào)文后,Server 端會(huì)將該 socket 連接存儲(chǔ)到半連接隊(duì)列(SYN Queue),如果 Server 端判斷半連接隊(duì)列滿了則會(huì)將連接 Drop 丟棄。
那么 Server 端是如何判斷半連接隊(duì)列是否滿的呢?除了上面一小節(jié)提到的半連接隊(duì)列最大長(zhǎng)度控制外,還和 /proc/sys/net/ipv4/tcp_syncookies 參數(shù)有關(guān)。(tcp_syncookies 的作用是為了防止 SYN Flood 攻擊的,下文會(huì)給出相關(guān)鏈接介紹)
流程圖
判斷是否 Drop SYN 請(qǐng)求的流程圖:
上圖是整理了多份資料后,整理出來(lái)的判斷是否 Drop SYN 請(qǐng)求的流程圖。
注意:第一個(gè)判斷條件 「當(dāng)前半連接隊(duì)列是否已超過(guò)半連接隊(duì)列最大長(zhǎng)度」在不同內(nèi)核版本中的判斷不一樣,Linux4.19.91 內(nèi)核判斷的是當(dāng)前半連接隊(duì)列長(zhǎng)度是否 >= 全連接隊(duì)列最大長(zhǎng)度。
相關(guān)內(nèi)核代碼:
- static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk)
- {
- return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
- }
我們假設(shè)如下參數(shù),來(lái)計(jì)算下當(dāng) Client 端只發(fā)送 SYN 包,理論上 Server 端何時(shí)會(huì) Drop SYN 請(qǐng)求:
- 調(diào)用 listen 時(shí)傳入的 backlog = 1024
- /proc/sys/net/core/somaxconn 值為 1024
- /proc/sys/net/ipv4/tcp_max_syn_backlog 值為 128
當(dāng) /proc/sys/net/ipv4/tcp_syncookies 值為 0 時(shí)
- 計(jì)算出的半連接隊(duì)列最大長(zhǎng)度為 256
- 當(dāng)半連接隊(duì)列長(zhǎng)度增長(zhǎng)至 96 后,再新增 SYN 請(qǐng)求,就會(huì)觸發(fā) Drop SYN 請(qǐng)求
當(dāng) /proc/sys/net/ipv4/tcp_syncookies 值為 1 時(shí)
1.計(jì)算出的半連接隊(duì)列最大長(zhǎng)度為 256
2.由于開(kāi)啟了 tcp_syncookies
- 當(dāng)全連接隊(duì)列未滿時(shí),永遠(yuǎn)不會(huì) Drop 請(qǐng)求 (注意:經(jīng)實(shí)驗(yàn)發(fā)現(xiàn)這個(gè)理論是錯(cuò)誤的,實(shí)驗(yàn)發(fā)現(xiàn)只要半連接隊(duì)列的大小 > 全連接隊(duì)列最大長(zhǎng)度就會(huì)觸發(fā) Drop SYN 請(qǐng)求)
- 當(dāng)全連接隊(duì)列滿了后,即全連接隊(duì)列大小到 1024 后,就會(huì)觸發(fā) Drop SYN 請(qǐng)求
PS:/proc/sys/net/ipv4/tcp_syncookies 的取值還可以為 2,筆者沒(méi)有詳細(xì)實(shí)驗(yàn)。
回顧全連接隊(duì)列實(shí)驗(yàn)結(jié)果
在上文全連接隊(duì)列實(shí)驗(yàn)中,有一類實(shí)驗(yàn)結(jié)果是:client 向 Server 發(fā)送 SYN 未得到響應(yīng),一直在 RETRY。
發(fā)生上述情況的原因由以下兩方面導(dǎo)致:
1. 開(kāi)啟了 /proc/sys/net/ipv4/tcp_syncookies 功能
2. 全連接隊(duì)列滿了
半連接隊(duì)列溢出實(shí)驗(yàn)
上文我們已經(jīng)知道如何計(jì)算理論上半連接隊(duì)列何時(shí)會(huì)溢出,下面我們來(lái)具體實(shí)驗(yàn)下
(Golang 調(diào)用 listen 時(shí)傳入的 backlog 值為 somaxconn)
實(shí)驗(yàn)一:syncookies=0,somaxconn=1024,tcp_max_syn_backlog=128
理論上:
- 計(jì)算出的半連接隊(duì)列最大長(zhǎng)度為 256
- 當(dāng)半連接隊(duì)列長(zhǎng)度增長(zhǎng)至 96 后,后續(xù) SYN 請(qǐng)求就會(huì)觸發(fā) Drop
將相關(guān)參數(shù)的配置更新
- $ sudo sysctl -p
- net.core.somaxconn = 1024
- net.ipv4.tcp_max_syn_backlog = 128
- net.ipv4.tcp_syncookies = 0
啟動(dòng)服務(wù)端 Server 監(jiān)聽(tīng) 8888 端口(代碼參考全連接隊(duì)列實(shí)驗(yàn)物料)
客戶端 Client 發(fā)起 SYN Flood 攻擊:
- $ sudo hping3 -S 33.9.192.157 -p 8888 --flood
- HPING 33.9.192.157 (eth0 33.9.192.157): S set, 40 headers + 0 data bytes
- hping in flood mode, no replies will be shown
查看服務(wù)端 Server 8888端口處于 SYN_RECV 狀態(tài)的 socket 最大個(gè)數(shù):
- [zechen.hg@function-compute033009192157.na63 /home/zechen.hg]
- $ sudo netstat -nat | grep :8888 | grep SYN_RECV | wc -l
- 96
- [zechen.hg@function-compute033009192157.na63 /home/zechen.hg]
- $ sudo netstat -nat | grep :8888 | grep SYN_RECV | wc -l
- 96
實(shí)驗(yàn)結(jié)果符合預(yù)期,當(dāng)半連接隊(duì)列長(zhǎng)度增長(zhǎng)至 96 后,后續(xù) SYN 請(qǐng)求就會(huì)觸發(fā) Drop。
實(shí)驗(yàn)二:syncookies = 0,somaxconn=128,tcp_max_syn_backlog=512
理論上:
- 計(jì)算出的半連接隊(duì)列最大長(zhǎng)度為 256,由于筆者實(shí)驗(yàn)機(jī)器上的內(nèi)核版本是 4.19.91,所以當(dāng)半連接隊(duì)列長(zhǎng)度 >= 全連接隊(duì)列最大長(zhǎng)度時(shí),內(nèi)核就認(rèn)為半連接隊(duì)列溢出了
- 所以當(dāng)半連接隊(duì)列長(zhǎng)度增長(zhǎng)至 128 后,后續(xù) SYN 請(qǐng)求就會(huì)觸發(fā) DROP
將相關(guān)參數(shù)的配置更新
- $ sudo sysctl -p
- net.core.somaxconn = 128
- net.ipv4.tcp_max_syn_backlog = 512
- net.ipv4.tcp_syncookies = 0
啟動(dòng)服務(wù)端 Server 監(jiān)聽(tīng) 8888 端口(代碼參考全連接隊(duì)列實(shí)驗(yàn)物料)
客戶端 Client 發(fā)起 SYN Flood 攻擊:
- $ sudo hping3 -S 33.9.192.157 -p 8888 --flood
- HPING 33.9.192.157 (eth0 33.9.192.157): S set, 40 headers + 0 data bytes
- hping in flood mode, no replies will be shown
查看服務(wù)端 Server 8888端口處于 SYN_RECV 狀態(tài)的 socket 最大個(gè)數(shù):
- [zechen.hg@function-compute033009192157.na63 /home/zechen.hg]
- $ sudo netstat -nat | grep :8888 | grep SYN_RECV | wc -l
- 128
- [zechen.hg@function-compute033009192157.na63 /home/zechen.hg]
- $ sudo netstat -nat | grep :8888 | grep SYN_RECV | wc -l
- 128
實(shí)驗(yàn)結(jié)果符合預(yù)期,當(dāng)半連接隊(duì)列長(zhǎng)度增長(zhǎng)至 128 后,后續(xù) SYN 請(qǐng)求就會(huì)觸發(fā) Drop
實(shí)驗(yàn)三:syncookies = 1,somaxconn=128,tcp_max_syn_backlog=512
理論上:
- 當(dāng)全連接隊(duì)列未滿,syncookies = 1,理論上 SYN 請(qǐng)求永遠(yuǎn)不會(huì)被 Drop
將相關(guān)參數(shù)的配置更新
- $ sudo sysctl -p
- net.core.somaxconn = 128
- net.ipv4.tcp_max_syn_backlog = 512
- net.ipv4.tcp_syncookies = 1
啟動(dòng)服務(wù)端 Server 監(jiān)聽(tīng) 8888 端口(代碼參考全連接隊(duì)列實(shí)驗(yàn)物料)
客戶端 Client 發(fā)起 SYN Flood 攻擊:
- $ sudo hping3 -S 33.9.192.157 -p 8888 --flood
- HPING 33.9.192.157 (eth0 33.9.192.157): S set, 40 headers + 0 data bytes
- hping in flood mode, no replies will be shown
查看服務(wù)端 Server 8888端口處于 SYN_RECV 狀態(tài)的 socket 最大個(gè)數(shù):
- [zechen.hg@function-compute033009192157.na63 /home/zechen.hg]
- $ sudo netstat -nat | grep :8888 | grep SYN_RECV | wc -l
- 128
- [zechen.hg@function-compute033009192157.na63 /home/zechen.hg]
- $ sudo netstat -nat | grep :8888 | grep SYN_RECV | wc -l
- 128
實(shí)驗(yàn)發(fā)現(xiàn)即使syncookies=1,當(dāng)半連接隊(duì)列長(zhǎng)度 > 全連接隊(duì)列最大長(zhǎng)度時(shí),就會(huì)觸發(fā) DROP SYN 請(qǐng)求!!!(TODO:有時(shí)間閱讀下相關(guān)內(nèi)核源碼,再分析下)
繼續(xù)做實(shí)驗(yàn),將 somaxconn 更新為 5
- $ sudo sysctl -p
- net.core.somaxconn = 5
- net.ipv4.tcp_max_syn_backlog = 512
- net.ipv4.tcp_syncookies = 1
發(fā)起 SYN Flood 攻擊后,查看服務(wù)端 Server 8888端口處于 SYN_RECV 狀態(tài)的 socket 最大個(gè)數(shù):
- [zechen.hg@function-compute033009192157.na63 /home/zechen.hg]
- $ sudo netstat -nat | grep :8888 | grep SYN_RECV | wc -l
- 5
- [zechen.hg@function-compute033009192157.na63 /home/zechen.hg]
- $ sudo netstat -nat | grep :8888 | grep SYN_RECV | wc -l
- 5
確實(shí) 即使 syncookies=1,當(dāng)半連接隊(duì)列長(zhǎng)度 > 全連接最大長(zhǎng)度時(shí),就會(huì)觸發(fā) DROP SYN 請(qǐng)求。
實(shí)驗(yàn)四:syncookies = 1,somaxconn=256,tcp_max_syn_backlog=128
理論上:
- 當(dāng)半連接隊(duì)列大小到 256 后,后觸發(fā) DROP SYN 請(qǐng)求
將相關(guān)參數(shù)的配置更新
- $ sudo sysctl -p
- net.core.somaxconn = 256
- net.ipv4.tcp_max_syn_backlog = 128
- net.ipv4.tcp_syncookies = 1
啟動(dòng)服務(wù)端 Server 監(jiān)聽(tīng) 8888 端口(代碼參考全連接隊(duì)列實(shí)驗(yàn)物料)。
客戶端 Client 發(fā)起 SYN Flood 攻擊:
- $ sudo hping3 -S 33.9.192.157 -p 8888 --flood
- HPING 33.9.192.157 (eth0 33.9.192.157): S set, 40 headers + 0 data bytes
- hping in flood mode, no replies will be shown
查看服務(wù)端 Server 8888端口處于 SYN_RECV 狀態(tài)的 socket 最大個(gè)數(shù):
- [zechen.hg@function-compute033009192157.na63 /home/zechen.hg]
- $ sudo netstat -nat | grep :8888 | grep SYN_RECV | wc -l
- 256
- [zechen.hg@function-compute033009192157.na63 /home/zechen.hg]
- $ sudo netstat -nat | grep :8888 | grep SYN_RECV | wc -l
- 256
實(shí)驗(yàn)結(jié)果符合預(yù)期,當(dāng)半連接隊(duì)列長(zhǎng)度增長(zhǎng)至 256 后,后續(xù) SYN 請(qǐng)求就會(huì)觸發(fā) Drop。
回顧線上問(wèn)題
再回顧值班時(shí)遇到的 Connection timeout 問(wèn)題,當(dāng)時(shí)相關(guān)系統(tǒng)參數(shù)配置為:
- net.core.somaxconn = 128
- net.ipv4.tcp_max_syn_backlog = 512
- net.ipv4.tcp_syncookies = 1
- net.ipv4.tcp_abort_on_overflow = 0
所以出現(xiàn) Connection timeout 有兩種可能情況:
1、半連接隊(duì)列未滿,全連接隊(duì)列滿,Client 端向 Server 端發(fā)起 SYN 被 DROP (參考全連接隊(duì)列實(shí)驗(yàn)結(jié)果情況三分析、半連接隊(duì)列溢出實(shí)驗(yàn)情況三)
2、全連接隊(duì)列未滿,半連接隊(duì)列大小超過(guò)全鏈接隊(duì)列最大長(zhǎng)度(參考半連接隊(duì)列溢出實(shí)驗(yàn)情況三、半連接隊(duì)列溢出實(shí)驗(yàn)情況四)
問(wèn)題的最快修復(fù)方式是將 net.core.somaxconn 調(diào)大,以及 net.ipv4.tcp_abort_on_overflow 設(shè)置為 1,net.ipv4.tcp_abort_on_overflow 設(shè)置為 1 是為了讓 client fail fast。
總結(jié)
半連接隊(duì)列溢出、全連接隊(duì)列溢出這類問(wèn)題很容易被忽略,同時(shí)這類問(wèn)題又很致命。當(dāng)半連接隊(duì)列、全連接隊(duì)列溢出時(shí) Server 端,從監(jiān)控上來(lái)看系統(tǒng) cpu 水位、內(nèi)存水位、網(wǎng)絡(luò)連接數(shù)等一切正常,然而卻會(huì)持續(xù)影響 Client 端業(yè)務(wù)請(qǐng)求。對(duì)于高負(fù)載上游使用短連接的情況,出現(xiàn)這類問(wèn)題的可能性更大。
本文詳細(xì)梳理了 TCP 半連接隊(duì)列、全連接隊(duì)列的理論知識(shí),同時(shí)結(jié)合 Linux 相關(guān)內(nèi)核代碼以及詳細(xì)的動(dòng)手實(shí)驗(yàn),講解了 TCP 半連接隊(duì)列、全連接隊(duì)列的相關(guān)原理、溢出判斷、問(wèn)題分析等內(nèi)容,希望大家在閱讀后可以對(duì) TCP 半連接隊(duì)列、全連接隊(duì)列有更充分的認(rèn)識(shí)。
PS:可以去線上檢查下服務(wù)器的相關(guān)參數(shù)喲~
附錄
這里羅列下相關(guān)參考博文資料:
Linux 源碼
- https://github.com/torvalds/linux
Linux 詭異的半連接隊(duì)列長(zhǎng)度
- https://www.cnblogs.com/zengkefu/p/5606696.html
TCP 半連接隊(duì)列和全連接隊(duì)列滿了會(huì)發(fā)生什么
- 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