Как оптимизировать синхронные HTTP-запросы в Python?

«Как оптимизировать синхронные HTTP-запросы в Python?» — вопрос из категории Архитектура, который задают на 10% собеседований Python Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Для оптимизации синхронных HTTP-запросов, особенно при выполнении множества обращений к одному хосту, следует применять несколько ключевых техник.

1. Использование пула соединений (Connection Pooling) Создание нового TCP-соединения для каждого запроса — дорогостоящая операция. requests.Session решает эту проблему, переиспользуя существующие соединения.

Почему это работает: Устраняются накладные расходы на TCP handshake и TLS negotiation для каждого последующего запроса к тому же хосту.

import requests

urls = [
    'https://api.example.com/data/1',
    'https://api.example.com/data/2',
    'https://api.example.com/data/3'
]

# Создаем сессию один раз
with requests.Session() as session:
    for url in urls:
        # Соединение переиспользуется
        response = session.get(url)
        print(f"{url}: {response.status_code}")

2. Параллельное выполнение запросов с помощью потоков Синхронные запросы по своей природе блокирующие. Пока один запрос ждет ответа от сети (I/O-bound операция), процессор простаивает. ThreadPoolExecutor позволяет выполнять несколько запросов одновременно в разных потоках.

import requests
from concurrent.futures import ThreadPoolExecutor

urls = ['https://example.com'] * 10

def fetch_url(url):
    with requests.get(url) as response:
        return response.status_code

# Выполняем до 5 запросов параллельно
with ThreadPoolExecutor(max_workers=5) as executor:
    results = list(executor.map(fetch_url, urls))

print(results)

3. Установка разумных таймаутов Чтобы избежать "зависания" приложения из-за медленного ответа сервера, всегда указывайте таймауты.

  • connect: время на установку соединения.
  • read: время на ожидание первого байта ответа.
# Ожидать соединения не более 3 секунд, ответа - не более 10 секунд
response = requests.get('https://slow-service.com', timeout=(3, 10))

4. Кэширование ответов Если данные меняются редко, кэшируйте ответы, чтобы избежать повторных запросов. Для простых случаев подойдет functools.lru_cache.

from functools import lru_cache
import requests

@lru_cache(maxsize=128)
def get_user_data(user_id):
    print(f"Fetching data for user {user_id}...")
    return requests.get(f'https://api.example.com/users/{user_id}').json()

# Первый вызов выполнит HTTP-запрос
user1 = get_user_data(1)
# Второй вызов мгновенно вернет результат из кэша
user1_cached = get_user_data(1)

Для высоконагруженных систем, где узким местом является ожидание I/O, стоит рассмотреть переход на асинхронный подход с asyncio и библиотеками aiohttp или httpx.