Wydajność Kubernetes: limity, requests i autoscaling w praktyce

0
31
Rate this post

Nawigacja:

Po co w ogóle ruszać limity, requests i autoscaling?

Jakie problemy zwykle próbujesz ugasić?

Masz już działający klaster Kubernetes, aplikacje odpowiadają, ale coś zgrzyta. Raz latencja skacze w górę, innym razem pody padają z tajemniczym statusem OOMKilled, a pod koniec miesiąca rachunek za infrastrukturę zaskakuje bardziej niż pik ruchu w Black Friday. Brzmi znajomo?

Zastanów się, który z tych scenariuszy pasuje do twojej sytuacji najbardziej:

  • Zrywane requesty i time-outy – podów jest za mało lub wąskim gardłem staje się CPU, a autoscaling reaguje zbyt wolno.
  • OOM i restarty – aplikacje dostają za mało pamięci, albo limit pamięci jest sztucznie zaniżony, więc kernel zaczyna zabijać procesy.
  • Wysokie rachunki za klaster – requests ustawione „na wszelki wypadek”, przez co scheduler upycha mało podów na nodzie, a ty płacisz za powietrze.
  • Niestabilne deploye – rolling update wchodzi, nowe pody nie startują, bo nie ma miejsca; starym ucina się pamięć, wszystko się sypie.

Jaki masz obecnie największy ból: wydajność, stabilność czy koszt? Od odpowiedzi zależy, czy agresywniej podejdziesz do autoscalingu, czy raczej do przycinania requests i limitów.

Gdzie limity i autoscaling wpinają się w architekturę klastra?

Żeby sensownie ustawiać limity i requests w Kubernetes, trzeba spojrzeć na klaster jak na łańcuch powiązań:

  • Aplikacja – proces, który realnie zużywa CPU i pamięć.
  • Kontener – izoluje aplikację, a jego zasoby ograniczają cgroups.
  • Pod – minimalna jednostka schedulowana, może zawierać kilka kontenerów.
  • Node – fizyczny lub wirtualny host z konkretnym CPU i RAM.
  • Cluster – zestaw node’ów zarządzanych przez control plane.

Scheduler patrzy na requests i decyduje, na który node można wcisnąć konkretny pod. Kubelet pilnuje wykonania limitów CPU i pamięci na poziomie node’a. Autoscaling (HPA, VPA, Cluster Autoscaler) reaguje na obciążenie i próbuje dobrać liczbę replik lub rozmiar zasobów tak, by ten łańcuch działał przewidywalnie.

Deklaracja zasobów jako część wydajności, nie tylko „konfiguracja”

Często koncentrujesz się na optymalizacji kodu: zapytania do bazy, cache, algorytmy. A jednocześnie w YAML-ach masz resources: {} albo kopiowane bezrefleksyjnie ustawienia typu 500m/512Mi dla wszystkiego. W efekcie szybki kod działa wolno, bo:

  • jest dławiący się throttlingiem CPU,
  • albo czeka w kolejce, bo HPA nie skaluje,
  • albo scheduler nie ma jak go rozlokować rozsądnie.

Deklaracja zasobów jest tak samo ważna jak tuning bazy czy cache. To ona definiuje, ile „paliwa” aplikacja faktycznie dostanie pod obciążeniem i jak Kubernetes będzie nią zarządzał.

Efekt dobrze dobranych requests, limitów i autoscalingu

Co chcesz osiągnąć? Najczęściej zestaw celów jest podobny:

  • Przewidywalne opóźnienia – przy określonym ruchu latencja nie wyskakuje nagle 10× w górę.
  • Stabilne rollouty – deploy nie wywołuje lawiny restartów i nie ubija losowych podów.
  • Niższy koszt – nie płacisz za puste CPU i niewykorzystaną pamięć.
  • Odporność na piki – HPA zareaguje na burst, zanim użytkownicy to poczują.

Zanim wejdziesz w szczegóły, odpowiedz sobie krótko: co jest priorytetem dla twojego systemu – wydajność, stabilność czy optymalizacja kosztów? Możesz mieć dwie z tych rzeczy na raz, trzy naraz tylko w umiarkowanej skali i przy bardzo świadomym ustawieniu zasobów.

Podstawy zasobów w Kubernetes: CPU, pamięć, requests i limity

Jak rozumieć CPU i pamięć w specyfikacji poda

CPU w Kubernetes opisuje się w millicores. 1000m to logicznie jedno pełne vCPU. Przykłady:

  • 250m – ćwierć CPU, 1/4 rdzenia procesora.
  • 500m – pół CPU.
  • 2 lub 2000m – dwa pełne vCPU.

Pamięć opisuje się najczęściej w Mi lub Gi, czyli Mebibajtach i Gibibajtach. 512Mi to ok. 512 MB, 1Gi to ok. 1 GB. Warto trzymać się jednej jednostki w całym klastrze, żeby nie mylić się przy przeliczaniu.

Na poziomie node’a te wartości są sumowane. Jeśli node ma 4 vCPU i 8Gi RAM, a ty zadeklarujesz dla podów łącznie 4 vCPU i 8Gi requests, scheduler uzna node za „pełny” – nawet jeśli realne zużycie CPU jest o połowę mniejsze.

Różnica między requests a limits – scheduler vs kubelet

