MAATRIX / Блог / Сколько параллельных парсеров запустить: где сеть, где процессор, а где бан

Сколько параллельных парсеров запустить: где сеть, где процессор, а где бан

MAATRIX

«Запусти в 50 потоков вместо 10 — будет в пять раз быстрее» — так рассуждают, пока не упрутся в стену. Иногда стена — это забитый канал, иногда — CPU, съеденный рендерингом JS, а чаще всего — целевой сайт, который после 30-го параллельного запроса просто начинает отвечать 429 или молча банит IP. Разберём, как понять, какой из трёх факторов ограничивает именно вашего парсера, и как подобрать параллелизм без угадывания.

Три разных узких места — и как их различить

Параллелизм парсера упирается всегда в один из трёх ресурсов, и лечится каждый по-своему. Если лечить не то, что сломано, — либо ничего не изменится, либо станет хуже.

  • Сеть и число соединений. Актуально для лёгких HTTP-парсеров: скачал HTML, разобрал регуляркой или простым парсером типа lxml/BeautifulSoup, пошёл дальше. Здесь узкое место — либо ширина канала (страницы весят много, трафика впритык), либо число одновременных TCP/TLS-соединений, которые может держать система и удалённый сервер.
  • CPU. Актуально для тяжёлого парсинга: разбор больших DOM-деревьев, регэкспы на мегабайтных страницах, а особенно — headless-браузер (Playwright, Puppeteer, Selenium) с полным рендерингом JS. Здесь каждый воркер — это процесс, который реально грузит ядро, и добавление потоков сверх числа ядер только плодит очередь.
  • Rate-limit и бан со стороны цели. Часто именно это — настоящий потолок, даже если у вас железо простаивает. Сайт видит 40 запросов в секунду с одного IP, решает, что это бот, и либо отдаёт капчу, либо банит адрес на час-сутки. В этом случае увеличение параллелизма не увеличивает скорость сбора данных, а увеличивает скорость получения бана.

Эти три ограничения не взаимоисключающие: парсер может упереться в CPU при рендеринге, а после решения этой проблемы — тут же в rate-limit цели. Диагностику стоит проводить по порядку — от простого к сложному.

Как диагностировать, где именно узкое место

Не гадайте — измеряйте прямо во время работы парсера.

Проверка сети. Во время прогона парсера смотрите загрузку канала и число соединений:

# загрузка интерфейса в реальном времени
iftop -i eth0

# альтернатива без интерактивного UI
vnstat -l -i eth0

# число активных TCP-соединений парсера
ss -tn state established | wc -l

# сколько соединений в TIME_WAIT (может быть само по себе узким местом)
ss -tn state time-wait | wc -l

Если канал утилизирован на 80-100% от заявленной полосы — упор в сеть, есть смысл смотреть в сторону более широкого канала или сжатия трафика (запрашивать Accept-Encoding: gzip и не тянуть картинки/шрифты, если нужен только текст).

Проверка CPU. Параллельно с парсером:

# общая загрузка по ядрам
mpstat -P ALL 1

# кто именно ест CPU
top -o %CPU

# для headless-браузера — сколько процессов реально запущено
ps aux | grep -c chrome

Если все ядра под 90-100% и в очереди load average заметно выше числа ядер — упор в CPU. Добавление потоков здесь не ускорит парсинг, а просто увеличит время ожидания в очереди планировщика — почти линейный рост throughput сменяется плато и затем деградацией. Природа этого эффекта разобрана в статье про точку перегиба у воркеров — она написана про универсальный случай, но логика 1:1 применима к парсерам.

Проверка бана/rate-limit. Тут метрика не системная, а прикладная — код ответа и латентность запросов к целевому сайту:

# считаем коды ответов за последний прогон (пример на логах curl/scrapy)
grep -oE 'status[\": ]+[0-9]{3}' scrapy.log | grep -oE '[0-9]{3}' | sort | uniq -c | sort -rn

