# ulimit и systemd LimitNOFILE — почему ulimit -n в unit не живёт

Индекс LLMS: [llms.txt](/llms.txt)

---

## Что такое 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 |

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

## LimitNOFILE в systemd unit

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

```ini
[Service]
LimitNOFILE=65536
```

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

```ini
LimitNOFILE=65536:1048576
```

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

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

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

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

```ini
[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.

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

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

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

```bash
# 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` можно добавить:

```ini
[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 напрямую — нужно настраивать на уровне ОС или контейнера.

> [!TIP]
> После изменения `LimitNOFILE` в unit-файле не забудьте `systemctl daemon-reload` и `systemctl restart <unit>`. `systemctl reload` не перезапускает процесс и лимиты не переприменяет.
