php-fpm重启是一个所有人都知道而所有人都往往忽视的问题,但是当php-fpm重启引发线上事故的时候,就是对稳定性的重大挑战了
1. 背景
业务压力在预期内,但是却会在某个时间点出现cpuidle骤降的情况,轻则服务稳定性受损;重则idle归零,警报四起,一阵鸡飞狗跳。
而往往整个集群全部机器idle突然断崖式下跌,对于监控、排查工作会产生一定的干扰;而对于业务增速快、业务逻辑迭代多、流量增长预期不明的业务团队来说,往往是一脸懵逼去查日志,但是结论总是似是而非。
这就是由一个已知的php-fpm重启导致的问题表现。
2. 解析
2.1 php-fpm 为什么要重启?
首先,fpm的全称是fascgi process manager,也就是fastcgi的进程管理器。nginx通过反向代理将请求发给php-fpm,fpm再分发给自己的worker子进程。内存泄露这个问题一直以来就是困扰各种语言的问题,对于php来说,第三方扩展的不稳定性,在fpm这样常驻内存的sapi模式,想要做到完美的内存回收,不得不说是一个很大的挑战,所以为了多快好省的解决内存问题,重启就成了一个最优解。
2.2 重启与cpuidle的关系
重启本质上是关闭当前的子进程,然后master再fork出新的子进程的过程,对于cpu就会有一定的消耗,这个消耗量和worker的数量正相关。
; 管理方式,静态,始终保持一固定量的子进程
pm = static
; 最大子进程数,64,
pm.max_children = 64
; 子进程处理完多少请求后自动重启,10w
pm.max_requests = 1000000
子进程数越大,重启对cpu消耗越多。低峰期的时候重启,一般表现还好;但是当本身服务有一定负载的时候,突然全集群的php-fpm都开始重启,就会导本身就有些吃紧的cpu更加紧张;而高负载下,就有可能直接idle归零。
操作系统是比较神奇的,当cpidle不足的时候,而压力不减,cpu就会由工作,变成了抢工作,也就是一般所说的“竞态”,这时候往往会使情况恶化。
2.3 集群整体cpuidl骤降
由于一般情况下存在负载均衡,所以请求流量会被均匀的分配到集群内的每一台机器上,考虑到运维简易性,和目前云服务的便利性,我们没有必搞不同配置的服务器组建成一个集群,然后通过权重做流量划分这样自寻烦恼的方案。
同时,由于完全相同的机器,完全同构的配置,在流量均分的情况下,也就势必导致全集群的机器会在同一时间点、同时达到pm.max_requests,结果就是整个集群的php-fpm同时重启了……
3. 方案
核心问题出在pm.max_requests,那我们就可以从这个点上入手想办法解决
3.1 php-fpm.conf异构化
土一点的办法就是手动异构,当前是10w,那么每台机器都上下加减个一万,形成一个阶梯,这样就可以有效的防止同一时间点重启.
computer1:pm.max_requests = 80000
computer2:pm.max_requests = 90000
computer3:pm.max_requests = 100000
computer4:pm.max_requests = 1100000
computer5:pm.max_requests = 1200000
当然,三五台机器还好,如果一个集群几十上百台机器,或者多个而集群,那手动异构配置就是一场运维人员的噩梦了;不过我们可以通过shell脚本自动批量异构的方法来做;而且这种脚本可以随时跑,生效一般也只会在php-fpm重启的时候;扩容的机器只要执行下单机脚本即可。
再专业一点的,我们可以考虑通过系统管理集群、机器及异构的pm.max_requests。
3.2 php-fpm源码改造
改配置的方法毕竟不够极客,改源码才够范 -。-
//php-src/sapi/fpm/fpm/fpm.c:123行
*max_requests = fpm_globals.max_requests;//这里就是从php-fpm.conf中获取到的pm.max_requests进行赋值操作
//改造为如下
php_mt_srand(GENERATE_SEED());
*max_requests = fpm_globals.max_requests + php_mt_rand()&32767;
//为什么使用32767?它的二进制是:111111111111111
看似很高端,但是这个改动会导致配置参数不够准确,在需要精准控制次数的时候,是不可控的;不可控在有些时候才是最大的问题。所以仅仅作为一种解决方案,但是并不推荐使用。