Сервис перестал принимать клиентов. В журнале - 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 означает исчерпание системного ресурса открытых файлов - это другая ветка расследования. Перезапуск может временно освободить дескрипторы, но по одному успешному рестарту нельзя заключать, что причина устранена.

Комментарии (0)