如何开启协程日志并定位死锁?
解读
国内高并发业务(电商秒杀、直播弹幕、金融支付)普遍把 Swoole 作为 PHP 的协程运行时。面试官问“开启协程日志并定位死锁”,本质想确认三件事:
- 你是否知道 PHP 的“协程”由 Swoole 提供,而非语言原生;
- 能否通过日志开关把调度器、通道、锁事件全部打出来;
- 拿到日志后,能否用“等图”或“资源占用链”快速判定哪两个(或以上)协程互相等待。
答不出日志开关=不会排错;不会画等图=只能重启,业务有损。
知识点
- 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'])。
- 死锁四要素:互斥、占有且等待、非抢占、循环等待;在协程里表现为 Channel 满/空或 Mutex 递归加锁。
- 定位工具:
swoole_coroutine_list()列出所有协程 ID;swoole_coroutine_getbacktrace($cid)拿到单协程 PHP 栈;- Linux
perf或off-cpu火焰图看调度器是否长时间 idle; - 手工构造“等图”:节点=协程,边=等待资源,出现环即死锁。
- 预防手段:统一锁序、超时机制
channel->push($data, 0.01)、co::sleep(0)主动让出、使用SWOOLE_CHANNEL_CLOSED做快速失败。
答案
线上开启步骤(以 Swoole ≥ 4.8 为例):
- 修改 php.ini 或启动脚本:
或在 PHP 代码:swoole.enable_coroutine_log=On swoole.log_level=DEBUG swoole.trace_flags=COROUTINE | CHANNEL | MUTEXswoole_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' ]); - 重启 PHP-FPM / 重新加载 Worker,让日志落盘。
- 复现场景(压测或灰度流量),收集 1~2 分钟日志。
- 日志关键字:
CORO#1256 waiting channel<0x7f3a8c014820> capacity=0CORO#892 hold mutex<0x563b2a0143c0>
把“等待”与“持有”关系写成有向边,出现闭环即死锁。
- 用
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'); } - 修复后关闭 DEBUG 日志,只保留 WARNING 以上级别,防止磁盘打满。
拓展思考
- 如果业务已上线且不能重启,如何热开启日志?
答:Swoole 4.6+ 支持swoole_coroutine_set运行时生效,可在 Worker 启动回调里动态打开,无需重启主进程。 - 日志量过大,怎样采样?
答:利用trace_flags的位运算,只打开SWOOLE_TRACE_CHANNEL;或在log_rotation中按 1/1000 采样协程 ID,其余直接 return。 - 除了 Channel/Mutex,Swoole Table 和 Redis 连接池也会死锁,如何统一监控?
答:封装一层Co\Lock和Co\Pool,在内部埋点 Prometheus:加锁次数、等待耗时、是否超时;Grafana 看板出现“等待 P99 突刺”即可提前预警,比事后抓日志更主动。