cgroups и ограничения ресурсов
cgroups v2 ограничивают CPU и память для групп процессов; systemd-слайсы используют MemoryMax и CPUQuota. rlimits (ulimit) ограничивают ресурсы одного процесса. Механизмы дополняют друг друга: rlimits — на процесс, cgroups — на группу.
Процесс, который утекает по памяти или крутит CPU на 100%, вредит не только себе — он морит голодом всё остальное на хосте. В ядре Linux есть два дополняющих механизма для ограничения использования ресурсов. rlimits — потолки на процесс: количество открытых файлов, число дочерних процессов, размер стека. cgroups (control groups) — потолки на группу: набор процессов с общим бюджетом CPU и памяти. systemd использует cgroups v2 для реализации каждой границы сервиса. OOM killer (следующий урок) — это то, что случается, когда ни один из механизмов не остановил процесс.
После этого урока ты сможешь читать и устанавливать rlimits через ulimit, определять самые частые ошибки rlimit, ориентироваться в файловой системе cgroups v2, устанавливать CPUQuota и MemoryMax для systemd-сервиса, и объяснять ключевое различие между rlimits (на процесс) и cgroups (на группу).
rlimits: потолки ресурсов на процесс, применяемые ядром.
Каждый процесс наследует набор rlimits от родителя. У каждого лимита есть мягкое значение (текущий применяемый потолок, который процесс может понижать, но не поднимать выше жёсткого) и жёсткое значение (потолок для мягкого — только root может его поднять).
# Просмотр rlimits текущей оболочки:
ulimit -a
# core file size (blocks, -c) 0
# max locked memory (kbytes, -l) 8192
# open files (-n) 1024 ← RLIMIT_NOFILE мягкий
# max user processes (-u) 63296 ← RLIMIT_NPROC
# Два лимита, которые чаще всего встречаются в продакшене:
# RLIMIT_NOFILE: максимальное количество открытых файловых дескрипторов
# По умолчанию мягкий: 1024. По умолчанию жёсткий: 1048576 (Ubuntu).
# При превышении: "Too many open files" (errno EMFILE)
# Node.js или Java-сервер под нагрузкой нуждается в гораздо большем.
# RLIMIT_NPROC: максимальное число процессов/потоков пользователя
# При превышении fork() возвращает EAGAIN: "Resource temporarily unavailable"
# Fork-бомбы останавливаются здесь.Установка rlimits: ulimit для сессии, /etc/security/limits.conf для постоянства.
# Поднять лимит открытых файлов для текущей оболочки и её детей:
ulimit -n 65536
ulimit -n
# 65536
# Это влияет только на текущую сессию.
# Для сервиса — в /etc/security/limits.conf:
# * soft nofile 65536
# * hard nofile 1048576
# www-data soft nproc 4096
# Для systemd-сервисов — предпочтительнее LimitNOFILE в unit-файле:
# [Service]
# LimitNOFILE=65536
# Это правильный способ — limits.conf обходится для сервисов без PAM.
# Проверить реальные лимиты работающего процесса (PID 1234):
cat /proc/1234/limits
# Limit Soft Limit Hard Limit Units
# Max open files 65536 1048576 files
# Max processes 63296 63296 processescgroups v2: единая иерархия.
cgroups v2 (по умолчанию на Ubuntu 22.04+) использует единую файловую иерархию в /sys/fs/cgroup. Каждый процесс принадлежит ровно одной cgroup. systemd автоматически отображает сервисы на cgroups.
# Просмотр иерархии cgroup:
systemd-cgls
# Control group /:
# -.slice
# ├─system.slice
# │ ├─nginx.service
# │ │ └─987 nginx: worker process
# └─user.slice
# cgroup текущего процесса:
cat /proc/$$/cgroup
# 0::/user.slice/user-1000.slice/session-1.scope
# Доступные контроллеры:
cat /sys/fs/cgroup/cgroup.controllers
# cpuset cpu io memory hugetlb pids rdma misc
# Текущее использование памяти cgroup:
cat /sys/fs/cgroup/system.slice/nginx.service/memory.current
# 45678592 (байты — около 44 МБ)
# Использование CPU:
cat /sys/fs/cgroup/system.slice/nginx.service/cpu.stat
# usage_usec 123456789CPUQuota и MemoryMax в systemd: установка лимитов для сервиса.
systemd транслирует директивы unit-файла в записи cgroup. Напрямую писать в /sys/fs/cgroup почти никогда не нужно.
# Установить лимит памяти и квоту CPU для сервиса:
sudo systemctl edit nginx
# Добавить:
# [Service]
# MemoryMax=512M
# CPUQuota=50%
# MemoryMax: cgroup memory.max — жёсткий потолок. При превышении
# ядро вызывает локальный OOM killer cgroup (убивает процесс в этой cgroup).
# НЕ системный OOM killer.
# CPUQuota=50%: cgroup получает не более 50% одного ядра CPU за период
# (период по умолчанию 100мс). На 4-ядерной машине 50% = одно ядро на полной
# скорости, НЕ 50% от всех 4 ядер.
sudo systemctl daemon-reload
sudo systemctl restart nginx
# Проверить применение лимитов:
systemctl show nginx | grep -E 'MemoryMax|CPUQuota'
# MemoryMax=536870912
# CPUQuotaPerSecUSec=500000
# Из cgroup напрямую:
cat /sys/fs/cgroup/system.slice/nginx.service/memory.max
# 536870912
cat /sys/fs/cgroup/system.slice/nginx.service/cpu.max
# 50000 100000 (50мс бюджета за 100мс период = 50%)
# Временные лимиты (без изменения unit-файла, для тестирования):
sudo systemctl set-property nginx.service MemoryMax=256MДиагностика “too many open files” в продакшн Node.js-сервисе.
# Симптом: в логах "EMFILE: too many open files"
# Шаг 1: найти PID сервиса
systemctl show node-api --property MainPID
# MainPID=7823
# Шаг 2: проверить его реальные rlimits
cat /proc/7823/limits | grep 'open files'
# Max open files 1024 1048576 files
# ↑ мягкий=1024 — проблема
# Шаг 3: сколько FD сейчас открыто
ls /proc/7823/fd | wc -l
# 1019 ← почти у лимита 1024
# Шаг 4: исправление — в unit-файле systemd
sudo systemctl edit node-api
# [Service]
# LimitNOFILE=65536
sudo systemctl daemon-reload
sudo systemctl restart node-api
# Проверка:
cat /proc/$(systemctl show node-api --property MainPID --value)/limits | grep 'open files'
# Max open files 65536 1048576 files▸Почему это работает
На macOS cgroups нет — это функциональность ядра Linux. macOS использует элементы управления ресурсами launchd и BSD setrlimit(). Команда ulimit работает в оболочках macOS и устанавливает те же POSIX rlimits (RLIMIT_NOFILE, RLIMIT_NPROC и т.д.), но конфигурируются они иначе: через launchctl limit или plist-ключи SoftResourceLimits/HardResourceLimits. Мягкий лимит RLIMIT_NOFILE по умолчанию на macOS исторически составлял 256 — ещё ниже, чем 1024 на Linux, поэтому “too many open files” — очень распространённая боль при разработке на macOS.
▸Частая ошибка
Установка LimitNOFILE в /etc/security/limits.conf для systemd-сервиса ничего не даст. limits.conf обрабатывается модулем PAM pam_limits во время сессий входа в систему. Сервисы, запускаемые systemd, не проходят через PAM — они наследуют лимиты от systemd, который читает LimitNOFILE из unit-файла. Исправление всегда в секции [Service] unit-файла (или в drop-in через systemctl edit). Многие операторы тратят время на редактирование limits.conf и перезапуск сервиса, обнаруживая что rlimit не изменился. Всегда проверяй через cat /proc/<pid>/limits, а не ulimit -a в оболочке.
У systemd-сервиса в unit-файле стоит MemoryMax=256M. Использование памяти сервиса вырастает выше 256 МБ. Что произойдёт, и то же ли это что системный OOM killer?
rlimits — потолки ресурсов на процесс: RLIMIT_NOFILE ограничивает открытые файловые дескрипторы (по умолчанию 1024 — мало для продакшн-сервисов), RLIMIT_NPROC — количество процессов пользователя. Устанавливай через ulimit в сессии или LimitNOFILE в unit-файле systemd (не через limits.conf — systemd его обходит). cgroups v2 организует процессы в иерархию под /sys/fs/cgroup и применяет групповые бюджеты: cpu.max реализует CPUQuota, memory.max реализует MemoryMax. При превышении cgroup MemoryMax срабатывает локальный OOM kill cgroup — отдельно от системного OOM killer, который срабатывает только при исчерпании памяти всей системы. systemd автоматически отображает каждый сервис в собственную cgroup; используй systemctl edit для установки MemoryMax и CPUQuota. Всегда проверяй лимиты через cat /proc/<pid>/limits, а не через ulimit в оболочке.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.