如何开启协程日志并定位死锁?

解读

国内高并发业务(电商秒杀、直播弹幕、金融支付)普遍把 Swoole 作为 PHP 的协程运行时。面试官问“开启协程日志并定位死锁”,本质想确认三件事:

  1. 你是否知道 PHP 的“协程”由 Swoole 提供,而非语言原生;
  2. 能否通过日志开关把调度器、通道、锁事件全部打出来;
  3. 拿到日志后,能否用“等图”或“资源占用链”快速判定哪两个(或以上)协程互相等待。
    答不出日志开关=不会排错;不会画等图=只能重启,业务有损。

知识点

  1. Swoole 协程调度器日志:
    • swoole.enable_coroutine_log=On(php.ini)或运行时 swoole_coroutine_set(['log_level' => SWOOLE_LOG_DEBUG, 'trace_flags' => SWOOLE_TRACE_COROUTINE])
    • 日志文件默认在 {glibc_error_log}/swoole.log,也可重定向 swoole_coroutine_set(['log_file' => '/data/log/swoole_coroutine.log'])
  2. 死锁四要素:互斥、占有且等待、非抢占、循环等待;在协程里表现为 Channel 满/空或 Mutex 递归加锁。
  3. 定位工具:
    • swoole_coroutine_list() 列出所有协程 ID;
    • swoole_coroutine_getbacktrace($cid) 拿到单协程 PHP 栈;
    • Linux perfoff-cpu 火焰图看调度器是否长时间 idle;
    • 手工构造“等图”:节点=协程,边=等待资源,出现环即死锁。
  4. 预防手段:统一锁序、超时机制 channel->push($data, 0.01)co::sleep(0) 主动让出、使用 SWOOLE_CHANNEL_CLOSED 做快速失败。

答案

线上开启步骤(以 Swoole ≥ 4.8 为例):

  1. 修改 php.ini 或启动脚本:
    swoole.enable_coroutine_log=On
    swoole.log_level=DEBUG
    swoole.trace_flags=COROUTINE | CHANNEL | MUTEX
    
    或在 PHP 代码:
    swoole_coroutine_set([
        'log_level' => SWOOLE_LOG_DEBUG,
        'trace_flags' => SWOOLE_TRACE_COROUTINE | SWOOLE_TRACE_CHANNEL | SWOOLE_TRACE_MUTEX,
        'log_file' => '/data/log/swoole_coroutine.log'
    ]);
    
  2. 重启 PHP-FPM / 重新加载 Worker,让日志落盘。
  3. 复现场景(压测或灰度流量),收集 1~2 分钟日志。
  4. 日志关键字:
    • CORO#1256 waiting channel<0x7f3a8c014820> capacity=0
    • CORO#892 hold mutex<0x563b2a0143c0>
      把“等待”与“持有”关系写成有向边,出现闭环即死锁。
  5. swoole_coroutine_getbacktrace($dead_cid) 拿到 PHP 层栈,定位业务代码行,重构锁顺序或加入超时:
    $chan = new Swoole\Coroutine\Channel(1);
    if (!$chan->push($data, 0.1)) {   // 100ms 超时
        throw new \RuntimeException('push timeout, possible deadlock');
    }
    
  6. 修复后关闭 DEBUG 日志,只保留 WARNING 以上级别,防止磁盘打满。

拓展思考

  1. 如果业务已上线且不能重启,如何热开启日志?
    答:Swoole 4.6+ 支持 swoole_coroutine_set 运行时生效,可在 Worker 启动回调里动态打开,无需重启主进程。
  2. 日志量过大,怎样采样?
    答:利用 trace_flags 的位运算,只打开 SWOOLE_TRACE_CHANNEL;或在 log_rotation 中按 1/1000 采样协程 ID,其余直接 return。
  3. 除了 Channel/Mutex,Swoole Table 和 Redis 连接池也会死锁,如何统一监控?
    答:封装一层 Co\LockCo\Pool,在内部埋点 Prometheus:加锁次数、等待耗时、是否超时;Grafana 看板出现“等待 P99 突刺”即可提前预警,比事后抓日志更主动。