ZIM-НИЙ SAAALEЗимние скидки: до −50% на старт и −20% на продление
до 31.01.2026 Подробнее
Выберите продукт

cgroups v2 и systemd: лимиты CPU, RAM и I/O для Docker в production

Пошагово разбираем, как на Linux с cgroups v2 ограничивать Docker-хост через systemd: проверка режима cgroup, лимиты памяти (MemoryHigh/MemoryMax), CPU (CPUQuota/CPUWeight), диска (IOWeight) и отладка. Примеры команд и типовые ошибки.
cgroups v2 и systemd: лимиты CPU, RAM и I/O для Docker в production

Зачем вам cgroups v2, если есть Docker

В production лимиты ресурсов важны не потому, что «так правильно», а потому что без них один неудачный контейнер способен уронить весь узел: забить память до OOM, выжечь CPU на 100%, устроить I/O-шторм и сделать сервисы «живыми, но бесполезными». В Linux за изоляцию ресурсов отвечает механизм control groups — cgroups.

Docker умеет ставить лимиты контейнерам, но на современных дистрибутивах всё чаще используется cgroups v2, а роль «дирижёра» берёт на себя systemd resource control. Связка systemd + cgroups v2 позволяет вводить production limits не только для контейнеров, но и для демона Docker/containerd, групп сервисов, отдельных systemd-юнитов и целых «слайсов».

Дальше — практичные шаги: как понять, что у вас cgroups v2, как посмотреть текущие лимиты, и как правильно ограничивать Docker-хост через systemd (CPU/RAM/IO) так, чтобы изменения были предсказуемыми и сохранялись после перезагрузки.

Как понять, что включён cgroups v2

Главный признак: смонтирован единый cgroup2 и systemd активно им управляет.

stat -fc %T /sys/fs/cgroup

Если команда вернёт cgroup2fs — это v2.

Ещё одна быстрая проверка:

mount | grep cgroup

В режиме v2 вы увидите строку с type cgroup2 для /sys/fs/cgroup.

И полезно сразу посмотреть, где systemd разместил Docker в дереве и разрешил ли делегирование:

systemctl show docker.service --property=ControlGroup --property=Delegate --property=Slice

Поле ControlGroup покажет путь, а Delegate=yes важно для контейнерных рантаймов (почему — разберём ниже).

Чтобы изолировать «шумные» сборки/воркеры от остальной инфраструктуры, часто помогает вынести контейнерные задачи на отдельный VDS и уже там жёстко зафиксировать лимиты по CPU/RAM/IO.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

После внедрения лимитов обязательно прогоните нагрузочные сценарии (сборки, бэкапы, пики трафика) и проверьте, что хост остаётся отзывчивым для SSH и системных демонов.

Проверка свойств docker.service и текущих лимитов через systemctl

Базовая модель: кто кому ставит лимиты

С точки зрения ядра, лимиты применяются к cgroup. С точки зрения администратора, чаще всего вы применяете их:

  • к системному сервису (например, docker.service или containerd.service);
  • к «слайсу» (группе сервисов, например, system.slice или кастомному docker.slice);
  • к отдельному контейнеру (через Docker flags / Compose), что тоже превращается в параметры cgroup.

Ключевой момент: лимит, заданный выше по дереву (на уровне сервиса/слайса), ограничивает сумму потребления всех дочерних процессов. Поэтому для production обычно работает двухуровневая стратегия:

  • «Потолок» на уровне Docker/контейнерного пула (systemd-юнит или slice), чтобы узел не умер полностью.
  • Более строгие лимиты на уровне отдельных контейнеров/сервисов, чтобы они не мешали друг другу.

Проверяем текущие ограничения: systemd, cgroup, Docker

1) Посмотреть лимиты systemd у юнита

systemctl show docker.service -p MemoryMax -p MemoryHigh -p CPUQuota -p CPUWeight -p IOWeight -p TasksMax

Если значения пустые или infinity — systemd не ограничивает этот юнит.

2) Посмотреть потребление ресурсов (быстро и наглядно)

systemd-cgtop

Удобный способ увидеть, какие cgroup реально «едят» CPU/память прямо сейчас.

3) Проверить, что Docker реально работает поверх cgroups v2

