Valgrind 检测协程内存泄漏的步骤
解读
国内一线互联网公司在面试 PHP 后端/全栈岗位时,常把「协程」与「内存泄漏」放在一起追问,目的是验证候选人是否真正在生产环境用 Swoole/协程写过高并发服务,而不仅停留在「听说」层面。
Valgrind 在 CLI 下默认只能感知「操作系统线程」的内存映射,而 Swoole 的协程本质是「用户态线程」,调度器在单个 OS 线程内反复切换栈空间,导致 Valgrind 的堆栈回溯、泄漏计数容易误报或漏报。因此,面试官想听的是:
- 你清楚 Valgrind 的局限性;
- 你有一套「让 Valgrind 看得见协程」的可落地方案;
- 你能把检测结果与 PHP 内部内存管理(ZendMM、emalloc)关联起来,快速定位到是哪段 PHP/C 扩展代码泄漏。
知识点
- Zend Memory Manager:PHP 默认启用 ZendMM,用自定义的 emalloc/efree 接管 glibc,导致 Valgrind 无法直接统计「PHP 层面」的泄漏。
- Swoole 协程栈:默认 8 KB 私有栈,由
swoole_coroutine在创建时 mmap 分配,销毁时 munmap;若 C 扩展在协程栈上分配堆块却忘记释放,Valgrind 会报「still reachable」或「definitely lost」。 - Valgrind 的线程模型:Valgrind 把每个「可调度实体」当线程,需要显式告诉它「协程切换」等价于「线程切换」,否则堆栈回溯会错位。
- debug 构建:必须关闭 ZendMM、开启 Swoole 的
--enable-debug、--enable-sanitizer选项,让内存直接走 glibc,才能被 Memcheck 捕捉。 - suppressions 文件:PHP/Swoole 自身会留下大量「一次型」分配,需要提前生成 suppressions,避免噪音淹没真正的业务泄漏。
答案
步骤以 CentOS 7 + PHP 8.2 + Swoole 5.1 为例,其他发行版同理:
-
编译「可被检测」的 PHP
./configure --disable-all --enable-cli --enable-debug \ --disable-zend-memory-manager-override \ --enable-mysqlnd --enable-pdo make -j$(nproc) && make install关键:
--disable-zend-memory-manager-override让 PHP 直接调用 glibc malloc/free,Valgrind 才能追踪。 -
编译「带调试符号」的 Swoole
phpize ./configure --enable-debug --enable-sanitizer --enable-openssl make clean && make -j$(nproc) && make install打开
--enable-debug后,Swoole 会定义SW_USE_VALGRIND宏,主动调用VALGRIND_MALLOCLIKE_BLOCK/VALGRIND_FREELIKE_BLOCK告知 Valgrind 协程栈的生命周期。 -
生成 PHP/Swoole 基准 suppressions
先跑一段「啥也不做」的脚本,收集系统自身泄漏:valgrind --leak-check=full --show-leak-kinds=all \ --gen-suppressions=all --log-file=php.supp \ php -r "echo 'hello';"手工编辑
php.supp,只保留「definitely lost」为 0 的区块,其余删除,得到valgrind-php.supp。 -
编写最小协程泄漏示例
<?php // leak.php Swoole\Runtime::enableCoroutine(); go(function () { $obj = new stdClass; $obj->buf = str_repeat('A', 1024*1024); // 1 MB // 故意不释放 $obj,让 Valgrind 捕捉 Co::sleep(0.001); });保存为
leak.php,确保脚本会正常退出,协程栈被销毁。 -
运行 Valgrind
USE_ZEND_ALLOC=0 valgrind --tool=memcheck \ --leak-check=full --show-leak-kinds=all \ --suppressions=valgrind-php.supp \ --log-file=leak.log \ php leak.php环境变量
USE_ZEND_ALLOC=0二次确保 ZendMM 被关闭。 -
解读报告
打开leak.log,搜索「definitely lost: 1,048,576 bytes in 1 blocks」,堆栈会指向zend_alloc、swoole_coroutine_create、php_execute_script,结合符号表可定位到leak.php第 6 行。 -
修复与复测
在协程末尾显式unset($obj)或缩短生命周期,再次执行步骤 5,确认「definitely lost」降为 0,即完成闭环。
拓展思考
- 线上无法重启:Valgrind 拖慢 20~50 倍,只能在灰度容器或压测克隆机执行;生产环境建议开启 Swoole 的
tracker模块,用co::stats()观察协程栈峰值,再抽样做离线 Valgrind。 - 与 AddressSanitizer 互补:ASan 编译开销 < 2 倍,适合 CI 阶段拦截;Valgrind 适合「偶发+难复现」的泄漏,二者结合可覆盖 95% 场景。
- 扩展开发注意:若在
coroutine_c层用sw_malloc分配,需要配对调用sw_free,并加VALGRIND_MALLOCLIKE_BLOCK宏,否则同样会被误报。