listen函数backlog参数的一点探讨

前言:

​ 今年上半年的时候, 因为自己工作的失误, 导致程序的TCP三次握手非常缓慢, 存在大量syn_recv状态连接. 查了很多资料(尤其是listen的相关资料)都无法完美解决问题, 虽然调大了backlog参数, 但是连接数达到一定值(backlog)后, 三次握手同样非常缓慢. 后面才发现是自己代码的原因, 具体来讲, 就是我那部分代码阻塞了进程, 相当于sleep了下(虽然不是真的sleep). 后面我把这部分代码给删除了, 程序恢复了正常.

​ 最近一段时间, 又想到了这个问题, 所以就想写个测试程序来模拟当时的场景, 顺便对当时搜的资料做个总结.

​ 本文基于centos7环境, linux3.10内核. 好吧, 那就开始吧.

开始:

​ 这里有几个名词/参数需要了解下, 本文会遇到.

​ 全连接队列: 完成三次握手处于ESTABLISHED状态.

​ 未全连接队列: 此处称之为半连接队列. 服务端收到了SYN后半连接队列+1然后再回复SYN+ACK. 回复SYN + ACK后此时处于SYN_RECV状态.

​ 变量backlog: 函数listen的第二个参数.

​ 变量somaxconn: /proc/sys/net/core/somaxconn.
在这里插入图片描述

​ 变量tcp_max_syn_backlog: /proc/sys/net/ipv4/tcp_max_syn_backlog.
在这里插入图片描述

​ 变量syncookies: /proc/sys/net/ipv4/tcp_syncookies.
在这里插入图片描述

​ 变量tcp_synack_retries: net.ipv4.tcp_synack_retries, 含义是未激活的TCP连接发送SYN/ACK段的重试次数. 可以写在/etc/sysctl.conf内, 也可以通过代码设置.

​ 可以通过man 2 listen命令可以查看如下解释:

img

​ 大概意思是: backlog指定的是等待被accept的全连接socket队列长度, 代替了未全连接请求的数量. 未全连接socket队列的最大长度可以通过tcp_max_syn_backlog设置, 当设置了syncookies, 则不是逻辑上的最大长度, 这个设置可以忽略.

​ 还有一个系统默认参数somaxconn, 两者取最小值为全连接队列长度。

​ 读完这个解释后, 心里冒出几个问题:

​ 1, 半连接队列长度是多大.

​ 2, 全连接队列长度是多大.

​ 3, 半连接队列长度和全连接队列长度何时加减.

​ 4, 如何测试.

​ 关于第1, 2个问题, 详细解答可以查看下面的链接, 这里只做简答答疑.

TCP 的backlog详解及半连接队列和全连接队列_Blue summer的博客-CSDN博客_tcp 半连接

第1部分, 半连接队列长度: img

​ 这个函数的功能是判断半连接队列是否满了. 第一个变量为当前半连接队列的实际大小, 第二个变量为2^n(实际分析下来, 2^n就是半连接队的设置大小)中的指数n. 现在只需要知道max_qlen_log这个数值如何得来的就可以了. 看下图.

img

​ 传入的参数nr_table_entries的实际值为min(backlog, somaxconn), 那么:

​ 第46行: x = min(backlog, somaxconn, tcp_max_syn_backlog)

​ 第47行: y = max(x, 8)

​ 第48行: nr_table_entries的最终值为 (y + 1)最接近的2^n, 此为半连接队列大小, max_qlen_log的值为n.

第2部分, 全连接队列长度:

img

​ 这个函数的功能是判断全连接队列是否满了. 第一个变量指的是当前全连接队列的实际大小, 第二个变量是全连接队列的设置大小, 如此我们只需知道第二个变量是如何得来的就可以了.

​ 在这里我们需要查看两个函数, 第一个函数为listen的系统调用SYSCALL_DEFINE2(listen, int, fd, int, backlog). 第二个函数为其调用的具体函数int inet_listen(struct socket *sock, int backlog). 两个函数里分别有如下实现:

img

​ 可以看到, 函数1的实现就是取两者最小值, 函数2的实现就跟上述判断全连接队列是否满了函数的第二个变量匹配起来, 所以全连接队列的最终大小是: min(backlog, somaxconn)

第3部分, 队列加减操作:

3.1 半连接队列+1:

在服务端收到第一次握手的SYN时半连接队列+1, 具体调用如下.

img

3.2 半连接队列-1同时全连接队列+1:

在服务端收到第三次握手客户端发送的ACK时半连接队列会-1, 同时全连接队列会+1. 具体调用如下.

img

3.3 全连接队列-1:

在服务端调用accept时全连接队列-1. 具体调用如下.

img

第4部分, 如何测试:

​ 主要测试的内容是, 全连接队列已满时的各种信息. 所以, 测试代码实现的是不完整的非常规socket: 服务端只listen而没有accept调用, 客户端只connect而不读写. 测试代码见末尾.

4.1 条件1:

backlogsomaxconntcp_max_syn_backlogsyncookiestcp_synack_retries
1616161(开启)127

​ 根据第1部分和第2部分, 我们可以知道半连接队列设置大小为32, 全连接队列设置大小为16. 下面通过测试也简单验证下.

4.1.1 测试1:

​ 客户端启动10个tcp连接. 通过ss -ln命令分析, 结果如下图:

img

​ 可以看到, 全连接队列设置大小为16, 结果跟预测匹配; 全连接队列当前大小为10, 跟10个tcp连接匹配.

4.1.2 测试2:

​ 客户端启动25个tcp连接. 通过ss和netstat命令分析, 结果如下图:

img

​ 可以看到, 全连接队列当前大小为17, ESTABLISHED状态的个数也为17,结果互相匹配; 半连接状态的个数为8, 总共25个tcp连接, 结果匹配.

4.1.3 测试3:

​ 客户端启动49个tcp连接. 通过netstat命令分析, 结果如下图:

img

​ 可以看到, 半连接状态的最大值为32, 那么半连接队列当前大小的最大值为32.

4.1.4 测试4:

​ 客户端启动50个tcp连接. 通过netstat命令分析, 并用demsg查看内核日志, 结果如下图:

img

img

​ 可以发现, 半连接状态的最大值为32, 那么半连接队列当前大小最大值为32, 再通过和测试3比对, 可以知道半连接队列设置大小为32, 跟预测结果一致. 如果半连接队列已满时还有客户端的SYN请求, 那么内核将会出现syn flooding日志. 内核实现如下:

img

4.2 条件2:

backlogsomaxconntcp_max_syn_backlogsyncookiestcp_synack_retries
1616160(关闭)127

4.2.1 测试1:

​ 客户端启动30个tcp连接, 用demsg命令查看内核日志, 并无明显日志.

4.2.2 测试2:

​ 客户端启动31个tcp连接, 用dmesg命令查看内核日志:

img

​ 说明, 当关闭syncookies时, 半连接队列未满就已经开始丢弃SYN请求了. 内核实现如下:

img

结尾:

​ 其实我还是有些疑问的.

​ 1, 为什么全连接队列设置大小为16, 而最终ESTABLISHED的个数为17, 多个数值尝试都为+1的关系.

​ 这个疑问通过跟别人交流, 已经有了答案. 如下:

​ 切换到全连接队列的处理逻辑是这样子的: 先判断全连接队列是否已满, 再执行++操作; 如果backlog为16, 第16个ACK(第三次握手)到来时, 全连接队列实际大小为15, 未满, 则++, 此时才为16; 第17个ACK到来时, 实际大小为16, 而判断逻辑是>, 未满, 又++, 此时为17, 后续的就满了, 符合+1的逻辑关系.