W polu resources masz dwa zbiory liczb:

  • requests – ile zasobów pod gwarantowanie potrzebuje, by działać poprawnie. Na tej podstawie scheduler decyduje, gdzie umieścić pod.
  • limits – twardy sufit, powyżej którego CPU będzie dławione (throttling), a pamięć może skończyć się OOMKilled.

Scheduler patrzy tylko na requests. To one decydują, czy na danym nodzie jest „miejsce” na kolejny pod. Limits egzekwuje kubelet i cgroups na node’ach – w praktyce kernel ogranicza zasoby procesów w kontenerze.

Co się dzieje przy przekroczeniu CPU i pamięci

W przypadku CPU mechanizm jest łagodniejszy:

  • Jeśli nie ustawisz limits.cpu, kontener może korzystać z wolnego CPU na nodzie, nawet znacznie ponad requests.cpu.
  • Jeśli limits.cpu jest niższy niż potrzebuje aplikacja, kernel zacznie ją dławić – procesy dostaną mniej czasu procesora (throttling).
  • Skutek: rośnie latencja, rośnie czas odpowiedzi, a pody dalej „żyją”.

Przy pamięci mechanizm jest brutalny:

  • Jeśli kontener przekroczy limits.memory, kernel zwykle nie ma jak go „przydusić”.
  • Następuje OOMKill – proces jest zabijany, pod restartuje się (lub przechodzi w CrashLoopBackOff).
  • Jeżeli limits.memory nie ustawisz, limitem jest pamięć node’a; wtedy kubelet przy presji pamięci wybiera ofiary na podstawie QoS.

Stąd prosty wniosek: limity pamięci muszą być ustawiane ostrożniej niż limity CPU. Przy CPU „tylko” zwalniasz aplikację, przy pamięci ryzykujesz jej ubijanie.

Przykład YAML – bez zasobów vs z requests/limits

Najpierw prosty pod bez deklaracji zasobów:

apiVersion: v1
kind: Pod
metadata:
  name: demo-no-resources
spec:
  containers:
    - name: app
      image: my-registry/demo-app:latest

Scheduler może taki pod upchnąć wszędzie, bo nie widzi żadnych wymagań. Jeśli aplikacja okaże się „głodna”, zacznie walczyć o CPU i pamięć z innymi podami na nodzie.

Teraz ten sam pod z zasobami:

apiVersion: v1
kind: Pod
metadata:
  name: demo-with-resources
spec:
  containers:
    - name: app
      image: my-registry/demo-app:latest
      resources:
        requests:
          cpu: "200m"
          memory: "256Mi"
        limits:
          cpu: "500m"
          memory: "512Mi"

Scheduler będzie szukał noda, na którym znajdzie się co najmniej 200m CPU i 256Mi pamięci wolnych w puli requests. Kubelet przytnie aplikacji CPU do 500m, a przy próbie zużycia więcej niż 512Mi pamięci – proces zostanie zabity przez OOM.

Jak masz to dziś u siebie? Większość podów bez zasobów, wszystko na twardych limitach, a może kopiowane uniwersalne „profile” typu 500m/512Mi dla każdego mikroserwisu?

Profil CPU-bound vs memory-bound – co dominującym kosztem?

Zanim zaczniesz ustawiać liczby, odpowiedz sobie: twoja aplikacja jest bardziej CPU-bound czy memory-bound?

  • CPU-bound: intensywne przetwarzanie, serializacja, szyfrowanie, raporty. Wysokie użycie CPU, ale umiarkowana pamięć. Tu opłaca się dać więcej CPU (lub większy limit CPU) i pilnować, by HPA skaluje na CPU.
  • Memory-bound: duże cache, duże struktury danych, ORM-y ładujące sporo do pamięci, biblioteki ML. Tu trzeba dobrze oszacować requests.memory i limits.memory, a HPA może bazować bardziej na innych metrykach.

Przy pierwszej grupie rozsądne są limity CPU wyższe od requests (np. 2×), żeby dać aplikacji możliwość „doburstowania”, ale bez zatkania całego noda. Przy drugiej grupie trzeba skupić się na tym, by requests.memory nie był ani za niski (OOM), ani dramatycznie za wysoki (marnowanie RAM i droższe nody).

Nowoczesna serwerownia z rzędami szaf i okablowaniem sieciowym
Źródło: Pexels | Autor: Brett Sayles

Klasy QoS w Kubernetes i ich realne konsekwencje

Trzy klasy QoS: Guaranteed, Burstable, BestEffort

Kubernetes przydziela pody do jednej z trzech klas QoS (Quality of Service), na podstawie tego, jak ustawisz requests i limits:

  • Guaranteed – każdy kontener w podzie ma ustawione requests == limits dla CPU i pamięci. To najwyższy poziom „ważności”.
  • Burstable – przynajmniej jeden kontener ma ustawiony requests, ale limits są wyższe lub niejednakowe. Najczęstszy przypadek w aplikacjach.
  • BestEffort – w podzie nie ma żadnych requests ani limits. Pod nie gwarantuje sobie niczego.

QoS nie wpływa na to, ile CPU „dostanie” aplikacja na luźnym nodzie przy braku presji. Decyduje natomiast o jej losie, gdy node zaczyna się dusić pamięcią.

Presja pamięci – kto ginie pierwszy

Przy presji pamięci kubelet korzysta z klas QoS, wybierając, które pody można poświęcić. Z grubsza kolejka „do odstrzału” wygląda tak:

  1. BestEffort – pierwsze do zabicia. Nie mają żadnych gwarancji.
  2. Burstable – uśmiercane w drugiej kolejności, zwykle te, które najbardziej przekraczają swoje requests.
  3. Guaranteed – giną jako ostatnie, jeśli nawet ich guaranteed zasoby nie wystarczają.

