Deep Engineering
Средний·Опубликовано·25 МИН

Too many open files: чей это лимит, почему он не общий и что считается дескриптором

Ошибку «Too many open files» (пер.: «слишком много открытых файлов») чинят перезапуском и правкой конфига, редко разбираясь, чей именно лимит кончился. Урок показывает механизм целиком: два лимита вместо одного, наследование при запуске, и то, что дескриптор — это не файл, а любой открытый объект, включая каждое сетевое соединение.

Полное техническое изложение

TL;DR

Дескриптор — это номер, которым процесс ссылается на что-то открытое, и лимит ограничивает число таких номеров у одного процесса. Лимитов два, и заняты они разным: мягкий ядро проверяет в тот момент, когда процесс открывает очередной дескриптор, а жёсткий ограничивает то, насколько высоко процесс может поднять себе мягкий. Оба принадлежат процессу и достаются его потомкам.

Отсюда главное следствие: «я поднял лимит» и «сервису его хватило» — разные утверждения. Мягкий процесс поднимает себе сам, без перезапуска и без нового конфига, но стартует он с тем лимитом, который стоял у запустившего: измерено — родитель поставил себе 128, потомок стартовал со 128. Поэтому ulimit -n в одной оболочке ничего не меняет для сервиса, поднятого из другого места. И считает лимит не файлы, а все занятые номера: при лимите 64 открыть удалось 61, потому что три номера процесс получил при рождении.

Дальше — то, что отличает знающего от читавшего. Ошибка называется EMFILE и говорит о лимите процесса; измерено: имя EMFILE, текст Too many open files (слишком много открытых файлов). У машины лимит свой, и его исчерпание даёт ENFILE — другую ошибку, которая лечится в другом месте. Дескриптором считается не только файл: при том же лимите сокет дал 61, канал 60 (у канала два конца — два номера), epoll — 61, так что каждое соединение клиента базы или HTTP-пула тоже занимает номер. А жёсткий лимит понижается необратимо: непривилегированный процесс может опустить свой потолок, но поднять обратно — уже нет; getrlimit(2) называет понижение необратимым, а в прогоне отвергнута попытка поднять жёсткий выше текущего значения.

Порог входа
Перед уроком достаточно понимать
  • программа открывает файлы и устанавливает сетевые соединения;
  • у сервиса под нагрузкой этих соединений много и живут они одновременно;
  • сервис кто-то запускает: оболочка, менеджер служб или контейнер.
Заранее знать не нужно
  • RLIMIT_NOFILE, EMFILE, ENFILE, setrlimit;
  • что такое мягкий и жёсткий лимит и чем они отличаются;
  • epoll, inotify, состояние CLOSE_WAIT.

Что здесь на самом деле спрашивают

Лестница обычно такая:

  1. «Что означает Too many open files (слишком много открытых файлов) — разминка.
  2. «Чей это лимит — процесса, пользователя или машины?» — здесь начинается содержание.
  3. «Мягкий и жёсткий лимит — в чём разница?» — знаете ли вы, что один из них процесс может поменять себе сам.
  4. «Что считается дескриптором?» — назовёте ли вы сокеты и epoll, а не только файлы.
  5. «Почему поднятие лимита в консоли не помогло?» — вопрос про наследование.
  6. «Как отличить утечку дескрипторов от честной нехватки?» — вопрос про то, что смотреть.

Числа получены прогоном bench/rlimits/descriptors.py и bench/rlimits/practice.py. Здесь нет ни одного замера времени — только счёт дескрипторов и имена ошибок, — поэтому на любой машине с Linux повторяется всё, кроме значений самих лимитов: они у каждой машины свои.

База: дескриптор — это номер, а не файл

Прежде чем говорить о лимитах, стоит назвать обычными словами, что именно считают.

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

Нумерация своя у каждого процесса. Таблица номеров принадлежит процессу: номер 7 у одного процесса и номер 7 у другого указывают на совершенно разные вещи. Поэтому и счёт открытого ведут по процессам, а не по машине целиком.

Три номера заняты ещё до первой строки вашего кода. Это 0, 1 и 2 — ввод, вывод и поток ошибок; их процесс получает при рождении. Открывали их не вы, но заняты они точно так же, как всё остальное.