Если доля 429/403/503 растёт с ростом параллелизма, а латентность отдельных запросов резко увеличивается — это сигнал throttling или soft-бана, даже если формального кода ошибки нет. Если после N успешных запросов подряд начинают идти сплошные ошибки — это не сеть и не CPU, это счётчик на сайте досчитал до лимита.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Лёгкие HTTP-парсеры: где реально упирается сеть

Для парсера на requests/httpx/aiohttp без браузера типичная ситуация — CPU почти не нагружен (разбор HTML — дешёвая операция), а узкое место — либо число одновременных соединений, либо задержка ответа (RTT) от целевого сервера.

Здесь работает простая арифметика: если один запрос занимает в среднем 300 мс и вы держите 20 параллельных соединений, теоретический потолок — около 65 запросов в секунду (20 / 0.3). Хотите больше — либо снижайте задержку (маловероятно, она на стороне цели), либо увеличивайте число параллельных соединений. Но у параллельных соединений есть свои границы:

  • Лимит файловых дескрипторов на уровне ОС и процесса — ulimit -n. По умолчанию часто 1024, для парсера с тысячами соединений его стоит поднять.
  • Диапазон эфемерных портов и TIME_WAIT — при очень высокой частоте коротких соединений можно упереться в исчерпание портов, если не используется keep-alive.
  • Асинхронность vs потокиasyncio+aiohttp держит тысячи соединений на одном ядре почти без CPU-накладных расходов, поток же (thread) стоит дороже по памяти и контекстным переключениям при том же числе одновременных соединений.
# поднять лимит дескрипторов для сессии
ulimit -n 65535

# постоянный лимит — в /etc/security/limits.conf
echo "* soft nofile 65535" >> /etc/security/limits.conf
echo "* hard nofile 65535" >> /etc/security/limits.conf

Практический ориентир (не измеренное число, а порядок величины, у вас будет иначе в зависимости от RTT до цели и веса страниц): для асинхронного HTTP-парсера без headless-рендеринга разумный старт — 20-50 одновременных соединений на один целевой домен, дальше рост параллелизма упирается уже не в вашу сеть, а в поведение цели.

Headless-браузер: где реально упирается CPU и память

Playwright или Puppeteer — это не «скачать HTML», это запустить полноценный движок рендеринга: разбор DOM, CSS, выполнение JS, иногда — загрузка шрифтов и изображений. Один инстанс headless Chrome в простое ест заметный объём RAM и периодически нагружает CPU при навигации; под параллельной нагрузкой десятки инстансов быстро съедают и то, и другое.

Здесь диагностика короче: смотрите mpstat и free -m во время прогона. Если CPU под потолком раньше, чем сеть или память, — упор именно в рендеринг, и решения такие:

  • Отключить лишнее. Если не нужны картинки/шрифты/видео — блокируйте их загрузку на уровне page.route (Playwright) или через CDP:
# Playwright: блокируем тяжёлые ресурсы, если нужен только текст/DOM
async def block_heavy(route):
    if route.request.resource_type in ("image", "font", "media"):
        await route.abort()
    else:
        await route.continue_()

await page.route("**/*", block_heavy)
  • Переиспользовать браузер, а не процесс. Один запущенный браузер с несколькими контекстами (browser.new_context()) дешевле, чем N отдельных процессов Chrome — общий движок рендеринга, разные cookies/сессии на уровне контекста.
  • Не рендерить, если можно не рендерить. Часть сайтов отдают нужные данные через внутренний API (смотрите вкладку Network в devtools) — прямой запрос к JSON-эндпоинту на порядки дешевле по CPU, чем полный рендеринг страницы браузером.
  • Ограничивать число одновременных инстансов явно, а не полагаться на то, что ОС сама разрулит — например, через семафор в коде или пул воркеров фиксированного размера, равного примерно числу физических ядер минус 1-2 для остальных процессов системы.

Отдельная головная боль headless-браузеров — эфемерные профили во временных файлах, способные забить диск быстрее, чем ожидаешь, при долгих прогонах: разбор похожего инцидента есть в статье Headless Chrome съел 300 ГБ временными файлами за одну ночь.

