FastApi结合Gunicorn与multiprocessing Pool使用时内存泄漏问题求助
问题排查与解决思路
核心常见触发原因
- 子进程资源未正确回收:如果FastAPI接口逻辑中直接/间接创建了multiprocessing Pool子进程,异步场景下没有显式调用
close()+join()回收进程池资源,会导致孤儿进程关联的内存块被主进程持有无法释放。 - Gunicorn worker进程复用的内存滞留:默认Gunicorn worker会持续处理请求,Python的内存分配器(ptmalloc)不会主动将未使用的内存归还给操作系统,即使对象已经被GC回收,这部分内存会被Python解释器缓存复用,表现为内存占用持续高位不下降。
- 异步上下文的对象泄漏:如果接口中存在全局缓存、未关闭的数据库连接/异步客户端实例、或者在请求上下文外持有请求生命周期内的临时对象,会导致对象被长期引用无法被GC回收。
- multiprocessing fork带来的copy-on-write内存泄漏:Gunicorn启动时会先fork worker进程,如果你在主进程初始化阶段就加载了大量数据或者创建了进程池,fork后子进程对这些内存页的修改会导致内存多份复制且无法跨进程释放。
可落地排查步骤
- 先确认内存泄漏源:使用
memray工具附加到运行中的Gunicorn worker进程,压测接口复现内存上涨场景,生成内存火焰图定位持续增长的对象类型。命令参考:memray attach <worker_pid> -o leak_profile.bin,结束后用memray tree leak_profile.bin查看内存分配栈。 - 验证是否为Python内存缓存问题:压测停止后手动触发GC,观察内存是否下降,可在FastAPI中注册SIGUSR1信号处理函数触发
gc.collect(),如果内存下降则是ptmalloc缓存问题,不是真泄漏。 - 隔离multiprocessing相关逻辑:暂时注释掉接口中所有调用multiprocessing相关的代码,重新压测观察内存走势,如果不再上涨则确定是进程池使用逻辑的问题。
对应解决方案
- 进程池使用规范:如果是接口内部使用multiprocessing Pool,必须在每个请求处理结束后显式关闭并回收,示例如下:
import asyncio from multiprocessing import Pool from fastapi import FastAPI app = FastAPI() def your_sync_task(params): # 任务逻辑 pass @app.post("/your-endpoint") async def your_endpoint(): loop = asyncio.get_running_loop() # 进程池仅在当前请求生命周期内创建使用 with Pool(processes=2) as pool: res = await loop.run_in_executor(None, pool.map, your_sync_task, your_params) # with语法会自动调用close+join回收资源 return res
如果是高频调用的接口,不要每次请求都创建进程池,改用全局单例进程池,同时配置maxtasksperchild参数,让进程池的工作进程处理固定数量任务后自动重启,避免内存累积。
- 配置Gunicorn自动回收worker:在gunicorn_conf.py中添加
max_requests和max_requests_jitter参数,设置每个worker处理固定数量请求后自动重启,主动释放滞留内存,配置示例:
max_requests = 1000 max_requests_jitter = 100 worker_tmp_dir = "/dev/shm"
- 优化fork时机:将全局大对象、进程池、数据库连接等资源的初始化逻辑移到worker进程启动后执行,不要在Gunicorn主进程初始化阶段加载,可通过FastAPI的
startup事件注册初始化逻辑。 - 替换内存分配器:如果是ptmalloc缓存导致的内存高位问题,安装jemalloc或者tcmalloc,启动Gunicorn前预加载:
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 gunicorn <你的启动参数>,jemalloc对未使用内存的回收策略更激进,能有效降低常驻内存占用。
内容的提问来源于stack exchange,提问作者Felipe Operti
相关产品推荐
相关产品推荐

