notes/oom-killer-invisible
Un broken pipe qui était un OOM, et Docker qui jure le contraire
Trois jours sans sauvegarde, un tueur de mémoire qui frappe dans un tmpfs, et deux champs de docker inspect qui affirment que non.
- docker
- linux
- mémoire
Du 7 au 9 août 2026, plus une seule sauvegarde n'aboutit, sur aucun système. Côté agent, toujours la même ligne : write content ... write: broken pipe. Onze occurrences, zéro cycle réussi. Tu regardes le conteneur du service d'ingestion en face, docker inspect te rend ExitCode=0 et OOMKilled=false. Tu rayes l'OOM de ta liste et tu pars chercher ailleurs. Trois heures plus tard, tu es toujours sur deux fausses pistes.
Ces deux champs mentent. La preuve d'un OOM de cgroup n'est pas dans Docker, elle est dans dmesg -T, et là elle est sans ambiguïté : shmem_write_begin, shmem_file_write_iter, vfs_write, try_charge_memcg, mem_cgroup_out_of_memory. Le même docker inspect disait quand même quelque chose d'utile, RestartCount=12, mais il fallait le lire.
La cause tient à une option absente. Le conteneur monte /tmp en tmpfs sans size. Un tmpfs sans taille annonce la moitié de la RAM de l'hôte, 7,9 Go ici, alors que le cgroup du conteneur tue à 3 Go. Le service écrit son stage d'ingestion dans ce /tmp, chaque octet écrit est facturé au cgroup, et le processus meurt en plein transfert. L'agent d'en face voit une connexion qui se coupe sans trame d'erreur : un broken pipe, rien de plus.
Un profil mémoire applicatif n'aurait rien montré non plus. Le processus tué portait 33 Mo de RSS anonyme. La mémoire vivait dans le tmpfs, hors du tas, invisible au profileur comme à toute garde mémoire écrite dans le code.
Borner le tmpfs sous la limite du cgroup remet les sauvegardes debout, et c'est une rustine. Le vrai défaut est que le mode fichiers matérialise tout le cycle sur disque avant de committer : tant qu'il n'écrit pas en flux, tu déplaces juste le mur.
Ce qu'il faut contrôler, dans cet ordre, dès qu'un broken pipe en écriture apparaît : docker ps -a sur la machine distante, en cherchant un conteneur dont l'ancienneté détonne par rapport aux autres ; docker inspect pour le seul RestartCount ; puis dmesg -T | grep -i oom. Et une règle qui vaut au-delà de ce cas : le journal du client ne désigne jamais la cause, il décrit juste le moment où l'autre bout a cessé de répondre.