从一次线上问题说起,详解 TCP 半连接队列、全连接队列

时间:2022-06-01 17:04:55

从一次线上问题说起,详解 TCP 半连接队列、全连接队列

本文转载自微信公众号「云巅论剑」,作者黄刚。转载本文请联系云巅论剑公众号。

前言

某次大促值班 ing,对系统稳定性有着充分信心、心态稳如老狗的笔者突然收到上游反馈有万分几的概率请求我们 endpoint 会出现 Connection timeout 。此时系统侧的 apiserver 集群水位在 40%,离极限水位还有着很大的距离,当时通过紧急扩容 apiserver 集群后错误率降为了 0。事后进行了详细的问题排查,定位分析到问题根因出现在系统连接队列被打满导致,之前笔者对 TCP 半连接队列、全连接队列不太了解,只依稀记得 《TCP/IP 详解》中好像有好像提到过这两个名词。

目前网上相关资料都比较零散,并且有些是过时或错误的结论,笔者在调查问题时踩了很多坑。痛定思痛,笔者查阅了大量资料并做了众多实验进行验证,梳理了这篇 TCP 半连接队列、全连接详解,当你细心阅读完这篇文章后相信你可以对 TCP 半连接队列、全连接队列有更充分的认识。

本篇文章将结合理论知识、内核代码、操作实验为你呈现如下内容:

  • 半连接队列、全连接队列介绍
  • 常用命令介绍
  • 全连接队列实战 —— 最大长度控制、全连接队列溢出实验、实验结果分析...
  • 半连接队列实战 —— 最大长度控制、半连接队列溢出实验、实验结果分析...
  • ...

半连接队列、全连接队列

从一次线上问题说起,详解 TCP 半连接队列、全连接队列

在 TCP 三次握手的过程中,Linux 内核会维护两个队列,分别是:

  • 半连接队列 (SYN Queue)
  • 全连接队列 (Accept Queue)

正常的 TCP 三次握手过程:

1、Client 端向 Server 端发送 SYN 发起握手,Client 端进入 SYN_SENT 状态

2、Server 端收到 Client 端的 SYN 请求后,Server 端进入 SYN_RECV 状态,此时内核会将连接存储到半连接队列(SYN Queue),并向 Client 端回复 SYN+ACK

3、Client 端收到 Server 端的 SYN+ACK 后,Client 端回复 ACK 并进入 ESTABLISHED 状态

4、Server 端收到 Client 端的 ACK 后,内核将连接从半连接队列(SYN Queue)中取出,添加到全连接队列(Accept Queue),Server 端进入 ESTABLISHED 状态

5、Server 端应用进程调用 accept 函数时,将连接从全连接队列(Accept Queue)中取出

半连接队列和全连接队列都有长度大小限制,超过限制时内核会将连接 Drop 丢弃或者返回 RST 包。

相关指标查看

ss 命令

