Pięć chybień podczas wdrażania pierwszej aplikacji na Kubernetes

Pięć chybień podczas wdrażania pierwszej aplikacji na KubernetesPorażka Arisa Dreamera

Wiele osób uważa, że ​​wystarczy przenieść aplikację na Kubernetes (albo przy pomocy Helma, albo ręcznie) – i będzie szczęście. Ale nie wszystko jest takie proste.

Zespół Rozwiązania chmurowe Mail.ru przetłumaczył artykuł inżyniera DevOps Juliana Gindy'ego. Opowiada, jakie pułapki napotkała jego firma podczas procesu migracji, abyś nie nadepnął na tę samą prowizję.

Krok pierwszy: skonfiguruj żądania podów i limity

Zacznijmy od skonfigurowania czystego środowiska, w którym będą działać nasze pody. Kubernetes świetnie radzi sobie z planowaniem podów i przełączaniem awaryjnym. Okazało się jednak, że program planujący czasami nie może umieścić poda, jeśli trudno oszacować, ile zasobów potrzebuje do pomyślnego działania. W tym miejscu pojawiają się prośby o zasoby i limity. Toczy się wiele dyskusji na temat najlepszego podejścia do ustalania żądań i limitów. Czasami wydaje się, że jest to bardziej sztuka niż nauka. Oto nasze podejście.

Żądania podów to główna wartość używana przez program planujący do optymalnego umieszczenia poda.

Z Dokumentacja Kubernetesa: Krok filtrowania definiuje zestaw węzłów, w których można zaplanować Pod. Na przykład filtr PodFitsResources sprawdza, czy węzeł ma wystarczającą ilość zasobów, aby spełnić określone żądania zasobów z poda.

Żądania aplikacji wykorzystujemy w taki sposób, że możemy oszacować, ile zasobów w rzeczywistości Aplikacja potrzebuje go do prawidłowego działania. W ten sposób program planujący może realistycznie rozmieścić węzły. Początkowo chcieliśmy przełożyć żądania, aby zapewnić wystarczającą ilość zasobów dla każdego Poda, ale zauważyliśmy, że czas planowania znacznie się wydłużył, a niektóre Pody nie zostały w pełni zaplanowane, tak jakby nie było dla nich żądań zasobów.

W takim przypadku program planujący często „wyciskał” strąki i nie był w stanie zmienić ich harmonogramu, ponieważ płaszczyzna kontrolna nie miała pojęcia, ile zasobów będzie potrzebowała aplikacja, co jest kluczowym elementem algorytmu planowania.

Limity podów jest wyraźniejszym limitem dla poda. Reprezentuje maksymalną ilość zasobów, które klaster przydzieli do kontenera.

Znowu od oficjalna dokumentacja: Jeśli kontener ma limit pamięci wynoszący 4 GiB, kubelet (i środowisko wykonawcze kontenera) wymusi to. Środowisko wykonawcze zapobiega używaniu przez kontener więcej niż określony limit zasobów. Na przykład, gdy proces w kontenerze próbuje użyć więcej niż dozwolona ilość pamięci, jądro systemu kończy proces z błędem „brak pamięci” (OOM).

Kontener zawsze może użyć więcej zasobów niż określa żądanie zasobu, ale nigdy nie może użyć więcej niż limit. Ta wartość jest trudna do prawidłowego ustawienia, ale jest bardzo ważna.

Idealnie byłoby, gdyby wymagania dotyczące zasobów poda zmieniały się w trakcie cyklu życia procesu bez ingerencji w inne procesy w systemie — taki jest cel ustalania limitów.

Niestety nie mogę podać konkretnych wskazówek, jakie wartości ustawić, ale sami przestrzegamy następujących zasad:

  1. Za pomocą narzędzia do testowania obciążenia symulujemy podstawowy poziom ruchu i obserwujemy wykorzystanie zasobów pod (pamięć i procesor).
  2. Ustaw żądania pod na dowolnie niską wartość (z limitem zasobów około 5-krotności wartości żądań) i obserwuj. Gdy żądania są na zbyt niskim poziomie, proces nie może się rozpocząć, co często powoduje tajemnicze błędy środowiska wykonawczego Go.

