Docker «Text file busy» під час розгортання: виправлення, яке зупиняє ненадійні перезапуски

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

Ви виконуєте розгортання. Контейнер перезапускається. Потім ще раз. Логи показують класичну помилку: Text file busy.
Іноді це спрацьовує з другої спроби, іноді після п’яти, іноді лише коли ви дивитесь.

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

Що насправді означає «Text file busy» (і чому Docker отримує звинувачення)

У Linux Text file busy зазвичай відповідає ETXTBUSY: ви намагалися виконати операцію над виконуваним
файлом (історично називаним «text», бо сегмент тексту містить інструкції), поки він виконується або в іншому випадку заблокований
ядром так, що операція не може завершитись.

У світі контейнерів це трапляється під час розгортань, тому що ми змішуємо два світи:

  • Майже незмінні образи (чудово): шари адресуються за вмістом, збираються один раз, запускаються багато разів.
  • Змінні bind-монти (небезпечно): файли хоста з’являються всередині контейнера «вживу».

Класична схема відмови: CI-джоб, скрипт розгортання або сайдкар оновлює бінарний файл або скрипт entrypoint у шляху з bind-монтом,
поки існуючий контейнер ще запускається, зупиняється або перезапускається. Ядро відмовляє операції в найгірший момент.
Docker тут лише посередник. Але Docker — це саме той посередник, на якого зручно кричати.

Виправлення, яке знімає нестабільність, нудне, але абсолютне: ніколи не оновлюйте виконувані файли на місці у шляху, який
може виконуватися працюючим контейнером. Створюйте новий файл, а потім атомарно переключайте те, що «current» вказує (зазвичай через
заміну символьного посилання або swap bind-монта), або взагалі не використовуйте bind-монти для виконуваних файлів.

Цитата, яку варто приклеїти біля скриптів розгортання

«Надія — не стратегія.» (парафраз ідеї, поширеної в колах ops/reliability)

Якщо ваше розгортання покладається на «сподіваємось, старий процес вийде до того, як ми перезапишемо файл», це не розгортання.
Це гра в азарт з іншим брендом.

Жарт №1: Контейнери — це худоба, але той, що тримає ваш скрипт розгортання, завжди домашня тварина. У нього є ім’я, і він кусає.

Швидкий план діагностики

Це послідовність «у мене є 10 хвилин до того, як реліз-менеджер почне тему». Мета не в філософській чистоті.
Мета — визначити, чи маєте ви справу з (a) мутацією bind-монта, (b) поведінкою overlayfs/шарів, (c) гонкою під час завершення, або (d) з іншим джерелом.

По-перше: знайдіть конкретний файл, який «зайнятий»

  • Зі сторінок логів витягніть шлях: /app/bin/service, /entrypoint.sh, /usr/local/bin/foo.
  • Якщо логи не показують, запустіть невдалу команду під strace (див. завдання нижче), щоб зафіксувати, який файл повертає ETXTBUSY.

По-друге: визначте, чи цей шлях — bind-монт

  • docker inspect.Mounts для контейнера.
  • Якщо це bind-монт, ви, ймовірно, знайшли винуватця.

По-третє: ідентифікуйте процес, який тримає його відкритим/виконує

  • Використайте lsof або fuser на хості для шляху з боку хоста.
  • Якщо це в межах файлової системи контейнера (overlay2), перевірте запущені процеси та їхні шляхи виконуваних файлів.

По-четверте: вирішіть, в який режим відмови ви потрапили

  • Скрипт розгортання перезаписав працюючий виконуваний файл → виправте метод розгортання (атомарне переключення), припиніть редагування на місці.
  • Entrypoint змонтовано як bind і замінено → припиніть монтувати скрипти entrypoint; запікайте їх в образ або монтуйте тільки для читання з версійними шляхами.
  • Завершення роботи триває занадто довго → обробляйте SIGTERM, додайте preStop/wait, збільшіть grace, не примушуйте вбивати раніше часу.
  • Процес оновлює себе (так, таке досі трапляється) → приберіть самостійне оновлення; постачайте новий образ замість цього.

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

  • Припиніть перезапис; записуйте в новий шлях і перейменовуйте/міняйте symlink атомарно.
  • Монтуйте виконувані файли тільки для читання.
  • Тимчасово вимкніть «restart always», щоб уникнути шторму перезапусків, який ховає корінь проблеми.