通过 ss 命令可以查看到全连接队列的信息

  1. #-n不解析服务名称
  2. #-t只显示tcpsockets
  3. #-l显示正在监听(LISTEN)的sockets
  4.  
  5. $ss-lnt
  6. StateRecv-QSend-QLocalAddress:PortPeerAddress:Port
  7. LISTEN0128[::]:2380[::]:*
  8. LISTEN0128[::]:80[::]:*
  9. LISTEN0128[::]:8080[::]:*
  10. LISTEN0128[::]:8090[::]:*
  11.  
  12. $ss-nt
  13. StateRecv-QSend-QLocalAddress:PortPeerAddress:Port
  14. ESTAB00[::ffff:33.9.95.134]:80[::ffff:33.51.103.59]:47452
  15. ESTAB0536[::ffff:33.9.95.134]:80[::ffff:33.43.108.144]:37656
  16. ESTAB00[::ffff:33.9.95.134]:80[::ffff:33.51.103.59]:38130
  17. ESTAB0536[::ffff:33.9.95.134]:80[::ffff:33.51.103.59]:38280
  18. ESTAB00[::ffff:33.9.95.134]:80[::

对于 LISTEN 状态的 socket

  • Recv-Q:当前全连接队列的大小,即已完成三次握手等待应用程序 accept() 的 TCP 链接
  • Send-Q:全连接队列的最大长度,即全连接队列的大小

对于非 LISTEN 状态的 socket

  • Recv-Q:已收到但未被应用程序读取的字节数
  • Send-Q:已发送但未收到确认的字节数

相关内核代码:

  1. //https://github.com/torvalds/linux/blob/master/net/ipv4/tcp_diag.c
  2. staticvoidtcp_diag_get_info(structsock*sk,structinet_diag_msg*r,
  3. void*_info)
  4. {
  5. structtcp_info*info=_info;
  6.  
  7. if(inet_sk_state_load(sk)==TCP_LISTEN){//socket状态是LISTEN时
  8. r->idiag_rqueue=READ_ONCE(sk->sk_ack_backlog);//当前全连接队列大小
  9. r->idiag_wqueue=READ_ONCE(sk->sk_max_ack_backlog);//全连接队列最大长度
  10. }elseif(sk->sk_type==SOCK_STREAM){//socket状态不是LISTEN时
  11. conststructtcp_sock*tp=tcp_sk(sk);
  12.  
  13. r->idiag_rqueue=max_t(int,READ_ONCE(tp->rcv_nxt)-
  14. READ_ONCE(tp->copied_seq),0);//已收到但未被应用程序读取的字节数
  15. r->idiag_wqueue=READ_ONCE(tp->write_seq)-tp->snd_una;//已发送但未收到确认的字节数
  16. }
  17. if(info)
  18. tcp_get_info(sk,info);
  19. }

netstat 命令

通过 netstat -s 命令可以查看 TCP 半连接队列、全连接队列的溢出情况

  1. $netstat-s|grep-i"listen"
  2. 189088timesthelistenqueueofasocketoverflowed
  3. 30140232SYNstoLISTENsocketsdropped

上面输出的数值是累计值,分别表示有多少 TCP socket 链接因为全连接队列、半连接队列满了而被丢弃

  • 189088 times the listen queue of a socket overflowed 代表有 189088 次全连接队列溢出
  • 30140232 SYNs to LISTEN sockets dropped 代表有 30140232 次半连接队列溢出

在排查线上问题时,如果一段时间内相关数值一直在上升,则表明半连接队列、全连接队列有溢出情况

实战 —— 全连接队列

全连接队列最大长度控制

TCP 全连接队列的最大长度由 min(somaxconn, backlog) 控制,其中:

  • somaxconn 是 Linux 内核参数,由 /proc/sys/net/core/somaxconn 指定
  • backlog 是 TCP 协议中 listen 函数的参数之一,即 int listen(int sockfd, int backlog) 函数中的 backlog 大小。在 Golang 中,listen 的 backlog 参数使用的是 /proc/sys/net/core/somaxconn 文件中的值。

相关内核代码:

  1. //https://github.com/torvalds/linux/blob/master/net/socket.c
  2.  
  3. /*
  4. *Performalisten.Basically,weallowtheprotocoltodoanything
  5. *necessaryforalisten,andifthatworks,wemarkthesocketas
  6. *readyforlistening.
  7. */
  8. int__sys_listen(intfd,intbacklog)
  9. {
  10. structsocket*sock;
  11. interr,fput_needed;
  12. intsomaxconn;
  13.  
  14. sock=sockfd_lookup_light(fd,&err,&fput_needed);
  15. if(sock){
  16. somaxconn=sock_net(sock->sk)->core.sysctl_somaxconn;///proc/sys/net/core/somaxconn
  17. if((unsignedint)backlog>somaxconn)
  18. backlog=somaxconn;//TCP全连接队列最大长度min(somaxconn,backlog)
  19.  
  20. err=security_socket_listen(sock,backlog);
  21. if(!err)
  22. err=sock->ops->listen(sock,backlog);
  23.  
  24. fput_light(sock->file,fput_needed);
  25. }
  26. returnerr;
  27. }

实验

服务端 server 代码

  1. packagemain
  2.  
  3. import(
  4. "log"
  5. "net"
  6. "time"
  7. )
  8.  
  9. funcmain(){
  10. l,err:=net.Listen("tcp",":8888")
  11. iferr!=nil{
  12. log.Printf("failedtolistendueto%v",err)
  13. }
  14. deferl.Close()
  15. log.Println("listen:8888success")
  16.  
  17. for{
  18. time.Sleep(time.Second*100)
  19. }
  20. }

在测试环境查看 somaxconn 的值为 128

  1. $cat/proc/sys/net/core/somaxconn
  2. 128

启动服务端,通过 ss -lnt | grep :8888 确认全连接队列大小

  1. LISTEN0128[::]:8888[::]:*

全连接队列最大长度为 128

现在更新 somaxconn 值为 1024,再重新启动服务端。

1、更新 /etc/sysctl.conf 文件,该文件为内核参数配置文件

a.新增一行 net.core.somaxconn=1024

2、执行 sysctl -p 使配置生效

  1. $sudosysctl-p
  2. net.core.somaxconn=1024

3、检查 /proc/sys/net/core/somaxconn 文件,确认 somaxconn 为更新后的 1024

  1. $cat/proc/sys/net/core/somaxconn
  2. 1024

重新启动服务端, 通过 ss -lnt | grep :8888 确认全连接队列大小

  1. $ss-lnt|grep8888
  2. LISTEN01024[::]:8888[::]:*

可以看到,现在全链接队列最大长度为 1024,成功更新。

全连接队列溢出

下面来验证下全连接队列溢出会发生什么情况,可以通过让服务端应用只负责 Listen 对应端口而不执行 accept() TCP 连接,使 TCP 全连接队列溢出。

实验物料

服务端 server 代码

  1. //server端监听8888tcp端口
  2.  
  3. packagemain
  4.  
  5. import(
  6. "log"
  7. "net"
  8. "time"
  9. )
  10.  
  11. funcmain(){
  12. l,err:=net.Listen("tcp",":8888")
  13. iferr!=nil{
  14. log.Printf("failedtolistendueto%v",err)
  15. }
  16. deferl.Close()
  17. log.Println("listen:8888success")
  18.  
  19. for{
  20. time.Sleep(time.Second*100)
  21. }
  22. }

客户端 client 代码

  1. //client端并发请求10次server端,成功建立tcp连接后向server端发送数据
  2. packagemain
  3.  
  4. import(
  5. "context"
  6. "log"
  7. "net"
  8. "os"
  9. "os/signal"
  10. "sync"
  11. "syscall"
  12. "time"
  13. )
  14.  
  15. varwgsync.WaitGroup
  16.  
  17. funcestablishConn(ctxcontext.Context,iint){
  18. deferwg.Done()
  19. conn,err:=net.DialTimeout("tcp",":8888",time.Second*5)
  20. iferr!=nil{
  21. log.Printf("%d,dialerror:%v",i,err)
  22. return
  23. }
  24. log.Printf("%d,dialsuccess",i)
  25. _,err=conn.Write([]byte("helloworld"))
  26. iferr!=nil{
  27. log.Printf("%d,senderror:%v",i,err)
  28. return
  29. }
  30. select{
  31. case<-ctx.Done():
  32. log.Printf("%d,dailclose",i)
  33. }
  34. }
  35.  
  36. funcmain(){
  37. ctx,cancel:=context.WithCancel(context.Background())
  38. fori:=0;i<10;i++{
  39. wg.Add(1)
  40. goestablishConn(ctx,i)
  41. }
  42.  
  43. gofunc(){
  44. sc:=make(chanos.Signal,1)
  45. signal.Notify(sc,syscall.SIGINT)
  46. select{
  47. case<-sc:
  48. cancel()
  49. }
  50. }()
  51.  
  52. wg.Wait()
  53. log.Printf("clientexit")
  54. }

为了方便实验,将 somaxconn 全连接队列最大长度更新为 5:

1、更新 /etc/sysctl.conf 文件,将 net.core.somaxconn 更新为 5

2、执行 sysctl -p 使配置生效

  1. $sudosysctl-p
  2. net.core.somaxconn=5

实验结果

客户端日志输出

  1. 2021/10/1117:24:488,dialsuccess
  2. 2021/10/1117:24:483,dialsuccess
  3. 2021/10/1117:24:484,dialsuccess
  4. 2021/10/1117:24:486,dialsuccess
  5. 2021/10/1117:24:485,dialsuccess
  6. 2021/10/1117:24:482,dialsuccess
  7. 2021/10/1117:24:481,dialsuccess
  8. 2021/10/1117:24:480,dialsuccess
  9. 2021/10/1117:24:487,dialsuccess
  10. 2021/10/1117:24:539,dialerror:dialtcp33.9.192.157:8888:i/otimeout

客户端 socket 情况

  1. tcp0033.9.192.155:4037233.9.192.157:8888ESTABLISHED
  2. tcp0033.9.192.155:4037633.9.192.157:8888ESTABLISHED
  3. tcp0033.9.192.155:4037033.9.192.157:8888ESTABLISHED
  4. tcp0033.9.192.155:4036633.9.192.157:8888ESTABLISHED
  5. tcp0033.9.192.155:4037433.9.192.157:8888ESTABLISHED
  6. tcp0033.9.192.155:4036833.9.192.157:8888ESTABLISHED

服务端 socket 情况

  1. tcp611033.9.192.157:888833.9.192.155:40376ESTABLISHED
  2. tcp611033.9.192.157:888833.9.192.155:40370ESTABLISHED
  3. tcp611033.9.192.157:888833.9.192.155:40368ESTABLISHED
  4. tcp611033.9.192.157:888833.9.192.155:40372ESTABLISHED
  5. tcp611033.9.192.157:888833.9.192.155:40374ESTABLISHED
  6. tcp611033.9.192.157:888833.9.192.155:40366ESTABLISHED
  7.  
  8. tcpLISTEN65[::]:8888[::]:*users:(("main",pid=84244,fd=3))

抓包结果

对客户端、服务端抓包后,发现出现了三种情况,分别是:

  • client 成功与 server 端建立 tcp socket 连接,发送数据成功
  • client 认为成功与 server 端建立 tcp socket 连接,发送数据失败,一直在 RETRY;server 端认为 tcp 连接未建立,一直在发送 SYN+ACK
  • client 向 server 发送 SYN 未得到响应,一直在 RETRY

全连接队列实验结果分析

上述实验结果出现了三种情况,我们分别对抓包内容进行分析

情况一:Client 成功与 Server 端建立 tcp socket 链接,发送数据成功

从一次线上问题说起,详解 TCP 半连接队列、全连接队列

上图可以看到如下请求:

  • Client 端向 Server 端发送 SYN 发起握手
  • Server 端收到 Client 端 SYN 后,向 Client 端回复 SYN+ACK,socket 连接存储到半连接队列(SYN Queue)
  • Client 端收到 Server 端 SYN+ACK 后,向 Server 端回复 ACK,Client 端进入 ESTABLISHED 状态
  • Server 端收到 Client 端 ACK 后,进入 ESTABLISHED 状态,socket 连接存储到全连接队列(Accept Queue)
  • Client 端向 Server 端发送数据 [PSH, ACK],Server 端确认接收到数据 [ACK]

这种情况就是正常的请求,即全连接队列、半连接队列未满,client 成功与 server 建立了 tcp 链接,并成功发送数据。

情况二:Client 认为成功与 Server 端建立 tcp socket 连接,后续发送数据失败,持续 RETRY;Server 端认为 TCP 连接未建立,一直在发送SYN+ACK

从一次线上问题说起,详解 TCP 半连接队列、全连接队列

上图可以看到如下请求:

  • Client 端向 Server 端发送 SYN 发起握手
  • Server 端收到 Client 端 SYN 后,向 Client 端回复 SYN+ACK,socket 连接存储到半连接队列(SYN Queue)
  • Client 端收到 Server 端 SYN+ACK 后,向 Server 端回复 ACK,Client 端进入 ESTABLISHED状态(重要:此时仅仅是 Client 端认为 tcp 连接建立成功)
  • 由于 Client 端认为 TCP 连接已经建立完成,所以向 Server 端发送数据 [PSH,ACK],但是一直未收到 Server 端的确认 ACK,所以一直在 RETRY
  • Server 端一直在 RETRY 发送 SYN+ACK

为什么会出现上述情况?Server 端为什么一直在 RETRY 发送 SYN+ACK?Server 端不是已经收到了 Client 端的 ACK 确认了吗?

上述情况是由于 Server 端 socket 连接进入了半连接队列,在收到 Client 端 ACK 后,本应将 socket 连接存储到全连接队列,但是全连接队列已满,所以 Server 端 DROP 了该 ACK 请求。

之所以 Server 端一直在 RETRY 发送 SYN+ACK,是因为 DROP 了 client 端的 ACK 请求,所以 socket 连接仍旧在半连接队列中,等待 Client 端回复 ACK。

tcp_abort_on_overflow 参数控制

全连接队列满DROP 请求是默认行为,可以通过设置 /proc/sys/net/ipv4/tcp_abort_on_overflow 使 Server 端在全连接队列满时,向 Client 端发送 RST 报文。

tcp_abort_on_overflow 有两种可选值:

  • 0:如果全连接队列满了,Server 端 DROP Client 端回复的 ACK
  • 1:如果全连接队列满了,Server 端向 Client 端发送 RST 报文,终止 TCP socket 链接 (TODO:后续有时间补充下该实验)

为什么实验结果中当前全连接队列大小 > 全连接队列最大长度配置?

上述结果中可以看到 Listen 状态的 socket 链接:

  • Recv-Q 当前全连接队列的大小是 6
  • Send-Q 全连接队列最大长度是 5
  1. StateRecv-QSend-QLocalAddress:PortPeerAddress:Port
  2. LISTEN65[::]:8888[::]:*

为什么全连接队列大小 > 全连接队列最大长度配置呢?

经过多次实验发现,能够进入全连接队列的 Socket 最大数量始终比配置的全连接队列最大长度 + 1。

结合其他文章以及内核代码,发现内核在判断全连接队列是否满的情况下,使用的是 > 而非 >= (具体是为什么没有找到相关资源 : ) )。

相关内核代码:

  1. /*Note:Ifyouthinkthetestshouldbe:
  2. *returnREAD_ONCE(sk->sk_ack_backlog)>=READ_ONCE(sk->sk_max_ack_backlog);
  3. *Thenpleasetakealookatcommit64a146513f8f("[NET]:Revertincorrectacceptqueuebacklogchanges.")
  4. */
  5. staticinlineboolsk_acceptq_is_full(conststructsock*sk)
  6. {
  7. returnREAD_ONCE(sk->sk_ack_backlog)>READ_ONCE(sk->sk_max_ack_backlog);
  8. }

情况三:Client 向 Server 发送 SYN 未得到相应,一直在 RETRY

图片上图可以看到如下请求:

  • Client 端向 Server 端发送 SYN 发起握手,未得到 Server 回应,一直在 RETRY

(这种情况涉及到半连接队列,这里先给上述情况发生的原因结论,具体内容将在下文半连接队列中展开。)

发生上述情况的原因由以下两方面导致:

1、开启了 /proc/sys/net/ipv4/tcp_syncookies 功能

2、全连接队列满了

实战 —— 半连接队列

半连接队列最大长度控制

翻阅了很多博文,查找关于半连接队列最大长度控制的相关内容,大多含糊其辞或不准确,经过不懈努力,最终找到了比较确切的内容(相关博文链接在附录中)。