docker info | grep -i cgroup

Обычно вас интересует строка Cgroup Version. Если там 2 — хорошо. Если 1 — на узле могут быть включены legacy-настройки ядра или вы на старом дистрибутиве.

Важно: «включён cgroups v2» и «Docker использует v2» — не всегда одно и то же на нестандартных конфигурациях. В production лучше один раз зафиксировать это проверками выше и задокументировать для команды.

Если вы дополнительно раскладываете сервисы по слайсам (например, выносите пул PHP-FPM/веба отдельно), пригодится разбор подхода со слайсами и иерархией: лимиты через systemd slices и иерархию cgroups.

Лимиты памяти: MemoryMax, MemoryHigh и OOM-поведение

Память — главный источник внезапных аварий. В cgroups v2 есть несколько уровней контроля. На практике чаще всего используют:

  • MemoryMax — жёсткий предел; при превышении начнутся OOM-киллы в рамках cgroup.
  • MemoryHigh — «мягкий» порог; ядро начинает давить (reclaim), но не убивает сразу.
  • MemorySwapMax — ограничение swap (если swap включён).

Почему MemoryHigh полезен: он даёт системе шанс «притормозить» нарушителя без мгновенного убийства. Для сервисов с кэшами (reverse proxy, приложения с JIT/кэшированием) это часто более предсказуемо, чем жизнь на грани OOM.

Временная установка лимита (без правки файлов)

sudo systemctl set-property docker.service MemoryHigh=6G MemoryMax=8G

Проверяем:

systemctl show docker.service -p MemoryHigh -p MemoryMax

На практике это удобно, когда вы вводите лимиты «аккуратно»: ставите пороги, наблюдаете метрики/логи, и только потом фиксируете конфигурацию на диске.

Постоянная установка лимита через drop-in

sudo systemctl edit docker.service

Добавьте:

[Service]
MemoryHigh=6G
MemoryMax=8G

Применяем изменения:

sudo systemctl daemon-reload
sudo systemctl restart docker.service

Если на узле крутятся критичные контейнеры, перезапуск Docker может быть нежелателен: он обычно остановит контейнеры. В этом случае используйте set-property на живой системе, а «окно» на перезапуск планируйте отдельно, чтобы зафиксировать конфигурацию.

Частая ошибка: лимит поставили, а «память всё равно кончается»

Проверьте три вещи:

  • Лимит стоит именно на том юните/слайсе, где реально находятся процессы контейнеров (сверяйте systemctl show -p ControlGroup и /proc/PID/cgroup).
  • Вы не путаете память cgroup и «общесистемные» вещи (например, сильный файловый кэш или память, которую съедают процессы вне ограниченной cgroup).
  • Не «съедает» память другой пул процессов (например, база данных на хосте без ограничений в system.slice).

Лимиты CPU: CPUQuota, CPUWeight и ожидания от них

CPU в systemd обычно ограничивают двумя подходами:

  • CPUQuota — жёсткая квота в процентах от одного ядра (в терминах systemd). Например, 200% примерно означает «два ядра».
  • CPUWeight — относительный вес (share), работает при конкуренции: кто важнее, тому больше.

Для production чаще выбирают CPUQuota как «страховочный потолок», а веса используют для приоритизации (например, веб важнее фоновых задач).

Пример: ограничить Docker-пул до 3 ядер

sudo systemctl set-property docker.service CPUQuota=300%

Проверка:

systemctl show docker.service -p CPUQuota -p CPUQuotaPerSecUSec

Нюанс: кратковременные «спайки» CPU

Даже при CPUQuota кратковременные всплески возможны на уровне планировщика и статистики (особенно если меряете «в моменте»). Корректнее оценивать на интервале по метрикам и сравнивать с ожидаемым профилем нагрузки.

I/O и дисковая справедливость: IOWeight и реальные ограничения

В cgroups v2 с systemd вы часто видите IOWeight. Это вес для распределения дискового времени между группами, а не жёсткий лимит мегабайт/сек.

Почему это важно: IOWeight помогает, когда несколько групп конкурируют за один диск. Если диск свободен — высокий вес не ускорит, низкий не замедлит. Но когда начинается «толкотня», веса решают, кто «душится» первым.

