Uvicorn进程RSS持续上涨大概率是应用逻辑泄漏,需先排除workers资源未释放、uvloop兼容性问题及on_startup未配on_shutdown清理;再验证是否因def同步函数误入线程池导致对象堆积,或torch/CUDA缓存未清等隐式泄漏。

Uvicorn进程RSS持续上涨但无OOM,先确认是不是它的问题
别急着改代码。Uvicorn本身极轻量,纯HTTP路由几乎不持有业务对象,内存涨得慢、稳、线性,大概率是你的应用逻辑在 leak,不是Uvicorn框架的问题。但有三个例外必须先排除:uvicorn 启动时用了 --workers 且子进程未正确释放资源;用了 --loop uvloop 但某些C扩展(如旧版 pydantic 或自定义异步驱动)存在引用未清理;或你在 on_startup 里注册了全局缓存/连接池却没配 on_shutdown 清理逻辑。
快速验证方法:
- 用 ps aux --sort=-%mem | head -10 看哪个 worker PID 内存最高
- 对该 PID 执行 cat /proc/<pid>/status | grep -E 'VmRSS|Threads'</pid>,若 VmRSS 持续增长而 Threads 数稳定,说明是堆内存泄漏,不是线程泄漏
- 临时改用单 worker + --loop asyncio 启动,观察是否仍上涨——如果涨速明显变缓,问题可能出在 uvloop 兼容性或 worker 隔离失效
FastAPI/Starlette handler 里 def 和 async def 混用导致隐式线程池堆积
这是最常被忽略的泄漏诱因。Uvicorn 路由到 def 同步函数时,会把它扔进默认线程池(concurrent.futures.ThreadPoolExecutor)。如果这个函数内部创建了长生命周期对象(比如缓存字典、数据库连接、torch.Tensor),又没显式销毁,这些对象就会被线程局部变量或闭包意外持有,无法被 GC 回收。
典型表现:
- 内存随请求数缓慢上升,但每个请求处理完后不回落
- gc.get_objects() 能扫出大量重复的 dict、list 或模型权重实例
- objgraph.show_most_common_types(limit=20) 显示 weakref、threading.local 或 _thread._local 类型异常多
解决办法:
- 所有路径操作函数统一用 async def,避免进入线程池
- 如果必须用 def(比如调用阻塞IO库),确保函数内不构造大对象,且用 with 或 try/finally 显式释放资源
- 不要依赖线程退出自动清理:Python 的线程池复用线程,threading.local 变量不会随任务结束自动清空
memory_profiler 在 Uvicorn 多进程下完全不生效?这样注入才对
@profile 装饰器在 uvicorn 里默认无效,因为 memory_profiler 的启动钩子没挂到子进程上。你看到的“没输出”,其实是 profiler 根本没加载。
Uvicorn 0.52.1 是 Python ASGI Web 服务器,提供异步应用服务及 WebSocket 支持,常用于 FastAPI、Starlette 等应用的开发和部署。本下载为官方 PyPI 通用 wheel 包,可使用 pip 安装,并由包管理器处理所需依赖。
正确做法是手动在关键 handler 中嵌入打点:
- 不要装全局装饰器,改用 LineProfiler 实例化后动态启用:
from memory_profiler import LineProfiler
<p>lp = LineProfiler()</p><p>@app.post("/infer")
async def infer_handler():
lp.enable()
result = heavy_computation() # 你怀疑的函数
lp.disable()
return {"result": result}</p><h1>启动时注册回调,把 profile 结果写到文件</h1><p>@app.on_event("shutdown")
async def dump_profile():
lp.dump_stats("/tmp/profile_result.lprof")
- 启动命令加
--include-children:uvicorn main:app --workers 4 --include-children
- 每个 worker 的 profile 数据独立,需分别分析;别用
mprof run,它只抓主进程,对多 worker 场景无意义
模型服务中 torch.Tensor / CUDA 缓存泄漏的静默陷阱
Qwen2.5、Llama 等大模型部署时,torch 的 CUDA 缓存机制(torch.cuda.memory_cached())和 Python 层引用容易脱节。即使你 del 了 tensor,底层 GPU 内存可能还在,而 Python 的 gc 完全感知不到。
常见错误:
- 在 async def 函数里反复 model.generate(...) 但没调 torch.cuda.empty_cache()
- 使用 functools.lru_cache 缓存了含 tensor 的返回值,cache key 是不可哈希的 dict 或 list
- model.to("cuda") 后没设 torch.set_grad_enabled(False),导致计算图残留
排查建议:
- 在 handler 开头/结尾打印 torch.cuda.memory_allocated() 和 torch.cuda.memory_reserved()
- 用 nvidia-smi 观察 GPU 显存是否同步上涨;如果 Python 进程 RSS 涨但 GPU 显存不涨,问题在 CPU 内存;反之则是 CUDA 缓存泄漏
- lru_cache 必须配合 @lru_cache(maxsize=128) 显式限容,且 key 类型只能是 str、int、tuple 等可哈希类型,不能是 dict 或 torch.Tensor
真正棘手的泄漏往往藏在 shutdown 钩子里没写的那一行 del model,或者 ThreadPoolExecutor 里没 close 的那个 connection。Uvicorn 不负责替你管业务对象的生命周期,它只保证事件循环干净退出。你得自己画清楚:哪个对象该活多久,谁持有它的引用,谁负责切断它。

















