Les Pi tournent sans swap, et sur un noyau sans swap un dépassement mémoire ne produit pas forcément d'OOM-kill. Le noyau part en thrash — il recycle sans fin le page cache pour récupérer quelques pages — et la machine reste vivante mais inutilisable, sans aucune auto-récupération.
C'est le mode de panne réel, mesuré sur pi1 le 2026-08-15 pendant qu'un nuxt generate occupait ~3 Go :
load average: 293 mem: 7876/8052 MiB utilisés, 175 MiB dispo, swap 0
sshd: kex_exchange_identification: Connection reset by peer ← plus de fork possible
Conséquence : apiserver k3s et Traefik affamés → tout *.arcodange.lab injoignable, Gitea compris, alors qu'il tourne sain sur pi2. Il a fallu ~50 min pour que la machine remonte. Même signature que le 2026-07-23.
Le cap docker --memory=3g de la #39fonctionnait (job mesuré à 2,82 Go) : il est simplement trop haut face aux ~4 Go de baseline de pi1. Un cap ne protège pas d'un débordement, il en déplace juste le seuil.
La solution : earlyoom, pas de swap
earlyoom surveille MemAvailable depuis l'espace utilisateur et déclenche avant que le noyau ne se bloque : SIGTERM sous 10 %, SIGKILL sous 5 %.
Choix volontaire du kill direct plutôt que zram/swap :
aucun swap à ajouter, donc aucun conflit avec le --fail-swap-on de k3s ;
un build qui déborde meurt en quelques secondes au lieu de coucher le lab pendant 3 h.
Avec SwapTotal = 0, la condition swap d'earlyoom est satisfaite en permanence — vérifié dans le log de pi1 (swap free: 0 of 0 MiB (0.00%)), donc c'est bien le seuil mémoire seul qui pilote.
Seuils sur 8 Gio → SIGTERM sous ~805 Mio, SIGKILL sous ~402 Mio. Pendant l'incident, pi1 est descendu à 175 Mio et a stagné vers 332 Mio : les deux seuils auraient été franchis largement à temps.
Deux détails qui font la différence
--prefer vise MainThread, pas node. earlyoom compare au nom du processus (/proc/PID/comm). Le nuxt generate qui a couché pi1 se présentait sous le nom MainThread (Node.js renomme son thread principal), pas node — vérifié dans ps -eo comm. Sans ce motif, --prefer serait resté sans effet sur le coupable réel.
--avoid tient compte de la troncature à 15 caractères : d'où longhorn-manage et non longhorn-manager. La liste épargne k3s, containerd, dockerd, sshd, systemd, pihole-FTL, longhorn, iscsid — plus postgres et gitea, qui portent les données du lab sur pi2 alors que les jobs CI qui débordent vivent sur les runners pi1/pi3. À noter : --avoid est une préférence, pas une immunité.
Vérification — appliqué et mesuré, pas seulement écrit
Déroulé du moins critique au plus critique (pi3 → pi2 → pi1), les deux assertions du playbook passant sur chaque hôte :
Hôte
Service
Seuils relus sur le processus vivant
pi3
actif
-m 10,5✅
pi2
actif
-m 10,5✅
pi1
actif
-m 10,5✅
Idempotence confirmée : second passage sur pi3 → changed=0.
Le playbook ne se contente pas du fichier de conf : il relit pgrep -a earlyoom pour vérifier que les seuils sont réellement appliqués au processus (un flush_handlers garantit que le redémarrage a eu lieu avant le contrôle).
system.yml : syntax-check OK. Les warnings restants (namespace réservé dans k3s_ssl.yml) préexistent sur main, vérifié par comparaison.
Portée
Ça borne les dégâts d'un débordement mémoire. Ça ne règle pas le SPOF pi1 — le wildcard DNS .arcodange.lab pointe entièrement sur pi1, donc Gitea et le registry restent solidaires de sa santé. Sujet distinct, non traité ici.
Complémentaire de cms#17 (timeout-minutes: 30), qui attaque la même panne par l'autre bout : borner la durée du job plutôt que sa consommation.
## Le problème
Les Pi tournent **sans swap**, et sur un noyau sans swap un dépassement mémoire ne produit **pas forcément d'OOM-kill**. Le noyau part en thrash — il recycle sans fin le page cache pour récupérer quelques pages — et la machine reste *vivante mais inutilisable*, sans aucune auto-récupération.
C'est le mode de panne réel, mesuré sur pi1 le 2026-08-15 pendant qu'un `nuxt generate` occupait ~3 Go :
```
load average: 293 mem: 7876/8052 MiB utilisés, 175 MiB dispo, swap 0
sshd: kex_exchange_identification: Connection reset by peer ← plus de fork possible
```
Conséquence : apiserver k3s et Traefik affamés → tout `*.arcodange.lab` injoignable, **Gitea compris**, alors qu'il tourne sain sur pi2. Il a fallu ~50 min pour que la machine remonte. Même signature que le 2026-07-23.
Le cap docker `--memory=3g` de la #39 **fonctionnait** (job mesuré à 2,82 Go) : il est simplement trop haut face aux ~4 Go de baseline de pi1. Un cap ne protège pas d'un débordement, il en déplace juste le seuil.
## La solution : earlyoom, pas de swap
`earlyoom` surveille `MemAvailable` depuis l'espace utilisateur et déclenche **avant** que le noyau ne se bloque : SIGTERM sous 10 %, SIGKILL sous 5 %.
Choix volontaire du **kill direct plutôt que zram/swap** :
- aucun swap à ajouter, donc **aucun conflit avec le `--fail-swap-on` de k3s** ;
- un build qui déborde meurt en quelques secondes au lieu de coucher le lab pendant 3 h.
Avec `SwapTotal = 0`, la condition swap d'earlyoom est satisfaite en permanence — vérifié dans le log de pi1 (`swap free: 0 of 0 MiB (0.00%)`), donc **c'est bien le seuil mémoire seul qui pilote**.
Seuils sur 8 Gio → SIGTERM sous ~805 Mio, SIGKILL sous ~402 Mio. Pendant l'incident, pi1 est descendu à 175 Mio et a stagné vers 332 Mio : les deux seuils auraient été franchis largement à temps.
## Deux détails qui font la différence
**`--prefer` vise `MainThread`, pas `node`.** earlyoom compare au nom du processus (`/proc/PID/comm`). Le `nuxt generate` qui a couché pi1 se présentait sous le nom **`MainThread`** (Node.js renomme son thread principal), pas `node` — vérifié dans `ps -eo comm`. Sans ce motif, `--prefer` serait resté sans effet sur le coupable réel.
**`--avoid` tient compte de la troncature à 15 caractères** : d'où `longhorn-manage` et non `longhorn-manager`. La liste épargne k3s, containerd, dockerd, sshd, systemd, pihole-FTL, longhorn, iscsid — plus **postgres et gitea**, qui portent les données du lab sur pi2 alors que les jobs CI qui débordent vivent sur les runners pi1/pi3. À noter : `--avoid` est une préférence, pas une immunité.
## Vérification — appliqué et mesuré, pas seulement écrit
Déroulé du moins critique au plus critique (pi3 → pi2 → pi1), les deux assertions du playbook passant sur chaque hôte :
| Hôte | Service | Seuils relus sur le processus vivant |
|---|---|---|
| pi3 | actif | `-m 10,5` ✅ |
| pi2 | actif | `-m 10,5` ✅ |
| pi1 | actif | `-m 10,5` ✅ |
- **Idempotence** confirmée : second passage sur pi3 → `changed=0`.
- Le playbook ne se contente pas du fichier de conf : il relit `pgrep -a earlyoom` pour vérifier que les seuils sont *réellement* appliqués au processus (un `flush_handlers` garantit que le redémarrage a eu lieu avant le contrôle).
- `system.yml` : syntax-check OK. Les warnings restants (`namespace` réservé dans `k3s_ssl.yml`) **préexistent sur `main`**, vérifié par comparaison.
## Portée
Ça borne les dégâts d'un débordement mémoire. Ça ne règle pas le **SPOF pi1** — le wildcard DNS `.arcodange.lab` pointe entièrement sur pi1, donc Gitea et le registry restent solidaires de sa santé. Sujet distinct, non traité ici.
Complémentaire de **cms#17** (`timeout-minutes: 30`), qui attaque la même panne par l'autre bout : borner la durée du job plutôt que sa consommation.
Les Pi tournent sans swap, et sans swap un dépassement mémoire ne produit pas
forcément d'OOM-kill : le noyau part en thrash et la machine reste vivante mais
inutilisable des heures. Mesuré sur pi1 le 2026-08-15 (load 293, 175 Mio dispo,
sshd incapable de forker, apiserver k3s + traefik affamés, tout
*.arcodange.lab HS pendant ~50 min).
earlyoom surveille MemAvailable en espace utilisateur et déclenche AVANT le
blocage du noyau : SIGTERM sous 10 %, SIGKILL sous 5 %. Choix volontaire du
kill direct plutôt que zram/swap — aucun swap à ajouter, donc aucun conflit
avec le --fail-swap-on de k3s.
Appliqué et vérifié sur pi1, pi2 et pi3 ; rejeu idempotent (changed=0).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Le problème
Les Pi tournent sans swap, et sur un noyau sans swap un dépassement mémoire ne produit pas forcément d'OOM-kill. Le noyau part en thrash — il recycle sans fin le page cache pour récupérer quelques pages — et la machine reste vivante mais inutilisable, sans aucune auto-récupération.
C'est le mode de panne réel, mesuré sur pi1 le 2026-08-15 pendant qu'un
nuxt generateoccupait ~3 Go :Conséquence : apiserver k3s et Traefik affamés → tout
*.arcodange.labinjoignable, Gitea compris, alors qu'il tourne sain sur pi2. Il a fallu ~50 min pour que la machine remonte. Même signature que le 2026-07-23.Le cap docker
--memory=3gde la #39 fonctionnait (job mesuré à 2,82 Go) : il est simplement trop haut face aux ~4 Go de baseline de pi1. Un cap ne protège pas d'un débordement, il en déplace juste le seuil.La solution : earlyoom, pas de swap
earlyoomsurveilleMemAvailabledepuis l'espace utilisateur et déclenche avant que le noyau ne se bloque : SIGTERM sous 10 %, SIGKILL sous 5 %.Choix volontaire du kill direct plutôt que zram/swap :
--fail-swap-onde k3s ;Avec
SwapTotal = 0, la condition swap d'earlyoom est satisfaite en permanence — vérifié dans le log de pi1 (swap free: 0 of 0 MiB (0.00%)), donc c'est bien le seuil mémoire seul qui pilote.Seuils sur 8 Gio → SIGTERM sous ~805 Mio, SIGKILL sous ~402 Mio. Pendant l'incident, pi1 est descendu à 175 Mio et a stagné vers 332 Mio : les deux seuils auraient été franchis largement à temps.
Deux détails qui font la différence
--preferviseMainThread, pasnode. earlyoom compare au nom du processus (/proc/PID/comm). Lenuxt generatequi a couché pi1 se présentait sous le nomMainThread(Node.js renomme son thread principal), pasnode— vérifié dansps -eo comm. Sans ce motif,--preferserait resté sans effet sur le coupable réel.--avoidtient compte de la troncature à 15 caractères : d'oùlonghorn-manageet nonlonghorn-manager. La liste épargne k3s, containerd, dockerd, sshd, systemd, pihole-FTL, longhorn, iscsid — plus postgres et gitea, qui portent les données du lab sur pi2 alors que les jobs CI qui débordent vivent sur les runners pi1/pi3. À noter :--avoidest une préférence, pas une immunité.Vérification — appliqué et mesuré, pas seulement écrit
Déroulé du moins critique au plus critique (pi3 → pi2 → pi1), les deux assertions du playbook passant sur chaque hôte :
-m 10,5✅-m 10,5✅-m 10,5✅changed=0.pgrep -a earlyoompour vérifier que les seuils sont réellement appliqués au processus (unflush_handlersgarantit que le redémarrage a eu lieu avant le contrôle).system.yml: syntax-check OK. Les warnings restants (namespaceréservé dansk3s_ssl.yml) préexistent surmain, vérifié par comparaison.Portée
Ça borne les dégâts d'un débordement mémoire. Ça ne règle pas le SPOF pi1 — le wildcard DNS
.arcodange.labpointe entièrement sur pi1, donc Gitea et le registry restent solidaires de sa santé. Sujet distinct, non traité ici.Complémentaire de cms#17 (
timeout-minutes: 30), qui attaque la même panne par l'autre bout : borner la durée du job plutôt que sa consommation.