Python. Итераторы, гинераторы и асинхронность
Практическая работа: Мини‑Nginx на asyncio (reverse proxy)
Описание задачи
Сделай асинхронный reverse proxy‑сервер, который принимает HTTP‑запросы от клиента и проксирует их к одному или нескольким upstream‑сервисам. Цель — закрепить материалы из папки async на реальной задаче: от базового event loop и задач до потоковой передачи данных, backpressure, timeouts и управления пулом соединений.
Цели
  • Освоить фундаментальные понятия: итераторы, генераторы, корутины, event loop,
    задачи, планирование и отмена задач, различия CPU‑bound и IO‑bound.
  • Применить asyncio для сетевой разработки: создание серверов, клиентских соединений, управление временем и ресурсами.
  • Понять паттерны проксирования: keep‑alive, стриминг тела запроса/ответа, балансировка, health‑checks, пул апстримов.
Функциональные требования (MVP)
  • Приём входящих соединений по TCP и обработка HTTP/1.1 запросов (минимальный парсер стартовой строки и заголовков).
  • Проксирование к upstream (ам):
    • Поддержка одного или нескольких апстримов.
    • Простейшая балансировка: round‑robin.
  • Стриминг данных без полного буферизования в памяти:
    • Потоковая передача тела запроса к апстриму (читать кусками у клиента — сразу писать апстриму).
    • Потоковая передача ответа от апстрима к клиенту.
    • Backpressure через await writer.drain().
  • Keep‑Alive: повторное использование соединений с клиентом и (по возможности) с апстримами.
  • Таймауты: connect/read/write/total (разумные значения по умолчанию, настраиваемые через конфигурацию).
  • Ограничения: максимум одновременных клиентских запросов и/или максимум соединений к каждому апстриму.
  • Простое логирование: старт запроса, апстрим‑хост, статус ответа, длительность, ошибки и timeouts.
Нефункциональные требования
  • Надёжная обработка ошибок: отмена задач, корректное закрытие потоков, отсутствие утечек.
  • Читаемая архитектура и модульность.
  • Минимальная деградация производительности при росте нагрузки.
Подсказки: связь с материалами в async
  • 01_async_start.md: понятия конкурентности и асинхронности.
  • 02_iterators.md, 04_generators.md: как устроены итераторы/генераторы — база для понимания корутин.
  • 06_concurrency.md, 07_multitasking.md, 09_concurrency_in_depth.md: кооперативная многозадачность, планирование, отмена, ожидания.
  • 10_async_cpu_vs_io.md: почему сеть — это IO‑bound и где нужна асинхронность.
  • 11_event_loop.md: архитектура цикла событий, реактор.
  • 12_asyncio.md: практическая работа с asyncio.start_server, asyncio.open_connection, StreamReader/StreamWriter, wait_for, Task.
Архитектура (рекомендация)
  • ProxyServer: создание TCP‑сервера, принятие клиентских соединений.
  • ClientConnectionHandler: чтение стартовой строки/заголовков запроса, выбор апстрима, проксирование данных (двунаправленный стриминг).
  • UpstreamPool: хранение списка апстримов, round‑robin выбор, лимиты на соединения, опционально — re‑use соединений.
  • TimeoutPolicy: значения таймаутов и функции обёртки asyncio.wait_for.
  • ConfigLoader: загрузка конфигурации (например, YAML/JSON).
  • Logger/Metrics (минимальный): время запроса, байты, коды статусов, количество активных соединений.
Стратегия стриминга:
  • Читать у клиента await client_reader.read (n) и сразу писать в апстрим up_writer.write (chunk); await up_writer.drain ().
  • Параллельно читать ответ апстрима и писать клиенту.
  • Корректно завершать половины соединения (half‑close) и закрывать при ошибках.
Конфигурация (пример YAML)
listen: "127.0.0.1:8080"
upstreams:
  - host: "127.0.0.1"
    port: 9001
  - host: "127.0.0.1"
    port: 9002
timeouts:
  connect_ms: 1000
  read_ms: 15000
  write_ms: 15000
  total_ms: 30000
