Ezt a félreértést rendszeresen látom csapatoknál. Beállítanak CPU limitet, az app elkezd furán viselkedni, és mindenki azt nézi, hogy újraindult-e a pod. Nem indul újra. Az a memóriás sztori.

A CPU limit nem öli meg a podot, hanem fojtja. Throttling lesz belőle. És ez a különbség sokkal fontosabb, mint elsőre tűnik.

A félreértés

Sokan így gondolkodnak:

  • Memória limit túllépve → pod meghal (OOMKill) ✅
  • CPU limit túllépve → pod meghal ❌

Az első igaz. A második nem. A hivatalos Kubernetes dokumentáció teljesen egyértelmű:

A CPU limiteket CPU throttlinggal érvényesíti a rendszer. Amikor egy konténer megközelíti a CPU limitjét, a kernel korlátozza a CPU hozzáférést. A konténerek nem használhatnak több CPU-t, mint amit a limitjük meghatároz.

A memória limiteket a kernel OOM (out of memory) kill-lel érvényesíti.

Két külön mechanizmus. CPU esetén lassítás, memória esetén kill.

Hogyan működik valójában a request és a limit

Nézzük külön a kettőt, mert teljesen más problémát oldanak.

resources:
  requests:
    cpu: "500m"
  limits:
    cpu: "1000m"

A CPU request (500m = fél mag) a schedulernek szól. A Kubernetes ezt CFS shares-re fordítja a cpu.shares cgroup beállításon át. Ez arányos rendszer, nem kemény plafon. Ha van szabad CPU a node-on, a konténer simán mehet a request fölé.

A CPU limit (1000m = 1 teljes mag) már kemény korlát. A kernel CFS bandwidth control-lal érvényesíti, a háttérben cfs_quota_us és cfs_period_us értékekkel. Ha a node-on üresen állnak magok, akkor sem mehetsz a limit fölé.

Röviden: request = foglalás és arányos osztozás. Limit = plafon és kényszerítés.

CFS és a throttling a gyakorlatban

A Kubernetes Linuxon a Completely Fair Scheduler (CFS) bandwidth control mechanizmust használja a CPU limit betartatására. A kernel dokumentáció alapján:

  • A CFS periódusokban dolgozik, alapból cfs_period_us 100 000 mikroszekundum (100ms)
  • 1000m limitnél a cfs_quota_us 100 000µs periódusonként
  • 500m limitnél 50 000µs periódusonként
  • Ha elfogy a kvóta, a szálak a következő periódusig throttlingolva vannak

Képzeld el, jön egy traffic spike. Az app gyorsan elégeti az 50ms CPU-t az első 30ms-ben, aztán vár 70ms-et. Nem omlik össze, csak megakad. A felhasználó ebből annyit lát, hogy random belassul minden.

A multi-core csapda

Itt kezd igazán fájni a történet.

Egy nagy állásportál mérnökei feltártak egy komoly CFS throttling hibát Linux v4.18-ban (512ac999 commit). A bug miatt akkor is túlthrottling történt, amikor a konténer még nem is használta ki rendesen a kvótáját.

A CFS egy globális kvóta poolból oszt szeleteket (alapból 5ms) a CPU magoknak. Ha egy magon marad futási idő, a kernel visszateszi a poolba, de 1ms-t bent hagy minden magon a lock contention csökkentésére.

2 magon ez semmi. 88 magon már brutális lehet: akár 87ms kvóta ragadhat be periódusonként, ami kb. 870 millicore elveszett kapacitás. Náluk a worst-case válaszidő 2+ másodpercről 30ms-re esett a javítás után.

A fix bekerült Linux 5.4+ környékére, és több régebbi ágba is visszaportolták. Ha régi kernel fut, simán benne lehetsz ebben úgy, hogy közben minden dashboard “zöld”.

Egy utazástechnológiai cég hasonló sztorit írt le: random megakadások, health check hibák, megszakadó kapcsolatok. A végén ugyanoda jutottak: agresszív CFS throttling.

Az alattomos tünetek

Ez benne a legidegesítőbb: nem ott látszik, ahol várnád.

Amit látsz dashboardon:

  • CPU 40-50%
  • Nincs pod restart
  • Nincs OOMKill
  • Látszólag minden oké

Amit a user érez:

  • p99 latency tüskék
  • Időszakos timeoutok
  • Lassú API válaszok
  • Health check hibák
  • 10ms-es endpointból néha 200ms

Az átlag CPU rendben van, mert időben kisimítod az adatot. Közben 100ms-es ablakokon belül újra és újra falba vered a konténert.

