Прямой ответ
Прод-запуск Python-веба — это сервер приложений между nginx и кодом: WSGI-сервер (Gunicorn/uWSGI) для синхронных фреймворков (Flask, Django без async) и ASGI-сервер (Uvicorn) для асинхронных (FastAPI, Starlette, Django async); стандартная связка для FastAPI — gunicorn app:app -k uvicorn.workers.UvicornWorker -w 4 (Gunicorn управляет процессами, Uvicorn обрабатывает ASGI внутри). Ключевые решения: число воркеров — формула-старт 2×CPU + 1 для sync/CPU-bound, для async-воркеров Uvicorn воркеров нужно меньше (каждый держит тысячи соединений — 1–2 на ядро; больше воркеров = больше памяти и соединений к БД — пулер снимает эту боль), таймауты — timeout Gunicorn должен превышать худший запрос (дефолт 30 с молча убивает воркер на долгой операции — классика «502 без причины в логах приложения»), graceful — SIGTERM с graceful_timeout и пре-стоп закрытием сокета (механика), и запуск под systemd с рестартами (юниты), не в tmux «пока работает». Запуск uvicorn app:app одним процессом без менеджера — нормален для dev, не для прода (рестарт при падении, ротация, деплой — не решены).
WSGI против ASGI: что запускать
WSGI (Pep 3333) — синхронный контракт «запрос → ответ»: воркер занят на всё время запроса; Gunicorn с sync-воркерами — прост и предсказуем, но медленный внешний API в запросе держит воркер впустую (лечение — gthread-воркеры с потоками или смена класса на async). ASGI — асинхронный контракт (событийный цикл, один процесс держит тысячи конкурентных запросов, включая WebSocket — realtime-практика); Uvicorn — эталонный ASGI-сервер (быстрый, на uvloop), но сам не управляет пулом процессов: в проде — либо Gunicorn с UvicornWorker (менеджер + ASGI), либо несколько Uvicorn-процессов под systemd/супервизором с --workers N (встроенный мультипроцессинг Uvicorn — проще, чуть меньше контроля). Что выбрать: Flask/Django sync — Gunicorn (gthread при IO в запросах); FastAPI/Starlette — связка с UvicornWorker (или чистый uvicorn --workers); «а нужен ли вообще Gunicorn» — для uvicorn --workers он не обязателен: современный Uvicorn сам форкает и рестартует воркеров; Gunicorn-обвязка — привычка и чуть более зрелая админ-механика (статистика, хуки, max_requests). Оба варианта легитимны — важнее systemd-обвязка и healthcheck снаружи.
Воркеры, потоки, память: настройка под нагрузку
Формулы-отправные точки (проверять нагрузочным тестом — locust): sync-воркеры — 2×ядер+1 (CPU-bound); gthread — воркеров ядер, потоков 4–10 (IO-bound синхронный код — поток ждёт сеть, воркер обслуживает следующий запрос); uvicorn-воркеры — 1–2×ядер (async: конкурентность внутри процесса, лишние воркеры — лишняя память ×RSS и множитель пулов БД). Память: 4 воркера × 250 МБ RSS = 1 ГБ просто на процессы — на 2 ГБ VPS это граница (диагностика памяти); max_requests=1000, max_requests_jitter=100 — перезапуск воркера после N запросов (страховка от утечек памяти — воркеры ротируются по очереди, незаметно для трафика). Практика деградации: при нехватке воркеров симптомы — рост очереди и латентности при спокойном CPU (все воркеры заняты ожиданием) — добавлять потоки/воркеры; при CPU-упоре — воркеры уже не помогут (оптимизация кода/горизонталь). Таймауты: timeout=60+ под реальный p99 запроса; keepalive=5 под настройки nginx (прокси — keepalive к апстриму согласуется, иначе лишние переподключения); долгий экспорт/отчёт — не через HTTP-запрос вовсе, а фоновой задачей (Celery) с немедленным 202.
Systemd-обвязка и деплой
Юнит сервиса: ExecStart=/opt/app/.venv/bin/gunicorn app:app -k uvicorn.workers.UvicornWorker -w 4 -b 127.0.0.1:8000 --timeout 60 --max-requests 1000 (bind на localhost — наружу смотрит только nginx), Restart=always, RestartSec=3, User=app (не root), EnvironmentFile для секретов (env-практика), лимиты MemoryMax (защита от OOM-сюрпризов). Деплой: смена кода + systemctl reload app — Gunicorn по HUP поднимает новых воркеров и гасит старых graceful (без обрыва соединений — rolling-логика внутри одного хоста); при смене зависимостей/мажорных правках — restart с паузой (healthcheck nginx-уровня прикрывает окно). Healthcheck: /health эндпоинт приложения (не / — отдельная лёгкая проверка с БД-пингом) — дисциплина; systemd WatchdogSec для детекции зависания мастера. Логи: stdout/stderr в journald (настройка) или access-логи nginx (дублировать access в приложении смысла мало — логи: приложению — бизнес-события, доступы — прокси).
Практический сценарий: FastAPI-сервис на VPS 4 ядра / 8 ГБ
Старт: uvicorn --workers 4 под systemd, nginx проксирует на 127.0.0.1:8000; пул БД 3 на воркер, PgBouncer рядом. Нагрузочный тест: 200 RPS смешанных запросов (IO к БД и внешнему API) — p95 = 180 мс, CPU 55% — запас есть; 400 RPS — p95 900 мс, CPU 90% — упор в CPU (сериализация ответов); оптимизация: orjson-ответы (сериализация) + кеш справочников — при тех же 400 RPS p95 = 240 мс, CPU 60%. Инцидент первой недели: периодические 502 утром — в journald: [CRITICAL] WORKER TIMEOUT — ночная миграция данных по HTTP-хендлеру длилась 45 с при timeout=30 — миграция переехала в Celery-задачу (эндпоинт — только постановка и статус), таймаут оставлен 30 (долгие запросы — баг дизайна, не повод поднимать потолок бесконечно). Деплой-рутина закрепилась: pull → migrate → systemctl reload → проверка /health и метрик (дашборд) — 90 секунд, ноль простоев за квартал; при росте трафика план — второй сервер + балансировка (воркеры вертикально уже не масштабируются — CPU-bound).
FAQ
Можно ли вообще без nginx, один uvicorn наружу? Технически да (Uvicorn держит TLS и соединения прилично), но nginx даёт статику, кеш, лимиты, мультидоменность и буферизацию медленных клиентов (всё сразу) — в проде прокси-слой почти всегда оправдан; исключение — контейнерные платформы, где балансировщик встроен.
Почему воркеры «умирают» с WORKER TIMEOUT?
Мастер Gunicorn следит за heartbeat воркеров: запрос/задача дольше timeout — воркер считается зависшим и убивается (клиент получит 502); причины — долгий запрос, блокирующий вызов в async-коде, дедлок; лечение — выносить долгое из запроса, не поднимать timeout до бесконечности.
Сколько держать соединений к БД при 8 воркерах? 8 × pool(5+overflow) — потенциально 40–60 соединений; на одиночной БД с max_connections=100 и другими сервисами — тесно: уменьшать пулы приложений и/или ставить PgBouncer (схема); метрика «использовано соединений» — в мониторинге обязательно.
Uvicorn с --reload в проде? Нет: reload — dev-режим (watcher файлов, один процесс, перезапуск на каждое сохранение); прод — фиксированные воркеры, перезапуск по деплою; «запустил с --reload на сервере и забыл» — источник и просадок, и случайных рестартов от touch-нутых файлов.
Источники: документация Gunicorn (settings), Uvicorn (deployment), ASGI spec — ссылки в шапке.