TL;DR Een e-commerceklant verhuisde een PHP-monoliet in oktober naar EKS, twee weken voor Black Friday, met resource-limits die gegokt waren op de oude serverspecs. Halverwege de ochtend liepen de pods tegen hun memory-limit aan, gingen vier uur lang in een loop dood, en de schatting was €240.000 aan gemiste omzet. De tweede helft gaat over hoe je die getallen uit loadtests en VPA-aanbevelingen haalt in plaats van ze te gokken.
“We willen voor november naar Kubernetes.”
Het was september. De klant was een e-commercebedrijf, en hun grootste verkoopdag van het jaar was Black Friday, eind november. Ik zei nee. Ze vroegen of ik iemand kende die het misschien toch wilde doen.
Die kende ik. Een collega-platform-engineer, iemand die ik respecteer en hoog heb zitten. Ik heb ze aan elkaar voorgesteld en hem gewaarschuwd voor de planning. Hij nam de opdracht aan, schreef dezelfde zorgen op die ik had, en liet ze aftekenen. De klant ging toch door.
Wat daarna gebeurde is zijn verhaal, hier verteld met zijn toestemming. Het is het duidelijkste voorbeeld dat ik ken van waarom je niet naar Kubernetes migreert zonder resource-metrics, en het raakt aan iets waar ik op dit blog steeds op terugkom. Over een systeem dat je niet begrijpt neem je geen goede beslissingen, en metrics zijn hoe je het begrijpt. Eerst wat hij zag, daarna hoe ik de sizing zou aanpakken.
Wat er gebeurde op Black Friday
De klant had een monolithische PHP-applicatie op vier dedicated servers. Twee appservers, één databaseserver, één voor Redis en background jobs. Saai, en het had drie jaar lang Black Friday-verkeer aangekund.
Maar de CTO was naar een conferentie geweest. Kubernetes was de toekomst. Microservices waren de toekomst. Alles moest in containers en voor Black Friday naar EKS, om “beter te kunnen schalen.”
De CTO was nieuw, binnengehaald om innovatie aan te jagen. Het bedrijf draaide al bijna tien jaar op dezelfde tech-stack, winstgevend maar stilstaand. De board wilde modernisering, en de CTO had een succes nodig om te laten zien dat de investering het waard was.
Pure overmoed was die planning trouwens niet. Hun oude hostingcontract liep af op 1 december. De provider was overgenomen, de nieuwe eigenaren faseerden het platform uit, en verlengen wilden ze niet. Dus óf migreren voor Black Friday, óf maand-tot-maand op noodhosting tegen 3x de kosten en in december migreren, de ene maand waarin code freezes alles lastiger maken en elke storing rampzalig is.
De CTO maakte de rekensom. Een riskante migratie in oktober met twee weken om te stabiliseren voor Black Friday, tegenover een zekere geldstroom naar buiten en een nog riskantere migratie in december. De CFO tekende. De CEO tekende. Een berekend risico, zou ik het noemen, geen roekeloosheid. Alleen rekenden ze zonder het ene getal dat ertoe deed, want dat had niemand gemeten.
Waar hij ze voor waarschuwde
Mijn collega heeft vier dingen opgeschreven voordat hij begon.
Ze hadden geen metrics. Basale Nagios-monitoring, dus CPU, memory en disk per server, en niks op applicatieniveau. Geen latency-histogrammen per request, geen idee hoe resourcegebruik meebewoog met verkeer.
Ze hadden de applicatie nooit loadgetest. Hun enige datapunt was “vorig jaar heeft ie Black Friday overleefd.”
Er was geen tijd voor fatsoenlijke sizing. Kubernetes resource requests en limits zijn getallen, en zonder metrics zouden die getallen gokken zijn.
En er was geen rollback-plan. De oude infrastructuur werd ontmanteld. Als Kubernetes omviel, was er niks om op terug te vallen.
Hij adviseerde om het hostingcontract te verlengen, ook tegen die hogere prijs, om tijd te kopen voor een fatsoenlijke aanloop. Dat wilden ze niet. De som was gemaakt, de directie had getekend, en het besluit stond.
Wat ze deployden
Zonder metrics deden ze een onderbouwde gok op basis van de bestaande serverspecs:
# Wat ze deployen (DOE DIT NIET)
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
Waarom überhaupt CPU-limits? Mijn collega kent de argumenten ertegen. Het antwoord was beleid. Het platformteam van de klant eiste dat elke deployment zowel requests als limits opgaf voor elke resource, en die regel kwam van het vorige bedrijf van de CTO, waar wekenlang een cryptominer op de servers had gedraaid voordat iemand het doorhad. CPU op alles begrenzen was een van de eerste regels die de CTO na aantreden invoerde, zodat een rogue process nooit onbeperkt resources kon opvreten. Logisch, maar het gaat ervan uit dat je weet welke limits je moet zetten. Zonder historische data om mee terug te duwen, en met de klok die liep, ging mijn collega erin mee. Dat beleid plus de gegokte waarden is wat de dag erger maakte dan nodig was.
Ze zetten een Horizontal Pod Autoscaler (HPA) op CPU-utilization:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
minReplicas: 4
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
Ik had ernaar gekeken en geknikt. Er staat niks fout in op zichzelf. Het klopt niet voor déze applicatie, en dat zie je niet aan de YAML.
Black Friday-ochtend
Het verkeer begon om 6 uur ’s ochtends te stijgen. Om 8 uur zaten ze op 3x normaal. De HPA deed z’n werk en de pods schaalden van 4 naar 12. Alles zag er goed uit.
Toen werd het 10 uur.
Het verkeer sprong naar 8x normaal. De HPA probeerde naar 20 pods te schalen. Wat niemand wist, was dat de applicatie memory-bound was. Elke pod liep tegen de memory-limit van 1Gi aan en werd OOMKilled. De HPA zag lage CPU-utilization, omdat de pods doodgingen voordat ze CPU konden gebruiken, en concludeerde dat alles prima was.
Nieuwe pods startten, laadden de PHP-opcache, warmden op, gingen verkeer serveren, kwamen zonder geheugen te zitten, gingen dood. En opnieuw. Ze hadden 20 pods die rondjes draaiden in CrashLoopBackOff.
Waar het geheugen heen ging
Het geheugengebruik van de applicatie hing sterk af van het type request:
- Categorie bekijken: ~200MB
- Product bekijken: ~300MB
- In winkelwagen leggen: ~400MB
- Checkout: ~800MB+
Op Black Friday was het checkout-verkeer 5x normaal. Elke pod probeerde checkout-requests af te handelen die 800MB nodig hadden, binnen een cap van 1Gi zonder enige marge.
Het werd erger. De CPU-requests stonden ook te laag, dus de pods die wél overleefden werden gethrottled. Responstijden gingen van 200ms naar 8 seconden. Gebruikers begonnen pagina’s te verversen. Meer requests, meer druk op het geheugen. Doodsspiraal.
De nasleep
- 4 uur verminderde dienstverlening tijdens de piekuren van Black Friday
- Geschatte gemiste omzet: €240.000
- Noodopschaling van de node-pool, die 20 minuten nodig had om te provisionen
- Handmatig ingrijpen om de memory-limits te verhogen, wat een redeployment betekende
- Een post-incident review met erg ongemakkelijke directieleden
De applicatie was prima. Kubernetes was prima. De configuratie was het probleem, en de configuratie was fout omdat niemand de data had om ’m goed te krijgen. Op dit stuk blijf ik hangen. De HPA schaalde. De scheduler plaatste pods. De kubelet killde pods die over hun memory-limit gingen. Elk onderdeel deed precies wat het opgedragen kreeg. Wat ontbrak was iemand die de workload goed genoeg kende om ze de juiste getallen te geven.
Sizen met data
Zo had ik de sizing aangepakt, met de tijd ervoor.
Instrumenteer voordat je migreert
Je kunt Kubernetes-resources niet sizen zonder te weten hoe je applicatie ze gebruikt. Verzamel minstens 2-4 weken aan metrics voordat je iets gaat plannen, en minimaal dit:
Voor memory:
- Baseline memory-gebruik in idle
- Working set memory onder load
- Piek-memory tijdens verkeerspieken
- Memory-gebruik per request-type, als dat varieert
Voor CPU:
- CPU-utilization onder normale load
- CPU-utilization tijdens pieken
- Request-latency bij verschillende CPU-niveaus
- Of de app CPU-bound of IO-bound is
Voor verkeerspatronen:
- Requests per seconde, RPS
- Latency-percentielen, p50, p95, p99
- Verkeerspatronen per moment van de dag en dag van de week
- Historische verkeerspieken
Eerst één container loadtesten
Voordat je Kubernetes aanraakt, loadtest je de applicatie in één losse container:
# Draai de app in een container met ruime limits
docker run -d --name load-test \
--memory 4g \
--cpus 2 \
-p 8080:8080 \
your-app:latest
# Monitor resource gebruik tijdens load test
docker stats load-test
Gebruik een loadtest-tool om realistisch verkeer na te bootsen:
# Voorbeeld met k6
k6 run --vus 100 --duration 10m load-test.js
Schrijf op:
- De memory high-water mark
- Het percentage CPU-utilization
- Het punt waar de performance inzakt
Requests en limits uit de getallen
Met de loadtest-data in de hand is het rekenwerk kort.
Voor memory:
requests.memory = p95 memory gebruik + 20% buffer
limits.memory = piek memory gebruik + 30% buffer
Voor CPU:
requests.cpu = gemiddelde CPU onder verwachte load
limits.cpu = piek CPU tijdens burst (of weglaten om bursting toe te staan)
Voor de e-commerceklant was dat hierop uitgekomen:
# Wat ze HADDEN MOETEN deployen
resources:
requests:
memory: "1Gi" # p95 was ~800MB
cpu: "500m" # Gemiddelde onder load
limits:
memory: "2Gi" # Piek tijdens checkout was ~1.5GB
# Geen CPU limit - sta bursting toe
Nog iets over die ontbrekende CPU-limit. Ik ben er voorstander van om er geen te zetten. CPU is compressible, dus als je erdoorheen gaat word je gethrottled. Memory niet, dus als je erdoorheen gaat word je gekilld. Het Google SRE book en veel practitioners raden CPU-limits in de meeste gevallen af.
Laat VPA het rightsizen doen
De Vertical Pod Autoscaler (VPA) kijkt naar het werkelijke resourcegebruik en beveelt betere resource-specificaties aan, of past ze voor je toe.
Installeer VPA:
# Clone de autoscaler repo
git clone https://github.com/kubernetes/autoscaler.git
cd autoscaler/vertical-pod-autoscaler
# Installeer VPA componenten
./hack/vpa-up.sh
Maak een VPA-resource aan in “Off”-mode, zodat je aanbevelingen krijgt zonder dat ze worden toegepast:
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: web-app-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
updatePolicy:
updateMode: "Off" # Alleen adviseren, niet toepassen
Na een paar dagen bekijk je de aanbevelingen:
kubectl describe vpa web-app-vpa
Output:
Recommendation:
Container Recommendations:
Container Name: web-app
Lower Bound:
Cpu: 200m
Memory: 800Mi
Target:
Cpu: 450m
Memory: 1200Mi
Upper Bound:
Cpu: 800m
Memory: 2Gi
De “Target”-waarden zijn wat VPA als requests aanbeveelt. Neem die als baseline en tel er marge bij op voor de limits.
HPA en VPA samen, voorzichtig
HPA en VPA kunnen elkaar in de weg zitten. HPA schaalt horizontaal op metrics, en VPA verandert de resource requests. Verhoogt VPA de requests, dan kan HPA besluiten dat de pods onderbenut zijn en terugschalen.
De aanpak die niet bijt is VPA in “Off”-mode draaien voor de aanbevelingen, die met de hand toepassen als baseline, HPA het horizontale schalen laten doen op custom metrics in plaats van alleen CPU, en de VPA-aanbevelingen af en toe opnieuw bekijken.
Voor de e-commerceklant had een betere HPA er zo uitgezien:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
minReplicas: 8 # Hoger minimum voor Black Friday
maxReplicas: 50 # Meer marge
metrics:
- type: Resource
resource:
name: memory # Schaal op memory, niet alleen CPU
target:
type: Utilization
averageUtilization: 70
- type: Pods
pods:
metric:
name: http_requests_per_second # Custom metric
target:
type: AverageValue
averageValue: "100"
De vragen die je beantwoordt voordat je migreert
Beantwoord deze vragen met data voordat je ook maar iets migreert:
| Vraag | Databron |
|---|---|
| Wat is de memory baseline van de applicatie? | Monitoring (Prometheus, Datadog, etc.) |
| Wat is piek memory onder load? | Load testing |
| Is de app CPU-bound of memory-bound? | Load testing + profiling |
| Wat is p99 latency bij verwachte load? | Load testing |
| Wat is het traffic patroon (dagelijks, wekelijks, seizoensgebonden)? | Historische metrics |
| Wat is de hoogste traffic spike in het afgelopen jaar? | Historische metrics |
| Hoe gedraagt de app zich bij 2x, 5x, 10x load? | Load testing |
Kun je ze niet beantwoorden, dan ben je niet klaar om te migreren. En soms is het antwoord dat je het niet moet doen, daar heb ik een aparte post over, When Not to Use Kubernetes.
De tweede poging
Na Black Friday ging de klant alsnog akkoord met de dure noodhosting die ze hadden willen vermijden. Drie keer de kosten, maar het kocht tijd. Mijn collega heeft december gebruikt om de migratie goed te doen.
Eerst alles geïnstrumenteerd. Prometheus, applicatie-metrics, custom metrics voor de checkout-flow. Daarna loadgetest tegen nagebootste Black Friday-verkeerspatronen, voordat er iets verhuisde. Ze begonnen met ruime resources, expres overprovisioned, en lieten VPA van daaruit rightsizen. Gevalideerd met canary-deployments, de nieuwe configuratie naast de oude, gedrag vergeleken. En ze hielden een weg terug open. De noodhosting bleef na de migratie nog 30 dagen beschikbaar als fallback.
De migratie zelf, eind december na de kerstdrukte, was saai. Er brak niks.
Waarom ik nee zei
Kubernetes lost geen schaalprobleem op dat je niet hebt gemeten. Het draait de getallen die je het geeft, gegokt of gemeten, net zo trouw op Black Friday-verkeer.
Elk getal in een resource-spec is een claim over hoe de applicatie zich gedraagt. Requests, limits, het HPA-target, allemaal. Gegokt uit serverspecs kan niemand ze onderbouwen. Gemeten zijn ze de reden dat de tweede poging saai was.
Ik zei in september nee omdat ik niet zag hoe iemand die getallen op tijd bij elkaar zou krijgen. Mijn collega nam de klus aan en kreeg ze op de dure manier. Het is zijn verhaal, en ik ben blij dat ik het mocht vertellen.
