从压测碰到的诡异断连问题聊聊Nginx的连接管理
本文主要分享一个在压测Nginx反向代理服务过程中碰到的连接异常断开问题,包括问题的定位与复现,最后由这个实际问题引申聊一下Nginx的连接管理。
本博客已迁移至CatBro's Blog,那是我自己搭建的个人博客,欢迎关注。
问题描述
问题是这样的,我们的Nginx服务是作为HTTP反向代理,前端是HTTPS,后端是HTTP。在一次压测过程中碰到了连接异常断开的问题,但是Nginx这边没有发现任何的错误日志(已经开了Info级别也没有)。因为是在客户那边进行的测试,而且是同事在对接的项目,我并不了解第一手的情况,包括测试方法、Nginx的配置等,唯一给到我这边的就是一个抓包,只有这个是确凿的信息,其余的就是一些零星的口头转述。同事多次尝试在公司复现这个问题但没有成功。
抓包的情况是这样的:
抓包文件很大,在一段很短的时间内出现很多连续的这种错误。上面的截图中是跟前端的交互,因为是压测的关系,短时间有大量的包,所以从抓包中无法确定对应的后端连接,不清楚后端连接是否已经建立,是否是后端出错了。
问题定位
我们首先分析下上图中抓包的情况,前面是一个GMVPN的握手,因为压测的关系,服务端回复ServerHello消息以及后面的ChangeCipherSpec消息都隔了一两秒的时间。握完手之后客户端发了两个应用数据包(对应HTTP请求头和请求体)。大概两秒之后,服务端发送了一个alert消息,然后紧接着发送了一个reset。
同事还观察到,抓包中从收到应用数据到连接断开的时间都是2s左右,所以猜测可能跟超时有关。reset的发送原因也是一个关键的线索,另外还有前面提到的Nginx日志(Info级别)中没有任何错误。我们就根据这几个线索来进行问题的定位,首先来分析reset的情况。
reset原因
有很多情况都会触发TCP的reset,其中大部分都是内核自身的行为。
端口没有监听
显然可以排除这种情况,我们的连接都已经开始处理了。
防火墙拦截
服务器并没有配置类似的防火墙策略,这种情况也排除。
向已经关闭的socket发送消息
socket可能是进程主动关闭,也可能是被动关闭,也就是进程崩了的情况。显然从抓包可以看出我们的进程并没有崩,况且进程崩了也会有内核日志。那如果是进程主动关闭socket的情况呢,我们从抓包中可以看到服务端是在发送了一个Encrypted Alert消息之后紧接着就发送了reset,其间并没有收到客户端的消息,所以也可以排除这种情况。
接收缓冲区还有数据未接收时关闭该socket
因为我们并不清楚后端连接的情况,所以抓包中的两个应用数据包是否已经被应用层接收是无法确定的。因此,这种情况是有可能的。
SO_LINGER
前面几种都是内核自发的行为,不需要用户参与。SO_LINGER是一个TCP选项,可以修改close()系统调用的行为。
- 默认不开的情况下,close调用会立即返回,发送队列一直保持到发送完成,连接通过正常的四次挥手进行关闭。
- 打开且时间设为0的情况下,直接丢弃发送缓冲区的数据,并发送RST给对方(不走4次挥手流程)。
- 打开且时间不为0的情况下,进程将阻塞直到1)所有数据发送完毕且收到对方确认,2)超时时间到。对于前者会正常关闭;对于后者,close将返回EWOULDBLOCK,然后跟第二种情况相同处理,即且丢弃发送缓冲区数据然后发送RST。这种情况socket必须是阻塞的,否则close会立即返回。
了解了SO_LINGER的情况,一看Nginx代码,确实是用到了这个选项,不过只有当连接超时并且打开了reset_timeout_connection配置项时才会去设置。而这个选项默认是关闭的,且我们也没有显式地设置过,所以这种情况也排除了。
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握手显然是完成了,因为客户端已经发送了应用数据消息,所以在接收缓冲区中数据应该就是Application Data。至于请求头是否已经被读取目前还不好判断。不过从抓包可以看出,服务端是直接关闭的连接,并没有给客户端发送响应,所以可以确认服务端还没有走到应用层处理的环节,要么是还没有接收请求头、要么是还没处理完毕,否则肯定会有应用层的响应。于是问题的范围就缩小到了SSL握手完成之后、请求头处理完毕之前。
是不是之前提到的超时原因呢?不过同事又指出超时时间已经设置了很大(分钟级别),那会不会遗漏了某些超时呢。但是好像没有两秒这么短的超时时间,Nginx默认的超时基本都是60s级别的。于是开始寻找可能存在的超时,发现SSL阶段并没有单独的超时,在请求头读完之前就只有一个超时时间就是client_header_timeout,但是显然这个超时不是2s。我们合理假设是现场配置错了,但我在确认代码后发现如果是超时也是会有INFO级别日志,接收到请求前和接收到请求后都是如此。
所以超时这条路走不通了,大概率不是由于超时导致的连接关闭。没有办法只能继续看源码分析,看看从SSL握手完成到HTTP请求头处理完成之前,Nginx到底干了些什么。在详细看了这部分代码之后,一个在不同handler中多次出现的函数ngx_reusable_connection
引起了我的注意。这个函数用于修改连接的reusable标记,并且ngx_cycle维护了一个resuable连接的队列,那么这个队列是干啥的呢?进一步探究发现,ngx_get_connection
在获取新连接的时候,如果空闲的连接不足,会尝试重用部分reusable的连接(一次最多32个)。而nginx连接在完成SSL握手之后、接收到HTTP请求之前就是处于reusable状态的。我们再次打开抓包文件一数,发现连续关闭的连接正好是31/32个左右,于是我们已经有八九成的把握就是因为这个原因导致的连接断开,而且正好这种情况下我们使用版本的Nginx是没有日志的(高版本加了WARN级别日志)。
复现问题
为了进一步证明就是这个原因导致的连接断开,我尝试构造场景复现问题,这个问题的关键在于worker进程的总连接数不足,但是只建立前端连接又是够的,有很多连接停留在SSL握手完成又没有开始处理HTTP请求头的阶段。当其他连接的请求尝试建立后端连接时,就会把这些reusable连接踢掉。所以worker最大连接数需要大于前端连接数、小于前端连接数的两倍。
因为我是用自己虚拟机简单测试,客户端用wrk设置了100个连接,nginx只配了1个worker,最大连接数是120(具体数值可能有点出入,因为已经过去有段时间了记得不太清楚了)。一测试成功复现了这个问题,抓包截图如下:
这是跟踪的其中一个流,可以看到也是在SSL握手完成之后,收到了客户端发送的应用数据,然后发送了Alert以及RST。顺带提一下,这里后面还多了一个RST,这是因为连接关闭之后收到了客户端的一个ACK。
再看下一个截图,可以观察到,在这两个reusable连接被踢掉之后,立马就往后端新建了两个连接。
至此,问题的原因基本已经确认。直接原因就是因为worker连接数不足。
总结回顾
问题原因已经定位到,再回过头看现场的测试配置。其实按照所有worker的连接总数来算,连接是够的,而单个worker则是不够的。但是因为其他几个配置的间接作用导致了连接集中在了单个worker中。首先,因为系统版本较低不支持reuseport,只能依赖nginx自身进行进程间的连接分配。其次又配置了multi_accept,所以只要有已经就绪的TCP连接,worker进程就一直进行accept,造成单个worker进程的接收了大部分连接。这几个因素结合在一起造成了最终的问题。归根结底,还是因为测试人员对Nginx配置理解不够深入导致的。
连接生命周期
在讨论前面的问题时我们也看到了,Nginx中的连接是有几个不同的状态的,我们分成连接建立时和连接关闭时两部分来看连接的生命周期。
连接建立
下面是连接建立时一个大致的调用关系图,实际情况要比着复杂的多。任何一处都可能超时或出错提前终止连接,碰到NGX_EAGAIN则可能多次调用同一个handler。
ngx_event_accept
|-- accept
|-- ngx_get_connection
+-- ngx_http_init_connection
|-- ngx_reusable_connection(1)
+-- ngx_http_ssl_handshake
|-- 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是从accept才开始接手连接的处理,在此之前则完全是内核的行为。不过在初始化阶段可以通过设置一些socket选项来改变其行为,大家比较熟知的有比如SO_REUSEPORT,TCP_DEFER_ACCEPT。前者允许多个socket绑定到在相同的地址端口对上,由内核在多个进程的socket间进行连接接收的负载均衡,后者则可以推迟连接就绪的时间,只有当收到客户端发来的应用数据时才可以accept。
accept之后nginx会从空闲的连接中获取一个,这个动作在ngx_get_connection中完成,然后进入HTTP初始化流程。我们这里主要关注连接状态的变化情况,它是通过ngx_resuable_connection函数进行修改。最初连接是处于free状态的,进入ngx_http_ssl_handshake完成一些基本的初始化之后,连接设置定时器开始准备接收消息,此时的超时时间是post_accept_timeout,也就是配置项中的client_header_timeout,同步地连接进入reusable状态。等到接收到SSL握手消息之后,会创建SSL连接,同步地nginx连接进入unreusable状态。后续会进入握手流程,等到握手完成之后,连接又变成了reusable状态,开始等待接收HTTP请求,此时的超时时间仍然是post_accept_timeout。直到接收到HTTP请求,连接就此进入unreusable状态。一直到请求结束为止,状态都不再变化。
连接关闭
接下来再来看下请求结束时的情况,如果是短连接的情况会进入ngx_http_close_request流程,释放请求之后会关闭连接,连接变为free状态被放入空闲队列中。如果是长连接的情况则会进入ngx_http_set_keepalive流程,此时请求被释放,但是连接进入reusable状态,此时定时器的超时时间就是keepalive_timeout了。如果在超时时间内收到了新的请求,那么连接又变为unreusable状态,进入请求的处理流程;如果直到超时都没有收到新请求,则会调用ngx_http_close_connection关闭连接,连接变为free状态被放入空闲队列中。
值得注意的是,连接变成reusable状态时,肯定是处于等待什么消息的状态,同步地会有一个定时器存在。
ngx_http_finalize_request
+-- ngx_http_finalize_connection
|-- ngx_http_set_keepalive
| |-- ngx_http_free_request
| |-- ngx_reusable_connection(1)
| +-- ngx_http_keepalive_handler
| |-- 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阶段为例,主要有以下这些超时:
- client_header_timeout (60s): 在这个时间内,这个请求头必须接收完。
- client_body_timeout (60s):读请求头的超时时间,只是限制两次连续操作之间的间隔
- send_timeout (60s):发送响应给客户端的超时时间,同样只是限制两次连续操作之间的间隔
- proxy_connect_timeout (60s):与后端服务器建立连接的超时
- proxy_send_timeout (60s):发送请求给代理服务器的超时,只是限制两次连续操作之间的间隔
- proxy_read_timeout (60s):从代理服务器读响应的超时,只是限制两次连续操作之间的间隔
- keepalive_timeout (75s):长连接保持打开的时间。
- resolver_timeout (5s): 域名解析的超时时间
连接的超时控制,当然是为了防止“坏”的连接一直占用系统资源。但是我们注意到,并不是所有超时都是限制总体的时间,很多超时都只是限制两次连续操作之间的间隔。所以一个恶意的连接,其实还是可以做到长时间占用一个连接的。比如客户端发送请求体时,每次只发一个字节,但是赶在服务端超时之前发送第二个字节。对于这种情况,貌似没有太好的避免办法。不过我们可以通过限速等其他手段,限制恶意方占用的连接个数,一定程度缓解了这个问题。
从压测碰到的诡异断连问题聊聊Nginx的连接管理的更多相关文章
- surging作者出具压测结果
前言 首先回应下@wen-wen 所贴的压测报告,我也把我和客户压测碰到的问题,和压测结果贴出来,这个结果是由客户提供的.不会有任何的舞弊手脚问题 问题一:Task.Run慎用 首先在最新的社区版本已 ...
- 京东全链路压测军演系统(ForceBot)架构解密
摘要:全链路压测是应对电商大促容量规划最有效的手段,如何有效进行容量规划是其中的架构关键问题.京东在全链路压测方面做过多年尝试,本文转载京东商城基础平台技术专家文章,介绍其最新的自动化压测 Force ...
- <转>二十问全链路压测干货汇总(上)
本文转载自:微信公众号-数列科技<二十问全链路压测干货汇总(上)> 最近几年全链路压测无疑成为了一个热门话题,在各个技术峰会上都可以看到它的身影. 一些大型的互联网公司,比如阿里巴巴.京东 ...
- 案例 | 荔枝微课基于 kubernetes 搭建分布式压测系统
王诚强,荔枝微课基础架构负责人.热衷于基础技术研发推广,致力于提供稳定高效的基础架构,推进了荔枝微课集群化从0到1的发展,云原生架构持续演进的实践者. 本文根据2021年4月10日深圳站举办的[腾讯云 ...
- 性能压测诡异的Requests/second 响应刺尖问题
最近一段时间都在忙着转java项目最后的冲刺,前期的coding翻代码.debug.fixbug都逐渐收尾,进入上线前的性能压测. 虽然不是大促前的性能压测要求,但是为了安全起见,需要摸个底心里有个数 ...
- 【原创】MySQL Replay线上流量压测工具
一. 背景 去年做过一次mysql trace 重放的测试,由于performance schema本身采集样本的长度等限制,实际回放的成功率比较低. 最近找到一款开源的工具,基于TCPCopy实现了 ...
- 高德全链路压测平台TestPG的架构与实践
导读 2018年十一当天,高德DAU突破一个亿,不断增长的日活带来喜悦的同时,也给支撑高德业务的技术人带来了挑战.如何保障系统的稳定性,如何保证系统能持续的为用户提供可靠的服务?是所有高德技术人面临的 ...
- 在Mac上利用压测工具Jmeter-Suite进行一次压测实践的保姆级详细步骤(参考腾讯云文章)
参考的文章 压测工具Jmeter-Suite详细操作步骤 写此文的目的 由于我是刚开始接触kubernetes和jmeter,所以在学习过程中遇到了很多很多问题,同时我很烦恼为什么网上没有文章是从真正 ...
- mysql每秒最多能插入多少条数据 ? 死磕性能压测
前段时间搞优化,最后瓶颈发现都在数据库单点上. 问DBA,给我的写入答案是在1W(机械硬盘)左右. 联想起前几天infoQ上一篇文章说他们最好的硬件写入速度在2W后也无法提高(SSD硬盘) 但这东西感 ...
随机推荐
- 程序语言与编程实践4-> 蓝桥杯C/C++备赛记录2 | 第二周学习训练
0323,又是一周星期三,按道理该总结了.这周前几天写题比较多,后面事情多了起来,就没怎么写了.主要方向是洛谷的基本语法熟悉,PTA平台数据结构的一些题目. 0323附上: 题目比较多,所以文章可能有 ...
- python psutila模块(示例)
# qianxiao996精心制作 #博客地址:https://blog.csdn.net/qq_36374896 import psutil import time import datetime ...
- Citus 分布式 PostgreSQL 集群 - SQL Reference(创建和修改分布式表 DDL)
创建和分布表 要创建分布式表,您需要首先定义表 schema. 为此,您可以使用 CREATE TABLE 语句定义一个表,就像使用常规 PostgreSQL 表一样. CREATE TABLE ht ...
- Linux 下通过ping判断机器有没有外网。(不用root)
背景: 想实现一个判断当前系统有没有外网的方法,想到了两种思路: 1)实现一个ICMP协议.但是这个需要root权限才能运行.可以参考:https://www.cnblogs.com/xcywt/p/ ...
- java反射和动态代理实现与原理详细分析
关于Java中的动态代理,我们首先需要了解的是一种常用的设计模式--代理模式,而对于代理,根据创建代理类的时间点,又可以分为静态代理和动态代理. 一.代理模式 代理模式是常用的java设计模式, ...
- Java 线程池中 submit() 和 execute()方法有什么区别?
两个方法都可以向线程池提交任务,execute()方法的返回类型是 void,它定义在 Executor 接口中. 而 submit()方法可以返回持有计算结果的 Future 对象,它定义在 Exe ...
- 关于kafka客户端版本与服务端版本不一致导致的一次坑
上周开发了一个功能,需要使用kafka接上游数据并入库,本地开发时,自己安装了一个kafka服务,开发测试时使用本地的kafka服务给主题发消息,然后在自己本地的代码中进行调试.使用的kafka版本如 ...
- switch 是否能作用在 byte 上,是否能作用在 long 上, 是否能作用在 String 上?
在 Java 5 以前,switch(expr)中,expr 只能是 byte.short.char.int.从 Java 5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型,从 ...
- springboot-数据库访问之jpa+mybatis+jdbc的区别
什么是jdbc jdbc(Java DataBase Connectivity)是java连接数据库操作的原生接口. 什么是jpa jpa(Java Persistence API)是java持久化规 ...
- (stm32学习总结)—spi基本原理
参考:spi详解 spi协议 SPI的基本介绍 SPI的简介 SPI,是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口,是Motorola首先在其M ...