从压测碰到的诡异断连问题聊聊Nginx的连接管理

本文主要分享一个Nginx反向代理服务压力测试过程中遇到的异常断开问题,包括问题的定位和重现,最后从这个实际问题来讨论Nginx的连接管理。

本博客已经迁移到’s Blog,这是我自己的个人博客,欢迎关注。

本文链接

问题描述

问题是这样的,我们的Nginx服务作为HTTP反向代理,前端是HTTPS,后端是HTTP。压测时异常断开连接,但是Nginx没有发现任何错误日志(Info级别已经开启,也没有)。因为测试是在客户端进行的,而且是同事对接的一个项目,所以不知道第一手的情况,包括测试方法,Nginx配置等等,唯一给我的东西是一个数据包捕获,只有这是可靠的信息,其余的只是一些零星的口头复述。同事多次尝试在公司重现此问题,均未成功。

抓包情况如下:

抓包文件很大,短时间内出现很多连续的包。这个错误。上面的截图显示了与前端的交互。因为压测,短时间内有大量数据包,所以无法从抓包中确定对应的后端连接。不清楚后端连接是否已经建立,是否是后端。错误。

问题位置

我们首先分析上图中的抓包情况。前面是 GMVPN 握手。因为压测,服务器回复消息和后面的消息都是一两秒后。握手之后,客户端发送两个应用数据包(对应HTTP请求头和请求体)。大约两秒钟后,服务器会发送一条警报消息,然后进行重置。

同事也观察到抓包中从收到应用数据到断开连接的时间大约是2s,所以猜测可能与超时有关。发送reset的原因也是一个关键线索,加上前面提到的Nginx日志(Info级别)没有任何错误。我们会根据这些线索定位问题,首先分析复位情况。

重置原因

触发TCP重置的情况很多,其中大部分是内核本身的行为。

看Nginx代码,确实使用了这个选项,但是只有在连接超时并且开启了tion配置项的时候才会设置。而且这个选项默认是关闭的,我们也没有明确设置,所以也排除了这种情况。

    if (r->connection->timedout) {
        clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
        if (clcf->reset_timedout_connection) {
            linger.l_onoff = 1;

            linger.l_linger = 0;
            if (setsockopt(r->connection->fd, SOL_SOCKET, SO_LINGER,
                           (const void *) &linger, sizeof(struct linger)) == -1)
            {
                ngx_log_error(NGX_LOG_ALERT, log, ngx_socket_errno,
                              "setsockopt(SO_LINGER) failed");
            }
        }
    }

至此可以断定reset的原因是在接收缓冲区还有数据的时候关闭了连接。至于为什么会关闭连接,还需要进一步定位。

连接关闭的原因

根据前面的结论,我们进一步调查,显然SSL握手完成了,因为客户端已经发送了应用数据报文,所以接收缓冲区中的数据应该是Data。至于请求头是否被读取,就不好判断了。但是从抓包中可以看出,服务端直接关闭了连接,并没有给客户端发送响应,所以可以确认服务端还没有到达应用层处理链路,或者没有收到请求头还没有,或者还没有处理,否则肯定会有应用层的响应。所以问题的范围缩小到 SSL 握手完成之后和请求头处理之前。

是不是前面提到的超时原因?但是我的同事也指出,超时时间已经设置为很大(分钟级别),那么会不会错过一些超时?但似乎没有两秒这么短的超时。 Nginx 的默认超时时间基本上是 60s。于是我开始寻找可能的超时,发现SSL阶段没有单独的超时。在读取请求头之前只有一个超时时间,但显然这个超时时间不是2s。我们合理的假设是现场配置错误,但确认代码后发现如果超时,接收请求前后都会有INFO级别的日志。

所以超时的路是行不通的,大概率是因为超时没有关闭连接。没办法,只能继续看源码分析,看看Nginx从完成SSL握手到完成HTTP请求头处理都做了些什么。详细查看了这部分代码后,一个在不同地方多次出现的函数 ion 引起了我的注意。这个函数是用来修改连接的标记,维护一个连接队列,那么这个队列有什么作用呢?进一步探索发现,在获取新连接时,如果没有足够的空闲连接,它会尝试重用部分连接(一次最多32个)。 nginx 连接处于 SSL 握手完成后收到 HTTP 请求之前的状态。我们再次打开抓包文件,发现连续关闭的连接数正好是31/32,所以我们已经有80-90%的把握是因为这个原因连接断开了,本例中我们使用的是版本Nginx 没有日志(更高版本添加 WARN 级别的日志)。

重现问题

为了进一步证明连接是因为这个原因断开的,我尝试构造一个场景再现问题。这个问题的关键是进程的总连接数不足,但是只建立前端连接就足够了,有很多连接卡在SSL握手完成和HTTP请求头的阶段不处理。当来自其他连接的请求尝试建立后端连接时,这些连接将被启动。因此,最大连接数需要大于前端连接数且小于前端连接数的两倍。

因为我用自己的虚拟机进行了简单的测试,客户端使用wrk设置了100个连接,nginx只配置了1个,最大连接数为120(具体数值可能有点不同,因为已经有一段时间了,记不太清了)。一次测试成功重现了这个问题,抓包截图如下:

这是跟踪到的流之一,可以看出也是在SSL握手完成之后。 ,接收到客户端发送的应用数据,然后发送Alert和RST。顺便说一句,这里还有一个额外的RST,因为在连接关闭后会收到来自客户端的ACK。

