【Nginx】记一次Nginx生产环境的Trouble Shooting过程
阅读数:
最新生产环境的Nginx出了点问题,总是会波动性地出现请求无法下发的错误,客户投诉爆炸了都,仔细排查了下,最终终于找到了解决问题的办法,特此记录一下。
其实在之前的【Nginx】Nginx高并发业务场景下的Linux系统配置及Nginx配置 这篇文章中就已经对高并发的服务器及NGINX配置做了比较详细的介绍,但是还不完整,正好通过这次的trouble shooting过程,对高并发服务器的配置再一次进行了完善。笔者也会在随后更新一下之前的文章内容。
系统架构
还是先来看一下系统架构,nginx在其中起到的作用就一个单纯的负载均衡,将前端发来的请求,均衡地分发到负载的节点上。
1 | +--------+ HTTP +-------+ HTTP +----------+ |
问题描述
出现报错的是nginx的这个报错:
1 | no live upstreams while connecting to upstreams |
从字面上看,就是没有可用的负载节点。没有负载节点? 其实无非就是下面两个原因
- 负载节点down掉了
- nginx评估认为负载节点不可用
负载节点有没有down掉可以很容易验证,而且我们的负载节点已经验证过是完全可靠的,只不过业务峰值的请求量超过了单线程处理时的最大能力,所以才在前面搭建了nginx作为负载均衡节点。
那就剩下为什么nginx会认为负载节点不可用。期间有人提到,会不会是tcp连接队列太小了,导致nginx任务负载节点连接出了问题。
TCP连接队列
网上有非常多的tcp连接队列的讲解和配置。不过中文的blog其实都是大家相互转载和借鉴,真正付诸实践的文章就那么几篇。但是每个人遇到的情况肯定是不同的,所以也不能单纯按照网上的文章去碰运气,还是要彻底搞清楚底层的原理,才能对自己的问题有所帮助。
TCP连接状态转换
关于一个TCP连接的各种状态之间的转换,网上已经有非常多的描述,这里我就不做赘述,网上有几张图,基本上介绍的比较清楚了
上面这张图是对TCP连接过程中的所有状态的一个集合,比较全面,但是也比较难懂,不过还是强烈建议彻底搞清楚TCP连接的各种状态,这样有利于分析网络上的问题。
我们拆成两部分来理解:
连接建立
大部分介绍TCP连接队列的中文blog都引用了这张图,其实这张图里面描述的都是正确的,但是有几个容易误解的点,笔者就是纠结全连接队列和establised连接,纠结了好久。
- 当server收到syn后,在系统底层,就把一个tcp连接,放到了半连接队列(syns queue)中,等到收到client回复的ack后,将其从半连接队列移入全连接队列(accept queue)。所以就有了针对半连接队列的攻击,即如果server收不到client回复的ack,那么半连接队列就有被占满的可能,也就是syn flood攻击。
- 当TCP连接被放入到accept queue中后,要等待上层的应用把这个连接取走(accpet),上层应用取走一个连接后,全连接队列就空出来一个位置。所以
全连接队列数*不等于*established数
连接关闭
上图介绍了在一个连接关闭时的全过程,图中已经画的非常详细了,这里就不做过多的介绍,关于TIME_WAIT的介绍在【Nginx】Nginx高并发业务场景下的Linux系统配置及Nginx配置 里面也有很详细的说明了。
仔细吃透TCP的连接建立和释放的过程,再看一看开篇的那张全状态集合图,基本就理解了TCP的各个状态及转换机制了。
TCP队列溢出及丢弃
回到我们分析问题的路上,既然TCP有两个队列,就存在溢出的可能,我们怎么确定是否队列有溢出呢?
执行下面的两个命令都可以查看TCP的连接队列是否有异常:
1 | $ netstat -s | egrep "listen|LISTEN" |
下图是我在一个发生过TCP连接队列溢出的机器上执行命令后的结果
理论上,ss
命令和netstat
都可以显示tcp连接的分析统计,但是笔者猜测,netstat看到的应该是从服务器启动到现在的所有记录,即其结果是不清零的,而ss -s
命令则是有一个时间统计区间的,所以上图中,只有netstat
命令显示了很多半连接队列溢出或全连接队列丢弃的统计,而ss -s
命令则只显示了当前的socket队列连接情况,因为上次调优后,没有出现tcp队列溢出的情况了,所以ss就显示正常的结果。
另外,当有大量连接时,ss
命令要比netstat
命令执行效率要高很多。例如当你想查看当前所有tcp连接状态时,下面的命令在当前有10k条连接时,要执行大概1秒才能统计出来,而ss
命令则是秒出的。
1 | $ netstat -n | awk '/^tcp/ {++state[$NF]} END {for(key in state) print key,"\t",state[key]}' |
既然TCP连接队列长度不够,我们就需要调整其长度。
- 半连接队列的最大值取决于:max(/proc/sys/net/ipv4/tcp_max_syn_backlog),默认为512;当启用syncookies时,没有逻辑最大长度,忽略tcp_max_syn_backlog设置,syncookies的设置可以防范SYN flood攻击。所以只要我们启用了syncookies,则队列长度即可忽略,而且现在基本上可以通过防火墙来防范syn flood攻击。
- 全连接队列的最大值取决于:min(backlog, /proc/sys/net/core/somaxconn),在linux内核2.2版本以后,backlog参数控制的accept queue的大小,backlog是在socket创建的时候传入的,属于listen函数里的参数;somaxconn是内核的参数,默认是128
所以,我们的重点就在于调整全连接队列长度,backlog是由应用程序在创建监听时传入的,nginx这个值默认是511,至于java和C程序都可以通过相关函数来设置backlog值;而somaxconn是通过net.core.somaxconn
参数来设置的,默认是128。
来看一下经过系统调优和nginx参数优化(调优过程参见http://lipeng1667.github.io/2019/08/05/high-concurrent-system-configration-with-nginx-and-rhel/)后的全连接队列数:
执行下列语句(ss 参数说明: -l 显示监听 -n忽略主机名 -t仅显示tcp -4 ipv4协议)
1 | $ ss -lnt -4 |
如上图显示,recQ就是当前TCP队列中的数量,当上层应用取TCP连接取的足够快时,队列都是空的; sendQ是队列总长度。 可以看到nginx监听的443端口上已经调优到了65535。
笔者之前一直纠结为何ss统计后的ESTABLISHED连接数量要远远大于监听时设置的128默认全连接队列长度,后来终于在这个StackOverFlow的回答中找到了答案。
nginx负载节点
但是经过上面的TCP队列调优后,发现情况并未得到任何改善,依旧是周期性稳定地出现no live upstreams while connecting to upstreams
error日志。
那我们就换一个思路,为何nginx会认为没有可用的负载节点呢?明明负载节点好好的呢? 于是笔者就详细检查了nginx的配置,终于找到了一条尝试的解决办法。
关于nginx的轮询机制这里也不做赘述,网上同样有很多”相似”的文章介绍。其中提到了,默认情况下,当一个节点出错时,nginx会将当前节点自动设置成down状态,及新的请求不会下发到当前节点,在一段时间后,重新上线该节点。
这样就和我们的现象很吻合了,当某个请求,导致了nginx的所有负载节点均返回了失败的结果,nginx将所有节点下线,所以新的请求就会没有负载可用,便返回错误;而过段时间后,节点尝试上线,业务恢复。
为了验证我们的想法,笔者通过在nginx的配置中,设置忽略负载节点错误max_fail=0
,业务运行正常,不在出现错误。详细的配置可以参见【Nginx】Nginx高并发业务场景下的Linux系统配置及Nginx配置最后的nginx配置部分。
nginx输入参数校验
至于为何请求会导致负载节点返回错误,到现在还是个迷,只是知道周期性的nginx无可用节点是因为一个客户在晚上做模拟攻击测试,输入的参数中有很多sql注入和脚本攻击等代码,但是我们的负载节点本身已经做了防范,如果直接访问负载节点是没有问题的,但是不知为何nginx任务负载节点返回了错误信息。。。
所以我们就不得不设置nginx直接拦截掉这些非法的输入参数请求,在nginx配置的server
部分
1 | # block invalid url |
以上是笔者针对我们的服务器遭受攻击时做部分拦截,网上有很多比较全的拦截语法,但是笔者不建议在nginx节点上开启过滤。因为这些过滤说白了就是字符串的匹配算法,而字符串的匹配是最慢的一种计算了,会浪费掉一些CPU资源。确实开启了字符串过滤后,nginx线程的CPU资源上升了大概20%个点。
其他相关点
在解决这个问题的过程中,也顺带了解了一些其他有用的知识,比如ESTABLISHED连接的时间状态,tcp keepalive的开启机制等等。
我们执行下面的命令:
1 | $ netstat -apno | grep ESTAB | head |
在最后一列,因为加入了-o
命令,所以会显示ESTABLISHED连接的检测时间。
- keepalive - when the keepalive timer is ON for the socket
- on - when the retransmission timer is ON for the socket
- off - none of the above is ON
我们可以通过这个值,看到每个连接检测的时间,如果想要使用keepalive,必须在客户端发起socket连接时,显示地声明keepalive才行。笔者的服务端看到的ESTABLISHED连接,基本都是off状态,即没有任何时间检测,一旦因为网络问题,比如防火墙直接掐掉了这些连接,这些连接就会变成不可用的假连接。。。
可以参见这个问题的讨论Too many established connections left open
写在最后,这次Trouble Shooting过程中,发现了很多blog的内容,都基本上一样,就比如那个tcp连接时序图,都打着同一个水印,而很多文字内容,都是一样的。很多内容都是许多年前的了,比如tcp_tw_recycle
这个参数,还有不少文章推荐打开,其实对现代服务器,节省的那点资源,基本上可以忽略不计。
所以,解决问题还要靠自己对很多基础知识的深入了解。笔者解决这个问题,前后研究TCP状态,研究Nginx配置,研究backlog参数等等,其实花费了很长的时间。只有如此,才能真正做到触类旁通,而不是停留在病急乱投医地状态。
另外,善用Google!