Веб-сервер под нагрузкой вдруг начинает отказывать, в журнале мелькает сообщение о слишком многом числе открытых файлов, и приложение перестаёт принимать соединения. Знакомая беда, и первое побуждение - поднять лимит привычной командой в оболочке. Команда отрабатывает, лимит вроде бы вырос, сервис перезапускают - и всё повторяется. Причина в том, что для сервисов, которыми управляет система инициализации, привычные способы поднятия лимита попросту не действуют. Их правят в одном месте, а они живут совсем в другом.

В основе проблемы - механизм файловых дескрипторов. Каждый открытый файл, сетевое соединение, канал занимает дескриптор, и на их число есть лимит. Сетевой сервер под нагрузкой держит тысячи соединений, и стандартного лимита быстро не хватает. Разберём, как посчитать открытые дескрипторы процесса, какие лимиты бывают, и почему сервис системы инициализации требует особого подхода вместо привычной правки.

Подсчёт открытых дескрипторов процесса через файловую систему

Сначала надо убедиться, что дело действительно в дескрипторах, и посчитать, сколько их держит подозреваемый процесс. У каждого процесса есть каталог в файловой системе процессов, где перечислены все его открытые дескрипторы. Подсчёт записей в нём даёт точное число.

ls /proc/12345/fd | wc -l    # сколько дескрипторов открыто процессом

Тут же, в файловой системе процесса, лежит и файл с действующими для него лимитами. Заглянув в него, видно, какой лимит на открытые файлы сейчас применён к этому конкретному процессу - что важно, ведь лимит процесса может отличаться от лимита оболочки.

cat /proc/12345/limits | grep "open files"   # действующий лимит процесса

Сопоставив число открытых дескрипторов с лимитом, сразу видно, упёрся ли процесс в потолок. Если открыто почти столько же, сколько разрешено, причина ошибки найдена. Для подробного разбора, какие именно файлы и соединения держит процесс, помогает утилита списка открытых файлов, показывающая их с типами.

lsof -p 12345                # подробный список открытых файлов процесса

Мягкий и жёсткий лимиты различаются по правам изменения

Лимитов на дескрипторы на самом деле два, и понимание разницы между ними ключевое. Мягкий лимит - тот, что действует прямо сейчас и реально ограничивает процесс. Жёсткий лимит - потолок, выше которого мягкий поднять нельзя. Проверяют оба отдельными флагами.

ulimit -Sn    # мягкий лимит, действующий сейчас
ulimit -Hn    # жёсткий лимит, потолок для мягкого

Разница в правах. Мягкий лимит непривилегированный пользователь может менять сам, но лишь в пределах жёсткого. Жёсткий лимит обычный пользователь способен только понизить, но не повысить обратно - поднять его может лишь привилегированный пользователь вроде администратора. Это сделано ради безопасности: чтобы пользователь не мог бесконтрольно нарастить себе ресурсы.

Помимо лимитов отдельного процесса есть и общесистемный потолок - предельное число дескрипторов для всех процессов вместе. Он хранится в особом системном файле и на современных системах обычно очень велик, так что упираются в него редко - чаще беда в лимите конкретного процесса.

cat /proc/sys/fs/file-max    # общесистемный потолок дескрипторов

Привычная правка лимитов и где она работает

Для обычных пользователей и процессов, запущенных из оболочки, лимит правят привычными способами. Временно, для текущей оболочки и её потомков, его поднимает команда установки мягкого лимита.

ulimit -Sn 65535    # поднять мягкий лимит в текущей оболочке

Постоянно лимиты для пользователей задают в конфигурации ограничений через особый механизм аутентификации. Туда вписывают мягкий и жёсткий лимиты для нужных пользователей или для всех сразу.

# в файле конфигурации ограничений
* soft nofile 65535
* hard nofile 131072

Этот способ работает для процессов, запускаемых при входе пользователя в систему. И именно здесь кроется ловушка, на которую натыкаются почти все.