很多博文中说半连接队列最大长度由 /proc/sys/net/ipv4/tcp_max_syn_backlog 参数指定,实际上只有在 linux 内核版本小于 2.6.20 时,半连接队列才等于 backlog 的大小。

这块的源码比较复杂,这里给一下大体的计算方式,详细的内容可以参考附录中的相关博文。半连接队列长度的计算过程:

  1. backlog=min(somaxconn,backlog)
  2. nr_table_entries=backlog
  3. nr_table_entries=min(backlog,sysctl_max_syn_backlog)
  4. nr_table_entries=max(nr_table_entries,8)
  5. //roundup_pow_of_two:将参数向上取整到最小的2^n,注意这里存在一个+1
  6. nr_table_entries=roundup_pow_of_two(nr_table_entries+1)
  7. max_qlen_log=max(3,log2(nr_table_entries))
  8. max_queue_length=2^max_qlen_log

可以看到,半连接队列的长度由三个参数指定:

  • 调用 listen 时,传入的 backlog
  • /proc/sys/net/core/somaxconn 默认值为 128
  • /proc/sys/net/ipv4/tcp_max_syn_backlog 默认值为 1024

我们假设 listen 传入的 backlog = 128 (Golang 中调用 listen 时传递的 backlog 参数使用的是 /proc/sys/net/core/somaxconn),其他配置采用默认值,来计算下半连接队列的最大长度

  1. backlog=min(somaxconn,backlog)=min(128,128)=128
  2. nr_table_entries=backlog=128
  3. nr_table_entries=min(backlog,sysctl_max_syn_backlog)=min(128,1024)=128
  4. nr_table_entries=max(nr_table_entries,8)=max(128,8)=128
  5. nr_table_entries=roundup_pow_of_two(nr_table_entries+1)=256
  6. max_qlen_log=max(3,log2(nr_table_entries))=max(3,8)=8
  7. max_queue_length=2^max_qlen_log=2^8=256

