FastAPI中run_in_threadpool内存占用过高且进程结束后未释放
问题解决思路:FastAPI BackgroundTask 内存占用过高与泄漏问题
核心问题拆解
当前场景是:FastAPI 中用 BackgroundTask 执行 Langchain 的 RecursiveCharacterTextSplitter 文本分割,为避免阻塞用 run_in_threadpool 包装同步任务后,出现内存占用飙升导致进程超时、任务完成后内存无法完全回收的问题。
可能的根因
- 线程池内的大对象持有:
RecursiveCharacterTextSplitter处理大文本时会在内存中生成大量文本块对象,线程池的工作线程可能长期持有这些对象引用,导致GC无法及时回收。 - Langchain 分割器的内存开销:默认配置下的分割器可能会缓存中间结果,或者一次性加载完整文本到内存,加剧内存占用。
- FastAPI 线程池的默认行为:默认线程池没有限制线程数量,高并发下多个任务同时执行会导致内存叠加,且线程复用机制可能让旧对象迟迟无法被回收。
- 循环引用或未释放的资源:文本分割过程中可能产生循环引用,或者某些内部对象没有正确释放,导致GC无法清理。
可行解决方案
1. 优化文本分割的内存使用
- 流式加载文本:不要一次性把整个大文本读入内存,改用逐行/逐块读取的方式喂给分割器,减少单任务的内存峰值。
- 调整分割器参数:减小
chunk_size降低单块文本的内存占用,同时检查keep_separator等参数是否会额外增加内存开销;必要时改用更轻量的分割实现(比如自定义基于字符串切片的分割逻辑)。 - 手动清理对象引用:在分割任务完成后,显式删除大文本对象、分割器实例等,解除引用以便GC回收:
def split_text_task(large_text: str): splitter = RecursiveCharacterTextSplitter(chunk_size=1000) chunks = splitter.split_text(large_text) # 处理chunks... # 手动清理引用 del large_text, splitter, chunks import gc gc.collect()
2. 自定义线程池/改用进程池
- 限制线程池大小:FastAPI 默认使用
anyio的线程池,可自定义线程池并限制线程数量,避免并发任务过多导致内存爆炸:from concurrent.futures import ThreadPoolExecutor from fastapi import FastAPI, BackgroundTasks app = FastAPI() custom_executor = ThreadPoolExecutor(max_workers=2) # 根据服务器配置调整 async def run_split_task(large_text: str): await app.state.executor.submit(split_text_task, large_text) @app.post("/split") async def split_endpoint(background_tasks: BackgroundTasks): large_text = ... # 获取文本 background_tasks.add_task(run_split_task, large_text) return {"status": "task started"} - 改用进程池:如果线程池的内存共享问题无法解决,改用
ProcessPoolExecutor,每个任务在独立进程中执行,进程结束后会自动释放所有内存(注意:进程间通信需要序列化对象,大文本可能有性能损耗,需权衡)。
3. 强制GC回收与内存监控
- 在任务完成后手动触发GC,尤其是处理大对象后:
import gc gc.collect() - 使用
tracemalloc监控内存分配,定位具体是哪些对象占用了内存:import tracemalloc tracemalloc.start() # 执行分割任务 snapshot = tracemalloc.take_snapshot() top_stats = snapshot.statistics('lineno') print("[Top 10 memory allocations]") for stat in top_stats[:10]: print(stat)
4. 替换BackgroundTask为独立任务队列
如果以上方案仍无法解决内存泄漏问题,建议将文本分割任务放到独立的异步任务队列(如Celery + Redis/RabbitMQ),让任务在单独的 Worker 进程中执行,完全隔离API进程的内存,从根本上避免内存占用叠加的问题。
内容的提问来源于stack exchange,提问作者monopoly
相关产品推荐
相关产品推荐

