Aller au contenu
medium

Observabilité Kubernetes : signaux clés et pièges à éviter

27 min de lecture

Kubernetes génère des centaines de métriques par pod, par node, par composant du control plane. Sans stratégie, vous collectez tout, stockez tout, et ne comprenez rien. Ce guide vous apprend à identifier les signaux clés qui révèlent les vrais problèmes (OOMKilled, pending pods, restarts excessifs, nodes not ready), à distinguer les 3 sources de métriques (kube-state-metrics, cAdvisor, kubelet), et à éviter les pièges qui font exploser votre cardinalité et vos coûts. À la fin, vous saurez configurer un monitoring Kubernetes efficace qui détecte les vrais problèmes sans noyer votre Prometheus.

  • Les 4 couches d’observabilité K8s : application, conteneur, node, control plane — et pourquoi vous devez couvrir chacune
  • Les 3 sources de métriques : kube-state-metrics, cAdvisor, kubelet — leur rôle, ce qu’elles exposent, comment les scraper proprement
  • Les 5 signaux clés : OOMKilled, pod restarts, pending pods, node not ready, CPU throttling — les métriques exactes et les requêtes PromQL
  • Les logs Kubernetes : stdout/stderr des pods, logs du control plane, pièges de volumétrie
  • Le piège de la cardinalité : pourquoi les labels K8s font exploser Prometheus et comment s’en protéger
  • Les noisy neighbors : détecter et isoler les workloads qui surconsomment les ressources partagées
  • Les alertes essentielles : 8 alertes production-ready pour un cluster K8s minimal
  • Les anti-patterns : les erreurs classiques qui rendent votre monitoring inutile ou ruineux

Dans Kubernetes, les métriques viennent de 4 couches différentes. Chaque couche répond à des questions distinctes, et manquer l’une d’elles crée des angles morts.

CoucheCe qu’elle mesureExemples de questions
ApplicationLe comportement métier de votre codeCombien de requêtes ? Quel taux d’erreur ? Quelle latence ?
ConteneurLa consommation ressource de chaque conteneurCombien de CPU/mémoire utilisés ? Des OOM ? Du throttling ?
NodeLa santé de la machine hôteDisque plein ? Réseau saturé ? Kernel panics ?
Control planeLe fonctionnement de Kubernetes lui-mêmeL’API server répond-il ? Le scheduler place-t-il les pods ? etcd est-il sain ?

Kubernetes expose ses métriques via 3 composants distincts. Chacun a un rôle différent — les confondre mène à des dashboards redondants ou incomplets.

kube-state-metrics (KSM) écoute l’API server Kubernetes et convertit l’état des objets (pods, deployments, nodes, etc.) en métriques Prometheus.

Ce qu’il exposeCe qu’il ne fait pas
Nombre de pods pending, running, failedConsommation CPU/mémoire des conteneurs
Nombre de replicas désiré vs actuelMétriques du node hôte
Raison du dernier redémarrage (OOMKilled, Error, etc.)Métriques de l’application
État des PVC (bound, pending)Latence réseau
Labels et annotations des objets K8sLogs

Exemples de métriques :

  • kube_pod_status_phase{phase="Pending"} — pods en attente de scheduling
  • kube_pod_container_status_restarts_total — compteur de redémarrages
  • kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} — OOM
  • kube_deployment_status_replicas_unavailable — replicas manquants

Installation : déployez kube-state-metrics dans le namespace kube-system (Helm chart officiel) et configurez Prometheus pour scraper le service.

cAdvisor : la consommation ressource des conteneurs

Section intitulée « cAdvisor : la consommation ressource des conteneurs »

cAdvisor (Container Advisor) est intégré au kubelet. Il mesure la consommation réelle de chaque conteneur : CPU, mémoire, réseau, I/O disque.

Ce qu’il exposeCe qu’il ne fait pas
CPU utilisé par conteneur (container_cpu_usage_seconds_total)État des objets K8s (replicas, pending)
Mémoire utilisée (container_memory_usage_bytes)Raison des redémarrages
I/O disque (container_fs_reads_bytes_total)Métriques du control plane
Réseau par pod (container_network_receive_bytes_total)Métriques applicatives

Exemples de métriques :

  • container_cpu_usage_seconds_total — CPU utilisé (cumulatif)
  • container_memory_working_set_bytes — mémoire effective (hors cache)
  • container_cpu_cfs_throttled_seconds_total — temps passé en throttling CPU

Le kubelet expose ses propres métriques sur le fonctionnement du node : opérations de pod lifecycle, stockage, runtime.

