Create a page on optimization (#7938)

This commit is contained in:
Adrian 2026-08-09 18:37:24 +02:00 committed by GitHub
parent 8e94df6526
commit 5427080f48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 366 additions and 198 deletions

View File

@ -31,9 +31,14 @@ extensions = [
"sphinx_scrapy",
"scrapyfixautodoc", # Must be after "sphinx.ext.autodoc"
"sphinx.ext.coverage",
"sphinx_reredirects",
"sphinx_rtd_dark_mode",
]
redirects = {
"topics/broad-crawls": "optimize.html#broad-crawls",
}
templates_path = ["_templates"]
exclude_patterns = ["build", "Thumbs.db", ".DS_Store"]

View File

@ -152,7 +152,7 @@ Solving specific problems
topics/contracts
topics/practices
topics/security
topics/broad-crawls
topics/optimize
topics/developer-tools
topics/dynamic-content
topics/leaks
@ -180,8 +180,8 @@ Solving specific problems
Understand the security implications of Scrapy defaults and how to harden
them.
:doc:`topics/broad-crawls`
Tune Scrapy for crawling a lot domains in parallel.
:doc:`topics/optimize`
Find the bottleneck of your crawls and learn how to address it.
:doc:`topics/developer-tools`
Learn how to scrape with your browser's developer tools.

View File

@ -3,6 +3,7 @@ pydantic
scrapy-spider-metadata
sphinx
sphinx-notfound-page
sphinx-reredirects
sphinx-rtd-theme
sphinx-rtd-dark-mode
sphinx-scrapy @ git+https://github.com/scrapy/sphinx-scrapy.git@0.8.10

View File

@ -134,6 +134,7 @@ sphinx==9.1.0
# sphinx-llms-txt
# sphinx-markdown-builder
# sphinx-notfound-page
# sphinx-reredirects
# sphinx-rtd-theme
# sphinx-scrapy
# sphinxcontrib-jquery
@ -147,6 +148,8 @@ sphinx-markdown-builder @ git+https://github.com/zytedata/sphinx-markdown-builde
# via sphinx-scrapy
sphinx-notfound-page==1.1.0
# via -r docs/requirements.in
sphinx-reredirects==1.1.0
# via -r docs/requirements.in
sphinx-rtd-dark-mode==1.3.0
# via -r docs/requirements.in
sphinx-rtd-theme==3.1.0

View File

@ -1,195 +0,0 @@
.. _topics-broad-crawls:
============
Broad Crawls
============
Scrapy defaults are optimized for crawling specific sites. These sites are
often handled by a single Scrapy spider, although this is not necessary or
required (for example, there are generic spiders that handle any given site
thrown at them).
In addition to this "focused crawl", there is another common type of crawling
which covers a large (potentially unlimited) number of domains, and is only
limited by time or other arbitrary constraint, rather than stopping when the
domain was crawled to completion or when there are no more requests to perform.
These are called "broad crawls" and is the typical crawlers employed by search
engines.
These are some common properties often found in broad crawls:
* they crawl many domains (often, unbounded) instead of a specific set of sites
* they don't necessarily crawl domains to completion, because it would be
impractical (or impossible) to do so, and instead limit the crawl by time or
number of pages crawled
* they are simpler in logic (as opposed to very complex spiders with many
extraction rules) because data is often post-processed in a separate stage
* they crawl many domains concurrently, which allows them to achieve faster
crawl speeds by not being limited by any particular site constraint (each site
is crawled slowly to respect politeness, but many sites are crawled in
parallel)
As said above, Scrapy default settings are optimized for focused crawls, not
broad crawls. However, due to its asynchronous architecture, Scrapy is very
well suited for performing fast broad crawls. This page summarizes some things
you need to keep in mind when using Scrapy for doing broad crawls, along with
concrete suggestions of Scrapy settings to tune in order to achieve an
efficient broad crawl.
.. _broad-crawls-scheduler-priority-queue:
.. _broad-crawls-concurrency:
Increase concurrency
====================
Concurrency is the number of requests that are processed in parallel. There is
a global limit (:setting:`CONCURRENT_REQUESTS`) and an additional limit that
can be set per domain (:setting:`CONCURRENT_REQUESTS_PER_DOMAIN`).
The default global concurrency limit in Scrapy is not suitable for crawling
many different domains in parallel, so you will want to increase it. How much
to increase it will depend on how much CPU and memory your crawler will have
available.
A good starting point is ``100``:
.. code-block:: python
CONCURRENT_REQUESTS = 100
But the best way to find out is by doing some trials and identifying at what
concurrency your Scrapy process gets CPU bounded. For optimum performance, you
should pick a concurrency where CPU usage is at 80-90%.
Increasing concurrency also increases memory usage. If memory usage is a
concern, you might need to lower your global concurrency limit accordingly.
Increase Twisted IO thread pool maximum size
============================================
Currently Scrapy does DNS resolution in a blocking way with usage of thread
pool. With higher concurrency levels the crawling could be slow or even fail
hitting DNS resolver timeouts. Possible solution to increase the number of
threads handling DNS queries. The DNS queue will be processed faster speeding
up establishing of connection and crawling overall.
To increase maximum thread pool size use:
.. code-block:: python
REACTOR_THREADPOOL_MAXSIZE = 20
Setup your own DNS
==================
If you have multiple crawling processes and single central DNS, it can act
like DoS attack on the DNS server resulting to slow down of entire network or
even blocking your machines. To avoid this setup your own DNS server with
local cache and upstream to some large DNS like OpenDNS or Verizon.
Reduce log level
================
When doing broad crawls you are often only interested in the crawl rates you
get and any errors found. These stats are reported by Scrapy when using the
``INFO`` log level. In order to save CPU (and log storage requirements) you
should not use ``DEBUG`` log level when performing large broad crawls in
production. Using ``DEBUG`` level when developing your (broad) crawler may be
fine though.
To set the log level use:
.. code-block:: python
LOG_LEVEL = "INFO"
Disable cookies
===============
Disable cookies unless you *really* need. Cookies are often not needed when
doing broad crawls (search engine crawlers ignore them), and they improve
performance by saving some CPU cycles and reducing the memory footprint of your
Scrapy crawler.
To disable cookies use:
.. code-block:: python
COOKIES_ENABLED = False
Disable retries
===============
Retrying failed HTTP requests can slow down the crawls substantially, especially
when sites causes are very slow (or fail) to respond, thus causing a timeout
error which gets retried many times, unnecessarily, preventing crawler capacity
to be reused for other domains.
To disable retries use:
.. code-block:: python
RETRY_ENABLED = False
Reduce download timeout
=======================
Unless you are crawling from a very slow connection (which shouldn't be the
case for broad crawls) reduce the download timeout so that stuck requests are
discarded quickly and free up capacity to process the next ones.
To reduce the download timeout use:
.. code-block:: python
DOWNLOAD_TIMEOUT = 15
Disable redirects
=================
Consider disabling redirects, unless you are interested in following them. When
doing broad crawls it's common to save redirects and resolve them when
revisiting the site at a later crawl. This also help to keep the number of
request constant per crawl batch, otherwise redirect loops may cause the
crawler to dedicate too many resources on any specific domain.
To disable redirects use:
.. code-block:: python
REDIRECT_ENABLED = False
.. _broad-crawls-bfo:
Crawl in BFO order
==================
:ref:`Scrapy crawls in DFO order by default <faq-bfo-dfo>`.
In broad crawls, however, page crawling tends to be faster than page
processing. As a result, unprocessed early requests stay in memory until the
final depth is reached, which can significantly increase memory usage.
:ref:`Crawl in BFO order <faq-bfo-dfo>` instead to save memory.
Be mindful of memory leaks
==========================
If your broad crawl shows a high memory usage, in addition to :ref:`crawling in
BFO order <broad-crawls-bfo>`, :ref:`lowering concurrency
<broad-crawls-concurrency>` and :ref:`delaying start request iteration
<start-requests-lazy>` you should :ref:`debug your memory leaks
<topics-leaks>`.
Install a specific Twisted reactor
==================================
If the crawl is exceeding the system's capabilities, you might want to try
installing a specific Twisted reactor, via the :setting:`TWISTED_REACTOR` setting.

353
docs/topics/optimize.rst Normal file
View File

@ -0,0 +1,353 @@
.. _optimize:
============
Optimization
============
A crawl goes as fast as its slowest part allows. :ref:`Find out which part that
is <optimize-bottleneck>` before changing any setting.
:ref:`Broad crawls <broad-crawls>` have their own set of recommended
adjustments.
.. _optimize-bottleneck:
Finding the bottleneck
======================
The bottleneck depends on the spider: on the same machine, one crawl can be
limited by its own parsing code and another by the target website. So measure
the crawl that you want to optimize.
:class:`~scrapy.extensions.logstats.LogStats` reports crawl speed every
:setting:`LOGSTATS_INTERVAL` seconds:
.. code-block:: text
[scrapy.extensions.logstats] INFO: Crawled 1200 pages (at 60 pages/min), scraped 1150 items (at 58 items/min)
A rate that stays flat as you raise :setting:`CONCURRENT_REQUESTS` means
something else is the limit.
Reading the engine status
-------------------------
The :ref:`telnet console <topics-telnetconsole>` reports, through ``est()``,
what every part of the engine is doing at a given moment:
.. code-block:: text
len(engine.downloader.active) : 16
len(engine._slot.scheduler.mqs) : 92
len(engine.scraper.slot.active) : 0
engine.scraper.slot.active_size : 0
engine.scraper.slot.needs_backout() : False
Take a few readings at different points of the crawl:
- ``len(engine.downloader.active)`` stays at :setting:`CONCURRENT_REQUESTS`:
the downloader is the limit. You are waiting on the network or on the
target website. See :ref:`optimize-concurrency`.
- ``len(engine.downloader.active)`` stays below
:setting:`CONCURRENT_REQUESTS` while the scheduler queues (``mqs``,
``dqs``) hold requests: something throttles those requests before they
reach the downloader, usually :setting:`CONCURRENT_REQUESTS_PER_DOMAIN`,
:setting:`DOWNLOAD_DELAY` or :ref:`AutoThrottle <topics-autothrottle>`.
- Both the downloader and the scheduler queues stay near empty: your spider
is not producing requests fast enough. A crawl that walks pagination one
page at a time cannot use more concurrency than it creates. See
:ref:`optimize-requests`.
- ``needs_backout()`` is ``True``, or ``active_size`` approaches
:setting:`SCRAPER_SLOT_MAX_ACTIVE_SIZE`: responses arrive faster than your
callbacks and :ref:`item pipelines <topics-item-pipeline>` handle them. The
bottleneck is your own code.
- ``len(engine._slot.scheduler.mqs)`` grows without settling: the crawl
discovers requests faster than it downloads them. This is what makes long
crawls run out of memory.
Reading resource usage
----------------------
CPU
Scrapy runs in a single process, and everything except DNS resolution and
code you explicitly move to a thread runs in a single thread. One CPU core
is the ceiling; a process sitting at 100% of a core is CPU-bound no matter
how many cores the machine has.
Use a sampling profiler, such as py-spy_, to find out which code is
spending that CPU. :ref:`Selectors <topics-selectors>` and item pipelines
are the usual answer.
.. _py-spy: https://github.com/benfred/py-spy
Memory
The :ref:`memory usage extension <topics-extensions-ref-memusage>` records
:stat:`memusage/startup` and :stat:`memusage/max`. A :stat:`memusage/max`
far above :stat:`memusage/startup` is expected; what matters is whether it
keeps growing for as long as the crawl runs.
Growth that tracks ``len(engine._slot.scheduler.mqs)`` is a scheduling
problem, covered in :ref:`optimize-memory`. Growth that does not is a
:ref:`memory leak <topics-leaks>`.
Network
Compare :stat:`downloader/response_bytes` over the crawl time against your
available bandwidth. Saturated bandwidth caps concurrency regardless of any
setting.
DNS resolution is separate: it runs on a thread pool of
:setting:`REACTOR_THREADPOOL_MAXSIZE` threads, and results are cached
(:setting:`DNSCACHE_ENABLED`, :setting:`DNSCACHE_SIZE`). It only becomes a
limit of its own when there are many different domains to resolve, as in
:ref:`broad crawls <broad-crawls>`, where it shows up as slow starts and
DNS timeouts.
Disk
:ref:`Feed exports <topics-feed-exports>` write to disk on most crawls,
although item data is usually small enough for that not to matter. The ones
to suspect are
:class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware` and
the :ref:`media pipelines <topics-media-pipeline>`, which write whole
responses, and :setting:`JOBDIR`, which writes every scheduled request.
.. _optimize-concurrency:
Sending more requests at a time
===============================
:setting:`CONCURRENT_REQUESTS` caps how many requests are being downloaded at
any given moment, :setting:`CONCURRENT_REQUESTS_PER_DOMAIN` caps how many of
those may target the same domain, and :setting:`DOWNLOAD_DELAY` sets a minimum
wait between two consecutive requests to the same domain. A project generated by
:command:`startproject` gets one request per second per domain out of these.
Raise them to crawl a single website faster, and see
:ref:`broad-crawls-concurrency` to spread requests across many websites
instead.
The limit that matters, though, is the one the target website tolerates.
Exceeding it gets you throttled, served errors or banned, all of which make the
crawl slower than a lower concurrency would have been. To find that limit:
- Read the :ref:`robots.txt <topics-dlmw-robots>` file of the website. Scrapy
does not act on its ``Crawl-delay`` and ``Request-rate`` directives, so when
they are present, translate them into :setting:`DOWNLOAD_DELAY` and
concurrency settings yourself.
- Check the traffic that the website already gets, using a service like
`SimilarWeb`_ or `Cloudflare Radar`_. A rate that is a rounding error next
to what the website serves anyway is unlikely to be a problem for it.
.. _SimilarWeb: https://www.similarweb.com/
.. _Cloudflare Radar: https://radar.cloudflare.com/
- Look for a documented way in. An API, a bulk export or a search endpoint is
both faster for you and cheaper for the website than crawling its pages, and
the terms of service may state a rate.
- Crawl when the website is idle, in its own timezone, so that the capacity
you take is capacity nobody else wanted.
- Raise concurrency gradually and watch the website respond.
:stat:`downloader/response_status_count/{status_code}` counts for 429, 503
or the ban page of the website, growing :stat:`retry/count`, or a
:ref:`download latency <download-latency>` that climbs as you push harder,
all mean you have gone past the limit.
.. _optimize-requests:
Producing requests faster
=========================
A spider that discovers its requests one response at a time keeps the
downloader idle no matter how high you set :setting:`CONCURRENT_REQUESTS`. To
put more requests in the scheduler earlier:
- Request every page at once when you can work out how many there are, e.g.
from a page count or from a result count and a page size in the first
response, instead of following a link to the next page on every response.
- Get URLs from a source that lists many of them at once, such as a sitemap
or a search or export endpoint of the target website. For a crawl that
needs nothing else, :class:`~scrapy.spiders.SitemapSpider` reads sitemaps
for you.
- Raise the :attr:`~scrapy.Request.priority` of pagination requests, so that
they are downloaded before the requests that they compete with, and
discover the rest of the crawl sooner.
Each of these trades memory for speed: a request produced before the downloader
can take it waits in the scheduler, or on disk if you set :setting:`JOBDIR`.
Pushed far enough, they turn memory or disk into your new bottleneck, which is
why :ref:`optimize-memory` recommends the reverse of the last point.
.. _optimize-resources:
Lowering resource usage
=======================
.. _optimize-memory:
Lowering memory usage
---------------------
- Lower :setting:`SCRAPER_SLOT_MAX_ACTIVE_SIZE`.
- Lower :setting:`DOWNLOAD_MAXSIZE`, which allows a single response to take up
to 1 GiB of memory by default, multiplied by your concurrency. Set
:setting:`DOWNLOAD_WARNSIZE` first to find out whether the website actually
serves responses that big.
- Lower the number of :ref:`scheduled requests <topics-scheduler>` held in
memory:
- Increase the :attr:`~scrapy.Request.priority` of requests whose
:attr:`~scrapy.Request.callback` cannot yield additional requests.
For example, the following spider uses a higher priority (1) for book
requests than for pagination requests:
.. code-block:: python
from scrapy import Spider
class BooksToScrapeComSpider(Spider):
name = "books_toscrape_com"
start_urls = [
"http://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
]
def parse(self, response):
next_page_links = response.css(".next a")
yield from response.follow_all(next_page_links)
book_links = response.css("article a")
yield from response.follow_all(book_links, callback=self.parse_book, priority=1)
def parse_book(self, response):
yield {
"name": response.css("h1::text").get(),
"price": response.css(".price_color::text").re_first("£(.*)"),
"url": response.url,
}
.. note:: If the number of request-yielding, low-priority requests
scheduled at any given time is lower than concurrency settings
(:setting:`CONCURRENT_REQUESTS_PER_DOMAIN` or
:setting:`CONCURRENT_REQUESTS`), as in the example above, this can
slow down your crawl by turning those requests into a bottleneck.
- If you have many :ref:`start requests <start-requests>`, consider
:ref:`delaying their iteration <start-requests-lazy>`.
- Set :setting:`JOBDIR` to offload all scheduled requests to disk.
- Be on the lookout for :ref:`memory leaks <topics-leaks>`.
Lowering network usage
----------------------
- Install brotli_ and zstandard_ to support brotli-compressed_ and
zstd-compressed_ responses.
.. _brotli-compressed: https://www.ietf.org/rfc/rfc7932.txt
.. _brotli: https://pypi.org/project/Brotli/
.. _zstd-compressed: https://www.ietf.org/rfc/rfc8478.txt
.. _zstandard: https://pypi.org/project/zstandard/
- Enable :class:`~scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware`
while developing your spider, so that re-runs do not download the same
responses again.
Lowering CPU usage
------------------
- Set :setting:`LOG_LEVEL` to ``"INFO"`` or higher.
- Restrict what you parse. A :ref:`selector <topics-selectors>` over a
smaller part of the response, or a single query whose result you reuse,
beats repeated queries over the whole document.
Other tips
----------
- Try :ref:`using the asyncio reactor <install-asyncio>` with uvloop_ as
:ref:`custom event loop <using-custom-loops>`, i.e. setting
:setting:`ASYNCIO_EVENT_LOOP` to ``"uvloop.Loop"``.
.. _uvloop: https://github.com/MagicStack/uvloop
Alternatively, try :ref:`switching to a non-asyncio reactor
<disable-asyncio>`.
- Disable unused :ref:`components <topics-components>`.
For example, set :setting:`COOKIES_ENABLED` to ``False`` unless you need
cookies.
- Split the crawl across separate processes to use more than one CPU core.
See :ref:`distributed-crawls`.
.. _broad-crawls:
.. _topics-broad-crawls:
Speeding up broad crawls
========================
While Scrapy is well suited for **broad crawls**, i.e. crawls that target many
websites, the default :ref:`settings <topics-settings>` are optimized for
crawls targeting a single website.
For broad crawls, consider these adjustments:
- .. _broad-crawls-concurrency:
Increase the global concurrency:
- Set :setting:`CONCURRENT_REQUESTS` as close to
:setting:`CONCURRENT_REQUESTS_PER_DOMAIN` × [number of target domains]
(e.g. 8 × 10 domains = 80 concurrent requests) as your CPU and memory
allow.
- Increase :setting:`SCRAPER_SLOT_MAX_ACTIVE_SIZE` when increasing
:setting:`CONCURRENT_REQUESTS` stops making a difference.
- .. _broad-crawls-bfo:
If memory is a bottleneck, see if :ref:`crawling in BFO order <bfo>` lowers
memory usage.
- Improve DNS resolution speed:
- Set up your own DNS server, with a local cache and upstream to a `large
DNS server`_, to avoid slowing down your network.
.. _large DNS server: https://en.wikipedia.org/wiki/Public_recursive_name_server#Notable_public_DNS_service_operators
- Increase :setting:`REACTOR_THREADPOOL_MAXSIZE` to the minimum value
that avoids DNS resolution timeouts and makes a noticeable positive
impact in crawl speed.
- Lower the negative impact of some responses:
- Set :setting:`RETRY_ENABLED` to ``False`` or, if you need retries,
consider lowering :setting:`RETRY_TIMES`.
- Lower :setting:`DOWNLOAD_TIMEOUT` to a more reasonable value, to
discard stuck requests more quickly.
- Set :setting:`REDIRECT_ENABLED` to ``False`` unless you want to follow
redirects.

View File

@ -1913,6 +1913,7 @@ Type of in-memory queue used by the scheduler. Other available type is:
.. setting:: SCHEDULER_PRIORITY_QUEUE
.. _broad-crawls-scheduler-priority-queue:
SCHEDULER_PRIORITY_QUEUE
------------------------