10 typowych błędów przy używaniu Kubernetes

Przyp. tłum.: autorzy tego artykułu to inżynierowie z małej czeskiej firmy pipetail. Udało im się stworzyć wspaniałą listę [czasami banalnych, ale wciąż] bardzo aktualnych problemów i nieporozumień związanych z eksploatacją klastrów Kubernetes.

10 typowych błędów przy używaniu Kubernetes

Na przestrzeni lat korzystania z Kubernetes mieliśmy okazję pracować z dużą liczbą klastrów (zarówno zarządzanych, jak i niezależnych - na GCP, AWS i Azure). Z czasem zaczęliśmy dostrzegać, że niektóre błędy nieustannie się powtarzają. Nie ma w tym nic wstydliwego: sami popełniliśmy większość z nich!

W artykule zebrano najczęstsze błędy, a także wzmiankowano o tym, jak je naprawić.

1. Zasoby: żądania i limity

Ten punkt zdecydowanie zasługuje na szczególną uwagę i pierwsze miejsce na liście.

Żądanie CPU zazwyczaj jest albo w ogóle nieustawione, albo ma bardzo niską wartość (aby zmieścić jak najwięcej podów na każdym węźle). W rezultacie węzły są przeciążone. W czasie dużego obciążenia moce obliczeniowe węzła są w pełni wykorzystywane, a konkretne obciążenie robocze otrzymuje tylko to, co 'zażądało' poprzez throttling CPU. Prowadzi to do zwiększenia opóźnień w aplikacji, timeoutów i innych nieprzyjemnych skutków. (Więcej na ten temat przeczytaj w naszym innym niedawnym tłumaczeniu: „Limity CPU i agresywny throttling w Kubernetes” — przyp. tłum.)

BestEffort (zdecydowanie nie zalecane):

resources: {}

Ekstremalnie niskie żądanie CPU (zdecydowanie nie zalecane):

   resources:
      Requests:
        cpu: "1m"

Z drugiej strony, posiadanie limitu CPU może prowadzić do nieuzasadnionego pomijania cykli przez pody, nawet jeśli procesor węzła nie jest całkowicie obciążony. Ponownie, może to prowadzić do zwiększenia opóźnień. Trwają dyskusje na temat parametru CPU CFS quota w jądrze Linuxa oraz throttlingu CPU w zależności od ustalonych limitów, a także wyłączania kwoty CFS… Niestety, limity CPU mogą powodować więcej problemów, niż są w stanie rozwiązać. Więcej na ten temat można znaleźć w poniższym linku.

Nadmierne przydzielanie (overcommitting) pamięci może prowadzić do poważniejszych problemów. Osiągnięcie limitu CPU wiąże się z pomijaniem cykli, podczas gdy osiągnięcie limitu pamięci prowadzi do „zabicia” poda. Czy kiedykolwiek obserwowałeś OOMkill? Да, речь идет именно о нем.

Chcesz zminimalizować prawdopodobieństwo tego zdarzenia? Nie przydzielaj nadmiernych ilości pamięci i używaj Guaranteed QoS (Quality of Service), ustawiając memory request równym limitowi (jak w poniższym przykładzie). Więcej na ten temat w prezentacji Henninga Jacobsa (głównego inżyniera w Zalando).

Burstable (większa szansa na OOMkilled):

   resources:
      requests:
        memory: "128Mi"
        cpu: "500m"
      limits:
        memory: "256Mi"
        cpu: 2

Guaranteed:

   resources:
      requests:
        memory: "128Mi"
        cpu: 2
      limits:
        memory: "128Mi"
        cpu: 2

Co może pomóc w konfiguracji zasobów?

Dzięki metrics-server można sprawdzić aktualne zużycie CPU oraz pamięci przez pody (i kontenery wewnątrz nich). Prawdopodobnie już z niego korzystasz. Wystarczy wykonać następujące komendy:

kubectl top pods
kubectl top pods --containers
kubectl top nodes

Jednakże pokazują one tylko aktualne zużycie. Na ich podstawie można uzyskać przybliżony obraz wielkości, ale w ostateczności potrzebne będzie historie zmian metryk w czasie (aby odpowiedzieć na pytania takie jak: „Jakie było maksymalne obciążenie CPU?”, „Jakie było obciążenie wczoraj rano?” itd.). W tym celu można użyć Prometheus, DataDog i innych narzędzi. One po prostu pobierają metryki z metrics-server i je przechowują, a użytkownik może je zamawiać i budować odpowiednie wykresy.