Jeśli więc twoje kluczowe komponenty (np. gateway, auth, krytyczny mikroserwis) są w klasie BestEffort, prosisz się o kłopoty przy każdym problemie z pamięcią na nodzie. Z drugiej strony, jeśli wszystko ustawisz jako Guaranteed z ogromnymi requests i limits, scheduler zapcha nody bardzo szybko i będziesz płacić za duże, słabo wykorzystane instancje.

Kiedy świadomie wybierać Guaranteed, a kiedy Burstable

QoS Guaranteed ma sens wtedy, gdy:

  • komponent jest absolutnie krytyczny dla działania systemu (np. ingress, auth, core API);
  • zależysz od bardzo stabilnej latencji i nie możesz sobie pozwolić na agresywne zabijanie przy presji pamięci;
  • masz dobrze zmierzony profil zużycia zasobów, więc możesz ustawić rozsądne requests == limits bez przesadnego marnowania.

QoS Burstable sprawdza się w większości mikroserwisów aplikacyjnych:

  • chcesz mieć gwarantowane minimum, ale pozwalasz, by aplikacja chwilowo użyła więcej zasobów;
  • masz HPA, które skaluje pody w odpowiedzi na rosnące obciążenie;
  • możesz zaakceptować, że w skrajnym kryzysie pamięci te pody będą bardziej narażone niż Guaranteed.

Zadaj sobie pytanie: które aplikacje naprawdę muszą być w QoS Guaranteed? Zwykle jest ich dużo mniej, niż myślisz.

BestEffort – śmietnik, sandbox czy świadome narzędzie?

QoS BestEffort to pody bez requests i limits. Scheduler traktuje je jak „lekki temat”, który można wrzucić na każdy node. Przy braku presji zjedzą tyle, ile znajdą. Przy lekkiej presji – zaczną być duszone, a przy silnej – będą zabijane jako pierwsze.

Jak korzystać z BestEffort z głową

BestEffort kusi prostotą – nic nie deklarujesz, wszystko „jakoś działa”. Tylko że to „jakoś” bywa bardzo losowe. Zanim wrzucisz coś w BestEffort, odpowiedz sobie: czy jestem w stanie zaakceptować nagłe zabicie tego poda przy presji pamięci?

BestEffort ma sens w kilku konkretnych scenariuszach:

  • zadania batchowe i jednorazowe, które mogą zostać przerwane i odpalone ponownie (np. raporty, importy danych);
  • środowiska deweloperskie, gdzie kluczowa jest elastyczność, a nie SLA aplikacji;
  • narzędzia pomocnicze, z których korzystasz okazjonalnie (np. webowe UI do narzędzi dev), a nie są krytyczne.

Przygotuj sobie listę: które workloady są „fajnie jak są”, ale przeżyjesz ich nagły brak? Te kandydatury możesz świadomie włożyć do BestEffort, zamiast zostawiać tam krytyczny backend, bo nikt nie ustawił mu zasobów.

Jak realnie dobrać requests i limity: podejście krok po kroku

Punkt startowy: co masz dzisiaj?

Nie ustawiaj liczb „z głowy” bez spojrzenia na stan obecny. Zadaj sobie kilka prostych pytań:

  • Jakie limity i requests masz dziś w deploymentach? Jest jakiś standard typu 500m/512Mi czy totalny miks?
  • Masz już metryki per pod z ostatnich dni/tygodni (Prometheus, Datadog, Grafana Cloud, cokolwiek)?
  • Jakie są rzeczywiste SLA dla poszczególnych usług – co musi „nigdy nie padać”, a co może zwolnić?

Bez tych odpowiedzi łatwo przesadzić w jedną stronę: albo przeprovisioning (wszędzie olbrzymie requests), albo chaos (prawie wszędzie BestEffort).

Krok 1: Zbierz metryki z produkcji lub bliskiego produkcji

Najlepszym źródłem prawdy jest produkcja. Jeśli nie możesz tam eksperymentować, użyj środowiska, które jest do niej jak najbardziej zbliżone pod kątem ruchu i danych.

Co konkretnie chcesz mieć zmierzone dla każdego poda/komponentu?

  • CPU usage w czasie – warto mieć percentyle (np. 50/90/95), a nie tylko średnią;
  • Memory usage – również z percentylami i widocznymi „pikami” po GC, deploymentach, jobach;
  • latencja i błędy (np. p95/p99, 5xx) – żeby widzieć wpływ ewentualnego throttlingu czy OOM;
  • liczba replik, jeśli masz HPA – jak zmienia się wraz z ruchem.

Masz już takie dashboardy? Jeśli nie, zacznij od jednego serwisu, który jest dla ciebie ważny, i zbuduj dla niego widok łączący CPU, pamięć i metryki aplikacyjne.

Krok 2: Ustal „normalne” zachowanie aplikacji

Po zebraniu danych spójrz, jak wygląda typowy dzień:

  • Jakie są stabilne widełki CPU i pamięci przy „zwykłym” ruchu?
  • Kiedy występują piki – godziny szczytu, batch processing, full GC?
  • Czy aplikacja rosnąco zużywa pamięć (memory leak), czy raczej oscyluje wokół jakiegoś poziomu?

