Kubernetes Horizontal Pod Autoscaler (HPA): pods schalen op basis van metrics

De Horizontal Pod Autoscaler voegt replicas toe of verwijdert ze op basis van metrics naar keuze: CPU-gebruik, geheugendruk, request rates uit Prometheus, of externe signalen zoals queue depth. Deze guide loopt door het configureren van HPA v2 voor elk van die scenario's, het tunen van het stabilisatievenster zodat opschalen snel gaat en afschalen voorzichtig, en het vermijden van het conflict dat ontstaat wanneer HPA en VPA dezelfde resource aansturen.

Wat je hebt aan het einde

Een set HPA-configuraties die een Deployment schalen op CPU-gebruik, geheugen en custom Prometheus-metrics. Je begrijpt het schalingalgoritme, weet hoe je het stabilisatievenster per richting instelt, en hebt een werkende Prometheus Adapter-regel die applicatiemetrics doorsluist naar de HPA control loop.

Vereisten

  • kubectl verbonden met een Kubernetes 1.27+ cluster (1.27 promoveerde HPAContainerMetrics naar beta, waardoor per-container schaling standaard werkt)
  • metrics-server geinstalleerd en werkend (kubectl top pods toont waarden, geen error: Metrics API not available)
  • Een Deployment met resource requests op alle containers, inclusief geinjecteerde sidecars. HPA berekent utilization als huidig gebruik / request, dus zonder requests zie je <unknown>
  • Voor de custom metrics secties: Prometheus draaiend in het cluster en de Prometheus Adapter Helm chart geinstalleerd

Het schalingalgoritme

HPA draait als control loop in kube-controller-manager en synchroniseert standaard elke 15 seconden. Elke cyclus berekent het:

desiredReplicas = ceil(currentReplicas * (currentMetricValue / desiredMetricValue))

Als de huidige gemiddelde CPU 200m is en het target 100m, is de ratio 2.0 en verdubbelt HPA het replicacount. Zakt het gebruik naar 50m, dan is de ratio 0.5 en halveert het.

Een ingebouwde tolerantieband van +/-10% voorkomt flapping. Valt de ratio tussen 0.9 en 1.1, dan onderneemt HPA geen actie. Kubernetes 1.33 introduceerde een alpha feature gate (HPAConfigurableTolerance) waarmee je deze tolerantie per HPA kunt overschrijven, maar op clusters ouder dan 1.33 zit je vast aan de 10%.

Bij meerdere metrics evalueert HPA elke metric onafhankelijk en pakt het maximum gewenste replicacount. Zo wordt aan alle constraints tegelijk voldaan.

CPU-gebaseerd schalen

CPU is een compressible resource: bereikt een container zijn limiet, dan throttlet de kernel het proces in plaats van het te killen. Dat maakt CPU-utilization het betrouwbaarste primaire schalingsignaal voor stateless services.

De HPA aanmaken

# hpa-cpu.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-app-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-app
  minReplicas: 2              # nooit minder dan 2 in productie
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70  # schaal uit wanneer gem. CPU boven 70% van requests komt

Toepassen en controleren:

kubectl apply -f hpa-cpu.yaml

kubectl get hpa web-app-hpa
# NAME           REFERENCE          TARGETS    MINPODS   MAXPODS   REPLICAS   AGE
# web-app-hpa    Deployment/web-app 45%/70%    2         20        3          5m

De TARGETS-kolom toont huidig%/target%. Staat er <unknown>/70%, dan ontbreekt er waarschijnlijk een resource request op een van de containers in de target Deployment.

Snel alternatief met kubectl

Voor niet-productieomgevingen kun je een HPA imperatief aanmaken:

kubectl autoscale deployment web-app --cpu-percent=70 --min=2 --max=20

Declareer HPA in productie altijd in versiebeheerde YAML. Imperatieve objecten hebben geen audit trail.

Geheugen-gebaseerd schalen (en waarom dat lastig is)

