Infrastructure

Azure AKS : diagnostiquer un HPA qui oscille avant d'augmenter les replicas

Un runbook de production pour séparer métrique bruitée, requests mal calibrées, démarrage lent et capacité nœud avant de modifier les bornes d'un Horizontal Pod Autoscaler AKS.

29 sept. 2026 azureakskuberneteshpaautoscalingmetrics-serverprometheusobservabilitycapacitycanaryrunbookrollbackproduction

Une API sur AKS passe de 4 à 12 pods, redescend à 4 quelques minutes plus tard, puis remonte au pic suivant. Les latences se dégradent pendant le warm-up, les connexions aval se multiplient et l’équipe propose d’augmenter minReplicas ou maxReplicas pour stabiliser la production.

Cette oscillation ne prouve pas que la borne est trop basse. Le HPA peut suivre une métrique qui ne représente pas la demande, calculer l’utilisation sur des requests CPU irréalistes, attendre une métrique manquante ou demander des pods que le cluster ne peut pas planifier. Le cas fil rouge est un Deployment AKS autoscalé sur CPU, avec un démarrage de 90 secondes et une base de données qui supporte au plus 120 connexions applicatives. La sortie attendue est une décision : corriger le signal, borner la vitesse, ajouter de la capacité ou rollbacker la modification sans masquer le problème avec des replicas permanents.

Figer une chronologie avant de toucher aux bornes

Conservez une fenêtre UTC couvrant au moins deux cycles complets de montée et descente. Exportez le HPA, le Deployment, les événements, les pods et les nœuds. Notez la release, la version Kubernetes, le fournisseur de métriques et tout déploiement ou pic de trafic corrélé.

bash 01-capture-hpa-incident.sh
NS="checkout-prod"
HPA="checkout-api"
DEPLOY="checkout-api"

kubectl -n "$NS" get hpa "$HPA" -o yaml > hpa.yaml
kubectl -n "$NS" describe hpa "$HPA" > hpa.describe.txt
kubectl -n "$NS" get deploy "$DEPLOY" -o yaml > deployment.yaml
kubectl -n "$NS" get pods -l app=checkout-api -o wide > pods.txt
kubectl get nodes -o wide > nodes.txt
kubectl -n "$NS" get events --sort-by=.metadata.creationTimestamp > events.txt

Ne changez pas simultanément requests, cible CPU, fenêtres de stabilisation et bornes. Une oscillation disparaîtrait peut-être, mais sans indiquer quel contrat était faux. Stoppez l’analyse si un second contrôleur écrit aussi spec.replicas, par exemple un ScaledObject KEDA ou une automatisation de déploiement : deux boucles concurrentes ne se règlent pas avec un seuil différent.

Lire le HPA comme une boucle de contrôle

Le calcul de base compare la valeur courante à la cible puis applique le ratio au nombre de replicas. Pour une cible moyenne, l’ordre de grandeur est :

text hpa-ratio.txt
desiredReplicas = ceil(currentReplicas * currentMetric / targetMetric)

Exemple
replicas courants: 4
CPU moyen observe: 90 % des requests
cible: 60 %
recommandation brute: ceil(4 * 90 / 60) = 6

Le résultat visible n’est toutefois pas la formule seule. Les pods en initialisation, les métriques manquantes, la tolérance, les recommandations récentes, minReplicas, maxReplicas et les policies de behavior modifient ou retardent l’action. Avec plusieurs métriques, le HPA retient la recommandation la plus élevée ; si une métrique est indisponible alors que les autres demandent une baisse, il peut refuser de réduire.

Lisez status.conditions, currentMetrics, desiredReplicas, lastScaleTime et les événements. AbleToScale, ScalingActive et ScalingLimited répondent à des questions différentes : le contrôleur peut agir, la métrique est exploitable, et la recommandation rencontre ou non une limite.

Prouver le signal réellement consommé