Dla requests interesuje cię zachowanie w „normalnym” scenariuszu, a dla limits – zachowanie przy pikach i odchyłkach.

Krok 3: Wybierz strategię dla CPU

CPU można traktować elastyczniej niż pamięć. W praktyce możesz przyjąć jeden z trzech wariantów:

  1. Konserwatywnyrequests.cpu ustawiasz blisko p90 zużycia, a limits.cpu maksymalnie 1.5–2× wyżej.
  2. Elastycznyrequests.cpu bliżej p70, limits.cpu ok. 2–3× requests, z dobrze skonfigurowanym HPA.
  3. Agresywnyrequests.cpu nawet poniżej mediany, wysokie limity lub brak limitu, ale z mocnym HPA i obserwacją latencji.

Który z nich jest bliżej twojego celu? Jeśli boisz się throttlingu, ale masz spory zapas CPU na nodach, elastyczny wariant jest rozsądnym środkiem. Jeśli walczysz o każdy vCPU w chmurze, zacznij konserwatywnie i potem stopniowo ścinaj requests, patrząc na stabilność.

Krok 4: Strategia dla pamięci – margines bezpieczeństwa

Przy pamięci mniej więcej odwracasz logikę. Nie chcesz zbliżać się do limitu zbyt często, bo kończy się to OOM. Możesz podejść do tego tak:

  • requests.memoryp90–p95 zużycia w normalnym ruchu;
  • limits.memory – p99 plus dodatkowy margines (np. 20–30%), żeby mieć bufor na GC, sporadyczne skoki, nieprzewidziane scenariusze.

Jeśli aplikacja ma tendencję do „pełzającego” wzrostu pamięci (leak), margines na limicie musi być większy, a równolegle planujesz cykliczne restarty lub naprawę kodu. Dobieranie limitów nie zastąpi pracy nad samą aplikacją.

Krok 5: Test pod presją – czy ustawienia trzymają?

Deklaracje w YAML to jedno, a zachowanie pod obciążeniem – drugie. Warto przeprowadzić kontrolowany test:

  • Odpal load test zbliżony do ruchu produkcyjnego (albo nieco wyższy).
  • Obserwuj CPU throttling (np. metryka container_cpu_cfs_throttled_seconds_total) i ewentualne OOMKilled.
  • Sprawdź latencję i błędy – czy ruch w szczycie nie powoduje nagłych skoków p95/p99.

Jeżeli widzisz intensywny throttling przy braku saturacji CPU na nodzie, twoje limits.cpu są prawdopodobnie zbyt niskie. Jeżeli widzisz OOM mimo nieosiągania limitu pamięci, być może problemem są inne pody na tym samym nodzie lub błędy w pomiarach.

Krok 6: Iteracja – małe zmiany, ciągła obserwacja

Dobieranie zasobów to proces iteracyjny. Zamiast robić jednorazową „kampanię optymalizacyjną” i zapomnieć, zbuduj prostą praktykę:

  • Wprowadzaj małe korekty (np. +/- 20–30% requests/limits) na pojedynczych serwisach.
  • Po każdej zmianie obserwuj zachowanie przynajmniej przez kilka dni: usage, latencję, błędy.
  • Raz na kwartał zrób przegląd zasobów – spójrz, które pody mają usage < 30% requests niemal cały czas, a które dotykają limitów.

Jak często teraz wracasz do ustawień zasobów? Jeżeli odpowiedź brzmi „prawie nigdy”, jest spora szansa, że płacisz za sporo niewidocznego marnotrawstwa albo żyjesz z niepotrzebnym ryzykiem OOM.

Zbliżenie serwerowej szafy rack z migającymi diodami i kablami Ethernet
Źródło: Pexels | Autor: Brett Sayles

Autoscaling w Kubernetes: przegląd narzędzi i kiedy którego użyć

Trzy główne poziomy skalowania

Samo ustawienie zasobów to połowa układanki. Druga to automatyczne reagowanie na zmiany obciążenia. W Kubernetes typowo masz trzy warstwy skalowania:

  • Horizontal Pod Autoscaler (HPA) – zmienia liczbę replik podów w Deployment/StatefulSet.
  • Vertical Pod Autoscaler (VPA) – sugeruje lub automatycznie zmienia requests/limits dla podów.
  • Cluster Autoscaler (CA) lub odpowiednik w managed K8s – dodaje/usuwa nody w klastrze.

Jak myśleć o podziale ról? HPA reaguje na zmiany ruchu, VPA dba o dobór zasobów jednej repliki, a CA dorzuca „żelazo” pod spodem, gdy zabraknie miejsca na dodatkowe pody.

Kiedy wystarczy sam HPA

Jeżeli masz aplikacje:

  • stateless lub łatwo replikowalne,
  • z relatywnie przewidywalnym zużyciem CPU/pamięci na replikę,
  • bez ekstremalnej wrażliwości na pojedyncze pody (czyli możesz mieć ich więcej/mniej bez dramatu),

to dobrze skonfigurowany HPA często w zupełności wystarczy. Warunek: requests i limity muszą być sensowne, żeby HPA nie liczył na fikcyjnych procentach.

Przykład: prosty serwis HTTP, który wykonuje kilka zapytań do bazy i trochę logiki biznesowej. Ustawiasz requests/limits w oparciu o rzeczywiste usage, a HPA skaluje na CPU lub custom metrykę typu requests_per_second.

Kiedy do gry wchodzi VPA

