502bad gateway寻因——php-fpm关键参数:max_children和backlog

背景分析

在高并发或者压测的情况下,网站请求容易出现一种错误——502 Bad Gateway。
出现这个错误的原因有很多,但在高并发的情况下,主要原因就在于php-cgi的进程数不够用。
对于一般的站点,简化版的请求处理流程图如下——
file
假如流程1的请求数在一瞬间达到一个很高的数值(比如2000),由于nginx的异步非阻塞架构,能从容应对这么多请求同时到达,并将这些请求都转发到了fpm主进程。但fpm并不一定能同时处理这么多请求,假如fpm只配置了能并发处理1000个请求,那么就会导致接近1000个请求会被异常返回502 bad gateway。
此时,就需要调整fpm的两个配置参数——max_children和listen.backlog的值。

max_children——

由于我们开启的是static模式,所以这个参数表示的就是fpm子进程的数量。这个数量不是越多越好,空闲的子进程太多会增加进程管理的开销以及上下文切换的开销。网上推荐的子进程数量计算公式为:

n = M / (m * 1.2)

其中,M 是PHP 能利用的内存数量。m 是每个 PHP 子进程平均使用的内存数量。
一般来讲,我们的m在20Mb到30Mb之间,假设取最大值30Mb,M取8G,那么可以计算得到子进程数量n约等于228。由于我们是按m取较大值算的,所以一般我们可以设置n为256左右。也就是说,此时,fpm可以同时处理256个请求(这里的“同时”其实并不是真实的并发,受机器CPU核数的限制,其实真正的并发处理请求个数也只有最高也只能等于CPU核数,但忽略这个影响的话,可以认为机器的fpm可以并发处理256个请求)。

listen.backlog——

假如我们的max_children配置了256,那如果此时并发数达到300,会不会出现40多个请求返回502错误呢?实际上不会。
首先TCP 建立连接时要经过 3 次握手,在客户端向服务器发起连接时,对于服务器而言,一个完整的连接建立过程,服务器会经历 2 种 TCP 状态:SYN_ RECEIVED,ESTABELLISHED。对应也会维护两个队列:1. 一个存放 SYN 的队列(半连接队列);2. 一个存放已经完成连接的队列(全连接队列)。当一个连接的状态是 SYN RECEIVED 时,它会被放在 SYN 队列中。当它的状态变为 ESTABLISHED 时,它会被转移到另一个队列。所以后端的应用程序只从已完成的连接的队列中获取请求。如果一个服务器要处理大量网络连接,且并发性比较高,那么这两个队列长度就非常重要了。因为,即使服务器的硬件配置非常高,服务器端程序性能很好,但是这两个队列非常小,那么经常会出现客户端连接不上的现象,因为这两个队列一旦满了后,很容易丢包,或者连接被复位。所以,如果服务器并发访问量非常高,那么这两个队列的设置就非常重要了。
对于以上说到的例子,实际上这300个请求会先依次到达全连接队列,然后fpm的子进程再依次去消费这个队列中的请求。由于这个过程耗时很短,所以几乎可以认为是此时有256个请求被fpm子进程进行处理,然后剩余的40多个请求就还是在这个全连接队列中等待被取出消费。所以此时不会出现502的错误返回。
但是如果此时并发请求量不止300,已经超过了(全连接队列的长度+max_children)的总和,就会有一部分已连接的请求没办法被及时处理,此时就会出现502 bad gateway的错误。
所以高并发情况下fpm是否会返回502错误,除了max_children的大小限制,另外一个限制就是全连接队列的长度,而这个长度配置就是在fpm.conf里的listen.backlog这个值。这个值一般默认是-1,也就是没限制,但其实由于linux系统本身也有一个对应的全连接队列长度的限制,而且fpm配置的backlog大小不能大于系统配置的大小。所以假设系统配置的全连接队列大小为128时,当fpm配置里的值为-1或者是一个大于128的值时,此时backlog的最大值还是只有128。也就是说,如果我们要修改fpm全连接队列长度,首先要保证系统的全连接队列值足够大。目前还没发现这个值设置为多少是最合适的,但最好设置为大于1024的值。同时这个值也不是越大越好,因为设置大了之后,虽然可以一定程度上避免出现502错误,但由于nginx有响应超时时间的设置,如果fpm处理不过来,nginx那边等待超时,断开连接,就会报504 gateway timeout的错。

优化操作

一、max_children与backlog的合适大小

1、对于8G的机器,max_children取256应该是比较合适的,如果内存较大,这个值可以对应的增大一些。
2、至于backlog的值,一般来讲,大于1024即可。后续如果要改的话,可以把当前值加倍增大,保持数值是2的指数最好,因为系统最终也会将这个值转为2的指数倍。

二、如何改max_children大小

1、打开fpm配置文件
2、找到pm.max_children这个配置,将等于号后面的值改为需要的值之后,保存配置。
3、重启fpm,执行 ps -ef|grep fpm|grep -v master|grep -v grep |wc -l 可以查看当前fpm子进程的数量,确认是不是已经改过来了

三、如何改backlog大小

1、cat /proc/sys/net/core/somaxconn (先查看系统的连接队列长度限制,如果太小,需要先通过下面的步骤23修改系统的这个值,否则就跳过这两步)    
2、vim /etc/sysctl.conf(在文件中新增一行net.core.somaxconn=x,x为希望改到的队列长度值,如果里面已经有这一行配置,则直接改这行配置的值)
3、sysctl -p(重新加载系统配置)
4、打开fpm配置文件
5、找到listen.backlog这个配置,将等于号后面的值改为需要的值。
6、重启fpm。

四、避免被同一个ip频繁请求攻击导致无法正常访问的优化建议

当大量非法请求通过nginx涌入到fpm时,正常用户的访问请求就会因为无法被fpm消费导致返回502或者接口一直处于加载中。所以,为了避免这种情况,最好的方式就是在nginx层就直接拒绝掉这个非法ip的请求,不让其到达fpm,浪费正常的fpm子进程资源。
Nginx有两个配置,可以用于限制每一个IP地址在一段时间内的访问次数:limit_req_zone和limit_req。具体的配置方法如下:

1、在nginx.conf的http模块里,新增 limit_req_zone$binary_remote_addr zone=allips:10m rate=30r/s;
2、在nginx.conf的server模块里,新增 limit_req zone=allips burst=5;
3、nginx -s reload 平滑重启nginx。

这样,就实现了每个ip每秒最多能发起35个请求的限制,超过的请求,nginx会直接返回503Service Temporarily Unavailable(服务暂时不可用)错误。这样就既保证了fpm不会在短时间内接收到同一个ip的高并发请求,也可以保证正常ip的用户可以访问接口。

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 1周前 自动加精
讨论数量: 1

谢谢你的文章,茅塞顿开,解决了压测java connection reset问题

1周前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!