Образ собран с `uv sync --frozen --no-dev`, но `uv run` перед каждым запуском заново синхронизирует venv и подтягивает dev-группу. В логах старта vidconf-backend-1 и vidconf-worker-1 на проде это видно как «Downloading ruff / mypy / pygments» и «Installed 12 packages». Хуже всего healthcheck'и: они выполняют ту же синхронизацию каждые 15 секунд всю жизнь контейнера. Проверено на локально собранном образе, одна и та же команда: uv run — качает 12 пакетов, venv 456 → 574 МБ uv run --no-sync — не качает ничего, venv остаётся 456 МБ Флаг добавлен во все вызовы в прод-путях: CMD образа, command/entrypoint/ healthcheck всех сервисов compose, миграции и seed в install.sh, те же команды в docs/deploy. Локальная разработка (dev-setup.md, backend/README.md, CI) не затронута — там dev-зависимости нужны. Заодно убрано устаревшее объяснение ретрая `up -d --wait`: первый старт больше не синхронизирует окружение. Версия uv в образе — 0.11.33, `--no-sync` поддерживается.
10 KiB
Горизонтальное масштабирование очередей Celery
Описывает, как развести обработку задач по отдельным
репликам/сервисам под нагрузкой, когда одного контейнера worker
недостаточно. Здесь — инструкция для оператора, применимая к текущей и
будущей конфигурации deploy/docker-compose.yml.
1. Карта очередей
Маршрутизация задач по очередям задана в workers/celery_app.py
(app.conf.task_routes) — приоритет между семьями задач реализован
изоляцией очередей, а не Redis-priorities Celery (транспорт redis
эмулирует приоритеты ненадёжно, без строгих гарантий порядка — в отличие от
RabbitMQ).
| Очередь | Задачи | Кто слушает по умолчанию |
|---|---|---|
transcription |
run_pipeline (faster-whisper) |
worker-transcriber (профиль transcribe), --pool=solo — ctranslate2/faster-whisper несовместимы с prefork |
summarize |
summarize_session (Qwen map-reduce) |
базовый worker |
notify |
notify_session, send_invitations (.ics-приглашения) |
базовый worker |
celery (default) |
cleanup_conferences, recover_stuck_summaries, recover_stuck_notifications (маршрута не имеют, обслуживание) |
базовый worker |
Базовый сервис worker слушает celery,summarize,notify — для малых
пресетов поставки (1–3) это один контейнер, обрабатывающий и суммаризацию, и
уведомления, и обслуживание. worker-transcriber — всегда отдельный процесс
(независимо от пресета), т.к. пул solo несовместим с остальными задачами
в том же процессе.
Глубину каждой очереди в реальном времени видно в GET /metrics
(vidconf_celery_queue_depth{queue=...}, backend/api/metrics.py) и в
Grafana-дашборде «Пайплайны пост-обработки» (алерт QueueGrowing,
deploy/monitoring/alerts.yml) — по этим показателям и принимается решение
о вынесении очереди в отдельную реплику.
2. Общее правило: celery beat — только один экземпляр
Периодические задачи (app.conf.beat_schedule) планирует celery beat.
Базовый worker запускается с флагом -B (worker + встроенный beat в одном
процессе, deploy/docker-compose.yml). При масштабировании нельзя просто
поднять несколько реплик сервиса с -B — каждая реплика завела бы
собственный планировщик, и periodic-задачи (cleanup_conferences,
recover_stuck_*) ставились бы в очередь многократно на каждом тике.
Правило: ровно один процесс во всей инсталляции запускается с -B
(встроенным или отдельным celery -A workers.celery_app beat); все
дополнительные реплики — только worker без -B, с явным -Q на нужные
очереди.
3. Вынос очереди в отдельную реплику
Пример: суммаризация (summarize) стала узким местом — очередь растёт,
базовый worker не успевает. Решение — отдельный сервис-потребитель только
этой очереди, без встроенного beat (он остаётся на базовом worker).
Добавить в deploy/docker-compose.override.yml (или новый профиль по
аналогии с worker-transcriber):
services:
worker-summarize:
build:
context: ../backend
dockerfile: Dockerfile
restart: unless-stopped
# Без -B: beat уже запущен на базовом worker (см. правило выше).
command: ["uv", "run", "--no-sync", "celery", "-A", "workers.celery_app", "worker",
"-Q", "summarize", "--hostname=worker-summarize-%h@%h", "--loglevel=info"]
env_file:
- ../.env
environment:
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://vidconf:vidconf@postgres:5432/vidconf}
REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD не задан в .env}@redis:6379/0
PLUGINS_CONFIG_PATH: ${PLUGINS_CONFIG_PATH:-config/plugins.yaml}
PYTHONPATH: /app
volumes:
- ../workers:/app/workers:ro
- ../config:/app/config:ro
- llm-models:/models/qwen:ro
depends_on:
redis:
condition: service_healthy
и одновременно убрать summarize из списка очередей базового worker
(его команда сужается до -Q celery,notify), чтобы задачи не выполнялись
дважды разными процессами (Celery доставляет задачу ровно одному
consumer'у одной и той же очереди — дублирования не будет, но держать
лишний неиспользуемый consumer незачем).
Аналогично можно выделить notify в worker-notify (та же схема,
-Q notify) — например, если рассылка приглашений/уведомлений на большую
аудиторию (SMTP-латентность) начинает задерживать саммаризацию соседних
сеансов при их совместном обслуживании базовым worker.
4. Масштабирование реплик через docker compose up --scale
Если одной выделенной очереди тоже мало (несколько ядер CPU для
суммаризации/уведомлений), реплицируем сервис командой --scale:
docker compose -f deploy/docker-compose.yml --env-file .env -f deploy/docker-compose.override.yml \
up -d --scale worker-summarize=3
Требования для корректного масштабирования сервиса:
- Без
-Bна масштабируемом сервисе (правило §2). - Без фиксированного
--hostnameна весь сервис — при нескольких репликах одинаковое имя узла Celery приведёт к конфликту регистрации в кластере (соединения будут путаться,celery inspectначнёт видеть произвольного из реплик). Использовать%h(имя контейнера, уникальное у каждой реплики Compose) — см.--hostname=worker-summarize-%h@%hв примере выше. Это же ограничение действует и дляworker-transcriber: его текущий healthcheck (--destination worker-transcriber@localhost) жёстко завязан на единственную реплику; при--scale worker-transcriber=Nhealthcheck и--hostnameвdeploy/docker-compose.ymlпотребуется поменять на шаблон%h(и убрать--destination, либо адресовать каждую реплику отдельно) — вне рамок этого документа, т.к. правит основнойdeploy/docker-compose.yml(devops-часть). - Без
container_nameи без фиксированных host-портов на масштабируемом сервисе (у Celery-воркеров портов нет — ограничение не актуально дляworker*, но актуально, если аналогичный приём применяется кbackendзаnginx upstream). - Ресурсные лимиты (
cpus/mem_limit) в Compose-файле применяются к КАЖДОЙ реплике, а не разделяются между ними — планировать суммарное потребление хоста (N × cpus).
Для worker-transcriber тот же приём уже применим на уровне профиля:
docker compose -f deploy/docker-compose.yml --env-file .env --profile transcribe \
up -d --scale worker-transcriber=2
(после снятия ограничения фиксированного --hostname, см. пункт 2 выше).
5. Когда масштабировать
Ориентир — глубина очереди (vidconf_celery_queue_depth) растущая дольше
15 минут (алерт QueueGrowing, deploy/monitoring/alerts.yml) либо
устойчиво положительная под обычной нагрузкой инстанса. Для summarize и
transcription также ориентир — доля CPU/GPU (транскрибация и
суммаризация — тяжёлые по вычислениям шаги, docs/deploy/hardware-profiles.md);
notify почти всегда I/O-bound (SMTP) — реплики полезны в первую очередь
при большой аудитории рассылок (закреплённые конференции с длинным списком
участников/приглашённых).