VPA przydaje się tam, gdzie:

  • aplikacja ma nieintuicyjny profil zużycia i trudno odgadnąć sensowne requests;
  • nie chcesz ręcznie dostrajać zasobów kilkudziesięciu mikroserwisów;
  • masz stabilne środowisko batchowe lub analityczne, gdzie liczba replik jest raczej stała, ale rozmiar pojedynczego poda może się dostosowywać.

VPA działa w tle, analizuje usage i generuje rekomendacje (lub automatycznie je stosuje). Możesz go traktować jako „asystenta”, który wskazuje, które pody mają zbyt wysokie requests, a które za niskie.

Cluster Autoscaler – kiedy node’ów jest za mało

Nawet najlepszy HPA i VPA nie pomogą, gdy na klastrze fizycznie brakuje miejsca na nowe pody. To rola Cluster Autoscalera:

  • gdy pods są w stanie Pending z powodu braku zasobów, CA dodaje nody (nowe VM/instancje);
  • gdy widzi niedowykorzystane nody, próbuje zwinąć je, przenosząc pody i usuwając nadmiarowe instancje.

Masz CA skonfigurowanego w klastrze? Jeżeli nie, przy agresywnym HPA możesz wpaść w sytuację, w której HPA „chce” 20 replik, ale scheduler wciąż ma dostępne miejsce tylko na 10 i reszta długo wisi w Pending.

Jak te warstwy działają razem

Spójrz na to jako na kaskadę decyzji:

  1. Aplikacja zaczyna zużywać więcej CPU/pamięci.
  2. HPA widzi rosnące metryki i zwiększa liczbę replik.
  3. Scheduler próbuje upchnąć nowe pody na istniejących nodach.
  4. Jeśli nie ma miejsca, Cluster Autoscaler dodaje nowe nody.
  5. VPA w tle obserwuje usage i może w kolejnych dniach skorygować requests per pod.

W idealnym świecie te trzy poziomy są ze sobą zgrane. W praktyce często masz tylko HPA i brak CA albo HPA + CA, ale nikt nie patrzy, czy requests są sensowne. Gdzie ty jesteś na tej osi?

Horizontal Pod Autoscaler od kuchni: konfiguracja, tryby, pułapki

Podstawowy HPA na CPU – jak działa i co naprawdę mierzy

Najprostszy HPA korzysta z procentu zużycia CPU względem requests.cpu:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: demo-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: demo-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

Kluczowy szczegół: averageUtilization: 70 oznacza 70% requests.cpu, a nie 70% „gołego” vCPU. Jeżeli ustawisz requests.cpu bardzo nisko, HPA będzie myślał, że pod jest „zapchany” przy minimalnym rzeczywistym usage i agresywnie podbijał liczbę replik.

Skalowanie po pamięci – kiedy to ma sens

HPA może skalować również po pamięci:

metrics:
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

To bywa kuszące, ale dla wielu aplikacji pamięć nie jest dobrym sygnałem do skalowania horyzontalnego. Sprawdza się głównie tam, gdzie:

  • pamięć rośnie liniowo z liczbą aktywnych sesji/użytkowników,
  • nie masz dużych cache’ów, które „zjedzą tyle, ile dasz”,
  • aplikacja nie ma intensywnego GC, który fałszuje chwilowe usage.

Kiedy łączyć metryki – CPU + coś jeszcze

Sam CPU rzadko opisuje pełny obraz. Jeżeli HPA ma robić rozsądne rzeczy, zwykle lepiej połączyć go z inną metryką. Pytanie: czego tak naprawdę broni twoja aplikacja – CPU, pamięci, czy może czasu odpowiedzi?

HPA w wersji v2 pozwala na wiele metryk jednocześnie. Przykład dla CPU + pamięć:

metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

Jak HPA to interpretuje? Jeżeli którakolwiek z metryk przekroczy cel, HPA będzie dążył do zwiększenia liczby replik. Efektywnie działa to jak maksimum z sugerowanej liczby podów z każdej metryki.

W praktyce często lepszym wyborem jest:

  • CPU lub RPS (custom metryka) jako główny trigger,
  • pamięć do monitoringu i alertów, ale niekoniecznie do samego skalowania.

Jeżeli i tak masz Prometheusa, spróbuj najpierw podejścia: CPU + custom metryka, a pamięć zostaw jako sygnał „coś jest nie halo z kodem lub konfiguracją”.

Custom Metrics i External Metrics – skalowanie po tym, co ma sens dla biznesu

Kiedy CPU przestaje wystarczać? Najczęściej wtedy, gdy:

  • aplikacja jest intensywnie I/O bound (np. czeka na bazę, kolejkę, API zewnętrzne),
  • masz duże różnice między rodzajami requestów, a CPU nie koreluje z „ciężkością” ruchu,
  • naprawdę zależy ci na czasie odpowiedzi pod kątem SLA.

Zastanów się: co najlepiej opisuje „obciążenie” twojego systemu? RPS? Liczba zadań w kolejce? Długość kolejki w brokerze? HPA może korzystać z takich sygnałów przez Custom Metrics API lub External Metrics.

Przykładowa konfiguracja HPA skalującego po metryce Prometheusowej (eksportowanej jako custom metric) może wyglądać tak:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: demo-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: demo-app
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second
        target:
          type: AverageValue
          averageValue: "50"

Tutaj definiujesz, że średnio na pod chcesz mieć ok. 50 requestów na sekundę. Gdy rośnie ruch, HPA zwiększa liczbę replik tak, żeby zbić RPS per pod do docelowego poziomu.

