Почему приложение упирается в лимиты файловых дескрипторов
Каждое соединение, открытый файл, сокет или канал — это файловый дескриптор, взятый из таблицы процесса. Таблица не бесконечна: её размер задаёт мягкий и жёсткий порог, который наследуется от родителя при старте. systemd задаёт своё значение для службы, оболочка — своё через ulimit, и рабочие процессы часто получают не тот предел, что настроен «на бумаге».
Пока сервис работает в спокойном режиме, свободные дескрипторы не заканчиваются — предел виден только под нагрузкой. Каждое входящее соединение, обращение к кэшу, запись в журнал или очередь занимает ещё один. Если соединения не закрываются вовремя — нет ограничения на время удержания, потеряны ответвления при ошибках, — возникает утечка дескрипторов: счётчик растёт монотонно и не возвращается к базовому уровню.
В логах это выглядит одинаково: ошибка «слишком много открытых файлов», отказ на приёме нового соединения, при этом процесс жив и метрики здоровья отвечают. Наружу — отказ сервиса под пиковой нагрузкой: балансировщик видит открытый порт, а запросы не обслуживаются. Клиенты уходят в повторные попытки и добивают остаток свободных дескрипторов.
Пик опасен тем, что незакрытые соединения и повторы складываются: каждое новое соединение требует дескриптор, а освобождаются они медленнее, чем приходят. Разбираем, как это закрывается — от аудита пределов до контроля утечек.
Что дают настроенные пределы: меньше отказов и простоев
Ненастроенный предел проявляется не при старте, а на пике: рабочий процесс упирается в soft-лимит, новые соединения отбиваются, в логах растёт доля ошибок — при этом сервис отвечает на проверку доступности и выглядит живым. Диагностика уходит в сторону приложения и сети, хотя причина в одном параметре, который никто не выставлял под реальный профиль нагрузки. Разбираем, что меняется, когда он выставлен.
Лимиты задаются явно — отдельно soft и hard, привязанные к самому сервису, а не к интерактивной сессии администратора. В systemd это LimitNOFILE в unit-файле, для процессов вне менеджера служб — правила в limits.conf. Иначе после перезапуска узел вернётся к значению по умолчанию, и инцидент повторится на том же месте.
Эффект измерим по трём осям. Снижение отказов видно по доле неуспешных соединений и коду возврата на приёме — она уходит в ноль в пределах проверенного запаса. Время восстановления сервиса после пика сокращается: не нужно ждать, пока освободятся дескрипторы, которые процессы держат в ожидании. Устойчивость под нагрузкой перестаёт зависеть от удачи — очередь соединений не растёт лавинообразно, деградация идёт плавно и снимается сама.
Предсказуемость работы даёт то, что предел описан в репозитории и раскатывается на каждый узел одинаково: новый инстанс в кластере ведёт себя как остальные. Запас подтверждаем нагрузочным прогоном с профилем, близким к пиковому, и держим порог выше расчётного максимума на понятный коэффициент. Предотвращение простоев держится именно на этом запасе, а не на круглосуточном дежурстве.
Варианты настройки лимитов: что подходит под нагрузку
Лимит дескрипторов задаётся не в одном месте: у процесса есть мягкий и жёсткий предел, у системы — общий потолок, а службы и пользовательские сессии получают значения разными путями. Порядок применения зависит от того, кто запускает процесс: оболочка при входе, менеджер служб или родитель, передавший значения по наследству.
Поэтому настройка под нагрузку — это выбор уровня, а не поиск «правильного файла». Правка на одном уровне не действует на процессы, стартующие иначе, и лимит молча остаётся прежним: приложение упирается в отказ при открытии соединений, хотя в настройках всё выглядит корректно.
| Уровень настройки | Когда подходит | Ограничения |
|---|---|---|
| Системный потолок | Общий запас дескрипторов для всех процессов узла | Задаёт верхнюю границу, но не распределяет её между службами |
| Мягкий предел процесса | Приложение само поднимает лимит на старте | Не может превысить жёсткий предел |
| Жёсткий предел процесса | Верхняя граница, выше которой процесс не поднимется | Применяется только к новым процессам или после перезапуска |
| Пределы для служб | Демоны, стартующие без входа пользователя | Не наследуются интерактивными сессиями и ручными запусками |
| Пределы для пользовательских сессий | Отладка и ручной запуск из оболочки | Не действуют на службы, стартующие до входа |
Проверяют результат по фактическому значению в работающем процессе, а не по файлу настроек: перезапуск службы и новая сессия дают разную картину. Для нагруженного профиля поднимают и системный потолок — иначе служба упрётся в общий предел раньше, чем в собственный.
Как проходит внедрение: от аудита до передачи регламентов
Лимиты файловых дескрипторов наследуются процессом в момент старта, поэтому правка конфигурации «на живую» ничего не меняет, а выставленное с большим запасом значение не решает проблему — оно лишь откладывает отказ и маскирует утечку. Работу ведём в фиксированном порядке: сначала замер и схема, потом изменения.
- Аудит конфигурации. Собираем фактические значения по всем слоям: ограничения ядра, системные пороги, параметры службы, которая запускает приложение, и ограничения пользовательской сессии. Расхождение между слоями — типовая причина, почему процесс видит не то, что прописали в файле.
- Замер текущего состояния. Смотрим, сколько дескрипторов процесс держит в покое и на пике, как растёт число открытых соединений и файлов, что осталось в журналах после прошлых отказов. Это даёт отправную точку, а не догадку.
- Схема настройки. Определяем, где именно правим, какие процессы получают исключения, как значение применится и к службе, и к ручному запуску. Разделяем мягкий и жёсткий пороги, фиксируем запас относительно пикового потребления.
- Настройка. Вносим правки в конфигурацию и в описание службы, перезапускаем по плану, а не в момент сбоя, проверяем, что новое значение действительно подхвачено процессом.
- Проверка под нагрузкой. Воспроизводим рабочий профиль: пиковый трафик, всплеск соединений, отказ смежного узла. Отдельно смотрим поведение приложения, когда оно приближается к порогу.
- Наблюдаемость. Настраиваем метрики по числу открытых дескрипторов и соединений и оповещение по скорости роста — тогда утечку видно задолго до того, как процесс упрётся в потолок.
- Передача регламентов. Отдаём документ с целевыми значениями, порядком применения на новом узле, чек-лист проверки после изменений и таблицу «симптом → что смотреть».
После передачи команда заказчика может сама раскатывать лимиты на новых серверах и понимать, когда рост числа дескрипторов — это норма, а когда сигнал проблемы.
Конфигурации, схемы и документация по итогам работ
Настройка лимитов не считается законченной, пока она живёт только в истории команд на конкретном сервере. Мы оставляем у вашей команды набор артефактов, по которым инженер воспроизведёт ту же конфигурацию на другом стенде, объяснит выбранные значения через полгода и поймёт, где искать причину, если счётчики снова упрутся в потолок.
- Конфигурационные файлы с раздельными мягкими и жёсткими значениями для системных служб и пользовательских сессий, с комментариями: откуда взялся запас и что произойдёт при его снижении.
- Профили запута приложений — где именно поднимается лимит: в описании службы, в обёртке запуска или в окружении контейнера, чтобы значение не терялось при перезапуске и деплое.
- Схема инфраструктуры с ролями узлов и пометкой, на каких машинах лимиты повышены, а на каких оставлены базовыми и почему.
- Политики доступа: какие роли могут менять параметры ядра и перезапускать службы, как это связано с правами на конфигурацию.
- Регламент обслуживания: как проверить фактическое значение у работающего процесса, какие счётчики смотреть при росте нагрузки и с какой периодичностью.
- Расчёт целевых значений: пиковые одновременные соединения на процесс, число обработчиков, служебные соединения и запас на восстановление.
- Чек-лист приёмки после применения и порядок отката к предыдущим значениям, если новая конфигурация даст неожиданный эффект.
Плюс памятка для дежурной смены: типовые симптомы упора в лимит и первая линия действий до эскалации. Всё это передаётся в вашем репозитории конфигураций, а не отдельными письмами.
Кейс: убрали отказы сервиса под пиковой нагрузкой
Как было
Сервис на пике переставал принимать новые соединения: в журналах — отказ открыть файл или сокет, наружу — всплеск ошибок и обрывы соединений. Дежурный видел симптом, а не причину: метрики следили за кодами ответов, но не за числом открытых дескрипторов, поэтому каждый разбор начинался с ручного перебора журналов. От первого сигнала до восстановления проходили десятки минут, и всё это время часть запросов терялась.
Что изменили и что получили
Сначала сняли базовую линию: сколько дескрипторов сервис держит в пике и сколько открывает на один запрос. Оказалось, что потолок мягкого лимита выбирался полностью уже при штатной работе — запаса не было, а сам лимит задаётся на процесс, поэтому при нескольких рабочих процессах потолок выбирается кратно быстрее, чем ожидаешь. Подняли мягкий и жёсткий лимиты для процесса, отдельно — общесистемный потолок, заложив запас на рост, и убрали утечку: соединения возвращаются в переиспользование, а не остаются открытыми до перезапуска.
Наблюдаемость перевели на причину. Число открытых дескрипторов и его доля от лимита выводятся на график рядом с трафиком в Prometheus и на общих панелях, оповещение срабатывает на подходе к потолку, а не после отказа.
После изменений пик проходит без отказов этого класса: занятость лимита держится в пределах двух третей вместо упора в предел, время диагностики сократилось с часов до минут, ручной перезапуск сервиса на нагрузке больше не требуется.
Чем системный предел отличается от предела сервиса
Предел файловых дескрипторов задаётся на двух разных уровнях, и путать их — источник большинства «мистических» отказов. Системный предел — суммарный потолок для всей машины, его держит ядро. Предел процесса и службы — сколько дескрипторов может открыть конкретный процесс; он всегда ниже системного и наследуется при запуске.
Приоритет определяется тем, кто породил процесс: дочерний процесс не читает конфигурацию заново, а получает значения от родителя, поэтому итог зависит от цепочки запуска.
| Уровень ограничения | Где задаётся | На что влияет | Что перекрывает |
|---|---|---|---|
| Системный | параметры ядра | суммарно по хосту | потолок для всех служб |
| Служба | описание юнита в systemd | только этот сервис | значения пользовательских профилей |
| Пользовательская сессия | limits.conf через PAM | интерактивные входы | мягкий предел сессии |
| Процесс | ulimit при запуске | текущий и дочерние процессы | ничего выше жёсткого предела |
Ключевой нюанс: службы, которые поднимает менеджер служб, не проходят через PAM, поэтому правки для пользовательских сессий их не затрагивают — отсюда классическое расхождение «в терминале предел один, под службой другой». Внутри одного уровня мягкий предел — рабочее значение, жёсткий — верхняя граница: мягкий можно поднять, но не выше жёсткого, а жёсткий не выше системного.
Фактическое значение смотрим по /proc/<pid>/limits у конкретного процесса, а не по файлам конфигурации. Порядок применения: системный потолок → параметры юнита или родителя → наследование дочерними процессами → значение, с которым процесс реально работает.
Частые технические сомнения при настройке пределов
Разбор лимитов почти всегда упирается не в вопрос «поднимать или нет», а в оговорки инженерной команды: чем это грозит работающему сервису и как потом доказать, что стало лучше. Ниже — то, что всплывает на согласовании чаще всего.
Отдельно стоит поведение под нагрузкой: при среднем трафике запас выглядит избыточным, а на пике тот же процесс упирается в предел, потому что часть дескрипторов держат соединения и временные файлы. Поэтому расчёт ведём от пикового профиля, а не от суточного среднего.
Придётся ли перезапускать сервис?
Не всегда, но планировать окно нужно. Часть параметров применяется к работающему процессу, часть — только при старте или новом входе в сессию, а системные пределы меняются на уровне ядра. Порядок такой: сначала правим описания служб и системные значения, затем перезапускаем по одному узлу за раз, чтобы не терять доступность.
Не замаскирует ли новый лимит реальную утечку?
Риск есть, и он главный. Если дескрипторы не закрываются в обработчиках, высокий предел просто отложит отказ. Поэтому перед изменением снимаем снимок открытых дескрипторов на нескольких процессах: рост, привязанный к числу запросов, — это утечка, ровный профиль — нехватка запаса. Диагностику делаем до правок, а не после.
Как убедиться, что запас действительно нужен?
По мониторингу: собираем число открытых дескрипторов по процессам, долю использования от предела и счётчик отказов открытия. Оповещение ставим на 75–80% от лимита, чтобы был запас до деградации, и сверяем его с историей вызовов — так видно, растёт потребление со временем или упирается в сезонные пики.
Не конфликтует ли это с текущей конфигурацией?
Пределы могут задаваться в нескольких местах — общесистемно, в описании службы, в контейнерной платформе. Применяется не самый большой, а заданный позже по цепочке запуска, поэтому значения сводим к одному источнику и фиксируем в документации. Иначе после перезапуска поведение окажется не тем, что проверяли на стенде.
Обсудим задачу и вернёмся с оценкой и планом работ
Оценка настройки лимитов файловых дескрипторов редко требует длинного обследования — но состав работ и план внедрения напрямую зависят от того, насколько полно описан контур. Ниже — входные данные, которых достаточно, чтобы вернуться со сроками и порядком изменений.
- Состав сервисов: какие процессы держат открытые соединения и файлы — веб-серверы, воркеры очередей, прокси, базы, — и сколько экземпляров работает на одном узле.
- Характер нагрузки: пиковое число одновременных соединений, суточный профиль, как ведёт себя система в момент пика.
- Текущие настройки: мягкий и жёсткий лимит для пользователя и процесса, ограничения в юните службы, общесистемный предел на открытые файлы.
- Симптомы отказов: тексты ошибок из логов, в какой момент появляются, что именно падает и перезапускается.
- Способ запуска: кто инициирует процессы — служба, супервизор или контейнер, — и пробрасываются ли лимиты внутрь.
- Что уже меняли: какие значения правили и почему результат не устроил.
- Ограничения окружения: контейнеры, оркестратор, есть ли права на изменение системных параметров.
С этим набором обсуждаем задачу и фиксируем договорённости: какие значения ставим, как проверяем под нагрузкой и в каком порядке откатываем.