Geheugen is non-compressible. Overschrijdt een container zijn geheugenlimiet, dan killt de kernel het proces direct: lopende requests gaan verloren, caches zijn weg, en de vervangende pod start koud op. Extra pods toevoegen helpt alleen als de geheugendruk komt van gelijktijdige gebruikers, niet van een lek.

HPA pollt elke 15 seconden. Een plotselinge geheugenspike die een container in 2 seconden vult veroorzaakt een OOM kill voordat HPA het ook maar merkt. Daarom werkt geheugen-gebaseerde HPA als aanvullend signaal, niet als primair.

Conservatieve targets

Workloadtype Aanbevolen target
Stateless webapps 70-75%
Safety-critical services 65-70%
Batchverwerking 75-80%

De lagere targets laten ruimte voor pieken voordat de OOM killer ingrijpt.

Geheugen-HPA manifest

# hpa-memory.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: memory-scaler
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  minReplicas: 3
  maxReplicas: 15
  metrics:
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 75    # conservatief; ~25% headroom
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 120
      policies:
      - type: Percent
        value: 50
        periodSeconds: 120
      selectPolicy: Max
    scaleDown:
      stabilizationWindowSeconds: 600   # 10 minuten; erg voorzichtig
      policies:
      - type: Percent
        value: 10
        periodSeconds: 180
      selectPolicy: Min

De langere stabilizationWindowSeconds aan de scale-down kant voorkomt dat pods te vroeg worden verwijderd wanneer geheugengebruik tijdelijk daalt tussen request-batches.

Wat geheugen-HPA niet kan oplossen

Een memory leak groeit tot OOM, ongeacht het aantal replicas. Zie je pods consequent richting hun limiet klimmen en herstarten, dan zit de fix in de applicatiecode of in de garbage collector configuratie, niet in HPA. Voor het juist dimensioneren van memory requests op basis van waargenomen gebruikspatronen kun je kijken naar VPA (updateMode: "Off" voor alleen aanbevelingen).

Custom metrics via Prometheus Adapter

CPU en geheugen dekken veel workloads, maar business-level signalen zoals request rate, queue depth of p95 latency correleren vaak beter met de daadwerkelijke belasting die gebruikers ervaren. De Prometheus Adapter maakt Prometheus-metrics beschikbaar via de custom.metrics.k8s.io en external.metrics.k8s.io API's die HPA kan bevragen.

Architectuur

Applicatie --> /metrics endpoint --> Prometheus (scraped) --> Prometheus Adapter
  --> custom.metrics.k8s.io API --> HPA

Een adapterregel configureren

In de Helm values.yaml voor prometheus-community/prometheus-adapter definieer je een regel die een Prometheus-counter omzet naar een per-pod rate:

rules:
  default: false                  # schakel standaard CPU/memory-regels uit; metrics-server doet dat
  custom:
    - seriesQuery: 'http_requests_total{namespace!="",pod!=""}'
      resources:
        overrides:
          namespace: {resource: "namespace"}
          pod: {resource: "pod"}
      name:
        matches: "^(.*)_total"
        as: "${1}_per_second"     # beschikbaar als http_requests_per_second
      metricsQuery: |
        sum(rate(<<.Series>>{<<.LabelMatchers>>}[2m])) by (<<.GroupBy>>)

De metricsQuery berekent een 2-minuten rate. De placeholders <<.Series>>, <<.LabelMatchers>> en <<.GroupBy>> worden door de adapter ingevuld op querytijd.

Controleer of de metric bereikbaar is

# bevestig dat de API service is geregistreerd
kubectl get apiservice v1beta1.custom.metrics.k8s.io
# NAME                              SERVICE                                  AVAILABLE
# v1beta1.custom.metrics.k8s.io     monitoring/prometheus-adapter             True

# toon alle beschikbare custom metrics
kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1 | jq '.resources[].name'