Jeżeli korzystasz z External Metrics (np. metryki z SQS, Pub/Sub lub innego systemu kolejkowego), konfiguracja ma podobną strukturę, ale z typem External. Rozsądne pytanie: czy twoje kolejki bywają zapchane, zanim skaluje się backend? Jeśli tak – to jest naturalny kandydat na metrykę do HPA.

Czasy opóźnień HPA – dlaczego skaluje się „za późno”

Częsty zarzut: „HPA skaluje się zbyt wolno, zanim dojdzie do nowych replik, użytkownicy już mają timeouty”. Jeżeli masz podobny problem, sprawdź trzy źródła opóźnienia:

  1. Okres zbierania metryk – Metrics Server / Prometheus scraping co 15–30 sekund.
  2. Okres ewaluacji HPA--horizontal-pod-autoscaler-sync-period (typowo 15s).
  3. Czas startu aplikacji – ile trwa, zanim nowy pod zacznie realnie obsługiwać ruch.

Jeżeli twoje piki ruchu trwają krótko, ale są bolesne, zadaj sobie pytanie: jak szybko jesteś w stanie wystartować nowy pod? Jeżeli potrzebujesz na to minutę, to nawet perfekcyjnie skonfigurowany HPA nie zdąży „dobić” do piku.

Kilka dźwigni, które masz do dyspozycji:

  • podkręcenie częstotliwości zbierania metryk / ewaluacji HPA (ale z umiarem – obciążysz API server i system monitoringu),
  • wydłużenie minReplicas, żeby mieć „bufor” na gwałtowne skoki,
  • optymizacja czasów startu (zimny cache, initialization, connection pooling)
  • pre-scaling przed znanym eventem (np. deploy z większym minReplicas na czas kampanii marketingowej).

Stabilizacja, cooldown i fluktuacje replik

Jeżeli widzisz, że HPA „pompkuje” replikami – co parę minut skaluje w górę i w dół – przyjrzyj się wbudowanym mechanizmom wygładzania.

W autoscaling/v2 możesz użyć behavior:

spec:
  minReplicas: 3
  maxReplicas: 15
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
        - type: Percent
          value: 100
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 50
          periodSeconds: 60

Co to zmienia w praktyce?

  • stabilizationWindowSeconds dla scaleDown mówi, że HPA patrzy na historię metryk z ostatnich 5 minut i nie ścina replik od razu po pierwszym spadku.
  • polityka „Percent” ogranicza jak bardzo liczba replik może się zmienić w danym oknie.

Jeżeli twoja aplikacja źle znosi częste scale down (długi cold start, kosztowne warm-upy), rozważ agresywniejszy scale up i konserwatywny scale down. Zadaj sobie pytanie: co jest droższe – chwilowy overprovisioning, czy uderzenie w SLO przy każdym „oddechu” ruchu?

HPA a rollouty i podmiana wersji

Przy rolling update łatwo o konflikt: Deployment skaluje pody według własnego algorytmu, HPA jednocześnie próbuje ich liczbą zarządzać według metryk. To normalne, ale trzeba uważać, żeby nie przegiąć.

Kilka praktycznych wskazówek:

  • ustaw rozsądne maxSurge i maxUnavailable, żeby w czasie deployu HPA nie walczył z Deployementem o docelową liczbę podów,
  • monitoruj metryki z podziałem na wersje (label version albo pod_template_hash), żeby nie mylić usage starej i nowej wersji,
  • przy dużych zmianach wydajności najpierw zrób deploy na stałej liczbie replik, dopiero potem zaadaptuj HPA do nowego profilu.

Jeżeli na produkcji regularnie widzisz „dziwne” skoki liczby replik w trakcie rolloutów, zapytaj: kto naprawdę steruje liczbą podów – Deployment czy HPA? I czy ich zakresy (replicas vs. min/maxReplicas) są ze sobą spójne.

HPA w środowiskach batchowych i workerowych

Nie każda praca nadaje się do skalowania po CPU, szczególnie batch i przetwarzanie kolejkowe. Tam lepiej sprawdza się skalowanie po:

  • długości kolejki (liczba zadań oczekujących),
  • czasie zalegania zadań (age of oldest message),
  • docelowym czasie opróżnienia kolejki.

Przykład: worker przetwarza wiadomości z kolejki, a celem jest, aby przeciętnie wiadomość nie czekała dłużej niż kilka minut. Możesz wystawić metrykę „tasks_pending” i dobrać liczbę workerów tak, aby:

  • tasks_pending / replicas ≈ stała,
  • lub aby czas najstarszego zadania nie przekraczał określonego progu.

Zanim to zrobisz, odpowiedz sobie szczerze: czy twoje workery są naprawdę stateless, czy przechowują lokalny stan na dysku? Jeżeli to drugie, agresywny HPA będzie cię bardziej boleć niż pomagać – każdy scale down to potencjalna utrata pracy albo skomplikowany mechanizm checkpointów.

Vertical Pod Autoscaler i współpraca z ręczną konfiguracją zasobów

Tryby pracy VPA – kiedy tylko sugerować, a kiedy ustawiać za ciebie

