Zero-downtime deployments w Kubernetes

Mityczny i często przereklamowany zero-downtime deployment jest dzisiaj łatwiejszy do osiągnięcia niż kiedykolwiek. Dlaczego przereklamowany? Ponieważ często klienci wymagają takich wdrożeń we wstępnych założeniach a przy rozmowie o strategii high availability mówią „dwie dziewiątki są jak najbardziej ok” (czyli możliwy downtime 3.65 dnia w roku).

Są jednak systemy w których nikt nawet nie pyta o zero-downtime – bo jest to oczywiste i pierwotne założenie. Zwykle jest to oprogramowanie, które w sposób ciągły musi procesować dane np. monitorowanie sieci energetycznych czy streaming video na skale globalna.

Kubernetes z uwagi na to, że powstał aby zarządzać setkami instacji aplikacji, musi w przystępny i pewny sposób dawać możliwość aktualizacji.

Zanim zacznę o strategiach aktualizacji, liczbie instancji aktywnych vs usuniętych czy czasie oczekiwania na dostępność aplikacji, należy powiedzieć o dwóch, bardzo istotnych „health checkach„, o których powinno się pomyśleć jeszcze przed pierwszym wdrożeniem aplikacji.

Ale co mają health checki do deploymentu aplikacji? – można by zapytać, okazuje się jednak, że maja i to dużo.

Kubernetes aktualizując naszą aplikację (np. rolling update) tworzy instancję aplikacji i jeżeli replika z nową wersją jest „ready” to usuwa jedną z replik ze starej wersji. Hop hop, nie tak szybko! Każda aplikacja potrzebuje jakiegoś czasu aby „wstać” (warmup time) a jeżeli nie mamy health checków to nasz kochany zarządca jak tylko postawi kontener oznacza go jako „ready” – pomimo tego, że nasza aplikacja nie jest ready i nie będzie jeszcze przez najbliższe 30 sekund.

Liveness probe

Definicja deploymentu czy statefulsetu (a właściwie to część konfiguracji kontenera) powinna zawierać LivenessProbe, czyli taki test co określony czas który zwróci informacje do Kubernetes czy ten kontener żyje. W przeciwnym wypadku Kubernetes oznacza go jako „dead” co ma dla niego niemiłe konsekwencje – usunięcie Pod’a i utworzenie nowego, czyli w praktyce restart kontenera. Nie zwlekając – taki check może być skonfigurowany w trzech typach:

httpGet – co określony czas (periodSeconds), wykonywany jest request (port i path) i jeżeli status odpowiedzi to 2xx lub 3xx to Pod oznaczany jest jako żywy.

exec – wykonanie komendy, w przypadku błędu – oznaczenie jako dead i restart.

tcpSocket – połączenie TCP zakończone sukcesem.

Ważnym parametrem jest tutaj initialDelaySeconds, czyli czas w sekundach po którym nastąpi pierwsze sprawdzenie po utworzeniu kontenera – warto pomyśleć o tym, żeby ten czas był większy niż czas warmup’u naszej aplikacji.

Często w aplikacjach tworzy się specjalny endpoint /healthz który służy tylko do tego, żeby tego typu sondy mogły monitorować aplikację.

Readiness probe

LivenessProbe powoduje restart kontenera w przypadku fail’a, natomiast fail ReadinessProbe skutkuje tym, że do aplikacji przestanie być serwowany ruch i będzie ona oznaczona jako NotReady. Czyli w przypadku rolling update, status NotReady spowoduje, że poprzednia wersja aplikacji nie zostanie usunięta i nadal będzie przyjmowała ruch podczas gdy my możemy spokojnie zająć się problemami nowej wersji.

Definicja tego health check’a wygląda tak samo jak definicja LivenessProbe:

Strategie aktualizacji

Jeżeli dwie powyższe „sondy” zostały wprowadzone, czas na określenie strategii aktualizacji aplikacji. Aktualizacja aplikacji bez żadnej przerwy w działaniu można skorzystać ze strategii:

  • Blue/Green – przygotowujemy manualnie nową wersję aplikacji obok tej działającej i po sprawdzeniu, że wszystko jest tak jak powinno, aktualizujemy service by kierował ruch do nowej wersji. Poprzednią wersję usuwamy po przekierowaniu ruchu.
  • Canary – przygotowana manualnie nowa wersja działa obok poprzedniej wersji i część ruchu jest do niej kierowana. W Kubernetes do osiągnięcia np. gdy uruchomione są 4 pody z poprzednią wersją i 1 z nową – wtedy 20% ruchu trafi do nowej wersji.
  • Rolling Update – automatyczny update gdzie pody będą zamieniane po jednym dopóki wszystkie instancje poprzedniej wersji nie zostaną zamienione nową wersją. Załóżmy, że aplikacja w wersji v1.0 działa w 5 replikach:
    • Pierwsza replika v1.0 zostaje oznaczona jako NotReady i Kubernetes nie będzie do niej kierował ruchu. Ruch będzie natomiast kierowany do pozostałych 4 replik.
    • Pierwsza replika v1.1 zostaje utworzona i dopóki jego status nie będzie Ready nie otrzyma ruchu.
    • Gdy pierwsza replika v1.1 będzie Ready, kolejna replika v1.0 zostanie oznaczona jako NotReady i usunięta a w jej miejsce zostanie utworzona kolejna replika v1.1. W ten sposób kolejne pody ze starszą wersją zostaną zamienione tymi z nowszą.

Istnieje również strategia Recreate pomimo tego, że nie zapewnia zero-downtime jest często stosowana w serwisach które nie obsługują bezpośredniego ruchu a wymagają szybkiego update’u. Szybkość tej aktualizacji jest powodowana tym, że wszystkie repliki zostają usunięte i równocześnie powstają nowe.

Rolling Update jest automatyczny w Kubernetesie (strategia nie ma żadnych kroków manualnych) więc moim zdaniem wygląda najciekawiej. Nie jest to jednak tzw. silver bullet, ponieważ nie sprawdzi się w aplikacjach które nie mają wstecznej kompatybilności pomiędzy aktualizowanymi wersjami – bo jednak przez jakiś czas w tym samym czasie działają dwie wersje aplikacji. Dla tej strategii – co prawda opcjonalne, ale ważne są dwa parametry:

maxSurge: określa jaka jest maksymalna liczba nadmiarowych podów aplikacji. Np. aplikacja działająca na 5 podach, przy maxSurge = 1 będzie mogła posiadać max 6 podów. Tą wartość można też określić procentowo.

maxUnavailable: określa jaka jest maksymalna liczba replik które zostaną usunięte. Np. aplikacja działająca na 5 podach, przy maxUnavailable = 2, będzie mogła mieć co najwyżej 2 brakujące pody – czyli może działać z minimalnie 3 podami. Tą wartość również można określić procentowo, wartość 0 jest niedopuszczalna (gdy jednak będzie 0 to walidacja zmieni to na 25%).

Poniżej znajduje się przykład definicji aplikacji web, który wykorzystujemy jako deployment i upgrade testowy. Posiada Livenessprobe i Readinessprobe oraz strategie RollingUpdate.

Przykład deploymentu z health checkami i strategią