可以得到半队列大小是 256。

判断是否 Drop SYN 请求

当 Client 端向 Server 端发送 SYN 报文后,Server 端会将该 socket 连接存储到半连接队列(SYN Queue),如果 Server 端判断半连接队列满了则会将连接 Drop 丢弃。

那么 Server 端是如何判断半连接队列是否满的呢?除了上面一小节提到的半连接队列最大长度控制外,还和 /proc/sys/net/ipv4/tcp_syncookies 参数有关。(tcp_syncookies 的作用是为了防止 SYN Flood 攻击的,下文会给出相关链接介绍)

流程图

判断是否 Drop SYN 请求的流程图:

从一次线上问题说起,详解 TCP 半连接队列、全连接队列

上图是整理了多份资料后,整理出来的判断是否 Drop SYN 请求的流程图。

注意:第一个判断条件 「当前半连接队列是否已超过半连接队列最大长度」在不同内核版本中的判断不一样,Linux4.19.91 内核判断的是当前半连接队列长度是否 >= 全连接队列最大长度。

相关内核代码:

  1. staticinlineintinet_csk_reqsk_queue_is_full(conststructsock*sk)
  2. {
  3. returninet_csk_reqsk_queue_len(sk)>=sk->sk_max_ack_backlog;
  4. }

