Po co w ogóle skalować aplikacje w Kubernetes
Intencją większości zespołów sięgających po Kubernetes jest uzyskanie powtarzalnego, automatycznego sposobu dostosowywania mocy obliczeniowej do realnego obciążenia. Chodzi o to, aby aplikacja była dostępna i szybka, ale jednocześnie nie generowała niepotrzebnych kosztów infrastruktury. Kluczowym elementem jest tu świadome wykorzystanie autoskalerów oraz poprawne zaprojektowanie zapotrzebowania na zasoby (CPU, pamięć i inne).
Jeśli aplikacja ma przejść z etapu „działa na mojej maszynie” do stabilnego środowiska produkcyjnego, potrzebuje powtarzalnych mechanizmów skalowania, które uwzględniają charakter ruchu (piki, kampanie, batch), architekturę (mikroserwisy, komponenty stanowe) oraz budżet zasobów. Kubernetes daje do tego bardzo rozbudowany zestaw narzędzi, pod warunkiem, że requests, limits i autoskalery są ustawione w sposób spójny i oparty o metryki.
Dlaczego skalowanie w Kubernetes jest inne niż w klasycznych środowiskach
Skalowanie monolitu na VM vs mikroserwisy na Kubernetes
W klasycznym podejściu aplikacja (często monolit) działa na jednej lub kilku maszynach wirtualnych. Skalowanie poziome sprowadza się do dodania kolejnych VM z tą samą aplikacją, a skalowanie pionowe do przydzielenia większej ilości CPU i RAM pojedynczej maszynie. Zwykle proces jest częściowo ręczny: zmiana rozmiaru VM, dodanie nowej instancji za load balancerem, konfiguracja autoskalera w chmurze na podstawie CPU maszyny.
W Kubernetes obiektem skalowania nie jest maszyna, ale Pod, czyli najmniejsza jednostka, w której uruchamiane są kontenery. Jeden Deployment może mieć jedną lub kilkadziesiąt (a nawet setki) replik Podów, a scheduler rozkłada je na węzłach klastra. Autoskalowanie dotyczy więc liczby Podów, a dopiero w drugiej kolejności liczby i rozmiaru węzłów (node’ów) w klastrze.
Do tego dochodzi architektura mikroserwisowa. Zamiast jednego monolitu mamy kilkanaście lub kilkadziesiąt serwisów, z których każdy może mieć własny profil obciążenia i własną politykę skalowania. Ustawienie jednego uniwersalnego autoskalera „na wszystko” przestaje mieć sens – skalowanie musi być zaprojektowane specyficznie dla każdego komponentu.
Niezmienność i deklaratywność – co to zmienia dla skalowania
Kubernetes opiera się na koncepcji immutable infrastructure. Obrazy kontenerów są niezmienne, a konfiguracja klastra deklaratywna. Zamiast „logować się na serwer i zmienić ustawienie”, definiuje się pożądany stan w manifestach YAML (Deployment, HPA, LimitRange itd.), a kontrolery doprowadzają klaster do tego stanu.
W kontekście skalowania oznacza to, że:
- skalowanie jest wyrażone deklaratywnie (np. minReplicas, maxReplicas, target CPU),
- zmiana strategii skalowania wymaga zmiany konfiguracji, a nie ręcznego „podniesienia” procesu na serwerze,
- można w łatwy sposób odtworzyć i wersjonować całą logikę skalowania wraz z kodem aplikacji (GitOps, IaC).
Dodatkowo autoskalery działają jako kontrolery w pętli reconcile: cyklicznie odczytują stan (metryki, liczbę replik) i porównują go z konfiguracją, po czym wykonują korekty. To zupełnie inne podejście niż jednorazowe wywołanie API chmury w stylu „zwiększ liczbę instancji VM”.
Poziomy skalowania: Pod, Deployment, Node, klaster
W Kubernetes można wyróżnić co najmniej cztery istotne poziomy skalowania:
- Pod – jednostka uruchomienia kontenera. Skalowanie „wewnątrz” poda to np. zmiana przydziału CPU/memory (requests/limits) lub użycie Vertical Pod Autoscaler.
- Deployment / StatefulSet – liczba replik Podów danego serwisu. Tu działa Horizontal Pod Autoscaler (HPA) – zwiększa lub zmniejsza liczbę replik.
- Node – węzeł klastra, VM lub fizyczny serwer, na którym lądują Pody. Tu wchodzi w grę Cluster Autoscaler (CA) lub autoskalery chmurowe.
- Klaster / region – skalowanie całych klastrów, multi‑cluster, failover między regionami. To poziom bardziej architektoniczny niż „pure Kubernetes”, zależny od chmury i topologii.
Skuteczne skalowanie wymaga zrozumienia, który poziom jest aktualnie „wąskim gardłem”. Jeśli HPA próbuje zwiększyć liczbę replik, ale klaster nie ma wolnych zasobów, bez poprawnie skonfigurowanego Cluster Autoscalera nowe Pody będą w stanie Pending, a użytkownicy nie dostaną żadnej korzyści z posiadania HPA.
Więcej ruchomych elementów i większa rola metryk
Kubernetes wprowadza więcej ruchomych elementów niż klasyczne podejście VM: kontrolery, scheduler, różne rodzaje autoskalerów, mechanizmy QoS, priorytety, affinities. W zamian daje dużą elastyczność i możliwość precyzyjnego sterowania zasobami.
Warunkiem sukcesu jest jednak dobry system metryk i obserwowalności. Autoskalery opierają się na danych z metrics‑server, Prometheusa lub innych źródeł. Jeśli metryki są niepełne, spóźnione albo błędnie zagregowane, decyzje o skalowaniu będą nietrafione.
W praktyce konfigurację HPA, Cluster Autoscalera i limitów zasobów trzeba rozpatrywać zawsze razem z systemem monitoringu. Bez tego łatwo wpaść w pułapkę: „mamy autoskalera, ale serwis i tak się dławi w godzinach szczytu”.
Podstawy zasobów w Kubernetes: CPU, pamięć i nie tylko
CPU w millicores i specyfika pamięci w Kubernetes
W Kubernetes zasoby procesora opisuje się w millicores. 1000m oznacza 1 pełny rdzeń logiczny. Przykładowo:
- 250m – jedna czwarta rdzenia,
- 500m – połowa rdzenia,
- 2000m – dwa rdzenie.
Z punktu widzenia schedulera suma requests CPU na node’zie nie powinna przekroczyć jego fizycznej (lub logicznej) mocy. Dzięki temu Kubernetes wie, czy ma miejsce na kolejne Pody. Limits z kolei określają maksymalny CPU, który Pod może zużyć – powyżej tej wartości kernel zaczyna throttlować cgroup.
Pamięć jest traktowana inaczej. Nie ma tu miękkiego throttlingu – jeśli kontener przekroczy limit pamięci, zostanie zabity przez OOMKiller (Out Of Memory). Stąd błędnie dobrane limity pamięci są znacznie bardziej dotkliwe niż zbyt ciasne limity CPU. Aplikacja może „jakoś działać” przy mniejszym CPU (tylko wolniej), natomiast przy zbyt niskim limicie RAM będzie po prostu restartowana.
Requests vs limits – logika i konsekwencje
Requests i limits to dwa różne poziomy deklaracji zasobów:
- requests – ilość zasobów, którą Pod gwarantuje, że będzie potrzebował. Scheduler wykorzystuje requests do decyzji, na którym węźle może umieścić Poda. Kluczowe dla planowania.
- limits – twardy sufit, którego Pod nie może przekroczyć. Dla CPU oznacza throttling, dla pamięci – ubijanie procesu przy przekroczeniu limitu.
Typowe błędy to:
- ustawienie limitu CPU zbyt blisko requestu, co ogranicza burst i sprawia, że aplikacja nie wykorzysta chwilowo wolnych zasobów node’a,
- ustawienie limitu pamięci równego requestowi przy znacznej zmienności zużycia, co prowadzi do częstych OOMKill i restartów,
- brak zdefiniowanych requests, co powoduje, że Pody mają klasę QoS BestEffort i są pierwsze do zabicia przy presji na zasoby.
Requests wpływają też na autoskalowanie klastrów. Cluster Autoscaler patrzy na Pody w stanie Pending z uwagi na brak możliwości spełnienia ich requests. Jeśli requests są zawyżone, CA może bez potrzeby dodawać kolejne node’y, generując niepotrzebne koszty.
Klasy QoS i ich wpływ na zachowanie przy niedoborze zasobów
Na podstawie requests i limits Kubernetes przypisuje Podom klasę QoS (Quality of Service):
| Klasa QoS | Warunek | Znaczenie praktyczne |
|---|---|---|
| Guaranteed | requests == limits dla CPU i memory, ustawione dla wszystkich kontenerów | najwyższy priorytet przy OOM; zabijane jako ostatnie |
| Burstable | ustawione requests i/lub limits, ale nie spełniają warunku Guaranteed | średni priorytet; zabijane przed Guaranteed, po BestEffort |
| BestEffort | brak requests i limits dla CPU i memory | najniższy priorytet; zabijane w pierwszej kolejności |
Dobór klasy QoS jest w praktyce narzędziem priorytetyzacji usług. Krytyczne komponenty (np. brama API, kluczowe mikroserwisy) często mają klasę Guaranteed, mniej istotne serwisy pomocnicze – Burstable, a elementy eksperymentalne lub batch – mogą być BestEffort, jeśli ich pad nie zagraża działaniu systemu.
Przemyślana kombinacja QoS, priorytetów i PodDisruptionBudget daje znacznie lepszą kontrolę nad zachowaniem systemu w sytuacjach presji na zasoby niż samo „podnoszenie limitów”.
Przykładowa definicja Deployment z requests i limits
Dla zobrazowania zależności spójrzmy na uproszczony manifest Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-gateway
spec:
replicas: 3
selector:
matchLabels:
app: api-gateway
template:
metadata:
labels:
app: api-gateway
spec:
containers:
- name: api-gateway
image: my-registry/api-gateway:1.2.3
resources:
requests:
cpu: "300m"
memory: "256Mi"
limits:
cpu: "800m"
memory: "512Mi"
Konsekwencje tej konfiguracji:
- scheduler zarezerwuje dla każdego Poda co najmniej 300m CPU i 256Mi RAM,
- Pod może zużyć do 800m CPU, jeśli node ma wolne zasoby,
- przy przekroczeniu 512Mi pamięci Pod zostanie zabity (OOMKill),
- klasa QoS to Burstable (requests != limits).
Jeśli ten serwis jest krytyczny i ma stabilny profil użycia zasobów, można rozważyć ustawienie requests == limits, co da klasę Guaranteed kosztem mniejszej elastyczności współdzielenia zasobów.
Kiedy świadomie nie ustawiać limitów CPU lub pamięci
Są scenariusze, w których świadome pominięcie limitów ma uzasadnienie:
- CPU bez limitu – przy aplikacjach intensywnie CPU‑chłonnych, w środowisku dobrze odizolowanym (dedykowany nodepool), można ustawić tylko requests, a pominąć limits. Dzięki temu proces będzie mógł użyć całego dostępnego CPU na node’zie bez throttlingu.
- pamięć bez limitu – rzadziej spotykane, ale czasem stosowane na nodepoolu dedykowanym pojedynczemu serwisowi. Zamiast limitu pamięci kontrolę przejmują limity node’a i monitoring; w razie problemu ucierpi tylko ten jeden serwis.
Takie podejście wymaga silnej dyscypliny i dobrego monitoringu. Brak limitów w środowisku współdzielonym (kilka ważnych serwisów na jednym nodepoolu) jest prostą drogą do sytuacji, w której jeden „rozbujany” proces zjada pamięć i kładzie cały node.
Modele skalowania w Kubernetes: w górę, w dół, na boki
Skalowanie pionowe, poziome i podejście mieszane
W Kubernetes można mówić o trzech głównych modelach skalowania:
- skala pionowa (vertical) – zwiększanie lub zmniejszanie zasobów pojedynczego Poda (CPU, pamięć). Realizowane najczęściej przez zmianę requests/limits lub przy użyciu Vertical Pod Autoscaler, który proponuje lub wymusza nowe wartości.
- skala pozioma (horizontal) – zwiększanie lub zmniejszanie liczby replik Podów danego Deploymentu/StatefulSetu. Tu króluje Horizontal Pod Autoscaler.
- podejście mieszane – jednoczesne stosowanie skalowania poziomego i pionowego, często z różnymi granicami i heurystykami.
W praktyce Kubernetes sprzyja skalowaniu poziomemu, bo pods są lekkie i łatwo dodać kolejne repliki. Jednak ignorowanie skalowania pionowego jest błędem: Pody zbyt „ciasne” zasobowo potrafią generować więcej problemów niż dawałoby to oszczędności.
Kiedy zwiększać rozmiar Poda, a kiedy liczbę replik
Dobór strategii zależy od kilku czynników:
- charakter obciążenia – jeśli serwis ma dużo krótkich, niezależnych żądań (typowy HTTP), najczęściej lepiej jest zwiększyć liczbę replik. Load balancer łatwo rozłoży ruch, a nowe Pody szybko zaczną obsługiwać kolejne requesty.
- narzut na start Poda – jeśli start Poda jest bardzo kosztowny (duży warmup cache, kompilacja, synchronizacja), bardziej sensowne może być zwiększenie zasobów dla istniejących Podów zamiast częstego dodawania nowych replik.
Granice skalowania poziomego i pionowego
Skalowanie „tylko w bok” albo „tylko w górę” szybko ujawnia swoje ograniczenia. Przy podejściu wyłącznie poziomym pojawiają się problemy:
- rosnąca liczba połączeń międzyserwisowych i większe opóźnienia sieciowe,
- większa presja na warstwę storage (np. bazy danych, kolejki), która często skaluje się gorzej,
- narzut operacyjny – rollout kilkuset Podów trwa dłużej i wiąże się z większym ryzykiem.
Jednocześnie nadmierne skalowanie pionowe ma swoje sztywne bariery:
- granica rozmiaru node’a – jeśli Pod wymaga więcej CPU/RAM niż pojedynczy node, scheduler nie będzie miał jak go uruchomić,
- limity wynikające z implementacji aplikacji (np. pojedynczy worker z jednym wątkiem nie przyspieszy po dodaniu kolejnych rdzeni),
- trudniejsze wykorzystanie fragmentów „dziurawego” klastra – duże Pody gorzej się upakowują.
Przy projektowaniu architektury dobrze jest założyć docelowy zakres: minimalną i maksymalną liczbę replik oraz minimalny i maksymalny rozmiar pojedynczego Poda. Z takim „korytarzem” można później spokojniej stroić autoskalery.
Wpływ architektury aplikacji na strategię skalowania
Architektura serwisu często narzuca preferowany model skalowania. Kilka typowych wzorców:
- mikroserwis HTTP stateless – idealny kandydat do agresywnego skalowania poziomego, małe Pody, szybkie rollouty, duża liczba replik,
- serwis z dużym lokalnym cache – zwykle lepiej reaguje na zwiększanie rozmiaru Poda niż liczby replik, bo cache jest rozproszony i nadmierne rozdrobnienie zwiększa miss ratio,
- komponenty przetwarzające batch – często sprawdza się podejście mieszane: kilka mocniejszych Podów zamiast setek minimalnych, które generują spory narzut sterowania,
- StatefulSet z dużą ilością I/O – tu często limitem jest storage; więcej replik to także więcej równoległych operacji I/O, co może prowadzić do degradacji całej warstwy dyskowej.
Jeśli aplikacja w naturalny sposób skaluje się wewnętrznie (np. sama zarządza pulą workerów na bazie dostępnych rdzeni), rozsądniej jest dać jej większy Pod niż mnożyć repliki. Gdy logika jest prostsza, a kod nie jest przystosowany do skomplikowanego zarządzania wątkami, łatwiej i bezpieczniej dzielić ruch między wiele małych Podów.

