Вычислительные ресурсы и память поступают к контейнерам через отдельные ограничители
DevOps

Ограничения CPU и памяти Docker: защита VPS от одного контейнера

Измеряем потребление контейнера, задаём memory, swap, CPU и PIDs limits, диагностируем OOMKilled и переносим проверенные ограничения в Compose.

Содержание

Без явных ограничений контейнер может занять почти всю доступную память и процессорное время хоста. На небольшой VPS зациклившийся процесс способен оставить без ресурсов базу данных, веб-сервер и SSH-сессию администратора. Лимиты уменьшают этот риск, но слишком низкие значения сами становятся причиной отказа.

Что ограничивать и в каком порядке

Сначала измерьте потребление при старте, обычной и пиковой нагрузке. Затем задайте контейнеру предел памяти, решите, разрешено ли ему использовать swap, ограничьте процессорное время и число процессов. После перезапуска проверьте сохранённые настройки, docker stats, задержку ответов, healthcheck и поле OOMKilled. Оставьте ресурсы для ядра, службы Docker и остальных программ VPS.

Снимите исходные показатели

На хосте с Docker откройте интерактивную таблицу потребления ресурсов работающими контейнерами. Выйти из неё можно клавишами Ctrl+C, сами контейнеры продолжат работу:

docker stats

Разовый снимок удобнее сохранить без интерактивного обновления:

docker stats --no-stream \
  --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.PIDs}}'

Наблюдайте не только спокойный процесс. Отдельно проверьте:

  • холодный запуск;
  • прогрев кеша;
  • импорт или миграцию;
  • фоновую задачу;
  • ожидаемый пик запросов;
  • временную недоступность зависимости;
  • корректную остановку и повторный запуск.

Одно среднее значение скрывает короткие пики. Сопоставляйте показатели Docker с состоянием хоста: свободной памятью, swap, средней нагрузкой, дисковыми операциями и сообщениями ядра.

Задайте тестовые пределы через docker run

Пример ограничивает тестовый сервис, а не предлагает готовые production-значения:

docker run -d --name limited-demo \
  --memory=256m \
  --memory-swap=256m \
  --cpus=0.50 \
  --pids-limit=128 \
  -p 127.0.0.1:8080:80 \
  nginx:alpine

Здесь --memory=256m задаёт жёсткий предел оперативной памяти. Равное значение --memory-swap означает, что дополнительный swap контейнеру не предоставляется. OOM означает нехватку памяти: ядро может завершить процесс, когда тот просит больше установленного предела. Такая политика подходит не каждому приложению.

--cpus=0.50 ограничивает доступное процессорное время примерно половиной одного CPU, но ничего не резервирует. Если host перегружен, контейнер может получить меньше. --pids-limit сдерживает бесконтрольное создание процессов и потоков, однако должен учитывать реальную модель приложения.

Проверьте применённые значения

docker inspect limited-demo --format \
  'memory={{.HostConfig.Memory}} swap={{.HostConfig.MemorySwap}} nano_cpus={{.HostConfig.NanoCpus}} pids={{.HostConfig.PidsLimit}}'
docker stats --no-stream limited-demo

Docker показывает часть величин в байтах и NanoCPUs, поэтому не сравнивайте строку механически с исходным YAML. Важно, чтобы значения были ненулевыми и соответствовали рассчитанным пределам.

Проверьте приложение под контролируемой нагрузкой:

curl --fail --silent --show-error http://127.0.0.1:8080/
docker logs --tail 100 limited-demo

Не запускайте генератор нагрузки на production без отдельного окна и ограничений.

Перенесите ограничения в Compose

В compose.yaml те же ограничения записываются рядом с сервисом. Пример ниже задаёт пределы для web; числа остаются учебными и должны быть заменены результатами измерений вашего приложения:

services:
  web:
    image: example/web:tested
    cpus: 0.50
    mem_limit: 256m
    memswap_limit: 256m
    pids_limit: 128

Проверьте поддержку установленной версией и итоговую модель:

docker compose version
docker compose config
docker compose up -d --force-recreate web
docker compose ps web