我们假设如下参数,来计算下当 Client 端只发送 SYN 包,理论上 Server 端何时会 Drop SYN 请求:

  • 调用 listen 时传入的 backlog = 1024
  • /proc/sys/net/core/somaxconn 值为 1024
  • /proc/sys/net/ipv4/tcp_max_syn_backlog 值为 128

当 /proc/sys/net/ipv4/tcp_syncookies 值为 0 时

  • 计算出的半连接队列最大长度为 256
  • 当半连接队列长度增长至 96 后,再新增 SYN 请求,就会触发 Drop SYN 请求

当 /proc/sys/net/ipv4/tcp_syncookies 值为 1 时

1.计算出的半连接队列最大长度为 256

2.由于开启了 tcp_syncookies

  • 当全连接队列未满时,永远不会 Drop 请求 (注意:经实验发现这个理论是错误的,实验发现只要半连接队列的大小 > 全连接队列最大长度就会触发 Drop SYN 请求)
  • 当全连接队列满了后,即全连接队列大小到 1024 后,就会触发 Drop SYN 请求

PS:/proc/sys/net/ipv4/tcp_syncookies 的取值还可以为 2,笔者没有详细实验。

回顾全连接队列实验结果

在上文全连接队列实验中,有一类实验结果是:client 向 Server 发送 SYN 未得到响应,一直在 RETRY。