Ce qu’il exposeCe qu’il ne fait pas
Latence des opérations pod (start, stop, sync)Consommation applicative
Erreurs du runtime (CRI)État des objets K8s
Espace disque disponible pour les podsMétriques du control plane
Nombre de pods par nodeLogs applicatifs

Exemples de métriques :

  • kubelet_pod_start_duration_seconds — temps de démarrage des pods
  • kubelet_volume_stats_available_bytes — espace disponible par PVC
  • kubelet_running_pods — pods actifs sur le node

Scraping en production : auth, TLS et ServiceMonitors

Section intitulée « Scraping en production : auth, TLS et ServiceMonitors »

Les endpoints kubelet (/metrics, /metrics/cadvisor) tournent sur le port 10250 qui est sécurisé (TLS + authentification). En production, évitez le “scrape brut” d’IP nodes sans durcissement.

SourceDéployé parEndpoint typiqueCas d’usage principal
kube-state-metricsVous (Helm/manifest)kube-state-metrics:8080/metricsÉtats K8s : pending, OOMKilled, replicas
cAdvisorIntégré au kubelet<node>:10250/metrics/cadvisorConso ressource : CPU, mémoire, I/O
kubeletKubernetes<node>:10250/metricsSanté node : pod lifecycle, volumes

Ces 5 signaux couvrent 80 % des problèmes opérationnels Kubernetes. Chacun a une métrique précise, une requête PromQL, et une alerte recommandée.

Symptôme : le pod redémarre sans explication évidente, l’application perd ses sessions ou ses transactions en cours.

Métrique : kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}

Requête PromQL (pods OOMKilled récemment — corrélation avec restarts) :

increase(kube_pod_container_status_restarts_total[15m]) > 0
and on (namespace, pod, container)
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1

Alerte :

- alert: ContainerOOMKilled
expr: |
increase(kube_pod_container_status_restarts_total[15m]) > 0
and on (namespace, pod, container)
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1
for: 0m
labels:
severity: warning
annotations:
summary: "Container {{ $labels.container }} in pod {{ $labels.pod }} was OOMKilled (recent)"

Actions :

  1. Vérifier les requests/limits mémoire actuels vs consommation réelle
  2. Investiguer les fuites mémoire (profiling, heap dumps)
  3. Vérifier la configuration du GC (Java, Go, Node.js)
  4. Augmenter les limits seulement si la consommation est légitime

Symptôme : un pod redémarre en boucle (CrashLoopBackOff), l’application est instable.

Métrique : kube_pod_container_status_restarts_total

Requête PromQL (pods avec > 0 restart sur 15 min) :

rate(kube_pod_container_status_restarts_total[15m]) > 0

Alerte :

- alert: PodCrashLooping
expr: rate(kube_pod_container_status_restarts_total[15m]) > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.pod }} is crash looping"

Actions : examiner les logs (kubectl logs --previous), vérifier les probes (liveness/readiness mal configurées), examiner les exit codes.

3. Pending pods — le scheduler ne peut pas placer

Section intitulée « 3. Pending pods — le scheduler ne peut pas placer »

Symptôme : les pods restent en Pending, les déploiements n’avancent pas, l’autoscaler ne scale pas.

Métrique : kube_pod_status_phase{phase="Pending"}

Requête PromQL (pods pending depuis > 5 min) :

sum by (namespace, pod) (kube_pod_status_phase{phase="Pending"}) > 0

Alerte :

- alert: PodPendingTooLong
expr: sum by (namespace, pod) (kube_pod_status_phase{phase="Pending"}) > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.pod }} is pending for more than 5 minutes"
runbook_url: "https://example.com/runbooks/pod-pending"

Actions : examiner les events (kubectl describe pod), vérifier les ressources disponibles (CPU/mémoire du cluster), les contraintes (nodeSelector, affinity, taints/tolerations), les PVC (pending storage).

Symptôme : les pods sur ce node sont évacués, ils basculent sur d’autres nodes, la capacité du cluster diminue.

Métrique : kube_node_status_condition{condition="Ready",status="true"}

Requête PromQL (nodes not ready — version robuste avec agrégation) :

sum by (node) (kube_node_status_condition{condition="Ready",status="true"}) == 0

Alerte :

- alert: NodeNotReady
expr: sum by (node) (kube_node_status_condition{condition="Ready",status="true"}) == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Node {{ $labels.node }} is not ready"

Actions : vérifier la connectivité réseau du node, l’état du kubelet (systemctl status kubelet), les ressources système (disque, mémoire), les logs kubelet (journalctl -u kubelet -f).

Symptôme : l’application est lente, la latence p99 augmente, mais l’utilisation CPU semble “normale”.