VPA ma trzy główne tryby, które realnie zmieniają sposób korzystania z narzędzia. Zanim go włączysz, zadaj sobie pytanie: ile kontroli chcesz oddać?

  • Off – VPA tylko zbiera dane, nie sugeruje ani nie zmienia niczego.
  • Initial – VPA ustawia requests/limits tylko przy tworzeniu nowego poda, potem ich nie rusza.
  • Auto – VPA automatycznie skaluje w górę i w dół, restartując pody, aby zastosować nowe wartości.
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: demo-app-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: demo-app
  updatePolicy:
    updateMode: "Initial"   # albo "Off", "Auto"

Bezpieczną ścieżką jest start od trybu Off lub Initial. Najpierw zbierasz dane, patrzysz, jakie rekomendacje generuje VPA, dopiero potem ewentualnie przechodzisz na „Auto” dla konkretnych, dobrze poznanych workloadów.

Jak czytać rekomendacje VPA

VPA dla każdego kontenera wystawia propozycje zakresów:

  • lowerBound – dolna granica sensownego requests (niżej zaczyna rosnąć ryzyko throttlingu / OOM),
  • target – sugerowana wartość requests,
  • upperBound – górna granica, powyżej której resources są raczej marnowane.

Możesz je podejrzeć przez kubectl describe vpa <nazwa> albo przez API. Typowy sposób pracy:

  1. Uruchamiasz VPA w trybie Off na kilka dni.
  2. Porównujesz target z aktualnymi requests/limits.
  3. Jeżeli target jest 2–3 razy niższy od twoich requests i usage to potwierdza, stopniowo obniżasz requests ręcznie.

Zwróć uwagę na historię – czy rekomendacje są stabilne, czy mocno pływają między dniami / porami dnia? Jeżeli widzisz duże wahania, być może aplikacja ma bardzo zmienny profil usage i lepiej zostawić margines bezpieczeństwa powyżej targetu VPA.

HPA + VPA – co można, a czego nie łączyć

Klasyczna pułapka: próba użycia HPA (CPU) i VPA (Auto) na tym samym obiekcie. Skoro CPU % liczone jest względem requests.cpu, a VPA te requests zmienia w locie, HPA dostaje co chwila nowy punkt odniesienia. Efekt? Chaos w liczbie replik.

Standardowy pattern wygląda tak:

  • HPA skaluje po metryce niezależnej od requests (np. custom „requests per second”),
  • VPA ustawia requests/limits, ale:
    • albo tylko na starcie poda (Initial),
    • albo w trybie Auto, ale wtedy nie używasz CPU-utilization w HPA.

Zanim włączysz oba, odpowiedz: co ma być główną dźwignią – liczba replik czy rozmiar pojedynczej repliki? HPA i VPA mają inne cele i nie ma sensu, by „ciągnęły” w różne strony według tej samej metryki CPU.

VPA w środowiskach produkcyjnych – gdzie zacząć

Nie musisz od razu wrzucać VPA na wszystkie mikroserwisy. Zwykle szybciej zyskasz, zaczynając od:

  • serwisów, które notorycznie przekraczają pamięć lub mają OOM-y,
  • komponentów batchowych, ETL, analitycznych – stała liczba podów, zmienny rozmiar,
  • serwisów, których nikt nie dotykał od miesięcy, a są podejrzanie „grube” pod kątem requests.

Możesz też użyć VPA wyłącznie jako „radaru”:

  • włączasz go dla całego namespace w trybie Off,
  • zbierasz rekomendacje i patrzysz, które workloady najbardziej odstają od targetów.

Jeżeli twój cluster-koszt rośnie, a nie robisz regularnych przeglądów zasobów, takie podejście potrafi szybko wskazać największe „grzejniki”. Pytanie do ciebie: czy wiesz dziś, które aplikacje zużywają najwięcej CPU/pamięci względem realnej potrzeby?

Limity dla VPA – jak nie pozwolić mu „zwariować”

VPA nie powinien mieć pełnej dowolności. Dla wielu systemów naturalne jest, że:

  • nie chcesz, aby pojedynczy pod przekroczył pewien poziom CPU (np. 4 vCPU),
  • nie chcesz, aby pojedynczy pod zajął pół noda pamięcią.

Do tego służą Pod Resource Policy:

Najczęściej zadawane pytania (FAQ)

Jak dobrać requests i limits w Kubernetes, żeby nie przepłacać i nie mieć OOMKilled?

Zacznij od pomiaru, a nie od „szablonu” 500m/512Mi. Puść aplikację pod realnym ruchem (lub choćby pod prostym testem obciążeniowym) i zobacz typowe zużycie CPU i pamięci oraz piki. Masz już takie dane z Prometheusa lub narzędzi chmurowych, czy dopiero je zbierzesz?

Praktyczny punkt startowy bywa taki: ustaw requests lekko powyżej typowego zużycia (np. mediana lub 75. percentyl), a limits wyżej – np. 1,5–2× requests dla CPU i ostrożniej dla pamięci (np. 1,2–1,5×). Potem obserwuj: jeśli widzisz throttling CPU, podnieś limit; jeśli pojawiają się OOM-y lub presja pamięci na nodach, podnieś requests.memory lub zoptymalizuj samą aplikację.

Czym różni się requests a limits w Kubernetes i jak wpływają na scheduler oraz wydajność?

requests to deklaracja: „tyle zasobów potrzebuję, żeby działać sensownie”. Na tej podstawie scheduler decyduje, na który node może wcisnąć poda. Za wysokie requests = mniej podów na nodzie i wyższy koszt, za niskie = ryzyko, że przy obciążeniu pod będzie się dusił razem z sąsiadami.

