diff --git a/.env.example b/.env.example index 2a0f30f..fc18713 100644 --- a/.env.example +++ b/.env.example @@ -81,6 +81,20 @@ LIVEKIT_NODE_IP=127.0.0.1 # с точкой монтирования тома в обоих сервисах. RECORDINGS_DIR=/recordings +# --- Производительность backend --- +# Число процессов uvicorn. Один процесс означает, что любой блокирующий вызов +# в обработчике останавливает весь event loop: параллельные входы в конференцию +# и WS-чат всех участников встают в очередь. Дефолт 2 рассчитан на 4-ядерный +# сервер, где ядра делятся с LiveKit (медиа важнее API). +UVICORN_WORKERS=2 +# Пул соединений с БД НА КАЖДЫЙ воркер. Общий расход инстанса — +# UVICORN_WORKERS × (DB_POOL_SIZE + DB_MAX_OVERFLOW), плюс соединения Celery +# и alembic. Держите сумму заметно ниже max_connections у Postgres (по +# умолчанию 100), иначе вместо понятной ошибки приложения получите отказ БД. +DB_POOL_SIZE=10 +DB_MAX_OVERFLOW=10 +DB_POOL_TIMEOUT=10 + # --- Email (рассылка саммари + .ics-приглашения) --- # `console` — дефолт для dev (письмо только логируется, ссылка подтверждения # email берётся из логов); `smtp` — реальная отправка через aiosmtplib. @@ -98,7 +112,7 @@ SMTP_TIMEOUT_S=30 # --- Версия инстанса (релиз v0.0.1) --- # install.sh копирует значение из корневого файла VERSION при каждой # установке/обновлении — руками менять не нужно. -VIDCONF_VERSION=0.0.11 +VIDCONF_VERSION=0.0.12 # --- Профили compose. Дефолт ниже (`media,monitoring`) — только для ручного # `docker compose up` БЕЗ install.sh: медиа (LiveKit+coturn) + мониторинг, diff --git a/backend/Dockerfile b/backend/Dockerfile index 4bbb729..002a9ab 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -37,4 +37,13 @@ EXPOSE 8000 HEALTHCHECK --interval=10s --timeout=5s --retries=10 --start-period=15s \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')" || exit 1 -CMD ["uv", "run", "uvicorn", "main:create_app", "--factory", "--host", "0.0.0.0", "--port", "8000"] +# Число воркеров — из окружения (`UVICORN_WORKERS`, см. docker-compose.yml). +# Один процесс означает, что любой блокирующий вызов в обработчике +# останавливает весь event loop: параллельные запросы и WS-чат всех +# участников встают в очередь. Дефолт 2, а не «по числу ядер»: медиа +# важнее API, и на 4-ядерном сервере LiveKit в пике забирает 1.6 ядра. +# +# `sh -c` нужен ради подстановки переменной (exec-форма её не делает), +# `exec` — чтобы uvicorn получил PID 1 и корректно принимал SIGTERM. +CMD ["sh", "-c", \ + "exec uv run uvicorn main:create_app --factory --host 0.0.0.0 --port 8000 --workers ${UVICORN_WORKERS:-2}"] diff --git a/backend/core/config.py b/backend/core/config.py index ee4ca6c..48f96b6 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -20,6 +20,24 @@ class Settings(BaseSettings): redis_url: str = "redis://localhost:6379/0" plugins_config_path: str = "../config/plugins.yaml" + # --- Пул соединений с БД --- + # Дефолт SQLAlchemy (5 + 10) на нагрузочном тесте 28.07.2026 выгребался + # за секунды: 226 ошибок `QueuePool limit of size 5 overflow 10 reached` + # и 37 ответов 500 на путях входа в конференцию. + # + # ⚠️ Бюджет соединений считается на ВЕСЬ инстанс, а не на процесс: каждый + # воркер uvicorn (`UVICORN_WORKERS`) держит собственный пул, плюс + # соединения нужны Celery-воркерам и alembic при миграциях. При + # `max_connections=100` у Postgres и двух воркерах 2 × (10 + 10) = 40 + # оставляет запас. Поднимая значения на более крупном сервере, поднимайте + # и `max_connections` — иначе вместо понятной ошибки приложения получите + # отказ Postgres, который диагностируется куда хуже. + db_pool_size: int = 10 + db_max_overflow: int = 10 + # 10 секунд вместо дефолтных 30 — сознательно: пусть запрос падает быстро + # и показывает проблему, а не висит полминуты, делая вид, что всё живо. + db_pool_timeout: int = 10 + # --- Версия инстанса (релиз v0.0.1) --- # install.sh копирует значение из файла `VERSION` (корень репозитория) в # `.env` при каждой установке/обновлении — здесь только чтение готового @@ -59,6 +77,13 @@ class Settings(BaseSettings): # (см. `deploy/docker-compose.yml`); в тестах переопределяется на `tmp_path`. recordings_dir: str = "/recordings" + # Таймаут запуска Track Egress. Когда egress-сервиса в деплое нет (профиль + # `transcribe` не поднят), LiveKit ждёт ответа воркера через Redis до + # собственного таймаута psrpc — на тесте 28.07.2026 это давало по 20–25 + # секунд на каждый вызов. Ждать столько бессмысленно: если egress жив, он + # отвечает за доли секунды. + egress_start_timeout_s: float = 3.0 + # --- Email (SMTP-бэкенд) --- # `console` — дефолт для dev (письмо только логируется); `smtp` — реальная # отправка через aiosmtplib. Секреты SMTP — только в `.env` (инвариант №6), diff --git a/backend/core/db.py b/backend/core/db.py index bca636c..c790c50 100644 --- a/backend/core/db.py +++ b/backend/core/db.py @@ -13,7 +13,16 @@ from core.config import get_settings settings = get_settings() -engine: AsyncEngine = create_async_engine(settings.database_url, pool_pre_ping=True) +engine: AsyncEngine = create_async_engine( + settings.database_url, + pool_pre_ping=True, + # Параметры пула — в настройках (`core/config.py`, там же расчёт бюджета + # соединений на инстанс). Дефолт SQLAlchemy 5 + 10 под нагрузкой + # выгребался за секунды. + pool_size=settings.db_pool_size, + max_overflow=settings.db_max_overflow, + pool_timeout=settings.db_pool_timeout, +) async_session_maker = async_sessionmaker(engine, expire_on_commit=False) diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 32b93f8..e35545c 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -82,7 +82,12 @@ services: MEDIA_ROOT: ${MEDIA_ROOT:-/app/media} # Версия инстанса (релиз v0.0.1) — install.sh копирует значение # из файла VERSION (корень репозитория) в .env; отдаётся в GET /api/health. - VIDCONF_VERSION: ${VIDCONF_VERSION:-0.0.11} + VIDCONF_VERSION: ${VIDCONF_VERSION:-0.0.12} + # Число процессов uvicorn (см. backend/Dockerfile). Дефолт 2 рассчитан + # на 4-ядерный сервер, где ядра делятся с LiveKit. Поднимая значение, + # проверьте бюджет соединений с БД: каждый воркер держит свой пул + # (DB_POOL_SIZE + DB_MAX_OVERFLOW), а у Postgres есть max_connections. + UVICORN_WORKERS: ${UVICORN_WORKERS:-2} # config/ лежит в корне репозитория и не попадает в образ (контекст сборки — # только backend/), поэтому plugins.yaml монтируется отдельно. volumes: