gpt4 book ai didi

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

转载 作者:qq735679552 更新时间:2022-09-27 22:32:09 28 4
gpt4 key购买 nike

CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.

这篇CFSDN的博客文章从一次线上问题说起,详解 TCP 半连接队列、全连接队列由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.

从一次线上问题说起,详解 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、全连接队列未满,半连接队列大小超过全链接队列最大长度(参考半连接队列溢出实验情况3、半连接队列溢出实验情况四) 。

问题的最快修复方式是将 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 。

最后此篇关于从一次线上问题说起,详解 TCP 半连接队列、全连接队列的文章就讲到这里了,如果你想了解更多关于从一次线上问题说起,详解 TCP 半连接队列、全连接队列的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。

28 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com