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
kubectlverbonden met een Kubernetes 1.27+ cluster (1.27 promoveerdeHPAContainerMetricsnaar beta, waardoor per-container schaling standaard werkt)- metrics-server geinstalleerd en werkend (
kubectl top podstoont waarden, geenerror: 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