前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从源码与实战分析TCP全连接队列溢出故障

从源码与实战分析TCP全连接队列溢出故障

原创
作者头像
五分钟学SRE
发布2024-04-29 02:37:29
1641
发布2024-04-29 02:37:29
举报
文章被收录于专栏:五分钟学SRE五分钟学SRE

文章总结-TCP 三次握手应该这么学

《深入解析TCP连接管理:三次握手与队列溢出应对策略》

图片
图片

我们先对文章内容的总结:

TCP三次握手过程:

  1. 客户端发送SYN:客户端调用connect系统调用,内核将套接字状态设置为TCP_SYN_SENT,并发送SYN报文。此时,内核创建request_sock结构,表示半连接请求。
  2. 服务端响应SYN-ACK:服务端收到SYN报文后,内核状态变为TCP_NEW_SYN_RECV,准备SYN-ACK报文响应客户端。
  3. 客户端完成握手:客户端收到SYN-ACK后,内核更新状态为TCP_ESTABLISHED,连接建立,客户端可以开始发送数据。

TCP队列管理:

  • 半连接队列(SYN queue):客户端发送SYN报文后,服务器接收进入SYN_RECV状态,连接被放入半连接队列。队列长度由tcp_max_syn_backlognet.core.somaxconnlisten(fd, backlog)backlog三者最小值决定。
  • 全连接队列(ACCEPT queue):客户端发送ACK报文后,服务器将连接从半连接队列移动到全连接队列,进入ESTABLISHED状态。队列长度由net.core.somaxconnlisten(fd, backlog)backlog两者最小值决定。

连接队列异常处理:

  • 半连接队列已满:服务器无法处理新的SYN请求,导致新的连接尝试失败。可以通过调整net.core.somaxconntcp_max_syn_backlog参数来增加队列大小。
  • 全连接队列已满:服务器已建立连接,但应用程序未及时调用accept(),导致新连接无法被接受。可以通过调整somaxconn参数来增加队列大小,并根据tcp_abort_on_overflow参数决定是丢弃ACK包还是发送RST包给客户端。

对应的内核参数:

  • tcp_max_syn_backlog:定义系统可以同时为还未完成三次握手的连接保留多少个半连接队列位置。
  • net.core.somaxconn:指定系统中所有套接字监听队列的最大长度。
  • tcp_abort_on_overflow:决定全连接队列溢出时的行为(丢弃ACK或发送RST)。

排查命令:

  • netstat -antss -ant:查看本地的TCP连接状态,检查SYN_SENT的数量是否异常。
  • sysctl net.core.somaxconn:查看和设置somaxconn的值。
  • sysctl net.ipv4.tcp_max_syn_backlog:查看TCP半连接队列的最大长度。
  • cat /proc/sys/net/ipv4/tcp_abort_on_overflow:查看tcp_abort_on_overflow的当前值。
  • netstat -s | grep "overflowed":查看全连接队列溢出的次数。

压测工具-wrk:轻量级的HTTP性能测试工具

wrk是一个基于C语言编写的HTTP性能测试工具,由GitHub用户wg/wrk开发。它能够通过生成大量的HTTP请求,对服务器进行压力测试,并实时输出测试结果,包括请求速率、传输速率、连接数等关键性能指标。wrk的设计初衷是为了提供一个简单易用的性能测试工具,同时保证测试结果的准确性和可靠性。

wrk的特点

  1. 轻量级:wrk采用C语言编写,资源占用少,运行效率高。它能够在不消耗过多系统资源的情况下,快速生成大量的HTTP请求。
  2. 功能强大:虽然wrk的界面简洁,但功能却十分强大。它支持自定义请求头、请求方法、请求内容等参数,能够模拟各种复杂的HTTP请求场景。
  3. 实时反馈:在测试过程中,wrk会实时输出各项性能指标,如请求速率、传输速率等,帮助开发者及时了解服务器的性能表现。
  4. 易于使用:wrk的使用非常简单,只需几个参数即可开始测试,使得开发者可以快速上手并进行性能测试。

wrk的使用方法

wrk的使用非常简单,基本的命令格式如下:

代码语言:javascript
复制
wrk [options] http://host:port/path

其中,[options]是可选的参数,http://host:port/path是待测试的URL。下面是一些常用的选项:

  • -c, --connections:设置并发连接数。
  • -t, --threads:设置线程数。
  • -d, --duration:设置测试持续时间(秒)。
  • -D, --header:添加自定义请求头。
  • -H, --default-header:设置默认请求头。
  • -s, --script:指定Lua脚本文件,用于自定义请求行为。

