下载中间件卡住吞吐量,根本原因是其串行执行模型下存在同步阻塞操作(如time.sleep、requests.get),导致整个downloader线程停滞;优化核心是将i/o操作异步化或移出中间件,仅保留轻量、无阻塞的请求标记与转发逻辑。

为什么下载中间件卡住吞吐量?
Scrapy 的下载中间件(Downloader Middleware)默认是串行执行的,每个请求都要过一遍所有启用的中间件。一旦某个中间件里有同步阻塞操作(比如 time.sleep()、未加 asyncio 包装的数据库查询、或调用慢响应的代理验证接口),整个 downloader 线程就停在那里——哪怕你开了 CONCURRENT_REQUESTS = 100,实际并发可能掉到个位数。
常见卡点包括:
- 在
process_request()中做 DNS 解析或 HTTPS 握手前的 IP 黑名单校验(没缓存) - 使用同步 HTTP 客户端(如
requests.get())调用鉴权服务 - 对每个请求都读取本地配置文件或重载 JSON 规则
-
process_response()里做全文正则匹配或 HTML 解析(本该交给parse())
如何让中间件不拖慢 downloader?
核心原则:中间件只做轻量、确定性、无 I/O 的事;I/O 操作必须异步化或移出中间件。
- 把耗时逻辑挪到
spider或pipeline,中间件只负责“打标”和“转发” - 若必须在中间件中发起网络请求,用
aiohttp+asyncio.to_thread()(Python 3.9+)包装,再配合await,但要注意 Scrapy 的 downloader 是基于 Twisted 的,需用deferToThread或改用scrapy-playwright这类原生支持异步的扩展 - 所有规则判断(如 UA 轮换、Referer 设置)提前加载进内存,用字典或 LRU cache(
@lru_cache)加速 - 避免在
process_request()中修改request.meta大量字段——序列化/反序列化开销会上升,尤其走 Redis 调度时
示例:错误写法
def process_request(self, request, spider):
# ❌ 同步请求,阻塞整个 downloader
resp = requests.get("https://api.example.com/token")
request.headers["Authorization"] = resp.json()["token"]
正确写法(简化版,实际需结合 Twisted Deferred)
def process_request(self, request, spider):
# ✅ 从预加载缓存取,毫秒级
token = self._token_cache.get(spider.name)
if token:
request.headers["Authorization"] = token
哪些中间件参数直接影响吞吐?
DOWNLOAD_DELAY 和 AUTOTHROTTLE_ENABLED 是表层开关,真正影响吞吐的是中间件本身的执行模型:
-
DOWNLOADER_MIDDLEWARES字典里的顺序很重要:越靠前的中间件越早执行,也越容易成为瓶颈。把日志、统计类中间件(如记录耗时)放最后 - 如果用了自定义重试中间件,检查是否在
process_response()里无条件调用request.replace()—— 这会触发新一轮中间件链,形成隐式递归 -
RETRY_TIMES设太高,配合慢中间件,会导致单个失败请求反复占用 downloader slot - 注意中间件返回值:返回
None继续流程;返回Response或Request会短路后续中间件——这本身是优化点,但若逻辑错乱,可能漏掉必要处理(比如跳过了 cookie 中间件)
怎么验证中间件真正在拖慢?
别猜,直接看指标:
- 开启 Scrapy 日志级别为
DEBUG,搜索Downloading和Got response时间戳,算差值。如果多数请求在process_request阶段停留 >200ms,基本就是中间件问题 - 用
cProfile跑单个爬虫周期:python -m cProfile -o profile.pstats scrapy crawl myspider
,然后python -c "import pstats; p = pstats.Stats('profile.pstats'); p.sort_stats('cumulative').print_stats(20)",重点关注process_request和process_response方法耗时 - 关掉所有自定义中间件,基准吞吐跑一次;再逐个启用,观察 QPS 下降幅度——超过 15% 就得查这个中间件
中间件不是万能胶,贴多了反而减速。最常被忽略的是:很多人把本该在 start_requests() 里预生成的 headers,硬塞进中间件实时计算;还有把 session 管理逻辑(如登录态维持)放在中间件里,结果每次请求都重新 login。这些,都不是中间件该干的事。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











