Когда нужно проверить доступность нескольких десятков или сотен сайтов, простая последовательная программа на requests быстро упирается в физический предел — так как сетевые операции занимают большую часть времени. Каждый HTTP-запрос блокирует выполнение, пока не придёт ответ, а процесс просто ждёт. Если таких запросов сотни, суммарное время проверки растёт пропорционально их количеству.
Типичные примеры — мониторинг сайтов, проверка зеркал, валидация ссылок или SEO-аудит. Проверка 500 доменов последовательно может занять минуты, хотя реальное время отклика каждого из них — доли секунды.
Асинхронность решает эту проблему за счёт одновременной обработки множества сетевых операций. Вместо того чтобы ждать каждый ответ по очереди, программа переключается между задачами, используя одно ядро процессора максимально эффективно. Python предоставляет для этого встроенные инструменты — asyncio и aiohttp. С их помощью можно проверять сотни сайтов одновременно, сокращая общее время выполнения в десятки раз.
Что такое асинхронность
Асинхронность — это способ организации кода, при котором программа не ждёт завершения одной операции, чтобы начать другую. В основе лежит цикл событий (event loop), который управляет множеством корутин — лёгких задач, выполняющихся параллельно, но в рамках одного потока. Когда одна корутина ждёт ответа от сети или диска, управление передаётся другой — так достигается высокая эффективность без создания десятков потоков.
В Python это реализовано через async и await. Функции, объявленные с async def, могут приостанавливать выполнение с помощью await, позволяя циклу событий переключаться на другие задачи. Это важно при выполнении I/O-bound задач — сетевых операций, работе с файлами, базами данных, API-запросами.
Асинхронность отличается от многопоточности и мультипроцессности:
- при многопоточности создаётся несколько потоков ОС, нужна синхронизация доступа к памяти;
- при мультипроцессности используются отдельные процессы с полной изоляцией;
- асинхронность же опирается на неблокирующий ввод-вывод, задачи выполняются по очереди, но без простоев.
В результате даже на слабом VPS можно одновременно обрабатывать сотни соединений без ощутимой нагрузки на CPU. Именно поэтому asyncio и aiohttp широко используются для написания сетевых утилит, API-клиентов и систем мониторинга.
Пример синхронной проверки сайтов
Чтобы понять разницу между синхронным и асинхронным выполнением запросов, проверим доступность сайтов с помощью библиотеки requests. Она выполняет запросы строго последовательно — пока не получен ответ от одного сайта, программа не перейдёт к следующему:
import requests
import time
urls = [
"https://example.ru",
"https://openai.com",
"https://python.org",
"https://github.com",
# здесь может быть список из сотен доменов
]
def check_url(url):
try:
response = requests.get(url, timeout=5)
return url, response.status_code
except requests.RequestException:
return url, None
start = time.perf_counter()
results = [check_url(url) for url in urls]
end = time.perf_counter()
for url, status in results:
print(f"{url} — {status}")
print(f"\nВремя выполнения: {end - start:.2f} секунд")
При 100–200 сайтах выполнение займёт от 30 секунд до нескольких минут — даже если большинство серверов отвечает быстро. Всё упирается в сетевую задержку: запросы выполняются один за другим, но из-за задержки процесс просто ждёт большую часть времени.
Такой способ хорош для единичных проверок, но он плохо масштабируется. Чем больше URL — тем дольше будет выполнение. Даже если 90% сайтов доступны мгновенно, программа будет простаивать при обращении к оставшимся 10%.
Асинхронная проверка с asyncio и aiohttp
Чтобы выполнять десятки или сотни запросов одновременно, используют встроенный модуль asyncio и библиотеку aiohttp, которая реализует асинхронный HTTP-клиент. Эти инструменты позволяют запускать множество сетевых операций параллельно в рамках одного потока, не создавая десятки потоков ОС.
Основные принципы:
- async def — объявление асинхронной функции (корутины);
- await — точка ожидания операции, где управление может перейти другой задаче;
- async with aiohttp.ClientSession() — контекст сессии, где создаются и переиспользуются соединения (это ускоряет работу);
- asyncio.Semaphore() — ограничение числа одновременных подключений, чтобы не перегружать сеть.
Пример:
import aiohttp
import asyncio
import time
urls = [
"https://example.ru",
"https://openai.com",
"https://python.org",
"https://github.com",
# список доменов
]
async def fetch(session, url):
try:
async with session.get(url, timeout=5) as response:
return url, response.status
except Exception:
return url, None
async def main():
semaphore = asyncio.Semaphore(50) # не более 50 запросов одновременно
async with aiohttp.ClientSession() as session:
async def limited_fetch(url):
async with semaphore:
return await fetch(session, url)
tasks = [asyncio.create_task(limited_fetch(url)) for url in urls]
return await asyncio.gather(*tasks)
start = time.perf_counter()
results = asyncio.run(main())
end = time.perf_counter()
for url, status in results:
print(f"{url} — {status}")
print(f"\nВремя выполнения: {end - start:.2f} секунд")
Теперь тот же список из 100–200 сайтов будет проверяться в 10–20 раз быстрее. Даже если каждый сайт будет отвечать за 1 секунду, суммарное время не превысит пары секунд, потому что запросы выполняются параллельно.
Ограничение через Semaphore необходимо: если запустить все запросы сразу, можно столкнуться с ошибками таймаута или блокировкой со стороны провайдеров. Оптимальное число одновременных подключений зависит от пропускной способности VPS и политики API.
Асинхронная модель незаменима в сетевых задачах — когда слабым местом является ожидание отклика, а не вычисления. Поэтому aiohttp — фактически стандарт для мониторинга, сканеров и сервисов, работающих с большим количеством URL.
Обработка ошибок и таймаутов
При массовой проверке сайтов ошибки — не исключение, а норма. Некоторые домены могут быть недоступны, серверы — перегружены, а DNS может не ответить вовремя. Если не предусмотреть обработку этих ситуаций, программа упадёт при первом сбое.
Типичные ошибки, с которыми сталкиваются при работе с aiohttp:
- asyncio.TimeoutError — сайт не ответил в течение заданного времени;
- aiohttp.ClientConnectorError — не удалось установить соединение (ошибка DNS или TCP);
- aiohttp.ClientResponseError — сервер вернул некорректный HTTP-ответ.
Чтобы сделать код устойчивым, следует обрабатывать исключения прямо внутри функции fetch() и при необходимости повторять попытку. Ниже — фрагмент с добавлением retry и ограничением времени ожидания:
import asyncio
import aiohttp
async def fetch(session, url, retries=3):
for attempt in range(retries):
try:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as response:
return url, response.status
except (aiohttp.ClientConnectorError, asyncio.TimeoutError):
if attempt < retries - 1:
await asyncio.sleep(0.5) # задержка между попытками
else:
return url, None
Теперь, если сайт не ответил сразу, запрос будет повторяться, но не бесконечно. Если сетевой сбой временный, то проверка завершится успешно.
Хорошая практика — логировать неудачные запросы для последующего анализа:
- записывать URL, статус, время и ошибку в файл или базу;
- использовать модуль logging вместо print() для контроля уровня сообщений;
- ограничивать общее время работы через aiohttp.ClientTimeout(total=5) — так программа не зависнет на долгих запросах.
Оптимизация и масштабирование
Когда код уже работает, следующий шаг — добиться от него максимальной производительности без перегрузки сети и CPU. Главный параметр, влияющий на скорость — количество одновременных задач (Semaphore).
Если поставить слишком низкий лимит, программа будет работать медленно, а если слишком высокий — начнутся таймауты и ошибки соединений. Оптимальное значение зависит от пропускной способности сети VPS и ограничений ОС (например, числа открытых файловых дескрипторов). Обычно устанавливают диапазон в 50–200 одновременных соединений.
Также важно выбрать подходящий способ вывода результатов:
- asyncio.gather() — собирает все результаты сразу, что удобно для коротких задач;
- asyncio.as_completed() — возвращает результаты по мере завершения, что позволяет обрабатывать данные в потоке (например, логировать ответы без ожидания).
Пример работы с as_completed():
tasks = [fetch(session, url) for url in urls]
for future in asyncio.as_completed(tasks):
url, status = await future
print(f"{url} — {status}")
Такой вариант уменьшает задержки, а за результатами можно следить прямо в процессе.
Для масштабных проверок (например, более 1000 сайтов) стоит добавить:
- сбор статистики по результатам — подсчёт кодов 200, 404, 500, среднего времени отклика;
- метрики производительности — общее время выполнения, количество ошибок;
- адаптивную настройку concurrency — если сеть перегружена, динамически снижать число одновременных запросов.
При проверке 1000 доменов с лимитом в 100 одновременных соединений и таймаутом 5 секунд выполнение занимает всего 8–10 секунд, тогда как синхронный вариант потребовал бы 10–15 минут. Но асинхронность даёт выигрыш не только во времени. Она позволяет масштабировать задачи вширь — добавлять тысячи URL без роста нагрузки на CPU. Всё, что ограничивает производительность в данном случае, — это скорость сети и параметры работы цикла событий.
Практическое применение
Асинхронная проверка доступности — реальный инструмент, который можно встроить в рабочие процессы:
- Мониторинг сайтов и API. Асинхронный скрипт легко превратить в мини-систему мониторинга, которая каждые несколько минут проверяет статус сайтов, REST API или внутренних сервисов. Можно логировать коды ответов, сохранять их в Prometheus или SQLite, а при отклонении от нормы — отправлять уведомление в мессенджеры.
- Проверка SSL-сертификатов. С помощью aiohttp и стандартного модуля ssl можно не только проверить доступность сайта, но и получить срок действия сертификата — полезно для DevOps-инфраструктуры и SaaS-сервисов с десятками доменов.
- Замер времени отклика. Добавив замер response_time = end - start, можно собрать статистику производительности по каждому сайту. Это помогает выявить, какие ресурсы стали медленнее отвечать, даже если они пока доступны.
- Интеграция с cron и CI/CD. Сценарий легко запускать по расписанию через cron или встроить в CI/CD-пайплайн, чтобы проверять доступность внешних зависимостей перед деплоем.
Заключение
Асинхронность в Python — синоним эффективности. Она позволяет выполнять сотни и тысячи сетевых операций параллельно, не блокируя CPU и не перегружая систему. asyncio и aiohttp дают возможность создавать лёгкие, быстрые и управляемые инструменты для мониторинга и тестирования сетевых ресурсов. Главное — придерживаться основных принципов:
- Не блокируйте цикл событий, используйте await везде, где есть ожидание.
- Обрабатывайте ошибки: должны быть предусмотрены retry, таймауты и логирование.
- Тестируйте код на небольшом списке URL, прежде чем запускать массовую проверку.
- Подберите лимит параллельных задач под пропускную способность сети.
- Используйте as_completed() для потоковой обработки результатов.
Асинхронный подход делает сетевые проверки быстрыми, надёжными и масштабируемыми. Для задач, где счёт идёт на сотни сайтов или API-запросов, это оптимальное решение, позволяющее сэкономить и время, и ресурсы.

