Перейти к содержимому

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
systemdLimitNOFILE= в unit, DefaultLimitNOFILE= в system.confДля сервисов, управляемых systemd
Примечание

/proc/sys/fs/nr_open — верхняя граница, до которой можно поднять nofile для одного процесса. По умолчанию обычно 1073741816 (≈1B), но на практике редко нужно больше 1048576.

LimitNOFILE в systemd unit

В unit-файле директива выглядит так:

[Service]
LimitNOFILE=65536

Можно задать и мягкий, и жёсткий лимит одновременно через пробел:

LimitNOFILE=65536:1048576

Первое значение — soft limit, второе — hard limit. Если указать одно — оно становится soft, а hard берётся из системного максимума.

Подсказка

Глобальный дефолт для всех unit — DefaultLimitNOFILE= в /etc/systemd/system.conf (и user.conf). По умолчанию часто 1048576 на современных дистрибутивах, но на старых может быть 4096 или 1024 — именно это ловит неожиданно.

Почему ulimit -n в ExecStart не работает

Типичная ошибка — пытаться задать лимит прямо в команде запуска:

[Service]
ExecStart=/bin/sh -c 'ulimit -n 65536 && exec /usr/bin/myapp'

ulimit -n внутри ExecStart не действует на процесс, который systemd считает основным (MainPID). systemd сам устанавливает лимиты до запуска ExecStart, а shell-обёртка работает уже в контексте, где лимит зафиксирован. Более того, ulimit -n может и упасть с Operation not permitted, если запрошенное значение превышает hard limit, заданный systemd.

Если нужно изменить лимит — делайте это через директиву [Service], а не через shell.

Предупреждение

ulimit -n в shell-скрипте, вызываемом из ExecStartPre, тоже не передаёт лимит в основной процесс. Лимиты — свойство процесса, а не сессии shell.

Как проверить применённые лимиты

Три способа, от простого к надёжному:

# 1. Изнутри процесса
cat /proc/self/limits | grep "Max open files"

# 2. Для конкретного PID
cat /proc/<pid>/limits | grep "Max open files"

# 3. Что systemd видит для unit
systemctl show myservice.service -p LimitNOFILE

systemctl show покажет именно то значение, которое systemd применил при fork() — это authoritative source. /proc/<pid>/limits — то, что видит ядро для процесса. Если они расходятся, проблема в промежуточном слое (PAM, container runtime, sudo).

Для отладки ExecStart можно добавить:

[Service]
ExecStartPre=/bin/sh -c 'cat /proc/self/limits | grep "Max open files"'

Это покажет лимиты до запуска основного процесса — то, что 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 не перезапускает процесс и лимиты не переприменяет.