发生上述情况的原因由以下两方面导致:

1. 开启了 /proc/sys/net/ipv4/tcp_syncookies 功能

2. 全连接队列满了

半连接队列溢出实验

上文我们已经知道如何计算理论上半连接队列何时会溢出,下面我们来具体实验下

(Golang 调用 listen 时传入的 backlog 值为 somaxconn)

实验一:syncookies=0,somaxconn=1024,tcp_max_syn_backlog=128

理论上:

  • 计算出的半连接队列最大长度为 256
  • 当半连接队列长度增长至 96 后,后续 SYN 请求就会触发 Drop

将相关参数的配置更新

  1. $sudosysctl-p
  2. net.core.somaxconn=1024
  3. net.ipv4.tcp_max_syn_backlog=128
  4. net.ipv4.tcp_syncookies=0

启动服务端 Server 监听 8888 端口(代码参考全连接队列实验物料)

客户端 Client 发起 SYN Flood 攻击:

  1. $sudohping3-S33.9.192.157-p8888--flood
  2. HPING33.9.192.157(eth033.9.192.157):Sset,40headers+0databytes
  3. hpinginfloodmode,noreplieswillbeshown

查看服务端 Server 8888端口处于 SYN_RECV 状态的 socket 最大个数:

  1. [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
  2. $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
  3. 96
  4.  
  5. [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
  6. $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
  7. 96

实验结果符合预期,当半连接队列长度增长至 96 后,后续 SYN 请求就会触发 Drop。

实验二:syncookies = 0,somaxconn=128,tcp_max_syn_backlog=512

理论上:

  • 计算出的半连接队列最大长度为 256,由于笔者实验机器上的内核版本是 4.19.91,所以当半连接队列长度 >= 全连接队列最大长度时,内核就认为半连接队列溢出了
  • 所以当半连接队列长度增长至 128 后,后续 SYN 请求就会触发 DROP

将相关参数的配置更新

  1. $sudosysctl-p
  2. net.core.somaxconn=128
  3. net.ipv4.tcp_max_syn_backlog=512
  4. net.ipv4.tcp_syncookies=0

启动服务端 Server 监听 8888 端口(代码参考全连接队列实验物料)

客户端 Client 发起 SYN Flood 攻击:

  1. $sudohping3-S33.9.192.157-p8888--flood
  2. HPING33.9.192.157(eth033.9.192.157):Sset,40headers+0databytes
  3. hpinginfloodmode,noreplieswillbeshown

查看服务端 Server 8888端口处于 SYN_RECV 状态的 socket 最大个数:

  1. [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
  2. $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
  3. 128
  4.  
  5. [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
  6. $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
  7. 128

实验结果符合预期,当半连接队列长度增长至 128 后,后续 SYN 请求就会触发 Drop

实验三:syncookies = 1,somaxconn=128,tcp_max_syn_backlog=512

理论上:

  • 当全连接队列未满,syncookies = 1,理论上 SYN 请求永远不会被 Drop

将相关参数的配置更新

  1. $sudosysctl-p
  2. net.core.somaxconn=128
  3. net.ipv4.tcp_max_syn_backlog=512
  4. net.ipv4.tcp_syncookies=1

启动服务端 Server 监听 8888 端口(代码参考全连接队列实验物料)

客户端 Client 发起 SYN Flood 攻击:

  1. $sudohping3-S33.9.192.157-p8888--flood
  2. HPING33.9.192.157(eth033.9.192.157):Sset,40headers+0databytes
  3. hpinginfloodmode,noreplieswillbeshown

查看服务端 Server 8888端口处于 SYN_RECV 状态的 socket 最大个数:

  1. [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
  2. $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
  3. 128
  4.  
  5. [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
  6. $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
  7. 128

实验发现即使syncookies=1,当半连接队列长度 > 全连接队列最大长度时,就会触发 DROP SYN 请求!!!(TODO:有时间阅读下相关内核源码,再分析下)

继续做实验,将 somaxconn 更新为 5

  1. $sudosysctl-p
  2. net.core.somaxconn=5
  3. net.ipv4.tcp_max_syn_backlog=512
  4. net.ipv4.tcp_syncookies=1

发起 SYN Flood 攻击后,查看服务端 Server 8888端口处于 SYN_RECV 状态的 socket 最大个数:

  1. [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
  2. $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
  3. 5
  4.  
  5. [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
  6. $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
  7. 5

确实 即使 syncookies=1,当半连接队列长度 > 全连接最大长度时,就会触发 DROP SYN 请求。

实验四:syncookies = 1,somaxconn=256,tcp_max_syn_backlog=128

理论上:

  • 当半连接队列大小到 256 后,后触发 DROP SYN 请求

将相关参数的配置更新

  1. $sudosysctl-p
  2. net.core.somaxconn=256
  3. net.ipv4.tcp_max_syn_backlog=128
  4. net.ipv4.tcp_syncookies=1

启动服务端 Server 监听 8888 端口(代码参考全连接队列实验物料)。

客户端 Client 发起 SYN Flood 攻击:

  1. $sudohping3-S33.9.192.157-p8888--flood
  2. HPING33.9.192.157(eth033.9.192.157):Sset,40headers+0databytes
  3. hpinginfloodmode,noreplieswillbeshown

查看服务端 Server 8888端口处于 SYN_RECV 状态的 socket 最大个数:

  1. [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
  2. $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
  3. 256
  4.  
  5. [zechen.hg@function-compute033009192157.na63/home/zechen.hg]
  6. $sudonetstat-nat|grep:8888|grepSYN_RECV|wc-l
  7. 256

实验结果符合预期,当半连接队列长度增长至 256 后,后续 SYN 请求就会触发 Drop。

回顾线上问题

再回顾值班时遇到的 Connection timeout 问题,当时相关系统参数配置为:

  • net.core.somaxconn = 128
  • net.ipv4.tcp_max_syn_backlog = 512
  • net.ipv4.tcp_syncookies = 1
  • net.ipv4.tcp_abort_on_overflow = 0

所以出现 Connection timeout 有两种可能情况:

1、半连接队列未满,全连接队列满,Client 端向 Server 端发起 SYN 被 DROP (参考全连接队列实验结果情况三分析、半连接队列溢出实验情况三)

2、全连接队列未满,半连接队列大小超过全链接队列最大长度(参考半连接队列溢出实验情况三、半连接队列溢出实验情况四)

问题的最快修复方式是将 net.core.somaxconn 调大,以及 net.ipv4.tcp_abort_on_overflow 设置为 1,net.ipv4.tcp_abort_on_overflow 设置为 1 是为了让 client fail fast。

总结

半连接队列溢出、全连接队列溢出这类问题很容易被忽略,同时这类问题又很致命。当半连接队列、全连接队列溢出时 Server 端,从监控上来看系统 cpu 水位、内存水位、网络连接数等一切正常,然而却会持续影响 Client 端业务请求。对于高负载上游使用短连接的情况,出现这类问题的可能性更大。

本文详细梳理了 TCP 半连接队列、全连接队列的理论知识,同时结合 Linux 相关内核代码以及详细的动手实验,讲解了 TCP 半连接队列、全连接队列的相关原理、溢出判断、问题分析等内容,希望大家在阅读后可以对 TCP 半连接队列、全连接队列有更充分的认识。

PS:可以去线上检查下服务器的相关参数哟~

附录

这里罗列下相关参考博文资料:

Linux 源码

  • https://github.com/torvalds/linux

Linux 诡异的半连接队列长度

  • https://www.cnblogs.com/zengkefu/p/5606696.html

TCP 半连接队列和全连接队列满了会发生什么

  • 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

原文链接:https://mp.weixin.qq.com/s/YpSlU1yaowTs-pF6R43hMw