Maîtrise des demandes et limites de ressources Kubernetes pour des performances optimales

Découvrez les différences cruciales entre les demandes et les limites de ressources Kubernetes pour le CPU et la mémoire. Ce guide explique comment ces paramètres déterminent les classes de qualité de service (QoS) (Garantie, Burstable, BestEffort), préviennent l'instabilité des nœuds et optimisent l'efficacité de l'ordonnancement du cluster. Inclut des exemples YAML pratiques et des bonnes pratiques pour le réglage des performances.

Maîtrise des demandes et limites de ressources Kubernetes pour des performances optimales

Les demandes et limites de ressources Kubernetes semblent simples dans YAML, mais elles façonnent presque tous les comportements de jour 2 dans un cluster : où les pods atterrissent, quelles charges de travail sont évincées en premier, si une application subit un étranglement CPU sous charge, et combien de capacité de réserve vous pensez avoir. Un mauvais réglage peut rendre une application saine défaillante. Un réglage manquant peut amener l'ordonnanceur à entasser des pods sur un nœud jusqu'à ce que le premier pic de trafic se transforme en incident de voisin bruyant.

Ce qui surprend les équipes, c'est que les demandes et les limites sont utilisées par des systèmes différents. Les demandes sont principalement une promesse d'ordonnancement. Les limites sont une frontière d'application. Les traiter comme la même chose conduit à des résultats étranges, surtout avec le CPU.

Comprendre les concepts fondamentaux : Demandes vs. Limites

Dans Kubernetes, un conteneur peut définir la consommation de ressources attendue en utilisant resources.requests et resources.limits. Ils ne sont techniquement pas obligatoires, sauf si votre cluster utilise des politiques telles que LimitRange ou des contrôles d'admission, mais les charges de travail de production devraient généralement définir au moins des demandes pour le CPU et la mémoire. Sans demandes, l'ordonnanceur a peu d'informations utiles et le pod tombe dans une posture de qualité de service plus faible.

1. Demandes de ressources (requests)

Les demandes représentent la quantité de ressources qu'un conteneur est garanti de recevoir lors de l'ordonnancement. C'est la quantité minimale de ressources que le kube-scheduler utilise pour décider sur quel nœud placer un Pod.

  • Ordonnancement : Un nœud doit avoir suffisamment de ressources allouables disponibles qui satisfont la somme de toutes les demandes de Pod avant qu'un nouveau Pod puisse y être ordonnancé.
  • Priorité d'exécution : Les demandes influencent les parts CPU et les décisions d'éviction mémoire. Elles ne sont pas une réservation magique qui empêche tout ralentissement, mais elles donnent à Kubernetes et au noyau de meilleures informations en cas de contention.

2. Limites de ressources (limits)

Les limites définissent la quantité maximale de ressources qu'un conteneur est autorisé à consommer. Le dépassement de ces limites entraîne des comportements spécifiques et définis pour le CPU et la mémoire.

  • Limites CPU : Si un conteneur tente d'utiliser plus de CPU que sa limite, les cgroups du noyau Linux vont limiter son utilisation, l'empêchant de consommer davantage de cycles.
  • Limites mémoire : Si un conteneur dépasse sa limite mémoire, le noyau peut terminer un processus dans le conteneur. Kubernetes signale cela comme OOMKilled lorsque c'est la raison de terminaison enregistrée.

Comportement CPU vs. Mémoire

Il est crucial de comprendre la différence qualitative dans la façon dont Kubernetes applique les limites CPU par rapport à la mémoire :

Ressource Comportement en cas de dépassement de la limite Mécanisme d'application
CPU Limité (ralenti) cgroups (contrôle de bande passante CPU)
Mémoire Terminé (OOMKill) Tueur OOM du noyau

Avertissement pratique : Les limites CPU peuvent protéger un nœud d'un processus incontrôlé, mais elles peuvent également créer des problèmes de latence lorsqu'elles sont définies trop bas. De nombreuses équipes de plateforme définissent les limites mémoire de manière cohérente et sont plus sélectives avec les limites CPU pour les services sensibles à la latence, en fonction de leur tolérance au risque et de la politique du cluster.

Définition des ressources dans les spécifications de Pod

Les ressources sont définies dans le bloc spec.containers[*].resources. Les quantités sont spécifiées en utilisant les suffixes Kubernetes standard (par exemple, m pour milli-CPU, Mi pour Mébioctets).

Définitions des unités CPU

  • 1 unité CPU équivaut à 1 cœur complet (ou vCPU chez les fournisseurs de cloud).
  • 1000m (millicœurs) équivaut à 1 unité CPU.

Définitions des unités mémoire

  • Mi (Mébioctets) ou Gi (Gibioctets) sont courants.
  • 1024Mi = 1Gi.

Exemple de configuration YAML

Considérons un conteneur qui nécessite un minimum garanti de 500m CPU et 256Mi de mémoire, mais ne doit jamais dépasser 1 CPU et 512Mi :

resources:
  requests:
    memory: "256Mi"
    cpu: "500m"
  limits:
    memory: "512Mi"
    cpu: "1"

Les chiffres doivent provenir du comportement observé, pas de conjectures. Pour une petite API HTTP, une demande de départ pourrait être basée sur l'utilisation normale p50 ou p90 pendant le trafic d'affaires, puis ajustée après des tests de charge. Pour un service JVM, la mémoire doit inclure le tas, le métaspace, la mémoire native, les piles de threads, les tampons directs et la surcharge du sidecar. Pour un travail par lots, la mémoire de pointe pendant la plus grande entrée peut être plus importante que la mémoire moyenne.

Classes de qualité de service (QoS)

La relation entre les demandes et les limites détermine la classe de qualité de service (QoS) attribuée à un Pod. Cette classe dicte la priorité du Pod lorsque les ressources deviennent rares et que le nœud doit récupérer de la mémoire (éviction).

Kubernetes définit trois classes QoS :

1. Garantie (Guaranteed)

Définition : Tous les conteneurs du Pod doivent avoir des demandes et des limites identiques et non nulles pour à la fois le CPU et la mémoire.

  • Avantage : Ces Pods sont les derniers à être évincés en cas de pression sur les ressources, assurant une stabilité maximale.
  • Cas d'utilisation : Composants système critiques ou bases de données nécessitant un isolement strict des performances.

2. Burstable

Définition : Au moins un conteneur du Pod a des demandes définies, mais soit les demandes et les limites ne sont pas égales pour tous les conteneurs, soit certaines ressources ne sont pas limitées (bien que la définition de limites soit fortement recommandée).

  • Avantage : Permet aux conteneurs de dépasser leurs demandes, en utilisant la capacité inutilisée sur le nœud, jusqu'à leurs limites définies.
  • Priorité d'éviction : Évincés avant les Pods BestEffort, mais après les Pods Garantis.
  • Cas d'utilisation : La plupart des applications sans état standard où une légère variation de latence est acceptable.

3. BestEffort

Définition : Le Pod n'a aucune demande ou limite définie pour aucun conteneur.

  • Avantage : Aucun, autre que la simplicité.
  • Risque : Ces Pods sont les premiers candidats à l'éviction lorsque le nœud subit une pression mémoire. Ils peuvent également mal rivaliser pour le CPU car aucune demande n'a été déclarée.
  • Cas d'utilisation : Travaux par lots non critiques ou agents de journalisation qui peuvent être facilement redémarrés.

Stratégies d'optimisation pratiques

Une gestion efficace des ressources nécessite des mesures, des itérations et une planification minutieuse.

Stratégie 1 : Mesurer et définir les demandes avec précision

