> ## Content Index
> Fetch the complete content index at: https://lsdev.pl/llms.txt
> Use this file to discover other available public pages before exploring further.

# Liveness probe restartuje pod podczas pauzy GC — jak policzyć próg
- URL: https://lsdev.pl/posts/liveness-probe-restartuje-pod-podczas-pauzy-gc-jak-policzyc-prog/
- Published: 2026-09-13T15:24:21.000Z
- Updated: 2026-09-13T15:24:51.000Z
- Description: Aplikacja żyje, ale kubelet nie czeka. Pauza garbage collectora wygląda dla sondy jak zawieszenie, a domyślne progi dają zaledwie dwie sekundy tolerancji. Wzór, liczby z logów i trzy błędy, które kosztują restartów.
- Author: Łukasz Sakowicz
- Tags: Kubernetes, Java, GC, health-checks, operacje, #lsdev-ai

W logach pojawia się `Liveness probe failed`, a pod, który sekundę wcześniej normalnie obsługiwał ruch, dostaje restart. Nic nie jest zepsute. Aplikacja wykonywała w tym momencie pauzę garbage collectora — proces żył, tylko przez chwilę nie odpowiadał, bo maszyna wirtualna Javy porządkowała pamięć i wstrzymała wątki aplikacji. Kubelet nie ma jak odróżnić tych kilku sekund ciszy od zawieszenia.

I na tym polega problem. Liveness probe został zaprojektowany do wykrywania sytuacji, w których proces działa, ale nie robi postępów — na przykład zakleszczenia — i restartuje kontener, gdy sonda zawiedzie więcej razy niż ustawiony próg tolerancji ([dokumentacja Kubernetesa](https://kubernetes.io/docs/concepts/workloads/pods/probes?ref=lsdev.pl)). Pauza GC wygląda dla niej identycznie jak zakleszczenie: brak odpowiedzi w wyznaczonym czasie. Różnica jest taka, że po pauzie GC aplikacja wraca sama, a restart tylko niszczy zapytania, które akurat były w locie.

Skutek jest gorszy niż pojedynczy restart. Dokumentacja Kubernetesa ostrzega, że źle skonfigurowana sonda prowadzi do kaskadowych awarii — restartów pod dużym obciążeniem i nieudanych żądań klientów. Dla zespołu operacyjnego wygląda to jak losowa niestabilność.

## Trzy sondy, trzy różne pytania

Zanim przejdziemy do progów, trzeba rozdzielić sondy, bo spora część problemów bierze się z mylenia ich ról. Każda odpowiada na inne pytanie i każda ma inną konsekwencję.

| Sonda          | Pytanie                                 | Co się dzieje przy niepowodzeniu                            |
| -------------- | --------------------------------------- | ----------------------------------------------------------- |
| startupProbe   | Czy aplikacja skończyła się uruchamiać? | Kontener zostaje zabity, gdy nie zdąży w wyznaczonym czasie |
| livenessProbe  | Czy ten proces jeszcze żyje?            | **Restart kontenera**                                       |
| readinessProbe | Czy proces może teraz przyjmować ruch?  | Pod przestaje dostawać ruch, ale **działa dalej**           |

Ta ostatnia różnica jest sednem. Readiness wycofuje pod z rotacji na czas, gdy coś mu przeszkadza, i przywraca go, gdy przeszkoda minie — bez utraty stanu i bez zabijania trwających zapytań. Liveness nie ma takiego biegu pośredniego: jedyne, co potrafi, to zabić kontener.

Dlatego pytanie „czy da się teraz obsłużyć ruch" nigdy nie powinno trafić do liveness. A pytanie „czy proces się zaciął" nie powinno zależeć od niczego poza samym procesem.

Startup probe rozwiązuje osobny przypadek: aplikację, która wolno wstaje. Przy 30 próbach co 10 sekund daje 300 sekund na rozruch, zanim liveness w ogóle zacznie działać, a jeśli aplikacja nie zdąży — kontener zostaje zabity i podlega `restartPolicy` poda ([dokumentacja Kubernetesa](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes?ref=lsdev.pl)). To lepsze niż podnoszenie progu liveness na stałe, bo długi rozruch nie musi oznaczać pobłażliwości przez całe życie poda.

## Ile czasu naprawdę masz — wzór

Trzy pola decydują, kiedy nastąpi restart, i mają domyślne wartości, których prawie nikt nie zmienia: `failureThreshold` (domyślnie 3), `periodSeconds` (domyślnie 10 s) i `timeoutSeconds` (domyślnie 1 s). Czas od pierwszego niepowodzenia sondy do restartu kontenera wynosi:

```text
failureThreshold × periodSeconds + timeoutSeconds

```

Dla wartości domyślnych: `3 × 10 + 1 = 31` sekund. To wygląda na dużo i zwykle wystarcza. Problem pojawia się przy popularnym „przyspieszaniu" wykrywania awarii:

```yaml
# Wygląda rozsądnie, a wywala pody podczas GC
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  periodSeconds: 1      # 1 × 1 + 1 = 2 sekundy tolerancji
  timeoutSeconds: 1
  failureThreshold: 1

```

Dwie sekundy. Opis wdrożenia z [resolve.ai](https://resolve.ai/glossary/how-to-debug-kubernetes-probe-issues?ref=lsdev.pl) pokazuje ten mechanizm wprost: przy `periodSeconds: 1` i `timeoutSeconds: 1` kubelet oczekuje odpowiedzi w ciągu jednej sekundy, a aplikacje ze zmiennym opóźnieniem — w tym z pauzami GC — będą to sporadycznie zawodzić. Ten sam opis wskazuje, że skutkuje to bezsensownymi restartami, które zabijają zapytania w locie i bywają częstą przyczyną błędów 502 na ingressie.

Warto zapamiętać jeszcze jedno pole: `successThreshold` domyślnie wynosi 1 i dla liveness oraz startup probe musi pozostać 1.

## Dlaczego 200 ms z dokumentacji nie jest gwarancją

Tu leży najczęstsze nieporozumienie. Domyślny cel pauzy G1, `MaxGCPauseMillis`, wynosi 200 milisekund ([dokumentacja Oracle](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1%5Fgc.html?ref=lsdev.pl)) — i stąd bierze się przekonanie, że dwusekundowy budżet ma ogromny zapas.

To jednak **cel, do którego kolektor dąży, a nie twardy limit**. G1 w domyślnej konfiguracji nie dąży ani do maksymalnej przepustowości, ani do najniższego opóźnienia, tylko do względnie małych, jednolitych pauz przy wysokiej przepustowości ([dokumentacja Oracle](https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-garbage-collector-tuning.html?ref=lsdev.pl)).

Full GC to zupełnie inny tryb pracy. Dokumentacja Oracle opisuje go jako proces często bardzo czasochłonny, który zachodzi wtedy, gdy aplikacja alokuje więcej obiektów, niż da się szybko odzyskać — zwykle dlatego, że współbieżne znakowanie nie zdążyło się zakończyć na czas. Innymi słowy: 200 ms to budżet na zwykłą pauzę, a nie na Full GC.

Współbieżne znakowanie startuje, gdy zajętość całej sterty osiąga próg `InitiatingHeapOccupancyPercent`, domyślnie 45%. Gdy nie zdąży na czas, dochodzi do Full GC. Jest też pułapka konfiguracyjna: ustawienie sztywnego rozmiaru młodej generacji jedną wartością zamiast zakresu praktycznie wyłącza kontrolę czasu pauzy G1\. Jeśli heap był kiedyś „dostrojony" w ten sposób, cel 200 ms już nie obowiązuje — a liveness probe i tak zakłada, że obowiązuje.

## Jak dobrać progi

Kolejność jest odwrotna niż w większości tutoriali. Nie zaczynaj od liczb z przykładu — zacznij od własnych logów GC i znajdź najdłuższą zaobserwowaną pauzę. Dopiero ta liczba jest wejściem do wzoru.

Praktyczne progi zbierane przez [kubernetes.recipes](https://kubernetes.recipes/recipes/configuration/kubernetes-liveness-probe-best-practices?ref=lsdev.pl) to `failureThreshold` ≥ 3, `timeoutSeconds` ≥ 5 s, ponieważ pauzy GC potrafią przekroczyć 1–2 sekundy, oraz `periodSeconds` ≥ 10 s dla liveness:

```yaml
# Punkt wyjścia — wynik wzoru zestaw z własnymi logami GC
livenessProbe:
  httpGet:
    path: /healthz     # lekki endpoint, bez zależności zewnętrznych
    port: 8080
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3   # 3 × 10 + 5 = 35 sekund tolerancji

```

Trzydzieści pięć sekund to punkt wyjścia, nie odpowiedź. Dopóki nie zestawisz tej liczby z najdłuższą pauzą Full GC z własnych logów, nie wiesz, czy wystarcza.

Sama sonda jest tania: przy `httpGet` kubelet czyta odpowiedź tylko do 10KiB, a o powodzeniu decyduje wyłącznie kod statusu. Częstym wzorcem jest użycie tego samego, taniego endpointu co dla readiness, ale z wyższym `failureThreshold` — to rozsądna domyślna decyzja, bo nie dodaje osobnej ścieżki kodu, którą trzeba utrzymywać.

## Antywzorzec, który kosztuje najwięcej

Drugi błąd jest niezależny od GC: liveness odpytująca endpoint zależny od bazy danych. Gdy baza zwalnia, wszystkie repliki zawodzą sondę jednocześnie i restartują się razem — dokładnie wtedy, gdy najmniej Ci to potrzebne. Zamiast jednej wolnej usługi masz całe wdrożenie w pętli restartów.

Liveness ma odpowiadać na pytanie „czy ten proces żyje", nie „czy cała jego zależność działa". To drugie zadaj readiness — wtedy pody wypadają z rotacji na czas awarii bazy i wracają, gdy minie, zamiast się restartować.

Jeśli masz dziś zmienić jedną rzecz: odetnij liveness od bazy danych, a próg policz wobec najdłuższej zaobserwowanej pauzy Full GC z własnych logów — nie wobec liczby skopiowanej z przykładu.