Кореневі причини: 8 способів заробити ETXTBUSY

1) Перезапис на місці працюючого бінарного файлу (bind-монт або спільний том)

Хтось виконує cp new-binary /srv/app/bin/service, поки старий сервіс ще працює, або контейнер перебуває посеред запуску.
Linux дозволяє багато операцій над відкритими файлами, але заміна виконуваного файлу може викликати ETXTBUSY залежно від послідовності та файлової системи.
Повідомлення — це єдине попередження, що ваша модель розгортання живе небезпечно.

2) Замінення скрипта entrypoint, який зараз виконується

Shell-скрипти теж виконувані файли. Якщо ваш контейнер стартує з /entrypoint.sh через bind-монт, і ваш деплой оновлює цей файл,
ви можете отримати «text file busy» під час старту — саме тоді, коли оркестратор робить багато перезапусків, перевірок стану та нетерпіння.

3) CI/CD пише в директорію, яка одночасно є live runtime директорією

«Простий» підхід: джоб збирає артефакти і кладе їх у /srv/app/current. Інший джоб перезапускає контейнер.
Якщо ці кроки перекриваються або повторюються, ви створили умову гонки, а продакшн став арбітром.

4) Два контейнери ділять той самий хост-шлях і розгортаються несинхронно

Один контейнер все ще виконує старий код з спільного bind-монта; інший оновлює цей монт.
Вітаємо: ви реалізували розподілений конкурентний доступ, використовуючи лише shell-скрипти.

5) Агресивні політики рестарту створюють самостійний DoS

restart: always підходить, коли відмови рідкісні. Коли помилка викликається кроком розгортання, що повторюється,
ви отримуєте часті перезапуски. Такий цикл підвищує шанси потрапити в вікно заміни файлу.
Помилка стає «нестабільною», бо таймінги змінюються.

6) Особливості overlayfs при мутації шляхів, які мали бути незмінними

Драйвер Docker overlay2 розроблений для copy-on-write шарів. Більшість часу він поводиться як звична файлова система.
Але коли ви намагаєтеся робити хитрі речі — наприклад гарячо підміняти виконувані файли у записуваних шарах під час старту — ви спираєтеся на тонкощі:
whiteouts, copy-up і семантику шарів. ETXTBUSY може з’являтися не кожного разу, але саме тоді, коли це найменш бажано.

7) Неправильне розуміння атомарності: rename — атомарний, copy — ні

Люди кажуть «ми оновлюємо атомарно», а потім показують cp. Копіювання файлу поверх іншого не є атомарним так, як вам потрібно.
Перейменування у межах однієї файлової системи є атомарним; копія з подальшим перезаписом запрошує часткові читання і дивні помилки.

8) Тривалий shutdown + примусове вбивство + негайний рестарт

Якщо ваш сервіс довго завершує роботу, а оркестратор вбиває його раніше, процес може ще залишатися під час наступної спроби старту
(або файлова система все ще має посилання на виконувані іноди). Тісні цикли підсилюють це.
Часто виправлення не в «sleep 5» (хоча це «працює»), а в зробленні завершення детермінованим і недопущенні перекриття кроків розгортання.

Жарт №2: Додавання sleep 10 для виправлення гонки — це як латати покрівлю купівлею голоснішого дощу.

Надійне рішення: припиніть редагувати виконувані файли на місці

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

Як це виглядає добре

  • Артефакти незмінні: бінар або скрипт записується один раз і більше не змінюється.
  • Активація атомарна: ви перемикаєте реліз A на реліз B за допомогою атомарної операції (перейменування символьного посилання, оновлення цілі bind-монта).
  • Відкат — та сама операція: повернутися до попереднього вказівника.
  • Контейнери не ділять змінні виконувані файли: якщо вони щось ділять — це дані, а не код.

Два патерни розгортання, що дійсно працюють

Патерн A: Запікати виконувані файли в образ (рекомендовано)

