Файлові дескриптори в Linux закінчуються тихо, а ламають сервіс голосно. Якщо процес уперся в ліміт, він перестає відкривати файли, приймати нові з’єднання і часто сипле в логах Too many open files. Тут розберемо, де саме шукати проблему, як відрізнити утечку від нормального піка, що міняти в ulimit і systemd, і як перевірити, що після правок сервіс справді ожив.
Коли файлові дескриптори стають реальною проблемою
Для веб-сервісів, проксі, self-hosted платформ і будь-якого демона з великою кількістю сокетів це звична межа. Один процес тримає не лише файли на диску. Туди ж входять сокети, пайпи, inotify-слухачі та інші I/O-ресурси.
Тому симптом виглядає підступно. CPU може бути в нормі. Пам’ять теж. А нові з’єднання вже не приймаються, лог не пишеться або відвалюється робота з БД.
У Linux є два окремі обмеження. Перше – глобальне, на рівні ядра. Друге – на рівні конкретного процесу. На практиці майже завжди стріляє саме процесний ліміт.
Як це працює без зайвої теорії
Кожен open() або socket() займає запис у таблиці дескрипторів процесу. Коли вільних записів більше немає, системні виклики повертають помилку. Для процесного ліміту це зазвичай EMFILE. Для глобального – ENFILE.
Окремо варто пам’ятати про старий сценарій із select(). Якщо застосунок або бібліотека досі на ньому сидить, там є жорстке обмеження через FD_SETSIZE. Тоді упертися можна раніше, ніж у системний ліміт. Для навантажених сервісів це вже поганий знак. Нормальний шлях – poll або epoll.
У сучасних серверних сценаріях це особливо помітно на reverse proxy, API-шлюзах, брокерах і сервісах з великим fan-out по сокетах. У homelab так само легко впертися в файлові дескриптори на одному вузлі, якщо на ньому крутиться кілька self-hosted сервісів, логування, моніторинг і резервне копіювання.
Діагностика: де саме закінчилися файлові дескриптори
1. Спочатку перевірити ліміт процесу
Для поточної shell-сесії цього достатньо:
ulimit -n cat /proc/$$/limits
Якщо проблема в сервісі під systemd, цього мало. У сервісу може бути власний ліміт, який не збігається з тим, що ви бачите в консолі.
2. Подивитися, скільки дескрипторів уже відкрито
Швидкий спосіб – порахувати записи в /proc:
ls -1 /proc/<PID>/fd | wc -l
Або використати lsof, якщо треба зрозуміти, що саме висить:
lsof -p <PID> | wc -l lsof -p <PID>
Перший варіант легший і швидший. Другий дає фактуру, але сам по собі важчий. Якщо сервіс уже на межі, зайвий запуск lsof іноді тільки дратує ситуацію.
3. Відрізнити локальний упор від глобального
Далі є сенс подивитися системний стан:
cat /proc/sys/fs/file-max cat /proc/sys/fs/file-nr
Якщо поточна кількість виділених дескрипторів близька до системного максимуму, проблема вже не в одному сервісі. Тоді дивляться ще й системний журнал на предмет повідомлень про досягнення file-max.
4. Перевірити, що саме росте: файли, сокети чи inotify
Якщо процес тримає тисячі сокетів, це один сценарій. Якщо десятки тисяч відкритих логів або watch-слухачів – зовсім інший. Корисний мінімум такий:
lsof -p <PID> ss -s
За цим уже видно, чи проблема в мережевих з’єднаннях, чи в утечці файлових ресурсів. inotify теж витрачає файлові дескриптори, тому сервіси з активним file watching ламаються не лише через мережу.
Покроково: як виправити Too many open files
-
Знайти процес, що впирається в ліміт.
-
Звірити його фактичну кількість відкритих дескрипторів із дозволеним лімітом.
-
Подивитися, що саме росте – сокети, файли, слухачі.
-
Якщо є утечка, спершу прибрати причину. Підняття ліміту без цього лише відсуне збій.
-
Якщо навантаження штатне, підняти процесний ліміт.
-
Якщо сервіс працює під systemd, задати LimitNOFILE саме там.
-
Перевірити, чи не впирається система в глобальний fs.file-max.
-
Після змін перезапустити сервіс і повторно звірити ліміти та фактичне споживання.
Тимчасове підняття ліміту
Для поточної сесії можна змінити ulimit:
ulimit -n 100000
Це працює лише для поточної оболонки і процесів, які вона запускає. Для daemon-сервісів під systemd такий спосіб зазвичай не вирішує нічого.
Постійний ліміт через limits.conf
Для користувача або сервісного акаунта можна додати:
youruser soft nofile 200000 youruser hard nofile 200000
Це вступить у силу після нового логіну або повного перезапуску сесії. Але для багатьох системних сервісів цього все одно недостатньо.
Правильний шлях для systemd
Якщо сервіс стартує через systemd, ліміт треба задавати в unit-файлі або drop-in override:
[Service] LimitNOFILE=65535
Після змін:
systemctl daemon-reload
Далі – перезапуск самого сервісу. Інакше новий ліміт не підхопиться.
Це типовий момент, де все ламається. Адмін піднімає ulimit у shell, бачить красиве число, а сервіс далі працює зі старим обмеженням. Якщо у вас systemd, дивитися треба саме на параметри unit-а.
Коли треба чіпати fs.file-max
Глобальний ліміт має сенс змінювати лише тоді, коли процесні ліміти вже адекватні, а система реально близька до загального максимуму. Змінити його можна так:
sysctl -w fs.file-max=1000000
Потім значення фіксують у конфігурації sysctl. Але тут важлива проста річ: піднімати fs.file-max без розуміння сумарного профілю навантаження – погана звичка. Якщо один сервіс тече, система просто тектиме довше.
Де зазвичай ламається на практиці
Найчастіший сценарій – утечка. Процес відкриває файли, сокети або watch-ресурси і не закриває їх вчасно. У коді це виглядає банально: open без close, accept без нормального життєвого циклу з’єднання, неохайна робота з логами або пулами підключень.
Другий сценарій – сервіс здоровий, але навантаження виросло, а старі ліміти залишилися з дефолтами. Це типово для reverse proxy, front-сервісів і self-hosted платформ після розростання числа користувачів.
Третій – неправильне місце зміни конфігурації. У limits.conf усе красиво, а unit systemd все одно ріже процес. Саме тому файлові дескриптори треба перевіряти не лише по документації, а по фактичному стану запущеного процесу.
Ще одна пастка – вважати, що якщо помилка виглядає як файлова, то справа лише в диску. Насправді в сервісах мережевий трафік часто з’їдає дескриптори швидше, ніж будь-які локальні файли.
Як перевірити, що виправлення спрацювало
Після зміни лімітів або перезапуску сервісу перевірка має бути короткою і прямою.
-
Звірити нові ліміти процесу.
-
Подивитися фактичну кількість відкритих дескрипторів.
-
Перевірити, чи зникли помилки Too many open files з логів.
-
Дати штатне навантаження і подивитися, чи не росте число дескрипторів без повернення назад.
Практично це виглядає так:
cat /proc/<PID>/limits ls -1 /proc/<PID>/fd | wc -l lsof -p <PID> | wc -l
Якщо після піку навантаження значення стабілізується, це хороший знак. Якщо число тільки росте, проблема не вирішена. Ви лише дали утечці більше повітря.
Моніторинг: щоб не ловити збій постфактум
Для разової діагностики вистачає /proc, lsof і ss. Для постійного контролю краще мати метрики. У Linux для цього часто використовують Prometheus разом із node_exporter.
Там варто дивитися щонайменше на метрики, пов’язані з виділеними і максимальними файловими дескрипторами. Далі вже будувати алерти, коли система або вузол підходить до межі.
Але тут теж є межа зручності. Якщо треба бачити проблему по кожному процесу, системних метрик вузла вже мало. Тоді потрібен або окремий збір процесних метрик, або власний exporter під конкретний стек.
Коли підхід із підняттям лімітів уже незручний
Якщо сервіс стабільно споживає дуже багато дескрипторів, це може бути нормально. Наприклад, для проксі під великим числом одночасних з’єднань. Але якщо ви щоразу лікуєте проблему лише підняттям LimitNOFILE, межа швидко настане знову.
У цей момент треба дивитися ширше: балансування, кешування, рознесення ролей по вузлах, обмеження fan-out, нормалізація connection pooling. Іноді простіше винести частину навантаження в інший сервіс, ніж без кінця розкручувати ліміти на одному процесі.
Для homelab це особливо актуально. Один універсальний хост з Docker, reverse proxy, моніторингом, медіасервісами і резервними копіями впирається в файлові дескриптори швидше, ніж здається. Не тому, що Linux слабкий. Просто все живе в одному місці.
Висновок
Помилка Too many open files майже ніколи не означає, що треба просто підняти число в конфігу. Спершу треба зрозуміти, хто саме з’їдає файлові дескриптори, чому це відбувається і де стоїть реальний ліміт – у процесі, в systemd чи вже на рівні системи. Коли це видно, виправлення зазвичай просте. Коли ні – можна довго лікувати не ту частину стеку.