Пример: дать Docker меньший вес, чем системным сервисам

sudo systemctl set-property docker.service IOWeight=200

Диапазон веса у systemd — от 1 до 10000. Если вы хотите, чтобы сервисы на хосте (например, база данных или SSH) оставались отзывчивыми при бэкапах/сборках внутри контейнеров, вы снижаете вес Docker-пула или, наоборот, повышаете вес критичных сервисов в отдельном slice.

Как проверить эффект

Проверка «на глаз» — через latency и очередь диска. Минимальный набор:

iostat -xz 1

Если видите высокий await и загрузку диска под 100% при «шуме» от контейнеров — веса и разнесение по cgroup часто помогают стабилизировать хост.

Делегирование и Docker: почему важен Delegate

Контейнеры сами создают вложенные cgroup для процессов внутри. Чтобы systemd не мешал этому, для сервиса Docker обычно включают делегирование: Delegate=yes. В большинстве современных пакетов Docker это уже сделано, но при кастомных юнитах/альтернативных рантаймах встречаются сюрпризы.

Проверьте:

systemctl show docker.service -p Delegate

Если Delegate=no, возможны странные эффекты: контейнеры не могут корректно управлять подгруппами, а лимиты применяются не так, как вы ожидаете. Исправление зависит от вашей схемы запуска Docker (пакетный юнит или свой), но общий принцип один — делегировать управление поддеревом контейнерному рантайму.

Куда ставить лимиты: docker.service или отдельный slice

Ограничивать прямо docker.service — быстро и просто. Но в production часто удобнее сделать отдельный slice (например, docker.slice) и запускать в нём связанные сервисы (docker, containerd, иногда buildkit), чтобы управлять потолком «для контейнерного мира» централизованно.

Практический плюс slice: вы меньше привязаны к конкретному названию юнита и можете переносить политику между узлами, где docker/containerd могут отличаться.

Пример: политика лимитов для docker.slice

sudo systemctl edit docker.slice

Содержимое:

[Slice]
CPUQuota=400%
MemoryHigh=10G
MemoryMax=12G
IOWeight=200

Дальше нужно убедиться, что нужные сервисы реально живут в этом slice. Для юнита это задаётся параметром Slice= (обычно в секции [Service]), например через drop-in для конкретного сервиса.

Если вы впервые вводите ограничения на узле, начинайте с мягких порогов (MemoryHigh, веса CPU/IO) и наблюдайте. Жёсткие потолки (MemoryMax, CPUQuota) вводите после того, как собрали базовую статистику потребления.

Пример drop-in конфигурации для docker.slice с лимитами CPU и памяти

Защита файловой системы: ProtectSystem в связке с Docker

ProtectSystem — это не про cgroups, но это важная часть «production hardening» в systemd. Параметр делает части файловой системы доступными только для чтения для процесса сервиса.

Где это может быть полезно: для вспомогательных демонов вокруг Docker, для самописных воркеров, для сервисов на хосте. Но к самому docker.service применять ProtectSystem=strict чаще всего нельзя без тщательной настройки: Docker должен писать в свои каталоги (например, /var/lib/docker), работать с сокетами и т.д.

Тем не менее, понимать механику полезно: если вы запускаете отдельный systemd-юнит для обслуживания (лог-шейпер, агент бэкапов, сбор метрик), то ProtectSystem вместе с ReadWritePaths и NoNewPrivileges может заметно снизить последствия компрометации.

Пример: жёсткая защита для утилитарного сервиса

[Service]
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/myapp
NoNewPrivileges=true

Для Docker-хоста это чаще применяют не к Docker, а к окружению: агентам, синхронизаторам, экспортерам, где запись нужна в 1–2 каталога.

Как отлаживать: почему лимиты «не сработали»

Проверяем, что процессы действительно внутри нужной cgroup

Найдите PID интересующего процесса (например, dockerd) и посмотрите его cgroup:

pidof dockerd
cat /proc/$(pidof dockerd | awk '{print $1}')/cgroup

В v2 вы увидите одну строку с путём. Сверьте её с ControlGroup у соответствующего юнита.

Смотрим параметры через systemctl, а не «на глаз»

