Как используется aiohttp для асинхронных запросов в Python
Что проверяют
Понимаете ли вы, как устроены сессии и конкурентность в aiohttp, а не только умеете ли переписать пример из документации. Следом почти всегда спрашивают, как ограничить число одновременных запросов и чем aiohttp отличается от requests в потоках.
Как отвечать
Каркас такой:
- одна сессия на приложение:
aiohttp.ClientSessionдержит пул соединений и keep-alive. Её создают один раз черезasync withи переиспользуют для всех запросов, а не открывают на каждый URL; - запрос и ответ:
async with session.get(url, params=...) as resp:, затемawait resp.json()илиawait resp.text(). Контекстный менеджер ответа возвращает соединение в пул; - статусы: по умолчанию 4xx и 5xx исключением не считаются. Нужен
resp.raise_for_status()или параметрraise_for_status=Trueу сессии; - таймауты:
aiohttp.ClientTimeout(total=10). По умолчанию на весь запрос отводится 5 минут, для боевого кода это слишком много; - конкурентность: задачи запускают через
asyncio.gatherилиasyncio.TaskGroup(Python 3.11+), а число одновременных запросов ограничиваютasyncio.Semaphoreили параметромlimitуTCPConnector, по умолчанию он равен 100; - ошибки: сетевые сбои приходят как
aiohttp.ClientErrorи его наследники, таймаут какasyncio.TimeoutError(с Python 3.11 это тот же встроенныйTimeoutError).
Объясните, почему это быстро: пока один запрос ждёт ответа, цикл событий переключается на другие. Выигрыш есть только на ожидании ввода-вывода, CPU-нагрузку aiohttp не ускорит.
Кроме клиента, в aiohttp есть серверная часть aiohttp.web, это стоит упомянуть одной фразой.
Где спотыкаются
Создают ClientSession внутри функции fetch на каждый запрос. Пул соединений теряет смысл, а на тысячах URL растут расходы на TLS-рукопожатия.
Запускают десять тысяч корутин через gather без ограничения. Сервер отвечает 429 или рвёт соединения, а у клиента заканчиваются файловые дескрипторы.
Вызывают внутри корутины синхронный requests.get или time.sleep. Это блокирует весь цикл событий, и асинхронность исчезает.