VerticalPodAutoscaler przekazywanie automatyzować ten proces. Śledzi historię zużycia CPU i pamięci i dostosowuje nowe request’y i limit’y na podstawie tych informacji.

Efektywne wykorzystanie mocy obliczeniowych to niełatwe zadanie. To jak ciągłe granie w tetris. Jeśli płacisz za dużo za moc obliczeniową przy niskim średnim zużyciu (powiedzmy, ~10%), zalecamy zwrócenie uwagi na produkty oparte na AWS Fargate lub Virtual Kubelet. Są one zbudowane na modelu billingowym serverless/pay-per-usage, co w takich warunkach może okazać się tańsze.

2. Liveness i readiness probes

Domyślnie kontrole stanu liveness i readiness w Kubernetes nie są włączone. Czasami zapomina się je włączyć…

Ale jak jeszcze można zainicjować ponowne uruchomienie usługi w przypadku nieusuwalnego błędu? I jak load balancer wie, że dany pod jest gotowy do przyjmowania ruchu? Albo że jest w stanie obsłużyć większy ruch?

Często myli się te proby:

  • Liveness — sprawdzenie „żywotności”, które ponownie uruchamia pod w przypadku nieudanego zakończenia;
  • Gotowość — sprawdzenie gotowości, które w przypadku niepowodzenia odłącza pod od usługi Kubernetes (można to sprawdzić za pomocą kubectl get endpoints) i ruch do niego nie dociera, dopóki kolejne sprawdzenie nie zakończy się pomyślnie.

Oba te sprawdzenia SĄ WYKONYWANE W TRAKCIE CAŁEGO CYKLU ŻYCIA POD’A. To jest bardzo ważne.

Powszechnym błędnym przekonaniem jest to, że próbki gotowości uruchamiane są tylko na początku, aby serwis balancers mógł dowiedzieć się, że pod jest gotowy (Gotowy) i może rozpocząć przetwarzanie ruchu. To jednak tylko jedna z opcji ich zastosowania.

Inną jest możliwość dowiedzenia się, że ruch do pod’a jest zbyt duży i przeciąża go (lub pod wykonuje zasobożerne obliczenia). W takim przypadku kontrola gotowości pomaga zredukować obciążenie pod’a i "schłodzić" go. Pomyślne zakończenie kontroli gotowości w przyszłości pozwala ponownie zwiększyć obciążenie pod’a. W takim przypadku (w przypadku niepowodzenia próby gotowości) niepowodzenie testu żywotności byłoby bardzo nieproduktywne. Dlaczego ponownie uruchamiać pod, który jest zdrowy i pracuje ze wszystkich sił?

Dlatego w niektórych przypadkach całkowity brak sprawdzeń jest lepszy niż ich włączenie z błędnie skonfigurowanymi parametrami. Jak wspomniano wcześniej, jeśli sprawdzenie żywotności kopiuje sprawdzenie gotowości, to jesteś w dużych kłopotach. Możliwą opcją jest skonfigurowanie tylko testu gotowości, a niebezpiecznego testu żywotności pozostawiając na boku.

Oba typy sprawdzeń nie powinny kończyć się niepowodzeniem w przypadku awarii wspólnych zależności, w przeciwnym razie doprowadzi to do kaskadowej (lawinowej) awarii wszystkich pod’ów. Innymi słowy, nie szkodź samemu sobie.

3. LoadBalancer dla każdej usługi HTTP

Prawdopodobnie masz w klastrze usługi HTTP, które chciałbyś przekazać światu zewnętrznemu.

Jeśli otworzysz usługę jako type: LoadBalancer, jego kontroler (w zależności od dostawcy) dostarczy i uzgodni zewnętrzny LoadBalancer (niekoniecznie działający na L7, z pewnością bardziej na L4), co może wpłynąć na koszty (zewnętrzny stały adres IPv4, moc obliczeniowa, taryfikacja za sekundę) z powodu konieczności stworzenia dużej liczby podobnych zasobów.