Частый источник путаницы — вы измеряете «всю систему», а ограничили только часть. Для корректной картины используйте:

systemctl status docker.service
systemctl show docker.service -p Slice -p ControlGroup
systemd-cgtop

Ловим OOM-события и причины

Если сработал OOM внутри cgroup, логика обычно такая: лимит памяти достигнут, ядро выбирает «жертву» в рамках группы. Ищите следы:

journalctl -k -g oom
journalctl -u docker.service --since "-1h"

Полезно также понимать, включён ли на системе дополнительный менеджер давления памяти (например, systemd-oomd), но это отдельная тема. Здесь важно: MemoryMax — это не предупреждение, это «стена».

Практические рекомендации для production limits на Docker-хосте

  • Сначала зафиксируйте базовую телеметрию: хотя бы пики RAM/CPU по узлу и по контейнерам, иначе лимиты будут «с потолка».
  • Делайте два уровня: общий потолок на пул контейнеров (юнит/слайс) и точные лимиты на критичные контейнеры.
  • Память ограничивайте аккуратно: начните с MemoryHigh, затем добавляйте MemoryMax с запасом.
  • CPU: потолок + приоритеты: CPUQuota как «не дать съесть всё», CPUWeight для справедливости.
  • I/O: это про конкуренцию: IOWeight помогает, когда диск становится узким местом, но не заменяет правильную архитектуру хранения.
  • Безопасность systemd применяйте точечно: ProtectSystem отлично подходит для «утилитарных» сервисов, но для Docker требует продуманного списка разрешённых путей.

Если вместе с лимитами вы наводите порядок и по транспортному уровню (HTTPS, mTLS в отдельных компонентах, безопасные панели), проще централизованно закрывать такие задачи через нормальные SSL-сертификаты и не держать «временные» самоподписанные решения в production.

FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

Мини-чеклист: что проверить после внедрения

  1. После перезагрузки лимиты сохранились (drop-in на месте): systemctl show ....
  2. Процессы действительно в нужном slice/cgroup: /proc/PID/cgroup, systemctl show -p ControlGroup.
  3. При нагрузке узел остаётся отзывчивым: SSH не «умирает», системные сервисы не голодают по CPU/IO.
  4. При достижении лимитов поведение ожидаемое: нет лавины рестартов, есть понятные логи OOM/pressure.

Итог

Связка cgroups v2 + systemd resource control — практичный способ навести порядок с ресурсами на Docker-хосте: задать понятные production limits, защитить узел от «плохих» контейнеров и сделать поведение предсказуемым. Начинайте с наблюдения и мягких ограничений, фиксируйте конфиг через drop-in, и постепенно вводите жёсткие потолки там, где они действительно нужны.

Если вы ещё и усиливаете изоляцию контейнеров (дополнительные sandbox-рантаймы), посмотрите обзор подходов и компромиссов: изоляция контейнеров через gVisor и Firecracker: подходы и компромиссы.

Поделиться статьей

Вам будет интересно

Apache HTTP/2: h2, ALPN и PHP-FPM через proxy_fcgi — настройка и диагностика OpenAI Статья написана AI (GPT 5)

Apache HTTP/2: h2, ALPN и PHP-FPM через proxy_fcgi — настройка и диагностика

Пошагово включаем HTTP/2 в Apache через mod_http2: как работает h2 и ALPN, какие требования к TLS и MPM, как правильно проксироват ...
Apache HTTP/2 (h2), ALPN и PHP-FPM: настройка и типовые проблемы OpenAI Статья написана AI (GPT 5)

Apache HTTP/2 (h2), ALPN и PHP-FPM: настройка и типовые проблемы

Пошагово включаем HTTP/2 (h2) в Apache: проверяем mod_http2, работу ALPN и настройки TLS, задаём Protocols во vhost’ах и подключае ...
Nginx allow/deny и IPv6: как ограничить доступ к админке и не заблокировать себя OpenAI Статья написана AI (GPT 5)

Nginx allow/deny и IPv6: как ограничить доступ к админке и не заблокировать себя

Разбираем, как в Nginx работают allow/deny для IPv4 и IPv6, какие префиксы указывать и почему «IPv6 allow» часто не совпадает с ре ...