# bevraag de specifieke metric voor pods in een namespace
kubectl get --raw \
  "/apis/custom.metrics.k8s.io/v1beta1/namespaces/production/pods/*/http_requests_per_second" \
  | jq '.items[].value'

Toont de API service False bij AVAILABLE, check dan de adapter pod logs: kubectl logs -n monitoring -l app.kubernetes.io/name=prometheus-adapter.

HPA op de custom metric richten

# hpa-custom.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-app-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "1000"    # target: 1000 req/s per pod

Je kunt resource metrics en custom metrics combineren in een enkele HPA. HPA evalueert ze allemaal en gebruikt het hoogste gewenste replicacount. Een gangbaar patroon is CPU als vangnet en request rate als primair signaal:

metrics:
- type: Resource
  resource:
    name: cpu
    target:
      type: Utilization
      averageUtilization: 70
- type: Pods
  pods:
    metric:
      name: http_requests_per_second
    target:
      type: AverageValue
      averageValue: "1000"

Scaling behaviour: stabilisatievensters en policies

Het behavior-veld (stabiel sinds autoscaling/v2) bepaalt hoe agressief HPA in elke richting schaalt. De defaults zijn bewust asymmetrisch: direct opschalen, 5 minuten wachten voor afschalen.

Het volledige schema

behavior:
  scaleUp:
    stabilizationWindowSeconds: 0       # reageer meteen op de laatste aanbeveling
    selectPolicy: Max                   # kies het beleid dat de meeste verandering toestaat
    policies:
    - type: Percent
      value: 100                        # verdubbel het huidige aantal per periode
      periodSeconds: 15
    - type: Pods
      value: 4                          # of voeg 4 pods toe per periode, wat meer is
      periodSeconds: 15
  scaleDown:
    stabilizationWindowSeconds: 300     # kijk 5 min terug; gebruik de hoogste aanbeveling
    selectPolicy: Min                   # kies het beleid dat de minste verandering toestaat
    policies:
    - type: Percent
      value: 10                         # verwijder max 10% per periode
      periodSeconds: 60

Hoe het stabilisatievenster werkt

Voor scale-down is het venster standaard 300 seconden. HPA kijkt naar alle desired-replica aanbevelingen van de afgelopen 300 seconden en pakt de hoogste. Dat betekent dat het geen pods verwijdert als er recentelijk een aanbeveling was die ze nodig had. Voor scale-up is de default 0 seconden: reageer direct.

Waarom asymmetrisch?

Te langzaam opschalen veroorzaakt latency spikes en timeouts. Te snel afschalen veroorzaakt cache churn, dropped connections op pods die net online kwamen, en onnodige pod restarts.

selectPolicy: Max bij scaleUp kiest het beleid dat de meest agressieve reactie toestaat. selectPolicy: Min bij scaleDown kiest het meest conservatieve. Dat is de juiste default voor productie HTTP-services.

Afschalen uitschakelen

Tijdens een bekend traffic-event (Black Friday, productlancering) kun je voorkomen dat HPA pods verwijdert:

behavior:
  scaleDown:
    selectPolicy: Disabled

Verwijder dit na het event.

HPA + VPA interactieregels

De Vertical Pod Autoscaler (VPA) past resource requests aan. HPA past het replicacount aan op basis van de verhouding gebruik/requests. Sturen beide op dezelfde resource aan, dan ontstaat een feedbackloop: VPA verhoogt de CPU-request, de utilization ratio daalt, HPA schaalt af, de load per pod stijgt, VPA verhoogt de request opnieuw.

Veilig patroon: splits per resourcetype

Laat HPA CPU beheersen (replica scaling) en VPA geheugen (request sizing). Geen van beide beinvloedt het signaal van de ander:

# VPA: alleen geheugen
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
spec:
  resourcePolicy:
    containerPolicies:
    - containerName: app
      controlledResources: ["memory"]
# HPA: alleen CPU
metrics:
- type: Resource
  resource:
    name: cpu
    target:
      type: Utilization
      averageUtilization: 70

