Najlepsze praktyki dla kontenerów Kubernetes: sprawdzanie dostępności

Najlepsze praktyki dla kontenerów Kubernetes: sprawdzanie dostępności

TL;DR

  • Aby osiągnąć wysoką widoczność kontenerów i mikroserwisów, logów i podstawowych metryk, to za mało.
  • Aby szybciej przywrócić działanie i zwiększyć odporność aplikacji, należy stosować Zasadę wysokiej widoczności (HOP, High Observability Principle).
  • Na poziomie aplikacji dla HOP wymagane są: odpowiednie logowanie, staranne monitorowanie, sprawdzanie dostępności i śledzenie wydajności/przechodzenia.
  • Jako element HOP użyj sprawdzeń readinessProbe i livenessProbe Kubernetes.

Czym jest szablon sprawdzania dostępności?

Podczas projektowania krytycznie ważnej i wysoko dostępnej aplikacji, bardzo istotne jest rozważenie aspektu takiego jak odporność. Aplikacja jest uważana za odporną, jeśli szybko przywraca działanie po awarii. Typowa aplikacja chmurowa wykorzystuje architekturę mikroserwisów – gdzie każdy komponent jest umieszczany w osobnym kontenerze. Aby upewnić się, że aplikacja na k8s jest wysoko dostępna, podczas projektowania klastra należy stosować określone szablony. Wśród nich znajduje się Szablon sprawdzania dostępności. Określa on, jak aplikacja informuje k8s o swojej dostępności. To nie tylko informacja, czy pod działa, ale także sposób, w jaki przyjmuje i odpowiada na zapytania. Im więcej Kubernetes wie o dostępności poda, tym mądrzejsze podejmowane są decyzje dotyczące trasowania ruchu i równoważenia obciążenia. W ten sposób Zasada wysokiej widoczności zapewnia aplikacji sprawne odpowiadanie na zapytania.

Zasada wysokiej widoczności (HOP)

Zasada wysokiej widoczności to jeden z zasad projektowania aplikacji kontenerowychW architekturze mikrousług serwisy nie przejmują się tym, jak ich żądanie jest przetwarzane (i tak jest dobrze), ale ważne jest, jak uzyskać odpowiedzi od przyjmujących serwisów. Na przykład, aby uwierzytelnić użytkownika, jeden kontener wysyła drugiemu żądanie HTTP, oczekując odpowiedzi w określonym formacie — to wszystko. Żądanie może być przetwarzane przez PythonJS, a odpowiedź wygenerować Python Flask. Kontenery są dla siebie nawzajem jak czarne skrzynki ze ukrytym wnętrzem. Jednak zasada NOR wymaga, aby każdy serwis ujawniał kilka punktów końcowych API, pokazując, jak bardzo jest operacyjny, a także stan jego gotowości i odporności na awarie. Te wskaźniki są zbierane przez Kubernetes, aby zaplanować następne kroki dotyczące routingu i równoważenia obciążenia.

Dobrze zaprojektowana aplikacja w chmurze rejestruje swoje podstawowe zdarzenia, używając standardowych strumieni wejścia-wyjścia STDERR i STDOUT. Następnie działa pomocnicza usługa, taka jak filebeat, logstash lub fluentd, dostarczająca logi do centralizowanego systemu monitorowania (na przykład Prometheus) oraz systemu zbierania logów (zestaw oprogramowania ELK). Na schemacie poniżej pokazano, jak aplikacja w chmurze działa zgodnie z Szablonem sprawdzania dostępności i Zasadą wysokiej obserwowalności.

Najlepsze praktyki dla kontenerów Kubernetes: sprawdzanie dostępności

Jak zastosować Szablon sprawdzania dostępności w Kubernetes?

Z pudełka k8s monitoruje stan podów za pomocą jednego z kontrolerów (Deployments, ReplicaSets, DaemonSets, StatefulSets i tym podobne). Po wykryciu, że pod z jakiegoś powodu się zawiesił, kontroler stara się go ponownie uruchomić lub przeplanować na innym węźle. Jednak pod może zgłaszać, że jest uruchomiony i działa, podczas gdy w rzeczywistości nie funkcjonuje. Podajmy przykład: Twoja aplikacja wykorzystuje Apache jako serwer WWW, zainstalowałeś komponent na kilku podach klastra. Ponieważ biblioteka została skonfigurowana niewłaściwie — wszystkie żądania do aplikacji zwracają kod 500 (wewnętrzny błąd serwera). Przy sprawdzaniu dostarczania kontrola stanu podów daje wynik pozytywny, jednak klienci są zdania inaczej. Tę niepożądaną sytuację opiszemy w następujący sposób:

