workerman心跳延迟主因是事件循环被同步大循环阻塞,需改用异步分片或房间索引优化。

Workerman 里心跳包延迟,八成不是网络问题,而是你代码里藏了个“大循环”卡住了整个事件循环。 它不支持多线程式并行,所有 onMessage、onClose、Timer 回调都挤在同一个事件循环里跑。一个 for 循环跑 500ms,那这半秒内所有连接的心跳响应、新消息、定时器全得排队等——客户端自然收不到 ping 回复。
为什么 foreach 遍历连接列表会拖慢心跳?
常见错误是写个“全服广播”逻辑,在 onMessage 或定时器里直接 foreach ($worker->connections as $conn),再逐个 $conn->send()。表面看只是遍历,但一旦连接数上几百、上千,且每个 $conn 上还带点属性判断(比如 if ($conn->roomId === $targetRoom && $conn->isConnected())),CPU 就实打实地在干这事,事件循环被占满。
- 连接数超 500 后,纯遍历本身就能吃掉 10~30ms,叠加判断逻辑更久
- 如果
$conn上没预设好roomId等字段,PHP 还要触发 __get 或动态属性查找,开销翻倍 - 这个循环若嵌在心跳检测定时器里(比如每 30 秒扫一遍),它一卡,整轮心跳就集体延迟
怎么把广播逻辑从“同步遍历”改成“异步分片”?
核心思路:别让一次回调干完所有事,把大任务切成小块,交由事件循环分批执行。Workerman 的 Timer::add(0, ...) 是最轻量的“让出控制权”方式。
- 把原本一个大
foreach拆成每次处理 20~50 个连接,用Timer::add(0, function() { ... })触发下一批 - 维护一个全局游标,比如
static $cursor = 0,每次只从$worker->connections中取一段array_slice(..., $cursor, 30) - 切片后仍需检查
$conn->isConnected(),因为连接可能在你切片过程中已断开 - 不要在切片回调里再嵌套
Timer::add(0, ...),改用 while + yield 或递归调用自身(加深度限制防爆栈)
更彻底的解法:用房间索引代替全量遍历
99% 的游戏/IM 场景根本不需要扫全部连接。与其每次 O(n) 遍历,不如提前建好 O(1) 查找结构。
- 在
onConnect里立刻给连接打标:$connection->roomId = $roomId; $connection->playerId = $uid; - 用
static $rooms = [];维护房间映射:$rooms[$roomId][] = $connection;,进房时 append,退房时 unset 对应 key - 心跳广播时,直接
foreach ($rooms[$targetRoom] as $conn)—— 数据量小、无冗余判断、不依赖$worker->connections的顺序或完整性 - 注意:
$rooms是静态变量,跨 Worker 进程不共享;如需跨进程,得用Channel或 Redis
真正容易被忽略的是:**心跳定时器本身不能成为性能瓶颈**。哪怕你用了房间索引,如果每秒触发 10 次心跳检测,而每次都要遍历房间内 200 个连接做 isConnected() 和 send(),照样会抖动。最优实践是——心跳只负责“探测”,状态广播走独立帧循环;两者完全解耦,各跑各的节奏。











