Learning Platform
Глоссарий Troubleshooting
Урок 13.05 · 24 мин
Средний
dockercomposescaleresourcesrestart

Scale, resources, restart_policy

В реальной работе compose-стенда часто нужны три вещи: запустить N копий одного worker’а (для параллелизма), ограничить ресурсы каждого контейнера (чтобы один не съел всю память), и настроить restart-policy (чтобы упавший контейнер сам поднимался). В этом уроке закрываем эти три темы. После него у тебя есть полный compose-toolkit для compose-стенда production-уровня.


—scale: несколько копий одного сервиса

services:
  worker:
    image: myorg/celery-worker
    depends_on:
      - redis
    environment:
      CELERY_BROKER_URL: redis://redis:6379/0
docker compose up -d --scale worker=3

Compose создаст три контейнера: myproj-worker-1, myproj-worker-2, myproj-worker-3. Все три из одного образа, с одной конфигурацией. Caждый берёт задачи из очереди Redis независимо.

Можно зафиксировать в compose:

services:
  worker:
    image: myorg/celery-worker
    deploy:
      replicas: 3

Но в compose (non-swarm) replicas работает как hint — --scale его перебивает. В swarm-режиме replicas — авторитет.

Requests и limits: основа управления ресурсами в k8s

Что НЕ масштабируется

--scale работает только для stateless сервисов. Не пробуй:

docker compose up --scale postgres=3
# Контейнеры создадутся, но все попытаются забиндить 5432 на хост — два упадут.
# Они все смонтируют один volume — конфликт.

Stateful сервисы (БД, очереди, координаторы) требуют другой подход — кластеризацию (Postgres-Citus, MinIO distributed, Kafka brokers), а это компетенция уже не базового compose.

Условия для скейлинга

  • Сервис не должен публиковать порт фиксированно (-p 8000:8000 сломает, потому что hosts:8000 один). Альтернатива — ports: ["8000-8010:8000"] (диапазон) или без публикации.
  • Сервис не должен зависеть от уникального имени hostname.
  • Должна быть очередь или балансировщик, чтобы workers распределяли работу.
services:
  web:
    image: myorg/api
    ports:
      - "8000-8010:8000"   # range — каждая копия получает свой порт хоста

  worker:
    image: myorg/celery-worker
    # без ports — стандартный stateless паттерн

deploy.resources: лимиты и резервации

services:
  app:
    image: myapp
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
  • limits — максимум, который контейнер может использовать. Если попытается больше — CPU будет throttled, память — OOM kill.
  • reservations — минимум гарантированных ресурсов. Если хост не имеет столько свободно — контейнер не стартует.

CPU

cpus: '0.5' — половина одного CPU-core. cpus: '2' — два полных core. На уровне ядра реализуется через cgroups CPU quota.

Memory

memory: 512M — 512 мегабайт. 512m, 512MB, 512MiB — тоже понимается. Поддерживаются K, M, G суффиксы.

Где это работает

  • В swarm-mode deploy.resources — авторитетная декларация.
  • В compose standalone (наша область) — работает, но через прокси-mapping в нативные docker run флаги --cpus, --memory. Есть мелкие отличия от swarm в edge-cases.
WARNING

В compose v2 для standalone-стенда deploy.resources работают, но раньше (compose v1) — нет. Если ты на v1.29 — лимиты надо указывать через cpus: и mem_limit: на верхнем уровне сервиса. На v2 — лучше через deploy.resources, формат более явный.


restart policies

services:
  app:
    image: myapp
    restart: unless-stopped

Опции:

restartПоведение
no (default)Никогда не перезапускать
alwaysВсегда перезапускать, даже после успешного завершения
on-failureТолько при exit code != 0
unless-stoppedПерезапускать, кроме случая когда ты сам остановил контейнер

Когда что использовать

  • no: one-shot job, миграция, скрипт. Запустился, отработал, завершился.
  • always: реже всего. Бывает нужен для daemon, который должен работать даже после docker stop.
  • on-failure: long-running job, которая должна переподниматься на ошибке, но не должна крутиться, если успешно завершилась.
  • unless-stopped: главное для production-сервисов. Подняли — работает, упало — поднимется. Остановили вручную — лежит.

on-failure с лимитом

restart: on-failure:5

Перезапустить максимум 5 раз. После 5-го фейла остановится. Полезно, чтобы не было infinite-loop при битой конфигурации.

В docker run / docker-compose v2 это --restart on-failure:5, в синтаксисе compose-файла поле restart поддерживает суффикс через двоеточие.


Полный пример

Production-like compose для DE-стенда с worker pool, лимитами и restart:

services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    volumes:
      - pgdata:/var/lib/postgresql/data
    secrets:
      - db_password
    restart: unless-stopped
    deploy:
      resources:
        limits: { cpus: '2', memory: 2G }
        reservations: { memory: 1G }
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    deploy:
      resources:
        limits: { memory: 512M }

  worker:
    image: myorg/celery-worker:1.0
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      CELERY_BROKER_URL: redis://redis:6379/0
      DATABASE_URL: postgresql://postgres@postgres:5432/etl
      DATABASE_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    restart: on-failure:3
    deploy:
      replicas: 2
      resources:
        limits: { cpus: '1', memory: 1G }

secrets:
  db_password:
    file: ./secrets/db_password.txt

volumes:
  pgdata:
docker compose up -d
docker compose ps
# postgres ... healthy
# redis ... up
# worker-1 ... up
# worker-2 ... up

# Скейл worker'ов на лету:
docker compose up -d --scale worker=4
docker compose ps
# worker-1, worker-2, worker-3, worker-4 — все up.

# Скейл вниз:
docker compose up -d --scale worker=1
docker compose ps
# Только worker-1.

Мониторинг ресурсов

docker stats

Live-таблица, показывающая CPU/Memory/Net/Disk I/O каждого контейнера в реальном времени. Полезно понять, кто упирается в лимит.

docker stats --no-stream myproj-worker-1
# Один snapshot, без watch-режима.
docker compose stats
# В compose v2 (новее ~2.30) — то же, но автоматически для всех сервисов стенда.

OOM kill: что когда происходит

Если контейнер пытается использовать память больше limits.memory, ядро его убивает (SIGKILL):

docker compose ps
worker-2  ... Exited (137)

137 = 128 + 9 = SIGKILL. В логах ядра (dmesg) видно событие OOM с PID.

В docker inspect:

docker inspect worker-2 | jq '.[0].State'
# {
#   "OOMKilled": true,
#   "ExitCode": 137,
#   ...
# }

Это сигнал: либо увеличить лимит, либо чинить утечку памяти в приложении.

Lifecycle сервиса с лимитами и restart
docker compose upКонтейнер запускается с deploy.resources.limits через cgroups
memory растёт
App жрёт памятьПайплайн с большим pandas-датафреймом — память растёт. Контейнер пока в пределах limit
Memory > limitЯдро видит превышение, шлёт OOM. SIGKILL без graceful shutdown. ExitCode = 137
restart
restart: on-failureCompose видит exit code != 0, перезапускает контейнер. Если 137 происходит постоянно — нужно поднять лимит или фиксить утечку
restart counterПосле 5-го раза подряд (если on-failure:5) compose сдаётся. Контейнер останется в Exited state
Manual interventiondocker compose logs показывает причину OOM. Нужно либо увеличить limit, либо чинить код, либо принять что workload слишком большой

CPU throttling vs OOM

CPU-лимит работает мягко: контейнер не получает больше CPU-time, чем лимит, но не убивается. Просто медленно работает. Это легко спутать с «контейнер завис»:

docker stats myproj-app-1
# CPU %  500%  означает, что контейнер реально использует 5 cores

Если CPU limit '1', а stats показывает 100% — это означает «использует свой полный лимит, throttled на пределе». Решение — либо поднять limit, либо оптимизировать код, либо смириться, что приложение тормозит.


Попробуй сам

mkdir -p scale-demo && cd scale-demo

cat > compose.yml <<'YAML'
services:
  redis:
    image: redis:7-alpine

  worker:
    image: alpine:3.20
    depends_on: [redis]
    command: sh -c 'while true; do echo "$$(hostname) tick"; sleep 5; done'
    deploy:
      resources:
        limits: { memory: 64M }
    restart: on-failure
YAML

# 1. Один worker.
docker compose up -d
docker compose ps
# worker-1

# 2. Scale до 3.
docker compose up -d --scale worker=3
docker compose ps
# worker-1, worker-2, worker-3

# 3. Логи всех.
docker compose logs --tail 3 worker
# worker-1: <hostname-1> tick
# worker-2: <hostname-2> tick
# worker-3: <hostname-3> tick

# 4. docker stats (Ctrl+C для выхода).
docker stats --no-stream $(docker compose ps -q worker)

# 5. Scale обратно к 1.
docker compose up -d --scale worker=1
docker compose ps
# Только worker-1.

# 6. Memory limit demo. Создадим контейнер, который жрёт память.
docker run --rm --memory 100M alpine sh -c 'apk add --no-cache stress-ng 2>/dev/null; stress-ng --vm 1 --vm-bytes 200M --timeout 5s' || echo "killed"
# OOM kill — потому что 200M > 100M limit.

# Cleanup.
docker compose down
cd .. && rm -rf scale-demo
TIP

Production-практика: для compose-стенда DE-данных всегда ставь limits.memory явно. Хост с 16 GB RAM, на котором крутятся Airflow + Postgres + Kafka, без лимитов запросто впадает в OOM-каскад, когда одно приложение скушает всё, ядро убивает других, цепной отказ. С лимитами — поведение предсказуемо: OOM убивает виновного, остальные работают.

На этом модуль advanced compose завершён. У тебя теперь полный набор: профили, override-файлы, secrets, scale + resources, restart policies. В следующем модуле — реальные data-services: Postgres, MinIO, Redis локально с типичными настройками.


Проверка знанийKnowledge check
У тебя в compose: worker с --scale worker=3 , postgres для метаданных, redis как broker. Что произойдёт, если попробовать --scale postgres=3 ? Объясни, почему worker scale работает, а postgres scale нет, и какие 3 типичные ограничения нужно соблюсти, чтобы scale работал для сервиса.
ОтветAnswer
Worker scale работает, postgres scale ломается. Worker — stateless: не публикует порт фиксированно, не привязан к volume, читает задачи из redis (брокер сам распределяет), три copy просто берут задачи параллельно. Postgres scale через --scale=3 создаст три контейнера, но: (1) все попытаются забиндить хост-порт 5432 — два контейнера упадут с "address already in use"; (2) все смонтируют один named volume pgdata — параллельная запись в Postgres data dir = corruption; (3) даже если бы порты и volume решили — Postgres не знает, что есть его клоны, нет встроенной координации между ними. Postgres requires кластеризацию через специализированное решение (Patroni, Citus, replication). Три ограничения для scalable сервиса: (1) Stateless — не зависит от persistent local state, всё state во внешнем хранилище (БД, S3, redis); (2) Без жёсткого порт-binding — либо без публикации (внутреннее общение через service-name), либо port range ports: ["8000-8010:8000"]; (3) Без shared volume на запись — либо каждая копия имеет свой volume, либо все читают одно read-only mount.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Почему docker compose up --scale worker=3 работает для celery worker'а, а --scale postgres=3 — нет?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5