Настоящий потолок: rate-limit и бан со стороны цели

Даже если у вас свободны и сеть, и CPU — сайт может ограничивать вас раньше, чем упрётся ваше железо. Как именно это устроено с точки зрения защищающейся стороны, разобрано в статье как работает rate limiting: обычно это счётчик запросов с одного IP (или IP+User-Agent, или отпечатка TLS) за скользящее окно времени.

Признаки, что вы упёрлись именно в это, а не в технику:

  • Растёт доля кодов 429 («Too Many Requests») или 403 при неизменной нагрузке на CPU/сеть.
  • Появляется капча вместо ожидаемого контента.
  • Ответы приходят, но с «подозрительно круглой» задержкой (сайт искусственно тормозит подозрительные IP) — это soft-throttling, часто без явного кода ошибки.
  • IP улетает в бан целиком — новые запросы обрываются на уровне TCP или отдают заглушку для всех URL подряд, не только для парсинга.

Здесь параллелизм — не решение, а часть проблемы: чем больше одновременных запросов с одного адреса, тем быстрее счётчик цели доходит до порога. Лечится снижением интенсивности и распределением нагрузки — следующий раздел.

Задержки, ротация IP и User-Agent — зачем и как правильно

Прежде чем городить обход — важная оговорка про легальность. Речь ниже — только про сбор публично доступных данных без обхода авторизации и без нарушения условий использования сайта. Обязательно проверяйте robots.txt целевого ресурса и Terms of Service: если сайт явно запрещает автоматический сбор конкретных разделов, это не техническая, а юридическая граница, и ротация IP её не отменяет. robots.txt — это заявленная сайтом политика, но даже там, где он формально это разрешает, разумно не грузить чужой сервер сверх меры и уважать Crawl-delay, если он указан.

Задержки между запросами. Простейший и часто самый эффективный инструмент — не бить по серверу максимально часто, а выдерживать паузу:

import random
import time

# случайная задержка вместо фиксированной — фиксированный интервал
# легко отличим от поведения человека по дисперсии
def polite_delay(base=1.5, jitter=0.7):
    time.sleep(base + random.uniform(0, jitter))

Фиксированная задержка (sleep(1) каждый раз) — это тоже сигнал бота: у человека и у браузера с реальным пользователем интервалы между запросами случайны. Небольшой джиттер снижает заметность, но не панацея — многие защиты смотрят не только на интервалы, а на паттерн запросов в целом.

Ротация User-Agent. Смысл не в том, чтобы притвориться конкретным браузером, а в том, чтобы не отправлять с одного IP тысячи запросов с одной и той же строкой UA — это тривиальный признак для группировки трафика. Ротация должна быть правдоподобной: смесь актуальных строк реальных браузеров/версий, а не случайный набор символов.

Ротация IP через прокси. Когда лимит цели считается по IP, распределение запросов между несколькими адресами снимает давление с каждого отдельного адреса. Практические варианты:

  • Собственный пул VPS в разных подсетях/локациях — дороже в администрировании, но полностью под вашим контролем и без вопросов к происхождению трафика.
  • Прокси-провайдер (датацентровые или резидентные IP) — быстрее развернуть, но резидентные пулы стоят заметно дороже датацентровых и качество/легальность конкретного пула стоит проверять у поставщика.

Как поднять собственный прокси-слой на VPS (3proxy/Squid, авторизация, базовая ротация) — пошагово разобрано в статье про настройку прокси для парсинга на VPS. Наличие пула IP не отменяет задержек между запросами: ротация IP и разумный темп работают вместе, а не вместо друг друга — 10 IP, с каждого из которых долбят на максимальной скорости, банятся почти так же быстро, как один.

Практическая методика подбора параллелизма