Alternatief: HPA met AverageValue in plaats van Utilization

Gebruik je type: AverageValue met een raw millicores target (averageValue: "200m"), dan hebben wijzigingen van VPA aan de request geen invloed op de schalingsbeslissing van HPA. Het target is immers een absoluut getal, geen ratio. Dit werkt, maar is lastiger te tunen: je moet de juiste absolute CPU-waarde per pod kennen, en die verandert bij applicatie-updates.

VPA in recommendation-only modus

Draai VPA met updateMode: "Off" zodat het data verzamelt en aanbevelingen doet zonder ze door te voeren. Gebruik die aanbevelingen om requests handmatig in te stellen en laat HPA vervolgens de replicas beheren zonder storing. Dit is de veiligste aanpak voor teams die VPA nog niet automatisch willen laten ingrijpen.

Eindresultaat controleren

Bevestig na het toepassen van je HPA dat deze actief is en metrics ontvangt:

kubectl describe hpa web-app-hpa

Let op drie condities:

Conditie Gezonde waarde Betekenis
AbleToScale True HPA kan de scale subresource lezen en aanpassen
ScalingActive True Metrics stromen; schaling is operationeel
ScalingLimited False Niet vastgelopen op minReplicas of maxReplicas

Is ScalingActive False, dan kan HPA geen metrics ophalen. Controleer of metrics-server draait (kubectl get deployment metrics-server -n kube-system) en of de target Deployment resource requests heeft op elke container.

Veelvoorkomende problemen

<unknown> in de TARGETS-kolom. De meest voorkomende oorzaak is een ontbrekend resources.requests-blok op een of meer containers, inclusief automatisch geinjecteerde sidecars van Istio of Linkerd. Op de tweede plaats: metrics-server is niet geinstalleerd, of faalt op TLS-verificatie tegen kubelets. Op kubeadm-clusters met self-signed certificaten lost het toevoegen van --kubelet-insecure-tls aan de metrics-server Deployment args dit op.

HPA schaalt niet af naar minReplicas. Dit is meestal correct gedrag. Het standaard stabilisatievenster is 300 seconden: HPA schaalt pas af na 5 volle minuten met consistent lagere aanbevelingen. Draai kubectl describe hpa en lees de Events-sectie om te zien wat HPA aanbeveelt.

Custom metric toont FailedGetPodsMetric. De Prometheus Adapter heeft geen regel die overeenkomt met de seriesnaam, of de PromQL in metricsQuery levert geen data op. Test de query eerst rechtstreeks in Prometheus: sum(rate(http_requests_total{namespace="production"}[2m])) by (pod).

Surge pods hangen in Pending tijdens opschalen. Het cluster heeft niet genoeg node-capaciteit. Combineer HPA met de Cluster Autoscaler of Karpenter om nieuwe nodes te provisioneren wanneer de pod-vraag de nodecapaciteit overschrijdt.

Voor queue-driven of scale-to-zero workloads: het minimum replicacount van HPA is 1. KEDA breidt HPA uit met 50+ native scalers en ondersteunt schalen naar nul, wat idle-pod kosten elimineert voor event-driven consumers.

Compleet multi-metric HPA manifest

Ter referentie: een productiewaardige HPA die CPU, geheugen en een custom metric combineert met getuned scaling behaviour:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-app-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-app
  minReplicas: 3
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 75
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "1000"
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15
      - type: Pods
        value: 4
        periodSeconds: 15
      selectPolicy: Max
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 10
        periodSeconds: 60
      - type: Pods
        value: 1
        periodSeconds: 60
      selectPolicy: Min

Terugkerende server- of deploymentproblemen?

Ik help teams productie betrouwbaar maken met CI/CD, Kubernetes en cloud—zodat fixes blijven en deploys geen stress meer zijn.

Bekijk DevOps consultancy

Doorzoek deze site

Begin met typen om te zoeken, of blader door de kennisbank en blog.