Contexte : la stack de monitoring (VictoriaMetrics, vmalert, Alertmanager, Telegram) est décrite dans Observabilité v2, et la dernière fois que l’alerting a fait parler de lui, c’était pour en dire beaucoup trop. Ici, c’est l’inverse : ce qu’il ne disait pas du tout.
Le point de départ : des alertes muettes ne prouvent rien
Le 10 octobre 2026, le canal d’alertes est calme. Tout va bien, donc.
Sauf qu’un canal d’alertes calme ne dit qu’une chose : aucune des règles écrites n’est vraie. Il ne dit rien de ce qu’aucune règle ne regarde. J’ai donc fait l’exercice que je repoussais depuis des mois : ouvrir chaque tableau Grafana et lire chaque panneau, en me posant à chaque fois la même question : « ce chiffre est-il normal, et qui me préviendrait s’il ne l’était plus ? ».
La nuit a produit cinq constats. Aucun n’était une panne. Tous étaient des pannes en préparation.
Angle mort n° 1 — La base de métriques ne se surveillait pas elle-même
Premier panneau qui accroche : la mémoire du pod VictoriaMetrics. Entre 80 et 92 % de sa limite de 512 Mi, en continu. Rien d’anormal en soi, ses caches sont dimensionnés sur la limite. Ce qui l’est davantage, c’est le nœud : un master, à côté d’etcd, avec environ 24 Go d’écritures Longhorn par jour. Si ce pod avait gonflé, c’est le quorum du cluster qui risquait d’en pâtir.
En cherchant à poser une alerte dessus, deuxième surprise : il n’existait aucune série vm_*. VictoriaMetrics collectait tout le cluster, sauf lui-même. Une base qui sature, rejette des écritures ou cesse d’ingérer restait invisible. C’est la panne la plus vicieuse : les tableaux se figent sur leur dernière valeur, et l’absence d’alerte vient de l’absence de données.
Dans la foulée, trois autres trous du même genre :
- aucune série
kubelet_volume_stats_*: le remplissage des PVC n’était mesuré nulle part ; - node-exporter tournait avec une limite CPU de 200m et était bridé sur 37 % des périodes CFS, pour une consommation moyenne d’environ 0,004 cœur. Les rafales arrivent au moment du scrape (0,5 s par scrape sur les Pi 4) : la limite rognait donc précisément les mesures ;
- le tableau « Kubernetes / Views / Pods » était vide : sa variable de source de données pointait sur la base d’un autre projet.
Les corrections sont banales, et c’est bien le problème : elles auraient pu être faites le premier jour.
# vmsingle : de la marge, et de préférence sur les workers Pi 5, loin d'etcd
resources:
requests: { cpu: 100m, memory: 512Mi }
limits: { cpu: 1000m, memory: 1Gi }
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference: { ... } # les deux workers Pi 5
# vmagent : le kubelet, mais seulement ce qui mesure les volumes
- job_name: kubelet
metric_relabel_configs:
- source_labels: [__name__]
regex: "kubelet_volume_stats_(capacity|available|used)_bytes|kubelet_volume_stats_inodes(_free)?"
action: keep
Un job victoriametrics qui scrape vmsingle et vmagent, node-exporter sans limite CPU, et un groupe vmalert capacite complètent le tout : VMSingleIngestionStopped (moins d’une ligne insérée par seconde pendant 10 minutes, critique), PVCAlmostFull sous 15 % libre, NodeSwapHigh, TmpfsFilling, LonghornBackupError.
Angle mort n° 2 — 994 bibliothèques fantômes dans un tmpfs
Les nouvelles règles de mémoire n’ont pas attendu longtemps. Sur le NAS, le swap était plein depuis cinq jours. Personne ne l’avait remarqué : le NAS répondait, les conteneurs tournaient, juste plus lentement.
Le coupable n’était pas un processus, mais un répertoire. Sur cette machine, /tmp est un tmpfs de 8 Go, donc de la RAM. Il contenait 994 fichiers .<hash>-00000000.so de 4,3 Mo chacun, créés entre le 29 septembre et le 9 octobre, 4,15 Gio au total. Aucun n’était ouvert par un processus.
Remonter la chaîne a pris plus de temps que le nettoyage :
- Ces fichiers sont des copies de
libopentui.so, une bibliothèque qu’opencode (un binaire Bun lancé depuis le cachenpx) extrait dans/tmpau démarrage et efface à la sortie… quand il sort proprement. - vibe-kanban, un service systemd utilisateur, lance
opencode serveà chaque démarrage, puis le tue d’un SIGKILL. Aucune chance d’effacer quoi que ce soit. - vibe-kanban redémarrait jusqu’à 300 fois par jour. Sans linger, le gestionnaire systemd d’un utilisateur s’arrête à la fermeture de sa dernière session. Or une tâche planifiée du PC (une synchronisation par ssh et scp toutes les deux minutes) ouvrait et fermait des sessions en boucle. Chaque fermeture arrêtait les services utilisateur, chaque ouverture les relançait, et chaque relance laissait 4,3 Mo de plus en RAM.
Le correctif tient en trois gestes : suppression des fichiers, loginctl enable-linger (autorisé sans sudo par polkit), et un cron horaire qui efface ceux de plus d’une heure, comme filet.
Détail vexant : TmpfsFilling n’aurait de toute façon pas sonné. Le pic d’occupation était à 64 %, et la règle se déclenche sous 35 % libre. C’est NodeSwapHigh qui a parlé. Une alerte sur un symptôme précis rate facilement sa cible ; une alerte sur la ressource finale, ici la mémoire, finit par la rattraper.
Angle mort n° 3 — Un exporter SMART qui ne lisait aucun disque
Le tableau SMART des six Raspberry Pi affichait des panneaux vides. Pas des valeurs à zéro : du vide. Je l’avais toujours lu comme « rien à signaler ».
Les logs de l’exporter, eux, étaient explicites : chaque appel à smartctl finissait en exec format error. L’image arm64 épinglée (tag master) embarquait un binaire smartctl compilé pour amd64. Le conteneur démarrait, l’exporter répondait sur /metrics, la sonde était verte, et chaque lecture échouait. Résultat : 0 disque surveillé sur 6 nœuds, avec un pod Running.
Passer à l’image amont v0.15.0 (construite sur alpine arm64, smartctl 7.5 aarch64) ne suffisait pas. Le scan automatique des disques posait deux problèmes : il étiquetait le SSD racine, branché en USB, comme un disque scsi sans attributs ATA, et il ramassait les volumes Longhorn, que le nœud voit comme des disques iSCSI. La solution consiste à nommer le disque explicitement :
args:
- --smartctl.device=/dev/sda;sat # SSD racine USB, derrière un pont SAT
Cinq SSD remontent désormais température, santé et heures de fonctionnement. Le sixième nœud démarre sur une clé USB, qui n’a tout simplement pas de SMART. Au moins, c’est maintenant une absence connue.
Au passage, j’ai activé PSI sur les six Pi (psi=1 sur la ligne de commande du noyau ; le noyau Raspberry Pi le livre désactivé). node-exporter expose depuis les temps d’attente CPU, mémoire et E/S, bien plus parlants qu’un pourcentage d’utilisation.
Angle mort n° 4 — Des certificats qu’un redémarrage complet n’avait pas renouvelés
Le constat le plus sérieux est venu d’un détail visuel. Sur le tableau « Benchmarks », les annotations orange « K8s warnings » formaient une rangée régulière. Je les avais prises pour du bruit de fond.
C’était l’événement CertificateExpirationWarning, émis toutes les 30 minutes par chaque master : 585 occurrences en quatre jours. k3s certificate check confirmait, en WARNING : client-auth-proxy, client-k3s-cloud-controller, etcd-client et consorts, tous à échéance du 2 février 2027.
Le contexte juste nécessaire. K3s émet ses certificats pour 365 jours. Il les renouvelle seul, à deux conditions : au démarrage du service, et seulement si l’expiration est à moins de 120 jours. Le cluster datant de début février 2026, la fenêtre s’est ouverte le 6 octobre.
Or le rack entier avait redémarré fin septembre, lors de son déménagement. Quelques jours trop tôt : à ce moment, aucun certificat n’était dans sa fenêtre, et rien n’a été renouvelé. Ensuite, plus aucun redémarrage n’était prévu avant… l’expiration, qui fait tomber l’API du cluster.
février 2026February 2026
Certificats émis pour 365 jours. Expiration : février 2027.Certificates issued for 365 days. Expiry: February 2027.
fin septembrelate September
Tous les nœuds redémarrent, mais à plus de 120 jours de l'échéance : rien n'est renouvelé.Every node restarts, but more than 120 days before expiry: nothing is renewed.
6 octobre6 October
Plus aucun redémarrage prévu. Seul signal : un événement Kubernetes toutes les 30 min par master (585 en 4 jours).No restart planned. Only signal: a Kubernetes event every 30 min per master (585 in 4 days).
10 octobre10 October
Certificats clients renouvelés jusqu'en octobre 2027. Ceux de service, hors fenêtre, gardent leur date.Client certificates renewed until October 2027. Serving ones, outside the window, keep their date.
juin 2027June 2027
Un master expire le 18 juin. Une alerte sonne dès l'ouverture de sa fenêtre, en février.One master expires on 18 June. An alert fires as soon as its window opens, in February.
La rotation elle-même est sans histoire, à condition d’être patient :
- snapshot etcd, comme filet ;
systemctl restart k3ssur chaque master, un par un, en attendant que les trois soientReadyet etcd sain avant le suivant ;systemctl restart k3s-agentsur chaque worker, en attendant que les 17 volumes Longhorn repassenthealthyentre chaque.
Six nœuds en cinq minutes. Les certificats clients valent désormais jusqu’au 10 octobre 2027.
Mais en relisant k3s certificate check après la rotation, colonne par colonne, certaines lignes n’avaient pas bougé. Les certificats de service (apiserver, serveur et pair etcd) ont leur propre date d’émission. Ils n’étaient pas dans leur fenêtre le 10 octobre, donc K3s les a laissés tels quels. L’un des masters expire le 18 juin 2027, les deux autres en septembre. Le certificat servi par l’API elle-même est encore à part : dynamique, partagé par les trois masters, il se renouvelle seul sans redémarrage, et k3s certificate check ne le liste même pas.
Un redémarrage ne remet donc pas tous les compteurs à zéro. Il en remet certains, selon leur date. La seule défense durable, c’est de mesurer.
La mesure : un script, un fichier texte, quelques règles
node-exporter sait lire des métriques dans des fichiers texte (collecteur textfile). Un script, lancé chaque heure par cron sur les six nœuds (et au démarrage), lit la date d’expiration de chaque certificat K3s avec openssl x509 -enddate et l’écrit au format Prometheus :
k3s_certificate_not_after_seconds{source="file",name="server/tls/client-admin.crt"} 1823189700
k3s_certificate_metrics_last_run_seconds 1791667020
Puis un groupe vmalert certificats (extrait simplifié) :
- alert: K3sCertificateRenewalWindow # un redémarrage suffit
expr: (min by (instance) (k3s_certificate_not_after_seconds{source="file"}) - time()) < 120 * 86400
- alert: K3sCertificateExpiringSoon # critique, remplace la précédente
expr: ... < 21 * 86400
- alert: K3sCertificateMetricsStale # le script ne tourne plus
expr: time() - k3s_certificate_metrics_last_run_seconds > 26 * 3600
La première version avait trois défauts, trouvés en relisant le correctif à froid, une heure plus tard :
- Une seule ligne sans valeur invalide tout le fichier pour node-exporter. Une date illisible aurait donc fait disparaître toutes les mesures du nœud, en silence. Le script saute désormais la ligne fautive.
- Un battement de cœur sans mesure : si
openssltombait en panne, le script écrivait quand même unlast_runfrais, sans aucun certificat, etMetricsStalerestait muette. Désormais, si aucun certificat n’est lu, l’ancien fichier est conservé : la mesure vieillit et l’alerte finit par sonner. Une règleMetricsMissingcouvre en plus le nœud vivant sans mesure complète. - Le certificat dynamique de l’API, qui se renouvelle seul, aurait déclenché
RenewalWindowau printemps 2027 avec une consigne fausse (« redémarrez »). Il est exclu de cette règle et a la sienne : alerte seulement s’il passe sous 60 jours sans s’être renouvelé.
Enfin, l’avertissement de fenêtre est routé une fois par semaine : environ 17 rappels entre février et juin 2027, au lieu de 114 avec la répétition par défaut. Pas question de recréer le spam dont on venait de sortir.
Angle mort n° 5 — Des snapshots qui gardaient 19 Gio de données mortes
Dernier panneau, côté Longhorn : la taille réelle des volumes de VictoriaMetrics et VictoriaLogs dépassait largement leur contenu. Les deux étaient inclus dans le job récurrent daily-snapshot, ce qui n’a guère de sens pour une base de séries temporelles qui réécrit ses blocs en permanence : chaque snapshot fige des données qui seront compactées le lendemain.
Les sortir du job ne suffisait pas, et la purge a été instructive :
| Étape (10 octobre) | vmsingle | victorialogs |
|---|---|---|
| Avant | 23,3 Gio | 4,35 Gio |
14 snapshots daily-snapshot supprimés | 21,7 Gio | 3,7 Gio |
| Le soir, 22,0 et 3,8 Gio : 2 derniers snapshots supprimés (un manuel, un hebdomadaire) + Trim Filesystem | 8,3 Gio | 0,3 Gio |
Supprimer un snapshot Longhorn ne libère presque rien : ses données sont fusionnées dans le snapshot suivant. L’espace ne revient qu’une fois la chaîne vidée et le trim du système de fichiers lancé (avec unmapMarkSnapChainRemoved activé sur le volume). Les sauvegardes S3 de chaque volume, elles, sont restées intactes. C’est un cousin de la fuite mémoire Longhorn : des ressources consommées par une mécanique interne, invisibles tant qu’on ne compare pas taille réelle et contenu.
Leçons retenues
- Un canal d’alertes calme ne prouve que l’absence de règles vraies. Planifier une relecture complète des tableaux, panneau par panneau, avec une question fixe : « qui me préviendrait si ce chiffre changeait ? ». Ici, une nuit pour cinq problèmes.
- Le système de mesure doit se mesurer lui-même : ingestion de la base, mesures qui disparaissent, scripts qui ne tournent plus. Une alerte « je n’ai plus de données » vaut plus que dix alertes sur les données.
- Un panneau vide n’est pas un panneau à zéro. Un exporter
Runningavec une sonde verte peut ne rien mesurer du tout. Vérifier qu’une série existe avant de lui faire confiance. - Les renouvellements automatiques ont des conditions. Pour K3s : au démarrage, sous 120 jours, certificat par certificat. Un redémarrage complet au mauvais moment ne renouvelle rien, et au bon moment, seulement une partie. Mesurer l’expiration plutôt que supposer le renouvellement.
- Préférer les alertes sur la ressource finale.
TmpfsFillingaurait raté le problème qu’elle visait ; la pression mémoire l’a trouvé.
Prochaine échéance notée : redémarrer tout le cluster entre le 13 et le 17 juin 2027. D’ici là, si j’oublie, c’est une alerte qui me le rappellera, et non plus un trait orange que je prends pour du bruit.