说句大实话,FastAPI的WebSocket性能瓶颈,90%都不在协议本身,而是卡在连接管理、消息广播和阻塞调用这三件事上。只要把这几块处理好了,单机轻松撑住5000+活跃连接,真不是什么难事。
async def websocket_endpoint() 必须全程异步,不能夹带任何同步操作
最容易踩的坑,就是在 websocket.receive_text() 或 websocket.send_text() 之间插入了同步操作。比如 time.sleep()、requests.get()、对大对象做 json.loads(),或者没加 await 的数据库查询。这些操作会直接卡死事件循环,结果就是所有连接都跟着变卡顿,一个拖慢一片。
- 耗时IO操作必须用异步方案替代:用
httpx.AsyncClient替换requests,用aiomysql或asyncpg替换pymysql或psycopg2 - CPU密集型任务,比如大JSON解析或加密运算,要丢进线程池:
await asyncio.to_thread(json.loads, data) - Pydantic模型校验默认是同步的,在高频场景下建议关掉验证,或者用
model_validate_json()来避免重复解析
用全局 list 保存 active_connections,是内存泄漏的重灾区
很多人图省事,直接用 active_connections = [] 来追加连接、遍历发送。表面上看挺简单,但一旦客户端异常断开——比如关浏览器、网络中断——websocket.send_text() 会抛异常。如果代码没捕获这个异常,这个连接就永远留在列表里,内存持续上涨,最后导致OOM。
- 建议改用
set而不是list,避免重复添加 - 每次广播前,一定要用
try/except捕获WebSocketDisconnect和通用Exception,把失效连接从集合中移除 - 加心跳检测:在
while True:循环里定期await websocket.ping(),超时了主动close() - 生产环境务必加上连接数监控,比如用
psutil.Process().memory_info().rss定期打点
Gunicorn + Uvicorn worker 数,不是越多越好
gunicorn main:app -k uvicorn.workers.UvicornWorker -w 4 是常见配置,但盲目增加 -w 往往适得其反。Uvicorn 本身是单线程异步模型,每个 worker 独占一个事件循环;worker 过多会导致 CPU 上下文切换激增、内存冗余,甚至文件描述符耗尽。
- 推荐值:worker 数 = CPU 核心数 × 1~1.5。例如4核机器设
-w 4或-w 6 - 必须配置
--limit-concurrency 100,防止单个 worker 接收过多连接压垮内存 - 启用
--ws-per-message-deflate开启 WebSocket 压缩,对文本消息可减小40%以上的传输体积 - 禁用
--reload(仅开发用),生产环境必须关掉——它会 fork 出多个进程,导致连接状态不同步
广播消息,不能 for-loop 逐个 await 发送
想象一下,当有2000个连接在线时,如果写 for conn in connections: await conn.send_text(...),这就是串行发送。哪怕每个 send 只耗时2毫秒,一轮也要4秒——用户感知就是“卡死了”。
- 改用
asyncio.gather(*[conn.send_text(...) for conn in connections], return_exceptions=True)并发发送 - 注意:
gather会同时触发所有 send,可能引发内核缓冲区溢出,需要配合asyncio.Semaphore(100)限流 - 更稳妥的做法:把消息推到
asyncio.Queue,由后台任务消费并分批广播,把接收和发送节奏解耦 - 纯通知类消息(比如“用户上线”),可以考虑只发给相关房间,而不是全量广播
其实,真正难的不是写 async 代码,而是判断哪一步该用 await、哪一步该丢进线程池、哪一步该进队列。这些边界问题,靠文档是看不出来的,得靠 print(time.time()) 和 psutil 实测,才能找到最合适的方案。