swoole websocket 服务中客户端断连后 fd 未及时回收是隐蔽而危险的内存泄漏源头,表现为连接失败、内存与 cpu 异常升高,可通过 lsof 和 fd 数对比确认,修复需在 onclose 中 unset 引用、关闭协程句柄、清除定时器,并加入监控与压测验证。

客户端断连后文件描述符(fd)没及时回收,是 Swoole WebSocket 服务中最隐蔽也最危险的内存泄漏源头之一。它不立刻报错,但会悄悄拖垮整个服务:新用户连不上、worker 内存持续上涨、CPU 占用异常升高——直到某次批量断连触发系统级 fd 耗尽。
一眼确认是否是 fd 泄漏
别猜,直接看系统指标:
- 执行 lsof -p
swoole_pid> | grep socket ,若返回几百甚至上千行 sock 条目,且状态多为 CLOSE_WAIT 或无明确协议标识,基本就是泄漏了 - 对比 ls -l /proc/
/fd/ | wc -l 和 ulimit -n,如果前者接近后者(比如 ulimit 是 65535,而 fd 数已达 64200),说明系统资源已绷紧 - 在 on('close') 回调里加日志,同时检查 lsof 输出——如果日志显示“客户端 123 已断开”,但 lsof 里仍能看到该 fd 对应的 socket 行,说明 close 回调没真正释放底层连接
常见泄漏点与对应修复写法
Swoole 的 fd 管理是显式的,不清理就永远挂着。以下三类写法最常出问题:
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 没在 onClose 中主动 unset 连接引用:比如把 $fd 存进全局数组 $this->clients[$fd] = $request,但 on('close') 里只 echo 了断开消息,没写 unset($this->clients[$fd])
- 用了协程 MySQL/Redis 却没关闭句柄:在 onMessage 里 new Swoole\Coroutine\MySQL(),但没在协程退出前 close(),句柄会随 worker 生命周期滞留
- 定时器或异步任务绑定到 fd 上,但未随断连清除:例如给每个连接启动心跳 timer,并把 timer_id 存入 $this->timers[$fd],却没在 onClose 中调用 Swoole\Timer::clear($this->timers[$fd]) 并 unset
加一道防御性检测机制
靠人工 review 容易漏,建议在服务启动时注入轻量级监控:
- 用 Swoole\Timer::tick(30000, ...) 每 30 秒统计一次当前活跃连接数:$server->connections,再对比 count($this->clients),两者长期不一致就是管理不同步
- 开启协程 GC:Swoole\Coroutine::set(['hook_flags' => SWOOLE_HOOK_ALL, 'enable_coroutine_gc' => true]),并配合 memory_get_peak_usage() 打印内存趋势
- 在 on('close') 开头加一句 if (isset($this->clients[$fd])) { unset($this->clients[$fd]); },哪怕重复删也不影响,但能兜底防止遗漏
验证修复是否生效
改完代码别急着上线,本地模拟压测更可靠:
- 起一个简单脚本,用 50 个并发 WebSocket 客户端连接 → 发几条消息 → 立即断开 → 等 10 秒再重复
- 循环 5 轮后,执行 lsof -p
| grep socket | wc -l ,数值应稳定在初始连接数附近(比如始终在 10–20),而不是一路飙升到上百 - 观察 top -p
中 RES 内存列,不应随连接反复增减而阶梯式上升










