Dziewięć wskazówek dotyczących zwiększenia wydajności Kubernetes

Dziewięć wskazówek dotyczących zwiększenia wydajności Kubernetes

Cześć wszystkim! Nazywam się Oleg Sidorenkov, pracuję w firmie DomKlik jako kierownik zespołu infrastruktury. Używamy 'Kubika' w produkcji już od ponad trzech lat, i przez ten czas doświadczyliśmy wielu różnych interesujących momentów. Dziś opowiem Wam, jak przy odpowiednim podejściu można wydobyć jeszcze więcej wydajności z 'waniliowego' Kubernetes dla Waszego klastra. Gotowi, startujemy!

Wszyscy doskonale wiecie, że Kubernetes to skalowalny system z otwartym kodem do orkiestracji kontenerami; no cóż, albo 5 binarek, które dokonują magii, zarządzając cyklem życia Waszych mikrousług w środowisku serwerowym. Ponadto to dość elastyczne narzędzie, które można składać jak klocki Lego, aby maksymalnie dostosować je do różnych zadań.

I wydaje się, że wszystko jest w porządku: wrzucaj serwery do klastra, jak drewno do pieca, i nie martw się o nic. Ale jeśli dbasz o ekologię, to zaczniesz się zastanawiać: 'Jak mogę podtrzymać ogień w piecu i jednocześnie oszczędzić las?'. Innymi słowy, jak znaleźć sposoby na poprawę infrastruktury i obniżenie kosztów.

1. Monitoruj zasoby zespołów i aplikacji

Dziewięć wskazówek dotyczących zwiększenia wydajności Kubernetes

Jedna z najprostszych, ale skutecznych metod to wprowadzenie requests/limits. Podziel aplikacje na namespace'y, a namespace'y na zespoły deweloperskie. Ustawiaj aplikacji przed wdrożeniem wartości dotyczące zużycia czasu procesora, pamięci, efemerycznego przechowywania.

resources:
   requests:
     memory: 2Gi
     cpu: 250m
   limits:
     memory: 4Gi
     cpu: 500m

Metodą prób i błędów doszliśmy do wniosku, że nie warto przeszacowywać requests w stosunku do limits więcej niż dwukrotnie. Objętość klastra oblicza się na podstawie requests, a jeśli zadasz aplikacjom różnicę w zasobach, na przykład 5-10 razy, to wyobraź sobie, co stanie się z Twoją węzłem, gdy zostanie zapełniona podami i nagle otrzyma obciążenie. Nic dobrego. Co najmniej throttling, a w najgorszym przypadku pożegnasz się z workerem i dostaniesz cykliczne obciążenie innych węzłów po tym, jak pody zaczną się przemieszczać.

Ponadto, za pomocą limitranges możesz na początku ustawić dla kontenera wartości dotyczące zasobów — minimalne, maksymalne i domyślne:

➜  ~ kubectl describe limitranges --namespace ops
Nazwa:       limit-range
Namespace:  ops
Typ         Zasób           Min   Max   Domyślny Zasób     Domyślny Limit  Max Limit/Request Ratio
----        --------         ---   ---   ----------------  -------------  -----------------------
Kontener   cpu              50m   10    100m             100m           2
Kontener   ephemeral-storage  12Mi  8Gi   128Mi            4Gi            -
Kontener   pamięć           64Mi  40Gi  128Mi            128Mi          2

Nie zapomnij ograniczyć zasobów namespace, aby jedna drużyna nie zabrała wszystkich zasobów klastra:

➜  ~ kubectl describe resourcequotas --namespace ops
Nazwa:                   resource-quota
Namespace:              ops
Zasób                   Używane        Maksymalne
--------                ----          ----
limits.cpu              77250m        80
limits.memory           124814367488  150Gi
pods                    31            45
requests.cpu            53850m        80
requests.memory         75613234944   150Gi
services                26            50
services.loadbalancers  0             0
services.nodeports      0             0

Jak widać na opisie resourcequotas, jeśli drużyna ops chciałaby wdrożyć pody, które będą zużywać dodatkowe 10 cpu, to scheduler nie pozwoli na to i zgłosi błąd:

Error creating: pods "nginx-proxy-9967d8d78-nh4fs" is forbidden: exceeded quota: resource-quota, requested: limits.cpu=5, requests.cpu=5, used: limits.cpu=77250m, requests.cpu=53850m, limited: limits.cpu=10, requests.cpu=10

Aby rozwiązać taki problem, można napisać narzędzie, na przykład takie jak ten, które potrafi przechowywać i commitować stan zasobów drużyn.

2. Wybierz optymalne przechowywanie plików

