Как выполнить множество HTTP-запросов одновременно в Python
Что проверяют
Понимаете ли вы разницу между потоками и асинхронностью для ввода-вывода и не забудете ли про ограничение параллелизма.
Как отвечать
Есть два рабочих подхода, и выбор зависит от того, асинхронный у вас код или нет.
Асинхронный вариант:
async def fetch_all(urls: list[str]) -> list[str]:
limit = asyncio.Semaphore(10) # не больше десяти одновременно
timeout = httpx.Timeout(5.0)
async with httpx.AsyncClient(timeout=timeout) as client:
async def one(url: str) -> str:
async with limit:
r = await client.get(url)
r.raise_for_status()
return r.text
return await asyncio.gather(*(one(u) for u in urls), return_exceptions=True)Синхронный вариант: concurrent.futures.ThreadPoolExecutor с обычным requests. Потоки на ожидании сети работают нормально, глобальная блокировка интерпретатора здесь не мешает, потому что она освобождается на время ввода-вывода.
Что обязательно назвать:
- ограничение параллелизма: тысяча одновременных запросов упрётся в лимиты сервера и в исчерпание сокетов. Семафор или размер пула;
- таймауты на каждый запрос, иначе один зависший тянет всю пачку;
- обработка ошибок:
return_exceptions=Trueвgather, чтобы одна неудача не отменила остальные, и разбор результатов после; - переиспользование клиента: один клиент на всю пачку, чтобы соединения брались из пула, а не открывались заново;
- повторы с нарастающей задержкой для временных ошибок и уважение к заголовку
Retry-After.
Где спотыкаются
Создают новый клиент на каждый запрос и теряют весь выигрыш на установке соединений.
Запускают gather без ограничения и получают отказ от сервиса или блокировку по адресу.
Ставят асинхронный код, но внутри вызывают синхронный requests, чем блокируют цикл событий.