Najlepsze praktyki dla kontenerów Kubernetes: sprawdzanie dostępności

W naszym przykładzie k8s wykonuje kontrolę dostępności. W tym typie sprawdzania kubelet na bieżąco monitoruje stan procesu w kontenerze. Gdy tylko zrozumie, że proces się zatrzymał, ponownie go uruchamia. Jeśli błąd można naprawić przez proste ponowne uruchomienie aplikacji, a program jest zaprojektowany tak, aby wyłączać się przy każdej usterce, wtedy wystarczy sprawdzenie procesu do przestrzegania NORM i Szablonu sprawdzania działania. Niestety, nie wszystkie błędy da się naprawić przez ponowne uruchomienie. W takim przypadku k8s oferuje 2 bardziej zaawansowane metody identyfikacji problemów z działaniem pod’a: livenessProbe i readinessProbe.

LivenessProbe

W czasie livenessProbe kubelet przeprowadza 3 typy sprawdzeń: nie tylko sprawdza, czy pod działa, ale także, czy jest gotowy na otrzymywanie i adekwatne odpowiadanie na zapytania:

  • Ustawić zapytanie HTTP do pod’a. Odpowiedź powinna zawierać kod odpowiedzi HTTP w zakresie od 200 do 399. W ten sposób kody 5xx i 4xx sygnalizują, że pod ma problemy, nawet jeśli proces działa.
  • Aby sprawdzić pod’y z usługami nie-HTTP (np. serwer pocztowy Postfix), należy nawiązać połączenie TCP.
  • Wykonanie dowolnego polecenia dla pod’a (wewnętrznie). Sprawdzenie uważa się za udane, jeśli kod zakończenia polecenia wynosi 0.

Przykład działania. Definicja następującego pod’a zawiera aplikację NodeJS, która na zapytania HTTP zwraca błąd 500. Aby upewnić się, że kontener jest ponownie uruchamiany w przypadku takiego błędu, używamy parametru livenessProbe:

apiVersion: v1
kind: Pod
metadata:
 name: node500
spec:
 containers:
   - image: magalix/node500
     name: node500
     ports:
       - containerPort: 3000
         protocol: TCP
     livenessProbe:
       httpGet:
         path: /
         port: 3000
       initialDelaySeconds: 5

Nie różni się to od żadnej innej definicji pod’a, ale dodajemy obiekt .spec.containers.livenessProbe. Parametr httpGet przyjmuje ścieżkę, na którą wysyła zapytanie HTTP GET (w naszym przykładzie to /, ale w scenariuszach produkcyjnych może być coś innego jak /api/v1/status). Dodatkowo livenessProbe przyjmuje parametr initialDelaySeconds, który nakazuje operacjom sprawdzającym czekać przez określoną liczbę sekund. Opóźnienie jest potrzebne, ponieważ kontener potrzebuje czasu na uruchomienie, a przy ponownym uruchomieniu przez pewien czas nie będzie dostępny.

Aby zastosować te ustawienia w klastrze, użyj:

kubectl apply -f pod.yaml

Po kilku sekundach można sprawdzić zawartość pod’a za pomocą następującego polecenia:

kubectl describe pods node500

Na końcu wyjścia znajdź oto co.

Jak widać, livenessProbe zainicjował zapytanie HTTP GET, kontener zwrócił błąd 500 (na co był zaprogramowany), kubelet go zrestartował.

Jeśli interesuje Cię, jak zaprogramowana była aplikacja NodeJS, oto plik app.js i Dockerfile, które zostały użyte:

app.js

var http = require('http');

var server = http.createServer(function(req, res) {
    res.writeHead(500, { "Content-type": "text/plain" });
    res.end("Wystąpił błądn");
});

server.listen(3000, function() {
    console.log('Serwer działa na porcie 3000')
})

Dockerfile

FROM node
COPY app.js /
EXPOSE 3000
ENTRYPOINT [ "node","/app.js" ]

Ważne jest, aby zwrócić uwagę na to: livenessProbe zrestartuje kontener tylko przy awarii. Jeśli restart nie naprawi błędu, który przeszkadza w pracy kontenera, kubelet nie będzie w stanie podjąć działań w celu naprawy usterki.

readinessProbe