limits to sufit: „powyżej tej wartości już nie możesz”. Przy przekroczeniu limitu CPU kernel dusi proces (throttling) i rośnie latencja. Przy przekroczeniu limitu pamięci proces jest ubijany (OOMKilled). Jeśli twoim priorytetem jest stabilność, ustawiaj limity pamięci z marginesem bezpieczeństwa i na początku skup się na precyzyjnych requests.memory.

Jak uniknąć throttlingu CPU w Kubernetes przy większym obciążeniu?

Najpierw sprawdź, czy throttling faktycznie występuje: metryki z cgroups lub Prometheusa (np. container_cpu_cfs_throttled_seconds_total) powiedzą ci, czy CPU jest przycinane. Widzisz skoki latencji przy jednoczesnym wysokim użyciu CPU? To częsty sygnał, że limit CPU jest zbyt niski.

Masz kilka opcji:

  • podnieś limits.cpu (często 2× requests.cpu działa dobrze dla usług CPU-bound),
  • jeśli możesz, tymczasowo usuń limit CPU i obserwuj, ile realnie zużywa aplikacja pod pikiem,
  • dostosuj HPA tak, by szybciej dokładane były repliki przy wzroście CPU (niższe progi lub inne metryki).

Kluczowe pytanie: wolisz, żeby pojedynczy pod miał „turbo” CPU, czy żeby szybciej dokładane były kolejne kopie aplikacji?

Dlaczego moje pody są OOMKilled mimo że mają ustawione limity pamięci?

Limit pamięci to surowy cap. Jeśli aplikacja go przekroczy, kernel nie będzie jej „przyduszał”, tylko ją zabije. Czy masz włączony większy cache, agresywną serializację, duże batch processingi, które powodują nagłe skoki pamięci? To typowe powody OOM przy zbyt ciasnych limitach.

Rozwiązanie zwykle jest dwutorowe:

  • po stronie K8s – zwiększ limits.memory i dopasuj requests.memory, by scheduler brał to pod uwagę,
  • po stronie aplikacji – ogranicz cache, zmniejsz rozmiar batchy, ustaw sensowny GC / heap (np. w JVM).

Jeżeli głównym celem jest stabilność, ustaw najpierw bezpieczny limit pamięci, a dopiero potem zacznij optymalizować go w dół, obserwując piki.

Jak skonfigurować HPA w Kubernetes: CPU czy pamięć, a może inne metryki?

Najpierw odpowiedz sobie: twoja aplikacja jest bardziej CPU-bound czy memory-bound? Jeśli kod jest intensywnie obliczeniowy (CPU-bound), HPA oparte o CPU (% użycia w stosunku do requests) zwykle dobrze działa. Dla aplikacji mocno pamięciożernych lepiej sprawdzają się inne metryki – np. kolejki, czas odpowiedzi, liczba requestów na sekundę, metryki z Prometheusa.

Typowy pattern:

  • HPA na CPU dla usług API i backendów biznesowych,
  • HPA na niestandardową metrykę (np. długość kolejki, RPS) dla workerów i konsumentów message queue,
  • raczej nie skaluj po pamięci, jeśli aplikacja naturalnie bierze „ile się da” pod cache – wtedy HPA tylko będzie mieszać.

Jeżeli HPA reaguje zbyt wolno, obniż target (np. z 80% do 60% CPU), skróć okno stabilizacji lub zwiększ minimalną liczbę replik.

Jak ustawić limity i autoscaling, jeśli moim priorytetem są koszty klastra Kubernetes?

Najpierw sprawdź, czy requests nie są „na zapas” – to one trzymają schedulera w szachu. Wysokie requests przy niskim realnym zużyciu = scheduler szybciej uznaje node za pełny, a ty płacisz za niewykorzystane CPU i RAM. Masz już raport, który porównuje requests vs usage dla najdroższych namespaces?

Strategia oszczędnościowa zwykle wygląda tak:

  • obniż requests tam, gdzie usage jest od nich dużo niższe (np. długo >50% różnicy),
  • utrzymuj rozsądne limits, żeby „hałaśliwe” pody nie zjadły całego noda,
  • włącz i dostrój Cluster Autoscaler, by node’y faktycznie znikały, gdy obciążenie spada,
  • dla batchy i zadań niekrytycznych rozważ profile z niższym QoS (Burstable) i tańsze node’y/preemptible.

Zadaj sobie proste pytanie: które serwisy mogą mieć gorsze SLA w zamian za niższy koszt – od nich zacznij agresywniejsze przycinanie requests.

Czy powinienem ustawiać takie same limity i requests dla wszystkich mikroserwisów?

Jednolite „profile” kuszą prostotą, ale zwykle prowadzą do trzech problemów naraz: przepalania zasobów w małych serwisach, throttlingu w cięższych usługach i nieprzewidywalnego autoscalingu. Czy w twoim klastrze widać kopiowane wartości typu 500m/512Mi praktycznie wszędzie?

Lepiej myśleć klasami usług:

  • małe, lekkie API – niskie requests, skromne limity, szybki HPA,
  • ciężkie backendy / raporty – wyższe CPU, dużo pamięci, ostrożne limity i dłuższe warm-upy przy deployu,
  • batch / workerzy – bardziej elastyczne, mogą działać na node’ach o gorszym SLA, ale z innym profilem zasobów.

Zamiast jednego „magicznego” profilu stwórz 2–3 sensowne klasy i per klasa zoptymalizuj requests, limity i zasady autoscalingu.