Métrique : container_cpu_cfs_throttled_seconds_total

Requête PromQL (% de temps passé en throttling — approximation opérationnelle) :

rate(container_cpu_cfs_throttled_seconds_total[5m])
/ (rate(container_cpu_usage_seconds_total[5m]) + 0.001) > 0.2

Alerte :

- alert: ContainerCPUThrottling
expr: |
rate(container_cpu_cfs_throttled_seconds_total[5m])
/ (rate(container_cpu_usage_seconds_total[5m]) + 0.001) > 0.2
for: 5m
labels:
severity: warning
annotations:
summary: "Container {{ $labels.container }} is being CPU throttled"

Actions :

  1. Vérifier les CPU limits actuels vs consommation réelle
  2. Augmenter les limits CPU si le throttling est constant
  3. Pour les workloads sensibles à la latence : envisager “requests only”

Dans Kubernetes, les logs applicatifs sont les flux stdout et stderr des conteneurs. Le kubelet les capture et les stocke sur le node.

Collecte : déployez un DaemonSet de collecteurs (Fluent Bit, Promtail, Vector) qui lit les fichiers /var/log/pods/ sur chaque node et les envoie vers Loki, Elasticsearch, ou votre backend de logs.

Pièges courants :

PiègeConséquenceSolution
Logs trop volumineuxStockage qui explose, coûts élevésFiltrer à la source, échantillonner, définir des rétentions courtes
Logs non structurésRecherche impossible, parsing fragileLogger en JSON, définir un schéma standard
Pas de labels K8sImpossible de corréler avec les métriques/tracesEnrichir avec pod, namespace, container au moment de la collecte

Les composants du control plane (API server, scheduler, controller manager, etcd) écrivent leurs logs vers stdout — visibles via kubectl logs dans le namespace kube-system ou via journald sur les nodes master.

ComposantLogs utilesCas d’usage
kube-apiserverRequêtes lentes, erreurs d’authentification, rate limitingDebug RBAC, audit, perf API
kube-schedulerÉchecs de placement, raisons de pendingComprendre pourquoi un pod ne se schedule pas
kube-controller-managerErreurs de reconciliationDebug deployments, replicasets, jobs
etcdLatences, compactions, élections leaderDiagnostic cluster instable

Kubernetes génère des labels dynamiques : pod, container, uid, revision, node. Ces labels changent à chaque déploiement, chaque restart, chaque scale. Le résultat : votre cardinalité explose.

Prometheus stocke chaque combinaison unique de metric + labels comme une time series. Le nombre de séries = produit du nombre de valeurs de chaque label.

Exemple catastrophique :

container_cpu_usage_seconds_total{
namespace="production",
pod="payment-api-abc123",
container="payment",
node="node-1"
}

Avec 50 pods × 20 conteneurs × 10 nodes × rolling updates fréquents, vous atteignez rapidement des millions de séries — et Prometheus s’effondre.

LabelCardinalité typiqueRisque
podIllimitée (change à chaque restart)🔴 Très élevé
uidIllimitée (unique par objet)🔴 Très élevé
container_idIllimitée🔴 Très élevé
idIllimitée (cgroup id)🔴 Très élevé
namePotentiellement illimitée🔴 Très élevé
imageHaute (change à chaque tag)🟠 Moyen-élevé
revisionPotentiellement élevée🟠 Moyen
nodeNombre de nodes (souvent < 100)🟢 Faible
namespaceNombre de namespaces (souvent < 50)🟢 Faible

Principe : on filtre d’abord les labels à très forte cardinalité, on drop des métriques seulement quand on sait qu’on ne les utilise pas (après audit des dashboards, alerts, recording rules).

1. Filtrez les labels explosifs au scraping (metric_relabel_configs) :

scrape_configs:
- job_name: 'cadvisor'
metric_relabel_configs:
# Supprimer les labels à cardinalité illimitée
- action: labeldrop
regex: 'id|name|image|container_id|uid'

2. Utilisez des labels bornés :

  • deployment, statefulset au lieu de pod
  • namespace, service au lieu de container_id
  • Normalisez les chemins : /users/12345/users/:id

3. Surveillez votre cardinalité :

# Top 10 métriques par nombre de séries
topk(10, count by (__name__) ({__name__!=""}))
# Séries par label pour une métrique
count by (pod) (container_cpu_usage_seconds_total)

4. Alerte sur la croissance :

- alert: HighCardinalityMetric
expr: count by (__name__) ({__name__=~"container_.*"}) > 100000
for: 5m
labels:
severity: warning
annotations:
summary: "Metric {{ $labels.__name__ }} has very high cardinality"

Noisy neighbors : détecter les workloads qui surconsomment

Section intitulée « Noisy neighbors : détecter les workloads qui surconsomment »

Dans un cluster multi-tenant, un workload peut consommer toutes les ressources d’un node et dégrader les performances des autres pods — c’est le “noisy neighbor problem”.

  • Latence p99 qui augmente sur des pods “innocents”
  • CPU throttling sur des pods qui ne devraient pas en avoir
  • Evictions de pods pour pression mémoire
  • Pods qui ne se schedulent plus (resources exhausted)

CPU par namespace (qui consomme le plus ?) :

sum by (namespace) (
rate(container_cpu_usage_seconds_total{container!="POD"}[5m])
) / ignoring(namespace) group_left
sum(
rate(container_cpu_usage_seconds_total{container!="POD"}[5m])
)

Mémoire par namespace :

sum by (namespace) (container_memory_working_set_bytes{container!="POD"})

Pods sans limits (candidats au noisy neighbor) :

kube_pod_container_resource_limits{resource="cpu"} == 0
MéthodeDescriptionQuand l’utiliser
Resource QuotasLimites par namespace (CPU/mémoire totaux)Multi-tenant
Limit RangesDefaults et limites par pod/conteneurEmpêcher les pods sans limits
Priority ClassesÉviction ordonnée en cas de pressionProtéger les workloads critiques
Pod Disruption BudgetsGarantir un minimum de réplicasHaute disponibilité

Voici un set minimal d’alertes production-ready, couvrant les signaux critiques sans bruit excessif :

groups:
- name: kubernetes-essential
rules:
# 1. Pod OOMKilled (événement récent)
- alert: ContainerOOMKilled
expr: |
increase(kube_pod_container_status_restarts_total[15m]) > 0
and on (namespace, pod, container)
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1
for: 0m
labels:
severity: warning
annotations:
summary: "Container {{ $labels.container }} was OOMKilled (recent)"
runbook_url: "https://example.com/runbooks/oomkilled"
# 2. Pod crash looping
- alert: PodCrashLooping
expr: rate(kube_pod_container_status_restarts_total[15m]) > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.pod }} is crash looping"
runbook_url: "https://example.com/runbooks/crashloop"
# 3. Pod pending
- alert: PodPendingTooLong
expr: sum by (namespace, pod) (kube_pod_status_phase{phase="Pending"}) > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.pod }} is pending"
runbook_url: "https://example.com/runbooks/pod-pending"
# 4. Node not ready (version robuste)
- alert: NodeNotReady
expr: sum by (node) (kube_node_status_condition{condition="Ready",status="true"}) == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Node {{ $labels.node }} is not ready"
runbook_url: "https://example.com/runbooks/node-not-ready"
# 5. CPU throttling
- alert: ContainerCPUThrottling
expr: |
rate(container_cpu_cfs_throttled_seconds_total[5m])
/ (rate(container_cpu_usage_seconds_total[5m]) + 0.001) > 0.2
for: 5m
labels:
severity: warning
annotations:
summary: "Container {{ $labels.container }} is CPU throttled"
runbook_url: "https://example.com/runbooks/cpu-throttling"
# 6. Mémoire proche de la limite
- alert: ContainerMemoryNearLimit
expr: |
container_memory_working_set_bytes
/ container_spec_memory_limit_bytes > 0.9
for: 5m
labels:
severity: warning
annotations:
summary: "Container {{ $labels.container }} memory at 90%+ of limit"
runbook_url: "https://example.com/runbooks/memory-near-limit"
# 7. Deployment replicas mismatch
- alert: DeploymentReplicasMismatch
expr: |
kube_deployment_spec_replicas
!= kube_deployment_status_replicas_available
for: 5m
labels:
severity: warning
annotations:
summary: "Deployment {{ $labels.deployment }} replicas mismatch"
runbook_url: "https://example.com/runbooks/replicas-mismatch"
# 8. PVC pending
- alert: PersistentVolumeClaimPending
expr: kube_persistentvolumeclaim_status_phase{phase="Pending"} == 1
for: 5m
labels:
severity: warning
annotations:
summary: "PVC {{ $labels.persistentvolumeclaim }} is pending"
runbook_url: "https://example.com/runbooks/pvc-pending"
Anti-patternPourquoi c’est un problèmeSolution
Scraper pod comme labelCardinalité explose à chaque deployAgréger par deployment, service
Collecter toutes les métriques cAdvisorCentaines de séries par conteneurFiltrer au scraping (labeldrop sur labels explosifs)
Pas de kube-state-metricsImpossible de voir pending, OOMKilled, replicasDéployer KSM dès le premier jour
Alertes sur les métriques containers seulesManque le contexte K8s (rollout, scaling)Combiner avec kube-state-metrics
Logs sans labels K8sImpossible de corréler avec metrics/tracesEnrichir avec namespace, pod, container
Ignorer le control planeProblèmes API server, scheduler invisiblesCollecter les métriques du control plane
Pas de resource quotasUn namespace peut tuer le clusterResource quotas en multi-tenant
Limits CPU trop bassesThrottling artificiel, latence dégradéeÉvaluer requests-only pour workloads sensibles
Dashboards par pod200 pods = dashboard illisibleAgréger par deployment, namespace, service
Rétention logs illimitéeCoûts qui explosentRétention courte (7-30 jours), tier froid

Workflow : déployer l’observabilité sur un cluster K8s

Section intitulée « Workflow : déployer l’observabilité sur un cluster K8s »
  1. Déployez kube-state-metrics

    Helm chart officiel ou manifest. Vérifiez que le service est scrapé par Prometheus. Vous devriez voir les métriques kube_*.

  2. Configurez le scraping cAdvisor / kubelet avec ServiceMonitors

    Utilisez les ServiceMonitors/PodMonitors de votre stack (kube-prometheus-stack). Configurez le RBAC et les certificats TLS. Ajoutez des metric_relabel_configs pour filtrer les labels dangereux (id, name, container_id, uid).

  3. Déployez un collecteur de logs

    Fluent Bit, Promtail, ou Vector en DaemonSet. Configurez l’enrichissement avec les labels K8s (namespace, pod, container) et l’envoi vers Loki ou votre backend.

  4. Créez les dashboards de base

    Un dashboard “Cluster overview” (nodes, pods pending, restarts). Un dashboard “Namespace” (CPU/mémoire par deployment, top consumers). Un dashboard “Pod debugging” (logs + métriques pour investigation).

  5. Déployez les 8 alertes essentielles

    PrometheusRule avec les alertes OOMKilled, crash loop, pending, node not ready, throttling, memory near limit, replicas mismatch, PVC pending. Ajoutez les annotations runbook_url vers vos runbooks.

  6. Configurez les resource quotas (clusters multi-tenant)

    Limites par namespace pour CPU et mémoire. Limit ranges pour les defaults pod.

  7. Surveillez votre cardinalité

    Dashboard dédié : top métriques par séries, croissance au fil du temps. Alerte si une métrique dépasse un seuil.

  8. Itérez sur les filtres et agrégations

    Chaque semaine, identifiez les métriques inutilisées et les labels redondants. Affinez vos recording rules pour pré-agréger.

PiègeConséquenceSolution
Scraper toutes les métriques K8sPrometheus OOM, requêtes lentesFiltrer dès le scraping
Ignorer le throttling CPULatence dégradée sans explicationAlerter sur container_cpu_cfs_throttled
Dashboards avec 500 sériesGrafana timeout, impossible à lireAgréger, filtrer, utiliser des variables
Logs en mode debug en prodVolumétrie ×10, coûts ×10Niveau info ou warn en prod
Pas de rétention différenciéeTout garder = coûts exponentielsHot/warm/cold tiers, rétention par criticité
  1. Kubernetes expose des métriques à 4 couches : application (vous l’instrumentez), conteneur (cAdvisor), node (kubelet), control plane (API server, scheduler, etcd)

  2. Les 3 sources principales : kube-state-metrics pour l’état K8s (pending, OOMKilled, replicas), cAdvisor pour la consommation ressource (CPU, mémoire, I/O), kubelet pour la santé node

  3. Les 5 signaux clés : OOMKilled (mémoire dépassée), pod restarts (crash loop), pending pods (scheduler bloqué), node not ready (node défaillant), CPU throttling (limits trop basses ou CFS quota)

  4. La cardinalité explose avec les labels dynamiques (pod, uid, container_id). Filtrez dès le scraping avec labeldrop, agrégez par deployment ou service, surveillez avec des requêtes PromQL dédiées

  5. Les noisy neighbors se détectent via comparaison de consommation par namespace/deployment. Protégez-vous avec resource quotas et limit ranges

  6. Les logs K8s sont stdout/stderr des pods. Enrichissez-les avec les labels K8s à la collecte, limitez la volumétrie avec des rétentions courtes

  7. Pour le control plane (clusters self-hosted), collectez les métriques de l’API server, scheduler, controller manager, etcd. Sur les clusters managés, utilisez l’intégration du provider

  8. Commencez avec 8 alertes essentielles : OOMKilled (événementiel), crash loop, pending, node not ready, throttling, memory near limit, replicas mismatch, PVC pending

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.