Сервис системы инициализации игнорирует привычные лимиты

Вот суть проблемы из заголовка. Сервисы, которыми управляет современная система инициализации, не проходят через тот механизм аутентификации, в котором правят лимиты для пользователей. Поэтому конфигурация ограничений на них попросту не распространяется. Можно сколько угодно поднимать лимиты в ней и в оболочке - сервис, запущенный системой инициализации, их не увидит, потому что стартует мимо этого механизма.

Лимит для такого сервиса задаётся в его собственной конфигурации, особым параметром числа открытых файлов. Правильный способ - создать переопределение конфигурации сервиса через штатную команду редактирования.

sudo systemctl edit веб-сервер.service

В открывшемся переопределении задают нужный лимит в секции сервиса.

[Service]
LimitNOFILE=65536

После этого конфигурацию перечитывают и сервис перезапускают, чтобы новый лимит вступил в силу. Только теперь процесс сервиса получит поднятый потолок дескрипторов. Это и есть разгадка, почему привычная правка не помогала: лимит правили в механизме для пользовательских сессий, а сервис живёт в системе инициализации, со своей конфигурацией лимитов.

Проверить, что новый лимит действительно применился, можно тем же файлом лимитов в файловой системе процесса сервиса - после перезапуска там должна стоять новая цифра.

Лимит приложения может быть и третьим уровнем

Стоит знать и о ещё одном уровне, который иногда добавляет путаницы. Некоторые приложения имеют собственную настройку лимита дескрипторов, действующую поверх системной. Например, у популярного веб-сервера есть директива, ограничивающая число дескрипторов на рабочий процесс. Если она задана ниже системного лимита, упрётся именно она, сколько ни поднимай лимит в конфигурации сервиса.

Тут важна согласованность: лимит в настройке приложения не должен превышать лимит, заданный в конфигурации сервиса системы инициализации, иначе приложение запросит больше, чем ему позволено, и снова упрётся в потолок. Правильный порядок - сначала поднять лимит сервиса, затем настроить лимит приложения в его пределах.

Отдельно стоит задуматься, не утечка ли дескрипторов лежит в основе. Если процесс открывает файлы и соединения, но не закрывает их, число дескрипторов будет неуклонно расти, пока не упрётся в любой лимит, как его ни поднимай. Признак утечки - число открытых дескрипторов, растущее со временем без видимой причины. В этом случае поднятие лимита лишь отсрочит беду, а лечить надо саму программу, заставив её закрывать то, что открыла. Наблюдение за ростом числа дескрипторов во времени отличает честную нехватку лимита от утечки.

Что складывается в методику

Картина выстраивается ясно. Сначала подтверждают, что дело в дескрипторах, подсчитав их у процесса через файловую систему процессов и сверив с действующим лимитом из файла лимитов процесса. Лимитов два: мягкий действует сейчас, жёсткий служит потолком, и менять их вверх может лишь привилегированный пользователь.

Ключевая развилка - чем запущен процесс. Обычные пользовательские процессы правят привычными способами через конфигурацию ограничений. Но сервис системы инициализации эти лимиты игнорирует, потому что стартует мимо механизма пользовательских сессий, и его лимит задают в собственной конфигурации сервиса через переопределение особым параметром. Приложение может добавить третий уровень собственной настройки, который должен укладываться в лимит сервиса.

Главная мысль: ошибка о слишком многих открытых файлах лечится ровно там, где задан реальный лимит процесса, а не там, где привычнее. Для сервиса системы инициализации это его собственная конфигурация, а не пользовательские настройки, сколько их ни правь. Стоит запомнить, что сервис живёт по своим лимитам, проверить число дескрипторов на утечку и задать потолок в правильном месте, и упрямая ошибка о переполнении дескрипторов уходит насовсем, а не возвращается после каждого перезапуска.