例如,要对一个Web服务器进行压力测试,可以使用以下命令:

代码语言:javascript
复制
wrk -c 100 -t 10 -d 60 http://example.com/

这个命令将会模拟100个并发连接,使用10个线程,持续测试60秒。

实战 - TCP 全连接队列溢出

全连接队列最大长度控制

TCP 全连接队列的最大值取决于 somaxconn 和 backlog 之间的最小值,也就是 min(somaxconn, backlog),其中:

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

相关的内核代码:

代码语言:javascript
复制
// 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 全连接队列最大长度 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; }

查看全连接队列溢出的命令

ss

代码语言:javascript
复制
# -n 不解析服务名称 # -t 只显示 tcp sockets # -l 显示正在监听(LISTEN)的 sockets  ss -lnt State               Recv-Q              Send-Q                           Local Address:Port                           Peer Address:Port              Process              LISTEN              0                   511                                    0.0.0.0:80                                  0.0.0.0:*                                      LISTEN              0                   128                                    0.0.0.0:22                                  0.0.0.0:*                                      LISTEN              0                   128                                  127.0.0.1:631                                 0.0.0.0:*                                      LISTEN              0                   4096                             127.0.0.53%lo:53                                  0.0.0.0:*                                      LISTEN              0                   511                                       [::]:80                                     [::]:*                                      LISTEN              0                   128                                       [::]:22                                     [::]:*                                      LISTEN              0                   128                                      [::1]:631                                    [::]:*

我们可以从源码看到 ss 命令获取的 Recv-Q/Send-Q 在「LISTEN 状态」和「非 LISTEN 状态」所表达的含义是不同的。

代码语言:javascript
复制
// 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 状态是 LISTEN 时     r->idiag_rqueue = READ_ONCE(sk->sk_ack_backlog);  // 当前全连接队列大小     r->idiag_wqueue = READ_ONCE(sk->sk_max_ack_backlog); // 全连接队列最大长度   } else if (sk->sk_type == SOCK_STREAM) {    // socket 状态不是 LISTEN 时     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);    // 已收到但未被应用程序读取的字节数     r->idiag_wqueue = READ_ONCE(tp->write_seq) - tp->snd_una;   // 已发送但未收到确认的字节数   }   if (info)     tcp_get_info(sk, info); }

对于 LISTEN 状态的 socket

  • Recv-Q:当前全连接队列的大小,即已完成三次握手等待应用程序 accept() 的 TCP 链接
  • Send-Q:全连接队列的最大长度,即全连接队列的大小
代码语言:javascript
复制
# -n 不解析服务名称 # -t 只显示 tcp sockets # -l 显示正在监听(LISTEN)的 sockets
图片
图片

对于非 LISTEN 状态的 socket

  • Recv-Q:已收到但未被应用程序读取的字节数
  • Send-Q:已发送但未收到确认的字节数
代码语言:javascript
复制
# -n 不解析服务名称 
# -t 只显示 tcp sockets
图片
图片

模拟环境

为了实验效果明显,我们修改系统的长连接队列设置

把somaxconn 设置为8

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

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

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

代码语言:javascript
复制
sudo sysctl -p 
net.core.somaxconn = 8

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

代码语言:javascript
复制
cat /proc/sys/net/core/somaxconn
8

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

代码语言:javascript
复制
ss -lnt | grep 8080
LISTEN 0      8          0.0.0.0:8080      0.0.0.0:*  

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

部署nginx 服务

代码语言:javascript
复制
apt  install nginx

配置nginx 监听

代码语言:javascript
复制
vim /etc/nginx/conf.d/bingo.conf
server {
    listen 8080 default; 
    server_name localhost; 

    location / {
    return 200 "bingo";
    }
}

修改work数

代码语言:javascript
复制
vim /etc/nginx/nginx.conf
worker_processes 1;

relead nginx

代码语言:javascript
复制
nginx -s reload

进行持续的压测

代码语言:javascript
复制
wrk -t 6 -c 30000 -d 60s http://127.0.0.1:8080/

使用ss 命令查看当前TCP全连接队列的情况:

图片
图片

可以看到当前的TCP全连接对接到了9大于最大TCP全连接队列,一旦超过了系统设置的TCP最大全连接队列,服务端就会丢掉后续进来的请求,我们可以使用netstat -s 进行统计查看

图片
图片

可以按到514458times 表示全连接队列溢出的次数,注意这个是累计值。