Zauważam, że wyższe limity zasobów utrudniają planowanie, ponieważ kapsuła potrzebuje węzła docelowego z wystarczającą ilością dostępnych zasobów.

Wyobraź sobie sytuację, w której masz lekki serwer WWW z bardzo wysokim limitem zasobów, na przykład 4 GB pamięci. Ten proces prawdopodobnie będzie wymagał skalowania w poziomie, a każdy nowy pod będzie musiał zostać zaplanowany w węźle z co najmniej 4 GB dostępnej pamięci. Jeśli taki węzeł nie istnieje, klaster musi wprowadzić nowy węzeł, aby przetworzyć ten pod, co może zająć trochę czasu. Ważne jest, aby osiągnąć minimalną różnicę między żądaniami zasobów a limitami, aby zapewnić szybkie i płynne skalowanie.

Krok drugi: skonfiguruj testy żywotności i gotowości

To kolejny subtelny temat często omawiany w społeczności Kubernetes. Ważne jest, aby dobrze rozumieć testy Liveness i Readiness, ponieważ zapewniają one mechanizm stabilnej pracy oprogramowania i minimalizują przestoje. Mogą one jednak poważnie wpłynąć na wydajność aplikacji, jeśli nie zostaną poprawnie skonfigurowane. Poniżej znajduje się podsumowanie tego, czym są obie próbki.

Żywotność pokazuje, czy kontener jest uruchomiony. Jeśli to się nie powiedzie, kubelet zabije kontener i zostaną dla niego włączone zasady ponownego uruchamiania. Jeśli kontener nie jest wyposażony w Liveness Probe, domyślnym stanem będzie sukces - jak podano w Dokumentacja Kubernetesa.

Sondy liveness powinny być tanie, czyli nie zużywać dużo zasobów, bo często się uruchamiają i powinny informować Kubernetesa, że ​​aplikacja działa.

Jeśli ustawisz opcję uruchamiania co sekundę, spowoduje to dodanie 1 żądania na sekundę, więc pamiętaj, że do przetworzenia tego ruchu będą wymagane dodatkowe zasoby.

W naszej firmie testy Liveness testują podstawowe komponenty aplikacji, nawet jeśli dane (na przykład ze zdalnej bazy danych lub pamięci podręcznej) nie są w pełni dostępne.

Skonfigurowaliśmy punkt końcowy „kondycja” w aplikacjach, który po prostu zwraca kod odpowiedzi 200. Jest to wskazówka, że ​​proces działa i jest w stanie obsłużyć żądania (ale jeszcze nie ruch).

Próbka Gotowość wskazuje, czy kontener jest gotowy do obsługi żądań. Jeśli sonda gotowości nie powiedzie się, kontroler punktu końcowego usuwa adres IP pod z punktów końcowych wszystkich usług pasujących do pod. Jest to również określone w dokumentacji Kubernetes.

Sondy gotowości zużywają więcej zasobów, ponieważ muszą trafić do backendu w taki sposób, aby pokazać, że aplikacja jest gotowa do przyjęcia żądań.

W społeczności toczy się wiele dyskusji na temat bezpośredniego dostępu do bazy danych. Biorąc pod uwagę narzut (kontrole są częste, ale można je kontrolować) zdecydowaliśmy, że dla niektórych aplikacji gotowość do obsługi ruchu jest liczona dopiero po sprawdzeniu, czy rekordy są zwracane z bazy danych. Dobrze zaprojektowane testy gotowości zapewniły wyższy poziom dostępności i wyeliminowały przestoje podczas wdrażania.

Jeśli zdecydujesz się wysłać zapytanie do bazy danych w celu przetestowania gotowości Twojej aplikacji, upewnij się, że jest to możliwie tanie. Weźmy to zapytanie:

SELECT small_item FROM table LIMIT 1

Oto przykład jak konfigurujemy te dwie wartości w Kubernetes:

livenessProbe: 
 httpGet:   
   path: /api/liveness    
   port: http 
readinessProbe:  
 httpGet:    
   path: /api/readiness    
   port: http  periodSeconds: 2