W tym przypadku znacznie logiczniej jest użyć jednego zewnętrznego balansu obciążenia, otwierając usługi jako type: NodePort. Lub, co jeszcze lepsze, wdrożyć coś w rodzaju nginx-ingress-controller (lub traefik), który będzie jedynym NodePort endpointem połączonym z zewnętrznym load balancerem i będzie kierować ruch w klastrze za pomocą ingress-zasobów Kubernetes.

Inne wewnątrzklastrowe (mikro)usługi, które współpracują ze sobą, mogą „rozmawiać” ze sobą za pomocą usług typu ClusterIP i wbudowanego mechanizmu odkrywania usług przez DNS. Tylko nie używaj ich publicznych DNS/IP, ponieważ może to wpłynąć na opóźnienie i zwiększyć koszty usług w chmurze.

4. Automatyczne skalowanie klastra bez uwzględnienia jego cech

Podczas dodawania węzłów do klastra i ich usuwania nie należy polegać na podstawowych metrykach, takich jak wykorzystanie CPU na tych węzłach. Planowanie poda powinno odbywać się z uwzględnieniem wielu ograniczeń, takich jak affinity podów/węzłów, tainty i tolerancje, wymagania zasobów, QoS itd. Użycie zewnętrznego autoskalera, który nie uwzględnia tych niuansów, może prowadzić do problemów.

Wyobraź sobie, że jakiś pod ma być zaplanowany, ale wszystkie dostępne moce CPU są zarezerwowane i pod utknie w stanie Pending. Zewnętrzny autoskaler widzi średnie aktualne obciążenie CPU (a nie wymagane) i nie inicjuje rozszerzenia (scale-out) — nie dodaje nowego węzła. W rezultacie dany pod nie zostanie zaplanowany.

Jednak odwrotne skalowanie (scale-in) — usunięcie węzła z klastra — zawsze jest trudniejsze do zrealizowania. Wyobraź sobie, że masz stateful pod (z podłączonym trwałym pamięcią). Persistent-volume zazwyczaj należą do określonej strefy dostępności i nie są replikowane w regionie. W ten sposób, jeśli zewnętrzny autoskaler usunie węzeł z tym podem, to scheduler nie będzie mógł zaplanować tego poda na innym węźle, ponieważ można to zrobić tylko w tej strefie dostępności, w której znajduje się trwała pamięć. Pod utknie w stanie Pending.

W społeczności Kubernetes cieszy się dużą popularnością cluster-autoscaler. Działa w klastrze, wspiera API od głównych dostawców usług w chmurze, uwzględnia wszystkie ograniczenia i jest w stanie skalować się w wyżej wymienionych przypadkach. Potrafi także wykonać scale-in, zachowując wszystkie ustalone ograniczenia, oszczędzając pieniądze (które w przeciwnym razie byłyby wydane na niepotrzebne moce).

5. Lekceważenie możliwości IAM/RBAC

Uważaj na używanie użytkowników IAM z stałymi sekretami dla maszyn i aplikacji. Organizuj tymczasowy dostęp, korzystając z ról i kont serwisowych (service accounts).

Często spotykamy się z tym, że klucze dostępu (i sekrety) są 'hardcodowane' w konfiguracji aplikacji, a także z lekceważeniem rotacji sekretnych, mimo dostępności do Cloud IAM. Używaj ról IAM i kont serwisowych zamiast użytkowników, gdy jest to właściwe.

10 typowych błędów przy używaniu Kubernetes

Zapomnij o kube2iam i przejdź bezpośrednio do ról IAM dla kont serwisowych (jak to opisano w tego samego tytułu notatka Štěpán Vraný):

apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-app-role
  name: my-serviceaccount
  namespace: default

Jedna adnotacja. Niespecjalnie trudne, prawda?

Ponadto, nie nadajaj kontom serwisowym i profilom instancji przywilejów admin i cluster-admin, jeśli nie są one do tego potrzebne. Realizacja tego jest nieco trudniejsza, szczególnie w RBAC K8s, ale z pewnością wart jest wysiłku.

6. Nie polegaj na automatycznej anti-affinity dla podów

Wyobraź sobie, że masz trzy repliki jakiegoś deploymentu na węźle. Węzeł pada, a razem z nim wszystkie repliki. Niezbyt przyjemna sytuacja, prawda? Ale dlaczego wszystkie repliki znajdowały się na jednym węźle? Czy Kubernetes nie powinien zapewniać wysokiej dostępności (HA)?!

