WebSocket不显式close()必然导致内存和FD双泄漏,因默认4KB~64KB缓冲区、未清理闭包、心跳定时器及permessage-deflate上下文持续驻留,GC无法回收强引用对象。

WebSocket 连接不显式 close(),内存和文件描述符(FD)就一定会涨——这不是配置调得不够细,而是资源生命周期根本没对齐。
为什么内存会持续上涨?
根本原因不是连接数多,而是每个活跃 WebSocket 实例背后绑定了默认 4KB~64KB 的读写缓冲区、未清理的闭包引用、心跳定时器,以及启用 permessage-deflate 后额外的 zlib 上下文。Go/gorilla 和 Python/websockets 都一样:GC 不回收还在事件循环中被强引用的对象。
-
ws.onmessage = (e) => this.handleData(e)这类写法,卸载组件时不设ws.onmessage = null,闭包一直锁着this和所有依赖项 - 心跳用
setInterval却没存到ws.heartbeatTimer,卸载时只清 state,没调clearInterval - 用
list存连接,异常断连后send_text()抛异常但没捕获 → 连接永远卡在列表里 -
upgrader.CheckOrigin返回False时,握手已开始但未完成,容易漏掉conn.Close()
Python websockets 库必须关的三个开关
禁用压缩、压小缓冲区、限制队列深度,三者叠加可把单连接内存从 64KB 降到 14KB 左右:
- 服务端启动时传
compression=None:彻底跳过permessage-deflate初始化,比客户端不发请求头更可靠 - 设
max_size=65536(而非默认 1 MiB),防大 payload 触发缓冲区无限扩容 - 设
max_queue=8(而非默认 32),避免消息积压拖垮内存;配合read_limit=8192和write_limit=8192控制缓冲区上限
FastAPI/Starlette 中连接管理的致命陷阱
别用 list 或全局变量存连接,广播必须异步且带异常兜底:
立即学习“Python免费学习笔记(深入)”;
- 用
set而非list存active_connections,避免重复添加 - 每次
send_json()前加try/except WebSocketDisconnect,失败就从set中移除 - 广播不用
for循环 +await,改用asyncio.gather(*tasks, return_exceptions=True) - 心跳检测不能只靠客户端 ping,要在服务端
while True里定期await websocket.ping(),超时主动close()
Linux 层面的句柄泄漏常被忽略
Too many open files 不是“连接太多”,而是该关没关——服务端每漏一个 close(),操作系统 FD 就多占一个:
- 确认
ulimit -n≥ 65535,且/etc/security/limits.conf中* soft nofile和* hard nofile都设为 1048576 - Uvicorn 启动加
--limit-concurrency 100,防止单 worker 接太多连接压垮内存 - 检查日志是否频繁出现
EMFILE或ConnectionResetError,这是 FD 耗尽的直接信号
真正难的不是写 ws.close() 这一行,而是确保它在所有路径下都被执行:组件卸载、网络中断、服务重启、甚至 CheckOrigin 拒绝时——只要连接对象创建了,就必须有明确的销毁契约。


