И вот вопрос, на который отвечает остальной урок: сколько номеров процессу разрешено держать одновременно и откуда берётся это разрешение? Разрешение есть всегда — бесконечных таблиц не бывает. У каждого процесса оно своё, и приходит оно не из его собственного кода, а от того, кто его запустил. Когда номера кончаются, ядро отказывается открыть следующий, и на этом месте в логе появляется знакомая строка про слишком много открытых файлов.

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

Механизм 1: лимитов два, и это не дублирование

контракт языкаПоведение, описанное в getrlimit(2). От дистрибутива и версии ядра оно не зависит.

getrlimit(2) определяет пару так:

The soft limit is the value that the kernel enforces for the corresponding resource. The hard limit acts as a ceiling for the soft limit: an unprivileged process may set only its soft limit to a value in the range from 0 up to the hard limit, and (irreversibly) lower its hard limit.

перевод

Мягкий лимит — это значение, которое ядро проводит в жизнь для соответствующего ресурса. Жёсткий лимит служит потолком для мягкого: непривилегированный процесс может лишь выставить свой мягкий лимит в диапазоне от 0 до жёсткого и (необратимо) понизить свой жёсткий.

getrlimit(2)

Три следствия, и все три спрашивают.

Проверяется мягкий. Ядро сверяется с ним в тот момент, когда процесс открывает очередной дескриптор: не хватило — отказ. Когда говорят «лимит дескрипторов равен тысяче», почти всегда имеют в виду именно его.

Жёсткий ограничивает не открытие, а подъём. Сколько дескрипторов процесс откроет, решает мягкий лимит; насколько высоко процесс может поднять себе мягкий — жёсткий. И поднять мягкий до жёсткого процесс может сам, без перезапуска. Это самый недооценённый способ починки: сервису, который упёрся в мягкий лимит, часто не нужен ни новый конфиг, ни перезапуск — достаточно одного вызова при старте.

Жёсткий понижается необратимо. Опустил — обратно не поднимешь, если у процесса нет CAP_SYS_RESOURCE; это свойство процесса, а не настройки.

Вот всё это в прогоне:

1. TWO LIMITS, AND ONLY ONE OF THEM IS ENFORCED
-----------------------------------------------
  soft limit (the one the kernel enforces)     20000
  hard limit (the ceiling for the soft one)    20000
  descriptors this process holds right now     3
  after lowering the soft limit to 256         256
  raised back to the hard limit, no restart    20000
  raising the hard limit                       refused: ValueError
наблюдение замераbench/rlimits/descriptors.py, Linux 6.18.44. Значения 20000 — свойство этой машины; у вас будут свои. Воспроизводится другое: мягкий поднимается до жёсткого, жёсткий выше себя не поднимается.

Обратите внимание на третью строку: у процесса уже три дескриптора — ввод, вывод и поток ошибок. Они тоже считаются — и именно из-за них в следующем разделе получится 61, а не 64.

Механизм 2: EMFILE — про процесс, а не про машину

Что происходит при исчерпании, getrlimit(2) говорит прямо:

This specifies a value one greater than the maximum file descriptor number that can be opened by this process. Attempts (open(2), pipe(2), dup(2), etc.) to exceed this limit yield the error EMFILE.

перевод

Задаёт значение на единицу больше максимального номера файлового дескриптора, который может открыть этот процесс. Попытки (open(2), pipe(2), dup(2) и т. д.) превысить этот лимит дают ошибку EMFILE.

getrlimit(2)

Ключевое слово — this process. Есть и общесистемный лимит, но он даёт другую ошибку, и open(2) разводит их явно: EMFILE — the per-process limit on the number of open file descriptors has been reached (достигнут лимит числа открытых файловых дескрипторов на процесс), ENFILE — the system-wide limit on the total number of open files has been reached (достигнут общесистемный лимит на общее число открытых файлов).

Практически это первое, что стоит выяснять по логу: какая из двух букв. EMFILE — виноват один процесс и лечится он на уровне процесса. ENFILE — кончился общий ресурс машины: proc(5) описывает его как /proc/sys/fs/file-max, файл, который defines a system-wide limit on the number of open files for all processes (задаёт общесистемный лимит на число открытых файлов для всех процессов). Правка лимита сервиса тут не поможет.

Смотрим на исчерпание. Потомок ставит себе мягкий лимит 64 и открывает файлы, пока ядро не откажет:

2. WHAT HITTING THE LIMIT LOOKS LIKE
------------------------------------
  soft limit set by the child                  64
  descriptors the child already had            3
  files it managed to open                     61
  error name                                   EMFILE
  error message                                Too many open files
наблюдение замераbench/rlimits/descriptors.py. Лимит 64, три дескриптора получены при рождении, открыть удалось 61 — арифметика сходится и воспроизводится.

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

Механизм 3: дескриптор — это не файл

Слово «файл» в названии сбивает. Считается любой объект, у которого есть номер в таблице дескрипторов процесса, и проверить это можно, открывая разные вещи до отказа:

3. IT IS NOT ONLY FILES THAT COUNT
----------------------------------
  file: opened before the refusal              61 — EMFILE: Too many open files
  socket: opened before the refusal            61 — EMFILE: Too many open files
  pipe: opened before the refusal              60 — EMFILE: Too many open files
  epoll: opened before the refusal             61 — EMFILE: Too many open files
наблюдение замераbench/rlimits/descriptors.py при мягком лимите 64. Канал даёт 60, а не 61, потому что открывается парой: два конца — два дескриптора.

Сокет, конец канала, экземпляр epoll — каждый занимает номер. Отсюда практический список того, что съедает лимит в реальном сервисе: соединения с базой, соединения HTTP-пула, слушающие сокеты, принятые соединения, inotify-наблюдатели, таймеры, открытые логи.

И отдельная строка про каналы: 60 вместо 61. Свободных номеров 61 — число нечётное, а канал берёт сразу два: тридцать каналов занимают шестьдесят номеров, и на тридцать первый одного оставшегося номера уже не хватает. Мелочь, но она объясняет, почему счёт открытых объектов и счёт занятых номеров расходятся: процесс, который порождает подпроцессы с перехватом вывода, тратит номера парами.

Механизм 4: лимит наследуется, и поэтому «я поднял, а не помогло»

Практическая ошибка, из-за которой лимит «не работает», обычно одна: его подняли не тому процессу. Лимит принадлежит процессу, но достаётся потомку, и execve(2) формулирует это от обратного — All process attributes are preserved during an execve(), except the following (При execve() сохраняются все атрибуты процесса, кроме следующих), — а ограничений ресурсов в списке исключений нет.

4. THE LIMIT IS PER PROCESS, AND IT IS INHERITED
------------------------------------------------
  soft limit set in the parent                 128
  soft limit the child starts with             128
  soft limit restored in the parent            20000
наблюдение замераbench/rlimits/descriptors.py. Родитель поставил себе 128, потомок стартовал со 128 — и после восстановления у родителя потомок ничего не заметил.

Отсюда правило, которым стоит отвечать: лимит выставляют тому, кто запускает. Если сервис поднимает менеджер служб, лимит задаётся в его описании службы; если контейнер — в его настройках; ulimit -n в вашей оболочке меняет лимит только для того, что вы запустите из этой оболочки, и ничего не значит для процесса, который стартовал иначе.

Проверять надо там же, где ядро его и держит: файл /proc/<pid>/limits показывает действующие лимиты конкретного процесса. Это единственный ответ, который не зависит от того, что написано в конфигах.

Глубже: утечка или честная нехватка

контракт языкаУстройство /proc/<pid>/fd и /proc/<pid>/limits описано в proc(5); от дистрибутива не зависит.

Обе выглядят одинаково — EMFILE в логе, — а лечатся противоположно. Различие видно по одному наблюдению: растёт ли число открытых дескрипторов монотонно.

Считается оно там же, где всё в этом уроке: в /proc/<pid>/fd лежит по записи на каждый открытый дескриптор, и их количество — это текущее потребление. Дальше два случая.

Число растёт и не падает при спаде нагрузки — это утечка: где-то не закрывается ответ, соединение, файл. Лимит её только отсрочит.

Число держится на полке, близкой к лимиту, и падает вместе с нагрузкой — это честная нехватка: столько соединений сервису и нужно. Тогда лимит поднимают, и поднимают тому, кто запускает.

Есть и третий случай, который путают с утечкой: сокеты в состоянии CLOSE_WAIT. Собеседник закрыл соединение, а приложение — нет, и дескриптор остаётся занятым, пока close не будет вызван. Сам он не освободится: это утечка, просто её причина видна снаружи.

Как отвечать на собеседовании

Короткий ответ: EMFILE — это лимит на число дескрипторов конкретного процесса, и считаются в нём не файлы, а все открытые объекты — сокеты, каналы, epoll, плюс три, которые процесс получил при рождении. Лимитов два: мягкий ядро проверяет при открытии очередного дескриптора, а жёсткий ограничивает то, насколько высоко процесс может поднять себе мягкий, — и поднять мягкий он может сам, без перезапуска.

Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.

Если интервьюер копает глубже

Хороший ответ отличают три вещи. Первая — вы различаете EMFILE и ENFILE: первый про процесс, второй про машину, и лечатся они в разных местах. Вторая — вы говорите про наследование и знаете, почему ulimit -n в чужой оболочке ничего не изменил: лимит достаётся от того, кто запускает. Третья — вы называете способ отличить утечку от нехватки: смотреть на счёт в /proc/<pid>/fd во времени, а не на факт ошибки.

Дальше спросят

Спросят дальше

Почему приложение упёрлось в лимит, хотя открывает всего десяток файлов?

Короткий ответ

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

Поэтому счёт стоит вести не по коду приложения, а по /proc/<pid>/fd. Там видно и то, чего в коде нет: пулы внутри клиентских библиотек, наблюдатели за файлами, таймеры.

Спросят дальше

Можно ли поднять лимит без перезапуска сервиса?

Короткий ответ

Мягкий — да, и это ровно то, что показывает первый блок прогона: процесс опустил себе мягкий лимит и поднял обратно до жёсткого, ничего не перезапуская. Если в программе есть такой вызов при старте, «поднять лимит» перестаёт быть операцией с простоем.

Выше жёсткого — нет: попытка в прогоне отвергнута. И если жёсткий кто-то понизил, поднять его обратно в том же процессе тоже нельзя — это описано как необратимое действие.

Спросят дальше

Чем EMFILE отличается от ENFILE и почему это важно?

Короткий ответ

EMFILE — лимит этого процесса; ENFILE — общесистемный лимит на число открытых файлов. Разные ошибки, разные потолки, разное лечение: в первом случае правят лимит процесса, во втором — общесистемную настройку, и правка не той стороны не даёт ничего.

Практическая ценность различения в том, что оно доступно сразу: имя ошибки уже есть в логе. Если там ENFILE, виноват может быть вовсе не ваш сервис — он просто оказался тем, кому не хватило.

Спросят дальше

Помогает ли поднять лимит до максимума и забыть?

Короткий ответ

Лимит — это не только защита от вас, но и защита от соседей: он ограничивает цену чужой ошибки. Подняв его до потолка, вы меняете EMFILE у одного сервиса на ENFILE у всей машины: общесистемный лимит из /proc/sys/fs/file-max никуда не делся, и упрётся в него уже не тот, кто виноват.

Полезнее другое: поднять лимит осознанно до величины, которая соответствует ожидаемому числу соединений, и следить за счётом в /proc/<pid>/fd. Тогда EMFILE перестаёт быть сюрпризом, а рост счёта становится сигналом заранее.

Частые заблуждения

Утверждение

сообщение Too many open files означает, что дескрипторы кончились на машине

На самом деле

Это лимит процесса: EMFILEthe per-process limit on the number of open file descriptors has been reached (достигнут лимит числа открытых файловых дескрипторов на процесс). Общесистемное исчерпание даёт другую ошибку — ENFILE, — и правится в другом месте. Первое, что стоит выяснить по логу, — какая из двух букв.

Утверждение

лимит один, и его меняет администратор

На самом деле

Лимитов два, и заняты они разным. Мягкий ядро проверяет в тот момент, когда процесс открывает очередной дескриптор; жёсткий ограничивает то, насколько высоко процесс может поднять себе мягкий. Поднять мягкий до жёсткого процесс может сам и без перезапуска — измерено в первом блоке прогона. Администратор нужен, только когда не хватает жёсткого.

Утверждение

при лимите 64 можно открыть 64 файла

На самом деле

Измерено: 61. Три дескриптора процесс получает при рождении — ввод, вывод, поток ошибок, — и они входят в лимит. Лимит считает не «файлы, которые открыл я», а все занятые номера.

Утверждение

дескрипторы тратятся только на файлы

На самом деле

Измерено при том же лимите: сокет — 61, канал — 60, epoll — 61. В сервисе основная масса дескрипторов приходится на соединения, а канал вообще берёт по два номера сразу, потому что у него два конца.

Утверждение

я поднял лимит через ulimit -n, значит сервису его хватит

На самом деле

Только если сервис запущен из этой самой оболочки: лимит наследуется от того, кто запускает. Измерено: родитель поставил себе 128 — потомок стартовал со 128. Сервису, поднятому менеджером служб, ваш ulimit неизвестен; проверять надо в /proc/<pid>/limits.

Утверждение

если поднять лимит повыше, проблема решена

На самом деле

Решена, если это была честная нехватка: число открытых дескрипторов держится на полке и падает вместе с нагрузкой. Если же оно растёт монотонно и не возвращается — это утечка, и лимит её только отсрочит. Различаются они не по тексту ошибки, а по счёту в /proc/<pid>/fd во времени.

Утверждение

жёсткий лимит можно опустить, а потом вернуть

На самом деле

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

Практика

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

Практика · что напечатает

Потомок ставит себе мягкий лимит 64 и открывает файлы, пока ядро не откажет. Печатаются три вещи: имя ошибки, её текст и мягкий лимит, с которым стартует ещё один потомок после того, как родитель поставил себе 128. Что напечатает этот код?
name, message, opened, held = probe(SOFT)
print(name)
print(message)
print(inherited_soft(128))

Практика · оцените

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

Проверка знаний

Вопрос 1 из 6

В логе сервиса — сообщение Too many open files. Что это говорит о лимите?

Источники и что читать дальше

4 ИСТОЧНИКА

  1. getrlimit(2), Linux man-pages 6.7Официальная документация. Откуда берутся два лимита и кто может их менять: «The soft limit is the value that the kernel enforces for the corresponding resource. The hard limit acts as a ceiling for the soft limit: an unprivileged process may set only its soft limit to a value in the range from 0 up to the hard limit, and (irreversibly) lower its hard limit» (Мягкий лимит — это значение, которое ядро проводит в жизнь для соответствующего ресурса. Жёсткий лимит служит потолком для мягкого: непривилегированный процесс может лишь выставить свой мягкий лимит в диапазоне от 0 до жёсткого и (необратимо) понизить свой жёсткий). И что именно ограничивает RLIMIT_NOFILE: «This specifies a value one greater than the maximum file descriptor number that can be opened by this process. Attempts (open(2), pipe(2), dup(2), etc.) to exceed this limit yield the error EMFILE» (Задаёт значение на единицу больше максимального номера файлового дескриптора, который может открыть этот процесс. Попытки (open(2), pipe(2), dup(2) и т. д.) превысить этот лимит дают ошибку EMFILE).https://man7.org/linux/man-pages/man2/getrlimit.2.html
  2. open(2), Linux man-pages 6.7Официальная документация. Различение, на котором строится половина урока. Про лимит процесса: «The per-process limit on the number of open file descriptors has been reached» (Достигнут лимит числа открытых файловых дескрипторов на процесс). Про лимит машины: «The system-wide limit on the total number of open files has been reached» (Достигнут общесистемный лимит на общее число открытых файлов). Первое — это EMFILE, второе — ENFILE: разные ошибки, разные лимиты и разное лечение.https://man7.org/linux/man-pages/man2/open.2.html
  3. execve(2), Linux man-pages 6.7Официальная документация. Почему лимит достаётся потомку — правило сформулировано от обратного: «All process attributes are preserved during an execve(), except the following» (При execve() сохраняются все атрибуты процесса, кроме следующих), и ограничений ресурсов в списке исключений нет. Отсюда практический вывод урока: лимит выставляют тому, кто запускает, а не тому, кто работает.https://man7.org/linux/man-pages/man2/execve.2.html
  4. proc(5), Linux man-pages 6.7Официальная документация. Где смотреть, а не гадать. Счёт открытых дескрипторов: «This is a subdirectory containing one entry for each file which the process has open, named by its file descriptor» (Это подкаталог, содержащий по одной записи на каждый открытый процессом файл, названной по его файловому дескриптору). Действующие лимиты процесса: «This file displays the soft limit, hard limit, and units of measurement for each of the process's resource limits» (Этот файл показывает мягкий лимит, жёсткий лимит и единицы измерения для каждого из ограничений ресурсов процесса). И общесистемный потолок, из-за которого возникает ENFILE: «This file defines a system-wide limit on the number of open files for all processes. System calls that fail when encountering this limit fail with the error ENFILE» (Этот файл задаёт общесистемный лимит на число открытых файлов для всех процессов. Системные вызовы, упирающиеся в этот лимит, завершаются ошибкой ENFILE).https://man7.org/linux/man-pages/man5/proc.5.html