可以隔几秒钟执行下,如果这个数字一直在增加的话肯定全连接队列偶尔满了。

通过抓包分析

图片
图片

可以看到很多SYN + ACK的报文,这个是因为全连接队列满了导致Client 认为成功与 Server 端建立 tcp socket 连接,后续发送数据失败,持续 RETRY;Server 端认为 TCP 连接未建立,一直在发送SYN+ACK.

可以看到请求流程如下:

  • client 端向server端发送SYN包 握手报文
  • server 端收到SYN包后,回复SYN + ACK包,把socket连接存储到半连接队列(SYN Queue)
  • client端收到server端的SYN + ACK包后,向server端回复ACK,client端进入ESTABLESHED 状态-tip:这个时候只有client端认为tcp 连接建立成功
  • 由于client端任务TCP连接已经建立完成,所以会向server端发送数据[PSH,ACK],但是一直没有收到server端的ACK包,所以会一直的RETRY
代码语言:javascript
复制
 Server 端 socket 连接进入了半连接队列,在收到 Client 端 ACK 后,本应将 socket 连接存储到全连接队列,但是全连接队列已满,所以 Server 端 DROP 了该 ACK 请求。
  • server端一直在RETRY发送SYN+ACK
代码语言:javascript
复制
 Server 端一直在 RETRY 发送 SYN+ACK,是因为 DROP 了 client 端的 ACK 请求,所以 socket 连接仍旧在半连接队列中,等待 Client 端回复 ACK。

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

代码语言:javascript
复制
cat /proc/sys/net/ipv4/tcp_abort_on_overflow
0

tcp_abort_on_overflow

tcp_abort_on_overflow 有两种可选值:

  • 0:如果全连接队列满了,Server 端 DROP Client 端回复的 ACK
  • 1:如果全连接队列满了,Server 端向 Client 端发送 RST 报文,终止 TCP socket 链接

把 tcp_abort_on_overflow 设置为 1

代码语言:javascript
复制
root@adming-virtual-machine:/mnt/hgfs# echo "1" > /proc/sys/net/ipv4/tcp_abort_on_overflowroot@adming-virtual-machine:/mnt/hgfs# cat  /proc/sys/net/ipv4/tcp_abort_on_overflow1
代码语言:javascript
复制
发起请求
代码语言:javascript
复制
wrk -t 6 -c 30000 -d 60s http://127.0.0.1:8080/

可以看到全连接队列已经满了

图片
图片

通过抓包可以看到很多reset的报文

图片
图片

应对措施

默认设置:通常推荐将tcp_abort_on_overflow设置为0,这有助于在面对突发流量时维持连接的稳定性。

流量管理:当TCP全连接队列因流量突增而溢出时,如果服务器丢弃了客户端的ACK包,客户端会认为连接未建立成功,从而触发重传机制。设置tcp_abort_on_overflow为0允许系统在队列有空间时继续处理这些连接请求,而不是立即终止它们。

连接状态:即使在服务器端的全连接队列溢出的情况下,如果客户端的连接状态已经是ESTABLISHED,客户端进程仍然会尝试在已建立的连接上发送请求。由于服务器没有回复ACK,客户端会不断重发请求。

队列管理:如果服务器进程只是暂时繁忙导致accept队列满,那么一旦TCP全连接队列有空闲,新的请求报文(包含ACK)可以触发服务器端成功建立连接。

设置场景:仅在确定TCP全连接队列会长期处于溢出状态时,才应将tcp_abort_on_overflow设置为1,这样可以快速通知客户端连接无法建立,避免资源浪费。

权衡考虑:设置为1可以迅速释放资源,但可能会牺牲一些连接成功率。因此,除非有明确的性能问题,否则保持默认的0设置通常是更优的选择。

增大TCP全连接队列:在系统确认是全连接对接溢出,而且未定位到根因我们可以根据系统处理能力把全连接队列调大,来恢复或缓解线上故障会客户的影响。

我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章总结-TCP 三次握手应该这么学
    • 《深入解析TCP连接管理:三次握手与队列溢出应对策略》
      • TCP三次握手过程:
        • TCP队列管理:
          • 连接队列异常处理:
            • 对应的内核参数:
              • 排查命令:
                • 压测工具-wrk:轻量级的HTTP性能测试工具
                  • wrk的特点
                  • wrk的使用方法
                  • 全连接队列最大长度控制
                  • 对于 LISTEN 状态的 socket
              • 实战 - TCP 全连接队列溢出
                • 模拟环境
                  • tcp_abort_on_overflow
              • 应对措施
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档