A Kubernetes issue #67577 is ezt mondja: “A CFS kvóták szükségtelen throttlinghoz vezethetnek.” Ismert jelenség, nem egyedi balszerencse.

Hogyan mérd ki

A kulcs metrika: container_cpu_cfs_throttled_periods_total.

rate(container_cpu_cfs_throttled_periods_total{container!=""}[5m])
/
rate(container_cpu_cfs_periods_total{container!=""}[5m])

Ha ez 0 fölött van, throttling van. 20-25% fölött általában már user impact is van.

kubectl-lel is gyorsan le tudod kérdezni. Cgroup v1 esetén:

# CFS statisztikák egy futó konténerhez
kubectl exec -it <pod-name> -- cat /sys/fs/cgroup/cpu,cpuacct/cpu.stat

Cgroup v2 esetén (mai disztrók többsége):

kubectl exec -it <pod-name> -- cat /sys/fs/cgroup/cpu.stat

Ezt a három mezőt nézd:

  • nr_periods - összes periódus, ahol volt futtatható szál
  • nr_throttled - hány periódusban fogyott el a kvóta
  • throttled_time - összes throttling idő (ns)

Ha az nr_throttled tempósan nő az nr_periods-hoz képest, akkor a konténered csendben büntetve van.

A nyers CFS beállításokat is nézd meg:

# cgroup v1
kubectl exec -it <pod-name> -- cat /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us
kubectl exec -it <pod-name> -- cat /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us

# cgroup v2
kubectl exec -it <pod-name> -- cat /sys/fs/cgroup/cpu.max

Miért dobják ki sokan a CPU limitet

Egyre több csapat request-only modellt használ. CPU request van, CPU limit nincs.

Miért működik ez jól:

  1. A request már ad garanciát versengésnél. A cpu.shares fair osztozást ad terhelés alatt.
  2. A limit kidobja a szabad kapacitást az ablakon. Ha van üres CPU, miért ne dolgozhatna az app? A Robusta cikk jó hasonlata: mintha lenne plusz víz, de nem engeded meginni, mert lejárt a napi keret.
  3. A throttling kiszámíthatatlan latencyt csinál. Sok rendszerben ez rosszabb, mint a magasabb átlag CPU.

Tim Hockin, a Kubernetes egyik eredeti maintainer-e régóta ezt javasolja.

Egy ilyen deployment tipikusan így néz ki:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      containers:
      - name: my-app
        resources:
          requests:
            cpu: "500m"
            memory: "256Mi"
          limits:
            memory: "512Mi"
            # Nincs CPU limit - szándékosan kihagyva

Memória limit maradjon. Az fontos a stabilitáshoz és az OOM káosz elkerüléséhez. CPU-nál viszont gyakran jobb a rugalmasabb működés.

QoS mellékhatás: ha kiveszed a CPU limitet, a pod Guaranteed helyett Burstable lesz. Ez számít node pressure és eviction alatt. Ha szigorú latency kell, nézd meg a static CPU Manager policy opciót dedikált magokhoz.

Kubernetes 1.34+ újdonság: a PodLevelResources (beta) már pod szinten is enged CPU és memória budgetet adni. Ez sidecar-heavy podoknál javíthatja a kihasználtságot, mert a konténerek rugalmasabban osztoznak.

Mikor maradjon CPU limit

Nem mindenhol jó ötlet kivenni. Tartsd meg, ha:

  • Multi-tenant klaszter van, és nem bízol a szomszéd workloadban
  • Szigorú költség- vagy chargeback szabály van
  • Batch/háttér jobok könnyen felfalnák az összes szabad CPU-t
  • Compliance/policy miatt kötelező erőforrás-korlát kell
  • Noisy neighbor jellegű problémát kell megelőzni

Egycsapatos, latency-érzékeny szolgáltatásnál viszont nagyon erős jelölt a request-only modell.

CFS burst röviden

Linux 5.14+ hozta a CFS burst funkciót a cpu.cfs_burst_us mezővel. Ennek lényege, hogy a cgroup félreteheti a korábban el nem használt kvótát, és spike-nál felhasználhatja.

Ez enyhítheti a throttlingot bursty terhelésnél, cserébe kicsit nőhet az interferencia más cgroupokkal.

Kubernetesben ez jelenleg nincs szépen kivezetve pod spec szintre, de alacsony szinten cgroupon keresztül hangolható.

Egy valós debugging session

Így nézne ki nálam egy gyors kivizsgálás:

# 1. lépés: Van-e throttlingolt pod?
kubectl top pods -n my-namespace

