真泄漏需rss线性上涨且memory_get_peak_usage同步攀升;用strace查epoll_pwait、gc_collect_cycles打点比对、xdebug_debug_zval验refcount;静态变量须用id作键并onclose unset;闭包禁use($this),改用数组回调或静态方法;redis等sdk须显式close再unset;php 8.0+强制用weakmap替代static数组。

Workerman内存占用过高会导致进程被系统OOM Killer强制终止、连接拒绝、响应延迟飙升,必须在RSS持续上涨时立即介入排查,不能等服务崩溃才行动。
确认是否真泄漏而非正常增长
执行ps aux --sort=-%mem | grep php,挑出运行超5分钟的Worker进程,记录其RSS值(单位KB);连续观测10分钟,若RSS呈线性上升趋势(如每分钟涨2MB以上),且memory_get_peak_usage(true)打点也同步攀升,则确认为泄漏;若仅启动初期快速上涨后趋于平稳,大概率是opcache或连接缓冲区预分配所致,非泄漏。
注意:【不要依赖memory_get_usage()单次返回值】,它只反映脚本堆内存,完全无法体现闭包捕获、静态变量、底层扩展持有的“僵尸对象”。
定位泄漏源头的三步法
第一步:用strace -tt -p PID观察进程行为。若每秒刷出数十行epoll_pwait且返回值为0或毫秒级超时,说明事件循环空转——这往往不是业务代码问题,而是内核层面连接关闭风暴或全连接队列溢出引发的sys CPU高,需同步检查net.core.somaxconn和backlog配置。
第二步:在onMessage、onClose、定时器回调入口处插入gc_collect_cycles()并记录memory_get_peak_usage(true),对比两次调用差值。若某次回调后峰值猛增5MB以上,该回调即为高危区。
第三步:对可疑对象执行xdebug_debug_zval($obj)(需启用Xdebug),重点看refcount是否异常高(如≥3)、is_ref是否为1。若一个TcpConnection对象refcount卡在4且无法下降,基本可断定它被闭包→类实例→静态数组三级强引用锁死。
检查高频泄漏点
方法一:静态变量清理缺失
检查所有self::$connections、static $cache = []类定义,在onClose中是否执行unset(self::$connections[$connection->id]);【键必须是标量ID,绝不可用$connection对象本身作键】,否则unset()无效。
方法二:闭包隐式捕获
将Timer::add(1, function() use ($this) { ... })全部替换为Timer::add(1, [$this, 'tick'])或独立静态函数;【Timer::add传数组回调比匿名函数更危险——它给实例加了一条强引用链】。
方法三:第三方SDK未释放
Redis客户端必须在onClose里先调$this->redis->isConnected() && $this->redis->close()再unset($this->redis);EasyWeChat、阿里云SDK等务必调用destroy()或close(),不能只unset。
启用php-memprof精准抓栈
在Worker::onWorkerStart回调内(仅子进程)、业务逻辑开始前插入:
if (function_exists('memprof_enable')) { memprof_enable(); }
让同一连接重复触发10次相同操作,随后执行memprof_dump_callgrind('/tmp/memprof-'.getmypid().'.out');用callgrind_annotate分析输出文件,聚焦allocations列增长最猛的函数调用栈——那里就是泄漏源头。
注意:【禁止在start.php全局启用memprof_enable()】,否则主进程也会开启profiling,fork后子进程冲突导致core dump。
PHP 8.0+ 必做重构
将所有static $cache = []替换为private WeakMap $cache;并在构造函数初始化:$this->cache = new WeakMap();;后续存取改用$this->cache[$connection] = $data;。连接断开后,WeakMap自动解绑,无需手动unset。
这一步做完,90%由静态数组+循环引用导致的泄漏直接消失。











