feat(system) — earlyoom sur les 3 Pi : tuer net au lieu de thrasher pendant 3 h (#57)
This commit was merged in pull request #57.
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
---
|
||||
# Filet mémoire des Raspberry Pi : tuer net plutôt que thrasher.
|
||||
#
|
||||
# Motivation (incidents 2026-07-23 et 2026-08-15, tous deux causés par le build
|
||||
# CMS) : les Pi tournent SANS swap, et sur un noyau sans swap un dépassement
|
||||
# mémoire ne produit PAS forcément un OOM-kill. Le noyau part en thrash — il
|
||||
# recycle sans fin le page cache pour retrouver quelques pages — et la machine
|
||||
# reste vivante mais inutilisable pendant des heures. Mesuré le 2026-08-15 sur
|
||||
# pi1 : load 293, 175 Mio disponibles sur 8 Gio, sshd incapable de forker
|
||||
# (« kex_exchange_identification: Connection reset »), apiserver k3s et traefik
|
||||
# affamés, donc tout *.arcodange.lab injoignable — Gitea compris, alors qu'il
|
||||
# tourne sain sur pi2. Aucune auto-récupération : il a fallu ~50 min.
|
||||
#
|
||||
# earlyoom surveille MemAvailable en espace utilisateur et déclenche AVANT que
|
||||
# le noyau ne se bloque : SIGTERM au premier seuil, SIGKILL au second. C'est
|
||||
# volontairement le choix « kill direct » plutôt que zram/swap :
|
||||
# - pas de 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.
|
||||
#
|
||||
# Avec SwapTotal = 0, la condition swap d'earlyoom est toujours satisfaite :
|
||||
# seul le seuil mémoire ci-dessous pilote réellement le déclenchement.
|
||||
|
||||
- name: Filet mémoire earlyoom (tuer net, sans swap)
|
||||
hosts: raspberries:&local
|
||||
become: yes
|
||||
gather_facts: yes
|
||||
|
||||
vars:
|
||||
# Seuils en % de MemAvailable : SIGTERM,SIGKILL.
|
||||
# Sur les Pi de 8 Gio → SIGTERM sous ~805 Mio, SIGKILL sous ~402 Mio.
|
||||
# Repère : pendant l'incident du 2026-08-15, pi1 est descendu à 175 Mio et
|
||||
# a stagné autour de 332 Mio — les deux seuils auraient été franchis.
|
||||
earlyoom_mem_thresholds: "10,5"
|
||||
|
||||
# Intervalle du rapport mémoire dans syslog (secondes) : utile en post-mortem.
|
||||
earlyoom_report_interval: 3600
|
||||
|
||||
# Processus à épargner. earlyoom compare au NOM du processus (/proc/PID/comm),
|
||||
# tronqué à 15 caractères — d'où « longhorn-manage » et non
|
||||
# « longhorn-manager ». Tuer l'un de ceux-là aggraverait la panne ou nous
|
||||
# couperait l'accès distant. On y ajoute postgres et gitea : sur pi2 ils
|
||||
# tournent en docker compose hors k3s et portent les données du lab, alors
|
||||
# que les jobs CI qui débordent vivent sur les runners de pi1 et pi3.
|
||||
# Attention, --avoid est une préférence (déprioriser), pas une immunité :
|
||||
# si rien d'autre ne peut être tué, earlyoom les prendra quand même.
|
||||
earlyoom_avoid_regex: '^(k3s-server|k3s|k3s-agent|containerd|dockerd|sshd|systemd|pihole-FTL|longhorn-manage|iscsid|postgres|gitea|earlyoom)$'
|
||||
|
||||
# Processus à sacrifier en priorité : les outils de build.
|
||||
# ATTENTION au premier motif : le `nuxt generate` qui a couché pi1 se
|
||||
# présentait sous le nom « MainThread » et non « node » (Node.js renomme son
|
||||
# thread principal). Sans lui, --prefer serait resté sans effet sur le
|
||||
# coupable réel. Vérifié dans `ps -eo comm` le 2026-08-15.
|
||||
earlyoom_prefer_regex: '^(MainThread|node|npm|npx|esbuild|vite|rollup|tsc|webpack|cc1|cc1plus|ld|go|rustc|cargo)$'
|
||||
|
||||
tasks:
|
||||
|
||||
- name: Installer earlyoom
|
||||
ansible.builtin.apt:
|
||||
name: earlyoom
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: Configurer les seuils et les préférences de victime
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/default/earlyoom
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
content: |
|
||||
# Generated by Ansible – playbooks/system/earlyoom.yml
|
||||
# Ne pas éditer à la main : toute modification sera écrasée.
|
||||
EARLYOOM_ARGS="-m {{ earlyoom_mem_thresholds }} -r {{ earlyoom_report_interval }} --avoid '{{ earlyoom_avoid_regex }}' --prefer '{{ earlyoom_prefer_regex }}'"
|
||||
notify: Restart earlyoom
|
||||
|
||||
- name: Activer et démarrer earlyoom
|
||||
ansible.builtin.systemd:
|
||||
name: earlyoom
|
||||
state: started
|
||||
enabled: yes
|
||||
daemon_reload: yes
|
||||
|
||||
# Appliquer la conf avant de vérifier : sans ça, le premier passage
|
||||
# validerait le service démarré avec les arguments par défaut du paquet.
|
||||
- name: Appliquer la configuration en attente
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
- name: Relire l'état du service
|
||||
ansible.builtin.systemd:
|
||||
name: earlyoom
|
||||
register: earlyoom_service
|
||||
|
||||
- name: Vérifier qu'earlyoom tourne
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- earlyoom_service.status.ActiveState == "active"
|
||||
fail_msg: >-
|
||||
earlyoom n'est pas actif sur {{ inventory_hostname }}
|
||||
(ActiveState={{ earlyoom_service.status.ActiveState }}).
|
||||
success_msg: "earlyoom actif sur {{ inventory_hostname }}"
|
||||
|
||||
# Le filet ne vaut que si les seuils sont réellement ceux qu'on croit : on
|
||||
# relit la ligne de commande du processus vivant, pas le fichier de conf.
|
||||
- name: Relire les arguments du processus earlyoom
|
||||
ansible.builtin.command: pgrep -a earlyoom
|
||||
register: earlyoom_cmdline
|
||||
changed_when: false
|
||||
|
||||
- name: Vérifier que les seuils configurés sont bien appliqués
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'-m ' ~ earlyoom_mem_thresholds in earlyoom_cmdline.stdout"
|
||||
fail_msg: >-
|
||||
earlyoom tourne sans les seuils attendus sur {{ inventory_hostname }} :
|
||||
{{ earlyoom_cmdline.stdout }}
|
||||
success_msg: "seuils -m {{ earlyoom_mem_thresholds }} appliqués sur {{ inventory_hostname }}"
|
||||
|
||||
handlers:
|
||||
|
||||
- name: Restart earlyoom
|
||||
ansible.builtin.systemd:
|
||||
name: earlyoom
|
||||
state: restarted
|
||||
@@ -3,6 +3,11 @@
|
||||
- name: Setup général des rpis
|
||||
ansible.builtin.import_playbook: rpi.yml
|
||||
|
||||
# Avant docker et k3s : le filet mémoire doit être en place avant les
|
||||
# consommateurs qu'il est censé borner.
|
||||
- name: Filet mémoire earlyoom
|
||||
ansible.builtin.import_playbook: earlyoom.yml
|
||||
|
||||
- name: dns
|
||||
ansible.builtin.import_playbook: ../dns/dns.yml
|
||||
|
||||
|
||||
Reference in New Issue
Block a user