
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

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: 500mMetodą 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 2Nie 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 0Jak 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=10Aby rozwiązać taki problem, można napisać narzędzie, na przykład takie jak , które potrafi przechowywać i commitować stan zasobów drużyn.
2. Wybierz optymalne przechowywanie plików

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 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

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:
Zmniejszenie obciążenia sieci dla całego klastra.
Skrócenie czasu uruchamiania kontenera.
Mniejszy rozmiar całego rejestru Docker.
4. Użyj pamięci podręcznej DNS

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, .
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

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 i .
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.
Obraz 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: 350mMechanizm 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: 525mJak 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.
Obraz 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

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: HIGHFREQBardziej 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:NoScheduleWarto również zauważyć, że mechanizm taint obsługuje trzy główne efekty: NoSchedule, NoExecute i PreferNoSchedule.
NoScheduleoznacza, że dopóki w specyfikacji poda nie będzie odpowiedniego wpisutolerations, nie będzie mógł zostać wdrożony na węźle (w tym przykładzienode10).PreferNoSchedule— uproszczona wersjaNoSchedule. W tym przypadku planista spróbuje nie rozdzielać podów, które nie mają odpowiedniego wpisutolerationsna 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 wpisutolerations.
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

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.

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ń:
Mieć dobre podzespoły, w zależności od rozmiaru klastra (można poczytać) ).
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ć) ).
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
