❯ storage · Running

Fuite mémoire Longhorn instance-manager : rapport d'incident et runbook Longhorn Instance-Manager RAM Leak: Incident Report & Runbook

Une régression Longhorn v1.11.0 faisait fuiter silencieusement plusieurs gigaoctets de RAM en quelques jours dans les pods instance-manager. Détection, investigation, contournement et fix upstream v1.11.2. A Longhorn v1.11.0 regression caused instance-manager pods to silently leak gigabytes of RAM over days. This post covers detection, investigation, the workaround, and the upstream fix in v1.11.2.

· 10 min de lecture 10 min read ·
k3s-clusterlonghornstorageincidentk8sdebugging
🇬🇧 Read this article in English

Contexte : l’architecture de stockage deux niveaux (tiers critical/standard, replica, tagging de disques) est détaillée dans Stockage bloc distribué avec Longhorn.

Le symptôme : 12 GiB pour un pod qui “ne fait rien”

Le 25 avril 2026, un dashboard de monitoring affiche quelque chose d’anormal sur worker-1 : la RAM disponible est tombée à moins de 4 GiB sur 16 GiB. Rien d’inhabituel côté charge applicative. En creusant avec kubectl top pods -n longhorn-system, le coupable saute aux yeux immédiatement.

NAME                                        CPU(cores)   MEMORY(bytes)
instance-manager-e3a8b2c1d4f6...            12m          12426Mi
longhorn-manager-xk9p2                      8m           87Mi
longhorn-csi-plugin-r4t7n                   3m           42Mi

12,4 GiB dans un pod instance-manager. Un pod dont le rôle est de gérer les processus engine et replica de Longhorn sur ce nœud — rien qui justifie d’approcher la limite RAM du RPi 5. Le pod était en vie depuis 32 jours.

Le pod est redémarré manuellement, la mémoire retombe à une trentaine de mégaoctets. Incident clos… provisoirement.

Le 11 mai, même scénario : worker-1 de nouveau, 9,6 GiB après 15 jours. Le 17 mai, trois pods en même temps lors d’une vérification de routine :

NodeRAM consomméeUptime du podCharge
worker-13,0 Gi6 joursrépliques actives
master-32,4 Gi6 joursengine vmsingle (I/O lourde)
worker-3311 Mi70 joursidle, zéro réplique

Le dernier cas est révélateur : worker-3 n’héberge aucune réplique active, aucun trafic I/O — et il fuit quand même, juste plus lentement. Ce n’est pas une question de charge, c’est structurel.

Le contexte : qu’est-ce qu’un instance-manager ?

Longhorn est un système de stockage bloc distribué pour Kubernetes. Son architecture repose sur trois couches de processus qui interagissent ensemble pour présenter un PersistentVolume à un pod applicatif.

Le longhorn-manager tourne comme DaemonSet sur tous les nœuds. C’est le cerveau central : il orchestre la création des volumes, gère les StorageClasses, et surveille l’état global du cluster. Il n’accède jamais directement aux données.

Les processus engine et replica sont les vrais moteurs du stockage. Pour chaque volume monté :

  • engine : expose le block device iSCSI/NVMe au pod applicatif, tourne sur le nœud où le pod est schedulé
  • replica : écrit et lit les données réelles sur le disque local, un processus par nœud de réplication

Avec numberOfReplicas: 2, un volume a un engine et deux replica tournant sur deux nœuds différents. Les données sont synchronisées en temps réel entre les replica via le réseau.

Ces processus sont des binaires Go indépendants, mais ils ne tournent pas directement comme pods Kubernetes. Longhorn les pilote via un pod coordinateur par nœud : l’instance-manager.

L’instance-manager joue le rôle de supervisor local. Il démarre les processus engine et replica quand un volume doit être servi sur le nœud, surveille leur état de santé, et communique avec eux via gRPC pour remonter les métriques et les événements au longhorn-manager. Il est aussi responsable de leur redémarrage si l’un d’eux crashe.

Sur un cluster typique avec 15 volumes et 3 nœuds workers, chaque instance-manager gère donc potentiellement 10 à 30 processus enfants simultanément, avec un trafic gRPC constant.