readinessProbe działa podobnie do livenessProbe (zapytania GET, TCP i wykonywanie poleceń), z wyjątkiem działań naprawczych. Kontener, w którym wystąpił błąd, nie jest restartowany, ale izolowany od ruchu przychodzącego. Wyobraź sobie, że jeden z kontenerów wykonuje wiele obliczeń lub jest pod dużym obciążeniem, co powoduje wzrost czasu odpowiedzi na zapytania. W przypadku livenessProbe uruchamiana jest kontrola dostępności odpowiedzi (za pomocą parametru timeoutSeconds), po której kubelet restartuje kontener. Po uruchomieniu kontener zaczyna wykonywać zasobożerne zadania i jest ponownie restartowany. Może to być krytyczne dla aplikacji, które potrzebują szybkiej odpowiedzi. Na przykład, maszyna w drodze oczekuje odpowiedzi od serwera, odpowiedź jest opóźniona – i maszyna ulega awarii.

Napiszmy definicję readinessProbe, która ustawi czas odpowiedzi na zapytanie GET na maksymalnie dwie sekundy, podczas gdy aplikacja będzie odpowiadać na zapytanie GET po pięciu sekundach. Plik pod.yaml powinien wyglądać następująco:

apiVersion: v1
kind: Pod
metadata:
 name: nodedelayed
spec:
 containers:
   - image: afakharany/node_delayed
     name: nodedelayed
     ports:
       - containerPort: 3000
         protocol: TCP
     readinessProbe:
       httpGet:
         path: /
         port: 3000
       timeoutSeconds: 2

Rozwińmy pod za pomocą kubectl:

kubectl apply -f pod.yaml

Poczekajmy chwilę, a potem sprawdźmy, jak zadziałała readinessProbe:

kubectl describe pods nodedelayed

Na końcu wyjścia można zobaczyć, że część zdarzeń jest podobna do tego.

Jak widać, kubectl nie zrestartował poda, gdy czas sprawdzenia przekroczył 2 sekundy. Zamiast tego anulował żądanie. Połączenia przychodzące są przekierowywane do innych działających podów.

Zauważ, że teraz, gdy pod nie jest przeciążony, kubectl znowu kieruje do niego zapytania: odpowiedzi na zapytania GET nie są już opóźniane.

Dla porównania: poniżej przedstawiony jest zmodyfikowany plik app.js:

var http = require('http');

var server = http.createServer(function(req, res) {
   const sleep = (milliseconds) => {
       return new Promise(resolve => setTimeout(resolve, milliseconds))
   }
   sleep(5000).then(() => {
       res.writeHead(200, { "Content-type": "text/plain" });
       res.end("Hellon");
   })
});

server.listen(3000, function() {
   console.log('Serwer działa na porcie 3000')
})

TL;DR
Przed pojawieniem się aplikacji chmurowych, głównym środkiem monitorowania i sprawdzania stanu aplikacji były logi. Nie istniały jednak narzędzia do podejmowania działań w celu usunięcia usterek. Logi są nadal przydatne, należy je zbierać i wysyłać do systemu zbierania logów w celu analizy sytuacji awaryjnych i podejmowania decyzji. [można to było robić i bez aplikacji chmurowych, na przykład za pomocą monitora, ale z k8s stało się to znacznie prostsze 🙂 – przyp. red. ]

Obecnie poprawki muszą być wprowadzane niemal w czasie rzeczywistym, dlatego aplikacje nie mogą już być czarnymi skrzynkami. Muszą one wykazywać punkty końcowe, które pozwalają systemom monitorowania zapytywać i zbierać cenne dane o stanie procesów, aby w razie potrzeby reagować natychmiastowo. Nazywa się to wzorcem projektowym sprawdzania stanu operacyjnego, który kieruje się zasadą wysokiej obserwowalności (HOP).

Kubernetes domyślnie oferuje 2 rodzaje sprawdzania stanu: readinessProbe i livenessProbe. Oba wykorzystują te same typy sprawdzeń (zapytania HTTP GET, połączenia TCP i wykonywanie poleceń). Różnią się tym, jakie decyzje podejmują w odpowiedzi na problemy w podach. livenessProbe uruchamia kontener ponownie, mając nadzieję, że błąd się nie powtórzy, a readinessProbe izoluje pod od przychodzącego ruchu – aż do usunięcia przyczyny problemu.

Właściwe projektowanie aplikacji powinno obejmować oba rodzaje sprawdzania, a także zbierać wystarczającą ilość danych, szczególnie w sytuacjach wyjątkowych. Powinno również wyświetlać niezbędne punkty końcowe API, które przekazują systemowi monitorowania (temu samemu Prometheus) ważne metryki stanu operacyjnego.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster