You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

FastAPI中run_in_threadpool内存占用过高且进程结束后未释放

问题解决思路:FastAPI BackgroundTask 内存占用过高与泄漏问题

核心问题拆解

当前场景是:FastAPI 中用 BackgroundTask 执行 Langchain 的 RecursiveCharacterTextSplitter 文本分割,为避免阻塞用 run_in_threadpool 包装同步任务后,出现内存占用飙升导致进程超时、任务完成后内存无法完全回收的问题。

可能的根因

  1. 线程池内的大对象持有:RecursiveCharacterTextSplitter 处理大文本时会在内存中生成大量文本块对象,线程池的工作线程可能长期持有这些对象引用,导致GC无法及时回收。
  2. Langchain 分割器的内存开销:默认配置下的分割器可能会缓存中间结果,或者一次性加载完整文本到内存,加剧内存占用。
  3. FastAPI 线程池的默认行为:默认线程池没有限制线程数量,高并发下多个任务同时执行会导致内存叠加,且线程复用机制可能让旧对象迟迟无法被回收。
  4. 循环引用或未释放的资源:文本分割过程中可能产生循环引用,或者某些内部对象没有正确释放,导致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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 14:55:19