Образ контейнера — це артефакт. Розгортання означає: витягнути новий образ, запустити новий контейнер, зупинити старий контейнер.
Жодних bind-монтів для /app/bin. Жодного «гарячого патчу» всередині контейнера. Саме для цього створювався Docker.

Патерн B: Каталоги релізів + атомарна заміна symlink (коли потрібно використовувати bind-монти)

Іноді ви застрягли: регуляторні обмеження, величезні артефакти, повітряно-щільні збірки, спадкова логіка рантайму.
Добре. Тоді робіть це як дорослі:

  • Запишіть новий реліз у /srv/app/releases/2026-01-03_120501/.
  • Перевірте його (контрольні суми, права, smoke-тест).
  • Атомарно оновіть символьне посилання /srv/app/current, щоб вказувати на новий реліз.
  • Перезапустіть контейнери, які монтують /srv/app/current тільки для читання.

Ключ у тому, що /srv/app/current змінюється миттєво як вказівник; вміст директорії релізу ніколи не змінюється.
Це усуває «половинчасто скопійований виконуваний файл» і значно знижує ймовірність «text file busy», бо ви не перезаписуєте файл, який виконують.
Якщо щось все ще виконує старий бінар, воно продовжує виконувати його з його старого inode. Нові контейнери стартують на новому inode.
Так ви купуєте здоровий глузд, спираючись на семантику файлової системи.

Малі, але важливі заходи жорсткості

  • Монтуйте код тільки для читання у контейнері. Якщо хтось спробує змінити його, це завершиться явною помилкою.
  • Ніколи не робіть bind-монт поверх /usr/local/bin, якщо вам не до вподоби археологія.
  • Зробіть entrypoint незмінним (запікайте його в образ). Якщо треба монтувати — монтуйте версійований шлях і перемикайте через symlink.
  • Контролюйте рестарти: уникайте нескінченних циклів, що маскують реальні відмови; використовуйте backoff і оповіщення.

Практичні завдання: 12+ команд, що покажуть, що відбувається

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

Завдання 1: Підтвердіть підпис невдачі в логах контейнера

cr0x@server:~$ docker logs --tail=80 api-1
exec /app/bin/api: text file busy

Що це означає: Ядро відкинуло execve() для /app/bin/api (або шелл його викликав) з ETXTBUSY.

Рішення: Визначте, чи /app/bin/api належить шару образу або монту. Якщо це монт — припиніть його оновлювати на місці.

Завдання 2: Перевірте монти і швидко знайдіть bind-монти

cr0x@server:~$ docker inspect api-1 --format '{{json .Mounts}}'
[{"Type":"bind","Source":"/srv/app/current","Destination":"/app","Mode":"ro","RW":false,"Propagation":"rprivate"}]

Що це означає: /app — bind-монт з хост-шляху /srv/app/current. Якщо скрипти деплою модифікують файли під цим деревом, ви можете змагатися з виконанням.

Рішення: Перевірте, чи /srv/app/current — символьне посилання на версифіковані релізи. Якщо ні — впровадьте це.

Завдання 3: Перевірте, чи «current» — симлінк (і куди він вказує)

cr0x@server:~$ ls -l /srv/app/current
lrwxrwxrwx 1 deploy deploy 44 Jan  3 11:58 /srv/app/current -> /srv/app/releases/2026-01-03_115801

Що це означає: Добрий знак: current — вказівник. Якщо деплой атомарно оновлює симлінк, контейнери бачать чистий перехід.

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

Завдання 4: Спіймайте поведінку «копіювання на місце» у скриптах деплою

cr0x@server:~$ grep -R --line-number -E 'cp .* /srv/app/current|rsync .* /srv/app/current' /srv/deploy/scripts
/srv/deploy/scripts/deploy.sh:83:cp build/api /srv/app/current/bin/api

Що це означає: Хтось копіює прямо в live-дерево. Це й є гонка.

Рішення: Змініть деплой так, щоб стадіювати в нову директорію і робити symlink swap, або збирати новий образ і перевиконувати деплой.

Завдання 5: Визначте, хто утримує файл відкритим (на хості)