Un graphique applicatif ne prouve pas ce que lit le contrôleur. Interrogez l’API de métriques depuis le cluster et comparez-la au statut du HPA au même instant.

bash 02-read-hpa-signal.sh
NS="checkout-prod"
HPA="checkout-api"

kubectl -n "$NS" get hpa "$HPA" -o jsonpath='{range .status.currentMetrics[*]}{.type}{"	"}{.resource.name}{"	"}{.resource.current.averageUtilization}{"
"}{end}'

kubectl get --raw '/apis/metrics.k8s.io/v1beta1/namespaces/'"$NS"'/pods' | jq -r '.items[] | select(.metadata.labels.app == "checkout-api") | [.metadata.name,.containers[].usage.cpu] | @tsv'

kubectl -n "$NS" top pods -l app=checkout-api --containers

Pour une métrique custom ou externe, interrogez respectivement custom.metrics.k8s.io ou external.metrics.k8s.io. Vérifiez nom, sélecteur, unité, fraîcheur et cardinalité. Une série agrégée sur le mauvais label peut faire scaler un service depuis la charge d’un autre ; une valeur cumulée utilisée comme un débit monte sans jamais représenter la demande instantanée.

Si kubectl top est vide ou intermittent, inspectez Metrics Server et ses erreurs avant de retoucher le HPA. Pour un adapter Prometheus, contrôlez la règle de mapping, la requête générée et le délai entre collecte et exposition. L’objectif est de relier chaque recommandation à une valeur reproductible, pas seulement à une courbe visuellement proche.

Vérifier les requests et l’enveloppe par replica

Une cible averageUtilization CPU est calculée par rapport aux requests. Si un pod demande 100m mais consomme normalement 80m, le HPA voit déjà 80 %, même si le nœud reste largement disponible. À l’inverse, une request surdimensionnée peut retarder une montée utile.

bash 03-compare-requests-and-usage.sh
NS="checkout-prod"
DEPLOY="checkout-api"

kubectl -n "$NS" get deploy "$DEPLOY" -o jsonpath='{range .spec.template.spec.containers[*]}{.name}{"	request="}{.resources.requests.cpu}{"	limit="}{.resources.limits.cpu}{"
"}{end}'

kubectl -n "$NS" top pods -l app=checkout-api --containers
kubectl -n "$NS" get pods -l app=checkout-api -o jsonpath='{range .items[*]}{.metadata.name}{"	ready="}{.status.containerStatuses[0].ready}{"	restarts="}{.status.containerStatuses[0].restartCount}{"
"}{end}'

Calibrez les requests depuis une distribution observée et un test de charge, pas depuis un snapshot isolé. Mesurez aussi la capacité métier d’un replica : requêtes par seconde, concurrence, latence, pool de connexions et coût du warm-up. Douze pods avec vingt connexions chacun dépassent déjà la limite aval du cas fil rouge, même si le cluster peut les exécuter.

Séparer oscillation, démarrage et manque de nœuds

Une montée saine peut ressembler à une oscillation si les nouveaux pods deviennent Running mais restent non prêts. Pendant le warm-up, les anciens pods portent la charge ; la métrique reste haute et le HPA demande encore des replicas. Quand tous deviennent prêts, la moyenne chute brutalement et déclenche la baisse.

Contrôlez les startupProbe et readinessProbe, la durée réelle jusqu’à Ready, le chargement des caches et les erreurs de dépendances. Un pod ne doit pas recevoir du trafic avant d’être prêt, mais son initialisation ne doit pas non plus rester invisible au budget de montée.

Comparez ensuite desiredReplicas, replicas disponibles et pods Pending. Si le HPA demande 12 pods mais que 4 restent Pending, le problème se situe dans le scheduling ou la capacité nœud : requests trop élevées, affinité, taints, quota, limites de node pool ou délai du Cluster Autoscaler. Relever maxReplicas ne crée aucune capacité.

Reconstruire les cycles avec Prometheus

