Docker «too many open files»: як правильно підвищувати ліміти (systemd + контейнер)

Було корисно?

Завжди одна й та сама історія: усе працює чудово, поки не приходить сплеск трафіку — тоді ваш застосунок починає кидати EMFILE, логи заповнюються повідомленнями «too many open files», а канал інцидентів перетворюється на групову терапію.

Коли це трапляється в Docker, часто «вирішують» проблему підняттям ліміту десь випадково. Іноді це допомагає. Частіше — ні, або працює лише до наступного деплою, перезавантаження чи ротації вузла. Зробімо це правильно: знайдемо, який ліміт насправді спрацьовує, піднімемо його на потрібному рівні (systemd, демон Docker, контейнер) і переконаємося, що ми не просто сховали витік.

Що насправді означає «too many open files» у контейнерах

У Linux «too many open files» зазвичай відповідає EMFILE (перевищено ліміт файлових дескрипторів для процесу) або ENFILE (вичерпано системну таблицю файлів). Більшість інцидентів у контейнерах — це EMFILE: один процес (або кілька) досягає свого ліміту FD і падає драматично: не може приймати з’єднання, не може відкрити лог-файли, не може виконати DNS-резолюцію, не може говорити з апстрімом, не може створити сокети. Іншими словами: не виконує свою роботу.

У Docker файлові дескриптори (FD) не є абстракцією контейнера. Це ресурс ядра. Контейнери ділять одне ядро, а в підсумку — і глобальну таблицю файлів. Але кожен процес все одно має пер-процесний ліміт (RLIMIT_NOFILE), який може відрізнятися для сервісів, контейнерів та процесів залежно від того, хто їх створив і які ліміти застосовано.

Отже, коли контейнер каже «too many open files», ви дебажите стек лімітів і налаштувань, включаючи:

  • Системну таблицю файлів ядра та максимально дозволені значення для процесів
  • Ліміти сесій користувача (PAM, /etc/security/limits.conf) — іноді релевантні, часто неправильно розуміються
  • Ліміти systemd для юніту dockerd (а іноді й для runtime контейнера, якщо він окремий)
  • Власні налаштування ulimit у Docker, що передаються в контейнер
  • Поведінку вашого застосунку: пулі з’єднань, keep-alive, спостерігачі файлів, витоки, шаблони логування

І так — ви можете «просто підняти ліміт». Але якщо робити це сліпо, ви даєте протіканому процесу більшу відерце і називаєте це надійністю. Це не інженерія; це відкладення проблеми.

Цитата, щоб не забувати: «Hope is not a strategy.» — General Gordon R. Sullivan

Швидкий план діагностики (перевірити перше/друге/третє)

Коли дзвонить пейджер і треба зупинити кровотечу, не починайте з редагування п’яти конфігів і перезавантаження хоста. Почніть з визначення, який саме ліміт ви досягли і де.

Перше: це пер-процесний (EMFILE) чи системний (ENFILE)?

  • Якщо падає лише один сервіс і хост здається стабільним, підозрюйте EMFILE.
  • Якщо багато несумісних сервісів одночасно не можуть відкривати сокети/файли, підозрюйте ENFILE або вичерпання на рівні хоста.

Друге: підтвердіть ліміт всередині проблемного контейнера

  • Перевірте ulimit -n всередині контейнера (або через /proc для точного процесу).
  • Подивіться, скільки FD процес фактично відкрив зараз (ls /proc/<pid>/fd | wc -l).

Третє: перевірте, що systemd надав Docker

  • Якщо dockerd обмежено малою величиною, це може обмежувати ліміти, які наслідують контейнери.
  • Підтвердіть LimitNOFILE у systemd-юніті Docker і який ліміт зараз у процесу Docker.

Четверте: усуньте сумнів «підняли, але нічого не змінилося»

  • Зміна /etc/security/limits.conf не впливає на сервіси systemd.
  • Зміна systemd-юнитів не вплине на вже запущені процеси до перезапуску.
  • Зміна дефолтів демона Docker не змінює вже запущені контейнери заднім числом.

П’яте: вирішіть, чи ви приховуєте витік

  • Стале зростання кількості FD протягом годин/днів — це шаблон витоку.
  • Сплеск FD з навантаженням і повернення назад частіше — нормальна масштабованість, але все одно потребує запасу.

Жарт #1: Файлові дескриптори як виделки на кухні офісу — усі думають, що їх багато, поки не приходить обід.

Цікаві факти та історичний контекст (коротко, по суті)

  1. Ранні UNIX мали малі ліміти FD (часто 20 або 64 на процес), бо оперативної пам’яті було мало, а навантаження — просте у сучасних стандартах.
  2. select(2) історично обмежував FD до 1024 через FD_SETSIZE, це вплинуло на дизайн серверів роками — навіть після появи кращих API.
  3. poll(2) і epoll(7) стали практичним вирішенням для масштабування великої кількості сокетів без обмеження select.
  4. Linux рахує «відкриті файли» широко: звичайні файли, сокети, pipe, eventfd, signalfd — багато чого, що для розробника не виглядає як «файл».
  5. systemd змінив правила гри, взявши на себе ліміти сервісів; редагування shell startup-файлів не впливає на сервіс, запущений PID 1.
  6. Дефолти Docker можуть бути консервативними залежно від пакета та дистрибутива; припущення, що «контейнери успадковують ліміт хоста», часто невірне.
  7. Тиск на системну таблицю файлів ядра проявляється як дивні затримки перед жорстким фейлом: спайки латентності, помилки connect(), та «рандомні» I/O збої.
  8. Деякі рантайми спалюють FD заради зручності (наприклад агресивний file watching у dev-режимі); відправляти це в продакшен — шлях до дзвінка о 2:00.

Стек лімітів: kernel, user, systemd, Docker, container

Ви не вирішите проблему, запам’ятавши одну чарівну ручку. Її вирішують через розуміння ланцюга відповідальності за RLIMIT_NOFILE і системні ліміти ядра.

1) Системна таблиця файлів ядра: fs.file-max

fs.file-max — це загальний системний ліміт на кількість файлових хендлів, які ядро виділятиме. Якщо ви досягнете його, багато речей одночасно впаде. Це варіант ENFILE і зазвичай катастрофічний для хоста.

2) Максимум на процес: fs.nr_open

fs.nr_open — це межа, яку накладає ядро на максимально можливу кількість відкритих файлів для процесу. Навіть якщо встановите ulimit -n у мільйон, ядро зупинить вас на nr_open. Це типова пастка «чому мій ліміт не застосувався?».

3) Ліміти процесу: RLIMIT_NOFILE та наслідування

Кожен процес має поточний soft-ліміт і hard-ліміт. Soft — це те, що процес використовує; hard — максимальна межа, до якої процес може самостійно підняти soft без привілеїв. Дочірні процеси наслідують ліміти від батька, якщо їх явно не змінено. Це важливо, бо:

  • dockerd успадковує від systemd
  • контейнери успадковують від runtime’у і налаштувань Docker
  • ваш процес застосунку успадковує від init/entrypoint контейнера

4) Ліміти сервісів systemd: правильне місце для налаштування

Для продакшн-хостів, що запускають Docker як сервіс systemd, найнадійніший спосіб задати FD-ліміти Docker — це systemd drop-in для docker.service. Правки в /etc/security/limits.conf не помилкові, просто часто не релевантні для сервісів під керуванням systemd.

5) Ulimits контейнерів Docker: явно краще, ніж «мабуть наслідує»

Ви можете задати ulimits для контейнера через Docker run-флаги, Compose або дефолти демона. Практичний підхід: встановлюйте ліміти явно для критичних сервісів. Це робить поведінку портативною між хостами і зменшує драму «на тім вузлі працювало».

Практичні завдання: команди, виходи, рішення (12+)

Це перевірки, які я реально виконую. Кожна включає що означає вихід і яке рішення приймати.

Завдання 1: Підтвердити тип помилки в логах (EMFILE vs ENFILE)

cr0x@server:~$ docker logs --tail=200 api-1 | egrep -i 'too many open files|emfile|enfile' || true
Error: EMFILE: too many open files, open '/app/logs/access.log'

Значення: EMFILE вказує на вичерпання FD для процесу, а не на колапс таблиці файлів у всьому хості.

Рішення: Сконцентруйтеся на ulimits контейнера/застосунку та на фактичному використанні FD процесом, перш ніж чіпати системні налаштування ядра.

Завдання 2: Перевірити ulimit контейнера зсередини

cr0x@server:~$ docker exec -it api-1 sh -lc 'ulimit -n; ulimit -Hn'
1024
1048576

Значення: Soft-ліміт 1024 (занадто малий для багатьох мережевих серверів). Hard-ліміт великий, отже процес міг би підняти soft, якби це зроблено або якщо entrypoint це робить.

Рішення: Підніміть soft-ліміт через Docker/Compose ulimits, щоб застосунок стартував із адекватним значенням.

Завдання 3: Визначити PID реального воркер-процесу

cr0x@server:~$ docker exec -it api-1 sh -lc 'ps -eo pid,comm,args | head'
PID COMMAND         COMMAND
1   tini            tini -- node server.js
7   node            node server.js

Значення: PID 7 — це реальний Node-процес; PID 1 — це init-обгортка.

Рішення: Інспектуйте ліміти та відкриті FD для PID 7, а не лише для PID 1.

Завдання 4: Прочитати ліміт процесу прямо з /proc

cr0x@server:~$ docker exec -it api-1 sh -lc "grep -E 'Max open files' /proc/7/limits"
Max open files            1024                 1048576              files

Значення: Підтверджує фактичний runtime-ліміт, застосований до процесу.

Рішення: Якщо він низький — виправляйте конфігурацію ulimit контейнера; якщо високий, але помилка все одно є — шукати витоки або перевірити fs.file-max/fs.nr_open.

Завдання 5: Порахувати відкриті FD у процесу зараз

cr0x@server:~$ docker exec -it api-1 sh -lc 'ls /proc/7/fd | wc -l'
1018

Значення: Процес практично досяг свого ліміту 1024; помилка очікувана.

Рішення: Підніміть ліміт і негайно зменшіть тиск на FD, якщо можливо (знизьте конкуренцію, пулі з’єднань, watchers).

Завдання 6: Подивитися, що саме ці FD

cr0x@server:~$ docker exec -it api-1 sh -lc 'ls -l /proc/7/fd | head -n 15'
total 0
lrwx------ 1 root root 64 Jan  3 10:11 0 -> /dev/null
lrwx------ 1 root root 64 Jan  3 10:11 1 -> /app/logs/stdout.log
lrwx------ 1 root root 64 Jan  3 10:11 2 -> /app/logs/stderr.log
lrwx------ 1 root root 64 Jan  3 10:11 3 -> socket:[390112]
lrwx------ 1 root root 64 Jan  3 10:11 4 -> socket:[390115]
lrwx------ 1 root root 64 Jan  3 10:11 5 -> anon_inode:[eventpoll]

Значення: Переважно сокети й eventpoll — типовий вигляд для завантаженого сервера. Якщо бачите тисячі однакових шляхів файлів — підозрюйте витік при відкритті/закритті файлів або логуванні.

Рішення: Якщо це переважно сокети — перевірте повторне використання з’єднань апстрімів і клієнтський keepalive; якщо це файли — аудитуйте відкриття/закриття файлів.

Завдання 7: Перевірити використання таблиці файлів на хості

cr0x@server:~$ cat /proc/sys/fs/file-nr
15872	0	9223372036854775807

Значення: Перше число — виділені файлові хендли. Третє — системний максимум (цей приклад фактично «дуже великий»). Якщо виділене наближається до максимуму, ви у зоні ENFILE.

Рішення: Якщо близько до максимуму — підніміть fs.file-max і шукайте FD-хогів на хості.

Завдання 8: Перевірити потолок ядра для відкритих файлів на процес

cr0x@server:~$ sysctl fs.nr_open
fs.nr_open = 1048576

Значення: Жоден процес не зможе перевищити цю кількість відкритих файлів незалежно від запитів ulimit.

Рішення: Якщо потрібно більше і ви розумієте витрати пам’яті, підніміть fs.nr_open (рідко потрібно).

Завдання 9: Подивитися поточний FD-ліміт демона Docker (наслідування systemd)

cr0x@server:~$ pidof dockerd
1240
cr0x@server:~$ cat /proc/1240/limits | grep -E 'Max open files'
Max open files            1048576              1048576              files

Значення: Сам демон Docker має великий ліміт — добре. Якби це було 1024 або 4096, спочатку виправляйте systemd.

Рішення: Якщо ліміт dockerd низький — застосуйте systemd-override і перезапустіть Docker. Не сперечайтеся з PID 1.

Завдання 10: Підтвердити systemd-ліміти для Docker

cr0x@server:~$ systemctl show docker --property=LimitNOFILE
LimitNOFILE=1048576

Значення: Це те, що systemd планує для сервісу, а не те, чого ви сподівалися.

Рішення: Якщо це низько — потрібен drop-in-override; якщо високо, але dockerd має низьке — ви забули перезапустити сервіс.

Завдання 11: Перевірити ефективні ulimits запущеного контейнера зовні

cr0x@server:~$ docker inspect api-1 --format '{{json .HostConfig.Ulimits}}'
[{"Name":"nofile","Soft":1024,"Hard":1048576}]

Значення: Docker явно виставляє soft-ліміт 1024 для цього контейнера.

Рішення: Виправте ваш Compose-файл або флаги запуску; не чіпайте sysctl ядра заради проблеми з пер-контейнерним soft-лімітом.

Завдання 12: Виміряти швидкість зростання FD (витік проти навантаження)

cr0x@server:~$ for i in 1 2 3 4 5; do docker exec api-1 sh -lc 'ls /proc/7/fd | wc -l'; sleep 5; done
812
845
880
914
950

Значення: Кількість FD стійко зростає за 25 секунд. Це не «нормальна варіативність». Може бути підвищення трафіку, але пахне витоком або некерованою конкуренцією.

Рішення: Підніміть ліміти для миттєвої стабільності, але відкрийте баг і інструментуйте використання FD. Також обмежте конкуренцію, допоки не зрозумієте криву зростання.

Завдання 13: Знайти найгірших споживачів FD на хості

cr0x@server:~$ for p in /proc/[0-9]*; do pid=${p#/proc/}; if [ -r "$p/fd" ]; then c=$(ls "$p/fd" 2>/dev/null | wc -l); echo "$c $pid"; fi; done | sort -nr | head
21012 3351
16840 2980
9021  1240

Значення: PID 3351 і 2980 тримають десятки тисяч FD. PID 1240 — dockerd із ~9k, що може бути нормальним на завантажених хостах.

Рішення: Смапіть топ-PID до сервісів. Якщо якийсь несумісний процес споживає FD — ви можете йти до вичерпання ресурсів на хості.

Завдання 14: Супоставити PID з systemd-юнитом (на хості)

cr0x@server:~$ ps -p 3351 -o pid,comm,args
PID COMMAND  COMMAND
3351 nginx    nginx: worker process

Значення: Це nginx. Питання: чому nginx тримає 21k FD? Ймовірно, забагато keep-alive, забагато upstream-з’єднань або витік через неправильну конфігурацію.

Рішення: Можливо, треба налаштувати worker_connections nginx або upstream keepalive, а не лише піднімати ulimit.

Правильне підвищення лімітів: systemd + Docker + контейнер

Є два аспекти, щоб зробити це правильно:

  1. Задати розумні дефолти для сервісу Docker (щоб ваша платформа не була крихкою).
  2. Задати явні ulimits для контейнерів, яким це потрібно (щоб поведінка була відтворювана та контрольована).

Крок 1: Встановити LimitNOFILE для Docker через systemd drop-in

Редагуйте через підтримуваний механізм systemd. Не форзьте убік vendor-юнити, якщо не любите конфлікти при оновленнях пакетів.

cr0x@server:~$ sudo systemctl edit docker

У редакторі додайте drop-in такого вигляду:

cr0x@server:~$ cat /etc/systemd/system/docker.service.d/override.conf
[Service]
LimitNOFILE=1048576

Потім перезавантажте systemd і рестартуйте Docker (так, перезапуск — ліміти застосовуються в момент exec):

cr0x@server:~$ sudo systemctl daemon-reload
cr0x@server:~$ sudo systemctl restart docker

Що ви захищаєте: від дефолту дистрибутива, який може бути 1024/4096, або від майбутнього образу вузла, що тихо скидає ліміти демона. Це поясний ремінь безпеки для платформи.

Крок 2: Переконатися, що ядро не є справжньою межею

Здебільшого fs.nr_open вже достатньо велике. Проте перевірте один раз і закріпіть у базовому наборі, якщо ваші навантаження FD-важкі.

cr0x@server:~$ sysctl fs.nr_open
fs.nr_open = 1048576

Якщо треба підняти (рідко), зробіть це явно й постійно:

cr0x@server:~$ sudo tee /etc/sysctl.d/99-fd-limits.conf >/dev/null <<'EOF'
fs.nr_open = 1048576
fs.file-max = 2097152
EOF
cr0x@server:~$ sudo sysctl --system
* Applying /etc/sysctl.d/99-fd-limits.conf ...
fs.nr_open = 1048576
fs.file-max = 2097152

Рекомендація: не крутіть fs.file-max «про всяк випадок» до космосу. Це не безкоштовно. Розрахуйте на реальну конкуренцію та додайте запас.

Крок 3: Явно встановити ulimits для контейнера (docker run)

Для одноразових тестів або екстреної міграції:

cr0x@server:~$ docker run --rm -it --ulimit nofile=65536:65536 alpine sh -lc 'ulimit -n; ulimit -Hn'
65536
65536

Значення: Контейнер стартує з вищими soft/hard-лімітами.

Рішення: Якщо це вирішує помилку зараз, перемістіть налаштування у Compose/Kubernetes spec, щоб не мати рукопашного походження конфігурації.

Крок 4: Явно встановити ulimits у Docker Compose

Compose робить це відтворюваним і оглядовим. Це хороша практика управління.

cr0x@server:~$ cat docker-compose.yml
services:
  api:
    image: myorg/api:latest
    ulimits:
      nofile:
        soft: 65536
        hard: 65536

Пересоздайте контейнер (ulimits не змінюються на ходу):

cr0x@server:~$ docker compose up -d --force-recreate api
[+] Running 1/1
 ✔ Container project-api-1  Started

Рішення: Якщо після пересоздання контейнер все ще показує 1024 — ви не деплоїте те, що думаєте (неправильний файл, неправильний проект, стара версія Compose або інший оркестратор керує цим).

Крок 5: Розглянути дефолти демона Docker (обережно)

Docker може встановлювати дефолтні ulimits для всіх контейнерів через конфіг демона. Це спокусливо. Це також грубий інструмент.

Використовуйте це, якщо ви контролюєте весь фліт і хочете консистентні дефолти, але зберігайте per-service overrides для дійсно FD-голодних робочих навантажень.

cr0x@server:~$ cat /etc/docker/daemon.json
{
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 65536,
      "Soft": 65536
    }
  }
}
cr0x@server:~$ sudo systemctl restart docker
cr0x@server:~$ docker run --rm alpine sh -lc 'ulimit -n'
65536

Попередження: дефолти демона можуть несподівано вплинути на навантаження, які були стабільні при 1024, але почнуть поводитися інакше з вищою конкуренцією. Підняття лімітів може виявити нові вузькі місця: max connections у БД, обмеження апстріму, тиск на NAT-таблицю тощо.

Крок 6: Перевірити зміну там, де це важливо: у процесі

Завжди валідуйте за реальним PID, що обробляє трафік.

cr0x@server:~$ docker exec -it api-1 sh -lc 'ps -eo pid,comm,args | head -n 5'
PID COMMAND         COMMAND
1   tini            tini -- node server.js
7   node            node server.js
cr0x@server:~$ docker exec -it api-1 sh -lc "grep -E 'Max open files' /proc/7/limits"
Max open files            65536                65536                files

Рішення: Якщо там показано правильний ліміт — налаштування виконано. Далі вирішуйте, чи потрібні зміни у дизайні застосунку.

Крок 7: Не забувайте, що застосунок може сам виставляти свої ліміти

Деякі рантайми і менеджери сервісів викликають setrlimit() при старті. Це може знизити ваш ефективний ліміт навіть якщо Docker/systemd щедрі.

Якщо ліміт контейнера високий, але ліміт процесу низький — перевірте на наявність:

  • Entrypoint-скриптів, що викликають ulimit -n з малим значенням
  • Налаштувань рантайму мови (наприклад JVM), шаблонів master/worker nginx, менеджерів процесів
  • Дефолтів у базовому образі

Жарт #2: «Ми виставили це в трьох місцях» — операційна версія «Я зберіг це на робочому столі».

Три корпоративні історії з практики

Міні-історія 1: інцидент через хибне припущення

Компанія запускала клієнтоорієнтований API на фліті Docker. Сервіс місяцями працював стабільно. Потім вийшов новий образ вузла — та ж сімейство ОС, новіший мажорний реліз. Через два дні API почав падати під навантаженням з періодичними 500. В логах у контейнерах було EMFILE. Реакція була в шоці: «Ми вже виставили nofile хоста в 1,048,576. Це не може бути через ліміти.»

Хибне припущення було тонким і поширеним: редагування /etc/security/limits.conf хоста впливає на контейнер Docker. Воно впливає на логін-сесії через PAM. Docker запускається systemd. systemd не віддає значення PAM-лімітів. У нього є власні налаштування лімітів.

Під час інциденту хтось підняв ulimit у Compose і перезапустив. Це допомогло — на деяких вузлах. На інших — ні. Така невідповідність була підказкою: дрейф образів вузлів змінив дефолт LimitNOFILE у systemd-юниті Docker, і лише деякі хости лишалися на старому значенні.

Виправлення було нудним і постійним: systemd drop-in для docker.service із LimitNOFILE, плюс явні per-service ulimits у Compose. Додали перевірку після провізії, яка фейлила вузол, якщо systemctl show docker -p LimitNOFILE не відповідав очікуванню.

Постійний результат був не «більше число», а спільне розуміння: контейнери не плавають над хостом; вони успадковують його. Припущення дешеві. Інциденти — ні.

Міні-історія 2: оптимізація, що повернулася бумерангом

Інша організація запускала high-throughput pipeline: веб-шар → черга → процесори → об’єктне сховище. Вони оптимізували для ефективності, постійно зменшуючи затримки. Хтось помітив TCP churn між процесорами і чергою і вирішив тримати з’єднання довше та підвищити конкуренцію воркерів.

У staging це виглядало чудово: менше handshakes, краща пропускна здатність, CPU більш рівний. Вони запустили у продакшен. Наступного тижня почалися rolling brownouts. Не повні відключення — гірше. Частка запитів почала падати; повторні спроби підсилювали навантаження; черга відставала; клієнт до об’єктного сховища почав таймаутитись. У логах — «too many open files».

Вони підняли ulimits. Це знизило частоту помилок, але система залишалася не стабільною. Ось де проявився бумеранг: вищі FD-ліміти дозволили сервісу тримати ще більше неактивних та напівзавислих з’єднань, що збільшило споживання пам’яті і тиск на downstream-обмеження (підключення до БД і трекінг балансера). Проблема була не «ми вдарили 1024», а «ми спроектували форму конкуренції, що крихка».

Виправлення було багатошаровим: помірне підвищення ulimit до раціональної бази, обмеження конкуренції, коротші keepalive для деяких апстрімів і краща поведінка backpressure. Після цього FD все ще підіймався під піком, але залишався обмеженим і передбачуваним.

Урок: підняття FD-лімітів — це не оптимізація. Це пропускна здатність. Якщо не контролювати конкуренцію і backpressure, ви просто впадете пізніше й голосніше.

Міні-історія 3: нудна, але правильна практика, що врятувала

Ще одна команда мала міксований фліт: bare metal, віртуалки, багато Docker, багато передавань між командами. Їх уже втомило «працює на моєму вузлі», тому вони зробили невеликий чекліст у bootstrap вузла: перевірити kernel sysctls, перевірити systemd-ліміти і перевірити дефолти демона Docker. Нічого фанового. Ніяких глянцевих дашбордів. Просто обмежувачі.

Одного дня новий реліз застосунку став тригерити EMFILE в одному регіоні. Інженери підозрювали регрес коду. Але on-call виконав чекліст: всередині контейнера ulimit -n був 1024. Це вже підозріло. На хості systemctl show docker -p LimitNOFILE показав низьке значення на підмножині вузлів.

Виявилося, що ті вузли були провіжнені паралельним пайплайном для тимчасового збільшення потужності. Він пропустив крок bootstrap. Застосунок не зламався; він сідав на хости з різними лімітами.

Оскільки команда мала відому добру базу і швидкі перевірки, реагування на інцидент було чистим: cordon/evict з поганих вузлів, патч bootstrap, поступове повернення потужності. Ніяких полювань на відьом, ніяких панічних правок у продакшні, ніяких героїчних «я вбив проблему швидко».

Нудна практика не потрапляє у слайд-діти. Вона тримає клієнтів онлайн.

Типові помилки: симптом → корінь → виправлення

1) Симптом: «Я підняв /etc/security/limits.conf, але контейнери все ще показують 1024»

Корінь: Docker запускається systemd; PAM-ліміти не застосовуються до сервісів systemd.

Виправлення: Встановіть LimitNOFILE у systemd drop-in для docker.service, перезапустіть Docker, потім пересоздайте контейнери.

2) Симптом: ulimit -n у контейнері високий, але застосунок все одно кидає EMFILE

Корінь: Ви перевірили шелл, а не процес. Реальний воркер має нижчий ліміт, або застосунок знизив його під час старту.

Виправлення: Інспектуйте /proc/<pid>/limits для воркер-процесу. Аудитуйте entrypoint-скрипти і менеджери процесів.

3) Симптом: підняття ulimit не допомогло; кілька сервісів не можуть відкривати файли/сокети

Корінь: Вичерпання системної таблиці файлів (ENFILE) або інший ліміт ядра (ephemeral ports, conntrack, пам’ять).

Виправлення: Перевірте /proc/sys/fs/file-nr, знайдіть топ-споживачів FD і піднімайте fs.file-max лише з доказами. Також перевірте мережеві таблиці, якщо є симптоми connect() помилок.

4) Симптом: ulimit застосовується після redeploy, але скидається після reboot або ротації вузла

Корінь: Ви виправили вручну один вузол, але не зберегли зміни у конфіг менеджменті, systemd drop-in або Compose/Kubernetes маніфестах.

Виправлення: Зафіксуйте зміни в infrastructure-as-code і мемах деплою; додайте перевірку в bootstrap.

5) Симптом: після підняття лімітів пам’ять росте і латентність погіршується

Корінь: Більше дозволених FD означає більше дозволеної конкуренції; ваш застосунок тепер тримає більше сокетів і буферів, що підвищує пам’ять і тиск на downstream.

Виправлення: Налаштуйте конкуренцію, пула, keepalive і backpressure. Встановлюйте ліміти на основі виміряного steady-state плюс запас, а не на теоретичних максимумах.

6) Симптом: не можу підняти вище певного числа навіть від root

Корінь: Ви вдарили в fs.nr_open або runtime контейнера відмовляється від більших значень.

Виправлення: Перевірте sysctl fs.nr_open. Підніміть його, якщо це обґрунтовано. Валідируйте через /proc/<pid>/limits.

7) Симптом: лише один контейнер отримує EMFILE, а на хості є багато вільних ресурсів

Корінь: У контейнера специфічний низький ulimit, часто успадкований від дефолтів демона або явно виставлений на 1024.

Виправлення: Встановіть ulimits для сервісу (Compose ulimits або --ulimit) і пересоздайте контейнер.

Чеклісти / покроковий план

Чекліст A: зупинити інцидент (15–30 хвилин)

  1. Підтвердити тип помилки в логах: EMFILE чи ENFILE.
  2. Перевірити ліміт процесу через /proc/<pid>/limits всередині контейнера.
  3. Порахувати відкриті FD для воркер-процесу. Якщо близько до ліміту — це ваш негайний обмежувач.
  4. Підняти ulimit контейнера (Compose/run flags) до розумного значення (зазвичай 32k–128k залежно від навантаження).
  5. Пересоздати контейнер, щоб нові ліміти застосувалися.
  6. Перевірити знову за воркер-PID.
  7. Застосувати тимчасове обмеження (зменшити кількість воркерів, конкуренцію, keepalive), якщо зростання FD неконтрольоване.

Чекліст B: закріпити зміни (той же день)

  1. Встановити systemd LimitNOFILE для docker.service через drop-in.
  2. Перевірити ліміт dockerd через /proc/<dockerd-pid>/limits.
  3. Встановити per-service ulimits у Compose, щоб поведінка була портативною.
  4. Задокументувати базові метрики: типовий FD на steady та піковому навантаженні.
  5. Додати health check (навіть простий скрипт), що сповіщає при досягненні 70–80% використання FD.

Чекліст C: запобігти повторенню (на наступний спринт)

  1. Розслідувати витоки: чи зростає FD монотонно при постійному навантаженні?
  2. Аудит пулів з’єднань: клієнти БД, HTTP keepalive, споживачі черг.
  3. Переглянути логування: уникати відкриття файлу на запит; надавати stdout/stderr агрегацію в контейнерах.
  4. Прогнати навантаження з видимістю FD: трекати FD як основний сигнал, а не як доповнення.
  5. Кодувати базовий профіль вузла: sysctls + systemd-ліміти у вашому пайплайні провізії.

FAQ

1) Чому мій контейнер показує ulimit -n як 1024?

Тому що 1024 — поширений дефолт soft-ліміту. Docker може передавати його явно, або ваш образ/entrypoint встановлює його. Перевірте docker inspect ...HostConfig.Ulimits і /proc/<pid>/limits.

2) Якщо я встановлю LimitNOFILE для Docker, чи треба ще контейнерні ulimits?

Так, для важливих сервісів. Ліміт демона — це базовий захист платформи; пер-контейнерні ulimits — це поведінка застосунку. Явні ліміти по сервісах запобігають дрейфу між хостами і змінам образів вузлів.

3) Чи шкодить підняття FD-лімітів хосту?

Прямо — ні, але це дає процесам можливість тримати більше об’єктів ядра і пам’яті. Якщо застосунок використовує цю потужність, пам’ять і навантаження на downstream зростуть. Потужність — інструмент, а не чеснота.

4) Який «розумний» nofile-ліміт для продакшн-контейнера?

Залежить. Багато веб-сервісів комфортно почуваються при 32k–128k. Проксі з високим фаном, message brokers або зайняті nginx можуть потребувати більше. Вимірюйте steady-state і пік, потім додавайте запас.

5) Я підняв ліміти, але все одно отримую «too many open files» під час DNS-запитів або TLS-рукопотискань. Чому?

Ці операції відкривають сокети і іноді тимчасові файли. Якщо воркер на FD-ліміті, усе, що вимагає нового FD, падає в несподіваних місцях. Підтвердіть FD count і ліміт воркера; не ганяйтеся за симптомом на місці помилки.

6) Чи поводяться Kubernetes-подібні під differently?

Принципи ті самі: ресурси ядра + пер-процесні ліміти. Клапани конфігурації відрізняються (runtime, kubelet-настройки, pod security context та спосіб, як runtime застосовує rlimits). Але принцип: перевірте в /proc реальний процес.

7) Чому мій змін не застосувався, поки я не перезапустив?

Бо ліміти застосовуються під час старту процесу (exec time). Зміни systemd вимагають перезапуску сервісу. Зміни ulimit контейнера вимагають пересоздання контейнера. Запущені процеси зберігають свої поточні ліміти.

8) Як визначити, витік це чи просто трафік?

Вимірюйте FD count з часом при відносно сталому навантаженні. Витік демонструє монотонне зростання без повернення. Навантаження зростає і спадає разом із конкуренцією. Завдання 12 — швидкий тест; найкраще — постійний моніторинг.

9) Можу я просто виставити все в 1,048,576 і забути?

Можете. Ви також дозволите багу споживати значно більше ресурсів до фейлу і перенесете вузькі місця в менш очевидні місця. Вибирайте ліміти, що відповідають архітектурі, і моніторьте.

10) Чи може «too many open files» викликатися проблемами з диском?

Косвено — так. Повільні диски і завислий I/O можуть змусити дескриптори залишатися відкритими довше, що підвищує одночасне використання FD. Але помилка все одно про ліміт/пропускну здатність — вирішіть і ліміти, і поведінку I/O.

Висновок: наступні кроки, що дійсно працюють

Коли Docker видає «too many open files», не сприймайте це як дивну особливість Docker. Це Linux робить саме те, що ви йому наказали: застосовує обмеження ресурсів. Ваше завдання — визначити, який ліміт, на якому шарі, і чи навантаження заслуговує більше потужності або ви ховаєте витік.

Зробіть це далі, у порядку:

  1. Підтвердіть реальний ліміт воркер-процесу у /proc/<pid>/limits та його поточне використання FD.
  2. Встановіть явні ulimits для сервісу (Compose/run flags), потім пересоздайте контейнер.
  3. Зробіть systemd drop-in для docker.service з LimitNOFILE, щоб платформа була консистентною після перезавантажень і ротацій вузлів.
  4. Вимірюйте поведінку FD під навантаженням і вирішуйте, чи ви вирішуєте питання пропускної здатності, чи просто ховаєте витік.

Якщо ви зробите ці чотири кроки, це перестане бути повторюваним інцидентом і стане вирішеним операційним питанням. Саме цього ви хочете: менше сюрпризів, менше геройств і більш тихе чергування.

← Попередня
Виправити помилку Proxmox «IOMMU не ввімкнено» для PCI passthrough (VT-d/AMD‑Vi) безпечно
Наступна →
Пріоритет resilver у ZFS: швидке відновлення без падіння продуктивності

Залишити коментар