# 2. lépés: Nézd meg a tényleges resource specifikációt
kubectl get pod <pod-name> -o jsonpath='{.spec.containers[*].resources}' | jq .

# 3. lépés: Ellenőrizd a CFS throttlingot Prometheusban
# Használd a fenti PromQL lekérdezést, vagy állíts be alertet:
# alert: CPUThrottlingHigh
# expr: rate(container_cpu_cfs_throttled_periods_total[5m])
#       / rate(container_cpu_cfs_periods_total[5m]) > 0.25

# 4. lépés: Ellenőrizd a kernel verziót (jelen van-e a CFS javítás?)
kubectl exec -it <pod-name> -- uname -r
# Az 5.4 előtti kernelek tartalmazhatják a kvóta-beragadás hibát

# 5. lépés: Ha throttlingol, emeld a limitet vagy távolítsd el
kubectl patch deployment my-app -n my-namespace --type=json \
  -p='[{"op": "remove", "path": "/spec/template/spec/containers/0/resources/limits/cpu"}]'

# 6. lépés: Figyeld, ahogy a latency szinte azonnal javul

Az “aha” pillanat általában az, amikor leveszed a CPU limitet, és a p99 pár percen belül látványosan beesik. Kódhoz nem nyúltál. Infrát nem cseréltél. Csak kivettél egy mesterséges szűk keresztmetszetet.

Valós sztori: CoreDNS és a csendes összeomlás

Ez a történet még mindig fáj egy kicsit.

Heteken át halogattunk egy Kubernetes frissítést egy production EKS klaszteren. Amikor végre nekiálltunk, a frissítés része volt, hogy az AWS-managed CoreDNS addonról átálltunk self-managed Helm chartra. Nem tűnt nagy dolognak.

A Helm chart alapértelmezett CPU limittel jött a CoreDNS-re. Nem vettük észre.

Pár perccel a váltás után minden összeomlott. A DNS feloldás megállt, a szolgáltatások nem találták egymást, kaszkád hibák léptek fel az összes mikroszervizen a klaszterben. Teljes production kiesés.

Gyorsan revertáltunk. Gyorsabban, mint ahogy a Prometheus alerting egyáltalán ki tudott volna szólni. A throttling alert be volt állítva, de az incidens annyira rövid és éles volt, hogy előbb reagáltunk, mint ahogy tüzelhetett volna.

A post-mortem mindent megmutatott. Amikor előhúztuk a container CPU dashboardot Grafanában, egyértelmű volt a kép. A CoreDNS keményen ütközött a CPU limitbe, a CFS throttling berúgott, a DNS latency az egekbe szökött. Minden szolgáltatás, ami hostnamet akart feloldani (vagyis az összes), timeoutolni kezdett.

A javítás egyszerű volt: levettük a CPU limitet a CoreDNS-ről. A DNS olyan infrastruktúra, ahol még az apró throttling késleltetések is megsokszorozódnak a klaszter összes kérésén keresztül.

Tanulságok ebből:

  • Mindig ellenőrizd a resource alapértelmezéseket, amikor managed addonról Helm chartra váltasz. Az alapértékek nem mindig ésszerűek a te workloadodra.
  • A CoreDNS latency-kritikus infrastruktúra. CPU limit rajta képes egy egész klasztert levinni.
  • Az alerting küszöböknek gyors incidensekre is fel kell készülniük. Ha a csapat 2 perc alatt reagál, de az alert 5 perc tartós throttlingot vár, ott rés van.
  • A Grafana container dashboardok a post-mortem legjobb barátai. A CFS throttling metrikák pontosan megmutatták, mi történt, akkor is, ha az alertek nem kapták el időben.

Összefoglalás

  • CPU limit túllépésnél throttling van, nem kill. Memória limitnél jön az OOMKill.
  • A throttling nem látszik jól sima CPU százalékból. CFS metrikát kell nézni (container_cpu_cfs_throttled_periods_total).
  • Létezik egy dokumentált kernel probléma (pre-5.4), ami multicore gépeken túlthrottlingot okozhat.
  • CPU request és CPU limit két külön cél: cpu.shares arányos osztozás vs. CFS kvóta alapú plafon.
  • Sok production csapat ma már request-only CPU modellt használ, és a latency szempontjából ez gyakran jobb.
  • Lassulásnál előbb nézd meg a throttlingot, aztán kezdj hálózatot vagy adatbázist hibáztatni.

Ha az app indokolatlanul lassú, miközben minden “zöld”, első körben nézz rá a throttling metrikákra. Meglepően gyakran ott van a hiba.

További olvasnivaló