Horizontal Pod Autoscaler – mechanizm, którego trzeba się nauczyć
Jak HPA zbiera i interpretuje metryki
Horizontal Pod Autoscaler nie „zgaduje” obciążenia. Opiera się na konkretnych metrykach, które pobiera z komponentu metrics-server lub systemów takich jak Prometheus (poprzez adaptery API). Podstawowe źródła danych to:
- zużycie CPU – w relacji do zdefiniowanych requests,
- zużycie pamięci – częściej jako pomocnicza metryka,
- niestandardowe metryki – np. liczba requestów na sekundę, długość kolejki, latency percentyle.
Sam algorytm jest prosty, ale jego konsekwencje są istotne. Dla CPU HPA liczy średnie wykorzystanie CPU względem requests dla wszystkich Podów w wybranym obiekcie (Deployment/StatefulSet). Jeśli średnia jest wyższa od celu, zwiększa liczbę replik; jeśli niższa – zmniejsza. Skala zmiany wynika z proporcji bieżącego i docelowego poziomu obciążenia.
Dlaczego HPA potrzebuje rozsądnych requests
HPA operuje na procentowym użyciu CPU względem requests. Jeśli requests są kompletnie oderwane od rzeczywistości, dostajemy błędne sygnały skalowania. Dwa typowe przypadki:
- requests drastycznie zawyżone – Pod zużywa w praktyce ułamki zadeklarowanego CPU; HPA „widzi” 10–20% obciążenia, więc nie skaluje, mimo realnych problemów z wydajnością (np. kolejki w bazie danych),
- requests rażąco zaniżone – Pody niemal cały czas „na 200%” względem requests, HPA co chwilę dodaje repliki, a serwis rozlewa się po klastrze bez odczuwalnej poprawy, bo wąskim gardłem jest coś innego.
Strojenie HPA trzeba zawsze łączyć z rzetelną analizą requests. Bez tego algorytm autoskalera jest ślepy lub nadwrażliwy.
Metryki podsawowe vs zaawansowane – kiedy wyjść poza CPU
CPU bywa dobrą przybliżeniem obciążenia, ale nie dla wszystkich aplikacji. Jeśli głównym ograniczeniem jest baza danych, system zewnętrzny lub kolejka, sam CPU niewiele powie o realnym backlogu. W takich sytuacjach przydają się metryki domenowe:
- liczba nierozwiązanych zadań w kolejce (np. lag w Kafka, długość kolejki w RabbitMQ),
- liczba aktywnych sesji lub połączeń,
- czas obsługi pojedynczego żądania (latency p90/p95),
- liczba requestów na sekundę na replikę.
Dobrze zaprojektowany HPA zwykle opiera się na kombinacji: podstawowej metryki technicznej (CPU lub pamięć) oraz jednej, dwóch metryk domenowych, które realnie odzwierciedlają obciążenie biznesowe.
Średnia a percentyle – jak zrozumieć zachowanie HPA
HPA liczy średnie wykorzystanie zasobów między Podami. To oznacza, że kilka bardzo obciążonych replik może zostać „przykrytych” dużą liczbą lekko używanych. Przykład: przy dziesięciu Podach, z których dwa są na 200% CPU, a reszta na 30%, średnia wcale nie wygląda dramatycznie. Tymczasem użytkownicy, którzy trafią na źle działające Pody, odczuwają poważne problemy.
Rozwiązaniem bywa:
- podział Deploymentu na kilka logicznych grup z inną konfiguracją HPA,
- dodatkowe alerty oparte o max lub percentyle zużycia CPU/pamięci między Podami,
- metryki typu „error rate per pod” i ich analiza przy strojenia autoskalera.
Jeśli w obciążeniu są silne piki chwilowe, warto spojrzeć na „agresywność” HPA – ustawienia okresu próbkowania i okna uśredniania wpływają na to, jak szybko reaguje na nagłe skoki.
Cooldown, stabilizacja i ochrona przed „flappingiem”
Źle ustawiony HPA może wprowadzać chaotyczne zmiany: raz zwiększa liczbę replik, za chwilę zmniejsza – i tak w kółko. Powoduje to niepotrzebne restarty, migracje i nieprzewidywalne zachowanie cache’ów. Dlatego w konfiguracji pojawiają się mechanizmy stabilizujące:
- stabilizationWindowSeconds – minimalny czas, jaki HPA czeka przed zmniejszeniem liczby replik, nawet jeśli metryki na to pozwalają,
- maxSurge i maxUnavailable w strategii Deploymentu – kontrola, jak agresywnie można zmieniać liczbę działających Podów podczas skalowania i rolloutów,
- odpowiednio dobrany scan interval (częstotliwość odczytu metryk) – zbyt niski powoduje „nerwowość”, zbyt wysoki – powolne reakcje.
Jeśli aplikacja ma bardzo skokowe obciążenie (np. kilkuminutowe piki), często lepiej tolerować lekko przewymiarowaną liczbę replik niż dopuszczać do nieustannego podnoszenia i obniżania skali.
Typowe pułapki przy konfiguracji HPA
Przy pierwszych podejściach do HPA regularnie powtarza się kilka błędów:
- za wąskie widełki min/maxReplicas – aplikacja ma okresowe piki, ale HPA nie ma jak ich obsłużyć, bo maksymalna liczba replik jest ustawiona „na oko”,
- brak korelacji z limitem bazy danych – HPA dodaje kolejne Pody aplikacji, które wszystkie jednocześnie bombardują bazę, prowadząc do jej zatykania,
- ignorowanie cold startu – przy dużym czasie inicjalizacji Poda HPA reaguje zbyt późno i w momencie peak’u nadal trwa uruchamianie nowych replik,
- brak synchronizacji z PDB – zbyt restrykcyjny PodDisruptionBudget potrafi utrudnić redukcję liczby Podów, co powoduje nieintuicyjne zachowanie przy scale-down.
Przy strojenia HPA warto przez kilka dni obserwować kluczowe metryki (CPU, latency, error rate, backlog) w funkcji liczby replik, zanim wprowadzi się agresywne autoskalowanie na środowisku produkcyjnym.
Cluster Autoscaler i inne poziomy automatycznego skalowania
Rola Cluster Autoscaler – kiedy węzły „oddychają” razem z Podami
Horizontal Pod Autoscaler dba o liczbę Podów. Cluster Autoscaler (CA) pilnuje, żeby w klastrze było wystarczająco dużo węzłów, by te Pody zmieścić. Działa na prostych zasadach:
- jeśli są Pody w stanie Pending z powodu braku zasobów (nie ma node’a, który spełnia ich requests), CA próbuje dodać nowe węzły,
- jeśli jakieś node’y są długo słabo wykorzystane i można „przepakować” z nich Pody na inne węzły, CA usuwa nadmiarowe node’y.
To zachowanie bardzo silnie zależy od jakości zdefiniowanych requests oraz od sposobu konfiguracji nodepooli (rozmiar, typ maszyn, tańsze preemptible/spoty vs standardowe instancje).
Interakcja HPA z Cluster Autoscaler
Gdy HPA zwiększa liczbę replik, może doprowadzić do wygenerowania Podów w stanie Pending. Wtedy następuje reakcja łańcuchowa: Cluster Autoscaler uruchamia nowe węzły, obciążenie się rozlewa, a po jakimś czasie – przy spadku ruchu – węzły zostają zredukowane. Brzmi idealnie, ale wymaga kilku warunków:
- czas startu nowego node’a + rozruch Poda musi być sensowny w relacji do dynamiki ruchu,
- requests Podów powinny pozwalać na ich „przepakowywanie” między node’ami bez powstawania fragmentacji (np. jeden Pod nie zajmuje prawie całego node’a),
- obciążenie nie może być zbyt „burstowe” – przy krótkich, gwałtownych pikach CA często zareaguje dopiero po ich zakończeniu.
Jeśli wzrost ruchu trwa minuty, a start nowego node’a kolejne kilka minut, to czysta reakcja CA/HPA jest zbyt powolna. W takiej sytuacji sprawdza się utrzymywanie minimalnej liczby node’ów i replik, które w całości obsłużą „typowy” ruch, a autoskalery służą raczej do rozwiązywania dłuższych peaków niż sekundowych skoków.
Podstawowe scenariusze konfiguracyjne Cluster Autoscaler
W praktyce pojawiają się trzy dominujące scenariusze:
- pojedynczy nodepool, jednolite maszyny – najprostszy model, łatwy do zrozumienia, ale mało elastyczny kosztowo,
- kilka nodepooli o różnym profilu – np. jeden zoptymalizowany pod CPU-chłonne batch’e, drugi pod serwisy memory-heavy, trzeci pod workloady wymagające GPU,
- mieszanie instancji on-demand i spot/preemptible – część obciążenia (np. mniej krytyczne batch’e) trafia na tańsze, ale niestabilne maszyny.
Dobór scenariusza jest mocno związany z klasami QoS oraz priorytetami Podów. Krytyczne serwisy zwykle przypina się do stabilnych nodepooli (taint/tolerations, nodeSelector), natomiast mniej ważne komponenty mogą lądować tam, gdzie koszt jednostkowy zasobu jest najmniejszy, kosztem ryzyka preemptów.
Inne poziomy automatycznego skalowania
Oprócz HPA i CA istnieją inne mechanizmy, które można włączyć do strategii skalowania:
- Vertical Pod Autoscaler (VPA) – analizuje historię zużycia zasobów i proponuje (lub automatycznie stosuje) nowe values requests/limits dla Podów,
- autoskalowanie baz danych (np. w chmurach zarządzanych) – zwiększanie rozmiaru instancji lub liczby replik, gdy rośnie ruch,
- autoskalowanie warstwy kolejek – np. liczby brokerów lub partitionów.
Najlepsze efekty daje zsynchronizowanie decyzji – jeśli HPA agresywnie dokłada Pody, a baza danych pozostaje sztywna, efekt bywa odwrotny do zamierzonego. W drugą stronę, zwiększenie mocy bazy bez zmiany warstwy aplikacyjnej też nie zawsze wykorzysta potencjał nowej konfiguracji.
Projektowanie zasobów dla aplikacji: jak dobrać requests i limits
Podejście oparte na pomiarach zamiast „zgadywania”
Dobór requests i limits najrozsądniej oprzeć na rzeczywistych danych. Typowy cykl wygląda tak:
- uruchomienie serwisu z konserwatywnymi, ale nie ekstremalnie niskimi wartościami,
- przeprowadzenie testów obciążeniowych lub obserwacja ruchu produkcyjnego przez kilka dni,
- analiza profilu zużycia – średnie, p50/p90/p99 dla CPU i pamięci,
- ustalenie docelowych requests na poziomie „bezpiecznej średniej” z marginesem,
Dobór limits – po co w ogóle je ustawiać
Requests decydują o schedulingu, limits – o tym, ile aplikacja może „zjeść” w trakcie pracy. Oba parametry mają inne konsekwencje:
- CPU limit – przy przekroczeniu Pod jest „dławiony” (throttling). Dla serwisów latency-sensitive zbyt niski limit często psuje czasy odpowiedzi bardziej niż lekka nadrezerwacja zasobów.
- memory limit – po przekroczeniu Poda czeka OOMKill. Z jednej strony chroni to innych współlokatorów node’a, z drugiej – restartujące się Pody w kluczowym serwisie bywają gorsze niż lokalny spike pamięci.
Praktyczny wzorzec dla serwisów HTTP to:
- CPU: zacząć od
limit ≈ 2 × request, potem korygować według testów; dla usług krytycznych czasem bez limitu (tylko request), ale na dobrze odizolowanych node’ach, - RAM:
limit ≈ 1.2–1.5 × requestprzy stabilnym zużyciu, dla komponentów z okresowymi szczytami (np. duże batch’e w pamięci) margines musi być wyższy.
Jeśli aplikacja ma nieprzewidywalne użycie RAM (np. dynamiczne ładowanie dużych modeli czy cache’y), czasem lepszym wyjściem jest brak limitu pamięci i kontrola przez osobny nodepool oraz monitoring, zamiast seryjnych OOMKill.
Relacja między requests/limits a klasą QoS
Kubernetes dzieli Pody na klasy QoS, które mają realny wpływ na zachowanie przy presji zasobów:
- Guaranteed – request = limit dla wszystkich kontenerów (CPU i RAM). Najwyższy priorytet przy OOM na node’zie, najmniejsze ryzyko wywalenia w pierwszej kolejności.
- Burstable – request < limit (lub brak limitu). Może otrzymać dodatkowe zasoby, ale przy globalnym braku pamięci będzie „tańszym” kandydatem do ubicia niż Guaranteed.
- BestEffort – brak requests i limits. Najniższy priorytet, zabijane jako pierwsze przy problemach.
Konsekwencja jest prosta: kluczowe serwisy „za pieniądze” często umieszcza się w Guaranteed, mniej istotne w Burstable, a BestEffort pozostaje dla zadań pomocniczych. Ten podział dobrze współgra z priorytetami Podów i regułami preempcji.
Jak czytać metryki przy projektowaniu zasobów
Surowe wykresy CPU i pamięci niewiele mówią, jeśli nie są osadzone w kontekście. Przy projektowaniu requests/limits przydają się co najmniej trzy zestawy danych:
- profil czasowy – jak zużycie zasobów zmienia się w ciągu dnia/tygodnia, czy widać wyraźne szczyty, czy raczej stabilny poziom,
- zależność od ruchu – wykres QPS vs CPU/RAM, pozwalający oszacować „koszt” jednego requestu,
- powiązanie z latency i error rate – czy przy rosnącym CPU czasy odpowiedzi rosną liniowo, czy następuje punkt załamania.
Na tej podstawie można policzyć, ile replik jest potrzebnych dla zadanej przepustowości przy zachowaniu akceptowalnego czasu odpowiedzi, a następnie przełożyć to na requests i widełki HPA.
Jak często aktualizować requests i limits
Konfiguracja zasobów nie jest „na zawsze”. Zmiany kodu, bibliotek, algorytmów cache’owania, a nawet inny rozkład ruchu potrafią kompletnie zmienić profil użycia CPU/RAM. Sensowny rytm przeglądu to:
- po większym releasie architektonicznym lub migracji wersji środowiska (runtime, JVM, Python, .NET),
- po istotnej zmianie „kroku” ruchu – np. wejście nowego klienta B2B, kampania marketingowa,
- cyklicznie, np. raz na kwartał, dla serwisów o największym koszcie infrastruktury.
Przy dużej liczbie mikroserwisów ręczne utrzymanie staje się nieefektywne i zwykle prowadzi do wdrożenia VPA w trybie „recommender” albo budowy własnych automatyzacji.
Wzorce i antywzorce w definiowaniu zasobów
W konfiguracjach z produkcji powtarza się kilka powtarzalnych schematów. Uporządkowanie ich pomaga uniknąć oczywistych pułapek.
Przykładowe wzorce:
- konserwatywne requests, umiarkowane limity – dobre dla typowych API, umożliwia podschedulowanie wielu Podów na node bez masowych throttlingów,
- osobne profile dla batch i online – procesy wsadowe z wysokimi requests/limits na CPU, trzymane na dedykowanych node’ach, aby nie degradować ruchu online,
- twarde limity pamięci dla „uciekających” procesów – celowe OOMKill w razie wycieku lub błędu, zamiast równoczesnego zabicia wszystkiego na node’zie.
Antywzorce pojawiają się równie często:
- „jeden szablon dla wszystkich serwisów” – kopiowanie tych samych wartości CPU/RAM do każdego Deploymentu, bez względu na charakter obciążenia,
- brak requests, tylko limity – chaos w schedulingu, nieprzewidywalne zachowanie przy wysokim obciążeniu, rozbieżność z planami HPA,
- nadmiernie wysokie requests „na wszelki wypadek” – fragmentacja zasobów, niedoskalowanie node’ów przez CA, „Pending” mimo pozornie wolnych maszyn.
Uwzględnienie zależności zewnętrznych przy projektowaniu zasobów
Aplikacja rzadko działa w próżni. Jeśli proces jest mocno związany z zewnętrznymi systemami (baza danych, cache, zewnętrzne API), maksymalna liczba replik, a więc i łączne requests muszą szanować te ograniczenia.
Typowe techniki to:
- limitowanie współbieżności na poziomie aplikacji (semafory, connection pool size) – tak, aby dodatkowe Pody nie generowały lawinowych timeoutów,
- koordynacja z zespołem od bazy danych – uzgodnienie maksymalnej liczby połączeń oraz parametrów autoskalowania DB,
- osobne klasy priorytetów dla batch’y obciążających te same źródła danych co ruch online.
Skalowanie „w ciemno”, bez spojrzenia na łańcuch zależności, zwykle kończy się tak, że najsłabsze ogniwo (najczęściej baza lub zewnętrzne API) wymusza nowe, tym razem twardsze limity.
Praktyczna konfiguracja HPA – od prostego do zaawansowanego scenariusza
Minimalny HPA oparty wyłącznie na CPU
Najprostszy przypadek to autoskalowanie Deploymentu HTTP po CPU. Przykładowa konfiguracja:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
Przy takiej konfiguracji Pod z request: 200m będzie dążył do średniego użycia ~140m CPU. Jeśli średnia wśród replik przekroczy ten poziom, HPA stopniowo zwiększy liczbę Podów, aż do maxReplicas.
Dodanie pamięci i podstawowych reguł stabilizacji
Kolejny krok to rozszerzenie metryk o RAM oraz wprowadzenie prostych reguł ograniczających zbyt gwałtowne zmiany. Używając API v2, można doprecyzować zachowanie scale-up/scale-down:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa-advanced
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 3
maxReplicas: 20
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 20
periodSeconds: 60
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 65
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 75
Ta konfiguracja pozwala szybko rosnąć (podwajanie liczby replik co minutę przy wysokim obciążeniu), ale ogranicza tempo redukcji do 20% na minutę, z pięciominutowym oknem stabilizacji przy scale-down. Dzięki temu przy krótkich pikach ruchu nie nastąpi natychmiastowe „ścięcie” liczby Podów.
HPA z wykorzystaniem metryk z Prometheusa
W wielu systemach kluczową informacją nie jest sam CPU, ale np. liczba requestów w kolejce, czas przetwarzania zadania czy średnia liczba zapytań na sekundę. Przy stacku z Prometheusem i adapterem metrics-server można zdefiniować HPA w oparciu o metryki niestandardowe:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: worker-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: worker
minReplicas: 1
maxReplicas: 50
metrics:
- type: Pods
pods:
metric:
name: queue_backlog
target:
type: AverageValue
averageValue: "50"
W tym przykładzie oczekiwany jest backlog ~50 zadań na Pod. Jeśli metryka queue_backlog raportowana przez każdy Pod rośnie, HPA zwiększy liczbę replik tak, aby przeciętna liczba zadań w kolejce na Pod wróciła do zadanej wartości. To zdecydowanie lepiej koreluje z rzeczywistym obciążeniem biznesowym niż sam CPU.
Łączenie metryk technicznych i biznesowych w jednym HPA
Częsty, praktyczny kompromis: HPA ma kilka metryk, z których każda może wywołać skalowanie. Konfiguracja z autoskalowaniem po CPU lub backlogu kolejki może wyglądać tak:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: worker-hpa-mixed
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: worker
minReplicas: 2
maxReplicas: 40
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Pods
pods:
metric:
name: queue_backlog
target:
type: AverageValue
averageValue: "30"
Silne obciążenie CPU przy niskim backlogu (np. z powodu algorytmu intensywnie liczącego na małej liczbie zadań) nadal może wywołać skalowanie. Z drugiej strony, długa kolejka przy jeszcze niskim CPU (np. po restarcie node’a, zanim CPU zdąży skoczyć) wywoła reakcję szybciej, niż czekanie na średnią z wykorzystania procesora.
Scenariusz z długim cold startem i wolnym Cluster Autoscalerem
Problematyczny, ale częsty przypadek: aplikacja ma długi cold start (np. ładuje duże modele ML, kompiluje coś przy starcie), a jednocześnie nowy node w chmurze uruchamia się kilka minut. Same widełki HPA nie wystarczą – trzeba zsynchronizować kilka elementów:
- podnieść minReplicas tak, aby stała część floty była zawsze „rozgrzana”,
- wydłużyć stabilizationWindowSeconds dla scale-down, aby nie zabijać zbyt szybko rozgrzanych Podów,
- zwiększyć minimalną liczbę node’ów w nodepoolu, jeśli cold start node’a jest równie bolesny.
Część zespołów rozwiązuje to przez utrzymywanie nadmiarowych replik w dzień i agresywne redukowanie w nocy, gdy ruch jest przewidywalnie niski. W takim przypadku HPA działa w tandemie z ręcznymi (lub zaplanowanymi) zmianami wartości minReplicas.
Rozdzielenie Deploymentów o różnych profilach skalowania
Nie każdy fragment serwisu powinien skalować się tak samo. Jeśli w ramach jednej aplikacji jedna część obsługuje wolnozmienny ruch batch, a druga frontend API, sensowne jest wydzielenie ich do osobnych Deploymentów i przypięcie różnych HPA:
- API – agresywne scale-up po CPU/QPS, krótkie okno próbkowania, umiarkowane scale-down,
- batch – skalowanie głównie po backlogu zadań, większe widełki min/max, dłuższe okna stabilizacji, aby uniknąć ciągłego „rozbijania” długich jobów.
W praktyce sprowadza się to do przeorganizowania komponentów tak, aby każdy Deployment miał w miarę jednorodne wymagania wydajnościowe i mógł korzystać z dedykowanej logiki autoskalowania.
Integracja HPA z priorytetami i preempcją
Gdy w klastrze współistnieją obciążenia krytyczne i pomocnicze, przydaje się PriorityClass i preempcja. Typowy schemat:
- serwisy produkcyjne online – wysoka PriorityClass, sensowne requests/limits, HPA gwarantuje im zasoby w pierwszej kolejności,
- zadania backoffice / batch – niższa PriorityClass, HPA może je skalować w górę, ale przy presji zasobów Pody są wypierane na rzecz krytycznych usług.
Najczęściej zadawane pytania (FAQ)
Po co w ogóle skalować aplikacje w Kubernetes zamiast „ręcznie” zwiększać zasoby?
Skalowanie w Kubernetes pozwala automatycznie dopasować zasoby do realnego obciążenia. Dzięki temu aplikacja jest dostępna i responsywna w szczycie ruchu, a w spokojnych godzinach nie „spala” niepotrzebnie CPU i pamięci. Zamiast ręcznie podnosić kolejne instancje, definiujesz zasady, a kontrolery same pilnują ich realizacji.
Dodatkowa korzyść to powtarzalność i przewidywalność. Mechanizmy skalowania (HPA, Cluster Autoscaler, requests/limits) są zapisane w manifestach i wersjonowane razem z kodem. Jeśli trzeba odtworzyć środowisko lub przeanalizować incydent, wiesz dokładnie, jakie były reguły skalowania w danym momencie.
Na czym polega różnica między skalowaniem aplikacji na VM a w Kubernetes?
W modelu VM zwykle skalujesz całe maszyny – dodajesz nowe instancje za load balancerem albo zmieniasz rozmiar istniejącej VM. Autoskalery w chmurze patrzą na metryki z poziomu maszyny (np. CPU całej VM), a aplikacja jest często monolitem działającym w kilku kopiach.
W Kubernetes jednostką skalowania jest Pod, a nie maszyna. Horizontal Pod Autoscaler zmienia liczbę replik konkretnego Deploymentu lub StatefulSetu, a scheduler rozmieszcza je na dostępnych węzłach. Dodatkowo niemal każdy mikroserwis może mieć własną politykę skalowania – nie ma jednego „globalnego” autoskalera dla całej aplikacji.
Czym różnią się requests i limits zasobów w Kubernetes i jak wpływają na skalowanie?
Requests to deklaracja minimalnych zasobów, których Pod potrzebuje, aby działać. Scheduler używa requests, aby zdecydować, na którym node’zie może umieścić Poda. Jeśli suma requests na node’zie przekroczyłaby jego możliwości, nowe Pody nie zostaną tam umieszczone. Limits to twardy sufit – CPU może być dławione (throttling), a po przekroczeniu limitu pamięci kontener zostanie ubity przez OOMKiller.
Dla skalowania ma to kilka konsekwencji. Zawyżone requests powodują, że scheduler szybciej „zapełnia” node’y, a Cluster Autoscaler może niepotrzebnie dodawać nowe węzły. Zbyt ciasne limits (szczególnie pamięci) prowadzą do restartów Podów. Dobrze dobrane requests i limits są więc warunkiem sensownej pracy zarówno HPA, jak i autoskalera klastra.
Jak działa Horizontal Pod Autoscaler (HPA) w Kubernetes w praktyce?
HPA cyklicznie odczytuje metryki (np. zużycie CPU, czasami niestandardowe metryki z Prometheusa) i porównuje je z celem zdefiniowanym w konfiguracji, np. średnie wykorzystanie CPU na poziomie 70%. Jeśli rzeczywiste zużycie jest wyższe, zwiększa liczbę replik; jeśli niższe – zmniejsza, ale w granicach minReplicas i maxReplicas.
Żeby HPA działał sensownie, potrzebne są wiarygodne metryki oraz dostępne zasoby w klastrze. Jeśli HPA próbuje dodać repliki, a na node’ach nie ma wolnych zasobów spełniających requests, nowe Pody trafią w stan Pending. Bez poprawnie skonfigurowanego Cluster Autoscalera, użytkownicy nie zobaczą żadnej poprawy, mimo że HPA formalnie „skalował” aplikację.
Co to jest Cluster Autoscaler i kiedy jest potrzebny?
Cluster Autoscaler (CA) to komponent, który skaluje liczbę węzłów (node’ów) w klastrze na podstawie aktualnych potrzeb. Jeśli w klastrze pojawiają się Pody w stanie Pending, bo brakuje miejsca spełniającego ich requests, CA może dodać nowe node’y. Gdy część node’ów jest trwale nieużywana, może je usunąć, obniżając koszty infrastruktury.
CA jest kluczowy przy dynamicznych lub trudnych do przewidzenia obciążeniach, gdzie samo skalowanie liczby Podów nie wystarcza. Jeżeli aplikacja rośnie w godzinach szczytu, ale klaster ma sztywno ustawioną liczbę node’ów, HPA szybko „dobije” do ściany i nowe repliki nie będą miały gdzie się uruchomić.
Jak dobrać requests i limits CPU/pamięci dla Podów w Kubernetes?
Punkt wyjścia to realne metryki z produkcji lub zbliżonego środowiska testowego. Najprostszy schemat to: zacząć od konserwatywnych wartości na podstawie obserwacji, po czym iteracyjnie je korygować. Dla CPU często ustawia się request niższy niż typowe zużycie „w szczycie”, a limit wyżej, aby aplikacja mogła wykorzystać chwilowo wolne zasoby.
Przy pamięci lepiej unikać agresywnego przycinania. Zbyt niski limit szybko zamieni się w serię OOMKill i restartów. Częsta praktyka to ustawienie request nieco poniżej typowego zużycia, a limit z marginesem, który pokrywa skoki np. podczas garbage collection czy większych batchy.
Jak klasy QoS (Guaranteed, Burstable, BestEffort) wpływają na zachowanie aplikacji przy braku zasobów?
Klasa QoS określa, w jakiej kolejności Pody będą zabijane przy presji na zasoby, głównie pamięć. Pody w klasie Guaranteed (requests == limits dla CPU i pamięci) mają najwyższy priorytet i są usuwane jako ostatnie. Burstable to środek stawki – mają ustawione requests i/lub limits, ale nie spełniają warunku Guaranteed.
Pody BestEffort (brak zdefiniowanych requests i limits) są traktowane najgorzej – przy OOM to one wylecą z klastra w pierwszej kolejności. Jeśli dany serwis jest krytyczny biznesowo, nie powinien działać jako BestEffort. Lepszym podejściem jest przypisanie mu realistycznych requests i limits oraz dopasowanie polityki skalowania do jego roli w systemie.