看下一个截图,你可以观察到这两个连接启动后,立即创建了两个新的连接到后端。

至此,问题的原因已经基本确定。直接原因是连接数不足。

总结回顾

问题原因已经定位,再回头看现场测试配置。其实按照所有连接的总数来看,连接就够了,单个还不够。但由于其他几个配置的间接影响,连接集中在一个单一的。首先,由于不支持系统版本,只能依靠nginx自己来分配进程间的连接。其次,它是配置的,所以只要有准备好的TCP连接,进程就会继续,导致单个进程接收大部分连接。这些因素结合起来产生了最终的问题。归根结底还是测试人员对Nginx配置缺乏深入了解造成的。

连接生命周期

我们在讨论上一个问题的时候也看到了,Nginx中的连接有几种不同的状态,我们分为连接建立时和连接关闭时两部分来看一个连接的生命周期。

连接建立

下面是建立连接时的一般调用关系图,实际情况要复杂得多。超时或错误都会提前终止连接,并且可能会多次调用同一个连接。

ngx_event_accept
|-- accept
|-- ngx_get_connection
+-- ngx_http_init_connection
    |-- ngx_reusable_connection(1)
    +-- ngx_http_ssl_handshake

图片[1]-从压测碰到的诡异断连问题聊聊Nginx的连接管理-唐朝资源网

|-- NGX_EAGAIN: ngx_reusable_connection(1) |-- ngx_ssl_create_connection |-- ngx_reusable_connection(0) |-- ngx_ssl_handshake +-- ngx_http_ssl_handshake_handler |-- ngx_reusable_connection(1) +-- ngx_http_wait_request_handler |-- ngx_reusable_connection(0) |-- ngx_http_create_request +-- ngx_http_process_request_line |-- ngx_http_read_request_header |-- ngx_http_parse_request_line +-- ngx_http_process_request_headers |-- ngx_http_read_request_header |-- ngx_http_parse_header_line |-- ngx_http_process_request_header +-- ngx_http_process_request |-- Switch stat from reading to writing +-- ngx_http_handler |-- HTTP to HTTPS? certificate verify fail? +-- ngx_http_core_run_phases +-- ngx_http_proxy_handler +-- ngx_http_read_client_request_body

首先,nginx从一开始就接管了连接处理,而在这之前,完全是内核的行为。但是,在初始化阶段,您可以通过设置一些选项来更改其行为,例如 .前者允许多个绑定到同一个地址和端口对,内核在多个进程之间执行连接接收的负载均衡,而后者可以延迟连接就绪的时间,只有在接收到来自客户端的应用程序时。仅限数据。

之后,nginx会从空闲连接中获取一个,这个动作在 中完成,然后进入HTTP初始化过程。这里我们主要关注连接状态的变化,它是由离子函数修改的。初始连接处于空闲状态。进入ke并完成一些基本的初始化后,连接设置定时器开始准备接收消息。此时超时时间在配置项中为t,连接同步进入状态。收到 SSL 握手消息后,会创建 SSL 连接,nginx 连接会同步进入状态。握手过程稍后进入。握手完成后,连接再次变为状态,开始等待接收HTTP请求。此时,超时时间仍为 。连接进入状态,直到收到 HTTP 请求。在请求结束之前状态不会改变。

连接关闭

接下来,我们看一下请求结束时的情况。如果是短连接,就会进入st进程。释放请求后,连接将关闭,连接将变为空闲状态。被放置在空闲队列中。如果是长连接,就会进入ve进程。此时请求被释放,但连接进入状态。这时候定时器的超时时间就足够了。如果在超时时间内收到新的请求,则连接再次变为状态,进入请求处理流程;如果在超时之前没有收到新的请求,则调用 关闭连接,连接变为空闲状态并在队列中置于空闲状态。

值得注意的是,当连接成为状态时,一定是处于等待消息的状态,并且会同步有一个定时器。

ngx_http_finalize_request
+-- ngx_http_finalize_connection
    |-- ngx_http_set_keepalive
    |   |-- ngx_http_free_request
    |   |-- ngx_reusable_connection(1)
    |   +-- ngx_http_keepalive_handler

图片[2]-从压测碰到的诡异断连问题聊聊Nginx的连接管理-唐朝资源网

| |-- ngx_http_close_connection | |-- ngx_reusable_connection(0) | |-- ngx_http_create_request | +-- ngx_http_process_request_line +-- ngx_http_close_request |-- ngx_http_free_request +-- ngx_http_close_connection |-- ngx_ssl_shutdown +-- ngx_close_connection |-- ngx_reusable_connection(0) |-- ngx_free_connection +-- ngx_close_socket

为了更清楚地展示连接状态的转变,我们用一张图来描述:

连接超时

连接的所有阶段都会出现超时。只要进程不处理当前连接,总会有一个定时器控制当前连接。以HTTP阶段为例,主要有以下几种超时:

连接的超时控制,当然是为了防止“坏”连接一直占用系统资源。但我们注意到,并非所有超时都限制了整体时间,许多超时仅限制了两次连续操作之间的间隔。因此,恶意连接实际上可以长时间占用一个连接。例如,客户端发送请求体时,一次只发送一个字节,但在服务器超时之前发送第二个字节。似乎没有什么好的方法可以避免这种情况。但是我们可以通过限速等其他手段来限制恶意方占用的连接数,一定程度上缓解了这个问题。

© 版权声明
THE END
喜欢就支持一下吧
点赞231赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容