Kubernetes i procedura drain: luka wynosząca 221 ms

Pomiary przeprowadzone narzędziem drainwatch w klastrze Kubernetes v1.34.0 ujawniły krytyczną lukę w routingu sieciowym podczas procedury wyłączania podów. Podczas gdy usunięcie gniazda TCP tworzy okno martwej reklamacji o medianie 221 ms (z punktami szczytowymi do 521 ms), ruch UDP zostaje bezpowrotnie przekierowany po 2,4 sekundy bez powiadomienia wyłączanego procesu. Brak precyzyjnej obsługi sygnału SIGTERM oraz błędy w obiektach PodDisruptionBudget prowadzą do zakleszczenia aktualizacji infrastruktury i błędów HTTP 503.

Pomiary narzędziem drainwatch i opóźnienia w EndpointSlice

Pomiary przeprowadzone przez inżyniera Jay Suresh Nirmala za pomocą narzędzia drainwatch w klastrze Kubernetes v1.34.0 wykazały istotne rozbieżności w czyszczeniu połączeń. Podczas wykonywania procedury kubectl drain standardowe założenie o płynnym wycofaniu ruchu zakłada równoległą aktualizację routingu, jednak testy na 25 próbach ujawniły istnienie stałej luki czasowej między zniszczeniem procesu TCP a modyfikacją EndpointSlice.

Badanie zrealizowano w klastrze kind z komponentem kube-proxy pracującym w trybie iptables. Test obejmował 5 scenariuszy zakończenia działania kontenera powtórzonych po 5 razy. Wyniki wskazują na wyraźny opóźniony czas reakcji warstwy sterowania Kubernetes względem rzeczywistego stanu procesu gniazda.

Metryka zdarzenia (n=5) Mediana Najgorszy wynik
Zamknięcie Ostatniego Połączenia TCP 45 ms 98 ms
Usunięcie Punktu z EndpointSlice 293 ms 591 ms
Okno Martwej Reklamacji w Usłudze Kubernetes 221 ms 521 ms

Przez medianowo 221 ms (oraz ponad pół sekundy w najgorszej próbie) zasób EndpointSlice wskazuje adres poda, którego proces zresetował już wszystkie gniazda. Zapytania kierowane w tym oknie czasowym trafiają w nieaktywny proces. Z kolei w scenariuszu zignorowania sygnału SIGTERM, system kubelet wymusza zamknięcie sygnałem SIGKILL w ciągu 62 ms od przekroczenia 30-sekundowego limitu czasu, zwracając kod wyjścia Exit code 137 we wszystkich 5 próbach. Prawidłowa obsługa wygaszania w aplikacji wydłuża trwałość gniazd TCP do końca 25-sekundowego okna.

Czy Kubernetes gubi ruch UDP podczas procedury wyłączania podów?

Analiza zachowania protokołu UDP w środowisku Kubernetes wykazuje całkowity brak mechanizmu łagodnego wygaszania połączeń bezpołączeniowych podczas czyszczenia węzła. Polecenie kubectl drain nie chroni aktywnych datagramów, co w aplikacjach klasy WebRTC, serwerach gier oraz usłudze DNS prowadzi do natychmiastowego porzucenia warstwy danych, niezależnie od ustawień zgłoszonych przez procedurę wyłączania poda.

W trakcie procedury usuwania poda w klastrze Kubernetes, przepływy UDP są automatycznie i po cichu przekierowywane (re-homed) do nowej instancji po 2,4 sekundy. Dzieje się to w momencie, gdy oryginalny pod jest nadal w pełni sprawny i ma medianowo 22,9 sekundy do końca swojego okna wygaszania.

Jeżeli w klastrze Kubernetes nie istnieje zastępczy pod (np. podczas skalowania do zera), ruch UDP nie zostaje przekierowany, lecz ginie z błędem ICMP port unreachable średnio w 177 ms po zakończeniu kontenera. Dzieje się tak w chwili, gdy kube-proxy zaczyna odrzucać pakiety kierowane do usługi bez aktywnych punktów końcowych. Procedura wygaszania kontenera nie wpływa na ten mechanizm — decyzja o losie pakietów UDP zapada na poziomie obecności zastępczych podów w klastrze.

Mechanizm PodDisruptionBudget i ryzyko blokady procedury drain

Mechanizm kubectl drain wykorzystuje domyślnie interfejs Eviction API, który wstrzymuje wycofanie podów w przypadku naruszenia reguł PodDisruptionBudget. Błędnie skonfigurowany zasób PodDisruptionBudget doprowadza do zakleszczenia automatycznych procesów aktualizacji systemu operacyjnego oraz łatania węzłów klastra Kubernetes, generując przestój w pętli utrzymaniowej.

Głównym czynnikiem blokującym polecenie kubectl drain jest parametr ALLOWED DISRUPTIONS: 0 w obiekcie PodDisruptionBudget. Sytuacja ta powstaje z trzech przyczyn: Błąd Zaokrąglania Procentowego: Ustawienie minAvailable: 80% przy klastrze przeskalowanym w dół do 1 repliki wymusza na Kubernetes zaokrąglenie w górę do 1 wymaganego zdrowego poda (\\(1 \times 0,8 = 0,8 \to 1\\)), redukując dozwolone zakłócenia do 0. Sztywna Wartość Minimalna: Konfiguracja minAvailable: 3 przy dokładnie 3 aktywnych podach uniemożliwia usunięcie któregokolwiek z nich. * Błędna Wartość Bezpośrednia: Zdefiniowanie maxUnavailable: 0, co całkowicie zakazuje niedostępności podów w klastrze Kubernetes.

Gdy wskaźnik dozwolonych zakłóceń wynosi zero, interfejs Eviction API zwraca błąd FATAL: EVICTION BLOCKED, co zatrzymuje automatyczne skrypty aktualizacyjne węzłów. Użycie wymuszenia w postaci flagi --disable-eviction pomija reguły PodDisruptionBudget i kasuje pody bezpośrednio, co powoduje brak dostępności aplikacji i błędy HTTP 503 na poziomie klientów.

Porównanie parametrów konfiguracyjnych w Kubernetes

Właściwy dobór parametrów dla polecenia kubectl drain oraz poprawna struktura obiektów PodDisruptionBudget decydują o dostępności usług w klastrze Kubernetes. Przejście na relatywne limity niedostępności chroni architekturę przed awariami HTTP 503 i pozwala uniknąć wymuszonego pomijania reguł ochronnych w środowisku produkcyjnym.

Zastosowanie parametru spec.maxUnavailable: 1 w definicji PodDisruptionBudget gwarantuje, że bez względu na liczbę podów w klastrze Kubernetes zawsze dozwolona będzie ewakuacja co najmniej jednej instancji. Z kolei używanie sztywnego minAvailable: N przy małej liczbie replik prowadzi do trwałego zablokowania czyszczenia węzła.

Parametr / Opcja w Kubernetes Rola i zachowanie w procedurze drain Ryzyko operacyjne
spec.maxUnavailable: 1 Zapewnia matematyczny margines na ewakuację co najmniej 1 poda. Brak; rekomendowany standard konfiguracyjny.
spec.minAvailable: N Wymaga utrzymania N gotowych podów w klastrze. Zakleszczenie wycofywania przy spadku liczby podów do N.
--ignore-daemonsets Ignoruje pody zarządzane przez DaemonSet na czyszczonym węźle. Pody DaemonSet pozostają na węźle do jego wyłączenia.
--delete-emptydir-data Zezwala na kasowanie podów używających pamięci lokalnej emptyDir. Bezpowrotna utrata danych lokalnych zapisanych w emptyDir.
--force Wymusza usunięcie podów bez kontrolera (bare pods). Pody pozbawione kontrolera nie zostaną ponownie uruchomione.
--disable-eviction Bipassuje Eviction API i ignoruje obiekty PodDisruptionBudget. Awaria dostępności serwisu i generowanie błędów HTTP 503.

Wnioski praktyczne i rekomendacje

  • Skonfiguruj obiekty PodDisruptionBudget przy użyciu reguły spec.maxUnavailable: 1 zamiast wartości procentowych lub wartości minAvailable: N, aby zapobiec zaokrąglaniu dozwolonych zakłóceń do zera i blokowaniu polecenia kubectl drain.
  • Zweryfikuj reakcję aplikacji na sygnał SIGTERM, wprowadzając celowe opóźnienie wyjścia procesu o co najmniej 500 ms po wyrejestrowaniu gniazd, co zamknie okno propagacji w zasobie EndpointSlice wynoszące medianowo 221 ms (a maksymalnie 591 ms).
  • Zmierz zachowanie ruchu UDP w usługach czasu rzeczywistego za pomocą środowisk testowych przed wdrożeniem na produkcję, uwzględniając fakt automatycznego przekierowania przepływów po 2,4 sekundy lub ich odrzucania przez kube-proxy z błędem ICMP w 177 ms.
  • Wdróż weryfikację skryptów automatyzacji za pomocą polecenia kubectl drain NODE_NAME --dry-run=client oraz kontrolę pola ALLOWED DISRUPTIONS przed uruchomieniem procedur aktualizacyjnych na węzłach produkcyjnych.

Źródła

Przemysław Majdak

Przemysław Majdak · AI Automation Engineer

Założyciel Baluarte i wydawca BitBiz.pl. Członek ISSA Polska. Ekspert z 18-letnim stażem w branży IT — wdraża bezpieczne automatyzacje AI, systemy full-stack i zaawansowane procedury cyberbezpieczeństwa dla firm. Wszystkie artykuły autora →

Jedna odpowiedź

💬 Kliknij tutaj, aby dodać komentarz

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *

  1. Awatar Wiktor

    Te 221 ms to tylko wierzchołek, bo przy skali mikrousług każdy serwis ma swój własny „narzut startowy” i łącznie te opóźnienia zjadają całą dostępność systemu przy większym ruchu. 📉