Сервис перестал принимать клиентов. В журнале - Too many open files. Больших логов нет, каталоги с данными выглядят нормально, а ulimit -n в вашей консоли показывает огромный запас. Откуда взялись «слишком многие файлы»?
У этой ошибки есть языковая ловушка: слово files заставляет искать обычные файлы на диске. Но сокет тоже занимает файловый дескриптор. Сервис может упереться в лимит, создавая только сокеты - даже без подключения к удалённому серверу. Ниже я воспроизведу именно это, затем подниму лимит и получу ту же ошибку повторно.
Поставим маленький предел одному процессу
Для эксперимента достаточно Linux и Python. Отдельному дочернему процессу задаю soft limit RLIMIT_NOFILE=64 и hard limit 256. Родитель сохраняет свой исходный лимит. Дочерний процесс создаёт TCP-сокеты и удерживает ссылки на все объекты, поэтому они остаются открытыми. Он не вызывает bind(), listen() или connect() - сетевого обмена здесь вообще нет.
В этом запуске у родителя:
$ ulimit -n 1048576
А у дочернего процесса, которого я собираюсь сломать:
Max open files 64 256 files
Это строка из его /proc/PID/limits. После накопления сокетов следующий socket() возвращает:
[Errno 24] Too many open files
В /proc/PID/fd насчитывается 64 записи. Из них 61 - ссылки вида socket:[число], ещё три - стандартные потоки ввода и вывода. Обычных пользовательских файлов процесс для этого не открывал. Ниже - настоящий снимок xterm с этим запуском и дальнейшими проверками.

Важная точность: RLIMIT_NOFILE ограничивает допустимый номер нового дескриптора - он должен быть меньше лимита. На нашем стенде номера заняты подряд от 0 до 63, поэтому в момент отказа видны ровно 64 открытых дескриптора. Для произвольного процесса число записей и границу допустимых номеров не всегда можно отождествлять.
Что скрывается за словом files
Файловый дескриптор - небольшой целый номер, через который процесс обращается к открытому ресурсу. В этой таблице живут обычные файлы, сокеты, каналы и другие объекты.
Для сетевого сервиса это меняет направление диагностики. Тысяча удерживаемых клиентских соединений требует дескрипторов, даже если сервис не читает тысячу документов. Дополнительные ресурсы нужны слушающим сокетам, соединениям с БД, логам и механизму обработки событий. Поэтому размеры каталога с файлами почти ничего не говорят о запасе работающего процесса по этому лимиту.
Посмотреть содержимое таблицы можно так:
ls -l /proc/PID/fd
Ссылки вида socket:[…], pipe:[…] и anon_inode:[…] помогают понять, что удерживается. На многопоточном или загруженном процессе это движущийся снимок: дескрипторы могут открываться и закрываться прямо во время просмотра.
Подняли лимит. Почему отказ вернулся?
Не меняя код накопления, поднимаю soft limit того же дочернего процесса с 64 до 128. Hard limit остаётся 256. Скрипт продолжает создавать и удерживать сокеты. Через дополнительные 64 успешных создания снова получаем EMFILE.
Теперь в таблице 128 дескрипторов: 125 сокетов и три стандартных потока. Повышение сработало - процесс действительно получил запас. Но запас закончился, потому что жизненный цикл ресурсов остался прежним.
Затем закрываю удерживаемые сокеты и выполняю 500 операций создания и закрытия:
for _ in range(500): with socket.socket(socket.AF_INET, socket.SOCK_STREAM): pass
После цикла у процесса снова три дескриптора, ошибок нет. Ключевое отличие - освобождение ресурса после использования. В реальном сетевом приложении сокет, конечно, должен оставаться открытым столько, сколько требует протокол и работа соединения. Эксперимент показывает цену бесконтрольного удержания, а не предлагает немедленно закрывать любое соединение.
Почему ваш ulimit не оправдывает сервис
Команда ulimit -n в SSH-сессии показывает лимит оболочки. Её будущие дочерние процессы обычно наследуют этот предел, но уже запущенный сервис - другой процесс со своей историей запуска. В эксперименте это видно непосредственно: у родителя 1048576, у дочернего процесса 64, и ошибка возникает именно у дочернего.
Для systemd-сервиса начинаю с фактического PID:
pid=$(systemctl show -p MainPID --value notify-gateway) grep 'Max open files' /proc/"$pid"/limits ls -l /proc/"$pid"/fd
Если MainPID равен 0, сначала выясняю, почему сервис не запущен. Если приложение создаёт рабочие процессы, проверяю также PID того воркера, у которого появилась ошибка. Доступ к чужим дескрипторам может требовать прав; изучать нужно процесс в нужном PID namespace.
Затем сравниваю реальность с настройкой запуска:
systemctl cat notify-gateway systemctl show notify-gateway -p LimitNOFILE -p LimitNOFILESoft
Для системной службы лимит обычно задают LimitNOFILE= в unit или drop-in. Обычная правка /etc/security/limits.conf, применяемая через PAM к сессиям, сама по себе не настраивает типичную системную службу. После изменения unit нужны daemon-reload и новый запуск процесса; затем лимит проверяется в /proc снова.
Конкретное число следует подбирать под приложение. Например, старый код с select() может не поддерживать номера дескрипторов выше 1023: бездумно увеличивать предел такому процессу опасно. Для пользовательских systemd-служб действует ещё и ограничение, унаследованное их менеджером. Универсальная команда «поставь 65536» скрывает слишком много условий.
Недостаточный запас или утечка?
Одна фотография полной таблицы не отвечает на этот вопрос. Нужны несколько замеров рядом с количеством активных соединений, запросов и рабочих процессов.
Если число дескрипторов следует за ожидаемым числом полезных соединений, возможно, лимит действительно мал для нагрузки. Если подключения давно ушли, а дескрипторы не освобождаются, стоит искать утечку, зависшее завершение запросов или бесконтрольное расширение пула. Сам по себе рост при прогреве ещё не доказывает утечку: кеш или пул может выйти на плато.
В нашем стенде причина известна по коду: каждый сокет добавляется в список и остаётся открытым. Поэтому повышение лимита только отодвигает отказ, а исправление жизненного цикла останавливает накопление. У реального сервиса этот вывод сначала нужно подтвердить наблюдениями.
Заодно проверьте точную ошибку. EMFILE говорит о пределе процесса. ENFILE означает исчерпание системного ресурса открытых файлов - это другая ветка расследования. Перезапуск может временно освободить дескрипторы, но по одному успешному рестарту нельзя заключать, что причина устранена.