Niestety, planista Kubernetes z własnej inicjatywy nie przestrzega zasad rozdzielności (anti-affinity) dla podów. Należy to wyraźnie określić:

// опущено для краткости
      labels:
        app: zk
// опущено для краткости
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: "app"
                    operator: In
                    values:
                    - zk
              topologyKey: "kubernetes.io/hostname"

I to wszystko. Teraz pody będą planowane na różnych węzłach (to warunek sprawdzany jest tylko w czasie planowania, ale nie ich pracy — stąd requiredDuringSchedulingIgnoredDuringExecution).

Tutaj mówimy o podAntiAffinity na różnych węzłach: topologyKey: "kubernetes.io/hostname", — a nie o różnych strefach dostępności. Aby zrealizować pełną HA, trzeba będzie zagłębić się w ten temat.

7. Ignorowanie PodDisruptionBudgetów

Wyobraź sobie, że masz produkcyjne obciążenie w klastrze Kubernetes. Okresowo węzły i sam klaster muszą być aktualizowane (lub wyłączane z eksploatacji). PodDisruptionBudget (PDB) to coś w rodzaju umowy serwisowej między administratorami klastra a użytkownikami.

PDB pozwala unikać zakłóceń w pracy usług spowodowanych brakiem węzłów:

apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: zk-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: zookeeper

W tym przykładzie jako użytkownik klastra mówisz administratorom: „Hej, mam serwis zookeeper, i niezależnie od tego, co robicie, chciałbym, żeby przynajmniej 2 repliki tego serwisu były zawsze dostępne.”

Można o tym przeczytać więcej tutaj.

8. Kilku użytkowników lub środowisk w jednym klastrze

Przestrzenie nazw Kubernetes (namespaces) nie zapewniają silnej izolacji.

Panuje powszechne przekonanie, że jeśli wdrożysz obciążenie nie-prod w jednej przestrzeni nazw, a obciążenie prod w drugiej, to nie będą wpływać na siebie nawzajem… Jednak pewien poziom izolacji można osiągnąć za pomocą zapytań/ograniczeń zasobów, ustawienia kwot, definiowania priorityClass. Pewną „fizyczną” izolację w warstwie danych zapewniają affinities, tolerations, taints (lub nodeselectors), jednak takie podział jest dość trudny do zrealizowania.

Ci, którzy muszą łączyć oba typy obciążeń w jednym klastrze, będą musieli zmierzyć się ze złożonością. Jeśli jednak nie ma takiej potrzeby, i jesteś w stanie utrzymać jeszcze jeden klaster (powiedzmy, w chmurze publicznej), to lepiej to zrobić. Pozwoli to osiągnąć znacznie wyższy poziom izolacji.

9. externalTrafficPolicy: Cluster

Bardzo często obserwujemy, że cały ruch do klastra przechodzi przez serwis typu NodePort, dla którego domyślnie ustalana jest polityka externalTrafficPolicy: Cluster. Oznacza to, że NodePort jest otwarty na każdym węźle w klastrze, i można użyć dowolnego z nich do kontaktu z potrzebnym serwisem (zestawem pod’ów).

10 typowych błędów przy używaniu Kubernetes

Jednakże rzeczywiste pod’y związane z powyższym serwisem NodePort zazwyczaj istnieją tylko na jakimś podzbiorze tych węzłów.Innymi słowy, jeśli połączę się z węzłem, na którym nie ma potrzebnego pod’a, przekieruje on ruch do innego węzła, dodając dodatkowy odcinek (hop) i zwiększając opóźnienie (jeśli węzły znajdują się w różnych strefach dostępności/centrum danych, opóźnienie może być dość wysokie; ponadto wzrosną koszty ruchu egress).

Z drugiej strony, jeśli dla jakiegoś serwisu Kubernetes ustalono politykę externalTrafficPolicy: Local, to NodePort otwiera się tylko na tych węzłach, gdzie faktycznie uruchomione są potrzebne pod’y. Przy użyciu zewnętrznego balansu obciążenia, który sprawdza stany (healthchecking) endpointów (jak to robi AWS ELB), on będzie kierować ruch tylko do odpowiednich węzłów., co korzystnie wpłynie na opóźnienia, potrzeby obliczeniowe oraz koszty egress (a zdrowy rozsądek również to sugeruje).

