Dlaczego skalowanie mikroserwisów „na zapas” kończy się przepalaniem budżetu
Mikroserwisy w Kubernetes potrafią „jakoś działać” niemal w każdych warunkach: wystarczy podnieść requesty, dodać repliki i powiększyć klaster. Problem pojawia się wtedy, gdy rachunek z chmury przestaje mieć sens, a mimo to system potrafi i tak dławić się pod obciążeniem. Różnica między „działa” a „działa tanio i stabilnie” to świadome podejście do skalowania, oparte na danych, a nie na strachu.
Częsta reakcja na pierwsze incydenty wydajnościowe jest bardzo ludzka: podwój zasoby, zwiększ limit replik, dorzuć większe węzły. To zdejmuje napięcie na chwilę, ale ma cenę: konto w chmurze zaczyna rosnąć szybciej niż przychód, a przy kolejnych wzrostach ruchu nie ma już prostego „x2”, bo wszystko i tak jest już spuchnięte.
Skalowanie mikroserwisów bez marnowania zasobów oznacza podejście iteracyjne: uruchamianie usług z możliwie minimalną, ale stabilną konfiguracją, obserwowanie zachowania w realnym ruchu, stopniową korektę oraz powiązanie autoskalowania z metrykami technicznymi i biznesowymi. Brzmi technicznie, ale w praktyce sprowadza się do kilku prostych nawyków, które można wdrożyć nawet w istniejącym już klastrze.
Sprawa nie dotyczy tylko kosztów. Zbyt agresywne lub źle skonfigurowane skalowanie powoduje niestabilność: częste restarty podów, thrashing GC, wąskie gardła w jednym mikroserwisie, podczas gdy cała reszta klastra się nudzi. Trudniej jest też debugować incydenty, bo nie wiadomo, czy problem wynika z kodu, czy z konfiguracji autoskalera. O wiele łatwiej żyje się zespołowi, gdy skalowanie jest przewidywalne, a użycie CPU i RAM ma sensowny związek z realnym ruchem.
Kiedy traktuje się skalowanie jako proces, a nie jednorazową „akcję ratunkową”, zmienia się sposób podejmowania decyzji. Zamiast pytać „ile CPU dać temu serwisowi?”, lepiej zacząć od pytań: „jak ten serwis zachowuje się pod obciążeniem?”, „jaką ma charakterystykę pamięci?”, „czy skaluje się lepiej w poziomie, czy w pionie?”. Odpowiedzi nie trzeba zgadywać – da się je wyciągnąć z prostych testów, metryk i logów.
Podstawy zasobów w Kubernetes – co naprawdę wpływa na skalowanie
Requesty i limity CPU/RAM w Deploymentach
Request i limit to dwa najważniejsze parametry, które decydują, jak Kubernetes przydziela zasoby Twoim podom. Request oznacza gwarantowaną ilość CPU/RAM, którą scheduler bierze pod uwagę układając pody na węzłach. Limit to maksymalna ilość zasobów, jaką kontener może wykorzystać. Różnica między nimi wpływa na klasę QoS podów i to, które z nich wylecą jako pierwsze przy presji na zasoby.
Jeśli request CPU jest ustawiony na 200m (0,2 vCPU), a limit na 500m, scheduler będzie liczył, że ten pod „zużywa” 200m CPU przy układaniu go na node’ach. Jeśli limit pamięci wynosi 512Mi, a request 256Mi, to pod ma gwarantowane 256Mi, ale może korzystać aż do 512Mi. Po przekroczeniu limitu pamięci kernel ubije kontener (OOMKilled). Przy CPU przekroczenie limitu powoduje „dławienie” (CPU throttling), a nie zabicie procesu.
Brak ustawionych requestów i limitów oznacza, że pod może (teoretycznie) zużyć cały dostępny CPU/RAM na węźle. W małym klastrze deweloperskim to „działa”, ale w produkcji prowadzi do chaosu: scheduler nie ma sensownych danych, jak zapakować pody, jedne usługi zjadają zasoby innym, a diagnozowanie problemów wydajnościowych jest jak wróżenie z fusów.
Prosty przykład fragmentu Deploymentu z rozsądnymi ustawieniami:
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "300m"
memory: "512Mi"
Taka konfiguracja mówi: „ten mikroserwis zazwyczaj potrzebuje 0,1 vCPU i 256Mi RAM, ale może chwilowo urosnąć do 0,3 vCPU i 512Mi”. Scheduler pakuje pody według requestów, a limity chronią przed niekontrolowanym rozrostem. Złą praktyką jest ustawianie identycznych, bardzo wysokich requestów i limitów „żeby być bezpiecznym” – to prosta droga do overprovisioningu.
Jak scheduler widzi zasoby węzłów i wpływ na binpacking
Kube-scheduler nie zna Twojej aplikacji. Widzi tylko liczby: ile CPU i RAM ma węzeł, ile jest już „zarezerwowane” przez istniejące pody (requests) oraz jakie są wymagania nowych podów. Na tej podstawie decyzje o umieszczeniu podów są czysto matematyczne: dopasowanie sumy requestów do dostępnych zasobów z uwzględnieniem reguł typu nodeSelector, taints/tolerations, affinity.
Jeśli requesty są zawyżone, scheduler uzna, że węzeł jest szybciej zapchany, niż jest w rzeczywistości. To powoduje słabe „upakowanie” (binpacking): powstaje wiele węzłów z niewykorzystanym CPU/RAM, ale formalnie „pełnych” według requestów. Autoscaler klastra, widząc „brak miejsca” dla nowych podów, zacznie dodawać kolejne węzły, a rachunek wzrośnie, mimo że rzeczywiste użycie zasobów jest umiarkowane.
Brak requestów też ma swoją cenę. Kiedy nie określisz requestów, Kubernetes przyjmuje domyślne wartości, a Ty tracisz kontrolę nad tym, jak usługi konkurują o zasoby. W sytuacji presji na pamięć kernel zaczyna ubijać pody na podstawie klasy QoS (BestEffort, Burstable, Guaranteed). Mikroserwis bez requestów i limitów może skończyć w klasie BestEffort, czyli „do odstrzału” jako pierwszy, nawet jeśli jest krytyczny biznesowo.
Dobrze dobrane requesty to podstawa efektywnego binpackingu. Chcesz, by scheduler układał pody tak, aby węzły były możliwie pełne, ale nie przepełnione. Do tego potrzebne są wartości oparte na rzeczywistym użyciu, a nie na intuicji.
Przykładowe dobre i złe ustawienia zasobów
Prosty kontrast pomaga szybko wychwycić antywzorce. Załóżmy mikrousługę HTTP w Go, która obsługuje ruch o umiarkowanej intensywności:
# Antywzorzec – „na wszelki wypadek”
resources:
requests:
cpu: "1000m"
memory: "1024Mi"
limits:
cpu: "2000m"
memory: "2048Mi"
# Bardziej realistyczne podejście
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "250m"
memory: "512Mi"
W pierwszym wariancie każdy pod „zjada” 1 vCPU i 1Gi RAM w kalkulacji schedulera, niezależnie od tego, czy używa realnie 5%, czy 80%. Klaster szybciej się skaluje w górę, a wykorzystanie węzłów jest słabe. W drugim podejściu requesty zostały dostosowane do realnych potrzeb na podstawie metryk, a limity dają niewielki zapas na piki.
Drugi typowa pułapka to brak limitów pamięci. Aplikacja w Javie czy Node.js, bez limitu RAM, ma tendencję do „puchnięcia” w czasie – GC i alokacje buforów przestają mieć twardy sufit. Gdy na węźle zbierze się kilka takich podów, jeden z nich wygrywa wyścig o pamięć, a reszta jest ubijana losowo. Dlatego nawet jeśli nie potrafisz jeszcze dobrać idealnych limitów, lepiej ustawić rozsądny sufit i sukcesywnie go korygować.

Rodzaje skalowania w Kubernetes: poziome, pionowe i skalowanie klastra
Skalowanie podów w poziomie (HPA)
Skalowanie poziome (horizontal scaling) polega na zwiększaniu lub zmniejszaniu liczby replik podów obsługujących daną usługę. W Kubernetes odpowiada za to Horizontal Pod Autoscaler (HPA). To narzędzie obserwuje metryki (CPU, pamięć lub metryki niestandardowe) i zgodnie z regułami przelicza potrzebną liczbę replik.
Dla mikroserwisów HTTP HPA jest zwykle pierwszym wyborem, bo łatwo jest równolegle przetwarzać żądania w wielu instancjach. Load balancer (Service) rozkłada ruch, a każda replika jest w miarę niezależna. O ile mikroserwis jest stateless lub dobrze współdzieli stan (np. cache, baza), skalowanie poziome działa bardzo przewidywalnie.
HPA odpowiada na pytanie „ile kopii tej usługi jest teraz potrzebne”, ale sam z siebie nie wymusza zmian rozmiaru pojedynczej repliki. Jeśli jeden pod ma za mało pamięci i często jest OOMKilled, to zwiększenie liczby podów nie rozwiązuje problemu – tylko go multiplikuje. Dlatego HPA potrzebuje sensownego punktu startowego w postaci requestów i limitów.
Skalowanie zasobów podów w pionie (VPA)
Skalowanie pionowe (vertical scaling) to zmiana wielkości pojedynczej repliki: przydzielanie jej więcej CPU lub RAM, zamiast dokładania kolejnych kopii. W Kubernetes wspomaga to Vertical Pod Autoscaler (VPA), który na podstawie historycznych metryk rekomenduje lub automatycznie modyfikuje requesty i limity zasobów dla podów.
VPA potrafi odpowiedzieć na pytanie: „czy ten serwis jest za tłusty lub za chudy pod względem konfiguracji zasobów?”. Może stwierdzić, że mikroserwis, któremu ręcznie ustawiono 1000m CPU i 1Gi RAM, w praktyce używa średnio 150m i 300Mi, więc spokojnie można obniżyć requesty i limity bez utraty stabilności. Albo odwrotnie – że serwis non-stop zbliża się do limitu pamięci i wymaga większego przydziału, żeby uniknąć restartów.
W odróżnieniu od HPA, który dostosowuje liczbę replik, VPA modyfikuje rozmiar pojedynczego poda. Dlatego łączenie obu mechanizmów wymaga nieco ostrożności. Typowy wzorzec to używanie HPA do skalowania kopiami i VPA w trybie „Off” lub „Initial” jako źródła rekomendacji dla requestów.
Skalowanie węzłów (Cluster Autoscaler / Karpenter)
Trzeci poziom to skalowanie samego klastra: liczby i rozmiaru węzłów (node’ów). Narzędzia takie jak Cluster Autoscaler (CA) czy Karpenter obserwują, czy w klastrze jest wystarczająco zasobów do uruchomienia wszystkich podów. Jeśli scheduler nie może ulokować nowych podów z powodu braku CPU/RAM, autoscaler dodaje nowe węzły. Gdy zasoby są długo nieużywane, węzły są usuwane.
W praktyce oznacza to, że agresywne HPA „przenosi” problem na poziom klastra. Jeśli HPA nagle podniesie liczbę replik z 5 do 50, a każdy pod ma duże requesty, Cluster Autoscaler zacznie masowo dodawać maszyny. Przy braku sensownych limitów i requestów może dojść do sytuacji, w której klaster wygląda na przeładowany, choć rzeczywiste wykorzystanie CPU jest dalekie od 100%.
Skalowanie klastra musi być zsynchronizowane ze strategią podów: rozmiarem pojedynczych instancji, typem workloadu (batch vs HTTP), tolerancją na opóźnienia przy podnoszeniu nowych node’ów. Wiele organizacji traci pieniądze nie przez sam HPA, ale przez nieprzemyślaną kombinację: gargantuiczne węzły, przeogromne requesty i brak podziału na pule node’ów o różnych profilach.
Jak dobrać początkowe requesty i limity zasobów bez zgadywania
Dane wejściowe: testy obciążeniowe, metryki z produkcji, logi
Ustawianie requestów i limitów „z głowy” kończy się zwykle tym, że są one albo groteskowo wysokie, albo zbyt niskie i prowokują niestabilność. Dużo pewniejsze są trzy źródła danych: proste testy obciążeniowe, metryki z rzeczywistego środowiska oraz logi (w tym logi GC, jeśli używasz JVM).
Nawet krótki test obciążeniowy potrafi dać cenne informacje. Uruchom mikroserwis na lokalnym klastrze lub środowisku staging z minimalną konfiguracją (np. 100m CPU, 256Mi RAM, 1 replika) i przepuść przez niego ruch zbliżony do oczekiwanego ruchu produkcyjnego. Obserwuj: użycie CPU, stabilność czasu odpowiedzi, zużycie pamięci w czasie (czy rośnie liniowo, czy stabilizuje się).
Kiedy usługa już działa w produkcji, promieniowo więcej mówią realne metryki: średnie użycie CPU na podzie, 95/99 percentyl użycia pamięci, maksymalne piki przy wyższych godzinach. Jeśli używasz Prometheusa, kilka prostych zapytań pozwala zobaczyć, czy mikroserwis jest „przejedzony”, czy „głodny” zasobów.
Logi dostarczają kontekstu: jeśli w logach pojawiają się komunikaty o długim GC, OOM, timeoute’ach do bazy, to znak, że konfiguracja zasobów nie pasuje do charakteru aplikacji. W serwisach w Javie warto włączyć uproszczone logi GC i sprawdzić, czy garbage collector nie spędza zbyt dużo czasu na sprzątaniu przy małej ilości pamięci.
Metodologia „minimalnego stabilnego zestawu”
Prosty sposób na sensowne startowe wartości to podejście „minimalnego stabilnego zestawu”. Zamiast zgadywać idealne wartości, zaczynasz od małych, ale realistycznych i zwiększasz je tylko wtedy, gdy obserwujesz konkretne symptomy problemów.
- Ustaw niskie, lecz nie śmiesznie małe requesty, np. 100m CPU i 256Mi RAM dla lekkich serwisów HTTP.
- Dodaj niewielki bufor w limitach, np. 2–3x requestu CPU i około 2x requestu RAM.
- Przepuść przez serwis kontrolowany ruch (staging lub wydzielony fragment produkcji).
- Obserwuj użycie zasobów i zachowanie: czas odpowiedzi, liczba błędów, restartów.
- Jeśli metryki są stabilne, a zapas zasobów nie jest wykorzystywany – stopniowo obniżaj limity.
Stopniowe korygowanie na podstawie obserwacji
Po pierwszym podejściu nie trzeba od razu „betonować” konfiguracji na lata. Dużo bezpieczniej jest patrzeć, jak aplikacja zachowuje się w kilku scenariuszach: normalny dzień, godziny szczytu, incydenty w systemach zależnych (np. spowolniona baza, pad Cache).
- Jeśli CPU przez większość czasu stoi w okolicach kilku procent, a opóźnienia są niskie – obniżaj stopniowo limity CPU, a potem requesty, aż zauważysz pierwsze delikatne pogorszenie metryk. To będzie praktyczna dolna granica.
- Jeśli pamięć „pełza” w górę i nie wraca do poprzedniego poziomu po spadku ruchu – zwiększ limit RAM i przyjrzyj się wzorcom alokacji (profilowanie, logi GC). Tak często wychodzą na jaw memory leak’i, a nie same problemy z limitem.
- Jeśli HPA często podbija liczbę replik przy niewielkich zmianach ruchu, request CPU jest zwykle zbyt wysoki w stosunku do realnego użycia lub metryka skalowania jest zbyt czuła.
Takie korekty najlepiej robić małymi krokami – po 10–20% – zamiast skakać o rząd wielkości. Nawet jeśli pierwsze ustawienia są dalekie od perfekcji, seria małych korekt połączona z obserwacją prawie zawsze prowadzi do sensownych wartości w rozsądnym czasie.
Gotowe „szablony” zasobów dla typowych typów serwisów
Gdy zespół ma kilkadziesiąt mikroserwisów, trudno każdemu poświęcić tyle samo uwagi. Pomaga prosty katalog „rozmiarówek”, z którymi startuje się na początku, a potem je koryguje:
- Lekki serwis HTTP w Go / Rust / Node: startowo 50–150m CPU, 128–256Mi RAM, limit CPU 2–3x request, RAM 1.5–2x request.
- Serwis w Javie / .NET: 200–500m CPU, 512Mi–1Gi RAM jako punkt wyjścia, z dopasowanym Xmx (np. ~60–70% limitu RAM).
- Workload batch / cronjob: wyższe limity CPU (żeby „przepalić” zadanie szybciej), ale krótszy czas życia poda – często korzystne jest przeszacowanie CPU, jeśli czas wykonania jest biznesowo krytyczny.
To tylko szkielet. Często już po kilku dniach zbierania metryk widać, które klasy trzeba poszerzyć, a które spokojnie „odchudzić”. Dla zespołu daje to jednak psychiczny komfort, że nie startuje się kompletnie w ciemno.
HPA w praktyce – skuteczne autoskalowanie podów, nie tylko po CPU
Dlaczego sama metryka CPU bywa zdradliwa
Domyślnie HPA w wielu klastrach patrzy jedynie na CPU. Na pierwszy rzut oka to wygodne: CPU jest łatwe do pomiaru i obsługiwane „z pudełka”. W praktyce szybko wychodzą ograniczenia:
- Serwis I/O-bound (dużo czekania na bazę, zewnętrzne API) może mieć niskie użycie CPU, a przy tym rosnące czasy odpowiedzi i kolejki żądań.
- Serwis z ciężką serializacją JSON/Protobuf może skakać z CPU w górę przy krótkich pikach, choć 95% czasu działa spokojnie – co prowadzi do „pompowania” replik bez realnej korzyści.
- Agresywna optymalizacja w kodzie (np. batchowanie zapytań) może spowodować, że CPU będzie wyglądało idealnie, ale opóźnienia dla użytkownika już nie.
Mikroserwis często powinien skalować się nie „bo CPU wysoki”, ale „bo kolejka rośnie” lub „czas odpowiedzi zbliża się do progu SLA”. To wymaga spojrzenia szerzej na metryki niż sam CPU.
Dobór metryki skalowania do typu obciążenia
Najprościej zacząć od pytania: co jest prawdziwym wąskim gardłem mojego serwisu? W zależności od odpowiedzi inaczej konfigurujesz HPA.
- Serwisy CPU-bound (przetwarzanie obrazów, raporty, ciężka kryptografia): skalowanie po CPU ma sens, bo każda dodatkowa replika rzeczywiście odciąża procesor. Można użyć targetu w stylu 60–70% CPU.
- Serwisy I/O-bound (HTTP, kolejki, usługi proxy): lepszy jest rps-based scaling (żądania na sekundę na pod) lub metryki kolejek (długość kolejki, liczba wiadomości w topicu).
- Serwisy pamięciowo intensywne (cache in-memory, analityka w RAM): metryka pamięci (użycie % limitu) będzie ważniejsza niż CPU, żeby uniknąć lawiny OOMów.
Jeśli maszynka metryk to Prometheus, łatwo wystawić np. liczbę obsłużonych żądań HTTP per pod i z niej policzyć RPS. Potem, przy pomocy prometheus-adapter albo innego adaptera, można udostępnić tę metrykę jako niestandardową dla HPA.
Przykładowa konfiguracja HPA z metryką niestandardową
Dla serwisu HTTP skalowanego po RPS na replikę można skonfigurować HPA mniej więcej tak:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-service
minReplicas: 2
maxReplicas: 50
metrics:
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "50" # 50 RPS na pod
W tym wariancie HPA dąży do utrzymania około 50 żądań na sekundę na jedną replikę. Przy 500 RPS w sumie zaproponuje około 10 replik, przy 1000 – około 20 itd. CPU jest wciąż kontrolowane przez requesty i limity, ale decyzja o liczbie podów bierze pod uwagę realny ruch.
Unikanie „ping-ponga” replik (oscylacji)
Częsta obawa przy HPA to skakanie replik góra–dół przy każdym małym wahnięciu metryk. Objawia się to logami w stylu: 5 → 10 → 6 → 9 replik w ciągu godziny, bez wyraźnego zewnętrznego powodu.
Żeby to uspokoić, warto wykorzystać kilka mechanizmów:
- Stabilization window – czas, w którym HPA „uśrednia” decyzję o skalowaniu w dół, np. 5–10 minut.
- Histereza w targetach – lekkie rozszerzenie akceptowalnego zakresu (np. 40–70% CPU zamiast sztywnego 50%).
- Ostrożne minima i maksima – nieustawianie maksymalnej liczby replik absurdalnie wysoko, jeśli backendy nie wytrzymają takiej równoległości.
W praktyce często wystarczy wydłużyć stabilization window dla scale-down, żeby serwis nie zwijał replik natychmiast po chwilowej poprawie sytuacji. Lepiej trzymać kilka minut „nadmiaru” niż co chwilę ubijać i znów startować pody.
Łączenie HPA z requestami zasobów
HPA liczy docelową liczbę replik, ale to scheduler musi je faktycznie upchnąć na węzłach. Jeżeli requesty są mocno przeszacowane, HPA może policzyć, że potrzeba np. 20 podów, a klaster doda kilka dużych maszyn tylko dlatego, że „papierowy” CPU się nie mieści, choć realne użycie byłoby spokojnie obsłużone przez mniejszy klaster.
Ustawiając HPA, dobrze jest więc:
- Najpierw skalibrować requesty (przynajmniej mniej więcej) na podstawie metryk.
- Później dobrać target metryki (CPU, RPS, pamięć) do tych requestów, zamiast próbować łatać złe requesty HPA–ką.
- Przy dużych różnicach obciążenia dobowego – rozważyć krzywe czasowe (np. zmiana limitów / minReplicas w zależności od godziny, przez dodatkowy kontroler lub pipeline CI/CD).

VPA i inne mechanizmy „autotuningu” zasobów
Tryby działania VPA i typowe pułapki
Vertical Pod Autoscaler potrafi być świetnym pomocnikiem, ale wciągnięty „na żywca” w produkcję bywa bolesny. Działa w kilku trybach:
- Off – tylko zbiera dane i wystawia rekomendacje, nie zmienia niczego w podach.
- Initial – ustawia requesty/limity przy tworzeniu nowego poda, potem ich nie rusza.
- Auto – modyfikuje zasoby istniejących podów, co najczęściej oznacza ich restarty.
W większości organizacji najlepszym startem jest tryb Off. VPA spokojnie zbiera statystyki, a zespół ogląda rekomendacje i porównuje je z realnym użyciem oraz SLA. Dopiero gdy jest zaufanie do działania, można darzyć nim kolejne komponenty.
Kiedy VPA ma największy sens
Nie każdy workload zyskuje tak samo na VPA. Najbardziej zyskują:
- Serwisy bez agresywnego HPA, gdzie liczba replik jest względnie stała (np. backendy integracyjne, joby, procesy kolejkowe).
- Komponenty o przewidywalnym obciążeniu, ale nieprzewidywalnych potrzebach zasobowych (np. różne typy zapytań w analityce, zmienne profile batchy).
- Środowiska nieprodukcyjne, gdzie można pozwolić sobie na większą liczbę restartów, a celem jest minimalizacja kosztów.
Przy bardzo dynamicznym HPA, szczególnie skalowanym po metrykach niestandardowych, VPA w trybie Auto lubi „walczyć” z HPA. Jedno narzędzie stwierdza, że serwis trzeba „rozsmarować” na więcej replik, drugie – że lepiej „dopompować” RAM i CPU w każdej. Bez wyraźnej strategii efektem jest chaos.
Jak czytać rekomendacje VPA
VPA wystawia typowo trzy wartości: lowerBound, target i upperBound dla CPU i pamięci. Można je czytać jak delikatne „widełki”:
- lowerBound – poniżej tej wartości zaczynają się wyraźne symptomy problemów (np. throttling, GC, OOMKille).
- target – poziom, przy którym workload działał stabilnie w zaobserwowanym okresie.
- upperBound – „bezpieczny sufit”, powyżej którego brak historycznych powodów, by iść, ale autoskaler i tak by mógł tam sięgnąć.
Rozsądna praktyka: jako request przyjąć wartość zbliżoną do target, jako limit – gdzieś między target a upperBound. Jeśli target jest np. 150Mi, a upperBound 300Mi, można ustawić 160–180Mi request i 256Mi limit, zamiast skakać od razu na 300Mi.
Łączenie VPA z HPA bez konfliktów
Bezpośrednie połączenie VPA w trybie Auto i HPA działającego po CPU jest wrażliwe. Dwie zasady pomagają ograniczyć kłopoty:
- Niech VPA nie zarządza zasobami, które są główną metryką HPA. Jeśli HPA skaluje się po CPU, skonfiguruj VPA tak, by dotykało tylko pamięci lub tylko dawało rekomendacje (tryb Off/Initial).
- Oddziel role: HPA głównie reaguje na zmiany obciążenia w czasie (więcej/mniej użytkowników), VPA ustawia „wielkość pudełka” dla pojedynczej instancji na bazie dłuższego trendu.
Przykładowy kompromis z życia wielu zespołów: HPA działa w pełni produkcyjnie, skalując się po RPS/CPU, a VPA działa w trybie Off i jest używane do okresowego przeglądu konfiguracji zasobów (np. raz na sprint). Zmiany w resources: wprowadzane są świadomie przez zespół, a nie automatem „w ciemno”.
Inne narzędzia „autotuningu”: Goldilocks, KEDA, własne kontrolery
Poza nativem od Google można skorzystać z dodatkowych narzędzi, które ułatwiają ustawianie zasobów bez nurkowania w każdy wykres ręcznie.
- Goldilocks – nakładka na VPA, która generuje raporty i rekomendacje requestów/limitów dla Deployments i StatefulSets. Zamiast surowych CRD VPA dostajesz czytelne tabele „co jest za duże, co za małe”. Dobre na start dla wielu serwisów naraz.
- KEDA – autoskaler skupiony na eventach (kolejki, Kafka, Prometheus, Azure Service Bus itd.). Świetnie sprawdza się przy workloadach, gdzie to nie CPU czy RAM, a np. długość kolejki jest wyznacznikiem „czy trzeba dołożyć replik”.
- Własne kontrolery – gdy potrzeby są bardzo specyficzne (np. skalowanie raportów raz dziennie w oknie czasowym), często prosty operator napisany w Go/Pythonie/TypeScript, reagujący na kilka metryk lub CRD, jest prostszy niż próba zmuszenia HPA/VPA do rzeczy, dla których nie były projektowane.
Narzędzi nie trzeba wdrażać wszystkich na raz. Można zacząć od jednego serwisu z Goldilocks, potem dodać KEDA do jobów kolejki, a na końcu rozważyć VPA w szerszej skali. Dobrze jest przy tym jasno komunikować zespołowi: które komponenty są już „pod opieką” automatyki, a gdzie zasoby nadal ustalane są ręcznie.
Skalowanie klastra i koszty – jak uniknąć marnowania węzłów
Autoskaler podów, VPA i sprytne requesty pomagają, ale ostatecznie to klaster płaci rachunek z chmury. Jeśli węzły są utrzymywane w gotowości „na wszelki wypadek”, szybko robi się drogo. Z drugiej strony agresywne oszczędzanie powoduje długie czasy startu i nieprzewidywalne opóźnienia przy skokach ruchu.
Z tyłu głowy zwykle są trzy pytania:
- Jak mieć miejsce na burst ruchu bez trzymania pustych maszyn?
- Jak ograniczyć fragmentację (dużo małych dziur na węzłach, w które nic sensownego nie wchodzi)?
- Jak nie przeszkadzać autoskalerowi klastra źle dobranymi requestami i politykami?
Cluster Autoscaler – jak naprawdę działa
Cluster Autoscaler (CA) dla większości chmurowych Kubernetesów jest standardem. Patrzy na pody, których nie da się upchnąć na istniejących węzłach, i na tej podstawie:
- Skaluje w górę – gdy są pody Pending z powodu braku zasobów, CA dodaje węzły w odpowiednich node poolach.
- Skaluje w dół – gdy węzeł jest słabo wykorzystany i da się jego pody przenieść na inne maszyny, CA może go odłączyć (cordon + drain + usunięcie VM).
Nie ma tu magii: jeśli requesty są zawyżone, CA będzie zakładał, że potrzeba większego klastra. Jeżeli większość podów ma anty-affinity „rozrzuć mnie jak najszerzej”, też zrobi się ciasno mimo niskiego realnego obciążenia.
Typowe anti‑wzorce, które podbijają rachunek
Nawet przy dobrze działającym CA można nieświadomie programować klaster na marnowanie zasobów. Kilka wzorców pojawia się regularnie:
- Za duże nody do małych podów – dużo małych serwisów z requestami rzędu 100–200m CPU na węzłach 16+ vCPU. Kilka większych podów blokuje spore części węzła i powstaje „sysadminowski ser żółty” – mnóstwo fragmentów nie do użycia.
- Za małe nody do dużych podów – pojedyncze pody z requestem blisko rozmiaru węzła (np. 3,5 vCPU na 4 vCPU), które praktycznie uniemożliwiają sensowny packing innych podów.
- Rozbudowane affinity/anti‑affinity – polityki typu „każdy pod na innym węźle”, zakazy współdzielenia nodów między serwisami itp. Scheduler musi rozrzucać pody po większej liczbie maszyn, a CA nie może ich skleić w mniej nodów.
- Brak podziału na klasy node’ów – wszystko ląduje na jednej puli maszyn „średniej wielkości”, mimo że workloads są skrajnie różne (batch, real‑time, cron, dev).
Gdy z tyłu głowy jest kontrola kosztów, dobrym nawykiem staje się patrzenie na node utilization i na to, jak fragmentują się zasoby: CPU, RAM, ale też dysk i przydziały sieciowe.
Projektowanie node pooli pod różne typy obciążeń
Zamiast jednego ujednoliconego klastra, wygodniej jest mieć kilka sensownie zaprojektowanych pul węzłów i kierować do nich konkretne workloady. Pozwala to pogodzić wydajność z kosztami.
Praktyczny podział, który często się sprawdza:
- Pula „services” – średnie węzły (np. 2–4 vCPU), gdzie lądują serwisy HTTP, API, integracje. Małe pody, duża gęstość, szybka reakcja na ruch.
- Pula „batch/analytics” – większe maszyny z większą ilością RAM lub CPU, nastawione na joby, raporty, ETL. Tutaj akceptowalne są dłuższe starty w zamian za lepszy stosunek cena/wydajność.
- Pula „spot/preemptible” – tańsze, mniej stabilne węzły pod obciążenia, które zniosą preempcję (np. cache, kolejkowanie, processing stateless).
- Pula „system/infra” – węzły na logowanie, monitoring, ingress, bazy typu operator-managed – stabilniejsza, zwykle mniejsza, z bardziej konserwatywnym autoscalingiem.
Do przypinania workloadów do pul można używać nodeSelector, nodeAffinity albo taintów i tolerancji. Przy okazji dzieje się coś pożytecznego: autoskaler może niezależnie składać i rozkładać konkretne pule, nie przeszkadzając innym.
Taints, toleracje i priorytety – świadome „kogo wycinać jako pierwszego”
Przy skalowaniu w dół CA zwykle próbuje pozbywać się węzłów najmniej obciążonych. Jeśli jednak wszystkie pody są traktowane tak samo, może się okazać, że na węźle z mniej istotnymi serwisami siedzą też kluczowe komponenty, które nie powinny tak łatwo wylecieć.
Kilka mechanizmów pomaga ustalić hierarchię:
- PriorityClass – serwisom krytycznym można nadać wyższy priorytet, tak by były ewakuowane w ostatniej kolejności, a przy braku miejsca preemptowały mniej istotne pody.
- Taints + tolerations – node pool z tańszymi, mniej stabilnymi węzłami oznaczasz taintem (np.
spot=true:NoSchedule), a tylko wybrane workloady dostają tolerancję. - PodDisruptionBudget (PDB) – dodatkowy bezpiecznik, który ogranicza, ilu replik można się pozbyć naraz przy drainie węzła.
Dobry efekt daje proste rozróżnienie: serwisy „mission critical” na stabilnych węzłach z wysoko ustawionym priorytetem i bardziej konserwatywnym autoscalingiem; rzeczy „nice to have” (np. dodatkowe indeksacje, joby pomocnicze) na tańszych nodach, które można agresywnie ucinać.
Balans między czasem skalowania a buforem zasobów
Naturalna obawa: jeśli nie trzyma się zapasu, autoskaler nie zdąży zareagować i użytkownicy zobaczą błędy. Z kolei trzymanie kilku pustych węzłów w każdej puli nie jest tanie. Kluczem jest dopasowanie:
- Czasu rozruchu węzła – provisioning VM, bootstrap Kubernetes, pobranie obrazów, inicjalizacja sidecarów.
- Czasu nagrzewania aplikacji – start runtime, warm‑up cache, rozgrzanie połączeń do baz.
- Charakteru skoków ruchu – wolne, przewidywalne wzrosty vs nagłe piki.
Dla serwisów z codziennym, powtarzalnym ruchem dużo daje proste „uprzedzające” skalowanie: cron w CI/CD, który przed spodziewanym pikiem zwiększa minReplicas lub rozmiar node poola, a po szczycie je zmniejsza. Nie trzeba tu skomplikowanej automatyki, czasem wystarczy jeden dodatkowy skrypt.
Przy usługach narażonych na nieprzewidywalne piki (np. kampanie marketingowe, integracje z zewnętrznymi partnerami) z reguły bezpieczniej jest utrzymywać minimalny, niewielki bufor node’ów i szybciej skalować pody niż czekać na pojawienie się nowych VM‑ek z chmury.
Obrazy, init‑kontenery i sidecary a koszt skalowania
Koszt węzłów to jedno, ale równie mocno wrażenie „powolnego klastra” tworzą ciężkie obrazy i skomplikowane sekwencje startowe. Widać to zwłaszcza wtedy, gdy HPA próbuje gwałtownie dorzucić replik, a każdy pod startuje długo.
Kilka elementów ma realny wpływ:
- Wielkość i liczba obrazów – duże, rzadko odświeżane obrazy, które nie mieszczą się w cache na nowych node’ach, wydłużają cold start. Lżejsze base image’y i wspólne warstwy mocno przyspieszają sytuację.
- Init‑kontenery – długie migracje baz danych, re‑indeksacje czy generowanie zasobów w init containers blokują gotowość podów i wypełniają węzły „półproduktem”. Im więcej logiki da się wynieść do osobnych jobów, tym sprawniej skaluje się sama warstwa runtime.
- Sidecary – każdy dodatkowy sidecar to osobny kontener z własnymi requestami. Przy niewielkich serwisach kilka sidecarów (proxy, logger, metrics, auth) może zużyć więcej CPU/RAM niż sam główny proces.
Często mały refactoring pipeline’u (np. osobny job do migracji, uproszczenie sidecarów, optymalizacja obrazów) wpływa na skalowalność klastra bardziej niż „dokładanie” kolejnych mechanizmów autoskalowania.
Monitorowanie pod kątem marnowania węzłów
Żeby szkolić intuicję, przydaje się kilka prostych dashboardów. Nie trzeba od razu pełnego FinOps – wystarczy parę wykresów, które pokazują, gdzie naprawdę ucieka kasa:
- wypełnienie CPU/RAM na poziomie węzłów i całych node pooli,
- procent zasobów „zarezerwowanych” (requests) vs faktycznie używanych,
- liczbę podów Pending oraz czas oczekiwania na schedulowanie,
- historię akcji Cluster Autoscalera (dodane/odjęte węzły, przyczyny),
- rozmiar i częstotliwość cold startów (czas od stworzenia poda do stanu Ready).
Na tej podstawie widać na przykład, że w godzinach nocnych klaster jest wypełniony mniej niż w jednej trzeciej, bo sporo serwisów ma wysokie minReplicas „na wszelki wypadek”. Albo że jedna pula jest wiecznie na granicy, a druga stoi pusta z powodu zbyt restrykcyjnych reguł affinity.
Praktyczne kroki, gdy rachunek „boli”
Gdy koszt klastra zaczyna być tematem na statusach, zwykle pojawia się presja na szybkie oszczędności. Można to poukładać bez nerwowych cięć, krok po kroku:
- Wyłączyć „świadomie nadmiarowe” środowiska – nieprodukcyjne klastry z pełną repliką produkcji, rzadko używane feature‑brancha, stare środowiska demo.
- Zidentyfikować największych „pożeraczy” zasobów – top N namespace’ów i serwisów wg wykorzystania CPU/RAM; często 20% workloadów generuje większość kosztu.
- Przejrzeć requesty/limity dla tych kilku największych serwisów – tu VPA w trybie Off lub Goldilocks potrafią oszczędzić najwięcej w najkrótszym czasie.
- Uporządkować node poole – wydzielić tańszą pulę na batch, ewentualnie włączyć spot/preemptible dla wybranych workloadów.
- Sprawdzić HPA i minReplicas – czy minima nie są ustawione pod „domniemany szczyt”, który realnie zdarza się raz w tygodniu.
Zwykle już te kilka ruchów robi zauważalną różnicę. Dopiero później dochodzi sens do wdrażania bardziej finezyjnych mechanizmów jak customowe kontrolery czy zaawansowane polityki schedulingu.
Świadome kompromisy zamiast „jednej słusznej konfiguracji”
Nie ma jednego wzorca, który będzie idealny dla każdej organizacji. Dla jednych priorytetem jest nieprzerwany uptime i minimalne opóźnienia – wtedy akceptuje się większy zapas węzłów. Dla innych kluczowe są koszty, a niewielkie wydłużenie czasu reakcji w szczycie jest akceptowalne.
Najlepiej działa podejście, w którym:
- serwisy krytyczne mają bardziej konserwatywne granice skalowania i większy bufor w klastrze,
- serwisy pomocnicze i batchowe są polem eksperymentów z agresywniejszym autoscalingiem i tańszymi node poolami,
- decyzje o zmianach w HPA/VPA/CA są oparte o konkretne metryki, a nie tylko poczucie „aplikacja jest wolna, dajmy +2 vCPU”.
Takie podejście pomaga odejść od skrajności: ani od „dokupmy jeszcze jeden node pool, będzie spokój”, ani od „zetnijmy wszystko o 50% i zobaczymy, co się wywali”. Zamiast tego pojawia się stopniowe, ciągłe strojenie – dokładnie to, czego potrzebują mikroserwisy w Kubernetes, żeby skalować się bez marnowania zasobów.
Co warto zapamiętać
- Skalowanie „na zapas” (wysokie requesty, dużo replik, duży klaster) szybko winduje koszty, a i tak nie gwarantuje stabilności – po pierwszym „podkręceniu” zasobów nie ma już prostego manewru x2 przy kolejnym skoku ruchu.
- Skuteczne, oszczędne skalowanie to proces iteracyjny: start z minimalną stabilną konfiguracją, obserwacja realnego ruchu, stopniowe korekty oraz powiązanie autoskalowania z metrykami technicznymi i biznesowymi.
- Zbyt agresywne lub chaotyczne skalowanie powoduje niestabilność (restarty podów, throttling CPU, OOMKilled, lokalne wąskie gardła), co utrudnia debugowanie – trudno wtedy odróżnić problemy w kodzie od błędnej konfiguracji zasobów.
- Sensowne requesty i limity CPU/RAM są fundamentem: request określa gwarancję i wpływa na planowanie podów, a limit chroni przed „rozlewaniem się” procesu; ustawianie wysokich, równych wartości „na wszelki wypadek” kończy się overprovisioningiem i pustymi węzłami.
- Kube-scheduler widzi tylko liczby – jeśli requesty są zawyżone lub ich brakuje, binpacking jest słaby, węzły formalnie „pełne” mimo niskiego wykorzystania, a autoscaler klastra dokłada kolejne maszyny, choć realnie nie są potrzebne.
- Brak requestów i limitów odbiera kontrolę nad priorytetami: przy presji na zasoby pody w klasie BestEffort mogą być ubijane jako pierwsze, nawet jeśli obsługują kluczowy fragment biznesu.