Вместо того чтобы гадать число потоков, найдите его пошаговым тестом.

  1. Стартуйте с малого параллелизма — 1-5 одновременных запросов/воркеров — и зафиксируйте базовые метрики: время на страницу, загрузку CPU, загрузку канала, долю ошибок.
  2. Увеличивайте параллелизм ступенями (например, ×2 на каждом шаге: 5 → 10 → 20 → 40) и на каждой ступени держите нагрузку достаточно долго, чтобы rate-limit цели успел проявиться — иногда он срабатывает не сразу, а после нескольких минут устойчиво высокой частоты.
  3. На каждой ступени фиксируйте три числа: суммарный throughput (страниц/сек), загрузку самого узкого системного ресурса (CPU или канал в %), долю ошибочных/подозрительных ответов (429/403/капча/аномальная задержка).
  4. Останавливайтесь на ступени, где начинает расти доля ошибок, даже если системные ресурсы (CPU, сеть) ещё не исчерпаны — это и есть реальный потолок, продиктованный целью, а не железом.
  5. Если ошибки не растут, а throughput перестал расти или упал — вы нашли системное узкое место (CPU или сеть по данным диагностики выше); дальше есть смысл либо добавлять ресурсы (больше ядер, шире канал), либо оптимизировать код (asyncio вместо потоков, отключить рендеринг лишнего).
  6. Возьмите ступень чуть ниже найденного потолка, а не впритык к нему — оставьте запас, потому что поведение цели может меняться (другое время суток, обновление защиты) и вчерашний безопасный уровень параллелизма завтра может начать банить.

Для парсинга действительно большого объёма данных стоит заранее прикинуть, во что это выльется по времени и стоимости инфраструктуры — методика расчёта разобрана в статье во сколько обходится парсинг миллиона страниц: при известном найденном потолке throughput пересчитать нужное время и число серверов — уже арифметика.

Таблица — сводка, с чего начинать по типу парсера:

Тип парсераОбычное узкое местоС чего стартоватьКуда смотреть при диагностике
Лёгкий HTTP (requests/httpx)Сеть/RTT/дескрипторы20-50 одновременных на доменss, ulimit -n, доля ошибок
Асинхронный HTTP (aiohttp/asyncio)RTT цели, реже CPU50-200 корутин на доменmpstat, латентность ответов
Headless-браузер (Playwright/Puppeteer)CPU и RAMЧисло физических ядер минус 1-2mpstat, free -m, число процессов
Любой при агрессивном темпеRate-limit/бан целиНачинать с задержек, расширять осторожноКоды 429/403, капча, аномальная задержка

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли просто взять сервер с большим числом ядер и не думать об узком месте?

Только если узкое место — CPU (headless-браузер). Если ограничение — rate-limit цели, дополнительные ядра не помогут вообще: сайт банит по IP и темпу запросов, а не по мощности вашего железа.

Асинхронность (asyncio) всегда лучше потоков для парсинга?

Для I/O-bound задач (ждём ответ сервера) — как правило да, она держит намного больше одновременных соединений при том же расходе CPU и памяти. Для CPU-bound задач (тяжёлый парсинг DOM, рендеринг) выигрыш меньше — там определяющим остаётся число ядер.

Как отличить временный rate-limit от постоянного бана IP?

Rate-limit обычно снимается сам через минуты-часы и часто явно возвращает код 429 с заголовком Retry-After. Постоянный бан — это когда даже после длительной паузы и снижения темпа запросы с этого IP продолжают получать отказ; тогда единственный выход — сменить исходящий адрес.

Нужно ли уважать Crawl-delay в robots.txt, если технически можно ходить чаще?

Да — это явно заявленная сайтом граница вежливого поведения, и её игнорирование увеличивает риск бана даже при формально разрешённом доступе к разделу.

Что делать, если после каждого повышения параллелизма растёт именно латентность, а не ошибки?

Это часто ранний признак throttling до того, как он проявится в кодах ошибок — сайт держит вас в очереди дольше обычного. Стоит остановиться на предыдущей ступени параллелизма, не дожидаясь явных 429.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →