教你使用 Swoole-Tracker 秒级定位 PHP 卡死问题 -

 

PHPer 肯定收到过这样的投诉:小菊花一直在转!你们网站怎么这么卡!当我们线上业务遇到这种卡住 (阻塞) 的情况,大部分 PHPer 会两眼一抹黑,随后想起那句名言:性能瓶颈都在数据库然后把锅甩给 DBA,赶紧找找慢 sql,但这是非常错误的做法,因为有太多因素能导致业务卡住,下面列举几种常见的卡住问题。

1. 死循环

最常见的就是写出了死循环代码

<?php
while(1){
   //do something
    if($condition){
        //满足条件后退出循环
         break;
    }
}
<?php
while(1){
   //do something
    if($condition){
        //满足条件后退出循环
         break;
    }
}

上述代码通过$condition控制循环退出,如果程序验证不严格,某些情况$condition永远为真就会导致请求卡死。

2.sesstion_start 函数导致卡死

PHP 的 session 锁等待 (ps: 很多地方叫做 session 死锁,这不太符合死锁定义),这个相信大部分 PHPer 都遇到过,PHP 默认会把 session 信息存储在/tmp/sess_下面的 session 文件里面,调用session_start()函数的时候会调用flock系统调用给 session 文件加锁,如果前一个请求没有结束或者手动释放 session 就会导致后面的请求无法获得锁,卡死在session_start()这个地方。下面举个例子,比如这种代码:

setInterval(function () {
    $.post("/ajax/doSomething", {}, function (result) {//1s进行一次ajax
         });
}, 1000)//1000ms == 1s
setInterval(function () {
    $.post("/ajax/doSomething", {}, function (result) {//1s进行一次ajax
         });
}, 1000)//1000ms == 1s

前端 js 定时通过 ajax 请求一下后端 PHP 的接口 (/ajax/doSomething) 做一些比较耗时的事情,写代码的人可能想当然的认为第一次的请求即使没有处理完,也不会影响第二次的请求,因为有很多的 FPM 进程每次请求会分发到不通的进程,但殊不知第二次请求会卡死在session_start()

3.flock 函数导致卡死

最常见的场景就是写日志,在 PHP 代码中确保每次fwrite写的日志内容小于 8k 的情况下我们可以利用 append 原子追加方式写日志,但是如果保证不了小于 8k 我们就需要在每次写日志前给文件加文件锁来避免两次日志间产生穿插的情况,代码如下:

<?php
$fp = fopen("/home/guoxinhua/php.log", "a+");
if (flock($fp, LOCK_EX)) {  //给日志文件加锁
   //do something
    fwrite($fp, "the huge string\n");
    flock($fp, LOCK_UN);    // 释放锁定
}
<?php
$fp = fopen("/home/guoxinhua/php.log", "a+");
if (flock($fp, LOCK_EX)) {  //给日志文件加锁
   //do something
    fwrite($fp, "the huge string\n");
    flock($fp, LOCK_UN);    // 释放锁定
}

如果在 A 进程获得锁后由于某种问题阻塞了那么 B 进程就会卡死在第三行flock的位置,除非 A 进程被 kill 掉,系统会自动释放这个文件锁

注意还有很多其他类型的锁即使进程被 kill 也不会自动被释放。
这个 8k 是可以改的,和 glibc 中的 fwrite 很多细节也不一样. 你是否有这样的烦恼,想学习高级技术,缺乏好的高级学习资料,11 年架构师授课的 TP5、laravel、swoole、swoft、高并发、,官方群:677079770 ,大牛带你飞 ,PHP/web 从入门到架构 722584796

4. 网络客户端未设置超时时间

MySQL、CURL、Swoole\Client 等网络客户端未设置超时可能会导致进程阻塞。Swoole\Client 建立 TCP 连接的时候connect方法的最后一个参数是超时时间,-1即为永不超时,注意这里设置不是单指这次connect方法,而是后面所有的send,recv都永不超时,在同步阻塞的编程模式下,如果此时对端机器直接宕机等原因导致网络不通,那么本端业务的表现就是卡死状态,所有的send,recv方法都将被阻塞,代码如下:

