在 workerman 中 onmessage 里频繁创建 timer 会导致内存泄漏甚至 oom,因闭包捕获 $this 或 $connection 形成强引用链;应改用单一定时器+连接状态管理,或确保 timer 创建后配对删除并避免大对象捕获。

在 Workerman 中,于 onMessage 回调里频繁创建 Timer(如 Timer::add()),是内存持续上涨甚至 OOM 的高发场景。根本原因不是“定时器本身很重”,而是每个定时器都持有一个闭包回调,而该闭包若捕获了 $this、$connection 或大对象,就会形成强引用链,导致关联对象无法被 GC 回收——尤其在长连接下,这种泄漏会线性累积。
为什么 onMessage 里 new Timer 很危险?
常见错误写法:
-
Timer::add(5, [$this, 'handleTimeout'])→ 给当前类实例加一条强引用,即使连接已断,只要定时器没删,实例就存活 -
Timer::add(1, function() use ($connection) { $connection->send(...); })→ 闭包持有$connection,而$connection又可能反向持有$this或静态缓存,构成循环引用 - 未配对调用
Timer::del($timerId)→ 定时器永远运行,回调持续注册,PHP 对象越积越多
安全替代方案:用状态标记 + 单一定时器
不为每次消息启新定时器,改用「一个常驻定时器 + 连接级状态管理」:
- 在
onConnect中为连接设置属性:$connection->pending_action = null; $connection->action_deadline = 0; - 全局只启一个低频定时器(如每 500ms 扫描一次):
Worker::addTimer(0.5, [$this, 'checkPendingActions']); -
checkPendingActions()遍历$worker->connections,对超时的连接执行清理或超时逻辑,然后unset($connection->pending_action) - 所有业务触发时只更新连接状态,不新建定时器
必须做的清理动作
若确实需动态创建定时器(如延迟推送),务必保证生命周期闭环:
- 保存返回的
$timerId到连接上下文:$connection->timer_id = Timer::add(...); - 在
onClose或业务完成时主动销毁:if (isset($connection->timer_id)) { Timer::del($connection->timer_id); unset($connection->timer_id); } - 避免使用匿名函数捕获大数组、资源句柄或整个
$connection;优先用静态方法或独立函数
辅助监控与兜底
光靠逻辑还不够,加两层防护:
- 在
onWorkerStart中启用周期性 GC:Worker::addTimer(30, 'gc_collect_cycles'); - 记录峰值内存:
echo "Peak: " . number_format(memory_get_peak_usage(true)) . "B\n";,观察是否随连接数线性增长 - 设
Worker::$max_requests = 1000(或更小),强制进程轮转,防止泄漏无限累积











