perf(backend): явный пул соединений БД и несколько воркеров uvicorn
Пул создавался с дефолтом SQLAlchemy (5 + 10) и под нагрузкой выгребался за секунды. Теперь параметры заданы явно и вынесены в настройки: DB_POOL_SIZE=10, DB_MAX_OVERFLOW=10, DB_POOL_TIMEOUT=10. Таймаут снижен с дефолтных 30 секунд намеренно — пусть запрос падает быстро и показывает проблему, а не висит полминуты. Backend запускался одним процессом uvicorn: любой блокирующий вызов останавливал и параллельные запросы, и WS-чат всех участников. Добавлен UVICORN_WORKERS с дефолтом 2 — не по числу ядер, потому что на четырёхъядерном сервере ядра делятся с LiveKit, а медиа важнее API. Бюджет соединений считается на весь инстанс: каждый воркер держит свой пул, поэтому UVICORN_WORKERS × (DB_POOL_SIZE + DB_MAX_OVERFLOW) должно оставаться заметно ниже max_connections у Postgres. Многопроцессность безопасна: бутстрап настроек в lifespan идемпотентен (INSERT ... ON CONFLICT DO NOTHING), а WS-чат разносит сообщения через Redis pub/sub и состояния в памяти процесса не держит.
This commit is contained in:
16
.env.example
16
.env.example
@@ -81,6 +81,20 @@ LIVEKIT_NODE_IP=127.0.0.1
|
|||||||
# с точкой монтирования тома в обоих сервисах.
|
# с точкой монтирования тома в обоих сервисах.
|
||||||
RECORDINGS_DIR=/recordings
|
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-приглашения) ---
|
# --- Email (рассылка саммари + .ics-приглашения) ---
|
||||||
# `console` — дефолт для dev (письмо только логируется, ссылка подтверждения
|
# `console` — дефолт для dev (письмо только логируется, ссылка подтверждения
|
||||||
# email берётся из логов); `smtp` — реальная отправка через aiosmtplib.
|
# email берётся из логов); `smtp` — реальная отправка через aiosmtplib.
|
||||||
@@ -98,7 +112,7 @@ SMTP_TIMEOUT_S=30
|
|||||||
# --- Версия инстанса (релиз v0.0.1) ---
|
# --- Версия инстанса (релиз v0.0.1) ---
|
||||||
# install.sh копирует значение из корневого файла VERSION при каждой
|
# install.sh копирует значение из корневого файла VERSION при каждой
|
||||||
# установке/обновлении — руками менять не нужно.
|
# установке/обновлении — руками менять не нужно.
|
||||||
VIDCONF_VERSION=0.0.11
|
VIDCONF_VERSION=0.0.12
|
||||||
|
|
||||||
# --- Профили compose. Дефолт ниже (`media,monitoring`) — только для ручного
|
# --- Профили compose. Дефолт ниже (`media,monitoring`) — только для ручного
|
||||||
# `docker compose up` БЕЗ install.sh: медиа (LiveKit+coturn) + мониторинг,
|
# `docker compose up` БЕЗ install.sh: медиа (LiveKit+coturn) + мониторинг,
|
||||||
|
|||||||
@@ -37,4 +37,13 @@ EXPOSE 8000
|
|||||||
HEALTHCHECK --interval=10s --timeout=5s --retries=10 --start-period=15s \
|
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 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}"]
|
||||||
|
|||||||
@@ -20,6 +20,24 @@ class Settings(BaseSettings):
|
|||||||
redis_url: str = "redis://localhost:6379/0"
|
redis_url: str = "redis://localhost:6379/0"
|
||||||
plugins_config_path: str = "../config/plugins.yaml"
|
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) ---
|
# --- Версия инстанса (релиз v0.0.1) ---
|
||||||
# install.sh копирует значение из файла `VERSION` (корень репозитория) в
|
# install.sh копирует значение из файла `VERSION` (корень репозитория) в
|
||||||
# `.env` при каждой установке/обновлении — здесь только чтение готового
|
# `.env` при каждой установке/обновлении — здесь только чтение готового
|
||||||
@@ -59,6 +77,13 @@ class Settings(BaseSettings):
|
|||||||
# (см. `deploy/docker-compose.yml`); в тестах переопределяется на `tmp_path`.
|
# (см. `deploy/docker-compose.yml`); в тестах переопределяется на `tmp_path`.
|
||||||
recordings_dir: str = "/recordings"
|
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-бэкенд) ---
|
# --- Email (SMTP-бэкенд) ---
|
||||||
# `console` — дефолт для dev (письмо только логируется); `smtp` — реальная
|
# `console` — дефолт для dev (письмо только логируется); `smtp` — реальная
|
||||||
# отправка через aiosmtplib. Секреты SMTP — только в `.env` (инвариант №6),
|
# отправка через aiosmtplib. Секреты SMTP — только в `.env` (инвариант №6),
|
||||||
|
|||||||
@@ -13,7 +13,16 @@ from core.config import get_settings
|
|||||||
|
|
||||||
settings = 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)
|
async_session_maker = async_sessionmaker(engine, expire_on_commit=False)
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,12 @@ services:
|
|||||||
MEDIA_ROOT: ${MEDIA_ROOT:-/app/media}
|
MEDIA_ROOT: ${MEDIA_ROOT:-/app/media}
|
||||||
# Версия инстанса (релиз v0.0.1) — install.sh копирует значение
|
# Версия инстанса (релиз v0.0.1) — install.sh копирует значение
|
||||||
# из файла VERSION (корень репозитория) в .env; отдаётся в GET /api/health.
|
# из файла 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/ лежит в корне репозитория и не попадает в образ (контекст сборки —
|
# config/ лежит в корне репозитория и не попадает в образ (контекст сборки —
|
||||||
# только backend/), поэтому plugins.yaml монтируется отдельно.
|
# только backend/), поэтому plugins.yaml монтируется отдельно.
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
Reference in New Issue
Block a user