Лимиты ресурсов в Docker: ограничение CPU и памяти контейнеров, защита от OOM

Без лимитов один контейнер способен исчерпать всю память хоста или загрузить все ядра процессора, что приведёт к деградации остальных сервисов или к массовому OOM-kill. Docker предоставляет встроенные механизмы ограничения ресурсов на уровне cgroup, которые задаются при запуске контейнера.

Ограничение памяти через docker run​


Флаг --memory (сокращённо -m) задаёт жёсткий потолок оперативной памяти, доступной контейнеру. Если процесс внутри превышает лимит, ядро убивает его через OOM-killer.

Bash:
docker run -d --name app --memory=512m nginx:alpine

Значение принимает суффиксы b, k, m, g. Слишком малый лимит (единицы мегабайт) приведёт к тому, что контейнер не сможет запуститься — процессу не хватит памяти даже на инициализацию.

Мягкий лимит: --memory-reservation​


--memory-reservation задаёт мягкий порог. Ядро старается удерживать потребление ниже этого значения, но не убивает процесс при превышении. Полезен для балансировки нагрузки в условиях нехватки памяти на хосте.

Bash:
docker run -d --memory=1g --memory-reservation=768m myapp

Здесь контейнер может использовать до 1 ГБ, но при давлении на память хоста ядро начнёт освобождать страницы этого контейнера раньше, чем контейнеров с более высоким reservation.

Ограничение swap: --memory-swap​


Флаг --memory-swap задаёт общий лимит RAM + swap. Поведение зависит от соотношения с --memory:

Значение --memory-swapПоведение
Не заданОбщий лимит RAM + swap равен 2 × --memory, то есть swap доступен в объёме --memory
-1Swap не ограничен
0Трактуется как «не задан» — применяется поведение по умолчанию
Равен --memorySwap полностью отключён: контейнер может использовать только RAM
Больше --memorySwap доступен в объёме --memory-swap минус --memory

Bash:
## Запретить swap полностью: memory-swap = memory
docker run -d --memory=256m --memory-swap=256m myapp

## Разрешить 256 МБ swap поверх 512 МБ RAM
docker run -d --memory=512m --memory-swap=768m myapp

Если --memory-swap меньше --memory, Docker выдаст ошибку при запуске.

Отключение OOM-kill: --oom-kill-disable​


Флаг --oom-kill-disable предотвращает убийство процесса ядром при превышении лимита. Вместо этого процесс будет заблокирован до освобождения памяти. Использовать с осторожностью: контейнер может зависнуть навсегда.

Bash:
docker run -d --memory=512m --oom-kill-disable myapp

Рекомендуется только в связке с --memory, иначе процесс может исчерпать всю память хоста без каких-либо последствий для себя.

Ограничение CPU​


Доля процессорного времени: --cpus​


Флаг --cpus задаёт количество ядер (или долю ядра), доступных контейнеру. Значение дробное.

Bash:
## Ограничить контейнер половиной одного ядра
docker run -d --cpus=0.5 myapp

## Ограничить двумя ядрами
docker run -d --cpus=2 myapp

Механизм работает через CFS bandwidth control в cgroup: контейнер получает квоту процессорного времени в рамках каждого периода планирования (по умолчанию 100 мс). При --cpus=0.5 квота составляет 50 мс из каждых 100 мс.

Привязка к конкретным ядрам: --cpuset-cpus​


Если нужно зафиксировать контейнер на определённых ядрах (например, для NUMA-оптимизации или изоляции от других нагрузок):

Bash:
## Только ядра 0 и 1
docker run -d --cpuset-cpus="0,1" myapp

## Ядра с 0 по 3
docker run -d --cpuset-cpus="0-3" myapp

Относительный вес: --cpu-shares​


--cpu-shares задаёт относительный приоритет при конкуренции за CPU. Значение по умолчанию — 1024. Если два контейнера конкурируют за одно ядро и у одного --cpu-shares=512, а у другого --cpu-shares=1024, второй получит примерно вдвое больше времени. При отсутствии конкуренции лимит не применяется.

Bash:
docker run -d --cpu-shares=512 low-priority-app

В отличие от --cpus, этот флаг не задаёт абсолютный потолок — только пропорцию при конкуренции.

Лимиты в Docker Compose​


В docker-compose.yml ограничения задаются по-разному в зависимости от режима запуска.

Standalone-режим (docker compose up без Swarm)​


YAML:
services:
  web:
    image: nginx:alpine
    mem_limit: 512m
    mem_reservation: 384m
    cpus: 1.5
    memswap_limit: 512m
    oom_kill_disable: false

Swarm-режим (deploy)​


YAML:
services:
  web:
    image: nginx:alpine
    deploy:
      resources:
        limits:
          cpus: '1.5'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M

В Swarm-режиме reservations используются для планирования: менеджер не разместит задачу на ноде, где свободных ресурсов меньше, чем указано в reservation.

Обновление лимитов без пересоздания контейнера​