Les noms exacts dépendent de votre export kube-state-metrics. Le tableau utile superpose recommandation, état réel, disponibilité et signal métier.

promql 04-hpa-oscillation.promql
# Ecart entre recommandation et replicas courants
kube_horizontalpodautoscaler_status_desired_replicas{
namespace="checkout-prod", horizontalpodautoscaler="checkout-api"
}
-
kube_horizontalpodautoscaler_status_current_replicas{
namespace="checkout-prod", horizontalpodautoscaler="checkout-api"
}

# Variation absolue de la recommandation sur 15 minutes
changes(kube_horizontalpodautoscaler_status_desired_replicas{
namespace="checkout-prod", horizontalpodautoscaler="checkout-api"
}[15m])

# Pods indisponibles du Deployment
kube_deployment_status_replicas_unavailable{
namespace="checkout-prod", deployment="checkout-api"
}

Ajoutez CPU utilisé et demandé, durée jusqu’à Ready, pods Pending, latence p95, taux d’erreur, débit et saturation aval. Une oscillation est importante lorsqu’elle dégrade le service ou consomme une ressource limitée ; le seul changement de nombre de pods n’est pas un verdict.

Canaryer un comportement, pas une valeur magique

Créez un HPA de canari sur un Deployment ou un namespace isolé avec le même profil de charge. Commencez par limiter la vitesse de descente et conserver une montée réactive. Le réglage ci-dessous est un exemple à mesurer, pas un défaut universel.

yaml 05-hpa-canary.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: checkout-api-canary
namespace: checkout-canary
spec:
scaleTargetRef:
  apiVersion: apps/v1
  kind: Deployment
  name: checkout-api-canary
minReplicas: 3
maxReplicas: 12
metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60
behavior:
  scaleUp:
    stabilizationWindowSeconds: 0
    policies:
      - type: Percent
        value: 100
        periodSeconds: 60
  scaleDown:
    stabilizationWindowSeconds: 600
    policies:
      - type: Pods
        value: 1
        periodSeconds: 60

Rejouez une rampe, un plateau, un pic bref et un retour au calme. Le canari doit respecter la latence cible, ne pas produire de pods Pending, rester sous le budget de connexions aval et revenir à sa base sans cycles répétés. Testez aussi la perte temporaire de métriques : le comportement doit être explicable et ne pas provoquer une baisse dangereuse.

Décider, valider ou rollbacker

Corrigez le signal lorsque la métrique ne représente pas la demande ou arrive trop tard. Recalibrez les requests lorsqu’elles faussent l’utilisation relative. Allongez la stabilisation de descente ou limitez sa vitesse lorsque les pics courts et le warm-up créent des cycles. Ajoutez de la capacité nœud uniquement si des pods utiles restent Pending et si les contraintes de scheduling sont intentionnelles.

Augmentez minReplicas lorsque la capacité minimale nécessaire à la disponibilité et au temps de démarrage est mesurée. Augmentez maxReplicas seulement après avoir prouvé que le cluster et les dépendances supportent ce nombre. Une borne plus haute sans budget aval déplace l’incident vers la base de données ou l’API partenaire.

Rollbackez si le canari augmente latence, erreurs, pods indisponibles, connexions aval ou coût sans réduire les cycles. Restaurez le manifeste HPA et les requests connus, arrêtez le canari, puis vérifiez que currentReplicas, desiredReplicas et replicas disponibles convergent pendant une fenêtre complète.

Conclusion

Un HPA qui oscille est une boucle de contrôle qui reçoit un signal, applique un contrat et agit sur une capacité finie. Le nombre de pods n’est que la sortie visible.

Figez la chronologie, prouvez la métrique, calibrez les requests, mesurez le warm-up et vérifiez le scheduling avant de changer les bornes. La décision devient alors défendable : corriger le signal, amortir la descente, fournir des nœuds, relever une limite prouvée ou revenir au dernier comportement stable.