Dziewięć wskazówek dotyczących zwiększenia wydajności Kubernetes

Tutaj chciałbym poruszyć temat trwałych wolumenów i systemu dyskowego worker nod Kubernetes. Mam nadzieję, że nikt nie używa „Kuba” na HDD w produkcji, ale czasem nawet zwykły SSD już jest niewystarczający. Spotkaliśmy się z problemem, że logi zabiły dysk z powodu operacji wejścia/wyjścia, a tutaj opcji rozwiązania nie jest zbyt wiele:

  • Użycie wysokowydajnych SSD lub przejście na NVMe (jeśli sam zarządzasz swoim sprzętem).

  • Zmniejszenie poziomu logowania.

  • Wykonywanie „inteligentnej” balansacji podów, które obciążają dysk (podAntiAffinity).

Zrzut ekranu powyżej pokazuje, co dzieje się z dyskiem podczas działania nginx-ingress-controller, gdy włączone jest logowanie access_logs (~12 tys. logów/sekunę). Taki stan, oczywiście, może prowadzić do degradowania wszystkich aplikacji na tej nodzie.

Jeśli chodzi o PV, niestety nie wypróbowałem wszystkich rodzajów Woluminy o stałym charakterze. Wybierz najlepszą opcję, która odpowiada Twoim potrzebom. Historycznie niewielka część usług wymaga woluminów RWX, więc dawno temu zaczęto używać przestrzeni NFS do tego celu. Tanio i... wystarczy. Oczywiście mieliśmy z nim sporo problemów — nie ma co ukrywać, ale nauczyliśmy się go dostosowywać i już nas to nie boli. Jeśli to możliwe, przejdź na obiektowe przechowywanie S3.

3. Twórz zoptymalizowane obrazy

Dziewięć wskazówek dotyczących zwiększenia wydajności Kubernetes

Najlepiej jest używać zoptymalizowanych obrazów dla kontenerów, aby Kubernetes mógł szybciej je pobierać i efektywniej realizować ich zadania. 

Zoptymalizowanie oznacza, że obrazy:

  • zawierają tylko jedną aplikację lub realizują tylko jedną funkcję;

  • są małych rozmiarów, ponieważ większe obrazy gorzej przechodzą przez sieć;

  • mają punkty końcowe do sprawdzania gotowości i stanu, dzięki którym Kubernetes może podjąć działanie w przypadku awarii;

  • używają przyjaznych dla kontenerów systemów operacyjnych (jak Alpine czy CoreOS), które są bardziej odporne na błędy konfiguracji;

  • używają wieloetapowych kompilacji, aby wdrażać tylko skompilowane aplikacje, a nie związane z nimi źródła.

Istnieje wiele narzędzi i usług, które pozwalają sprawdzać i optymalizować obrazy na żywo. Ważne jest, aby zawsze je aktualizować oraz weryfikować pod kątem bezpieczeństwa. W rezultacie uzyskujesz:

  1. Zmniejszenie obciążenia sieci dla całego klastra.

  2. Skrócenie czasu uruchamiania kontenera.

  3. Mniejszy rozmiar całego rejestru Docker.

4. Użyj pamięci podręcznej DNS

Dziewięć wskazówek dotyczących zwiększenia wydajności Kubernetes

Mówiąc o dużych obciążeniach, bez dostrojenia systemu DNS klastra życie jest dość nieprzyjemne. Dawno temu programiści Kubernetes wspierali swoje rozwiązanie kube-dns. Zostało ono wdrożone u nas, ale ten program nie był szczególnie dostrajany i nie osiągał potrzebnej wydajności, mimo że zadanie wydawało się proste. Potem pojawił się coredns, na który przeszliśmy i nie mieliśmy problemów, później stał się domyślnym serwisem DNS w K8s. W pewnym momencie osiągnęliśmy 40 tys. rps do systemu DNS i to rozwiązanie również stało się niewystarczające. Na szczęście pojawił się Nodelocaldns, znany także jako lokalna pamięć podręczna, NodeLocal DNSCache.

Dlaczego to stosujemy? W jądrze systemu Linux występuje błąd, który przy wielokrotnym dostępie przez conntrack NAT w protokole UDP prowadzi do stanu wyścigu przy zapisywaniu do tabeli conntrack, co powoduje utratę części ruchu przez NAT (każde wezwanie przez Service to NAT). Nodelocaldns rozwiązuje ten problem, eliminując NAT i aktualizując połączenie do TCP z upstreamowymi DNS, a także lokalnie buforując zapytania DNS do upstreamów (w tym krótki 5-sekundowy negatywny cache).