limits:
  max_client_conns: 1000
  max_conns_per_upstream: 100
logging:
  level: "info"
Пошаговый план реализации
  • Подними asyncio TCP‑сервер (asyncio.start_server) и выведи в лог входящие соединения.
  • Реализуй минимальный парсер HTTP‑запроса: стартовая строка (метод, путь, версия) + заголовки (без сложных кейсов, достаточно CRLF). Тело — передавать как raw‑стрим.
  • Сделай подключение к одному апстриму (asyncio.open_connection), проксируй запрос и ответ (двунаправленно), соблюдая backpressure через drain ().
  • Добавь таймауты (asyncio.wait_for) на этапы connect/read/write, и общий total.
  • Подключи балансировку round‑robin по нескольким апстримам.
  • Введи лимиты на количество одновременных соединений/запросов и соединений к апстриму (например, через asyncio. Semaphore).
  • Добавь логирование и минимальные метрики (счётчики/таймеры). Опционально — отдельная /metrics ручка на другом порту.
Критерии приёмки (минимум)

  • curl -v 127.0.0.1:8080/anything возвращает ответ апстрима с правильными заголовками/статусом.
  • Под нагрузкой (см. ниже) сервер не падает, корректно ограничивает одновременные соединения и не течёт памятью заметно.
  • Таймауты срабатывают предсказуемо: зависший апстрим не вешает клиента навсегда.
Тестирование и эксперименты
Локальные апстримы (два инстанса):
uvicorn tests.echo_app:app --host 127.0.0.1 --port 9001 --workers 1
uvicorn tests.echo_app:app --host 127.0.0.1 --port 9002 --workers 1
Где tests/echo_app.py — простой HTTP‑сервер (можно FastAPI/Starlette или даже python -m http.server с доработками).
Проверка запросов:
curl -v http://127.0.0.1:8080/
curl -v -X POST http://127.0.0.1:8080/echo -d 'hello world'
Нагрузка (выбери любой инструмент):
wrk -t4 -c128 -d30s http://127.0.0.1:8080/
ab -n 5000 -c 200 http://127.0.0.1:8080/
vegeta attack -duration=30s -rate=500 | vegeta report
Проверь метрики: RPS, latency p95/p99, ошибки, timeouts, распределение по апстримам (round‑robin).
Продвинутые задания (необязательно, по желанию)
  • Health‑checks апстримов (active/passive), исключение недоступных из балансировки.
  • Retry политика (например, при connect/read таймаутах, но не для небезопасных методов).
  • Circuit Breaker (отключение проблемного апстрима на интервал).
  • Rate limiting (token bucket) на клиента или общий.
  • Поддержка HTTPS на фронте (TLS termination) и/или к апстриму.
  • Горячая перезагрузка конфигурации (SIGHUP) без остановки сервера.
  • HTTP/1.1 keep‑alive пул к апстримам, повторное использование соединений.
  • Проброс/модификация заголовков (X-Forwarded-For, Via, Connection: keep-alive и т. п.).
  • Мини‑панель метрик: простая страница со статистикой.
Рекомендуемая структура проекта (пример)
proxy/
  main.py
  config.py
  proxy_server.py
  client_handler.py
  upstream_pool.py
  timeouts.py
  logger.py
  metrics.py
  utils/http.py
tests/
  echo_app.py
  load_scenarios.md
README.md
Вопросы для самопроверки
  • Где и как применять await writer.drain() и чем он помогает?
  • Чем отличается отмена задачи от таймаута и как корректно закрывать соединения?
  • Что происходит с event loop, когда апстрим «задумался»? Почему CPU почти не растёт?
  • Как избежать утечек при исключениях во время стриминга?
  • Почему важно ограничивать число одновременных соединений?
Ожидаемый результат

Запускаемый mini-nginx на asyncio, который умеет проксировать HTTP/1.1, балансировать по нескольким апстримам, корректно обрабатывает таймауты, backpressure и ошибки, и выдерживает простую нагрузку.
Удачи! Начни с простого сквозного стриминга, затем добавляй фичи по шагам.