​ 2, 4.1.3 测试3, 49个tcp连接, 此时并没有引起syn flooding, 为何半连接队列当前大小不能维持在32个, 因为我设置的重试次数为127次, 并没有达到重试的次数就开始降低了.

​ 3, 4.2.2 测试2, 根据代码, 半连接数达到13个可以使第二个条件成立(16 – 13 < 4), 那么理论上总连接数30(13 + 17全连接)个就能触发drop条件, 而最终的结果是启动31个连接数才能触发.

​ 本人能力有限, 文中或许有些错误, 如果有同学发现, 敬请指教. 或者有同学知道我的疑问, 也希望不吝赐教. 但是通过这次测试, 我对backlog参数有一个比较深的理解. 也期望自己能在余下的时间完善自己, 找到这些问题的答案.

代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
 
typedef struct config
{
        int mode;
        char *addr;
        unsigned short int port;
        int backlog;
        int conn_num;
 
        struct sockaddr_in addr_in;
        socklen_t  addr_in_len;
}config_t;
 
static int tcp_socket(const config_t *conf)
{
        int sfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sfd == -1)
        {
                printf("socket failed, err msg: %s\n", strerror(errno));
                return -1;
        }
 
        int val1 = 1;
        if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR | (conf->mode == 0 ? SO_REUSEPORT : 0), (void *)&val1, sizeof(val1)) == -1)
        {
                printf("setsockopt failed, err msg: %s\n", strerror(errno));
                goto FAILED;               
        }
 
        if (conf->mode == 0)
        {
                if (bind(sfd, (struct sockaddr*)&conf->addr_in, conf->addr_in_len) == -1)
                {
                        printf("bind failed, err msg: %s\n", strerror(errno));
                        goto FAILED;               
                }
                if (listen(sfd, conf->backlog))
                {
                        printf("bind failed, err msg: %s\n", strerror(errno));
                        goto FAILED;                              
                }                       
        }
        else
        {
                if (connect(sfd, (struct sockaddr*)&conf->addr_in, conf->addr_in_len))
                {
                        printf("connect failed, err msg: %s\n", strerror(errno));
                        goto FAILED;                        
                }                
        }
        return sfd;
 
FAILED:
        close(sfd);
        return -1;
}
 
static void sig_call(int sig)
{
        printf("capture sig: %d\n", sig);
}
 
static void server(config_t *conf)
{
        int sock_fd = tcp_socket(conf);
        if (sock_fd == -1)
        {
                return;
        }
 
        // 不accept
        sleep(3600);
}
 
static void client(config_t *conf)
{
        const int SIZE = conf->conn_num;
        int fds[SIZE], i = 0;
        for(; i < SIZE; ++i)
        {
                if ((fds[i] = tcp_socket(conf)) == -1)
                {
                        return;
                }
                if (i > conf->backlog)
                {
                        sleep(1);
                }
        }
        sleep(3600);
}
 
int main(int argc, char *argv[])
{
        signal(SIGPIPE, sig_call);
        config_t conf = {
                .mode = atoi(argv[1]),          // 0-服务端, 非0-客户端
                .addr = argv[2],                // 地址
                .port = atoi(argv[3]),          // 端口
                .backlog = atoi(argv[4]),       // 服务端listen的第二个参数
                .conn_num = atoi(argv[5]),      // 客户端启动TCP连接数
 
                .addr_in.sin_family = AF_INET,
                .addr_in.sin_addr.s_addr = inet_addr(argv[2]),
                .addr_in.sin_port = htons(atoi(argv[3])),
                .addr_in_len = sizeof(struct sockaddr_in),
        };
 
        if (conf.mode == 0)
        {
                server(&conf);
        }
        else
        {
                client(&conf);
        }
        return 0;
}

backlog 参数是用来限制 tcp listen queue 的大小的,真实的 listen queue 大小其实也是跟内核参数 somaxconn 有关系,somaxconn 是内核用来限制同一个端口上的连接队列长度。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值