5. Automatyczne poziome i pionowe skalowanie podów

Dziewięć wskazówek dotyczących zwiększenia wydajności Kubernetes

Czy możesz z pewnością powiedzieć, że wszystkie twoje mikrousługi są gotowe na dwukrotny lub trzykrotny wzrost obciążenia? Jak prawidłowo przydzielać zasoby swoim aplikacjom? Utrzymywanie kilku podów uruchomionych ponad robocze obciążenie może okazać się nadmiarowe, a zbyt ciasne skompresowanie może prowadzić do przestojów przy nagłym wzroście ruchu w serwisie. Złotą średnią pomagają osiągnąć takie usługi jak Horizontal Pod Autoscaler i Autoskaler pionowy podów.

VPA pozwala automatycznie zwiększać requests/limits twoich kontenerów w podzie w zależności od rzeczywistego użycia. Jak może być przydatne? Jeśli masz pady, które nie mogą być z jakiegoś powodu skalowane poziomo (co nie jest całkowicie niezawodne), to możesz spróbować powierzyć zmianę jego zasobów VPA. Jego cechą jest system rekomendacji oparty na danych historycznych i bieżących z metric-server, więc jeśli nie chcesz automatycznie zmieniać requests/limits, możesz po prostu śledzić zalecane zasoby dla swoich kontenerów i optymalizować ustawienia, aby zaoszczędzić CPU i pamięć w klastrze.

Dziewięć wskazówek dotyczących zwiększenia wydajności KubernetesObraz pochodzi z https://levelup.gitconnected.com/kubernetes-autoscaling-101-cluster-autoscaler-horizontal-pod-autoscaler-and-vertical-pod-2a441d9ad231

Planer w Kubernetes zawsze opiera się na requests. Jakąkolwiek wartość tam wprowadzisz, planer będzie szukał odpowiedniego węzła na jej podstawie. Wartości limits są potrzebne kubeletowi, aby zrozumieć, kiedy throttling lub zabijanie poda jest konieczne. Ponieważ jedynym ważnym parametrem jest wartość requests, VPA będzie pracować z tym. Za każdym razem, gdy ustalasz pionowe skalowanie aplikacji, określasz, jakie powinny być requests. A co z limits? Ten parametr również będzie proporcjonalnie skalowany.

Na przykład oto typowe ustawienia poda:

zasoby:
   żądania:
     pamięć: 250Mi
     cpu: 200m
   limity:
     pamięć: 500Mi
     cpu: 350m

Mechanizm rekomendacji określa, że Twoja aplikacja do prawidłowego działania potrzebuje 300m CPU i 500Mi. Otrzymasz takie ustawienia:

zasoby:
   żądania:
     pamięć: 500Mi
     cpu: 300m
   limity:
     pamięć: 1000Mi
     cpu: 525m

Jak wspomniano wcześniej, jest to proporcjonalne skalowanie na podstawie stosunku requests/limits w manifeście:

  • CPU: 200m → 300m: stosunek 1:1.75;

  • Pamięć: 250Mi → 500Mi: stosunek 1:2.

Jeśli chodzi o HPA, tutaj mechanizm działania jest bardziej przejrzysty. Ustala się progi wartości metryk, takich jak CPU i pamięć, a jeśli średnia wartość dla wszystkich replik przekracza próg, to aplikacja jest skalowana o +1 pod do momentu, gdy wartość znowu spadnie poniżej progu, lub do osiągnięcia maksymalnej liczby replik.

Dziewięć wskazówek dotyczących zwiększenia wydajności KubernetesObraz pochodzi z https://levelup.gitconnected.com/kubernetes-autoscaling-101-cluster-autoscaler-horizontal-pod-autoscaler-and-vertical-pod-2a441d9ad231

Oprócz standardowych metryk, takich jak CPU i pamięć, możesz ustawić progi na swoich niestandardowych metrykach z Prometheus i pracować z nimi, jeśli uważasz, że jest to najdokładniejsze określenie, kiedy należy skalować swoją aplikację. Po ustabilizowaniu się aplikacji poniżej zadanej wartości metryki, HPA zacznie skalować pody w dół do minimalnej liczby replik lub do momentu, gdy obciążenie będzie spełniać dany próg.

6. Nie zapominaj o Node Affinity i Pod Affinity

Dziewięć wskazówek dotyczących zwiększenia wydajności Kubernetes

Nie wszystkie węzły działają na takim samym sprzęcie, nie wszystkie pody potrzebują uruchamiać aplikacje wymagające intensywnych obliczeń. Kubernetes pozwala na określenie specjalizacji węzłów i podów za pomocą Node Affinity i Pod Affinity.

