Rien ne dit « bonne fin de semaine » comme une pipeline de déploiement qui fonctionnait depuis des mois et qui se met soudainement à tomber en erreur : Text file busy. Même code, mêmes hôtes, même job CI. Maintenant la mise en production échoue à mi-chemin d’une copie de fichier et laisse votre service dans cet état particulier où il est à la fois « en ligne » et « pas exactement la version souhaitée ».
C’est l’un de ces messages d’erreur Linux qui paraît mesquin tant que vous ne comprenez pas ce que le noyau essaie de vous protéger contre. Une fois que vous l’avez compris, vous cessez d’essayer de « forcer l’écrasement du binaire » (s’il vous plaît, ne le faites pas) et vous adoptez des modèles de déploiement qui ne combattent pas le système d’exploitation.
Que signifie réellement « Text file busy » (ETXTBSY)
Sur Linux, Text file busy est typiquement le visage côté utilisateur de ETXTBSY. Le noyau le renvoie lorsqu’un processus tente de modifier un fichier exécutable qui est en cours d’exécution (ou, dans certains cas, ouvert d’une façon qui indique qu’il est utilisé comme texte programme).
« Text » est un vocabulaire historique d’Unix pour le segment de code exécutable. « Busy » signifie « quelqu’un l’exécute, n’effacez pas dessus ».
Il y a une nuance importante : Linux vous permet normalement de unlink (supprimer) ou de renommer un exécutable pendant qu’il tourne. Le processus en cours conserve une référence fichier ouverte ; l’entrée d’annuaire peut disparaître. C’est l’astuce classique d’Unix. Mais Linux est beaucoup moins enthousiaste quand vous écrivez de nouveaux octets dans le même inode d’un exécutable en cours d’exécution. C’est là qu’intervient ETXTBSY. Le système d’exploitation dit : « vous pouvez remplacer ; vous ne pouvez pas muter en place. »
Si votre stratégie de déploiement est « copier le nouveau binaire par-dessus l’ancien chemin », vous faites une mutation en place. Parfois cela marche ; parfois non ; parfois ça échoue au pire moment. Et Debian 13, avec des noyaux modernes et des systèmes de fichiers courants, appliquera volontiers cette limite.
Une citation à garder sur un post-it près de vos scripts de déploiement : idée paraphrasée : « l’espoir n’est pas une stratégie »
— attribuée dans les cercles ops à Gene Kranz, généralement en avertissement contre l’ingénierie basée sur des souhaits. Si votre release dépend de « j’espère que le fichier n’est pas exécuté quand je l’écrase », vous fonctionnez sur l’espoir.
À quoi ça ressemble en pratique
Vous le verrez sous plusieurs formes courantes :
cp: cannot create regular file '...': Text file busymv: cannot move 'new' to 'old': Text file busy(moins courant, mais apparaît avec certaines sémantiques de systèmes de fichiers)bash: ./mybin: Text file busy(lorsqu’un script ou une automatisation tente d’exécuter quelque chose en cours de remplacement)- Mises à jour de paquets échouant quand un script du mainteneur tente de remplacer un binaire en cours d’utilisation de façon non atomique
Blague n°1 : ETXTBSY est Linux vous disant poliment « je vois ce que vous essayez de faire, et je choisis la violence contre votre script de déploiement ».
Pourquoi les déploiements échouent sur Debian 13 : les mécanismes réels
Cette erreur n’est pas « un truc Debian ». C’est une question Unix et Linux qui devient visible quand votre méthode de déploiement est incompatible avec le comportement du noyau, du système de fichiers et du chargeur d’exécution.
Mécanisme 1 : écrasement en place d’un exécutable en cours d’exécution
Le mode d’échec canonique est douloureusement simple :
- Le service tourne
/opt/myapp/myapp. - Le déploiement fait
cp myapp /opt/myapp/myappou télécharge dans le même chemin de fichier. - Le noyau refuse l’écriture/le remplacement parce que le fichier est « busy » en tant que texte exécutable.
Certaines outils sont plus sournois qu’ils n’en ont l’air. Un utilitaire de copie « sûr » peut ouvrir la destination en écriture et la tronquer avant la copie. C’est exactement le type de mutation en place que ETXTBSY est conçu pour empêcher.
Mécanisme 2 : bind mounts et sémantiques de volumes en conteneur
Si vous bind-montez un répertoire hôte dans un conteneur, vous déployez souvent en écrivant dans ce répertoire depuis l’hôte (ou depuis un autre conteneur). Le processus applicatif à l’intérieur du conteneur a le fichier ouvert/exécuté, donc le remplacement côté hôte peut déclencher ETXTBSY. La frontière du conteneur ne change pas les sémantiques du noyau ; elle ne fait que changer qui est blâmé.
Mécanisme 3 : bibliothèques partagées, chargeurs et « ce n’est pas que le binaire »
Parfois, l’objet que vous écrasez n’est pas l’exécutable principal. C’est un plugin, une bibliothèque partagée, ou un outil auxiliaire invoqué pendant le déploiement.
Linux gère généralement bien la mise à jour des bibliothèques partagées via un remplacement atomique (nouveau fichier, renommage en place). Mais si votre outil de déploiement écrit directement dans des fichiers .so en place, vous pouvez déclencher le même comportement busy. De plus, un auto-sabotage courant est de remplacer /usr/bin/python (ou un runtime) pendant qu’un script du mainteneur l’utilise encore. Ce n’est pas théorique ; c’est comme ça que des scripts d’upgrade explosent en plein vol.
Mécanisme 4 : cas limites des systèmes de fichiers (NFS, overlayfs, stockage réseau « utile »)
Les systèmes de fichiers locaux (ext4, xfs) se comportent de manière prévisible : rename est atomique ; les sémantiques d’unlink sont stables ; l’application d’ETXTBSY correspond aux attentes Linux. Les systèmes de fichiers réseau et les couches overlay peuvent introduire des vérifications supplémentaires ou des sémantiques légèrement différentes. NFS en particulier a un historique de comportement « silly rename » et de complexité de cohérence de cache ; il peut transformer un remplacement propre en une condition busy étrange ou en visibilité retardée.
Cela importe parce qu’une grande partie des « déploiements Debian 13 » sont en réalité « Debian 13 au-dessus d’une nouvelle couche de stockage que nous avons introduite discrètement ». Quand vous voyez ETXTBSY, demandez toujours : est-ce que le stockage ou l’environnement d’exécution a changé ?
Mécanisme 5 : votre pipeline de déploiement exécute accidentellement le fichier pendant son remplacement
Voici un cas plus embarrassant : le script de déploiement vérifie la version en lançant myapp --version depuis le même chemin qu’il va écraser. Si le script exécute ce binaire pendant qu’une autre étape commence à l’écraser, félicitations : vous vous êtes mis en compétition vous-même.
Blague n°2 : La façon la plus rapide de reproduire ETXTBSY est de planifier votre déploiement exactement au moment où vous avez promis au service commercial que ce serait « un petit changement ».
Playbook de diagnostic rapide
Si vous êtes d’astreinte et que la pipeline est rouge, faites ceci dans l’ordre. L’objectif est d’identifier quel processus tient l’exécutable (ou la bibliothèque) ouvert, et si votre méthode de déploiement effectue des écritures en place.
1) Identifier le chemin exact qui a déclenché ETXTBSY
- Depuis les logs CI : capturez le chemin du fichier dans la ligne d’erreur.
- Depuis les logs système : identifiez la commande et la cible qui ont échoué.
2) Trouver qui utilise ce fichier (processus + PID)
lsofoufusersur le chemin.- Confirmez s’il s’agit d’un mapping exécutable (
txt) ou simplement d’un descripteur de fichier ouvert.
3) Déterminer le pattern de déploiement : remplacement atomique vs écrasement en place
- Si vous voyez
cpdirectement dans le chemin final, vous écrasez en place. - Si vous voyez
rsyncdans un répertoire actif, vous pouvez réécrire en place selon les options. - Si vous voyez « écrire en temp puis renommer » et que vous avez quand même ETXTBSY, suspectez des cas limites de FS/conteneur.
4) Décider du correctif immédiat le moins risqué
- Si le service peut redémarrer : arrêter/redémarrer le service, puis redéployer en utilisant un swap atomique.
- Si le redémarrage est risqué : déployer dans un nouveau répertoire de release, basculer via un symlink, puis redémarrer proprement (ou utiliser socket activation / handoff).
- Si c’est une mise à jour de paquet : planifier un redémarrage, ou utiliser le workflow
needrestart; n’écrasez pas brutement les fichiers.
5) Appliquer le correctif permanent
- Changer le déploiement pour utiliser des répertoires de release immuables + swap atomique de pointeur.
- Ou déplacer l’exécutable vers un chemin versionné et garder un symlink stable.
- Ou s’assurer que le fichier remplacé n’est jamais modifié en place (télécharger en temp puis renommer).
Tâches pratiques : commandes, sorties et la décision suivante
Voici des tâches éprouvées sur le terrain. Chacune inclut une commande, un exemple de sortie, ce que ça signifie et la décision que vous prenez ensuite. Exécutez-les en tant qu’utilisateur avec suffisamment de privilèges (root ou via sudo) sur l’hôte affecté.
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 +++
Signification : Votre outil de déploiement essaie d’ouvrir la destination avec O_TRUNC (écrasement en place). Le noyau le bloque à cause du mapping d’exécution.
Décision : Arrêtez l’écrasement en place. Passez à « écrire en temp puis renommer » ou au swap de répertoire de release.
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
Signification : Le PID 14231 exécute /opt/myapp/myapp (FD type txt).
Décision : Vous ne pouvez pas écraser cet inode. Soit redémarrez/arrêtez ce processus, soit déployez un nouvel inode et échangez les pointeurs.
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
Signification : ...e. indique un accès en exécution.
Décision : Si plusieurs PID apparaissent, vous êtes face à des workers multiples ou un superviseur (systemd, gunicorn, etc.). Planifiez le redémarrage en conséquence.
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
Signification : Le chemin stable est un symlink vers un répertoire de release. C’est une bonne nouvelle : vous pouvez échanger le symlink de façon atomique.
Décision : Déployez la nouvelle release dans un nouveau répertoire, puis basculez le symlink. Ne touchez pas au répertoire de release existant en place.
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
Signification : Le service s’exécute depuis /opt/myapp/current. Si votre déploiement modifie ce répertoire en place, vous risquez ETXTBSY (et des mises à jour partielles).
Décision : Faites de /opt/myapp/current un symlink vers un répertoire de release immuable, et ne changez que le 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)
Signification : Vous avez déjà remplacé/unlinked le fichier, mais le processus utilise toujours l’ancien inode (maintenant supprimé). C’est le comportement Unix normal.
Décision : Planifiez un redémarrage si vous avez besoin que la nouvelle version soit active. N’essayez pas de « réparer » en écrasant ; cela n’affectera pas le mapping mémoire du processus en cours.
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
Signification : Vous avez un inode spécifique. Si votre déploiement écrit en place, il cible cet inode et déclenche ETXTBSY.
Décision : Assurez-vous que le déploiement crée un nouvel inode (écrire sous un nom différent, puis renommer/swap du symlink).
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]
Signification : --inplace est l’élément incriminé. Il force la modification en place.
Décision : Retirez --inplace. Privilégiez --delay-updates et déployez vers un chemin de staging, ou utilisez des répertoires de release.
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)
Signification : Vous êtes dans un environnement overlayfs (courant dans les conteneurs). Certaines opérations se comportent différemment, et « remplacer en place » reste une mauvaise idée.
Décision : Ne mutiez pas d’exécutables à l’intérieur d’un système de fichiers de conteneur en cours d’exécution. Construisez une nouvelle image et redéployez, ou utilisez un pattern release-dir à l’intérieur d’un volume inscriptible avec swap atomique de pointeur.
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:42 configure nginx:amd64 1.26.2-1
2025-12-30 12:04:42 status installed nginx:amd64 1.26.2-1
Signification : Une mise à jour dpkg a eu lieu ; si vous avez vu ETXTBSY à ce moment-là, cela a pu se produire pendant des scripts du mainteneur ou une tentative de redémarrage postinst.
Décision : Si des paquets touchent des composants en cours d’utilisation, coordonnez les redémarrages et évitez d’exécuter des jobs de déploiement longue durée pendant des runs 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
Signification : Un processus utilise encore un ancien mapping de fichiers mis à jour. C’est la variante « vous l’avez remplacé, mais il tourne toujours ».
Décision : Planifiez un redémarrage contrôlé. Si l’absence de downtime est importante, effectuez des rolling restarts instance par instance derrière un équilibreur de charge.
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
Signification : Le pointeur current a bougé instantanément. Les processus existants continuent d’exécuter l’ancien inode ; les nouveaux démarrages utilisent la nouvelle cible.
Décision : C’est la primitive de déploiement que vous voulez. Combinez-la avec un redémarrage ou un reload gracieux.
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
Signification : L’exécutable en cours d’exécution diffère de ce qui est actuellement pointé sur le disque. Votre rollout n’a pas pris effet pour ce PID.
Décision : Redémarrez ou faites évoluer le pool de processus. Ne continuez pas à redéployer ; cela ne changera pas les mappings mémoire en cours.
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
Signification : L’heure de démarrage du processus correspond au moment du déploiement. Cela signifie souvent que votre script de déploiement ou le superviseur l’a redémarré en plein copy.
Décision : Rendre les étapes de déploiement idempotentes et sérialisées : stagez la release, changez le pointeur, puis redémarrez (ou reload) une seule fois, pas plusieurs fois.
Corrections sûres qui ne transforment pas les déploiements en roulette
Corriger ETXTBSY en production consiste moins à « tuer le processus qui tient le fichier » et plus à adopter une primitive de déploiement que le noyau apprécie. Vous voulez arrêter de traiter un exécutable en cours d’exécution comme un blob mutable.
Fix 1: Utiliser des répertoires de release immuables + swap atomique de pointeur (symlink)
C’est le pattern que je recommande le plus souvent parce qu’il est ennuyeux, et l’ennui est le plus grand compliment qu’on puisse faire à un système de déploiement.
Disposition :
/opt/myapp/releases/<release-id>/myapp(immuable)/opt/myapp/current -> /opt/myapp/releases/<release-id>(pointeur symlink)- Le service systemd exécute
/opt/myapp/current/myapp
Étapes de déploiement :
- Uploader dans un nouveau répertoire de release (pas encore utilisé).
- Vérifier la santé du binaire dans le répertoire de release directement.
- Basculer le symlink
currentde façon atomique. - Redémarrer ou recharger le service (idéalement de manière gracieuse).
Pourquoi ça marche : le processus en cours continue d’utiliser son ancien inode. La nouvelle release est un inode différent dans un répertoire différent. Pas d’écritures en place. Pas d’ETXTBSY. Le rollback est aussi un changement de symlink unique.
Fix 2: Écrire sur un fichier temporaire, fsync, puis renommer (remplacement atomique)
Si vous devez absolument garder le même chemin (par exemple un outil tiers l’attend), faites un remplacement sûr :
- Téléchargez/écrivez sur
myapp.newdans le même répertoire. chmodet vérifiez les sommes de contrôle.mv -f myapp.new myapp(rename est atomique sur le même système de fichiers).
Important : rename est atomique, mais ce n’est pas magique. Si vous remplacez un exécutable en cours d’exécution, rename est normalement autorisé et sûr, car vous échangez des entrées d’annuaire, sans muter l’inode. Le processus en cours utilise toujours l’ancien inode.
Mais ne combinez pas cela avec des outils qui font des mises à jour en place (cp vers le chemin final, rsync --inplace).
Fix 3: Cesser de déployer dans des répertoires partagés et vivants
Un anti-pattern courant est de déployer dans /usr/local/bin ou un dossier d’application partagé où plusieurs services récupèrent des helpers, plugins ou runtimes. Vous « mettez juste à jour le helper », et soudain le déploiement de l’API échoue avec ETXTBSY parce que le helper est exécuté pendant le déploiement.
Gardez les artefacts applicatifs privés par service, versionnés, et remplacez via swap de pointeur. Les répertoires partagés doivent être réservés aux paquets gérés par le système, mis à jour pendant des fenêtres de maintenance contrôlées.
Fix 4: Patterns systemd qui s’accordent bien avec les rollouts
systemd n’est pas la cause, mais il peut amplifier les races si votre déploiement déclenche des redémarrages à des moments délicats.
- Utilisez
ExecStartpointant vers le chemin symlink stable. - Utilisez
ExecReloadpour un reload gracieux si votre app le supporte. - Considérez
Restart=on-failure(pasalways) pour réduire le flapping lors d’erreurs de déploiement. - Si vous avez plusieurs workers, pensez à la socket activation ou à un proxy frontal pour découpler le redémarrage de l’impact client.
Fix 5: Monde des conteneurs : reconstruire les images, ne pas patcher les exécutables en place
Si vous exécutez des conteneurs et que vous « déployez » en copiant un nouveau binaire dans un conteneur vivant, vous réinventez à la main les pires aspects de la dérive de configuration.
Préféré :
- Construire une nouvelle image avec le nouveau binaire.
- Déployer la nouvelle image (rolling update).
- Utiliser des tags immuables ou des digests en production, pas « latest ».
Si vous devez utiliser un volume monté pour hot-swapping d’artefacts, utilisez le pattern release-dir + swap de symlink à l’intérieur du volume. Ne faites pas d’écrasement en place depuis l’hôte ou un sidecar.
Fix 6: Considérations stockage (parce que « busy » peut masquer une histoire de stockage)
En tant qu’ingénieur stockage, je dirai la partie la plus importante : ETXTBSY vous montre souvent que votre déploiement attend des sémantiques de système de fichiers local, mais vous lui avez fourni autre chose.
- Sur NFS, assurez-vous de placer votre répertoire d’artefacts de déploiement sur un disque local si possible.
- Si vous devez utiliser NFS : évitez les mises à jour en place, préférez des répertoires versionnés, et assurez une option de montage cohérente sur toute la flotte.
- Sur overlayfs : traitez le système de fichiers de conteneur comme immuable ; utilisez des volumes pour les données mutables.
Trois mini-récits du monde de l’entreprise (comment ça mord les équipes)
Mini-récit n°1 : L’incident causé par une mauvaise hypothèse
Ils exploitaient une petite flotte de serveurs Debian derrière un load balancer. Le processus de déploiement était « simple » : copier le nouveau binaire dans /opt/app/app, puis envoyer un signal pour un reload. Ça fonctionnait depuis longtemps, et c’est ainsi que de mauvaises hypothèses deviennent des « décisions de conception ».
Un trimestre, ils ont introduit un runner de jobs en arrière-plan sur les mêmes hôtes. Il utilisait le même binaire, invoqué avec un drapeau différent. Le runner était supervisé par systemd et redémarré agressivement en cas d’échec. Pendant le déploiement, la pipeline copiait le binaire pendant que le service web tournait. Parfois ça échouait, mais pas toujours. Ensuite c’est devenu pire : le runner a redémarré en plein déploiement et a tenté d’exécuter le binaire au même moment où le déploiement le tronquait.
Résultat : ETXTBSY dans les logs de déploiement, plus des plantages occasionnels quand un processus arrivait à exécuter un binaire partiellement mis à jour (parce que certaines étapes tournaient sous des utilisateurs différents et que tous les hôtes n’étaient pas cohérents). Ils ont blâmé Debian. Debian était innocent ; il faisait son travail.
La correction n’a pas été « réessayer jusqu’à ce que ça marche ». Ils sont passés aux répertoires de release immuables. Le service web et le runner exécutaient tous deux via /opt/app/current/app symlink. Le déploiement créait un nouveau répertoire de release, flipait le symlink et redémarrait les services dans un ordre contrôlé. L’hypothèse erronée était « écraser le fichier équivaut à remplacer le programme en cours d’exécution ». Ce n’est pas le cas.
Mini-récit n°2 : L’optimisation qui s’est retournée contre eux
Une équipe plateforme voulait des déploiements plus rapides et moins d’utilisation disque. Quelqu’un a remarqué que copier des répertoires de release entiers consommait espace et temps. Ils ont remplacé l’approche release-dir par un « rsync optimisé » dans un répertoire partagé unique, utilisant --inplace pour éviter les fichiers temporaires et réduire l’amplification d’écriture.
Ça benchmarkait très bien sur une VM de test tranquille. En production, les déploiements ont commencé à échouer avec ETXTBSY. Pire, ils ont eu des comportements proches de la corruption sous forte charge : certains hôtes avaient brièvement un mélange d’anciens et de nouveaux assets parce que rsync mettait à jour les fichiers dans un ordre qui ne correspondait pas aux attentes runtime de l’app.
L’équipe a répondu par des retries et des timeouts plus longs. Les temps de déploiement ont augmenté. Les échecs sont devenus plus rares mais plus mystérieux. Le load balancer était sain, mais les utilisateurs voyaient des erreurs intermittentes parce que des nœuds servaient des versions différentes pendant la fenêtre de sync partielle.
Ils ont fait un rollback de l’« optimisation » et sont revenus aux releases versionnées. L’utilisation disque a augmenté, prévisible. La fiabilité aussi, ce qui était le seul nombre qui comptait durant la revue d’incident. Leçon : si votre « optimisation » supprime l’atomicité, ce n’est pas une optimisation. C’est une dette avec une meilleure communication.
Mini-récit n°3 : La pratique ennuyeuse mais correcte qui a sauvé la situation
Une autre organisation tournait Debian 13 avec une gestion des changements stricte. Leur pipeline de déploiement plaçait toujours les artefacts en staging dans un nouveau répertoire nommé par un ID de release. Il exécutait ensuite un contrôle canari qui lançait le binaire depuis le chemin staged, pas depuis le symlink live. Après validation, il flipait le symlink.
Un jour, une mise à jour système a remonté une nouvelle bibliothèque runtime. Quelques services ont eu besoin d’un redémarrage pour prendre en compte les nouveaux mappings de bibliothèque. L’équipe ne l’a pas remarquée immédiatement, parce que tout tournait encore. Mais plus tard, pendant un déploiement, leurs vérifications comparaient la somme du processus en cours (via /proc/<pid>/exe) avec l’artefact staged. Ce n’était pas identique, ce qui était attendu. L’important : le déploiement a quand même réussi parce que rien n’a tenté d’écraser le binaire live en place.
Pendant la fenêtre de maintenance, ils ont redémarré les services en rolling. Pas d’ETXTBSY, pas d’upgrades cassés, pas de « pourquoi le gestionnaire de paquets a explosé ». La pratique qui les a sauvés était douloureusement peu séduisante : ne jamais muter les exécutables live ; toujours swapper des pointeurs ; pouvoir revenir en arrière en changeant une seule chose.
Ils n’ont pas « résolu » ETXTBSY parce qu’ils le déclenchaient rarement. C’est l’état rêvé : prévenir la classe d’erreur au lieu de devenir bon pour la combattre.
Erreurs courantes : symptôme → cause racine → correction
1) Symptom: cp fails with “Text file busy” when copying a binary
Cause racine : Vous écrasez l’inode d’un exécutable en cours d’exécution (copy ouvre la destination avec truncate/écriture).
Correction : Copier sous un nouveau nom puis renommer, ou déployer dans un nouveau répertoire de release et basculer un symlink.
2) Symptom: rsync error code 26 or 3 with “Text file busy”
Cause racine : rsync est configuré pour mettre à jour en place (--inplace) ou cible des chemins live contenant des exécutables.
Correction : Retirez --inplace. Utilisez --delay-updates pour stagez les mises à jour puis déplacer atomiquement les fichiers temporaires, ou mieux : des répertoires de release.
3) Symptom: deploy “succeeds,” but the running service is still the old version
Cause racine : Vous avez remplacé le fichier sur disque, mais le processus utilise toujours l’ancien inode (peut-être maintenant supprimé). Comportement classique d’Unix.
Correction : Redémarrez le service (rolling restart). Validez via la somme de contrôle /proc/<pid>/exe ou needrestart.
4) Symptom: error happens only in containers, not on bare metal
Cause racine : Vous patchiez un système de fichiers de conteneur vivant ou un volume bind-monté pendant que le binaire s’exécute dans le conteneur.
Correction : Construisez et déployez une nouvelle image. Si vous utilisez des volumes pour les artefacts, employez des releases immuables et des swaps de pointeur.
5) Symptom: ETXTBSY appears during package upgrades or unattended-upgrades
Cause racine : Un service tourne pendant que les paquets essaient de mettre à jour des exécutables ou helpers associés ; des scripts du mainteneur peuvent exécuter des outils en plein upgrade.
Correction : Coordonnez les upgrades avec des redémarrages de services, évitez de chevaucher les déploiements d’apps avec les runs apt, et utilisez needrestart pour gérer les redémarrages.
6) Symptom: Only some hosts fail, usually those under load
Cause racine : La fenêtre de course dépend de la charge : un IO plus lent fait que votre fenêtre d’écrasement chevauche davantage les événements d’exec/restart. Ou les scripts de déploiement se comportent différemment selon le timing.
Correction : Éliminez la race : pas d’écritures en place ; uniquement des changements atomiques de pointeurs ; sérialisez les actions de déploiement ; réduisez les « restart storms ».
7) Symptom: “mv: Text file busy” even though rename should be atomic
Cause racine : Indique souvent que vous ne faites pas un renommage sur le même système de fichiers (move cross-device), ou que vous êtes sur un système de fichiers aux sémantiques spéciales (overlayfs/NFS).
Correction : Assurez-vous que le fichier temporaire est créé dans le même répertoire (même montage). Vérifiez stat -f ou mount et ajustez l’emplacement de déploiement.
Listes de vérification / plan pas à pas
Confinement immédiat (pendant un incident)
- Geler les tentatives de déploiement supplémentaires vers les mêmes hôtes (arrêter le flapping).
- Identifier le chemin qui a déclenché ETXTBSY et la commande qui l’a fait.
- Utiliser
lsofoufuserpour identifier les PIDs utilisant le fichier. - Décider si vous pouvez redémarrer en toute sécurité :
- Si oui : effectuer un redémarrage contrôlé (un hôte à la fois si nécessaire).
- Si non : déployer un nouveau répertoire de release et planifier une coupure en douceur.
- Vérifier la version en cours via la somme
/proc/<pid>/exeou la sortie de version.
Remédiation permanente (faire cesser l’apparition)
- Adopter l’une de ces politiques :
- répertoires de release + swap de symlink (recommandé)
- fichier temp + fsync + rename atomique (acceptable)
- déploiements basés sur images pour les conteneurs (meilleur pour workloads containerisés)
- Auditer les outils de déploiement pour les écritures en place :
- retirer
--inplacede rsync - éviter
cpdirectement vers le chemin final de l’exécutable - éviter les patterns « télécharger directement dans le fichier final »
- retirer
- Rendre les redémarrages explicites :
- redémarrage systemd dans le pipeline après swap de pointeur
- rolling restarts derrière un LB
- reload gracieux quand possible
- Ajouter un garde-fou de déploiement :
- refuser le déploiement si
lsofmontre que vous êtes sur le point d’écraser un exécutable mappé - refuser si le chemin cible est sur NFS/overlay sauf si vous utilisez des répertoires de release
- refuser le déploiement si
- Operationaliser le rollback :
- conserver N releases précédentes
- revenir en arrière via symlink + redémarrage
Checklist de vérification (prouver que la correction fonctionne)
- Déployer une nouvelle release pendant que le service tourne. Confirmer qu’il n’y a pas d’ETXTBSY.
- Confirmer que le symlink a été basculé et pointe vers le nouveau répertoire.
- Redémarrer le service et confirmer que la somme du fichier en cours d’exécution correspond à l’artefact prévu.
- Faire un rollback en basculant le symlink vers la release précédente ; redémarrer ; confirmer le rollback via checksum.
- Lancer deux déploiements consécutifs et s’assurer qu’aucun état partiel n’existe entre eux.
Faits intéressants et contexte historique
- Le nom d’erreur ETXTBSY est antérieur à Linux : il vient des débuts d’Unix, où « text segment » était le terme formel pour le code exécutable.
- Unix vous permet de supprimer des exécutables en cours d’exécution : un processus en cours garde une référence ouverte, donc l’entrée d’annuaire peut disparaître pendant l’exécution.
- Rename est atomique (localement) : sur les systèmes de fichiers POSIX locaux, échanger des entrées d’annuaire avec
rename()est atomique sur le même filesystem, d’où l’efficacité des swaps de pointeur. - Les mises à jour en place sont historiquement tentantes : les administrateurs le faisaient pour économiser de l’espace et éviter les « binaires dupliqués », surtout quand les disques étaient petits et chers.
- Les systèmes de fichiers réseau compliquent les invariants : la cohérence de cache NFS et le comportement côté client ont historiquement rendu la promesse « atomique et immédiate » plus conditionnelle qu’on ne le suppose.
- Les conteneurs n’ont pas changé le noyau : les namespaces isolent les processus, mais les sémantiques d’exécution de fichiers suivent toujours les règles du noyau ; ETXTBSY n’est pas « un bug de conteneur ».
- Les gestionnaires de paquets ont appris à la dure : dpkg et consorts s’appuient fortement sur des remplacements basés sur rename et un staging soigneux parce qu’écraser des binaires système vivants est une excellente façon de casser les upgrades.
- Le code en cours d’exécution ne se met pas à jour tout seul : remplacer le fichier sur disque ne patch pas les pages déjà mappées en mémoire ; vous avez toujours besoin de redémarrage/reload.
FAQ
1) Est-ce que « Text file busy » est un bug de Debian 13 ?
Non. C’est un comportement du noyau mis en lumière par votre méthode de déploiement. Debian 13 n’est que le lieu où votre timing, votre charge de travail ou votre couche de stockage l’ont rendu visible.
2) Pourquoi rm marche parfois mais cp échoue ?
rm unlinke l’entrée d’annuaire ; le processus en cours garde l’inode ouvert. cp écrase/tronque le même inode, ce que le noyau bloque lorsqu’il est exécuté.
3) Puis-je corriger ça en ajoutant des retries au déploiement ?
Vous pouvez réduire le bruit des pagers, certes. Vous ne corrigerez pas la race sous-jacente, et vous finirez par tomber sur un hôte où le timing ne s’aligne jamais. Remplacez la primitive de déploiement à la place.
4) Si rename est sûr, pourquoi j’ai vu mv: Text file busy ?
Généralement parce que ce n’était pas un vrai renommage dans le même filesystem (déplacement inter-périphérique), ou vous êtes sur une couche FS aux sémantiques spéciales (overlayfs, NFS). Assurez-vous que le fichier temp est créé dans le même répertoire et montage.
5) Cela affecte-t-il les scripts aussi, ou seulement les binaires ?
Principalement les binaires, mais les scripts peuvent déclencher des problèmes similaires quand ils sont exécutés via un interpréteur qui les ouvre d’une manière qui trippe les vérifications busy, ou quand votre déploiement exécute le script pendant qu’il le réécrit. Ne mutiez aucun point d’entrée « live » en place.
6) Comment prouver quel processus bloque le déploiement ?
Utilisez lsof <path> et cherchez les mappings txt ou descripteurs ouverts. fuser -v est aussi pratique pour obtenir rapidement une liste de PIDs.
7) Arrêter le service résout-il toujours le problème ?
En général, cela éliminera ETXTBSY pour ce fichier, oui. Mais arrêter les services comme mécanisme de déploiement crée du downtime. Préférez des swaps atomiques + redémarrages contrôlés pour la prévisibilité.
8) Quel est le pattern de déploiement le plus sûr « sans surprises » sur Debian ?
Répertoires de release immuables + swap atomique de symlink + redémarrage piloté par systemd (rolling across nodes). Cela évite ETXTBSY et prévient les déploiements partiels.
9) Dois-je redémarrer après avoir mis à jour un binaire si le chemin reste le même ?
Oui, si vous voulez que le processus en cours utilise le nouveau code. Un processus en cours ne remappe pas automatiquement ses pages exécutables juste parce que le fichier sur disque a changé.
10) Et si je ne peux pas redémarrer (contraintes temps réel ou sessions longues) ?
Alors la correction est architecturale : lancez plusieurs instances, drainez les connexions, ou utilisez un superviseur/proxy qui supporte le handoff gracieux. ETXTBSY est un symptôme ; « pas de redémarrage possible » est la contrainte réelle.
Conclusion : prochaines étapes réalisables aujourd’hui
ETXTBSY n’est pas Linux qui fait le difficile. C’est Linux qui fait respecter une limite que votre processus de déploiement ne devrait pas franchir. Écraser un exécutable live en place, c’est comme changer un pneu pendant que la voiture roule sur l’autoroute : techniquement possible à tenter, mais vous n’aimerez pas le résultat.
Étapes pratiques :
- Trouvez le chemin exact qui déclenche « Text file busy » et identifiez le PID qui le tient avec
lsof. - Auditez votre pipeline pour les écritures en place (
cpvers le chemin final,rsync --inplace, téléchargements directs dans des emplacements live). - Choisissez une primitive de déploiement sûre et standardisez-la :
- répertoires de release + swap de symlink (meilleur polyvalent)
- fichier temp + fsync + rename (si vous devez garder le chemin)
- nouvelles images de conteneur (si vous êtes containerisé)
- Rendez les redémarrages délibérés et en rolling, pas accidentels et en compétition avec votre copie.
- Ajoutez un garde-fou : refuser de déployer si l’exécutable cible est mappé (
lsofmontretxt) et que vous êtes sur le point de l’écraser.
Faites ces cinq choses, et « Text file busy » redeviendra une erreur que vous lirez dans les postmortems d’autres équipes. C’est là qu’elle doit rester.