Możesz dodać kilka dodatkowych opcji konfiguracyjnych:

  • initialDelaySeconds - ile sekund upłynie między wystrzeleniem kontenera a rozpoczęciem wystrzelenia sond.
  • periodSeconds — odstęp czasu między przebiegami próbek.
  • timeoutSeconds — liczba sekund, po których kapsuła zostaje uznana za awaryjną. Normalny limit czasu.
  • failureThreshold to liczba niepowodzeń testu przed wysłaniem sygnału restartu do poda.
  • successThreshold to liczba udanych prób przed przejściem poda do stanu gotowości (po awarii, gdy pod uruchamia się lub odzyskuje).

Krok trzeci: ustawienie domyślnych zasad sieciowych kapsuły

Kubernetes ma „płaską” topografię sieci, domyślnie wszystkie pody komunikują się ze sobą bezpośrednio. W niektórych przypadkach nie jest to pożądane.

Potencjalnym problemem związanym z bezpieczeństwem jest to, że osoba atakująca może użyć jednej podatnej aplikacji do wysyłania ruchu do wszystkich podów w sieci. Podobnie jak w wielu obszarach bezpieczeństwa, tutaj obowiązuje zasada najmniejszych uprawnień. Idealnie byłoby, gdyby zasady sieciowe wyraźnie określały, które połączenia między podami są dozwolone, a które nie.

Na przykład poniżej przedstawiono prostą zasadę, która odrzuca cały ruch przychodzący do określonej przestrzeni nazw:

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:  
 name: default-deny-ingress
spec:  
 podSelector: {}  
 policyTypes:  
   - Ingress

Wizualizacja tej konfiguracji:

Pięć chybień podczas wdrażania pierwszej aplikacji na Kubernetes
(https://miro.medium.com/max/875/1*-eiVw43azgzYzyN1th7cZg.gif)
Więcej szczegółów tutaj.

Krok czwarty: niestandardowe zachowanie z hakami i kontenerami inicjującymi

Jednym z naszych głównych celów było zapewnienie wdrożeń w Kubernetes bez przestojów dla programistów. Jest to trudne, ponieważ istnieje wiele opcji zamykania aplikacji i zwalniania ich zasobów.

Szczególne trudności pojawiły się m.in nginx. Zauważyliśmy, że podczas sekwencyjnego wdrażania tych kapsuł, aktywne połączenia były przerywane przed pomyślnym zakończeniem.

Po szeroko zakrojonych badaniach w Internecie okazało się, że Kubernetes nie czeka, aż połączenia Nginx się wyczerpią, zanim zamknie kapsułę. Za pomocą haka pre-stop wdrożyliśmy następującą funkcjonalność i całkowicie pozbyliśmy się przestoju:

lifecycle: 
 preStop:
   exec:
     command: ["/usr/local/bin/nginx-killer.sh"]

a tutaj nginx-killer.sh:

#!/bin/bash
sleep 3
PID=$(cat /run/nginx.pid)
nginx -s quit
while [ -d /proc/$PID ]; do
   echo "Waiting while shutting down nginx..."
   sleep 10
done

Innym niezwykle użytecznym paradygmatem jest użycie kontenerów inicjujących do obsługi uruchamiania określonych aplikacji. Jest to szczególnie przydatne w przypadku procesu migracji bazy danych wymagającego dużej ilości zasobów, który musi zostać uruchomiony przed uruchomieniem aplikacji. Możesz także określić wyższy limit zasobów dla tego procesu bez ustawiania takiego limitu dla aplikacji głównej.

Innym powszechnym schematem jest dostęp do sekretów w kontenerze init, który dostarcza te poświadczenia do głównego modułu, co zapobiega nieautoryzowanemu dostępowi do sekretów z samego głównego modułu aplikacji.

Jak zwykle cytat z dokumentacji: kontenery inicjujące bezpiecznie uruchamiają kod użytkownika lub narzędzia, które w przeciwnym razie zagroziłyby bezpieczeństwu obrazu kontenera aplikacji. Trzymając niepotrzebne narzędzia oddzielnie, ograniczasz obszar ataku obrazu kontenera aplikacji.

Krok piąty: konfiguracja jądra

Na koniec porozmawiajmy o bardziej zaawansowanej technice.

Kubernetes to niezwykle elastyczna platforma, która umożliwia uruchamianie obciążeń w dowolny sposób. Mamy wiele wysoce wydajnych aplikacji, które są niezwykle wymagające pod względem zasobów. Po przeprowadzeniu szeroko zakrojonych testów obciążenia stwierdziliśmy, że jedna z aplikacji miała trudności z nadążaniem za oczekiwanym obciążeniem ruchem, gdy obowiązywały domyślne ustawienia Kubernetes.

Jednak Kubernetes umożliwia uruchamianie uprzywilejowanego kontenera, który zmienia tylko parametry jądra dla określonego zasobnika. Oto, czego użyliśmy do zmiany maksymalnej liczby otwartych połączeń:

initContainers:
  - name: sysctl
     image: alpine:3.10
     securityContext:
         privileged: true
      command: ['sh', '-c', "sysctl -w net.core.somaxconn=32768"]

Jest to bardziej zaawansowana technika, która często nie jest potrzebna. Ale jeśli Twoja aplikacja ma trudności z radzeniem sobie z dużym obciążeniem, możesz spróbować poprawić niektóre z tych ustawień. Więcej informacji o tym procesie i ustawianiu różnych wartości - jak zawsze w oficjalnej dokumentacji.

Na zakończenie

Chociaż Kubernetes może wydawać się rozwiązaniem gotowym do użycia, istnieje kilka kluczowych kroków, które należy podjąć, aby aplikacje działały płynnie.

Podczas migracji do Kubernetes ważne jest przestrzeganie „cyklu testowania obciążenia”: uruchom aplikację, przetestuj ją pod obciążeniem, obserwuj metryki i zachowanie skalowania, dostosuj konfigurację na podstawie tych danych, a następnie powtórz ten cykl ponownie.

Podchodź realistycznie do oczekiwanego ruchu i spróbuj wyjść poza niego, aby zobaczyć, które komponenty psują się jako pierwsze. Dzięki temu iteracyjnemu podejściu tylko kilka z wymienionych zaleceń może wystarczyć do osiągnięcia sukcesu. Lub może być wymagane bardziej dogłębne dostosowanie.

Zawsze zadawaj sobie następujące pytania:

  1. Ile zasobów zużywają aplikacje i jak zmieni się ta ilość?
  2. Jakie są rzeczywiste wymagania dotyczące skalowania? Jaki ruch będzie średnio obsługiwał aplikacja? A co ze szczytem ruchu?
  3. Jak często usługa będzie musiała być skalowana w poziomie? Jak szybko nowe pody muszą być uruchomione, aby odbierać ruch?
  4. Jak wdzięcznie zamykają się strąki? Czy jest to w ogóle konieczne? Czy możliwe jest wdrożenie bez przestojów?
  5. Jak zminimalizować ryzyko związane z bezpieczeństwem i ograniczyć szkody wyrządzane przez naruszone pody? Czy jakieś usługi mają uprawnienia lub dostępy, których nie potrzebują?

Kubernetes zapewnia niesamowitą platformę, która umożliwia wykorzystanie najlepszych praktyk do wdrażania tysięcy usług w klastrze. Jednak wszystkie aplikacje są różne. Czasami wdrożenie wymaga trochę więcej pracy.

Na szczęście Kubernetes zapewnia niezbędne ustawienia do osiągnięcia wszystkich celów technicznych. Korzystając z kombinacji żądań zasobów i limitów, sond aktywności i gotowości, kontenerów inicjujących, zasad sieciowych i niestandardowego dostrajania jądra, można osiągnąć wysoką wydajność wraz z odpornością na błędy i szybką skalowalnością.

Co jeszcze przeczytać:

  1. Najlepsze praktyki i najlepsze praktyki dotyczące uruchamiania kontenerów i Kubernetes w środowiskach produkcyjnych.
  2. Ponad 90 przydatnych narzędzi dla Kubernetes: wdrażanie, zarządzanie, monitorowanie, bezpieczeństwo i nie tylko.
  3. Nasz kanał Around Kubernetes w Telegramie.

Źródło: www.habr.com

Dodaj komentarz