Немає нічого кращого для зіпсованого п’ятничного вечора, ніж пайплайн розгортання, який місяцями працював і раптом вилітає з помилкою: Text file busy. Такий же код, ті самі хости, та ж CI-робота. Тепер реліз падає посеред копіювання файлу й залишає сервіс у тому особливому стані, де він «працює», але «не зовсім та версія, яку ви хотіли».
Це одна з тих помилок у Linux, яка здається дрібною, поки ви не зрозумієте, від чого саме вас захищає ядро. Після цього ви перестанете намагатися «примусово перезаписати бінарник» (і не робіть цього) і перейдете на схеми розгортання, які не конфліктують з ОС.
Що насправді означає «Text file busy» (ETXTBSY)
У Linux Text file busy зазвичай є користувацьким відображенням ETXTBSY. Ядро повертає його, коли процес намагається змінити виконуваний файл, який наразі виконується (або в деяких випадках відкритий у режимі, що вказує на те, що він використовується як текст програми).
«Text» — це історичний термін Unix для сегмента виконуваного коду. «Busy» означає «хтось виконує його, не шарпайте цей файл».
Є важлива нюанс: Linux зазвичай дозволяє вам unlink (видаляти) або перейменовувати виконувані файли під час їх виконання. Запущений процес тримає відкриту файлову дескрипцію; директорна запис може зникнути. Це класичний трюк Unix. Але Linux значно менш охоче дозволяє вам записувати нові байти в той самий індексний вузол (inode) виконуваного файлу. Саме тут з’являється ETXTBSY. ОС каже: «ви можете замінити файл, але не змінювати його на місці».
Якщо ваша стратегія розгортання — «копіювати новий бінарник поверх старого шляху», ви робите мутацію на місці. Іноді це працює; іноді ні; іноді воно відмовляє у найневідповідніший момент. І Debian 13 із сучасними ядрами та поширеними файловими системами охоче забезпечує дотримання цієї межі.
Одне висловлювання варто наклеїти на стікер поруч зі скриптами розгортання: перефразовано: «надія — не стратегія»
— приписується в оупс-колах Gene Kranz, застерігаючи від інженерії на основі бажань. Якщо ваш реліз покладається на «можливо файл не виконується, коли я його перезаписую», ви працюєте на надії.
Як це виглядає на практиці
Ви побачите це у кількох поширених варіантах:
cp: cannot create regular file '...': Text file busymv: cannot move 'new' to 'old': Text file busy(менш поширено, але з’являється з певними семантиками ФС)bash: ./mybin: Text file busy(коли скрипт або автоматизація намагається виконати те, що замінюють)- Провали апгрейдів пакетів, коли скрипти підтримки намагаються замінити використовуваний бінарник неатомарно
Жарт №1: ETXTBSY — це ввічливий спосіб Linux сказати «я бачу, що ви намагаєтеся зробити, і я обираю насильство стосовно вашого скрипта розгортання».
Чому розгортання падають на Debian 13: реальні механізми
Ця помилка — не «річ Debian». Це річ Unix і Linux, яка стає помітною, коли ваш метод розгортання несумісний зі способом поведінки ядра, файлової системи та завантажувача рантайму.
Механізм 1: перезапис на місці виконуваного файлу
Канонічний режим відмови надзвичайно простий:
- Сервіс працює з
/opt/myapp/myapp. - Розгортання робить
cp myapp /opt/myapp/myappабо завантажує прямо в той самий шлях. - Ядро відмовляє в записі/заміні, бо файл «зайнятий» як виконуваний текст.
Деякі інструменти більш хитрі, ніж здаються. «Безпечна» утиліта копіювання може відкрити призначення для запису і виконати усікання перед копіюванням. Це саме той тип мутації на місці, для якого ETXTBSY призначений зупиняти.
Механізм 2: bind-монти та семантика томів контейнерів
Якщо ви робите bind-mount каталогу хоста в контейнер, часто ви розгортаєте, записуючи в цей каталог з хоста (або з іншого контейнера). Процес всередині контейнера має файл відкритим/виконує його, тому заміна на стороні хоста може викликати ETXTBSY. Межа контейнера не змінює семантику ядра; вона лише змінює, хто отримує звинувачення.
Механізм 3: спільні бібліотеки, завантажувачі і «це не тільки бінарник»
Іноді те, що ви перезаписуєте, — не головний виконуваний файл. Це плагін, спільна бібліотека або допоміжний інструмент, який викликається під час розгортання.
Linux зазвичай нормально ставиться до оновлення спільних бібліотек через атомарну заміну (новий файл, перейменування на місці). Але якщо ваш інструмент розгортання записує безпосередньо в .so-файли на місці, ви можете викликати ту ж поведінку «busy». Також поширена самопідстава — заміна /usr/bin/python (або рантайму), поки скрипт підтримки все ще його використовує. Це не теоретично; так відбуваються вибухи скриптів при оновленнях.»
Механізм 4: граничні випадки файлових систем (NFS, overlayfs, «помічний» мережевий сховище)
Локальні файлові системи (ext4, xfs) поводяться передбачувано: rename атомарний; семантика unlink стабільна; застосування ETXTBSY відповідає очікуванням Linux. Мережеві ФС і шари overlay можуть додати додаткові перевірки або трохи іншу семантику. NFS, зокрема, має історію «silly rename» та складностей з кешуванням; воно може перетворити чисту заміну на дивний стан busy або затриману видимість.
Це важливо, бо багато «розгортань Debian 13» насправді є «Debian 13 поверх нової шарової системи зберігання, яку ми підключили непомітно». Коли ви бачите ETXTBSY, завжди запитуйте: чи змінилося сховище або середовище виконання?
Механізм 5: ваш пайплайн розгортання випадково виконує файл під час його заміни
Ось ще більш незручний випадок: скрипт розгортання перевіряє версію, виконавши myapp --version з того самого шляху, який збирається перезаписати. Якщо скрипт виконає цей бінарник, поки інший крок починає його перезаписувати, вітаю: ви влаштували гонку з самим собою.
Жарт №2: Найшвидший спосіб відтворити ETXTBSY — запланувати розгортання на той самий момент, коли ви пообіцяли Відділу продажів, що це буде «швидка зміна».
Швидкий план діагностики
Якщо ви на чергуванні і пайплайн червоний, зробіть це в порядку. Мета — ідентифікувати, який саме процес тримає виконуваний файл (або бібліотеку) відкритим, і чи робить ваш метод розгортання записи на місці.
1) Визначте точний шлях, що спричинив ETXTBSY
- З логів CI: зафіксуйте шлях файлу у рядку помилки.
- З системних логів: ідентифікуйте невдалу команду і ціль.
2) Знайдіть, хто використовує цей файл (процес + PID)
lsofабоfuserна шляху.- Підтвердіть, чи це мапінг виконуваного тексту (
txt) чи просто відкритий файловий дескриптор.
3) Визначте шаблон розгортання: атомарна заміна чи перезапис на місці
- Якщо бачите
cpпрямо в кінцевий шлях — ви перезаписуєте на місці. - Якщо бачите
rsyncу живій директорії — залежно від прапорів, можливо, ви перезаписуєте на місці. - Якщо бачите «запис у тимчасовий файл потім rename» і все одно отримуєте ETXTBSY — підозрівайте граничні випадки ФС/контейнера.
4) Вирішіть найменш ризикований негайний фікс
- Якщо сервіс можна перезапустити: зупиніть/перезапустіть сервіс, потім повторіть розгортання, використовуючи атомарну заміну.
- Якщо перезапуск ризикований: розгорніть у нову директорію релізу, переключіть через symlink, а потім акуратно перезапустіть (або використайте socket activation / handoff).
- Якщо це оновлення пакету: заплануйте перезапуск або використайте робочий процес
needrestart; не намагайтеся примусово писати файли.
5) Застосуйте постійне виправлення
- Змініть розгортання на незмінні директорії релізів + атомарну зміну вказівника.
- Або перемістіть виконуваний файл у версовані шляхи і тримайте стабільний symlink.
- Або гарантуйте, що файл, який замінюють, ніколи не модифікується на місці (завантаження у тимчасовий файл + rename).
Практичні завдання: команди, вивід і наступне рішення
Це перевірені в полі завдання. Кожне містить команду, приклад виводу, що це означає, і рішення, яке ви приймаєте далі. Запускайте з привілеями (root або через sudo) на ураженому хості.
Task 1: Confirm the failing syscall is ETXTBSY (strace the failing step)
cr0x@server:~$ strace -f -o /tmp/deploy.strace cp -f ./myapp /opt/myapp/myapp
cp: cannot create regular file '/opt/myapp/myapp': Text file busy
cr0x@server:~$ tail -n 5 /tmp/deploy.strace
openat(AT_FDCWD, "/opt/myapp/myapp", O_WRONLY|O_CREAT|O_TRUNC, 0666) = -1 ETXTBSY (Text file busy)
write(2, "cp: cannot create regular file"..., 72) = 72
exit_group(1) = ?
+++ exited with 1 +++
Значення: Ваш інструмент розгортання намагається відкрити призначення з прапором O_TRUNC (перезапис на місці). Ядро блокує це через мапінг виконуваного коду.
Рішення: Перестаньте робити перезапис на місці. Перейдіть на «запис у тимчасовий файл потім rename» або swap директорій релізів.
Task 2: Find which process is executing or mapping the file (lsof)
cr0x@server:~$ sudo lsof /opt/myapp/myapp | head
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
myapp 14231 myapp txt REG 259,2 18239440 1310723 /opt/myapp/myapp
Значення: PID 14231 виконує /opt/myapp/myapp (тип FD — txt).
Рішення: Ви не можете перезаписати цей inode. Або перезапустіть/зупиніть процес, або розгорніть новий inode і поміняйте вказівники.
Task 3: Confirm all processes referencing the inode (fuser with verbose)
cr0x@server:~$ sudo fuser -v /opt/myapp/myapp
USER PID ACCESS COMMAND
/opt/myapp/myapp: myapp 14231 ...e. myapp
Значення: ...e. вказує на доступ на виконання.
Рішення: Якщо показано кілька PID, ви маєте справу з мульти-воркерами або супервізором (systemd, gunicorn тощо). Плануйте перезапуск відповідно.
Task 4: Check whether the binary path is a symlink (and what it points to)
cr0x@server:~$ readlink -f /opt/myapp/myapp
/opt/myapp/releases/2025-12-30_121500/myapp
Значення: Стабільний шлях — це symlink у директорію релізу. Це хороша новина: ви можете атомарно поміняти цей symlink.
Рішення: Розгорніть новий реліз у нову директорію, потім переключіть symlink. Не чіпайте існуючу директорію релізу на місці.
Task 5: Inspect the service unit for ExecStart and WorkingDirectory (systemd)
cr0x@server:~$ systemctl cat myapp.service
# /etc/systemd/system/myapp.service
[Unit]
Description=MyApp API
[Service]
User=myapp
WorkingDirectory=/opt/myapp/current
ExecStart=/opt/myapp/current/myapp --config /etc/myapp/config.yaml
Restart=on-failure
[Install]
WantedBy=multi-user.target
Значення: Сервіс запускається з /opt/myapp/current. Якщо ваше розгортання змінює цю директорію на місці, ви ризикуєте ETXTBSY (і частковими оновленнями).
Рішення: Зробіть /opt/myapp/current symlink на незмінну директорію релізу і змінюйте лише symlink.
Task 6: Determine whether the running process is using a deleted file (classic “unlinked but still running”)
cr0x@server:~$ sudo ls -l /proc/14231/exe
lrwxrwxrwx 1 root root 0 Dec 30 12:18 /proc/14231/exe -> /opt/myapp/releases/2025-12-30_121500/myapp (deleted)
Значення: Ви вже замінили/відвязали файл, але процес усе ще працює зі старим inode (тепер видаленим). Це нормальна поведінка Unix.
Рішення: Плануйте перезапуск, якщо вам потрібна активна нова версія. Не намагайтеся «виправити» перезаписом; це не вплине на запущений процес.
Task 7: Verify the inode number and see whether deploy attempts to rewrite the same inode
cr0x@server:~$ stat -c 'inode=%i path=%n' /opt/myapp/myapp
inode=1310723 path=/opt/myapp/myapp
cr0x@server:~$ sudo ls -li /opt/myapp/myapp
1310723 -rwxr-xr-x 1 myapp myapp 18239440 Dec 30 12:15 /opt/myapp/myapp
Значення: У вас є конкретний inode. Якщо ваше розгортання робить перезапис на місці, воно тарґетує цей inode і викликає ETXTBSY.
Рішення: Переконайтеся, що розгортання створює новий inode (запис у інший файл, потім rename/symlink swap).
Task 8: Check how your deploy tool behaves (rsync flags matter)
cr0x@server:~$ rsync --version | head -n 2
rsync version 3.2.7 protocol version 32
cr0x@server:~$ rsync -av --inplace ./myapp /opt/myapp/myapp
rsync: [receiver] open "/opt/myapp/myapp" failed: Text file busy (26)
rsync error: errors selecting input/output files, dirs (code 3) at main.c(772) [Receiver=3.2.7]
Значення: --inplace — головний винуватець. Він примушує модифікацію на місці.
Рішення: Видаліть --inplace. Віддавайте перевагу --delay-updates і розгортанню в staging-шлях, або використовуйте директорії релізів.
Task 9: Determine whether the filesystem is overlayfs / container layer (mount)
cr0x@server:~$ mount | grep -E '/opt/myapp|overlay'
overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/...,upperdir=/var/lib/docker/overlay2/.../diff,workdir=/var/lib/docker/overlay2/.../work)
tmpfs on /run type tmpfs (rw,nosuid,nodev,size=328284k,mode=755)
Значення: Ви в середовищі overlayfs (поширено в контейнерах). Деякі операції поводяться інакше, і «заміна на місці» все одно погана ідея.
Рішення: Не мутуйте виконувані файли всередині робочого файлового шару контейнера. Створюйте новий образ і розгорніть його, або використовуйте патерн release-dir всередині примонтованого тому з атомарною зміною вказівника.
Task 10: If it’s system packages, see what dpkg tried to do (dpkg logs)
cr0x@server:~$ sudo tail -n 8 /var/log/dpkg.log
2025-12-30 12:04:41 upgrade nginx:amd64 1.26.0-1 1.26.2-1
2025-12-30 12:04:41 status half-configured nginx:amd64 1.26.2-1
2025-12-30 12:04:41 configure nginx:amd64 1.26.2-1
2025-12-30 12:04:42 status installed nginx:amd64 1.26.2-1
Значення: Відбулося оновлення dpkg; якщо ви бачили ETXTBSY в цей час, воно могло статися під час скриптів підтримки або postinst-спроби перезапуску.
Рішення: Якщо оновлення пакетів торкаються використовуваних компонентів, координуйте перезапуски і уникайте запуску довгих робіт розгортання під час apt-онз?
Task 11: Check for pending restarts (needrestart) and interpret what it tells you
cr0x@server:~$ sudo needrestart -r l
NEEDRESTART-VER: 3.6
Processes using old versions of upgraded files:
14231 /opt/myapp/current/myapp
Service restarts suggested:
systemctl restart myapp.service
Значення: Процес все ще використовує стару змонтовану версію файлу. Це варіант «ви замінили його, але він все ще працює».
Рішення: Заплануйте контрольований перезапуск. Якщо потрібен нульовий простоїв, робіть катання інстансів по черзі за балансувальником навантаження.
Task 12: Validate atomic symlink swap behavior (ln -sfn + readlink)
cr0x@server:~$ ls -l /opt/myapp
lrwxrwxrwx 1 root root 38 Dec 30 12:15 current -> /opt/myapp/releases/2025-12-30_121500
drwxr-xr-x 4 root root 4096 Dec 30 12:15 releases
cr0x@server:~$ sudo ln -sfn /opt/myapp/releases/2025-12-30_123000 /opt/myapp/current
cr0x@server:~$ readlink -f /opt/myapp/current
/opt/myapp/releases/2025-12-30_123000
Значення: Вказівник current перемістився миттєво. Існуючі процеси продовжують працювати з старим inode; нові запуски використовуватимуть нову ціль.
Рішення: Це примітив розгортання, який вам потрібен. Комбінуйте з перезапуском або акуратним reload.
Task 13: Prove the running binary version vs on-disk version (checksum via /proc)
cr0x@server:~$ sha256sum /opt/myapp/current/myapp
b1c5e3e2f4cbd9b5e6f3d5b2b5f5a9d6b9c8a7d5a3b2c1d0e9f8a7b6c5d4e3f2 /opt/myapp/current/myapp
cr0x@server:~$ sudo sha256sum /proc/14231/exe
9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b /proc/14231/exe
Значення: Запущений бінарник відрізняється від того, що зараз вказано на диску. Ваш реліз не набрав сили для цього PID.
Рішення: Перезапустіть або оновіть пул процесів по черзі. Не продовжуйте повторно деплоїти — це не змінить уже змеплені в пам’яті сторінки.
Task 14: If you suspect a race, catch who launches the binary during deploy (audit via ps and timestamps)
cr0x@server:~$ ps -eo pid,lstart,cmd | grep -E '/opt/myapp/current/myapp' | grep -v grep
14231 Tue Dec 30 12:18:02 2025 /opt/myapp/current/myapp --config /etc/myapp/config.yaml
Значення: Час запуску процесу збігається з часом розгортання. Часто це означає, що ваш скрипт розгортання або супервізор перезапустив його посеред копіювання.
Рішення: Робіть кроки розгортання ідемпотентними і послідовними: stage реліз, переключіть вказівник, потім перезапустіть (або перезавантажте) лише один раз, а не багаторазово.
Безпечні виправлення, що не перетворюють розгортання на рулетку
Виправлення ETXTBSY у продакшені — це більше про впровадження примітиву розгортання, який подобається ядру, ніж про «вбивство процесу, що тримає файл». Ви повинні перестати ставитися до запущеного виконуваного файлу як до зміненого блоку.
Виправлення 1: Використовуйте незмінні директорії релізів + атомарну зміну вказівника (symlink)
Це патерн, який я рекомендую найчастіше, бо він нудний, а нудність — найбільша похвала для системи розгортання.
Структура:
/opt/myapp/releases/<release-id>/myapp(незмінний)/opt/myapp/current -> /opt/myapp/releases/<release-id>(symlink-вказівник)- systemd сервіс запускає
/opt/myapp/current/myapp
Кроки розгортання:
- Завантажити в нову директорію релізу (поки не використовується).
- Перевірити здоров’я бінарника у директорії релізу напряму.
- Атомарно переключити symlink
current. - Перезапустити або перезавантажити сервіс (переважно граціозно).
Чому це працює: запущений процес продовжує використовувати старий inode. Новий реліз — це інший inode в іншій директорії. Ніяких перезаписів на місці. Нема ETXTBSY. Відкат теж — одна атомарна зміна symlink.
Виправлення 2: Запис у тимчасовий файл, fsync, потім rename (атомарна заміна)
Якщо вам обов’язково потрібно зберегти той самий шлях (наприклад, третя сторона очікує цей шлях), робіть безпечну заміну:
- Завантажуйте/записуйте в
myapp.newу тій же директорії. chmodі перевірте контрольні суми.mv -f myapp.new myapp(rename атомарний на тій самій ФС).
Важливо: rename атомарний, але це не магія. Якщо ви замінюєте виконуваний файл, rename зазвичай дозволений і безпечний, бо ви міняєте записи в директорії, а не мутуєте inode. Запущений процес усе одно використовуватиме старий inode.
Але не поєднуйте це з інструментами, що роблять оновлення на місці (cp в фінальний шлях, rsync --inplace).
Виправлення 3: Припиніть розгортати в живі, спільні директорії
Поширений антипатерн — розгортати в /usr/local/bin або в спільну директорію додатків, де кілька сервісів підбирають хелпери, плагіни або рантайми. Ви «просто оновлюєте хелпер», і раптом API-розгортання падає з ETXTBSY, бо хелпер виконується під час розгортання.
Тримайте артефакти застосунків приватними для сервісу, версованими і замінюйте через pointer swap. Спільні директорії залиште для системно-керованих пакетів, що оновлюються у контрольовані вікна технічного обслуговування.
Виправлення 4: systemd-патерни, що добре працюють з релізами
systemd не є причиною, але може посилити гонки, якщо ваше розгортання тригерить перезапуски в невдалий момент.
- Використовуйте
ExecStart, що вказує на стабільний шлях-symlink. - Визначте
ExecReloadдля граціозного перезавантаження, якщо додаток його підтримує. - Розгляньте
Restart=on-failure(неalways), щоб зменшити флапінг під час помилок розгортання. - Якщо є кілька воркерів, подумайте про socket activation або фронт-проксі, щоб розмежувати перезапуск та вплив на клієнтів.
Виправлення 5: Світ контейнерів: збирайте образи, не патчіть бінарники на місці
Якщо ви запускаєте контейнери і «деплоїте», копіюючи новий бінарник у живий контейнер, ви вручну винаходите найгірші частини дрейфу конфігурації.
Переважно:
- Збирайте новий образ з новим бінарником.
- Розгорніть новий образ (rolling update).
- Використовуйте незмінні теги або дайджести в продакшені, а не «latest».
Якщо ви мусите використовувати примонтований том для гарячої заміни артефактів, застосуйте патерн release-dir + symlink swap всередині тому. Не робіть перезапис на місці з боку хоста чи сайдкару.
Виправлення 6: Питання зберігання (бо «busy» може приховувати історію зі сховищем)
Як інженер зі зберігання, скажу відверто: ETXTBSY часто показує, що ваше розгортання очікує локальну семантику ФС, а ви дали йому щось інше.
- На NFS: переконайтеся, що директорія артефактів розгортається на локальному диску, якщо це можливо.
- Якщо мусите використовувати NFS: уникайте перезаписів на місці, віддавайте перевагу версованим директоріям і забезпечуйте однакові опції монтування по всьому кластеру.
- На overlayfs: ставтеся до файлової системи контейнера як до незмінної; використовуйте томи для змінних даних.
Три міні-історії з корпоративного життя (як це кусає команди)
Міні-історія №1: Інцидент, спричинений хибним припущенням
Вони мали невеликий флот Debian-серверів за балансувальником навантаження. Процес розгортання був «простим»: копіювати новий бінарник у /opt/app/app, потім надіслати сигнал на reload. Це працювало роками, і так погані припущення піднімаються до «архітектурних рішень».
Одного кварталу вони додали фонового раннера задач на ті ж хости. Він використовував той самий бінарник, викликаний з іншим прапорцем. Раннер контролював systemd і агресивно перезапускався при падінні. Під час розгортання пайплайн копіював бінарник, поки веб-сервіс працював. Іноді це не вдавалося, але не завжди. Потім стало гірше: раннер перезапустився посеред розгортання і намагався exec-нути бінарник в той же момент, коли розгортання усікало файл.
Результат: ETXTBSY в логах розгортання і випадкові крахи, коли процеси встигали виконати частково оновлений бінарник (бо деякі кроки запускалися від різних користувачів і не на всіх хостах усе було однаково). Вони звинуватили Debian. Debian був невинний; він робив свою роботу.
Виправлення не було «повторювати до успіху». Вони перейшли на незмінні директорії релізів. Веб-сервіс та раннер запускалися через /opt/app/current/app symlink. Розгортання створювало свіжу директорію релізу, змінювало symlink і перезапускало сервіси у контрольованому порядку. Хибне припущення було в тому, що «перезапис файлу еквівалентний заміні запущеної програми». Це не так.
Міні-історія №2: Оптимізація, що обернулася проти
Платформна команда хотіла швидших деплоїв і меншого використання диска. Хтось помітив, що копіювання повних директорій релізів споживає простір і час. Вони замінили підхід з директоріями релізів на «оптимізований rsync» в одну спільну директорію, використовуючи --inplace, щоб уникнути тимчасових файлів і зменшити кількість записів.
На тихому тестовому VM воно чудово пройшло бенчмарки. У продакшені розгортання почали падати з ETXTBSY. Ще гірше, з’явилися тонкі корупт-подібні симптоми під високим навантаженням: деякі хости коротко мали суміш старих і нових артефактів, бо rsync оновлював файли в порядку, що не відповідав очікуванням рантайму.
Команда відповіла ретраями і довшими тайм-аутами. Часи розгортання зросли. Відмови стали рідшими, але загадковішими. Балансувальник був здоровий, але користувачі бачили інтермітентні помилки, бо різні ноди віддавали різні версії під час часткового синку.
Вони відкотили «оптимізацію» і повернулися до версованих релізів. Використання диска зросло, як прогнозували. Надійність також зросла — і це була єдина метрика, що мала значення під час розбору інциденту. Урок: якщо ваша «оптимізація» знищує атомарність, це не оптимізація. Це технічний борг з гарною PR.
Міні-історія №3: Нудна, але правильна практика, що врятувала день
Інша організація працювала на Debian 13 зі суворим change management. Їх пайплайн завжди стажив артефакти у нову директорію з ідентифікатором релізу. Потім він запускав canary-перевірку, яка виконувала бінарник зі staged-шляху, а не з живого symlink. Лише після проходження перевірок вони міняли symlink.
Одного дня рутинний апгрейд ОС підтягнув нову рантайм-бібліотеку. Декілька сервісів потребували перезапуску, щоб підхопити нові мапінги бібліотек. Команда не помітила цього відразу, бо все продовжувало працювати. Але пізніше, під час розгортання, їх sanity-чек порівняв контрольну суму запущеного процесу (через /proc/<pid>/exe) зі staged-артефактом. Вона не співпала, і це було очікувано. Важливе: розгортання пройшло успішно, бо ніхто не намагався перезаписати живий бінарник на місці.
Під час вікна технічного обслуговування вони по черзі перезапустили сервіси. Ніякого ETXTBSY, ніяких зламаних оновлень, ніякого «чому пакетний менеджер вибухнув». Практика, що їх врятувала, була нудною й правильна: ніколи не мутувати живі виконувані файли; завжди міняти вказівники; завжди могти відкотитися, змінивши одну річ.
Вони не «виправляли» ETXTBSY, бо його рідко викликали. Це ідеальний стан: запобігати класу помилок, а не навчатися боротися з ним.
Типові помилки: симптом → корінна причина → виправлення
1) Симптом: cp падає з «Text file busy» при копіюванні бінарника
Корінна причина: Ви перезаписуєте inode запущеного виконуваного файлу (копія відкриває призначення з усіканням/записом).
Виправлення: Скопіюйте в інше ім’я і перейменуйте, або розгорніть у нову директорію релізу і переключіть symlink.
2) Симптом: помилка rsync код 26 або 3 з «Text file busy»
Корінна причина: rsync налаштований на оновлення на місці (--inplace) або таргетує живі шляхи з виконуваними файлами.
Виправлення: Приберіть --inplace. Використовуйте --delay-updates для стаджингу оновлень і потім атомарного переміщення тимчасових файлів, або краще — директорії релізів.
3) Симптом: розгортання «успішне», але сервіс все ще стара версія
Корінна причина: Ви замінили файл на диску, але процес все ще використовує старий inode (можливо він тепер видалений). Класична Unix-поведінка.
Виправлення: Перезапустіть сервіс (rolling restart). Перевірте через /proc/<pid>/exe checksum або needrestart.
4) Симптом: помилка відбувається лише в контейнерах, не на bare metal
Корінна причина: Ви латали живу файлову систему контейнера або bind-mounted том, поки бінарник виконувався всередині контейнера.
Виправлення: Збирайте і розгорніть новий образ. Якщо використовуєте томи для артефактів, застосуйте незмінні релізи і pointer swaps.
5) Симптом: ETXTBSY з’являється під час оновлення пакетів або unattended-upgrades
Корінна причина: Сервіс працює, поки пакети намагаються оновити виконувані файли або пов’язані допоміжні інструменти; скрипти підтримки можуть виконувати інструменти під час оновлення.
Виправлення: Координуйте оновлення з перезапусками сервісів, уникайте накладання апгрейдів пакетів та розгортання додатків, і використовуйте needrestart для керування перезапусками.
6) Симптом: тільки деякі хости падають, зазвичай ті під навантаженням
Корінна причина: Вікно гонки залежить від навантаження: повільніший I/O означає, що ваше вікно перезапису частіше перекривається з подіями exec/restart. Або скрипти розгортання поводяться інакше через таймінги.
Виправлення: Усуньте гонку: ніяких перезаписів на місці; лише атомарні зміни вказівників; серіалізуйте дії розгортання; зменшіть «restart storms».
7) Симптом: «mv: Text file busy», хоча rename має бути атомарним
Корінна причина: Часто це означає, що ви насправді не робите перейменування в межах тієї самої файлової системи (переміщення між пристроями), або ви на файловій системі зі спеціальною семантикою (overlayfs/NFS).
Виправлення: Переконайтеся, що тимчасовий файл створений у тій же директорії (той самий mount). Перевірте stat -f або mount і відкоригуйте місце розгортання.
Контрольні списки / покроковий план
Негайне стримування (під час інциденту)
- Заморозьте подальші спроби розгортання на тих же хостах (зупиніть флапінг).
- Визначте шлях, що спричинив ETXTBSY, і команду, яка це зробила.
- Використайте
lsofабоfuser, щоб ідентифікувати PIDs, що використовують файл. - Вирішіть, чи можна безпечно перезапустити:
- Якщо так: зробіть контрольований перезапуск (поштучно по хостах, якщо потрібно).
- Якщо ні: розгорніть нову директорію релізу і сплануйте граціозний cutover.
- Підтвердіть версію запущеного процесу через
/proc/<pid>/exechecksum або версійну команду.
Постійне усунення (щоб це припинилося)
- Прийміть одну з політик:
- директорії релізів + symlink swap (рекомендовано)
- тимчасовий файл + fsync + атомарний rename (прийнятно)
- образно-орієнтовані деплоя для контейнерів (найкраще для контейнеризованих навантажень)
- Аудитуйте інструменти розгортання на предмет перезаписів на місці:
- видаліть
--inplaceз rsync - уникайте
cpпрямо в фінальний шлях виконуваного файлу - не завантажуйте прямо в фінальний файл
- видаліть
- Зробіть перезапуски явними:
- systemd перезапуск у пайплайні після pointer swap
- rolling restarts за балансувальником навантаження
- граціозний reload де підтримується
- Додайте guardrail для розгортання:
- відмовлятися від розгортання, якщо
lsofпоказує, що ви збираєтеся перезаписати запущений виконуваний файл - відмовлятися, якщо цільовий шлях знаходиться на NFS/overlay, якщо не використовуються директорії релізів
- відмовлятися від розгортання, якщо
- Операціоналізуйте відкат:
- зберігайте N попередніх релізів
- повернутися назад через symlink + перезапуск
Перевірка (протестуйте, що виправлення працює)
- Розгорніть новий реліз при працюючому сервісі. Підтвердіть відсутність ETXTBSY.
- Підтвердіть, що symlink переключився і вказує на нову директорію.
- Перезапустіть сервіс і переконайтеся, що контрольна сума запущеного процесу відповідає артефакту.
- Відкотіть, перемістивши symlink на попередній реліз; перезапустіть; підтвердіть відкат через checksum.
- Запустіть два деплоя підряд і переконайтеся, що між ними немає часткового стану.
Цікаві факти та історичний контекст
- Назва помилки ETXTBSY старша за Linux: вона походить з раннього Unix, де «text segment» був формальним терміном для виконуваного коду.
- Unix дозволяє видаляти запущені виконувані файли: запущений процес тримає відкриту посилання на файл, тож запис у директорії може зникнути, а процес продовжує працювати.
- Rename атомарний (локально): на локальних POSIX-файлових системах swap записів директорії через
rename()атомарний в межах тієї самої ФС, і саме тому pointer swaps такі ефективні. - Оновлення «на місці» історично привабливі: адміністратори робили це, щоб зекономити простір і уникнути «дублювання бінарників», особливо коли диски були маленькі й дорогі.
- Мережеві ФС ускладнюють інваріанти: сумісність кешування NFS і поведінка клієнтів робили «атомарне й миттєве» обіцянкою з умовами.
- Контейнери не змінили ядра: неймспейси ізолюють процеси, але семантика виконання файлів залишається правилом ядра; ETXTBSY — не «баг контейнера».
- Пакетні менеджери вивчили жорсткі уроки: dpkg та інші опираються на rename-based заміни і уважне стаджинґ, бо перезапис живих системних бінарників — гарний спосіб зіпсувати оновлення.
- Запущений код сам себе не оновлює: заміна файлу на диску не «запатчить» вже змеплені сторінки в пам’яті; потрібні механізми перезапуску/перезавантаження.
ЧаПи
1) Чи є «Text file busy» багом Debian 13?
Ні. Це поведінка ядра, яку проявляє ваш метод розгортання. Debian 13 — просто місце, де ваш таймінг, навантаження чи шар зберігання зробили помилку помітною.
2) Чому rm іноді працює, а cp падає?
rm видаляє запис у директорії; запущений процес тримає inode відкритим. cp перезаписує/усікає той самий inode, що ядро блокує, коли він використовується як виконуваний текст.
3) Чи виправить проблему додавання ретраїв у деплой?
Ви можете зменшити кількість повідомлень для викликів, але базову гонку це не виправить, і рано чи пізно ви натрапите на хост, де таймінги ніколи не відповідатимуть. Замініть примітив розгортання натомість.
4) Якщо rename безпечний, чому я бачив mv: Text file busy?
Зазвичай тому, що це не було справжнє перейменування в межах тієї самої файлової системи (перехід між пристроями), або ви на файловій системі зі спеціальною семантикою (overlayfs, NFS). Переконайтеся, що тимчасовий файл у тій же директорії/монті.
5) Чи стосується це скриптів теж, чи лише бінарників?
Переважно — бінарники, але скрипти можуть викликати схожі проблеми, коли інтерпретатор відкриває їх таким чином, що тригерує перевірку busy, або коли розгортання виконує сценарій під час його перезапису. Не мутуйте ніякі «живі» entrypoint-скрипти на місці.
6) Як довести, який процес блокує деплой?
Використовуйте lsof <path> і шукайте мапінги txt або відкриті дескриптори. fuser -v також зручний для швидкого списку PID.
7) Чи завжди допомагає зупинка сервісу?
Зазвичай це усуває ETXTBSY для цього файлу. Але зупинка сервісів як механізм розгортання — це стратегія простоїв. Віддавайте перевагу атомарним swap-ам вказівників та контрольованим перезапускам для передбачуваності.
8) Який найбезпечніший «без сюрпризів» патерн розгортання на Debian?
Незмінні директорії релізів + атомарний symlink swap + systemd-контрольований перезапуск (rolling across nodes). Це уникає ETXTBSY і запобігає частковим розгортанням.
9) Чи потрібно перезапускати після оновлення бінарника, якщо шлях лишився той самий?
Так, якщо ви хочете, щоб запущений процес використовував новий код. Запущений процес не перемапить свої виконувані сторінки автоматично лише через зміну на диску.
10) Що робити, якщо я не можу перезапускати (жорсткі реального часу або довготривалі сесії)?
Тут потрібна архітектура: запускайте кілька інстансів, зливаєте підключення, або використовуйте супервізор/проксі, що підтримує граціозну передачу. ETXTBSY — симптом; «неможливо перезапустити» — це реальне обмеження, яке треба врахувати.
Висновок: кроки, які можна зробити вже сьогодні
ETXTBSY — це не «Linux поводиться суворо». Це ядро, яке захищає межу, через яку ваш процес розгортання не повинен переходити. Перезаписувати живий виконуваний файл на місці — це як міняти шину на машині, що їде по автостраді: технічно ви можете спробувати, але результат вам не сподобається.
Практичні кроки:
- Знайдіть точний шлях, що спричиняє «Text file busy», і ідентифікуйте PID, що його тримає, за допомогою
lsof. - Аудитуйте пайплайн на перезаписи на місці (
cpпрямо у фінальний шлях,rsync --inplace, прямі завантаження у живі локації). - Виберіть один безпечний примітив розгортання і стандартизуйтесь:
- директорії релізів + symlink swap (найкращий універсальний варіант)
- тимчасовий файл + fsync + rename (якщо треба зберегти шлях)
- нові образи контейнерів (якщо контейнеризовано)
- Зробіть перезапуски навмисними і поетапними, а не випадковими і гонячимися з копіюванням.
- Додайте один guardrail: відмовлятися від розгортання, якщо цільовий виконуваний файл змеплений (
lsofпоказуєtxt) і ви збираєтеся його перезаписати.
Зробіть ці п’ять речей, і «Text file busy» знову стане помилкою, про яку читають тільки у чужих постмортемах. Саме там їй і місце.