Команда docker update позволяет изменить лимиты работающего контейнера без его остановки:

Bash:
docker update --memory=1g --cpus=2 myapp

Ограничения: --memory нельзя безопасно уменьшить ниже текущего потребления без риска немедленного OOM-kill. Для --cpus изменение применяется мгновенно через cgroup.

Диагностика OOM и превышения лимитов​


Проверка через docker inspect​


Bash:
docker inspect --format='{{.State.OOMKilled}}' myapp

Возвращает true, если контейнер был убит OOM-killer'ом.

Логи ядра​


Bash:
dmesg | grep -i "oom|killed process"

Типичная строка:

Код:
Memory cgroup out of memory: Killed process 12345 (java) total-vm:1048576kB, anon-rss:524288kB

Статистика в реальном времени​


Bash:
docker stats myapp

Показывает текущее потребление CPU, памяти, сети и дискового I/O. Колонка MEM % отображает долю от лимита (если он задан) или от общей памяти хоста.

Файлы cgroup​


Для детальной диагностики можно читать файлы cgroup напрямую. Путь зависит от версии cgroup (v1 или v2) и идентификатора контейнера:

Bash:
## cgroup v2 (современные дистрибутивы)
cat /sys/fs/cgroup/system.slice/docker-<container_id>.scope/memory.current
cat /sys/fs/cgroup/system.slice/docker-<container_id>.scope/memory.max

memory.current — текущее потребление, memory.max — установленный лимит (или max, если лимит не задан).

Типичные ошибки и их последствия​


ОшибкаЧто происходитРешение
--memory задан, но приложение использует JVM/Go без ограничения кучиПроцесс не знает о лимите контейнера, аллоцирует больше, получает OOMПередать -Xmx для JVM или GOMEMLIMIT для Go
--memory-swap равен --memory при активной работе с дискомЛюбая потребность в подкачке приводит к мгновенному OOMЛибо разрешить swap, либо увеличить --memory
--oom-kill-disable без --memoryПроцесс может исчерпать память хостаВсегда комбинировать с жёстким лимитом
--cpus=0.1 для CPU-интенсивной задачиЗадача выполняется крайне медленно, таймаутыУвеличить лимит или вынести на отдельный хост
Лимит задан только в Compose, но контейнер запущен через docker runЛимиты из Compose не применяютсяИспользовать единый способ запуска
--memory-swap=0 в ожидании отключения swapSwap не отключается — значение трактуется как «не задан»Для отключения swap установить --memory-swap равным --memory

JVM и Go: настройка под лимит контейнера​


Java-приложения исторически не учитывали cgroup-лимиты. Начиная с JDK 8u191+ и JDK 10+ флаг -XX:+UseContainerSupport (включён по умолчанию) заставляет JVM читать лимит из cgroup и рассчитывать MaxHeapSize автоматически. Для старых версий или точного контроля:

Bash:
docker run -d --memory=1g myapp java -Xmx768m -jar app.jar

Для Go начиная с версии 1.19 доступна переменная окружения GOMEMLIMIT, которая задаёт мягкий лимит памяти для garbage collector:

Bash:
docker run -d --memory=512m -e GOMEMLIMIT=480MiB myapp

Рекомендация: оставляйте запас 10–20% между GOMEMLIMIT/-Xmx и --memory на стек, метаданные и аллокации вне кучи.

Ограничение дискового I/O и PID​


Помимо CPU и памяти, Docker позволяет ограничить скорость чтения/записи на блочное устройство и количество процессов:

Bash:
## Ограничить запись на /dev/sda до 10 МБ/с
docker run -d --device-write-bps /dev/sda:10mb myapp

## Лимит количества процессов внутри контейнера
docker run -d --pids-limit=100 myapp

--pids-limit защищает от fork-бомб. Значение -1 снимает ограничение (поведение по умолчанию).

Проверка результата после настройки​


После применения лимитов убедитесь, что они действительно активны:

Bash:
docker inspect myapp --format='Memory: {{.HostConfig.Memory}}, CPUs: {{.HostConfig.NanoCpus}}'

NanoCpus возвращает значение в наносекундах: 1500000000 соответствует --cpus=1.5. Если Memory равен 0, лимит не задан.

Альтернативно — через docker stats под нагрузкой: потребление не должно превышать заданный порог.

Когда лимиты не спасают​


Ограничения ресурсов решают проблему изоляции, но не заменяют мониторинг и алертинг. Если контейнер регулярно упирается в потолок памяти, это сигнал к масштабированию или оптимизации, а не к бесконечному увеличению лимита. Комбинируйте лимиты с healthcheck'ами Healthcheck в Docker Compose: как отличить запущенный контейнер от реально работающего сервиса и настройте алерты на OOMKilled в системе мониторинга, чтобы ловить деградацию до полного отказа Docker-контейнер постоянно перезапускается: как диагностировать restart loop.

Источники​


 
Назад
Верх Низ