bigywater.tech
Scrapy · 15

并发控制:CONCURRENT_REQUESTS 与按域名、按 IP 的限制

文档说 CONCURRENT_REQUESTS_PER_DOMAIN 限制单个域名的并发。这篇在服务器端数同时在处理的请求,发现默认的 asyncio reactor 下它根本拦不住一次性涌入的请求;全局限制和 DOWNLOAD_DELAY 才是靠得住的。本文的结论都是 2.19.0 本机实测。

48
默认 asyncio reactor 下,CONCURRENT_REQUESTS_PER_DOMAIN=1~16 时单域名的服务器端峰值(SelectReactor 下等于设置值)
1 → 16
放行循环里槽的 transferring 数一路涨到 16,而 concurrency 只有 4
4 / 2
DOWNLOAD_DELAY 为 0.1、0.2 时的服务器峰值(有效的限制手段)

预期与实测

实测 · 本机执行输出
       每域名并发       asyncio reactor(默认)             SelectReactor                 无 reactor       理想耗时 48×0.3/N
           1               峰值 48(0.35)               峰值 1(14.69)               峰值 1(14.65)                14.4
           2               峰值 48(0.37)                峰值 2(7.35)                峰值 2(7.35)                 7.2
           4               峰值 48(0.38)                峰值 4(3.70)                峰值 4(3.68)                 3.6
           8               峰值 48(0.36)                峰值 8(1.88)                峰值 8(1.85)                 1.8
          16               峰值 48(0.37)               峰值 16(0.96)               峰值 16(0.95)                 0.9

文档上,每个域名(下载槽)同时下载的请求数不超过 CONCURRENT_REQUESTS_PER_DOMAIN。实测 2.19.0 在 SelectReactor 和无 reactor 模式下确实如此,但默认的 asyncio reactor 下单域名的并发等于已经排在槽里的请求数,与设置无关。

机制

实测 · 本机执行输出
  asyncio reactor(默认):槽的 concurrency = 4;共 17 次开始下载;各次开始时槽里 transferring(含自己)依次是 [1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];服务器峰值 16
  SelectReactor:槽的 concurrency = 4;共 17 次开始下载;各次开始时槽里 transferring(含自己)依次是 [1, 1, 2, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4];服务器峰值 4

槽按 concurrency − len(transferring) 放行,而 asyncio 下下载是以任务的方式排队的,transferring 要等任务运行才增加,放行循环看不到,于是一次放光。

哪些限制有效

实测 · 本机执行输出
        全局并发        服务器总峰值                       各主机峰值     总耗时 s
           4             4     127.0.0.1=2 localhost=2      2.46
           6             6     127.0.0.1=3 localhost=3      1.87
          16            16     127.0.0.1=8 localhost=8      0.65

全局 CONCURRENT_REQUESTS(引擎按在途请求数判断)和 DOWNLOAD_DELAY(每次只放出一个)在 asyncio reactor 下有效。

延迟的效果

实测 · 本机执行输出
    DOWNLOAD_DELAY       服务器峰值     总耗时 s           相邻请求到达间隔的中位数 ms
                 0          24      0.34                         0
               0.1           4      2.73                       101
               0.2           2      5.13                       201

有 DOWNLOAD_DELAY 时相邻请求间隔等于延迟,同时在途的请求数约等于请求耗时除以延迟。

PER_IP 与 DOWNLOAD_SLOTS

实测 · 本机执行输出
  PER_IP=3(ScrapyPriorityQueue)                      asyncio reactor:峰值 48 SelectReactor:峰值 3 无 reactor:峰值 48
  DOWNLOAD_SLOTS={"127.0.0.1": {"concurrency": 3}}   asyncio reactor:峰值 48 SelectReactor:峰值 3 无 reactor:峰值 48

它们设置的也是槽的 concurrency,所以有同样的现象;无 reactor 模式下它们也不起作用。

无 reactor 模式

实测 · 本机执行输出
    55  
    56      def __init__(self, crawler: Crawler):
    57          super().__init__(crawler)
    58          self._ssl_context: ssl.SSLContext = _make_ssl_context(crawler.settings)
    59          connector = aiohttp.TCPConnector(
    60              local_addr=self._bind_address,
    61              # hard limit on simultaneous connections
    62              limit=self._pool_size_total,
    63              # hard limit on simultaneous connections per host
    64              limit_per_host=self._pool_size_per_host,
    65          )
    66          self._session: aiohttp.ClientSession = aiohttp.ClientSession(

aiohttp 下载处理器自己的连接器带 limit 和 limit_per_host,所以每域名限制由它保证。

分步演示

并发限制
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。服务器端用线程服务器记录每个 Host 头(主机名)同时在处理的请求数的峰值;两个主机名用 127.0.0.1 和 localhost;为了看到槽的状态给 Downloader._download 打了补丁(只记录)
    • 没有验证:Scrapy 其它版本(2.18 及更早)是否有同样的现象,这是否已经在后续版本修复;启用 AutoThrottle 时的并发(下一批文章);真实网络(请求完成时间有抖动)下“一次放光”是否变轻;http11 下载处理器的连接池(maxPersistentPerHost)与此的关系;HTTP/2 处理器(见后面的文章)
    京ICP备2026067502号-1京公网安备11010502063487号