Prawdopodobnie już używasz czegoś takiego jak traefik lub nginx-ingress-controller jako końcowy punkt NodePort (lub LoadBalancer, który również korzysta z NodePort) do routingu ruchu HTTP ingress, a ustawienie tej opcji może znacząco zmniejszyć opóźnienia w takich zapytaniach.

W tej publikacji można dokładniej poznać externalTrafficPolicy, jej zalety i wady.

10. Nie przywiązywuj się do klastrów i nie nadużywaj kontrolnej warstwy.

Kiedyś serwery nazywano imionami własnymi: Anton, HAL9000 i Colossus... Dziś zastąpiły je losowo generowane identyfikatory. Niemniej jednak, nawyk pozostał i teraz własne imiona przypisuje się klastrom.

Typowa historia (oparta na rzeczywistych wydarzeniach): wszystko zaczęło się od dowodu koncepcji, dlatego klaster nosił dumne imię testing… Minęły lata, a on wciąż jest używany w produkcji i wszyscy boją się go dotknąć.

Nie ma w tym nic zabawnego, że klastry zamieniają się w pupile, dlatego zalecamy okresowe ich usuwanie, przy okazji ćwicząc w odzyskiwaniu po awariach (w tym pomoże chaos engineering — przyp. tłum.)). Ponadto warto zająć się także warstwą zarządzającą (control plane).Obawa przed jej dotykaniem to niezbyt dobry znak. Etcd umiera? Chłopaki, naprawdę wplątaliście się w poważne kłopoty!

Z drugiej strony nie należy przesadzać z manipulacjami przy niej. Z biegiem czasu warstwa zarządzająca może stać się wolna.Prawdopodobnie jest to związane z dużą liczbą tworzonych obiektów bez ich rotacji (typowa sytuacja przy używaniu Helm z ustawieniami domyślnymi, przez co nie aktualizuje ich stan w configmapach/sekretach — jako rezultat, w warstwie zarządzającej gromadzą się tysiące obiektów) lub z ciągłym edytowaniem obiektów kube-api (do automatycznego skalowania, CI/CD, monitorowania, logów zdarzeń, kontrolerów itp.).

Ponadto zalecamy sprawdzenie umów SLA/SLO z dostawcą managed Kubernetes i zwrócenie uwagi na gwarancje. Dostawca może gwarantować dostępność warstwy zarządzającej (lub jej podkomponentów), ale nie p99-opóźnienie zapytań, które do niego wysyłasz. Innymi słowy, można wprowadzić kubectl get nodes, a odpowiedź można uzyskać dopiero po 10 minutach, i nie będzie to naruszeniem warunków umowy serwisowej.

11. Bonus: użycie tagu latest

A to już klasyka. Ostatnio rzadziej spotykamy się z tą techniką, ponieważ wielu, nauczeni gorzkim doświadczeniem, przestało używać tagu :latest i zaczęło stabilizować (pin) wersje. Hurra!

ECR zapewnia niezmienność tagów obrazów; zalecamy zapoznać się z tą niezwykłą cechą.

Podsumowanie

Nie oczekuj, że wszystko zadziała jak za dotknięciem czarodziejskiej różdżki: Kubernetes nie jest panaceum. Zła aplikacja pozostanie taka nawet w Kubernetes (i być może stanie się jeszcze gorsza). Lekkomyślność doprowadzi do nadmiarowej złożoności, wolnej i napiętej pracy warstwy zarządzającej. Ponadto, ryzykujesz brak strategii odzyskiwania po awarii. Nie liczyć, że Kubernetes rozwiąże z kilkoma prostymi ustawieniami kwestie izolacji i wysokiej dostępności. Poświęć trochę czasu, aby uczynić swoją aplikację prawdziwie cloud native.

Można zapoznać się z nieudanymi doświadczeniami różnych zespołów w tej zbiórce historii od Henninga Jacobsa.

Chętni do uzupełnienia listy błędów zawartych w tym artykule mogą skontaktować się z nami na Twitterze (@MarekBartik, @MstrsObserver).

P.S. od tłumacza

Przeczytaj także na naszym blogu:

Ź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