ProHoster > Blog > administracja > Tworzenie dodatkowego harmonogramu kube z niestandardowym zestawem reguł planowania
Tworzenie dodatkowego harmonogramu kube z niestandardowym zestawem reguł planowania
Kube-scheduler to integralny komponent Kubernetesa, który odpowiada za planowanie podów pomiędzy węzłami zgodnie z określonymi politykami. Często podczas działania klastra Kubernetes nie musimy się zastanawiać, jakie polityki służą do planowania podów, gdyż zestaw polityk domyślnego kube-scheduler nadaje się do większości codziennych zadań. Są jednak sytuacje, gdy ważne jest dla nas dopracowanie procesu przydzielania podów i są na to dwa sposoby, aby zrealizować to zadanie:
Utwórz harmonogram kube z niestandardowym zestawem reguł
Napisz własny harmonogram i naucz go obsługi żądań serwera API
W tym artykule opiszę realizację punktu pierwszego, aby rozwiązać problem nierównomiernego rozmieszczenia palenisk na jednym z naszych projektów.
Krótkie wprowadzenie do działania kube-scheduler
Warto szczególnie zwrócić uwagę na fakt, że kube-scheduler nie odpowiada za bezpośrednie planowanie podów - odpowiada jedynie za określenie węzła, na którym zostanie umieszczony pod. Inaczej mówiąc, efektem pracy kube-scheduler jest nazwa węzła, który zwraca do serwera API w celu wysłania żądania harmonogramu i na tym kończy się jego praca.
Najpierw kube-scheduler kompiluje listę węzłów, na których można zaplanować pod zgodnie z polityką predykatów. Następnie każdy węzeł z tej listy otrzymuje określoną liczbę punktów zgodnie z polityką priorytetów. W rezultacie wybierany jest węzeł z maksymalną liczbą punktów. Jeśli istnieją węzły, które mają ten sam maksymalny wynik, wybierany jest losowy. Listę i opis zasad predykatów (filtrowania) i priorytetów (punktowania) można znaleźć w dokumentacja.
Opis treści problemu
Pomimo dużej liczby różnych klastrów Kubernetes utrzymywanych w Nixys, po raz pierwszy zetknęliśmy się z problemem planowania podów dopiero niedawno, gdy jeden z naszych projektów wymagał uruchomienia dużej liczby zadań okresowych (~100 jednostek CronJob). Aby maksymalnie uprościć opis problemu, jako przykład weźmiemy mikroserwis, w ramach którego raz na minutę uruchamiane jest zadanie cron, powodując pewne obciążenie procesora. Do uruchomienia zadania cron przydzielono trzy węzły o absolutnie identycznych charakterystykach (po 24 vCPU na każdym).
Jednocześnie nie można dokładnie określić, ile czasu zajmie wykonanie CronJob, ponieważ ilość danych wejściowych stale się zmienia. Średnio podczas normalnej pracy kube-scheduler każdy węzeł uruchamia 3-4 instancje zadań, które powodują ~20-30% obciążenia procesora każdego węzła:
Problem sam w sobie polega na tym, że czasami pody zadań cron przestały być planowane w jednym z trzech węzłów. Oznacza to, że w pewnym momencie dla jednego z węzłów nie zaplanowano ani jednego poda, podczas gdy na pozostałych dwóch węzłach działało 6-8 kopii zadania, tworząc ~40-60% obciążenia procesora:
Problem powtarzał się z całkowicie losową częstotliwością i czasami był powiązany z momentem wdrożenia nowej wersji kodu.
Zwiększając poziom rejestrowania kube-scheduler do poziomu 10 (-v=10), zaczęliśmy rejestrować, ile punktów zdobył każdy węzeł w procesie oceny. Podczas normalnego planowania w dziennikach mogą być widoczne następujące informacje:
Te. sądząc po informacjach uzyskanych z logów, każdy z węzłów uzyskał jednakową liczbę punktów końcowych, a do planowania wybrano losowo jeden z węzłów. W momencie problematycznego planowania dzienniki wyglądały tak:
Z czego wynika, że jeden z węzłów uzyskał mniej punktów końcowych niż pozostałe, dlatego planowanie przeprowadzono tylko dla dwóch węzłów, które uzyskały maksymalną liczbę punktów. Tym samym byliśmy zdecydowanie przekonani, że problem leży właśnie w harmonogramowaniu strąków.
Dalszy algorytm rozwiązania problemu był dla nas oczywisty - przeanalizuj logi, dowiedz się, jakim priorytetem węzeł nie zdobył punktów i, jeśli to konieczne, dostosuj zasady domyślnego harmonogramu kube. Natrafiamy tu jednak na dwie istotne trudności:
Na maksymalnym poziomie logowania (10) uwzględniane są punkty zdobyte tylko za niektóre priorytety. Z powyższego fragmentu logów widać, że dla wszystkich priorytetów odzwierciedlonych w logach węzły zdobywają tę samą liczbę punktów w harmonogramowaniu normalnym i problemowym, ale ostateczny wynik w przypadku planowania problemów jest inny. Można zatem stwierdzić, że dla niektórych priorytetów punktacja odbywa się „za kulisami” i nie mamy możliwości zrozumienia, za jaki priorytet węzeł nie otrzymał punktów. Szczegółowo opisaliśmy ten problem w problem Repozytorium Kubernetes na Githubie. W chwili pisania tego tekstu otrzymano odpowiedź od programistów, że obsługa rejestrowania zostanie dodana w aktualizacjach Kubernetes v1.15,1.16, 1.17 i XNUMX.
Nie ma łatwego sposobu, aby zrozumieć, z jakim konkretnym zestawem zasad aktualnie pracuje kube-scheduler. Tak w dokumentacja lista ta znajduje się na liście, ale nie zawiera informacji o tym, jakie konkretne wagi są przypisane do poszczególnych polityk priorytetowych. Możesz zobaczyć wagi lub edytować zasady domyślnego programu kube-scheduler tylko w kody źródłowe.
Warto zaznaczyć, że raz udało nam się odnotować, że węzeł nie otrzymał punktów zgodnie z polityką ImageLocalityPriority, która przyznaje punkty węzłowi, jeśli posiada już obraz niezbędny do uruchomienia aplikacji. Oznacza to, że w momencie wdrożenia nowej wersji aplikacji zadanie cron udało się uruchomić na dwóch węzłach, pobierając do nich nowy obraz z rejestru dokerów, dzięki czemu dwa węzły uzyskały wyższą ocenę końcową w stosunku do trzeciego .
Tak jak pisałem wyżej w logach nie widzimy informacji o ocenie polityki ImageLocalityPriority, dlatego w celu sprawdzenia naszego założenia zrzuciliśmy obraz z nową wersją aplikacji na trzeci węzeł, po czym harmonogram zadziałał poprawnie . To właśnie ze względu na politykę ImageLocalityPriority problem z harmonogramem był obserwowany dość rzadko, częściej był on powiązany z czymś innym. W związku z tym, że nie mogliśmy w pełni zdebugować każdej z polityk znajdujących się na liście priorytetów domyślnego kube-scheduler, pojawiła się potrzeba elastycznego zarządzania politykami planowania podów.
Stwierdzenie problemu
Chcieliśmy, aby rozwiązanie problemu było jak najbardziej konkretne, czyli główne byty Kubernetesa (tutaj mamy na myśli domyślny kube-scheduler) powinny pozostać niezmienione. Nie chcieliśmy rozwiązać problemu w jednym miejscu i stworzyć go w innym. W ten sposób doszliśmy do dwóch możliwości rozwiązania problemu, które zostały ogłoszone we wstępie do artykułu - utworzenie dodatkowego harmonogramu lub napisanie własnego. Głównym wymogiem planowania zadań cron jest równomierne rozłożenie obciążenia na trzy węzły. Wymaganie to można spełnić za pomocą istniejących polityk kube-scheduler, więc aby rozwiązać nasz problem, nie ma sensu pisać własnego harmonogramu.
Instrukcje dotyczące tworzenia i wdrażania dodatkowego harmonogramu kube są opisane w dokumentacja. Wydawało nam się jednak, że encja Deployment nie wystarczy, aby zapewnić odporność na awarie w działaniu tak krytycznej usługi jak kube-scheduler, dlatego postanowiliśmy wdrożyć nowy kube-scheduler jako Static Pod, który będzie bezpośrednio monitorowany przez Kubelet. Zatem mamy następujące wymagania dla nowego programu kube-scheduler:
Usługę należy wdrożyć jako moduł statyczny na wszystkich wzorcach klastra
Należy zapewnić tolerancję na błędy w przypadku, gdy aktywny pod z programem kube-scheduler jest niedostępny
Głównym priorytetem przy planowaniu powinna być liczba dostępnych zasobów w węźle (LeastRequestedPriority)
Rozwiązania wdrożeniowe
Warto od razu zaznaczyć, że całość prac będziemy wykonywać w Kubernetesie v1.14.7, gdyż To jest wersja, która została wykorzystana w projekcie. Zacznijmy od napisania manifestu dla naszego nowego kube-scheduler. Weźmy jako podstawę domyślny manifest (/etc/kubernetes/manifests/kube-scheduler.yaml) i sprowadźmy go do następującej postaci:
Zmieniono nazwę kapsuły i kontenera na kube-scheduler-cron
Określono użycie portów 10151 i 10159 jako zdefiniowaną opcję hostNetwork: true i nie możemy używać tych samych portów, co domyślny program kube-scheduler (10251 i 10259)
Za pomocą parametru --config określiliśmy plik konfiguracyjny, z którym należy uruchomić usługę
Skonfigurowane montowanie pliku konfiguracyjnego (scheduler-custom.conf) i pliku zasad planowania (scheduler-custom-policy-config.json) z hosta
Nie zapominaj, że nasz kube-scheduler będzie potrzebował uprawnień podobnych do domyślnych. Edytuj rolę klastra:
Porozmawiajmy teraz o tym, co powinno znajdować się w pliku konfiguracyjnym i pliku zasad planowania:
Plik konfiguracyjny (scheduler-custom.conf)
Aby uzyskać domyślną konfigurację kube-scheduler, należy użyć parametru --write-config-to z dokumentacja. Powstałą konfigurację umieścimy w pliku /etc/kubernetes/scheduler-custom.conf i sprowadzimy do poniższej postaci:
UstawiamyharmonogramName na nazwę naszej usługi kube-scheduler-cron.
W parametrze lockObjectName musisz także ustawić nazwę naszej usługi i upewnić się, że parametr leaderElect ustaw na true (jeśli masz jeden węzeł główny, możesz ustawić go na false).
Określono ścieżkę do pliku z opisem zasad planowania w parametrze algorithmSource.
Warto przyjrzeć się bliżej drugiemu punktowi, w którym edytujemy parametry klucza leaderElection. Aby zapewnić odporność na błędy, włączyliśmy (leaderElect) proces wybierania lidera (mastera) pomiędzy podami naszego kube-scheduler przy użyciu dla nich jednego punktu końcowego (resourceLock) o nazwie kube-scheduler-cron (lockObjectName) w przestrzeni nazw kube-system (lockObjectNamespace). Jak Kubernetes zapewnia wysoką dostępność głównych komponentów (w tym kube-scheduler) można znaleźć w Artykuł.
Plik zasad planowania (scheduler-custom-policy-config.json)
Jak pisałem wcześniej, z jakimi konkretnymi politykami współpracuje domyślny kube-scheduler, możemy dowiedzieć się jedynie analizując jego kod. Oznacza to, że nie możemy uzyskać pliku z zasadami planowania dla domyślnego kube-scheduler w taki sam sposób, jak plik konfiguracyjny. Opiszmy interesujące nas polityki planowania w pliku /etc/kubernetes/scheduler-custom-policy-config.json w następujący sposób:
Zatem kube-scheduler najpierw kompiluje listę węzłów, do których można zaplanować pod zgodnie z polityką GeneralPredicates (która obejmuje zestaw zasad PodFitsResources, PodFitsHostPorts, HostName i MatchNodeSelector). Następnie każdy węzeł jest oceniany zgodnie z zestawem zasad w tablicy priorytetów. Aby spełnić warunki naszego zadania, uznaliśmy, że taki zestaw polityk będzie optymalnym rozwiązaniem. Przypominam, że zbiór polis wraz z ich szczegółowym opisem dostępny jest w dokumentacja. Aby zrealizować swoje zadanie, możesz po prostu zmienić zestaw stosowanych polityk i przypisać im odpowiednie wagi.
Nazwijmy manifest nowego kube-scheduler, który stworzyliśmy na początku rozdziału, kube-scheduler-custom.yaml i umieść go w następującej ścieżce /etc/kubernetes/manifests na trzech głównych węzłach. Jeśli wszystko zostanie wykonane poprawnie, Kubelet uruchomi poda na każdym węźle, a w logach naszego nowego kube-scheduler zobaczymy informację, że nasz plik polityki został pomyślnie zastosowany:
Creating scheduler from configuration: {{ } [{GeneralPredicates <nil>}] [{ServiceSpreadingPriority 1 <nil>} {EqualPriority 1 <nil>} {LeastRequestedPriority 1 <nil>} {NodePreferAvoidPodsPriority 10000 <nil>} {NodeAffinityPriority 1 <nil>}] [] 10 false}
Registering predicate: GeneralPredicates
Predicate type GeneralPredicates already registered, reusing.
Registering priority: ServiceSpreadingPriority
Priority type ServiceSpreadingPriority already registered, reusing.
Registering priority: EqualPriority
Priority type EqualPriority already registered, reusing.
Registering priority: LeastRequestedPriority
Priority type LeastRequestedPriority already registered, reusing.
Registering priority: NodePreferAvoidPodsPriority
Priority type NodePreferAvoidPodsPriority already registered, reusing.
Registering priority: NodeAffinityPriority
Priority type NodeAffinityPriority already registered, reusing.
Creating scheduler with fit predicates 'map[GeneralPredicates:{}]' and priority functions 'map[EqualPriority:{} LeastRequestedPriority:{} NodeAffinityPriority:{} NodePreferAvoidPodsPriority:{} ServiceSpreadingPriority:{}]'
Teraz pozostaje tylko wskazać w specyfikacji naszego CronJob, że wszystkie żądania dotyczące planowania jego podów powinny być przetwarzane przez nasz nowy kube-scheduler:
Docelowo otrzymaliśmy dodatkowy kube-scheduler z unikalnym zestawem polityk planowania, którego działanie jest monitorowane bezpośrednio przez kubelet. Dodatkowo ustawiliśmy wybór nowego lidera pomiędzy podami naszego kube-scheduler na wypadek, gdyby stary lider z jakiegoś powodu stał się niedostępny.
Regularne aplikacje i usługi są nadal planowane za pomocą domyślnego programu kube-scheduler, a wszystkie zadania cron zostały całkowicie przeniesione do nowego. Obciążenie utworzone przez zadania cron jest teraz równomiernie rozłożone na wszystkie węzły. Biorąc pod uwagę, że większość zadań cron jest wykonywana w tych samych węzłach, co główne aplikacje projektu, znacznie zmniejszyło to ryzyko przenoszenia podów z powodu braku zasobów. Po wprowadzeniu dodatkowego kube-scheduler nie pojawiały się już problemy z nierównym planowaniem zadań cron.