Workerman内存泄露典型表现为RES持续上涨、GC无效、触发max_connections拒绝新连接;根源在闭包引用、静态变量未清理、资源句柄未释放,且memory_get_usage无法反映底层扩展内存。

Workerman内存泄露的典型表现是什么?
Workerman常驻进程不重启,运行时间越长,ps aux里看到的 RES 内存持续上涨、GC回收无效、甚至触发 max_connections 拒绝新连接——这些不是“内存用得多”,而是泄露的信号。特别注意:PHP 7.4+ 的 gc_collect_cycles() 在 Workerman 的多进程+异步 I/O 下效果有限,不能靠它“清掉”泄露。
- 泄露往往藏在闭包引用、全局静态变量、未 unset 的资源句柄(如
$connection->onMessage里闭包持有了大对象) - Workerman 的
Worker::$pidMap或自定义static $cache = []若没做生命周期管理,极易累积 - 不要依赖
memory_get_usage(true)看峰值——它只反映当前脚本分配的堆内存,漏掉 PHP 底层扩展(如 libevent、sockets)持有的内存
php-memprof 怎么装和启用才不崩 Workerman?
php-memprof 是目前少数能抓到 PHP 层级真实分配栈的工具,但它默认不兼容常驻进程。直接在 start.php 开头加 memprof_enable() 会导致子进程反复启用/冲突,最终 core dump。
- 必须在 每个子进程启动后、业务逻辑开始前 单独启用:
if (function_exists('memprof_enable')) { memprof_enable(); } - 禁止在
onWorkerStart外启用(比如放在require后),否则主进程也会开 profiling,干扰 fork 行为 - 配合
memprof_dump_callgrind()使用时,务必写入可写的临时路径(如/tmp/memprof-'.getmypid().'.out),避免权限问题导致 dump 失败却无报错 - 不要长期开启:
memprof_disable()建议在检测窗口结束后立刻调用,否则 Profiling 开销会让 QPS 明显下降
怎么用 php-memprof 定位到具体哪行代码在 leak?
核心是「对比 + 栈追踪」:让 Worker 跑两轮相似请求(比如同个 WebSocket 连接发 10 次消息),分别 dump,再用 callgrind_annotate 分析增长最猛的 allocation site。
- 先在关键位置手动触发 dump:
// 比如 onMessage 回调末尾 if (isset($_GET['dump']) && $_GET['dump'] === '1') { memprof_dump_callgrind('/tmp/memprof-'.getmypid().'-after.out'); } - 用
callgrind_annotate --inclusive=yes /tmp/memprof-*.out查看每行分配的字节数,重点关注:-
include/require的文件反复出现(说明自动加载器或配置类被重复实例化) -
array_merge、json_decode出现在高分配栈顶(大概率是没释放的大数组或 JSON 解析结果被缓存) - 你的闭包函数名(如
{closure} in /path/to/Handler.php:42)——这基本就是泄露源头
-
- 注意:
callgrind_annotate输出中self列为 0 但incl很高,说明泄露发生在被调用的下层函数,需顺藤摸瓜
为什么 dump 出来没看到你的业务文件?常见断点在哪?
php-memprof 默认只 profile 当前执行栈,如果泄露发生在异步回调(如 Timer::add)、事件循环外的 pcntl_signal 处理、或 stream_select 后续回调里,而你只在 onMessage 里 dump,就捕不到。
立即学习“PHP免费学习笔记(深入)”;
- 泄露可能根本不在 HTTP/WebSocket 流程里:检查
onWorkerStart中是否初始化了单例、全局缓存、PDO 连接池(没设PDO::ATTR_PERSISTENT => false就会累积) -
memprof_enable()必须在所有可能触发泄露的代码执行前启用;Workerman 的onConnect、onClose、onError都要覆盖 - 如果用了 Swoole 兼容层或自定义 EventLoop,确认
php-memprof是否支持该扩展的内存分配钩子(目前仅稳定支持原生 stream/socket)
真正难的不是跑通 php-memprof,是得把 Workerman 的生命周期拆解清楚:哪个变量该在连接级销毁,哪个该在进程级复用,哪个其实根本不该存在。漏掉一次 unset($this->bigData),几小时后就多出 200MB。



