<?php
$cli = new Swoole\Client(SWOOLE_SOCK_TCP);
if ($cli->connect('127.0.0.1', 9501,-1)) {
      $cli->send("data");
      $cli->recv();
} else {
      echo "connect failed.";
}
<?php
$cli = new Swoole\Client(SWOOLE_SOCK_TCP);
if ($cli->connect('127.0.0.1', 9501,-1)) {
      $cli->send("data");
      $cli->recv();
} else {
      echo "connect failed.";
}

5. Swoole 协程的 lock

在 Swoole 协程模式下,不正确的使用 lock 也会导致所有协程大面积卡死,如下代码,通过go方法创建 2 个协程 (不理解协程的同学可以理解为创建了 2 个线程),第一个协程 lock 获得锁后在co::sleep位置让出了 cpu 此时开始执行第二个协程,第二个协程会卡死在第 6 行获得锁的位置,同时第一个协程也永远无法恢复继续执行。

<?php
$lock = new Swoole\Lock();
$c = 2;//创建2个协程
while ($c--) {
    go(function () use ($lock) {//创建协程
        $lock->lock();//获得锁
        Co::sleep(1);//让出cpu
        $lock->unlock();//释放锁
    });
}
<?php
$lock = new Swoole\Lock();
$c = 2;//创建2个协程
while ($c--) {
    go(function () use ($lock) {//创建协程
        $lock->lock();//获得锁
        Co::sleep(1);//让出cpu
        $lock->unlock();//释放锁
    });
}

如何发现卡死

上述只是举了一些例子,真实业务中还有各种姿势的卡死,遇到这种问题有经验的 PHPer 会用strace -p命令查看当前 PHP 进程到底阻塞在哪个系统调用上面来定位问题,但这种方式有几个问题:

  • 定位问题不清晰
    比如死锁这种问题 strace 的时候只能看到类似futex(0x7f4c8d567128, FUTEX_WAIT, 2, NULL)这种信息,非常的不直观,很多人根本不知道哪些 PHP 代码会触发futex系统调用,还有前文提到session_start那个问题,很多人根本不知道这里会触发flock,也就说很难根据一个系统调用定位到具体问题。
  • 不知道 - p 哪一个进程
    我们线上环境通常会启动几十个甚至上百个 PHP 进程,在有些请求卡死,有些请求正常的情况下,你到底该strace -p哪个进程呢?貌似只能碰碰运气了。
  • 发现不了死循环的问题
    由于strace命令的原理是追踪所有的系统调用,如果是前文提到的第一种情况,也就是死循环的卡死,strace根本无法获得任何有用的信息。此时我们只能用gdb工具来获取当前死循环在哪里具体,具体做法如下:首先:gdb attach后面接个进程 id。
    然后:
    p (char *)executor_globals.current_execute_data.func.op_array.filename.val打印当前执行的 PHP 文件。
    p (char *)executor_globals.current_execute_data.func.op_array.function_name.val打印当前执行的函数名。
    p executor_globals.current_execute_data.opline.lineno打印当前执行的行数。
    进一步也可以获取调用堆栈这里就不展开了。
    但这明显太底层了,很多细节要注意,不精通 PHP 内核的人很难这样找问题(ps: 通过.gdbinit能稍微减少点难度,但是也有很多其他问题)。

使用 Swoole Tracker 发现卡死问题

针对上述问题,Swoole 官方出了一个解决方案 Swoole Tracker 的堆栈工具,同时支持 FPM 和 Swoole。
使用方法很简单:

  • 首先点击上面的连接注册个账户。
  • 然后装上swoole_tracker扩展。
  • 最后登陆后台,在调试器=>进程列表中点击堆栈按钮就能获得当前卡在哪了,如图:

结尾

除了上面的卡死问题,还有一种情况是调用变慢,比如原来一个系统调用 5ms,但是由于网络等等原因,这个调用 100ms 才返回,业务的表现是变慢了而不是卡死在那里,这种情况通过 tracker 的抓堆栈工具是无法定位问题的,因为卡住时间很短,很难抓到调用堆栈,此时需要 Swoole 工具链中的另外一个工具阻塞IO检测工具我们会在后面给大家介绍。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章