cr0x@server:~$ sudo lsof /srv/app/releases/2026-01-03_115801/bin/api | head
COMMAND   PID USER  FD   TYPE DEVICE SIZE/OFF    NODE NAME
api     23144  1001 txt    REG  253,0  834912 4123912 /srv/app/releases/2026-01-03_115801/bin/api

Що це означає: PID 23144 виконує бінар (відмітка txt). Перезапис цього inode саме так і викликає ETXTBUSY і гірше.

Рішення: Не перезаписуйте цей файл. Розгорніть новий inode (новий шлях) і перемкніть через симлінк; або зупиніть процес коректно перед заміною.

Завдання 6: Використайте fuser, щоб підтвердити процеси, що використовують виконуваний файл

cr0x@server:~$ sudo fuser -v /srv/app/releases/2026-01-03_115801/bin/api
                     USER        PID ACCESS COMMAND
/srv/app/releases/2026-01-03_115801/bin/api:
                     1001     23144 ...e.  api

Що це означає: Такий самий результат, інший інструмент: процес виконує файл.

Рішення: Виправте деплой, щоб уникати заміни файлу; не «повторюйте, доки не вийде».

Завдання 7: Побачте погляд контейнера на виконуваний файл і підтвердьте, що він збігається з монтом

cr0x@server:~$ docker exec api-1 readlink -f /app/bin/api
/srv/app/current/bin/api

Що це означає: Контейнер виконується з bind-монтованого дерева.

Рішення: Ставтеся до /srv/app як до сховища продуктивного коду. Версіонуйте його на диску; монтуйте тільки для читання; переключайте вказівники атомарно.

Завдання 8: Доведіть, чи файл модифікується під час деплою (inotify)

cr0x@server:~$ sudo inotifywait -m /srv/app/current/bin -e create,modify,move,delete
Setting up watches.
Watches established.
/srv/app/current/bin/ MODIFY api

Що це означає: Щось модифікує api на місці. Це ваш курцівник.

Рішення: Приберіть мутації на місці. Якщо потрібно оновити — запишіть api.new, перевірте, а потім перейменуйте або зробіть symlink swap.

Завдання 9: Перевірте поведінку циклу рестарту Docker

cr0x@server:~$ docker ps --filter name=api-1 --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
NAMES   STATUS                          IMAGE
api-1   Restarting (1) 3 seconds ago    api:prod

Що це означає: Контейнер флапає. Це підсилює таймінгові гонки і ускладнює інтерпретацію логів.

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

Завдання 10: Перевірте стан контейнера для останньої помилки

cr0x@server:~$ docker inspect api-1 --format '{{.State.Status}} {{.State.ExitCode}} {{.State.Error}}'
restarting 1 

Що це означає: Docker тут не зафіксував окремий рядок «error»; потрібні логи + трасування системних викликів для деталей.

Рішення: Використайте strace на невдалому exec-шляху або відтворіть у однорідному контейнері щоб зафіксувати джерело ETXTBUSY.

Завдання 11: Відтворіть у one-shot контейнері, що виводить невдалий exec

cr0x@server:~$ docker run --rm -v /srv/app/current:/app:ro api:prod /app/bin/api --version
bash: /app/bin/api: Text file busy

Що це означає: Навіть чистий one-shot запуск потрапляє в проблему. Це вказує, що хост-шлях активно модифікується або файл у проміжному стані.

Рішення: Зупиніть pipeline розгортання, перевірте цілісність файлу (розмір, контрольна сума, права) і підтвердіть, що ніхто не пише у live-шлях.

Завдання 12: Підтвердіть, чи файл замінюється через rename (добре) або перезаписується (погано)

cr0x@server:~$ sudo auditctl -w /srv/app/current/bin/api -p wa -k api-bin-watch
cr0x@server:~$ sudo ausearch -k api-bin-watch | tail -n 6
type=SYSCALL msg=audit(1735906101.220:911): arch=c000003e syscall=2 success=yes exit=3 a0=7f5b7a3c a1=241 a2=1b6 a3=0 items=1 ppid=1102 pid=28440 auid=1000 uid=1000 gid=1000 exe="/usr/bin/cp" key="api-bin-watch"

Що це означає: Ви зафіксували cp, що пише безпосередньо у виконуваний файл. Це неатомарно і колізує з виконанням.

Рішення: Замініть «копіювання поверх» на «запис нового файлу, потім rename» або «стейджинг релізу і symlink swap».

Завдання 13: Перевірте атомарність перейменування у вашій директорії деплою

cr0x@server:~$ cd /srv/app
cr0x@server:~$ ln -sfn /srv/app/releases/2026-01-03_115801 current.new
cr0x@server:~$ mv -Tf current.new current

Що це означає: mv -T трактує ціль як файл; -f змушує заміну. Це поширений, надійний патерн для атомарних оновлень симлінків у Linux.

Рішення: Стандартизувати це як крок активації. Жодних часткових копій у current.

Завдання 14: Підтвердіть, що монт всередині контейнера тільки для читання

cr0x@server:~$ docker exec api-1 sh -lc 'mount | grep " /app "'
/dev/sda1 on /app type ext4 (ro,relatime,errors=remount-ro)

Що це означає: Контейнер не може модифікувати код під /app. Це добре: забороняє самомодифікацію і перекладає відповідальність на pipeline розгортання.

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

Завдання 15: Перевірте час коректного завершення, щоб уникнути перекриття виконань

cr0x@server:~$ docker stop -t 30 api-1
api-1
cr0x@server:~$ docker ps -a --filter name=api-1 --format 'table {{.Names}}\t{{.Status}}'
NAMES   STATUS
api-1   Exited (0) 3 seconds ago

Що це означає: Процес коректно завершується в межах grace-періоду. Якби ні — ви б побачили примусове вбивство і вищу ймовірність колізій з розгортанням.

Рішення: Якщо shutdown повільний, виправте обробку сигналів, додайте preStop-хуки і налаштуйте таймаути. Не компенсуйте це «повторами деплою».

Завдання 16: Використайте strace, щоб підтвердити системний виклик, що повертає ETXTBUSY

cr0x@server:~$ strace -f -e trace=execve,openat,rename,unlink -s 256 docker run --rm -v /srv/app/current:/app:ro api:prod /app/bin/api --version
execve("/usr/bin/docker", ["docker", "run", "--rm", "-v", "/srv/app/current:/app:ro", "api:prod", "/app/bin/api", "--version"], 0x7ffd1efc8b10 /* 36 vars */) = 0
...
execve("/app/bin/api", ["/app/bin/api", "--version"], 0x55d2b5d3d3a0 /* 14 vars */) = -1 ETXTBUSY (Text file busy)

Що це означає: Ніяких здогадок. Ядро повернуло ETXTBUSY при execve цього шляху.

Рішення: Розглядайте це як помилку у життєвому циклі артефакта. Змініть спосіб створення і активації файлу; не «налаштовуйте Docker» під це.

Три міні-історії з корпоративного життя

Міні-історія №1: Інцидент через неправильне припущення

Середня фінтех-компанія запускала стек Docker Compose на кількох потужних ВМ. Вони доставляли Go-бінар та кілька скриптів через bind-монт:
/srv/finapp/current змонтовано в /app. Припущення було просте і неправильне: «Linux дозволяє замінювати файли, поки вони в використанні».

Джоб деплою робив cp нового /srv/finapp/current/bin/api і відразу запускав docker compose up -d.
У тихі дні це працювало. У зайняті дні деякі контейнери перезапускалися, потрапляли на Text file busy і флапали. Pager спрацював.
Люди звинувачували «ненадійний Docker», бо помилка з’являлася при старті контейнера, а не під час копіювання.

Постінцидентний розбір показав справжню модель відмови: кілька інстансів додатка виконували bin/api з bind-монта,
поки деплой замінював його на місці. Іноді копія і exec зіткнулися. Іноді копія частково записала і наступний exec отримав іншу помилку. Вони створили гонку,
що залежала від таймінгу, планувальника CPU і щедрої ділянки люті.

Виправлення не було екзотичним. Вони стадіювали артефакти у /srv/finapp/releases/<id>, перевіряли їх і атомарно міняли
current за допомогою mv -Tf. Також вони монтували /app тільки для читання, щоб жоден контейнер не міг його мутувати.
Наступне розгортання було нудним, що є правильним емоційним тоном для деплою.

Міні-історія №2: Оптимізація, що підвела

Рекламна техкомпанія хотіла швидших деплоїв. Вони втомилися збирати образи, тому спробували «впровадження артефактів»:
збирати один раз на хості, а потім монтувати в контейнер. Вони також ввімкнули агресивну політику рестарту, щоб сервіси «відновлювалися».

Це було швидко. Але й це стало чудовим рецептом перетворити нешкідливу гонку в глобальний фестиваль рестартів.
Коли стартував деплой, контейнери швидко рестартували, деякі схопили файл у середині оновлення, і кілька потрапили в ETXTBUSY.
Політика рестарту включилася, що знову перезапустило їх, підвищуючи шанси зіткнення з вікном деплою. Петля зворотного зв’язку досягнута.

Команда «виправила» це, додавши sleep між копією і рестартом, потім додавши ще більше sleep, коли перший сон був недостатнім.
Деплоями стали повільнішими. Відмови стали рідшими. Потім настав дуже зайнятий день і таймінги знову зрушилися.
Помилка повернулася, як сезонна алергія.

Справжнє виправлення — припинити оптимізувати неправильну річ. Вони повернулися до збірки образів для продакшну і використовували bind-монти тільки в dev.
Для кількох сервісів, які ще потребували хост-монтованих активів, вони застосували каталоги релізів і symlink swap. Час деплою трохи зріс.
Час інцидентів впав значно. Ось такий обмін ви хочете.

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

Постачальник SaaS у галузі охорони здоров’я мав сувору політику «незмінних артефактів». Інженери скаржилися на неї так, як люди скаржаться на ремені безпеки.
Кожне розгортання створювало версійований каталог релізу з контрольними сумами і маніфестом. Активація — це був symlink swap. Відкат — та сама операція.

Якось у п’ятницю збій у сховищі спричинив повторний джоб деплою посеред стаджингу. Другий джоб стартував, поки перший ще не закінчився.
Це могло б стати класичним інцидентом «text file busy», бо обидва джоби цілеспрямовано писали в один і той же шлях. Але вони не писали в один і той же каталог.
Кожен запуск стаджингу писав у новий, унікальний шлях релізу.

Крок активації використовував лок і атомарне оновлення current. Переміг лише один джоб. Інший швидко і голосно впав.
Сервіс ніколи не побачив часткових артефактів. Контейнери перезапустилися рівно один раз. Ніхто того дня не вивчив нове повідомлення про помилку.

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

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

1) Симптом: «Text file busy» лише під час деплою, потім зникає

Корінь: Оновлення артефакта на місці конфліктує з перезапусками контейнера; гонка залежна від таймінгу.

Виправлення: Стадіюйте у версифіковану директорію; атомарно перемикайте symlink; монтуйте код тільки для читання.

2) Симптом: це трапляється більше при високому трафіку

Корінь: Повільне завершення або довгий старт збільшує вікно перекриття; цикли рестарту підсилюють колізії.

Виправлення: Зробіть shutdown детермінованим (обробка SIGTERM), збільшіть grace period, додайте backoff; уникайте негайних штормів рестарту під час деплою.

3) Симптом: лише один вузол показує проблему

Корінь: Неправильна поведінка скрипту деплою на вузлі, інша файлова система (NFS vs локальний ext4), або різні опції монтування.

Виправлення: Порівняйте типи і опції монтування, стандартизуйте механізм розгортання, уникайте виконання з мережевих шарів там, де можливо.

4) Симптом: «Text file busy» для entrypoint.sh або стартових скриптів

Корінь: Entrypoint змонтовано як bind і перезаписано; або менеджмент конфігів його переписує.

Виправлення: Запікайте entrypoint в образ; або версіонуйте скрипти і перемикайте вказівники, ніколи не перезаписуйте.

5) Симптом: деплой використовує rsync і все одно падає

Корінь: rsync за замовчуванням оновлює файли на місці; тимчасові файли/поведінка rename залежать від прапорів.

Виправлення: Використайте staging-директорію; або rsync у новий шлях релізу; потім symlink swap. Не rsync в «current».

6) Симптом: «Але ми використовуємо rename, воно має бути атомарним»

Корінь: Rename атомарний лише в межах тієї самої файлової системи і лише для самої операції rename; ваш процес може все ще перезаписувати або копіювати.

Виправлення: Переконайтеся, що staging і активація відбуваються в одній файловій системі; використовуйте mv -Tf для симлінків; уникайте переміщень між файловими системами.

7) Симптом: контейнер падає з «permission denied» після того, як ви «виправили»

Корінь: Нова директорія релізу має неправильні права/виконувані біти; read-only монт відкрив недбале пакування.

Виправлення: Встановіть правильні права на етапі збірки; перевірте через stat; додайте pre-activation smoke-test.

8) Симптом: це трапляється лише в Kubernetes, не локально

Корінь: Lifecycle hooks, швидке реседулювання та readiness/liveness рестарти створюють агресивніший таймінг. Також спільні томи зустрічаються частіше.

Виправлення: Уникайте спільних томів для виконуваних файлів; використовуйте образи; якщо томи необхідні, застосуйте версіфіковані шляхи і атомарне оновлення вказівників плюс коректні terminationGracePeriodSeconds.

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

Чекліст: Невідкладне стримування (сьогодні)

  1. Зупиніть шторм рестартів: тимчасово вимкніть автоматичні рестарти або зменшіть масштаб проблемного сервісу.
  2. Заморозьте джоби деплою, що пишуть у runtime-шлях.
  3. Визначте зайнятий шлях у логах або за допомогою strace.
  4. Перевірте, чи цей шлях — bind-монт; якщо так — вважайте його основним підозрюваним.
  5. Використайте lsof/fuser на хості, щоб підтвердити, який процес його виконує.
  6. Якщо потрібно, зупиніть сервіс коректно перед подальшими змінами. Уникайте примусових циклів вбивств.

Чекліст: Надійне виправлення (цього тижня)

  1. Визначте модель артефактів:
    • Перевага: запікати бінарі в образ і перевиконувати контейнери.
    • Резерв: версифіковані каталоги релізів + symlink swap.
  2. Змініть деплой, щоб стадіювати артефакти в унікальну директорію:
    • Писати в: /srv/app/releases/<release-id>/
    • Ніколи не писати в: /srv/app/current/
  3. Додайте перевірки перед активацією:
    • Перевірка контрольних сум/розміру
    • Перевірка прав (виконувані біти)
    • Базовий запуск тесту (--version або --help)
  4. Активація через атомарне переключення вказівника:
    • ln -sfn ... current.new
    • mv -Tf current.new current
  5. Монтуйте шлях коду тільки для читання у Docker/Compose/Kubernetes.
  6. Забезпечте коректне завершення:
    • Обробляти SIGTERM
    • Встановити розумні таймаути для зупинки

Чекліст: Запобіжні заходи (цей квартал)

  1. Додайте CI-перевірку, що фейлить збірки, якщо скрипти деплою копіюють у «current».
  2. Додайте аудит/inotify під час canary-розгортань, щоб переконатися, що немає мутацій на місці.
  3. Стандартизуйте шляхи артефактів і патерни монтування між сервісами.
  4. Реєструйте конфлікти деплоїв: якщо два деплоя перекриваються — фейлити один з чіткою помилкою.
  5. Зробіть відкат першокласною операцією: зберігайте попередні релізи на диску; перемикайте symblink назад.

Цікаві факти та історичний контекст

  • ETXTBUSY — старий багаж Unix з сучасними наслідками: назва походить від «text segment», сегменту виконуваного коду в ранньому Unix.
  • Linux може unlink-ати працюючі виконувані файли: процес може продовжувати працювати навіть якщо його виконуваний файл видалено, бо він тримає відкритий інод.
  • Атомарне перейменування — одне з найміцніших гарантій файлової системи: на POSIX-файлових системах перейменування в межах однієї файлової системи атомарне, через що symlink swap працює.
  • Copy-on-write файлові системи змінили очікування: overlayfs і шарові образи заохочують незмінність, але bind-монти знову вводять змінність туди, де боляче.
  • Shell-скрипти теж можуть викликати ETXTBUSY: будь-що, що викликається через execve, може бути «зайнятим», не лише скомпільовані бінарі.
  • Цикли рестарту ховають корінь проблем: оркастратори швидко повторюють спроби; логи ротаються; перше повідомлення про помилку губиться під купою ідентичних рестартів.
  • NFS та мережеві файлові системи додають свої особливості: модель близько-відкриття-консистентності і кешування можуть породжувати таймінгові явища, що виглядають як ETXTBUSY або часткові оновлення.
  • «Гарячий патчинг» зазвичай запах деплою: самоновнісні сервіси були поширені до епохи контейнерів; з образами це майже завжди неправильний підхід.
  • Релізи на основі symlink існували до контейнерів: патерн десятиліттями використовували в хостингу вебів і аппсерверів, бо він відповідає тому, як поводиться ядро і файлові системи.

FAQ

1) Чи «Text file busy» — це баг Docker?

Майже ніколи. Це ядро, що відмовляє операцію (зазвичай execve або оновлення файлу) через те, як файл використовується.
Docker просто місце, де ви це бачите.

2) Чому це трапляється тільки іноді?

Гонки залежать від розкладу. Завантаження CPU, затримки вводу/виводу, таймінги рестарту та те, чи перекривається інший джоб деплою — усе це змінює вікно.
«Іноді» — саме так проявляються умови гонки.

3) Можу я виправити це додавши повтори або sleep?

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

4) Чи допомагає монтування директорії тільки для читання?

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

5) Що якщо я змушений використовувати bind-монти для коду (legacy)?

Використовуйте версифіковані каталоги релізів і символьний вказівник типу /srv/app/current. Ніколи не копіюйте в current.
Монтуйте current тільки для читання у контейнер. Перемикайте symlink атомарно.

6) Чому допомагає перемикання symlink, якщо процеси все ще виконують старий код?

Тому що процеси виконують іноди, а не рядки шляху. Працюючий процес продовжує використовувати вже відкритий інод.
Нові процеси розв’язують симлінк у новий інод. Ви уникаєте редагування інода, від якого залежить працюючий процес.

7) Чи відбувається це з overlay2 навіть без bind-монтів?

Може, але рідше. Більшість проблем ETXTBUSY при деплої походять від мутації файлів, змонтованих з хоста.
Якщо ви модифікуєте файли всередині контейнера під час рантайму (особливо виконувані), ви відтворюєте ту саму проблему в overlayfs.

8) Як довести, який процес відповідальний?

Використайте lsof або fuser на хост-шляху і підтвердіть мапінг монту контейнера через docker inspect.
Якщо потрібно, користуйтеся strace, щоб спіймати execve, що повертає ETXTBUSY.

9) Чи безпечний rsync з –inplace або –delay-updates?

--inplace активно небезпечний для живих виконуваних файлів. --delay-updates кращий, але найнадійніший патерн:
rsync у абсолютно нову директорію релізу, перевірити, потім переключити вказівник.

10) Яке найшвидше структурне виправлення з найменшим організаційним драмою?

Залиште існуючу структуру bind-монтів, але змініть деплой так, щоб створювати нову директорію релізу і робити атомарний symlink swap.
Це низькоімпактне, високоцінне і легко аудитоване рішення.

Висновок: що змінити в перший робочий день

«Text file busy» під час розгортань Docker — це не космічна таємниця. Це ваша файлова система, що каже вам, що метод розгортання небезпечний.
Виправлення також не містить загадок: припиніть мутувати виконувані файли на місці і активуйте релізи атомарно.

Наступні кроки, які приносять миттєвий ефект:

  1. Знайдіть шлях, що викликає ETXTBUSY, і підтвердіть, чи він змонтований з хоста.
  2. Припиніть in-place копії у живу директорію рантайму.
  3. Прийміть або деплои на основі образів, або каталоги релізів + symlink swap.
  4. Монтуйте код тільки для читання і зробіть entrypoint незмінним.
  5. Зменшіть шторм рестартів, щоб відмови були видимими, а не розмазаними по повторним спробам.

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

← Попередня
Ефективність: чому швидше не завжди краще
Наступна →
Семантика синхронізації ZFS через NFS: чому клієнти змінюють надійність записів

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