Sur un homelab RPi hétérogène — 16 GiB sur worker-1 et worker-2, mais seulement 2 GiB sur worker-3 — RAM partagée entre le système, les pods applicatifs, et les processus Longhorn, un leak de plusieurs GiB dans ce seul pod coordinateur met directement en danger la stabilité du nœud entier. Le danger est proportionnellement pire sur worker-3, où quelques centaines de Mio suffisent déjà à peser lourd. Un OOM killer qui tue un pod applicatif est gênant. Un OOM killer qui tue des processus engine ou replica en cours d’écriture, c’est une corruption de données potentielle.

L’investigation : isoler la fuite

Face à une consommation mémoire anormale dans le namespace longhorn-system, il y a trois suspects naturels : le longhorn-manager lui-même, les processus engine/replica enfants, ou le binaire instance-manager. La méthode pour les distinguer est reproductible et vaut d’être documentée.

Étape 1 — Confirmer que c’est bien l’IM, pas les enfants

La première hypothèse naïve : ce sont les processus engine ou replica qui consomment. On entre dans le pod pour vérifier.

# Identifier le PID du processus instance-manager principal
kubectl -n longhorn-system exec -it instance-manager-e3a8b2c1d4f6 -- \
  ps aux --sort=-%mem | head -20
PID   USER  %CPU  %MEM  VSZ      RSS      COMMAND
1     root  0.1   77.4  13107200 12654592 /usr/local/bin/longhorn-instance-manager
142   root  0.0   0.2   458240   32768    longhorn-engine --listen ...
143   root  0.0   0.1   368640   28672    longhorn-engine --listen ...
201   root  0.0   0.2   442368   30720    longhorn-replica --sync-agent ...

Clair : le binaire principal (longhorn-instance-manager, PID 1) absorbe toute la RAM. Les enfants engine et replica restent stables à ~30 MiB chacun.

Étape 2 — Distinguer RSS et VmData

RSS (Resident Set Size) peut inclure des pages partagées. Pour confirmer que c’est bien une fuite heap dans le binaire Go lui-même, on lit /proc/1/status directement :

kubectl -n longhorn-system exec -it instance-manager-e3a8b2c1d4f6 -- \
  cat /proc/1/status | grep -E "VmRSS|VmData|VmHeap"
VmRSS:  12654592 kB
VmData: 12587008 kB

VmData représente le segment de données du processus — heap Go inclus. Avec 12,3 GiB là-dedans et seulement ~60 MiB d’autre chose, la conclusion est sans appel : le garbage collector Go ne récupère pas cette mémoire. C’est une fuite dans le binaire instance-manager lui-même.

Étape 3 — Chercher l’issue upstream

Le pattern est suffisamment spécifique (Go binary, gRPC, croît avec le temps et l’I/O) pour chercher directement dans le tracker Longhorn. La recherche instance-manager memory leak mène en quelques minutes à l’issue #12573 :

[BUG] Very high memory consumption in longhorn-instance-manager Severity: 1 — Regression introduced in v1.11.0

La cause confirmée : une régression dans la couche gRPC de l’instance-manager v1.11.0 provoque une accumulation de structures non libérées en heap Go. Le taux de fuite est proportionnel au volume de trafic gRPC (donc à l’activité I/O du nœud), mais présent même au repos — ce qui explique les 311 MiB sur worker-3 idle après 70 jours.

Le fix a été mergé et backporté en v1.11.1, puis intégré en v1.11.2.

Le contournement (historique, v1.11.0 uniquement)

Le workaround est chirurgical : supprimer le pod bloaté. Longhorn-manager le recrée en 15-30 secondes avec une mémoire fraîche. Les données sur disque ne bougent pas — le nouvel instance-manager remonte les répertoires replica existants depuis /var/lib/longhorn sans perte.

Mais supprimer un pod instance-manager n’est pas anodin. Il faut d’abord s’assurer que chaque volume hébergé sur ce nœud a bien au moins une réplique saine ailleurs.

Pre-flight checks obligatoires

# 1. Lister les engines actifs sur le nœud concerné
sudo kubectl -n longhorn-system get engines.longhorn.io \
  -o custom-columns="NAME:.metadata.name,NODE:.spec.nodeID,STATE:.status.currentState" \
  | grep worker-1

# Exemple de sortie attendue :
# pvc-abc123-e-0   worker-1   running
# pvc-def456-e-0   worker-1   running

