由于负载能力无法满足需求,纸飞机服务器从去年7月开始迁移到nginx服务器,不过PHP 5.2上安装了eAccelerator扩展后服务器经常发生500错误,无奈升级到了PHP 5.3系列,虽然说500错误没了,不过新的内建PHP-FPM稳定性不如打过FPM补丁PHP-CGI,经常发生所有PHP-FPM进程全部卡死不接受任何请求,前端的nginx不停地显示502 Bad Gateway,除非手动kill掉PHP进程再重新启动才能恢复正常,让我非常郁闷。 在网上逛了无数的论坛,在nginx英文论坛里面也转了很久,试过了文件描述符限制、转换到APC、修改各种FPM参数等等一系列方法依然没有效果,无意中Google到一篇文章讲利用cron计划任务每分钟用curl监控一下服务器是否有502错误,如果有的话就自动重启PHP进程,最后只能选择这种方法了。参考了网站上的PHP脚本,用Shell改写了一下(PHP挂了我再用PHP去干掉PHP不是自己找麻烦么,果断用Shell语言来执行),得到了以下一段脚本: #!/bin/sh if [ `curl --connect-timeout 5 -I http://my.nuaa.edu.cn/ 2>/dev/null | grep '502 Bad Gateway' -c` != '0' ] then killall -9 php-fpm 2>/dev/null service php-fpm start >/dev/null echo "[ `date +'%h %d %T'` ] PHP-FPM died with 502 bad gateway, all processes restarted">>/path/to/log fi 并且在/etc/crontab中加入一行: * * * * * root /path/to/script 使脚本在每分钟0秒时自动被执行。 因为PHP进程卡死的时候已经无法接受任何信号,即使使用service php-fpm stop命令也无法停止了,所以这里只能选择使用killall命令来结束PHP进程。为了方便诊断以及未来在稳定性上再作优化,在脚本中增加了日志记录的命令。 不过PHP进程卡死到nginx报502错误其实是有一个时间差的,因为PHP和nginx通过TCP或SOCKET进行连接,当PHP卡死不接受请求时,nginx并不会收到请求被拒绝的类似信息,而是把请求积压在SOCKET或TCP上,此时浏览器所得到的响应就是一个“正在等待响应”的提示,除非SOCKET报错或浏览器关闭否则等待永远都不会停止,curl也一样。当积压到一定值时(Linux下SOCKET backlog的值默认为1024),Linux系统会认为该SOCKET已失效,再试图发送请求会收到Resource temporarily unavailable,直到这时nginx才会报502错误。也就是说如果在网站流量比较小的时候从PHP卡死到nginx报502错误可能需要数分钟甚至更久,而在这段时间里上述脚本运行curl命令时curl将一直处于等待状态,而每分钟会有一个新的curl进程出现,这样当最终SOCKET上的请求达到backlog值时这一群curl将全部得到502错误返回值,并且同时kill掉所有的PHP进程并且启动PHP服务,也就是说,PHP服务被启动了很多遍。因为PHP-FPM配置文件中启动的工作进程数是根据内存实际情况确定的,如果说PHP服务被启动了很多遍的话就会直接导致内存的占用过多造成不良后果,因此为了避免这种情况我在该脚本第一行加入了: killall -9 curl 2>/dev/null 这样就避免了多个PHP服务被启动,几倍于正常值的PHP进程在运行的情况。不过在实际使用过程中又发现在PHP卡死到nginx报502错误之间的时间差有时可能需要数十分钟到半小时,大大超出了我们所能忍受的范围,想到Linux下很多服务通过在/var/lock/subsys/文件夹下创建以自己名字命名的空文件来表明自己是否处于运行状态,我决定在502错误的监控脚本中也加入锁机制,使得脚本能够识别出PHP卡死而nginx没有报502错误的情况并且及时地重启PHP进程,于是监控脚本变成了这个样子: #!/bin/sh if [ -e /var/lock/subsys/502 ] then killall -9 curl 2>/dev/null killall -9 php-fpm 2>/dev/null rm -f /var/lock/subsys/502 fi touch /var/lock/subsys/502 if [ `curl --connect-timeout 5 -I http://my.nuaa.edu.cn/ 2>/dev/null | grep '502 Bad Gateway' -c` != '0' ] then killall -9 php-fpm 2>/dev/null service php-fpm start >/dev/null echo "[ `date +'%h %d %T'` ] PHP-FPM died with 502 bad gateway, all processes restarted">>/path/to/log fi rm -f /var/lock/subsys/502 脚本每次运行curl前在/var/lock/subsys/下创建一个锁文件“502”,结束运行时删除锁文件。当每次运行时发现锁文件存在就意味着上一分钟的脚本仍未运行完毕,也就是说PHP卡死但nginx还未报502错误,此时依次结束掉curl和php-fpm进程并删除锁文件,随后在之后的命令中重新启动PHP服务。这里curl和php-fpm进程的结束顺序不能颠倒,因为上一个脚本仍在运行,如果先结束php-fpm进程会导致curl收到502错误并且重新启动PHP,和当前脚本之后的启动PHP操作很可能会同时发生,导致PHP服务被多次启动的情况。 不过仔细思考了一下就发现,如果说上一个脚本在curl被结束掉之后运行速度太慢,直到当前脚本的创建锁操作完成之后才运行到删除锁操作一行,而当前脚本创建锁之后又一次在curl命令上发生卡死,那么就会导致当前脚本实际上处于运行状态但锁文件已经不存在,也就是说锁已经无法发挥作用,下一个脚本又会重新创建锁并且启动curl探测服务器状态,如果此时发生502错误又会导致PHP服务被多次启动。再三考虑后决定如果当前脚本运行时发现上一脚本运行的锁依然存在,那么就不再尝试创建锁也不删除锁,而是结束掉curl命令使得上一脚本继续运行到删除锁,而当前脚本在不创建锁的情况下重启PHP服务并记录错误信息后直接退出,在未来直到上一脚本的锁被删除才创建锁,使得锁能够正确指示脚本的执行情况。于是我又得到了以下脚本: #!/bin/sh if [ -e /var/lock/subsys/502 ] then killall -9 curl 2>/dev/null killall -9 php-fpm 2>/dev/null service php-fpm start >/dev/null echo "[ `date +'%h %d %T'` ] PHP-FPM died with no response, all processes restarted">>/path/to/log else touch /var/lock/subsys/502 if [ `curl --connect-timeout 5 -I http://my.nuaa.edu.cn/ 2>/dev/null | grep '502 Bad Gateway' -c` != '0' ] then killall -9 php-fpm 2>/dev/null service php-fpm start >/dev/null echo "[ `date +'%h %d %T'` ] PHP-FPM died with 502 bad gateway, all processes restarted">>/path/to/log fi rm -f /var/lock/subsys/502 fi 这样一来就可以保证由于PHP进程卡死造成服务器不能正常工作的时间总小于1分钟了。 (责任编辑:IT) |