❯ monitoring · Running

Relire ses propres tableaux Grafana : cinq angles morts qu'aucune alerte ne voyait Re-reading Your Own Grafana Dashboards: Five Blind Spots No Alert Could See

Une nuit à relire chaque tableau, panneau par panneau, a fait remonter une base de métriques coincée à côté d'etcd, un tmpfs rempli de bibliothèques orphelines, un binaire amd64 dans une image arm64 et des certificats K3s qu'un redémarrage complet du cluster n'avait pas renouvelés. Aucune alerte n'avait sonné, parce que rien ne les mesurait. One night spent reading every dashboard panel by panel turned up a metrics database squeezed next to etcd, a tmpfs full of orphaned libraries, an amd64 binary inside an arm64 image and K3s certificates that a full cluster restart had not renewed. None of it had triggered an alert, because nothing was measuring it.

· 10 min de lecture 10 min read ·
monitoringgrafanavictoriametricsvmalertk3slonghorn
🇬🇧 Read this article in English

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 :

  1. Ces fichiers sont des copies de libopentui.so, une bibliothèque qu’opencode (un binaire Bun lancé depuis le cache npx) extrait dans /tmp au démarrage et efface à la sortie… quand il sort proprement.
  2. 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.
  3. 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.

La rotation elle-même est sans histoire, à condition d’être patient :

  1. snapshot etcd, comme filet ;
  2. systemctl restart k3s sur chaque master, un par un, en attendant que les trois soient Ready et etcd sain avant le suivant ;
  3. systemctl restart k3s-agent sur chaque worker, en attendant que les 17 volumes Longhorn repassent healthy entre 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 openssl tombait en panne, le script écrivait quand même un last_run frais, sans aucun certificat, et MetricsStale restait 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ègle MetricsMissing couvre en plus le nœud vivant sans mesure complète.
  • Le certificat dynamique de l’API, qui se renouvelle seul, aurait déclenché RenewalWindow au 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)vmsinglevictorialogs
Avant23,3 Gio4,35 Gio
14 snapshots daily-snapshot supprimés21,7 Gio3,7 Gio
Le soir, 22,0 et 3,8 Gio : 2 derniers snapshots supprimés (un manuel, un hebdomadaire) + Trim Filesystem8,3 Gio0,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

  1. 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.
  2. 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.
  3. Un panneau vide n’est pas un panneau à zéro. Un exporter Running avec une sonde verte peut ne rien mesurer du tout. Vérifier qu’une série existe avant de lui faire confiance.
  4. 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.
  5. Préférer les alertes sur la ressource finale. TmpfsFilling aurait 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.

monitoringgrafanavictoriametricsvmalertk3slonghorn