Limity CPU i agresywny throttling w Kubernetes

Przyp. tłum.: ta pouczająca historia Omio — europejskiego agregatora podróży — prowadzi czytelników od podstawowej teorii do fascynujących praktycznych szczegółów w konfiguracji Kubernetes. Zapoznanie się z takimi przypadkami pomaga nie tylko poszerzyć horyzonty, ale także zapobiegać niecodziennym problemom.

Limity CPU i agresywny throttling w Kubernetes

Czy zdarzyło ci się, że aplikacja "zatrzymywała się" w miejscu, przestawała odpowiadać na zapytania o stan (health check'i) i nie mogłeś zrozumieć powodu takiego zachowania? Jednym z możliwych wyjaśnień jest limit kwot na zasoby CPU. O tym będzie mowa w tym artykule.

TL;DR:
Zdecydowanie zalecamy rezygnację z limitów CPU w Kubernetes (lub wyłączenie kwot CFS w Kubelet), jeśli używana jest wersja jądra Linux z błędem w kwotach CFS. W jądrze jest jest poważny i dobrze znany błąd, który prowadzi do nadmiernego throttlingu i opóźnień.
.

W Omio cała infrastruktura jest zarządzana przez Kubernetes.Wszystkie nasze stateful- i stateless-obciążenia działają wyłącznie na Kubernetes (używamy Google Kubernetes Engine). W ciągu ostatnich sześciu miesięcy zaczęliśmy obserwować losowe przycinanie. Aplikacje zawieszają się lub przestają odpowiadać na health check'i, tracą łączność z siecią itd. Takie zachowanie długo nas frustrowało, aż w końcu postanowiliśmy poważnie zająć się problemem.

Streszczenie artykułu:

  • Kilka słów o kontenerach i Kubernetes;
  • Jak zrealizowane są żądania i limity CPU;
  • Jak działa limit CPU w środowiskach z wieloma rdzeniami;
  • Jak śledzić throttling CPU;
  • Rozwiązanie problemu i jego niuanse.

Kilka słów o kontenerach i Kubernetes

Kubernetes jest w zasadzie współczesnym standardem w świecie infrastruktury. Jego głównym zadaniem jest orkiestracja kontenerów.

Kontenery

W przeszłości musieliśmy tworzyć artefakty takie jak Java JAR'y/WAR'y, Python Egg'i lub pliki wykonywalne do późniejszego uruchomienia na serwerach. Jednak, aby sprawić, że działały, trzeba było wykonać dodatkową pracę: zainstalować środowisko uruchomieniowe (Java/Python), umieścić niezbędne pliki w odpowiednich miejscach, zapewnić zgodność z konkretną wersją systemu operacyjnego itd. Innymi słowy, trzeba było zwracać szczególną uwagę na zarządzanie konfiguracjami (co często było przyczyną konfliktów między programistami a administratorami systemów).

Kontenery wszystko zmieniły. Obecnie artefaktem jest obraz kontenera. Można go przedstawić jako rozszerzony plik wykonywalny, zawierający nie tylko program, ale także pełne środowisko uruchomieniowe (Java/Python/…) oraz niezbędne pliki/pakiety, wstępnie zainstalowane i gotowe do uruchomienia. Kontenery można wdrażać i uruchamiać na różnych serwerach bez dodatkowych działań.

Co więcej, kontenery działają w swoim własnym piaskownicowym środowisku. Mają swój własny wirtualny adapter sieciowy, system plików z ograniczonym dostępem, swoją hierarchię procesów, swoje ograniczenia na CPU i pamięć itd. Wszystko to realizowane jest dzięki specjalnemu podsystemowi jądra Linux - namespaces (przestrzenie nazw).

Kubernetes

Jak już wspomniano, Kubernetes to orkiestrator kontenerów. Działa to w następujący sposób: dostarczasz mu pulę maszyn, a następnie mówisz: "Hej, Kubernetes, uruchom dziesięć instancji mojego kontenera z 2 procesorami i 3 GB pamięci na każdą, i utrzymuj je w działaniu!". Kubernetes zajmie się resztą. Znajdzie wolne zasoby, uruchomi kontenery i będzie je restartować w razie potrzeby, zaktualizuje je przy zmianie wersji itd. W zasadzie Kubernetes pozwala abstrahować od sprzętu i sprawia, że różnorodność systemów jest gotowa do wdrażania i działania aplikacji.

Limity CPU i agresywny throttling w Kubernetes
Kubernetes z punktu widzenia zwykłego użytkownika

Co to są requesty i limity w Kubernetes

Ok, zrozumieliśmy kontenery i Kubernetes. Wiemy również, że kilka kontenerów może znajdować się na jednej maszynie.

Można przeprowadzić analogię do mieszkania komunalnego. Bierze się przestronny lokal (maszyny/węzły) i wynajmuje go kilku lokatorom (kontenerom). Kubernetes pełni rolę pośrednika. Pojawia się pytanie, jak powstrzymać lokatorów przed konfliktami? Co jeśli jeden z nich, powiedzmy, zdecyduje się zająć łazienkę na pół dnia?

Właśnie tutaj w grę wchodzą requesty i limity. CPU Request jest potrzebny wyłącznie do planowania. To coś w rodzaju „listy życzeń” kontenera i jest używane do dobierania najbardziej odpowiedniego węzła. Równocześnie CPU Limit można porównać z umową najmu - gdy już dobierzemy węzeł dla kontenera, to nie może przekroczyć ustalonych limitów. I tutaj pojawia się problem…

Jak są realizowane requesty i limity w Kubernetes

Kubernetes wykorzystuje wbudowany w jądro mechanizm throttlingu (opóźnienia) do realizacji limitów CPU. Jeśli aplikacja przekracza limit, włącza się throttling (tzn. otrzymuje mniej cykli CPU). Requesty i limity dla pamięci są zorganizowane inaczej, więc łatwiej je wykryć. Wystarczy sprawdzić ostatni status restartu poda: czy nie jest on "OOMKilled". Z throttlingiem CPU sprawy nie są tak proste, ponieważ K8s udostępnia jedynie metryki użytkowania, a nie cgroups.

CPU Request

Limity CPU i agresywny throttling w Kubernetes
Jak jest realizowany CPU request

Dla uproszczenia rozważmy proces na przykładzie maszyny z 4-rdzeniowym CPU.

K8s używa mechanizmu kontrolnych grup (cgroups) do zarządzania przydziałem zasobów (pamięci i CPU). Dla niego dostępny jest hierarchiczny model: potomstwo dziedziczy limity grupy rodzica. Szczegóły przydziału są przechowywane w wirtualnym systemie plików (/sys/fs/cgroup). W przypadku procesora to /sys/fs/cgroup/cpu,cpuacct/*.

K8s używa pliku cpu.share do przydziału zasobów CPU. W naszym przypadku główna kontrolna grupa otrzymuje 4096 udziałów zasobów CPU — 100% dostępnej mocy procesora (1 rdzeń = 1024; to wartość stała). Główna grupa przydziela zasoby proporcjonalnie w zależności od udziałów potomków, zapisanych w cpu.share, a te z kolei postępują analogicznie z własnymi potomkami itd. W typowym węźle Kubernetes główna kontrolna grupa ma trzech potomków: system.slice, user.slice i kubepods. Dwie pierwsze podgrupy są używane do przydziału zasobów między krytycznymi obciążeniami systemowymi a aplikacjami użytkowników spoza K8s. Ostatnia — kubepods — jest tworzona przez Kubernetes do przydziału zasobów między podami.

Na schemacie powyżej widać, że pierwsza i druga podgrupa otrzymały po 1024 udziale, przy czym podgrupie kubepod przydzielono 4096 udziałów. Jak to możliwe: przecież głównej grupie dostępne są tylko 4096 udziały, a suma udziałów jej potomków znacznie przekracza tę liczbę (6144)? Chodzi o to, że wartość ta ma logiczny sens, dlatego planista Linuxa (CFS) używa jej do proporcjonalnego przydziału zasobów CPU. W naszym przypadku pierwsze dwie grupy otrzymują po 680 rzeczywistych udziałów (16,6% z 4096), a kubepod otrzymuje pozostałe 2736 udziały. W przypadku bezczynności pierwsze dwie grupy nie będą używać przydzielonych zasobów.

Na szczęście w planie umieszczono mechanizm, który pozwala uniknąć straty nieużywanych zasobów CPU. Przekazuje on "bezczynne" moce do globalnej puli, z której są one rozdzielane grupom, które potrzebują dodatkowych mocy procesora (przekaz odbywa się partiami, aby uniknąć strat związanych z zaokrąglaniem). Podobna metoda jest stosowana także w przypadku wszystkich potomków potomków.

Ten mechanizm zapewnia sprawiedliwy podział mocy procesora i dba o to, aby żaden proces nie "kradł" zasobów od innych.

Limit CPU

Chociaż konfiguracje limitów i żądań w K8s wyglądają podobnie, ich realizacja jest zasadniczo różna: to najbardziej myląca i najmniej udokumentowana część.

K8s wykorzystuje mechanizm kwot CFS do realizacji limitów. Ich ustawienia określane są w plikach cfs_period_us i cfs_quota_us w katalogu cgroup (tam również znajdują się pliki cpu.share).

W przeciwieństwie do cpu.share, kwota opiera się na okresie czasu, a nie na dostępnej mocy procesora. cfs_period_us określa długość okresu (epoki) — zawsze 100000 μs (100 ms). W K8s istnieje możliwość zmiany tej wartości, jednak obecnie jest ona dostępna tylko w wersji alfa. Planista wykorzystuje epokę do ponownego uruchamiania wykorzystanych kwot. Drugi plik, cfs_quota_us, określa dostępny czas (kwotę) w każdej epoce. Zauważ, że jest ona również podana w mikrosekundach. Kwota może przekraczać długość epoki; innymi słowy, może być większa niż 100 ms.

Przyjrzyjmy się dwóm scenariuszom na maszynach z 16 rdzeniami (najpopularniejszy typ komputerów u nas w Omio):

Limity CPU i agresywny throttling w Kubernetes
Scenariusz 1: 2 wątki i limit 200 ms. Bez throttlingu

Limity CPU i agresywny throttling w Kubernetes
Scenariusz 2: 10 wątków i limit 200 ms. Throttling zaczyna się po 20 ms, dostęp do zasobów CPU zostaje wznowiony po dodatkowych 80 ms

Załóżmy, że ustawiliście limit CPU na 2 rdzenie; Kubernetes przetłumaczy tę wartość na 200 ms. Oznacza to, że kontener może wykorzystać maksymalnie 200 ms czasu procesora bez throttlingu.

I tu zaczyna się najciekawsze. Jak wspomniano powyżej, dostępna kwota wynosi 200 ms. Jeśli równocześnie działa dziesięć wątków na maszynie z 12 rdzeniami (patrz ilustracja do scenariusza 2), podczas gdy wszystkie inne pod’y będą w bezczynności, kwota zostanie wyczerpana zaledwie po 20 ms (ponieważ 10 * 20 ms = 200 ms), a wszystkie wątki tego pod’a "utkną" (throttle) na następne 80 ms. Sytuację pogarsza już wspomniany błąd planera, przez który występuje nadmierne throttling i kontener nie może wykorzystać nawet dostępnej kwoty.

Jak ocenić throttling w pod’ach?

Wystarczy wejść do poda i wykonać cat /sys/fs/cgroup/cpu/cpu.stat.

  • nr_periods — całkowita liczba okresów planera;
  • nr_throttled — liczba okresów throttlingu w składzie nr_periods;
  • throttled_time — łączny czas throttlingu w nanosekundach.

Limity CPU i agresywny throttling w Kubernetes

Co tak naprawdę się dzieje?

W rezultacie uzyskujemy wysoki throttling w wszystkich aplikacjach. Czasami jest on półtora raza silniejszy niż przewidywane!

Prowadzi to do różnych błędów — awarii kontroli gotowości (readiness), zawieszania się kontenerów, przerywania połączeń sieciowych, timeoutów w wywołaniach serwisowych. Ostatecznie objawia się to zwiększoną latencją i wzrostem liczby błędów.

Rozwiązanie i konsekwencje

Tu wszystko jest proste. Zrezygnowaliśmy z limitów CPU i zajęliśmy się aktualizacją jądra systemu operacyjnego w klastrach do najnowszej wersji, w której błąd został naprawiony. Liczba błędów (HTTP 5xx) w naszych usługach natychmiast znacznie spadła:

Błędy HTTP 5xx

Limity CPU i agresywny throttling w Kubernetes
Błędy HTTP 5xx jednego krytycznie ważnego serwisu

Czas odpowiedzi p95

Limity CPU i agresywny throttling w Kubernetes
Opóźnienie zapytań krytycznie ważnego serwisu, 95. percentyl

Koszty eksploatacji

Limity CPU i agresywny throttling w Kubernetes
Liczba wydanych godzin egzemplarzowych

Gdzie jest haczyk?

Jak wspomniano na początku artykułu:

Można porównać to do mieszkania komunalnego… Kubernetes działa jako agent nieruchomości. Ale jak powstrzymać najemców przed konfliktami między sobą? Co jeśli jeden z nich, powiedzmy, postanowi zająć łazienkę na pół dnia?

Oto w czym tkwi haczyk. Jeden nieodpowiedzialny kontener może wchłonąć wszystkie dostępne zasoby procesora w maszynie. Jeśli masz solidny stos aplikacji (na przykład odpowiednio skonfigurowane JVM, Go, Node VM), to nie jest problem: można pracować w takich warunkach przez długi czas. Ale jeśli aplikacje są źle zoptymalizowane lub w ogóle nie są zoptymalizowane (FROM java:latest), sytuacja może wymknąć się spod kontroli. W Omio mamy zautomatyzowane podstawowe pliki Dockerfile z odpowiednimi ustawieniami domyślnymi dla głównych języków, więc taki problem nie wystąpił.

Zalecamy monitorowanie metryk UŻYJ (użycie, nasycenie i błędy), opóźnienia API i częstość występowania błędów. Upewnij się, że wyniki spełniają oczekiwania.

Linki

Taka jest nasza historia. Następujące materiały bardzo pomogły zrozumieć, co się dzieje:

Raporty o błędach Kubernetes:

Czy napotkałeś podobne problemy w swojej praktyce lub masz doświadczenie związane z ograniczaniem w konteneryzowanych środowiskach produkcyjnych? Podziel się swoją historią w komentarzach!

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