scrapy中meta默认引用传递,直接赋值会导致response对象被request长期持有而无法释放;应使用copy.copy()或response.meta.copy()浅拷贝,遇复杂对象需手动剥离或重构逻辑。

Scrapy爬虫内存占用过大,不是配置没调好那么简单,而是对象生命周期失控的直接结果——Request、Response、meta 一旦被意外延长存活时间,就会像滚雪球一样把整个页面 HTML、重定向链、甚至闭包变量全钉在内存里。
为什么 meta 赋值会锁死 Response 对象
Scrapy 中 meta 是字典,Python 赋值默认是引用传递。如果你在 parse 方法里写 meta = response.meta,再把这个 meta 传给新 Request,那原 response 就被新请求间接持有了——只要这个请求还在调度队列里,response.body(可能几 MB)就无法释放。
- 典型错误:
yield Request(url, meta=response.meta, callback=self.parse_detail) - 正确做法:用
copy.copy(response.meta)浅拷贝(value 是简单类型时足够),或response.meta.copy()(等价) - 危险增强:如果
meta里存了response本身、selector或自定义类实例,浅拷贝也不行,得手动剥离或重构逻辑 - 验证方式:进 telnet(
telnet localhost 6023),执行prefs(),看HtmlResponse和Request数量是否随爬取持续增长且“oldest”时间越来越长
如何用 trackref 快速定位泄漏源头
Scrapy 内置的 trackref 模块不依赖外部库,专盯 Request、Response、Item 等核心对象的存活状态。它不分析内存布局,只告诉你“哪些对象还活着、活了多久”,这是最贴近真实泄漏场景的诊断方式。
- 启动时确保
TELNETCONSOLE_ENABLED = True(默认开启) - 连接后输入
prefs(),重点关注oldest时间:如果HtmlResponse最老的已存在 300 秒以上,基本可判定 parse 逻辑没及时释放 - 进一步查对象:用
get_oldest('HtmlResponse')获取那个最老响应的引用路径,常能暴露出是哪个 spider 的哪个 callback 持有了它 - 注意陷阱:
trackref不跟踪普通 dict/list,所以泄漏若来自你自定义的全局缓存(如self.cache = []),它不会显示——得靠tracemalloc配合
JOBDIR 不是万能解药,但能掩盖调度器堆积
JOBDIR 把内存队列刷到磁盘,表面看 RSS 下降了,其实只是把内存压力转嫁成磁盘 IO 和 inode 消耗。它解决不了根本问题:如果你的 parse 还在往 self.items 堆 response,或者 pipeline 异步写入太慢,Request 依然会在 Redis 或文件队列里越积越多,最终拖垮整个引擎循环。
- 必须配对使用:
JOBDIR+MEMUSAGE_LIMIT(如4096MB),否则 OOM Killer 仍会杀进程 - 慎用
SCHEDULER_PERSIST = True:重启后旧请求重放,若目标页已失效或反爬升级,反而造成无效重试和资源浪费 - 真正治本的是控制生成节奏:在中间件里加
time.sleep()或用scrapy-redis的BRPOP阻塞式消费,让Spider生成速度匹配Downloader处理能力
最易被忽略的一点:泄漏往往不出现在你写的主逻辑里,而藏在某个中间件的 process_spider_output 方法中——比如它悄悄把每个 response 存进类属性做统计,却忘了清空。排查时别只盯着 spider,prefs() 显示的 “oldest” 对象,得顺着引用链一层层翻到最深的持有者。