Les demandes doivent refléter la quantité de ressources dont l'application a besoin pour fonctionner de manière acceptable la plupart du temps. Si vous définissez des demandes trop élevées, vous gaspillez la capacité du cluster car l'ordonnanceur traite cette capacité comme déjà réservée. Si vous les définissez trop bas, l'ordonnanceur peut placer trop de pods sur un nœud, et la charge de travail peut être plus susceptible de souffrir en cas de contention.

Utilisez des outils de surveillance tels que Prometheus et Grafana pour comparer les valeurs de demande avec l'utilisation réelle. Un point de départ courant consiste à examiner plusieurs jours de trafic normal, à ignorer les incidents ponctuels évidents et à définir les demandes près d'un percentile soutenu plutôt que le pic le plus élevé. Le percentile exact est un choix de politique ; le point principal est d'utiliser des données et de les revoir.

Par exemple, si un service utilise normalement 180m CPU, atteint un pic d'environ 450m pendant l'échauffement du déploiement et a des pics rares à 900m lors d'une tâche par lots connue, définir une demande de 900m peut gaspiller de la capacité toute la journée. Définir une demande de 50m peut rendre le pod bon marché à ordonnancer mais instable en cas de contention. Une demande dans la plage soutenue normale, plus un traitement séparé pour le chemin par lots, est souvent une meilleure conversation.

Stratégie 2 : Définir des limites conservatrices

Les limites agissent comme une frontière de sécurité, mais elles ne sont pas gratuites. Pour la mémoire, une limite légèrement supérieure au pic d'utilisation mesuré peut empêcher un conteneur de consommer le nœud. Pour le CPU, une limite empêche un processus incontrôlé d'utiliser un CPU illimité, mais des limites agressives peuvent limiter un service même lorsque le nœud a des cœurs inactifs.

Avertissement sur les limites CPU : Définir des limites CPU en dessous de la demande réelle peut provoquer une latence visible via l'étranglement. La QoS Burstable est un bon choix pour de nombreux services sans état, tandis que la QoS Garantie est mieux réservée aux charges de travail où le compromis d'isolement est intentionnel.

Stratégie 3 : Tirer parti de l'autoscaler vertical de pods (VPA)

Le réglage manuel des ressources est difficile et prend du temps. L'autoscaler vertical de pods (VPA) surveille l'utilisation en cours d'exécution et peut recommander ou mettre à jour les demandes de ressources, selon son mode. Dans de nombreuses configurations, le VPA est d'abord utilisé en mode recommandation afin que les équipes puissent examiner les demandes suggérées avant d'autoriser les mises à jour automatiques.

Soyez prudent lorsque vous combinez le VPA avec l'autoscaler horizontal de pods. Le HPA évolue souvent en fonction de l'utilisation par rapport aux demandes, donc modifier les demandes peut changer le comportement de mise à l'échelle. Cela peut bien fonctionner, mais cela doit être testé délibérément.

Stratégie 4 : Quotas de ressources pour les espaces de noms

Pour empêcher l'accaparement des ressources entre les équipes ou les environnements, les administrateurs doivent utiliser des quotas de ressources au niveau de l'espace de noms. Un ResourceQuota impose des limites globales sur la quantité totale de demandes et de limites CPU/Mémoire qui peuvent exister dans cet espace de noms, garantissant l'équité.

Exemple de quota d'espace de noms

apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-quota
  namespace: development
spec:
  hard:
    requests.cpu: "10"
    limits.memory: "20Gi"

Cela garantit que le CPU total demandé sur tous les Pods dans l'espace de noms development ne peut pas dépasser 10 cœurs, et les limites mémoire totales ne peuvent pas dépasser 20Gi.

Comment les mauvais réglages se manifestent dans les clusters réels

Les erreurs de ressources s'annoncent rarement comme des "erreurs de ressources". Elles ressemblent généralement à des incidents d'application.

Si les limites CPU sont trop basses, vous pouvez observer une latence de requête élevée tandis que les graphiques d'utilisation CPU semblent plafonnés. Le conteneur veut plus de CPU, mais l'étranglement cgroups le retient. Dans les configurations basées sur Prometheus, des métriques telles que container_cpu_cfs_throttled_periods_total et container_cpu_cfs_periods_total peuvent aider à montrer si l'étranglement fait partie de l'histoire.

Si les limites mémoire sont trop basses, le pod peut redémarrer avec Reason: OOMKilled. Les journaux d'application peuvent se terminer brusquement car le processus n'a pas eu d'arrêt gracieux. kubectl describe pod dit généralement la vérité plus rapidement que le journal d'application dans ce cas.

Si les demandes sont trop élevées, les pods peuvent rester en Pending même si les tableaux de bord montrent une utilisation moyenne des nœuds faible. L'ordonnanceur ne place pas les pods en fonction de l'utilisation réelle moyenne ; il compare les ressources demandées avec les ressources allouables. C'est pourquoi un cluster peut sembler sous-utilisé et pourtant refuser un nouveau pod.

Si les demandes sont manquantes, la charge de travail peut sembler correcte pendant les périodes calmes, puis devenir la première chose à être compressée lorsque le nœud est occupé. Cela peut être acceptable pour des travaux jetables, mais c'est un mauvais défaut pour les services destinés aux utilisateurs.

Un workflow de réglage plus sûr

Une boucle de réglage pratique ressemble à ceci :

  1. Commencez par des demandes CPU et mémoire explicites pour chaque conteneur de production, y compris les sidecars.
  2. Définissez les limites mémoire en fonction du pic observé plus une marge, puis surveillez les redémarrages OOMKilled.
  3. Décidez si les limites CPU sont requises par la politique ou le risque de la charge de travail. Si vous les utilisez, surveillez l'étranglement.
  4. Comparez le CPU et la mémoire demandés avec l'utilisation réelle chaque semaine ou chaque mois, en particulier après les versions majeures.
  5. Traitez le redimensionnement comme une gestion du changement. Une demande plus faible peut augmenter la densité de regroupement, mais elle peut également modifier le comportement en cas de défaillance lors de la pression sur le nœud.

Les sidecars méritent de l'attention. Un proxy de maillage de services, un expéditeur de journaux ou un agent de sécurité peut consommer suffisamment de CPU ou de mémoire pour modifier l'empreinte réelle du pod. Si seul le conteneur d'application principal est réglé, le pod peut toujours être mal représenté à l'ordonnanceur.

Exemple : Correction d'un pic de latence causé par l'étranglement CPU

Imaginez un conteneur API avec cette configuration :

resources:
  requests:
    cpu: "100m"
    memory: "256Mi"
  limits:
    cpu: "200m"
    memory: "512Mi"

Lors d'un événement de vente, la latence augmente. Le nœud a encore du CPU inactif, mais le conteneur est plafonné à 200m. Augmenter les répliques peut aider, mais chaque pod est toujours individuellement limité. Une meilleure solution pourrait être d'augmenter ou de supprimer la limite CPU, d'augmenter la demande pour correspondre à la demande soutenue normale, et d'utiliser le HPA pour que le service évolue avant que la latence ne devienne laide.

La leçon importante est qu'une faible utilisation CPU dans le graphique ne signifie pas toujours que l'application est inactive. Cela peut signifier que l'application n'est pas autorisée à en utiliser plus.

Vérification finale

Les demandes indiquent à Kubernetes comment placer et prioriser le pod. Les limites indiquent au noyau où l'arrêter. De bonnes valeurs proviennent de métriques réelles, de tests de charge et d'une décision claire sur ce qui compte le plus pour chaque charge de travail : densité, isolement, latence ou coût. Revenez sur les valeurs après les changements de trafic, les changements de dépendances et les mises à niveau d'exécution. Les paramètres de ressources obsolètes sont l'un des moyens les plus silencieux par lesquels un cluster dérive vers de mauvaises performances.