Любой процесс на машине - потенциальный источник отказа в обслуживании. Утёкшая память, форк-бомба, скрипт в бесконечном цикле, прожорливый сборщик мусора - и хост перестаёт отвечать, OOM-killer начинает убивать не тех, кого вы хотели, а SSH-сессия зависает в самый неподходящий момент. В этом уроке мы разберём, как заранее очертить каждому сервису его клетку по CPU, памяти, числу процессов и операциям ввода-вывода. Инструменты: cgroups v2 как фундамент, директивы ресурсов systemd как удобный фасад над cgroups, и старая добрая пара ulimit плюс limits.conf для лимитов на уровне сессии PAM.

Как это работает
В современном ядре все ограничения ресурсов сводятся к cgroups version 2 - единой иерархии в /sys/fs/cgroup. В отличие от первой версии с её россыпью раздельных иерархий, в v2 есть одно дерево, и каждый контроллер (cpu, memory, io, pids) включается для поддерева через файл cgroup.subtree_control. Процесс всегда принадлежит ровно одной cgroup, а лимиты наследуются вниз по дереву. На июнь 2026 во всех живых дистрибутивах (Debian 13, Ubuntu 24.04 LTS, RHEL 10, Fedora 41+) по умолчанию работает именно unified-иерархия cgroup v2, а cgroup v1 считается легаси.
systemd - это менеджер cgroups номер один в системе. Каждый юнит он помещает в свою cgroup-ветку (system.slice, user.slice и так далее), поэтому управлять ресурсами проще через директивы юнита, чем руками писать в файлы cgroupfs. Директива MemoryMax ставит жёсткий потолок памяти (превышение ведёт к OOM внутри cgroup), MemoryHigh - мягкий порог, при котором ядро начинает агрессивно отжимать страницы и притормаживать процесс, не убивая его. CPUQuota ограничивает процессорное время (200% значит два полных ядра), TasksMax режет число задач - именно это спасает от форк-бомб. IOWeight и IOReadBandwidthMax управляют дисковым вводом-выводом.
ulimit и /etc/security/limits.conf - это другой, более старый слой. Он опирается на rlimits ядра (системный вызов setrlimit) и применяется через PAM-модуль pam_limits при входе пользователя. Лимиты тут на процесс или на пользователя, а не на cgroup: nproc - максимум процессов пользователя, nofile - дескрипторов на процесс, fsize - размер файла. Это работает только для интерактивных и PAM-сессий, поэтому для системных сервисов всегда предпочитайте директивы systemd - они точнее и применяются гарантированно.
Команды и примеры
Посмотреть, какая версия cgroup активна, и текущее потребление юнита:
Код: Выделить всё
stat -fc %T /sys/fs/cgroup # cgroup2fs = unified v2
systemctl status nginx.service # внизу строка CGroup и Memory/Tasks
systemd-cgtop # top по cgroup: CPU, память, IO
systemd-cgls # дерево cgroup как ls
Код: Выделить всё
systemctl set-property nginx.service MemoryMax=512M CPUQuota=150% TasksMax=200
systemctl show nginx.service -p MemoryMax -p CPUQuota -p TasksMax
Код: Выделить всё
systemctl edit nginx.service
# в открывшийся override.conf пишем:
[Service]
MemoryMax=512M
MemoryHigh=400M
CPUQuota=150%
CPUWeight=50
TasksMax=200
IOWeight=80
systemctl daemon-reload
systemctl restart nginx.service
Код: Выделить всё
systemd-run --scope -p MemoryMax=200M -p TasksMax=50 ./suspicious_script.sh
Код: Выделить всё
ulimit -a # все лимиты текущей сессии
ulimit -u # nproc: число процессов
ulimit -n 8192 # поднять nofile в этой сессии
Код: Выделить всё
# /etc/security/limits.d/90-app.conf
# домен тип элемент значение
appuser hard nproc 1024
appuser soft nproc 512
appuser hard nofile 65536
@devs hard fsize 1048576
Код: Выделить всё
session required pam_limits.so
Код: Выделить всё
[Service]
LimitNOFILE=65536
LimitNPROC=1024
- limits.conf не действует на systemd-сервисы. PAM тут вообще не участвует - пишите LimitNOFILE/LimitNPROC в юните.
- Лимит nproc в limits.conf считается на всего пользователя по всем сессиям, а не на процесс. Поставите 50 - и пятидесятая ssh-сессия того же юзера не залогинится.
- Путаница soft и hard. soft - текущее значение, его можно поднять до hard без рута. hard - потолок, выше которого не прыгнуть без привилегий.
- root по умолчанию игнорирует многие лимиты nproc, поэтому форк-бомба из-под root уложит систему даже при выставленном limits.conf. Спасает только TasksMax на уровне slice/cgroup.
- MemoryLimit устарел - это директива cgroup v1. В v2 используйте MemoryMax и MemoryHigh.
- CPUQuota без понимания процентов: 100% это одно ядро. На 8-ядерной машине 100% сильно недозагрузит сервис.
- Чтобы контроллер cpu или io работал на listed-юните, он должен быть делегирован выше по дереву. Если ограничение тихо игнорируется - проверьте cgroup.controllers и subtree_control родителя.
- Проверьте версию cgroup: stat -fc %T /sys/fs/cgroup. Убедитесь, что это cgroup2fs.
- Создайте простой сервис-нагрузку (например, юнит, запускающий stress-ng или цикл на bash) и запустите его.
- Через systemctl set-property ограничьте его MemoryMax=100M и TasksMax=20. Перезапустите и посмотрите systemd-cgtop.
- Запустите внутри ограниченной cgroup форк-бомбу через systemd-run --scope -p TasksMax=20 и убедитесь, что хост остался жив, а размножение упёрлось в лимит.
- Создайте /etc/security/limits.d/99-lab.conf с hard nproc для тестового пользователя, перелогиньтесь под ним и проверьте ulimit -u.
- Сравните: попробуйте форк-бомбу из-под этого юзера (с лимитом) и убедитесь, что cgroup-лимит надёжнее, чем PAM-лимит.
- Уберите лимиты (systemctl revert), верните стенд в исходное состояние.
- Чем отличается MemoryHigh от MemoryMax и какое поведение ядра стоит за каждым?
- Почему форк-бомба из-под root может пройти limits.conf, и какой механизм её всё же остановит?
- Что означает CPUQuota=250% на машине с четырьмя ядрами?
- В каких случаях лимиты из /etc/security/limits.conf не применяются и почему?
- Чем cgroup v2 принципиально отличается от v1 в части иерархии и делегирования контроллеров?
- Как одной командой запустить произвольный процесс в ограниченной по памяти и числу задач временной cgroup?