bigywater.tech
Scrapy · 2

Twisted reactor 与 asyncio reactor:单线程里怎么同时处理几百个请求

Scrapy 靠 Twisted 的 reactor 在一个线程里同时处理几百个请求。2.19 里默认 reactor 已经是 asyncio 的,还有完全不用 reactor 的模式。这篇用同一个 spider 跑三种方式,看日志、线程、阻塞回调和 async 回调的真实差别。

0.62 s
100 个各延迟 0.5 s 的请求在一个线程里并发完成(默认 reactor)
0.34 → 2.39 s
回调里同步 sleep(0.1) 之后 20 个请求的总耗时
20 个
SelectReactor 下 async 回调里 await asyncio.sleep 抛出的 RuntimeError 数

三种运行方式

实测 · 本机执行输出
  [asyncio reactor(默认)] 设置 (无),退出码 0
      [scrapy.crawler] DEBUG: Using AsyncCrawlerProcess
      [scrapy.utils.log] DEBUG: Using reactor: twisted.internet.asyncioreactor.AsyncioSelectorReactor
      [scrapy.utils.log] DEBUG: Using asyncio event loop: asyncio.unix_events._UnixSelectorEventLoop
  [SelectReactor] 设置 {'TWISTED_REACTOR': 'twisted.internet.selectreactor.SelectReactor'},退出码 0
      [scrapy.crawler] DEBUG: Using CrawlerProcess
      [scrapy.utils.log] DEBUG: Using reactor: twisted.internet.selectreactor.SelectReactor
  [无 reactor] 设置 {'TWISTED_REACTOR_ENABLED': 'False'},退出码 0
      [scrapy.crawler] DEBUG: Using AsyncCrawlerProcess
      [scrapy.crawler] DEBUG: Not using a Twisted reactor
      [scrapy.core.downloader.handlers._base_streaming] WARNING: AiohttpDownloadHandler is experimental and is not recommended for production use.(http 与 https 各打印一次)
  默认值:TWISTED_REACTOR = twisted.internet.asyncioreactor.AsyncioSelectorReactor True None 10  (依次:TWISTED_REACTOR、TWISTED_REACTOR_ENABLED、ASYNCIO_EVENT_LOOP、REACTOR_THREADPOOL_MAXSIZE)

2.19 里 TWISTED_REACTOR 默认就是 AsyncioSelectorReactor,用的进程类是 AsyncCrawlerProcess;设成别的 reactor 则回到 CrawlerProcess;TWISTED_REACTOR_ENABLED=False 不装 reactor,直接用 asyncio 事件循环,下载由实验性的 AiohttpDownloadHandler 完成。

一个线程同时处理 100 个请求

实测 · 本机执行输出
  方式                          总耗时 s       服务器并发峰值                  回调线程     线程数(最大)     成功响应
  asyncio reactor(默认)          0.62           100            MainThread           2      101
  SelectReactor                0.67           100            MainThread           2      101
  无 reactor                    0.59           100            MainThread           1      101

网络 I/O 是非阻塞的,100 个各等 0.5 s 的请求同时在路上,总耗时 0.6 s 左右;所有回调都在 MainThread 里运行。

回调不能阻塞

实测 · 本机执行输出
  情形                                              总耗时 s  理想值
  回调什么都不做                                          0.34  0.3+≈0(状态码 200 的响应 21 个)
  回调里 time.sleep(0.1)                              2.39  0.3+20×0.1=2.3(状态码 200 的响应 21 个)
  回调里 time.sleep(0.1),并发改成 1                       8.21  20×(0.3+0.1)=8(状态码 200 的响应 21 个)
  async def 回调里 await asyncio.sleep(0.1)           0.43  0.3+0.1(状态码 200 的响应 21 个)

回调、中间件、pipeline 与网络 I/O 共用一个线程,同步的 time.sleep 或重计算会拖住所有请求。需要等待时用 async def 和 await。

async 回调依赖 asyncio

实测 · 本机执行输出
  [asyncio reactor(默认)] 总耗时 0.45 s;spider_exceptions/count=0;response_received_count=21
  [SelectReactor] 总耗时 0.35 s;spider_exceptions/count=20;response_received_count=21
      异常:RuntimeError: no running event loop
  [无 reactor] 总耗时 0.43 s;spider_exceptions/count=0;response_received_count=21

默认 reactor 和无 reactor 模式都在 asyncio 事件循环上,await asyncio.sleep 可以用;SelectReactor 下没有运行中的 asyncio 事件循环,会抛 RuntimeError: no running event loop。

换事件循环

实测 · 本机执行输出
  默认事件循环:asyncio.unix_events._UnixSelectorEventLoop;3 次的 elapsed_time_seconds [0.302, 0.299, 0.335],中位数 0.302 s;301 个响应
  uvloop:uvloop.Loop;3 次的 elapsed_time_seconds [0.275, 0.277, 0.271],中位数 0.275 s;301 个响应

ASYNCIO_EVENT_LOOP 可以指定事件循环类,例如 uvloop.Loop。在本机零延迟的小实验里 uvloop 略快,但差距有限。

分步演示

reactor 与事件循环
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Scrapy 2.19.0 / Twisted 26.4.0 / lxml 6.1.3 / parsel 1.12.1(临时虚拟环境);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、重定向、压缩、断连等行为,并记录收到的原始请求;每个实验在临时目录里新建 Scrapy 项目或单文件 spider,命令输出来自真实的 scrapy 进程;计时用 time.perf_counter,数字每次运行会有小幅抖动。uvloop 0.23.0 另装在虚拟环境里。每个情形都是独立进程里的一次 scrapy runspider;服务器是 Python 标准库的线程服务器,用 time.sleep 制造延迟,峰值是服务器端同时在处理的请求数
    • 没有验证:真实网络下的吞吐;大量域名、大量 item 时三种方式的区别;无 reactor 模式下 aiohttp 下载处理器与默认处理器在重定向、压缩、代理、TLS 上的差异(该处理器被标为实验性);进程里多出的那个线程是什么;uvloop 在 Linux 上的表现;CrawlerRunner / AsyncCrawlerRunner 在已有事件循环的程序里的用法(下一篇);reactor 线程池 REACTOR_THREADPOOL_MAXSIZE 的实际用途
    京ICP备2026067502号-1京公网安备11010502063487号