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

引子:

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

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

发现问题

近期发现线上很多机器的磁盘空间报警, 且日志文件已经清理,但是磁盘空间没有释放。通过ps aux | grep php-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过程中调用glibc 中time 函数的位置,不是php模块造成的。而glibc 中的time相关函数是线程安全的,不会产生死锁。

那是什么导致的死锁呢?

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

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

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

PHP-CGI的执行流程

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

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

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

“Max execution timeout of 60 seconds exceeded”.

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

void zend_set_timeout(long seconds)

{

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会调用用户自定义的php脚本中实现的shutdown函数。如果CGI执行超市,那么定时器会产生SIGPROF信号使执行流程中断。如果此时脚本刚好处于调用时间函数的状态,且还没有释放锁资源。然后执行流程进入了 timeout 函数,继续跳转到zend_request_shutdown。此时如果自定义的shutdown函数中访问了时间函数。就会产生死锁。我们从代码中找到了证据:

register_shutdown_function ('SimpleWebSvc:: shutdown’);

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

结论

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

解决办法

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

PHP 死锁问题分析的更多相关文章

  1. MySQL 死锁问题分析

    转载: MySQL 死锁问题分析 线上某服务时不时报出如下异常(大约一天二十多次):"Deadlock found when trying to get lock;". Oh, M ...

  2. MySQL死锁案例分析与解决方案

    MySQL死锁案例分析与解决方案 现象: 数据库查询: SQL语句分析:  mysql. 并发delete同一行记录,偶发死锁.   delete from x_table where id=?   ...

  3. 死锁问题分析(个人认为重点讲到了gap间隙锁,解决了我一些不明报死锁的问题)

    线上某服务时不时报出如下异常(大约一天二十多次):“Deadlock found when trying to get lock;”. Oh, My God! 是死锁问题.尽管报错不多,对性能目前看来 ...

  4. MySQL死锁问题分析及解决方法实例详解(转)

      出处:http://www.jb51.net/article/51508.htm MySQL死锁问题是很多程序员在项目开发中常遇到的问题,现就MySQL死锁及解决方法详解如下: 1.MySQL常用 ...

  5. 【MySQL】死锁问题分析

    1.MySQL常用存储引擎的锁机制: MyISAM和MEMORY采用表级锁(table-level locking)   BDB采用页面锁(page-level locking)或表级锁,默认为页面锁 ...

  6. Mysql update后insert造成死锁原因分析及解决

    系统中出现死锁的日志如下: ) TRANSACTION: , ACTIVE sec inserting mysql tables , locked LOCK WAIT lock struct(s), ...

  7. Linux内核调试方法总结之死锁问题分析

    死锁问题分析 死锁就是多个进程(线程)因为等待别的进程已占有的自己所需要的资源而陷入阻塞的一种状态,死锁状态一旦形成,进程本身是解决不了的,需要外在的推动,才能解决,最重要的是死锁不仅仅影响进程业务, ...

  8. [转]MySQL批量更新死锁案例分析

    文章出处:http://blog.csdn.net/aesop_wubo/article/details/8286215 问题描述 在做项目的过程中,由于写SQL太过随意,一不小心就抛了一个死锁异常, ...

  9. mysql死锁问题分析

    线上某服务时不时报出如下异常(大约一天二十多次):“Deadlock found when trying to get lock;”. Oh, My God! 是死锁问题.尽管报错不多,对性能目前看来 ...

  10. SQL Server死锁的分析、处理与预防

    1.基本原理 所谓“死锁”,在操作系统的定义是:在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态. 定义比较抽象,下图可以帮助你比较直观的 ...

随机推荐

  1. Browsersync 省时浏览器同步测试工具,浏览器自动刷新,多终端同步

    官网地址 http://www.browsersync.cn/ 1.安装 BrowserSync npm install -g browser-sync 2.启动 BrowserSync // --f ...

  2. shell脚本awk的基本用法

    AWK 1 AWK 2 3 linux取IP地址 4 5 ifconfig | grep -w inet | sed -n '1p' | awk '{print $2}' 6 7 eg: 8 9 aw ...

  3. 目标检测的mAp

    众多目标检测的知识中,都提到了mAp一值,那么这个东西到底是什么呢: 我们在评价一个目标检测算法的"好坏"程度的时候,往往采用的是pascal voc 2012的评价标准mAP.目 ...

  4. AJax的三种响应

    AJax的响应 1.普通文本方式(字符串) resp.getWriter().print("你好"); 2.JSON格式当要给前台页面传输 集合或者对象时 使用普通文本传输的时St ...

  5. springcloud(二)

    springcloud路由网关 一.什么是网关 Zuul的主要功能是路由转发和过滤器.路由功能是微服务的一部分,比如/api/user转发到到user服务,/api/shop转发到到shop服务.zu ...

  6. pkgconfig

    # tree hiredis/ hiredis/└── usr └── local ├── include │   └── hiredis │   ├── adapters │   │   ├── a ...

  7. learning java FileVisitor 遍丽文件及路径

    import java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttribut ...

  8. C++中继承 声明基类析构函数为虚函数作用,单继承和多继承关系的内存分布

    1,基类析构函数不为虚函数 #include "pch.h" #include <iostream> class CBase { public: CBase() { m ...

  9. 计蒜之道 百度AI小课堂-上升子序列

    计蒜之道 百度AI小课堂-上升子序列 题目描述 给一个长度为 \(n\) 的数组 \(a\) .试将其划分为两个严格上升子序列,并使其长度差最小. 输入格式 输入包含多组数据. 数据的第一行为一个正整 ...

  10. 【loj3123】【CTS2019】重复

    题目 给出一个长度为\(n\)的串\(s\),询问有多少个长度为\(m\)的串\(t\) 满足 \(t\) 的无限循环串存在一个长度为\(n\)且比\(s\)字典序严格小的子串 $ n , m \le ...