Nginx под нагрузкой пишет «Too many open files» и отдаёт пустые ответы вместо файлов: три уровня лимитов, которые нужно поднять согласованно

Что было. Публичный сервис раздачи статики около тысячи запросов в секунду. В пике в error.log: open() failed (24: Too many open files). Хуже того, try_files расценил невозможность открыть файл как его отсутствие и отдал пустой ответ, который CDN закэшировала на долгий срок. У одного рабочего процесса оказалось около 65 тысяч открытых дескрипторов при заявленном worker_rlimit_nofile 300000: лимит на уровне ОС был ниже заявленного в nginx.

Откуда берётся ограничение. Каждое соединение занимает минимум два дескриптора (клиентский сокет и файл или соединение с бэкендом), плюс open_file_cache держит открытые файлы. Лимиты стоят на трёх уровнях, и работает самый низкий:

  1. Ядро: fs.file-max (общесистемный максимум) и fs.nr_open (максимум на процесс).
  2. Процесс: ulimit -n для пользователя nginx. Для сервиса под systemd это LimitNOFILE= в юните, а не /etc/security/limits.conf, который на systemd-службы не действует.
  3. nginx: worker_rlimit_nofile (сколько сам nginx попросит у ОС) и worker_connections.

Как настроить.

# /etc/systemd/system/nginx.service.d/override.conf
[Service]
LimitNOFILE=65536

systemctl daemon-reload && systemctl restart nginx (не reload: лимит применяется к новому процессу).

# nginx.conf
worker_processes auto;
worker_rlimit_nofile 65536;
events { worker_connections 16384; }

Правило из обсуждения: worker_rlimit_nofile примерно в два раза больше worker_connections. Проверка фактического лимита: cat /proc/$(pgrep -o nginx)/limits | grep "open files" для мастера и для рабочего процесса.

Про кэш открытых файлов. open_file_cache max=1000000 на миллион записей был частью проблемы: кэш сам держит дескрипторы. Разумно max=10000 inactive=30s и подбирать по замерам. open_file_cache_errors on безопасен: он кэширует только «файл не найден», а не системные ошибки.

Чтобы ошибка не превращалась в пустой 200. В обсуждении предложили не полагаться на try_files для различения «нет файла» и «система не смогла открыть»: при системной ошибке возвращать 503 с Cache-Control: no-store, чтобы CDN не запомнила мусор. Простейший вариант: error_page 500 502 503 504 /50x.html; с add_header Cache-Control no-store always; в этом location.

Признаки заранее. Растущее число дескрипторов у рабочих процессов (ls /proc/<pid>/fd | wc -l) и предупреждения worker_connections are not enough в логе. Мониторить оба.

Директива описана в документации nginx.