Jeżeli masz węzły odpowiednie do operacji z intensywnymi obliczeniami, najlepiej jest powiązać aplikacje z odpowiednimi węzłami. W tym celu użyj nodeSelector z etykietą węzła.

Załóżmy, że masz dwa węzły: jeden z CPUType=HIGHFREQ i dużą liczbą szybkich rdzeni, drugi z MemoryType=HIGHMEMORY dużą ilością pamięci i wyższej wydajności. Najprościej jest przypisać wdrożenie poda do węzła HIGHFREQ, dodając w sekcji spec taki selektor:

…
nodeSelector:
	CPUType: HIGHFREQ

Bardziej kosztowny i specyficzny sposób, aby to zrobić, to użycie nodeAffinity w polu affinity sekcji spec. Istnieją dwie opcje:

  • requiredDuringSchedulingIgnoredDuringExecution: sztywna konfiguracja (planista będzie wdrażał pody tylko na konkretnych węzłach (i nigdzie więcej));

  • preferredDuringSchedulingIgnoredDuringExecution: łagodna konfiguracja (planista spróbuje wdrożyć na konkretnych węzłach, a jeśli się nie uda, spróbuje wdrożyć na następnym dostępnym węźle).

Możesz zdefiniować określoną składnię do zarządzania etykietami węzłów, na przykład, In, NotIn, Exists, DoesNotExist, Gt lub Lt. Jednak pamiętaj, że złożone metody w długich listach etykiet zwolnią podejmowanie decyzji w krytycznych sytuacjach. Innymi słowy, nie komplikuj.

Jak wspomniano wcześniej, Kubernetes pozwala zdefiniować powiązania bieżących podów. To znaczy, że możesz sprawić, aby określone pody działały razem z innymi podami w tej samej strefie dostępności (dotyczy chmur) lub węzłach.

W podAffinity pola affinity sekcji spec są dostępne te same pola, co w przypadku nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution i preferredDuringSchedulingIgnoredDuringExecution. Jedyną różnicą jest to, że matchExpressions powiąże pody z węzłem, na którym już działa pod z taką etykietą.

Kubernetes oferuje również pole podAntiAffinity, które, przeciwnie, nie powiązuje poda z węzłem z określonymi podami.

Jeśli chodzi o wyrażenia nodeAffinity można dać tę samą radę: staraj się zachować prostotę i logiczność zasad, nie próbuj przeciążać specyfikacji podów złożonym zestawem zasad. Bardzo łatwo stworzyć zasadę, która nie będzie spełniać warunków klastra, co stworzy dodatkowe obciążenie dla planisty i obniży ogólną wydajność.

7. Taints & Tolerations

Jest jeszcze jeden sposób zarządzania planistą. Jeśli masz duży klaster z setkami węzłów i tysiącami mikrousług, bardzo trudno jest nie pozwalać określonym podom na umieszczanie się na określonych węzłach.

Pomaga w tym mechanizm taints — zasad zakazujących. Na przykład można w określonych scenariuszach zabronić określonym węzłom uruchamiać pody na sobie. Aby zastosować taint do konkretnego węzła, musisz użyć opcji taint w kubectl. Podaj klucz i wartość, a następnie taint na przykład NoSchedule lub NoExecute:

$ kubectl taint nodes node10 node-role.kubernetes.io/ingress=true:NoSchedule

Warto również zauważyć, że mechanizm taint obsługuje trzy główne efekty: NoSchedule, NoExecute i PreferNoSchedule.

  • NoSchedule oznacza, że dopóki w specyfikacji poda nie będzie odpowiedniego wpisu tolerations, nie będzie mógł zostać wdrożony na węźle (w tym przykładzie node10).

  • PreferNoSchedule — uproszczona wersja NoSchedule. W tym przypadku planista spróbuje nie rozdzielać podów, które nie mają odpowiedniego wpisu tolerations na węzeł, ale to nie jest sztywne ograniczenie. Jeśli w klastrze nie będzie zasobów, pody zaczną się wdrażać na tym węźle.

  • NoExecute — ten efekt powoduje natychmiastową ewakuację podów, które nie mają odpowiedniego wpisu tolerations.

Ciekawostką jest to, że takie zachowanie można cofnąć za pomocą mechanizmu tolerancji. Jest to przydatne, gdy istnieje "zabroniony" węzeł, a trzeba na nim umieścić tylko usługi infrastrukturalne. Jak to zrobić? Zezwolić tylko tym podom, dla których istnieje odpowiednia tolerancja.

Oto jak będzie wyglądać specyfikacja podu:

spec:
   tolerations:
     - key: "node-role.kubernetes.io/ingress"
        operator: "Equal"
        value: "true"
        effect: "NoSchedule"

To nie oznacza, że przy następnym redeploy'u pod trafi dokładnie na ten węzeł, to nie jest mechanizm Node Affinity i nodeSelector. Ale łącząc kilka funkcji, można osiągnąć bardzo elastyczne dostosowanie harmonogramu.

8. Skonfiguruj priorytet wdrażania podów

To, że skonfigurowałeś powiązanie podów z węzłami, nie oznacza, że wszystkie pody muszą być realizowane z takim samym priorytetem. Na przykład, możesz chcieć wdrażać niektóre pody wcześniej niż inne.

Kubernetes oferuje różne sposoby konfiguracji priorytetów podów (Pod Priority and Preemption). Konfiguracja składa się z kilku części: obiektu PriorityClass oraz opisu pola priorityClassName w specyfikacji podu. Rozważmy przykład:

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 99999
globalDefault: false
description: "Ta klasa priorytetowa powinna być używana tylko dla bardzo ważnych podów"

Tworzymy PriorityClass, nadając mu nazwę, opis i wartość. Im wyższy value, tym wyższy priorytet. Wartość może być dowolną 32-bitową liczbą całkowitą, mniejszą lub równą 1 000 000 000. Wyższe wartości są zarezerwowane dla krytycznych podów systemowych, które zazwyczaj nie mogą być wypychane. Wypieranie będzie się odbywać tylko wtedy, gdy wysokopriorytetowy pod nie będzie miał gdzie się wdrożyć, wtedy część podów z danego węzła zostanie ewakuowana. Jeśli ten mechanizm jest dla Ciebie zbyt rygorystyczny, możesz dodać opcję preemptionPolicy: Never, a wtedy wypierania nie będzie, pod ustawi się na pierwszym miejscu w kolejce i poczeka, aż harmonogram znajdzie dla niego wolne zasoby.

Następnie tworzymy pod, w którym wskazujemy nazwę priorityClassName:

apiVersion: v1
kind: Pod
metadata:
  name: static-web
  labels:
    role: myrole
 spec:
  containers:
    - name: web
      image: nginx
      ports:
        - name: web
          containerPort: 80
          protocol: TCP
  priorityClassName: high-priority
          

Można tworzyć dowolną liczbę klas priorytetów, chociaż zaleca się nie przesadzać (na przykład, ograniczyć się do niskiego, średniego i wysokiego priorytetu).

W ten sposób, w razie potrzeby, będziesz mógł zwiększyć efektywność wdrażania krytycznych usług, takich jak nginx-ingress-controller, coredns itp.

9. Optymalizuj klaster ETCD

Dziewięć wskazówek dotyczących zwiększenia wydajności Kubernetes

ETCD można nazwać mózgiem całego klastra. Ważne jest, aby utrzymać tę bazę danych na wysokim poziomie, ponieważ to od niej zależy prędkość operacji w „Kube”. Standardowym, a zarazem dobrym rozwiązaniem jest utrzymanie klastra ETCD na węzłach master, aby mieć minimalne opóźnienie do kube-apiserver. Jeśli nie jest to możliwe, umieszczaj ETCD tak blisko, jak to możliwe, mając dobrą przepustowość między uczestnikami. Zwracaj także uwagę na to, ile węzłów ETCD może wypaść bez szkody dla klastra.

Dziewięć wskazówek dotyczących zwiększenia wydajności Kubernetes

Pamiętaj, że nadmierne zwiększenie liczby uczestników w klastrze może poprawić odporność na awarie kosztem wydajności, wszystko powinno być w umiarze.

Jeśli chodzi o konfigurację usługi, to jest niewiele zaleceń:

  1. Mieć dobre podzespoły, w zależności od rozmiaru klastra (można poczytać) tutaj).

  2. Dostosować kilka parametrów, jeśli rozłożono klaster między paroma centrami danych lub Twoja sieć i dyski pozostawiają wiele do życzenia (można poczytać) tutaj).

Podsumowanie

W tym artykule opisano punkty, które nasz zespół stara się przestrzegać. Nie jest to krok po kroku opis działań, lecz opcje, które mogą się przydać do optymalizacji kosztów związanych z klastrem. Jasne jest, że każdy klaster jest unikalny, a rozwiązania do konfiguracji mogą się znacznie różnić, dlatego byłoby interesujące uzyskać od Ciebie informacje zwrotne: jak monitorujesz swój klaster Kubernetes, czym poprawiasz jego działanie. Dziel się swoim doświadczeniem w komentarzach, chętnie je poznamy.

Ź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