# 2. Lister les répliques sur ce nœud
sudo kubectl -n longhorn-system get replicas.longhorn.io \
  -o custom-columns="NAME:.metadata.name,VOL:.spec.volumeName,NODE:.spec.nodeID,STATE:.status.currentState" \
  | grep worker-1

# Exemple :
# pvc-abc123-r-abc   pvc-abc123   worker-1   running
# pvc-abc123-r-def   pvc-abc123   worker-2   running  ← réplique sur autre nœud OK
# pvc-def456-r-ghi   pvc-def456   worker-1   running
# pvc-def456-r-jkl   pvc-def456   worker-3   running  ← réplique sur autre nœud OK

# 3. Pour chaque volume, confirmer ≥1 réplique healthy sur un autre nœud
sudo kubectl -n longhorn-system get volumes.longhorn.io pvc-abc123 \
  -o jsonpath='{.status.robustness}'
# Attendu : healthy ou degraded (jamais faulted avant de continuer)

Le check essentiel : chaque volume doit avoir au moins une réplique en running sur un nœud autre que celui qu’on va redémarrer. Si un volume n’a qu’une seule réplique (sur ce nœud), le supprimer rendrait le volume indisponible — c’est la seule situation où il ne faut pas procéder.

L’action

# Identifier le nom exact du pod
sudo kubectl -n longhorn-system get pod -l app=longhorn-instance-manager \
  -o wide | grep worker-1

# Supprimer avec grace period raisonnable
sudo kubectl -n longhorn-system delete pod instance-manager-e3a8b2c1d4f6 \
  --grace-period=30

Ce qui se passe ensuite

t+0s   Pod instance-manager-e3a8b2c1d4f6 : Terminating
t+5s   Volumes concernés : transition vers Degraded (engine perd le gRPC local)
t+15s  Pod instance-manager-f7c9d1e2b4a8 : Running (nouveau pod recréé)
t+25s  Volumes : retour à Healthy (IM reconnecté aux répliques sur disque)
t+30s  RAM instance-manager : ~25 Mi (idle)

Les applications ne voient rien pendant ce laps de temps : les I/O continuent via les répliques sur les autres nœuds. Le Longhorn CSI driver maintient le block device monté côté applicatif.

Quand ne pas appliquer le workaround

  • Volume avec une seule réplique ET engine sur ce nœud → tolérance zéro, le volume devient indisponible
  • Node en cours d’éviction Longhorn → attendre la fin avant d’intervenir
  • Plusieurs nodes bloqués simultanément → traiter un nœud à la fois, vérifier le retour à Healthy avant le suivant

Le fix permanent : upgrade vers v1.11.2

Le 18 mai 2026, upgrade du cluster via Helm :

# Vérifier la version actuelle
sudo helm list -n longhorn-system
# NAME      NAMESPACE        REVISION  UPDATED              STATUS    CHART
# longhorn  longhorn-system  7         2026-04-01 ...       deployed  longhorn-1.11.0

# Upgrade vers v1.11.2
sudo -E helm upgrade longhorn longhorn/longhorn \
  -n longhorn-system \
  --version 1.11.2 \
  --reset-then-reuse-values \
  --wait --timeout 15m

L’upgrade rolling remplace les pods instance-manager nœud par nœud. Après chaque remplacement, les volumes transitent brièvement par Degraded puis reviennent à Healthy — comportement attendu, identique au workaround manuel.

Ensuite, déclencher l’upgrade automatique des binaires engine (les processus enfants tournent encore avec le binaire v1.11.0 jusqu’à cette étape) :

sudo kubectl -n longhorn-system patch setting \
  concurrent-automatic-engine-upgrade-per-node-limit \
  --type=merge -p '{"value":"3"}'

Métriques post-upgrade (observées le 18 mai)

NodeRôleRAM instance-manager
worker-3idle, zéro réplique11 Mi
worker-112 répliques actives250-330 Mi
worker-212 répliques actives260-310 Mi
master-11-2 engines13-43 Mi
master-3engine vmsingle (I/O lourde)43 Mi

Le contraste est saisissant : worker-3 qui plafonnait à 311 MiB après 70 jours de leak lent est maintenant à 11 MiB stable. master-3 qui gérait l’engine vmsingle à 2,4 GiB est à 43 MiB.

Historique complet de l’incident

DateActionRésultat
2026-04-25Premier bloat documenté — worker-1 à 12,4 Gi après 32j — restart podRécupération de 12,2 Gi
2026-05-11Récurrence — worker-1 à 9,6 Gi après 15j — restart podRécupération de ~9,5 Gi
2026-05-17Cycle confirmé (6j après dernier fix) — worker-1 3 Gi, master-3 2,4 Gi, worker-3 311 MiRestart × 3 pods
2026-05-18Upgrade v1.11.0 → v1.11.2Cause racine éliminée upstream

Leçons retenues

1. Monitorer les pods système individuellement, pas seulement le nœud

kubectl top nodes montre la RAM totale du nœud — mais ne distingue pas un pod qui gonfle d’une charge applicative légitime. L’alerte qui aurait dû exister dès le premier jour :

# Alerte Prometheus à ajouter
- alert: LonghornInstanceManagerMemoryHigh
  expr: |
    container_memory_working_set_bytes{
      namespace="longhorn-system",
      pod=~"instance-manager-.*"
    } > 2 * 1024 * 1024 * 1024
  for: 10m
  labels:
    severity: warning
  annotations:
    summary: "Instance-manager {{ $labels.pod }} > 2 GiB RAM"

2. Sur un homelab, les Severity/1 latents restent longtemps invisibles

Dans un environnement de production managé, un Severity/1 déclenche des alertes, des war rooms, des patchs d’urgence dans les heures qui suivent. Sur un homelab, v1.11.0 a été installé et oublié. La fuite a mis 32 jours pour devenir suffisamment visible pour alerter — 32 jours pendant lesquels le nœud s’approchait silencieusement du seuil OOM.

Deux réflexes à prendre avant d’upgrader un composant de stockage :

  1. Consulter le CHANGELOG de la version cible et les issues ouvertes avec le label kind/bug ou severity/1
  2. Traiter les releases .0 de composants critiques (stockage, réseau, PKI) avec la même prudence qu’une beta — attendre au minimum une .1 ou quelques semaines de retour communautaire

Dans ce cas, v1.11.1 était disponible rapidement après le signalement de l’issue #12573. Rester sur v1.11.0 plus de quelques semaines après la sortie de v1.11.1 était évitable.

3. Les données survivent au restart de l’instance-manager — c’est du design

Le comportement le plus contre-intuitif de l’incident : supprimer le pod qui “gère” les répliques ne supprime pas les répliques. Les données sont sur le disque physique dans /var/lib/longhorn. L’instance-manager est un coordinateur stateless du point de vue des données — il sait où sont les répertoires replica sur le disque, il les remonte au démarrage, il ne les possède pas.

Cette séparation stricte entre le plan de contrôle (instance-manager) et le plan de données (fichiers sur disque) est un principe de design de Longhorn. Elle rend le workaround safe, et elle explique aussi pourquoi un crash OOM de l’instance-manager lui-même — sans kill des enfants — ne corrompt pas les données. Ce sont les processus engine et replica qui détiennent les verrous sur les blocs de données, pas leur superviseur.

4. Un runbook pré-écrit change tout

La différence entre “panique à 23h avec 500 MiB de RAM libre sur un worker” et “exécute la procédure en 3 minutes” tient à l’existence d’un runbook avec les pre-flight checks déjà rédigés. Le workaround lui-même prend moins d’une minute à exécuter. Ce qui prend du temps sans préparation, c’est la phase de décision : est-ce que c’est safe ? Est-ce que mes volumes ont bien des répliques ailleurs ? Est-ce que le timing est bon ?

Un runbook répond à ces questions avant qu’elles se posent en urgence. Il documente aussi les seuils de déclenchement (“à partir de combien de GiB on intervient ?”) et les critères d’échec (“si le volume ne revient pas à Healthy dans les 60 secondes, escalader vers X”). La prochaine personne à lire ce runbook — ou vous-même dans six mois — aura les mêmes garanties qu’aujourd’hui.

Références

  • Issue upstream : longhorn/longhorn#12573 — Very high memory consumption in longhorn-instance-manager (Severity/1, v1.11.0)
  • Fix backporté en v1.11.1, intégré en v1.11.2
  • Runbook interne : docs/runbooks/longhorn-instance-manager-bloat.md (marqué résolu au 2026-05-18)
k3s-clusterlonghornstorageincidentk8sdebugging