配色: 字号:
PHP 死锁问题分析
2016-08-29 | 阅:  转:  |  分享 
  
PHP死锁问题分析

对于死锁的问题,人们往往想到出现一些关于访问很缓慢,有白页现象,要是测试环境(我就真实遇到测试环境有本文谈及一样的问题)你也就重启一下PHP的php-fpm进程发现又好了

背景:对于死锁的问题,人们往往想到出现一些关于访问很缓慢,有白页现象,要是测试环境(我就真实遇到测试环境有本文谈及一样的问题)你也就重启一下PHP的php-fpm进程发现又好了,隔一段时间又出类似的问题,你会看下日志,你会发现有很多日志是“Maxexecutiontimeoutof60secondsexceeded”,你会发现这可能是一些php的守护进程导致的,你为了解决测试环境的问题,于是觉得应该把那个php-fpm的进程数开多点,可能会好一些,于是你开多了,一直没有面对这个问题的原因,为什么呢,因为公司装PHP的是运维装的,你没有办法或时间去装一个debug版本的php,你说这个问题让运维的人来查,你觉得能查出来?So,这个问题一拖再拖,但就是没解决,但是有一天你发现磁盘满了,用du去看整体时发现满了,但是如果一个个目录去看发现并没有占用多少,也万万没有想到PHP的死锁还会导致磁盘空间占用太多,上面这种情况我就真实遇到过,后来重新reboot操作系统,磁盘又回来了,所以,我认为是一篇好文章,所以转了此文,也想说明对于PHP的扩展这方面代码质量把关需要严格,再就是PHP本身关于锁这块要弱化(除开cookie/session和cache锁外,其它能不用就不用),尽可能少用锁,这是博主一点小看法,下面言归正传。



引子:



本期我们邀请到了云盘服务端团队的技术达人-徐铁成,一个隐蔽已久的PHP死锁问题被层层掘出,感谢铁成为我们带来这次畅快的体验,小伙伴们,准备好这次技术之旅了么?



---------------



发现问题



近期发现线上很多机器的磁盘空间报警,且日志文件已经清理,但是磁盘空间没有释放。通过psaux|grepphp-cgi发现,很多进程的启动时间在几天到几周甚至几个月之前。我们线上的php-cgi都有最大执行次数的。一般在1天内都会重启一次。初步结论,这些cgi进程有问题。



通过lsof-p[pid]发现,启动时间很久的cgi进程中打开了一些日志文件句柄,并且没有关闭。这些日志文件在文件系统中已经删除了。但是句柄没关闭,导致磁盘空间没有释放。到此,磁盘空间异常的问题基本确定。是由于cgi没有关闭文件句柄造成的。



进一步分析进程,strace-p[pid],发现所有异常的进程都阻塞与fmutex状态。换句话所,异常的cgi进程死锁了。进程死锁导致打开的文件句柄没有关闭,所以导致磁盘空间异常。



为什么cgi进程会死锁呢?



什么是死锁



学过操作系统的通同学,都了解多线程的概念。在多线程中访问公共资源,需要对资源加锁。访问结束后,释放锁。如果没有释放锁,那么下一个线程来获取资源的时候就会永远都无法获取资源的锁,于是这个线程死锁了。那么CGI是多线程的公共资源访问导致的死锁吗?答案是NO。



1.CGI是单线程进程,通过ps就能看到。(进程状态Sl的才是多线程进程)。



2.即使是多线程的,死锁发生在PHP的shutdown过程中调用www.zycaihui.comglibc中time函数的位置,不是php模块造成的。而glibc中的time相关函数是线程安全的,不会产生死锁。



那是什么导致的死锁呢?



通过分析linux中死锁产生的机制,发现除了多线程会产生死锁外,信号处理函数同样会产生死锁。那么cgi是由于信号处理导致的死锁吗?在这之前介绍一个感念。



函数的可重入性与信号安全



函数可重入是指,无论第几次进入该函数,函数都能正常执行并返回结果。那么线程安全函数是可重入的吗?答案是NO。线程安全函数,在第一次访问公共资源时,会获取全局锁。如果函数没有执行完成,锁还没释放,此时进程被中断。那么在中断处理函数中,再次访问该函数,就会产生死锁。那么什么样的函数才可以在中断处理函数中访问呢?除了没有使用全局锁的函数,还有一些signalsafe的系统调用可以使用。调用任何其他的非signalsafe的函数都会产生不可预知的后果(比如死锁)。详见mansignal。在分析死锁的原因前,我们先看看cgi执行的流程,分析其中有没有产生死锁的可能。



PHP-CGI的执行流程



Glibc中的时间函数使用到了全局锁,保证函数的线程安全,但没有保证信号安全(signalsafe)。经过之前的分析,我们初步怀疑死锁是由于PHP-CGI进程接收到了一个信号,然后在signalhandle中执行了非signalsafe的函数。主流程在中断前,正在执行glibc中的时间函数。在函数获取的锁没释放前,进入中断流程。而中断过程中又访问了glibc中的时间函数。于是导致了死锁。



PHP-CGI的执行流程,如下图所示:







进一步分析发现,所有死锁的cgi进程的sapi_global中都记录了一个错误信息



“Maxexecutiontimeoutof60secondsexceeded”.



60s是我们php-cgi中设置执行超时。所以我们确认了,cig在执行过程中的确产生了超时异常,然后由于longjmp进入了shutdown过程。在shutdown过程中访问了glibc中的时间函数。导致了死锁。



voidzend_set_timeout(longseconds)



{



TSRMLS_FETCH();



EG(timeout_seconds)=seconds;



if(!seconds){



return;



}



……



setitimer(ITIMER_PROF,&t_r,NULL);



signal(SIGPROF,zend_timeout);//此处会调用zend异常处理函数



sigemptyset(&sigset);



sigaddset(&sigset,SIGPROF);



……



}



通过gdb调试发现,所有PHP-CGI都阻塞在zend_request_shutdown中。zend_request_shutdown会调用用户自定义的phpwww.edu800.cn脚本中实现的shutdown函数。如果CGI执行超市,那么定时器会产生SIGPROF信号使执行流程中断。如果此时脚本刚好处于调用时间函数的状态,且还没有释放锁资源。然后执行流程进入了timeout函数,继续跳转到zend_request_shutdown。此时如果自定义的shutdown函数中访问了时间函数。就会产生死锁。我们从代码中找到了证据:





register_shutdown_function(''SimpleWebSvc::shutdown’);



我们在php代码中使用qalarm系统,qalarm系统会在cgi执行结束(shutdown)的时候,注入一个钩子函数,来分析cgi执行是否正常,如果不正常,则发送报警信息。而刚好qalarm的报警处理函数中访问了时间函数。于是就有一定的概率产生死锁。



结论



通过上面的分析,我们找到了cgi死锁产生的原因,是应为在signalhandler中使用了非signalsafe的函数,导致了死锁。



解决办法



去掉或简化qalarm注册到shutdown中的钩子函数。避免不安全的函数调用。



献花(0)
+1
(本文系换换爱66首藏)