Django集成Celery爬虫任务中Worker内存泄漏问题:任务完成后内存持续增长未释放
我之前在做Django+Celery驱动的爬虫任务时,也踩过这个内存只升不降的坑——任务跑完内存完全不回落,重启Worker就立刻恢复,折腾了好一阵才把问题捋清楚。结合你的配置和已经做的尝试,给你分享一些实用的排查方向和解决办法:
一、先从爬虫任务本身的内存管理入手
爬虫任务天生容易积累内存,很多时候不是Celery的锅,而是我们的代码没做好资源回收:
及时释放临时资源:
用requests发起请求时,尽量用with语句自动关闭响应,避免响应体长期占用内存:with requests.get(url, stream=True) as resp: # 分块处理响应内容 for chunk in resp.iter_content(chunk_size=1024): pass用BeautifulSoup解析DOM后,手动删除对象并触发垃圾回收:
soup = BeautifulSoup(resp.text, "html.parser") # 解析逻辑... del soup import gc gc.collect()避免全局变量缓存大数据:
不要在任务函数外定义全局的列表、字典存储爬取结果,每次任务都用局部变量——任务结束后,局部变量会被Python的GC自动回收,不会长期占用内存。数据库连接及时清理:
爬虫任务耗时可能较长,Django的数据库连接可能会长期占用且未释放,建议在任务的开头或结尾调用连接清理方法:from django.db import close_old_connections @app.task def crawl_task(): close_old_connections() # 任务开始前关闭失效的旧连接 # 爬虫逻辑... close_old_connections() # 任务结束后再次清理连接流式处理大内容:
如果爬取大文件、批量数据,不要一次性加载到内存,比如用requests.get(url, stream=True)分块读取,或者用分页解析的方式处理列表数据,避免内存瞬间暴涨。
二、强化Celery Worker的内存回收机制
你已经尝试了--max-tasks-per-child=200,这是非常有效的兜底手段,再补充几个可以配合的配置:
调整
--max-tasks-per-child的数值:
这个参数强制Worker进程处理N个任务后自动重启,彻底释放内存。你可以根据单个任务的内存涨幅调整数值——如果每个任务跑完内存涨100MB以上,就把值设小一点(比如50);如果涨幅不大,保持200也没问题。新增
--max-memory-per-child限制:
这个参数可以让Worker进程在内存占用超过指定阈值(单位:MB)时自动重启,比按任务数重启更灵活。比如启动Worker时加上:celery -A your_project worker --loglevel=info --max-tasks-per-child=200 --max-memory-per-child=1024意思是:要么处理满200个任务,要么内存超过1024MB,满足任一条件就自动重启Worker。
控制Worker并发数:
虽然你配置了worker_prefetch_multiplier=1(每次预取1个任务),但如果启动Worker时用了--concurrency=N(N>1),多个进程同时运行会导致内存叠加。如果服务器内存有限,建议把并发数设为1或者和CPU核心数匹配,避免内存过载。
三、排查依赖库与Celery本身的潜在泄漏
有时候第三方库的内存泄漏也会导致问题:
检查爬虫依赖的内存行为:
比如requests的全局Session会保存所有Cookie和连接池,导致内存持续上涨,建议在每个任务内部创建局部Session:@app.task def crawl_task(): session = requests.Session() try: resp = session.get(url) # 处理逻辑... finally: session.close() # 手动关闭Session释放资源用工具定位内存泄漏点:
可以用Python自带的tracemalloc或者第三方库memory_profiler、objgraph来监控任务的内存变化:- 用
tracemalloc记录任务前后的内存快照,定位内存占用最高的代码行:import tracemalloc @app.task def crawl_task(): tracemalloc.start() # 爬虫逻辑... snapshot = tracemalloc.take_snapshot() top_stats = snapshot.statistics('lineno') print("[Top 10 memory usage]") for stat in top_stats[:10]: print(stat) - 用
objgraph找出未被回收的大对象,定位引用链,看哪些对象被意外持有导致无法GC。
- 用
四、你的现有Celery配置的小补充
看了你的Celery配置,大部分都是合理的:
task_ignore_result=True和result_backend="rpc://"的搭配没问题,不会因为存储任务结果占用内存;task_acks_late=True和worker_prefetch_multiplier=1适合爬虫这种耗时任务,避免任务丢失;task_time_limit和soft_time_limit能防止任务卡死,建议保持。
最后给个小建议:先跑几个测试任务,用top或者ps aux --sort=-rss盯着Worker进程的内存变化,看是每个任务都线性上涨,还是某个任务突然暴涨,定位清楚是普遍问题还是个别任务的问题,再针对性优化,效率会更高!




