ulimit и systemd LimitNOFILE — почему ulimit -n в unit не живёт
Что такое nofile и где он живёт
nofile — максимальное количество открытых файловых дескрипторов на один процесс. Это не только обычные файлы, а сокеты, pipe, stdin/stdout/stderr, логи через journald — всё считается. Когда nginx или Go-приложение падает с too many open files, дело в этом лимите.
Лимиты живут на трёх уровнях:
| Уровень | Где смотреть | Что контролирует |
|---|---|---|
| Ядро (системный) | /proc/sys/fs/file-max, /proc/sys/fs/nr_open | Абсолютный потолок по всей системе |
| PAM / login | /etc/security/limits.conf, /etc/security/limits.d/ | Для сессий через pam_limits.so |
| systemd | LimitNOFILE= в unit, DefaultLimitNOFILE= в system.conf | Для сервисов, управляемых systemd |
/proc/sys/fs/nr_open — верхняя граница, до которой можно поднять nofile для одного процесса. По умолчанию обычно 1073741816 (≈1B), но на практике редко нужно больше 1048576.
LimitNOFILE в systemd unit
В unit-файле директива выглядит так:
Можно задать и мягкий, и жёсткий лимит одновременно через пробел:
Первое значение — soft limit, второе — hard limit. Если указать одно — оно становится soft, а hard берётся из системного максимума.
Глобальный дефолт для всех unit — DefaultLimitNOFILE= в /etc/systemd/system.conf (и user.conf). По умолчанию часто 1048576 на современных дистрибутивах, но на старых может быть 4096 или 1024 — именно это ловит неожиданно.
Почему ulimit -n в ExecStart не работает
Типичная ошибка — пытаться задать лимит прямо в команде запуска:
ulimit -n внутри ExecStart не действует на процесс, который systemd считает основным (MainPID). systemd сам устанавливает лимиты до запуска ExecStart, а shell-обёртка работает уже в контексте, где лимит зафиксирован. Более того, ulimit -n может и упасть с Operation not permitted, если запрошенное значение превышает hard limit, заданный systemd.
Если нужно изменить лимит — делайте это через директиву [Service], а не через shell.
ulimit -n в shell-скрипте, вызываемом из ExecStartPre, тоже не передаёт лимит в основной процесс. Лимиты — свойство процесса, а не сессии shell.
Как проверить применённые лимиты
Три способа, от простого к надёжному:
systemctl show покажет именно то значение, которое systemd применил при fork() — это authoritative source. /proc/<pid>/limits — то, что видит ядро для процесса. Если они расходятся, проблема в промежуточном слое (PAM, container runtime, sudo).
Для отладки ExecStart можно добавить:
Это покажет лимиты до запуска основного процесса — то, что systemd установил для сервиса.
PAM limits vs systemd limits
На большинстве современных дистрибутивов (RHEL 8+, Ubuntu 20.04+, Debian 11+) systemd не вызывает pam_limits.so для системных сервисов. Лимиты из /etc/security/limits.conf для unit-файлов не применяются. Они работают только для логин-сессий (SSH, local login, su).
| Механизм | Применяется к | Где настраивается |
|---|---|---|
LimitNOFILE= в unit | Конкретный systemd-сервис | /etc/systemd/system/*.service |
DefaultLimitNOFILE= | Все systemd-сервисы | /etc/systemd/system.conf |
pam_limits.so / limits.conf | Логин-сессии пользователя | /etc/security/limits.conf |
ulimit в shell | Текущий shell и его потомки | Интерактивная сессия |
Если сервис запускается не через systemd (например, через supervisor, docker --ulimit, или напрямую), то LimitNOFILE в unit-файле не участвует вообще. В Docker это --ulimit nofile=65536:1048576, в supervisor — stdout_maxbytes и прочие опции не связаны с nofile напрямую — нужно настраивать на уровне ОС или контейнера.
После изменения LimitNOFILE в unit-файле не забудьте systemctl daemon-reload и systemctl restart <unit>. systemctl reload не перезапускает процесс и лимиты не переприменяет.