Асинхронность в Python
Теория: Работа с сетью
Асинхронные HTTP-клиенты (aiohttp, httpx)
aiohttp
Работа с сетью — одно из главных применений асинхронности. Когда приложение делает запросы к веб-серверам, оно большую часть времени ждёт ответа. В таких сценариях синхронные решения могут работать медленно, так как блокируют выполнение программы.
Асинхронный HTTP-клиент позволяет отправлять десятки и сотни запросов одновременно, не блокируя выполнение программы. В Python самым распространённым инструментом для этого является библиотека aiohttp.
Она построена на asyncio и поддерживает все принципы асинхронного программирования. Библиотека позволяет выполнять HTTP-запросы без блокировки, управлять соединениями, заголовками, телом запроса, обрабатывать ответы и даже работать с потоковыми данными. Главная идея — каждый сетевой вызов является корутиной, и выполнение других задач не приостанавливается во время ожидания отклика сервера.
Для отправки запроса в aiohttp используется клиентская сессия — объект, который управляет подключениями и хранит общие параметры. Сессия переиспользует TCP-сокеты к одному хосту, поэтому важно создавать одну сессию на несколько запросов. Она создаётся при помощи async with, что гарантирует корректное закрытие соединений после завершения работы. Такой подход позволяет избежать утечек ресурсов и делает код безопасным.
Простейший пример:
Когда выполняется session.get(), создаётся запрос, который не блокирует цикл событий (event loop). Корутина await response.text() возвращает управление до тех пор, пока сервер не пришлёт ответ. Как только данные получены, они преобразуются в строку.
Помимо .text(), у объекта ответа есть методы .json() и .read(). Первый возвращает данные как Python-объект, если сервер ответил JSON-структурой, второй — необработанные байты, что удобно для скачивания файлов или изображений.
Пример с обработкой JSON-ответа:
Работа с aiohttp не ограничивается только GET-запросами. Для отправки данных используется метод post(), который принимает аргумент data или json. Например, чтобы передать JSON-тело на сервер:
Использование параметра json позволяет aiohttp автоматически преобразовать объект Python в JSON и установить правильный заголовок Content-Type: application/json. При необходимости можно добавить собственные заголовки, например токен авторизации или ключ API, передав их через параметр headers. Для добавления query-параметров используется аргумент params, который автоматически кодирует словарь в строку запроса.
Асинхронность aiohttp позволяет обрабатывать большие объёмы данных потоково. Иногда ответ слишком большой, чтобы загружать его целиком в память, например при скачивании видеофайлов или логов. В этом случае можно читать поток частями с помощью response.content.iter_chunked():
Здесь файл записывается по мере поступления данных, без ожидания загрузки всего ответа. Это не только экономит память, но и позволяет обрабатывать поток в реальном времени.
httpx
Асинхронный HTTP-клиент httpx — это современная библиотека для работы с сетью, которая сочетает простоту requests и преимущества асинхронного программирования. В отличие от aiohttp, где интерфейс более низкоуровневый и близок к самому asyncio, httpx предлагает лаконичный и привычный API, похожий на классический requests, но с поддержкой async/await. Это делает его удобным инструментом для построения асинхронных приложений, микросервисов и API-клиентов.
Главное преимущество httpx — универсальность: библиотека может работать как в синхронном, так и в асинхронном режиме. Это значит, что код, написанный для requests, может быть легко адаптирован под асинхронное выполнение без полной переработки. Если используется asyncio, то просто нужно создавать AsyncClient вместо обычного клиента, а вызовы методов выполнять через await. Такой подход снижает порог вхождения и позволяет быстро переходить от синхронных HTTP-запросов к конкурентной работе с сетью.
Работа с httpx начинается с создания клиента. Как и в aiohttp, используется контекст async with:
Результат запроса представлен объектом Response с теми же свойствами, что и в requests.
Параллельное выполнение запросов реализуется так же, как в aiohttp, через asyncio.gather().
Одно из преимуществ httpx заключается в том, что он изначально построен поверх стандарта HTTP/1.1 и поддерживает HTTP/2 без дополнительных расширений. Это значит, что при необходимости клиент может использовать мультиплексирование соединений — несколько запросов через одно TCP-соединение, что особенно эффективно при обращении к одному серверу.
Кроме стандартных GET-запросов, httpx поддерживает все остальные методы HTTP: POST, PUT, DELETE, PATCH, OPTIONS. Для передачи данных используется параметр data, а для отправки JSON-структур — json:
Заголовки можно передать через параметр headers как при инициализации клиента, так и при каждом запросе:
httpx также поддерживает работу с параметрами запроса. Для добавления query-параметров используется аргумент params, который автоматически кодирует словарь в строку запроса:
При работе с большими ответами, например при загрузке файлов, важно не загружать всё содержимое в память сразу. В httpx можно читать поток данных по частям через aiter_bytes(). Это даёт возможность сохранять файл постепенно, не блокируя другие операции:
Режим stream() позволяет считывать данные последовательно, не загружая весь ответ в память.
Обработка ошибок в httpx проста и безопасна. Все сетевые исключения наследуются от базового httpx.RequestError, что упрощает их перехват. Если сервер вернул код ошибки, например 404 или 500, библиотека не выбрасывает исключение по умолчанию, но можно вызвать response.raise_for_status(), чтобы явно проверить корректность ответа:
Клиент в httpx поддерживает пул соединений, переиспользуя TCP-соединения к одному серверу. Подробнее об этом в следующем разделе.
Таймауты и пулы соединений
Таймауты
В httpx таймауты задаются через объект httpx.Timeout. Он позволяет указать отдельные значения для разных стадий сетевого обмена. Например, connect — время ожидания установки TCP-соединения, read — время ожидания чтения данных, write — отправки тела запроса, а pool — время ожидания свободного соединения из пула:
Если указать только одно значение timeout=5.0, оно будет применяться ко всем этапам сразу. Таймаут срабатывает не мгновенно, а при следующей операции ввода-вывода. Это значит, что если сервер уже прислал часть данных, клиент не оборвёт соединение до тех пор, пока не произойдёт попытка чтения или записи. Важно понимать, что внутри клиента нет жёсткого обрыва сокета — это логическая отмена операции, выполняемая через event loop.
Пулы соединений
Каждый HTTP-запрос требует открытия TCP-соединения. Установка соединения — затратная операция, особенно при HTTPS, где добавляется TLS-рукопожатие. Чтобы не открывать новые соединения при каждом запросе, клиент хранит их в пуле и переиспользует. В httpx пул создаётся автоматически внутри AsyncClient. Он управляет количеством одновременных соединений и временем их жизни. Если запросов поступает больше, чем свободных соединений, клиент ждёт освобождения слота в пуле.
Параметр Limits позволяет контролировать поведение пула. Например, max_connections задаёт общее число соединений, а max_keepalive_connections ограничивает количество неактивных соединений, которые можно переиспользовать:
Если пул заполнен, ожидание свободного соединения ограничивается таймаутом pool — если клиент не получил соединение вовремя, выбрасывается PoolTimeout. В примере выше мы увеличили размер пула до 100 соединений, чтобы 20 параллельных запросов могли выполниться без ожидания.
В aiohttp концепция похожа, но реализация отличается. Клиент использует объект TCPConnector, который управляет соединениями и кешем keep-alive. Таймауты задаются через aiohttp.ClientTimeout:
Параметр limit задаёт общее количество открытых соединений, ttl_dns_cache управляет временем хранения DNS-записей. Если сервер использует балансировку по DNS, этот параметр помогает не делать повторные запросы к DNS при каждом подключении. Таймаут total в aiohttp определяет максимальное время всей операции, включая все стадии. Это удобно для простых сценариев, где не нужно разделять время ожидания по этапам.
При большом количестве параллельных запросов управление пулом становится особенно важным. Если пул слишком маленький, клиенты начнут ждать и общая производительность снизится. Если слишком большой — возникнет нагрузка на сеть и на сервер, а память приложения будет занята неиспользуемыми соединениями. В асинхронных программах оптимальный размер пула подбирается экспериментально, исходя из числа одновременно выполняемых задач и скорости ответов сервера.
Также важно понимать, что соединения в пуле могут устаревать. Сервер может их закрыть по таймауту keep-alive, а клиент не всегда узнает об этом сразу. Поэтому библиотеки сами выполняют проверку соединения перед повторным использованием. Если соединение закрыто, оно автоматически пересоздаётся. Этот процесс невидим для пользователя, но может влиять на задержку при первом запросе после простоя.
Некоторые приложения, например API-сервисы, требуют строгого контроля таймаутов, чтобы не держать открытые соединения дольше определённого времени. В httpx можно задавать таймауты и пулы соединений отдельно для каждого клиента, что удобно при работе с несколькими источниками данных:
Такая настройка позволяет гибко распределять ресурсы: быстрые API получают короткие таймауты и больше соединений, а медленные — меньше. Это защищает event loop от блокировки на долгих запросах и сохраняет отзывчивость приложения.
Повторные попытки с exponential backoff и jitter
При работе с сетью неизбежны временные сбои: сервер может быть перегружен, сеть — нестабильна, или возникнет временная недоступность. Вместо немедленного отказа лучше повторить запрос через некоторое время. Однако простые retry-механизмы могут усугубить проблему, создавая эффект «грозы повторных запросов» (retry storm), когда множество клиентов одновременно повторяют запросы, ещё больше перегружая сервер.
Exponential backoff (экспоненциальная задержка) решает эту проблему, увеличивая паузу между попытками: первая попытка через 1 секунду, вторая — через 2, третья — через 4, и так далее. Это даёт серверу время восстановиться.
Jitter (случайный разброс) добавляет случайную составляющую к задержке, чтобы избежать синхронизации запросов от разных клиентов. Без jitter все клиенты, начавшие retry одновременно, снова обратятся к серверу в один момент.
Пример реализации:
Ключевые моменты:
- Задержка растёт экспоненциально: 1с, 2с, 4с, 8с, 16с...
- Jitter добавляет случайность ±25%, чтобы избежать синхронизации
- Максимальная задержка ограничена
max_delay, чтобы не ждать слишком долго - Все попытки логируются для последующего анализа
WebSocket: поддержание соединения и переподключение
WebSocket устанавливает постоянное соединение для двустороннего обмена сообщениями в реальном времени.
Библиотека aiohttp предоставляет удобный асинхронный интерфейс для работы с WebSocket. После установления соединения с помощью метода session.ws_connect() создаётся объект ClientWebSocketResponse, через который можно как отправлять, так и получать сообщения. Работа с ним полностью асинхронная, поэтому для чтения используется await ws.receive(), а для отправки — await ws.send_str() или аналогичные методы для передачи бинарных данных.
Рассмотрим простой пример клиента, который подключается к серверу, отправляет сообщения и обрабатывает ответы:
Метод ws.receive() возвращает объект сообщения, в котором может быть текст, бинарные данные или уведомление о закрытии. Такой код корректен для базовой работы, но в реальных приложениях нужно уметь восстанавливаться после обрыва соединения и повторно подключаться.
Корректное управление сессией при ошибках
Критическая проблема: при ошибках подключения сессия может остаться незакрытой, что приводит к утечкам ресурсов. Необходимо всегда гарантировать закрытие сессии, даже при исключениях.
Плохой пример — сессия может не закрыться:
Правильный подход — всегда используйте async with или явное закрытие в finally:
Ключевые изменения для корректного управления ресурсами:
- Создание сессии внутри try — позволяет перехватить ошибки
- Явное закрытие в except — гарантирует освобождение ресурсов при ошибке подключения
- Блок finally — закрывает ws и session независимо от исхода
- Проверка состояния — закрываем только если не закрыто (
not ws.closed) - Логирование — отслеживаем все попытки подключения и закрытия
Поддержание соединения активным (ping-pong)
WebSocket полезен не только для получения данных, но и для двусторонней коммуникации, например, при обмене уведомлениями, синхронизации состояния или потоковой передаче данных. Поэтому важно уметь поддерживать соединение активным. Для этого применяют ping-pong механизм, позволяющий серверу и клиенту убедиться, что друг друга всё ещё можно достичь. В aiohttp это делается вызовом метода ping(). Иногда сервер автоматически закрывает неактивные соединения, поэтому клиенту стоит периодически отправлять пинг:
Эта корутина выполняется параллельно с основной задачей, чтобы поддерживать активность соединения. Если ping() не получает ответа, соединение обычно закрывается, и цикл переподключения снова активируется.
Ограничение количества WebSocket-соединений
Если приложение активно использует множество WebSocket-соединений, стоит ограничить их количество с помощью семафора (asyncio.Semaphore). Это помогает избежать перегрузки клиента и предотвращает превышение лимитов сервера:
Такой подход гарантирует, что одновременно будет активно не более 5 соединений, что предотвращает перегрузку и помогает контролировать использование ресурсов.
Контрольный список для работы с сетью
Используйте единый клиент/пул на процесс — создавайте один AsyncClient или ClientSession для всего приложения, переиспользуя пул соединений
Задавайте отдельные таймауты для разных стадий — используйте раздельные connect, read, write и pool таймауты для точного контроля
Всегда используйте async with для сессий — это гарантирует корректное закрытие соединений даже при ошибках
Закрывайте сессии явно в except или finally — при ошибках подключения сессия может остаться незакрытой
Реализуйте exponential backoff с jitter для retry — избегайте retry storm, добавляя случайную задержку
Логируйте все попытки подключения — это критически важно для диагностики проблем в продакшене
Настройте размер пула соединений — выбирайте баланс между производительностью и нагрузкой на сервер
Для WebSocket всегда проверяйте ws.closed перед закрытием — избегайте попыток закрыть уже закрытое соединение
Используйте семафоры для ограничения параллелизма — контролируйте количество одновременных соединений
Реализуйте keep-alive для длительных WebSocket-соединений — предотвращайте автоматическое закрытие неактивных соединений
Пример правильной конфигурации HTTP-клиента:
Ключевые особенности правильной конфигурации:
- Единый клиент на всё приложение — переиспользуется пул соединений
- Раздельные таймауты — точный контроль над каждой стадией
- Настроенный пул — баланс между производительностью и ресурсами
- Контекстный менеджер — гарантирует закрытие при любом исходе
- Встроенный retry — exponential backoff с jitter и логированием
- Обработка ошибок — использование
return_exceptions=Trueдля массовых запросов
Такая конфигурация обеспечивает надёжную работу в production-окружении с тысячами запросов в секунду.
Рекомендуемые программы
Завершено
0 / 10

