feat(system) — earlyoom sur les 3 Pi : tuer net au lieu de thrasher pendant 3 h #57

Merged
arcodange merged 2 commits from arcodange/earlyoom-filet-memoire into main 2026-08-16 03:12:23 +02:00
Owner

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.

## 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.
arcodange added 2 commits 2026-08-15 22:32:24 +02:00
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).
Placé juste après le setup général et avant docker/k3s : le filet mémoire doit
exister avant les consommateurs qu'il est censé borner.
arcodange merged commit 2e8b7c63d8 into main 2026-08-16 03:12:23 +02:00
arcodange deleted branch arcodange/earlyoom-filet-memoire 2026-08-16 03:12:23 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: arcodange-org/factory#57