После пересоздания снова посмотрите docker inspect и docker stats. Не считайте сам факт успешного старта доказательством правильного лимита: нехватка проявляется во время пика, сборки отчёта или обслуживания очереди.

Разберитесь с памятью и swap

--memory-swap имеет смысл только вместе с --memory. Положительное значение задаёт общий объём RAM плюс swap. Например, memory 300m и memory-swap 1g разрешают до 300m RAM и примерно 700m swap при наличии swap на host.

Если --memory-swap не задан, поведение зависит от конфигурации host и Docker. Не полагайтесь на free внутри контейнера: он может показывать swap host, а не фактический доступ cgroup.

Не отключайте OOM killer без веской причины. Docker отдельно предупреждает, что такое отключение без memory limit способно поставить под угрозу host. Правильнее измерить приложение, ограничить его и настроить наблюдение за приближением к пределу.

Подтвердите причину остановки по состоянию и журналу

После неожиданной остановки соберите три независимых признака: состояние контейнера, последние строки приложения и сообщения ядра за тот же период. Команды ничего не перезапускают и не удаляют:

docker inspect limited-demo --format \
  'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{.State.Error}}'
docker logs --tail 200 --timestamps limited-demo
journalctl -k --since '-15 min' | grep -i -E 'oom|out of memory|killed process'

Поле OOMKilled=true — сильный признак, но сохраните также журнал и контекст нагрузки. Exit code 137 может соответствовать SIGKILL, однако сам по себе не доказывает OOM: процесс могли остановить другой командой.

Если контейнер регулярно упирается в память, не увеличивайте предел автоматически. Проверьте утечку, размер кеша, параллелизм, число workers и объём одной задачи. После исправления повторите нагрузочный сценарий.

Проверьте задержки при ограничении процессора

При достижении CPU quota процесс не завершается — scheduler ограничивает его время. Симптомами могут стать рост latency, таймауты и неуспешный healthcheck. Снижение --cpus полезно для защиты соседей, но не заменяет достаточную ёмкость VPS.

CPU shares — относительный вес при конкуренции, а не жёсткий потолок. Для понятного первого ограничения удобнее --cpus; тонкую настройку period/quota и real-time scheduler оставьте задачам, где она измерена и действительно необходима.

Как подобрать значение и откатить

Начните с измеренного пика плюс обоснованный запас. Наблюдайте процентили latency, рестарты, OOM, throttling и свободные ресурсы host. Пересматривайте значения после изменения версии приложения, числа workers и нагрузки.

Если новый предел нарушил работу, верните предыдущую Compose-конфигурацию и пересоздайте только сервис:

docker compose config
docker compose up -d --force-recreate web
docker compose ps web

Не снимайте все ограничения навсегда из-за одной ошибки оценки. Сохраните измерения, скорректируйте конкретный предел и повторите тест.

Механизм контейнерной изоляции объяснён в обзорной статье о Docker. Состояние приложения проверяется через healthcheck, а последствия нехватки памяти на host разобраны в руководстве по swap и OOM.

Источники

Рекламное местоВаша компания здесьРазместить рекламу

Самопроверка

Проверьте, что материал усвоен

Ответьте на все вопросы. Результат сохранится только в этом браузере и будет учтён в статистике прочитанных материалов.

01Что произойдёт при достижении жёсткого memory limit?
02Какая команда показывает текущее потребление работающих контейнеров?
03Почему предел нельзя выбирать по одному спокойному замеру?

Разбираем коротко

Частые вопросы

Есть ли у контейнера лимит памяти по умолчанию?

Нет. Без явного ограничения контейнер может использовать столько памяти, сколько разрешат host и ядро. Поэтому предел выбирают после измерения рабочей и пиковой нагрузки.

Означает ли --cpus=0.5 резервирование половины ядра?

Нет. Это верхняя квота процессорного времени, а не гарантированный резерв. Фактическая производительность также зависит от нагрузки host и других контейнеров.

Как понять, что контейнер завершил OOM killer?

Проверьте поле .State.OOMKilled через docker inspect, exit code, журнал приложения и сообщения ядра. Один exit code без контекста недостаточен для вывода.