CPU-Grenzen und aggressives Throttling in Kubernetes

Anmerkung des Übersetzers.: Diese lehrreiche Geschichte von Omio – dem europĂ€ischen Reiseaggregator – fĂŒhrt die Leser von den grundlegenden Theorien zu spannenden praktischen Feinheiten in der Kubernetes-Konfiguration. Das Kennenlernen solcher FĂ€lle hilft nicht nur, den Horizont zu erweitern, sondern auch, nicht triviale Probleme zu vermeiden.

CPU-Grenzen und aggressives Throttling in Kubernetes

Haben Sie schon einmal erlebt, dass eine Anwendung "eingefroren" war, nicht mehr auf Statusanfragen (health checks) reagierte und Sie nicht die Ursache dieses Verhaltens verstehen konnten? Eine mögliche ErklÀrung hÀngt mit dem Limit der CPU-Ressourcenquoten zusammen. Darum geht es in diesem Artikel.

TL;DR:
Wir empfehlen dringend, auf CPU-Limits in Kubernetes zu verzichten (oder CFS-Quoten im Kubelet zu deaktivieren), wenn eine Linux-Kernelversion mit einem CFS-Quota-Bug verwendet wird. Im Kernel gibt es gibt es einen schwerwiegenden und wohlbekannten Bug, der zu ĂŒbermĂ€ĂŸigem Drosseln und Verzögerungen fĂŒhrt.
.

Bei Omio wird die gesamte Infrastruktur von Kubernetes verwaltet.Alle unsere stateful und stateless Workloads laufen ausschließlich auf Kubernetes (wir verwenden Google Kubernetes Engine). In den letzten sechs Monaten haben wir zufĂ€llige Leistungsprobleme beobachtet. Anwendungen frieren ein oder hören auf, auf health checks zu reagieren, verlieren die Netzwerkverbindung usw. Dieses Verhalten hat uns lange Zeit ratlos gemacht, und schließlich haben wir uns entschieden, das Problem intensiv anzugehen.

Zusammenfassung des Artikels:

  • Einige Worte ĂŒber Container und Kubernetes;
  • Wie CPU-Requires und Limits implementiert sind;
  • Wie CPU-Limits in Umgebungen mit mehreren Kernen wirken;
  • Wie man CPU-Drosselung ĂŒberwacht;
  • Lösungen fĂŒr das Problem und Tipps.

Einige Worte ĂŒber Container und Kubernetes

Kubernetes ist im Wesentlichen der moderne Standard in der Welt der Infrastruktur. Seine Hauptaufgabe ist die Orchestrierung von Containern.

Container

FrĂŒher mussten wir Artefakte wie Java JARs/WARs, Python Eggs oder ausfĂŒhrbare Dateien erstellen, um diese spĂ€ter auf Servern auszufĂŒhren. Um sie jedoch zum Funktionieren zu bringen, mussten wir zusĂ€tzliche Arbeit leisten: Laufzeitumgebungen (Java/Python) installieren, die erforderlichen Dateien an den richtigen Stellen platzieren und die KompatibilitĂ€t mit einer bestimmten Version des Betriebssystems sicherstellen. Mit anderen Worten, man musste der Konfigurationsverwaltung große Aufmerksamkeit schenken (was hĂ€ufig zu Konflikten zwischen Entwicklern und Systemadministratoren fĂŒhrte).

Container haben alles verĂ€ndert. Jetzt fungiert das Artefakt als Container-Image. Es kann als eine Art erweitete ausfĂŒhrbare Datei betrachtet werden, die nicht nur das Programm, sondern auch eine vollstĂ€ndige Laufzeitumgebung (Java/Python/
) sowie die notwendigen Dateien/Pakete enthĂ€lt, vorinstalliert und bereit zur AusfĂŒhrung. Container können auf verschiedenen Servern bereitgestellt und gestartet werden, ohne dass zusĂ€tzliche Maßnahmen erforderlich sind.

DarĂŒber hinaus arbeiten Container in einer eigenen Sandbox-Umgebung. Sie verfĂŒgen ĂŒber einen eigenen virtuellen Netzwerkadapter, ein Dateisystem mit eingeschrĂ€nktem Zugriff, eine eigene Prozesshierarchie, eigene CPU- und SpeicherkapazitĂ€tsbeschrĂ€nkungen usw. All dies wird durch ein spezielles Subsystem des Linux-Kernels – Namespaces – ermöglicht.

Kubernetes

Wie bereits erwĂ€hnt, ist Kubernetes ein Container-Orchestrator. Er funktioniert folgendermaßen: Sie stellen ihm einen Pool von Maschinen zur VerfĂŒgung und sagen dann: „Hey, Kubernetes, starte zehn Instanzen meines Containers mit 2 Prozessoren und 3 GB RAM pro Einheit und halte sie am Laufen!“. Kubernetes kĂŒmmert sich um den Rest. Er findet verfĂŒgbare KapazitĂ€ten, startet die Container und startet sie bei Bedarf neu, rollt Updates bei Versionswechseln aus usw. Im Grunde genommen ermöglicht Kubernetes, sich von der Hardware abzukoppeln und macht die Vielfalt der Systeme fĂŒr die Bereitstellung und den Betrieb von Anwendungen nutzbar.

CPU-Grenzen und aggressives Throttling in Kubernetes
Kubernetes aus der Sicht eines einfachen BĂŒrgers

Was sind Requests und Limits in Kubernetes?

Okay, wir haben die Container und Kubernetes geklĂ€rt. Außerdem wissen wir, dass mehrere Container auf einer Maschine laufen können.

Man kann eine Analogie zu einer Wohngemeinschaft ziehen. Ein gerĂ€umiger Raum (Maschinen/Knoten) wird genommen und an mehrere Mieter (Container) vermietet. Kubernetes fungiert als Immobilienmakler. Die Frage ist, wie man die Mieter von Konflikten untereinander abhalten kann? Was ist, wenn einer von ihnen beschließt, das Badezimmer fĂŒr einen halben Tag zu besetzen?

Genau hier kommen Requests und Limits ins Spiel. CPU Request ist ausschließlich fĂŒr die Planung gedacht. Es ist eine Art „Wunschliste“ des Containers und wird verwendet, um den am besten geeigneten Knoten zu finden. Gleichzeitig kann CPU Begrenzung mit einem Mietvertrag verglichen werden – sobald wir einen Knoten fĂŒr den Container gefunden haben, kann dieser nicht die festgelegten Grenzen ĂŒberschreiten. Und hier entsteht das Problem


Wie Requests und Limits in Kubernetes umgesetzt sind

Kubernetes verwendet einen integrierten Kernmechanismus zur Drosselung (Throttling), um CPU-Limits umzusetzen. Wenn eine Anwendung das Limit ĂŒberschreitet, wird das Throttling aktiviert (d.h. sie erhĂ€lt weniger CPU-Zeit). Requests und Limits fĂŒr den Arbeitsspeicher sind anders organisiert, weshalb sie leichter erkannt werden können. Es reicht aus, den letzten Status des Pod-Neustarts zu ĂŒberprĂŒfen: Ist er nicht „OOMKilled“? Bei CPU-Throttling ist es nicht so einfach, da K8s nur Metriken zur Nutzung anzeigt, nicht zu cgroups.

CPU-Anfrage

CPU-Grenzen und aggressives Throttling in Kubernetes
Wie die CPU-Anfrage umgesetzt ist

Zur Vereinfachung betrachten wir den Prozess am Beispiel einer Maschine mit 4-Kern-CPU.

K8s verwendet den Mechanismus der Kontrollgruppen (cgroups) zur Verwaltung der Ressourcenzuteilung (Speicher und CPU). DafĂŒr steht ein hierarchisches Modell zur VerfĂŒgung: Ein Nachkomme erbt die Limits der ĂŒbergeordneten Gruppe. Details zur Ressourcenzuteilung werden im virtuellen Dateisystem gespeichert (/sys/fs/cgroup). Im Falle des Prozessors ist das /sys/fs/cgroup/cpu,cpuacct/*.

K8s verwendet die Datei cpu.share zur Zuteilung von Prozessorrressourcen. In unserem Fall erhĂ€lt die Wurzel-Kontrollgruppe 4096 Anteile der CPU-Ressourcen – 100% der verfĂŒgbaren CPU-Leistung (1 Kern = 1024; das ist ein fester Wert). Die Wurzelgruppe verteilt die Ressourcen proportional entsprechend den Anteilen der Nachkommen, die in cpu.sharevermerkt sind, und diese verfahren ihrerseits Ă€hnlich mit ihren Nachkommen usw. In einem typischen Kubernetes-Knoten hat die Wurzel-Kontrollgruppe drei Nachkommen: system.slice, user.slice und kubepods. Die ersten beiden Untergruppen werden zur Ressourcenzuteilung zwischen kritischen Systemlasten und Benutzerprogrammen außerhalb von K8s verwendet. Die letzte – kubepods – wird von Kubernetes zur Ressourcenzuteilung zwischen Pods erstellt.

In der obigen Abbildung ist zu sehen, dass die erste und zweite Untergruppe jeweils 1024 Anteile erhalten haben, wĂ€hrend der Untergruppe kubepod 4096 Anteile zugewiesen wurden. Wie ist das möglich: Die Wurzelgruppe hat nur 4096 Anteile zur VerfĂŒgung, wĂ€hrend die Summe der Anteile ihrer Nachkommen diese Zahl erheblich ĂŒbersteigt (6144)? Die Sache ist, dass der Wert eine logische Bedeutung hat, weshalb der Linux-Scheduler (CFS) ihn fĂŒr die proportionale Zuteilung von CPU-Ressourcen verwendet. In unserem Fall erhalten die ersten beiden Gruppen jeweils 680 reale Anteile (16,6% von 4096), wĂ€hrend kubepod die verbleibenden 2736 Anteile erhĂ€lt. Im Falle von Leerlauf werden die ersten beiden Gruppen die zugewiesenen Ressourcen nicht verwenden.

GlĂŒcklicherweise gibt es im Scheduler einen Mechanismus, der den Verlust ungenutzter CPU-Ressourcen vermeidet. Er ĂŒbertrĂ€gt «untĂ€tige» KapazitĂ€ten in einen globalen Pool, aus dem sie an Gruppen verteilt werden, die zusĂ€tzliche Prozessorleistung benötigen (die Übertragung erfolgt in Chargen, um Verluste durch Rundung zu vermeiden). Ein Ă€hnliches Verfahren wird auch auf alle Nachfahren der Nachfahren angewandt.

Dieser Mechanismus sorgt fĂŒr eine faire Verteilung der Prozessorleistung und ĂŒberwacht, dass kein Prozess Ressourcen von anderen «stehlt».

CPU-Limit

Obwohl die Konfigurationen von Limits und Requests in K8s Ă€hnlich aussehen, ist ihre Implementierung grundlegend unterschiedlich: Es ist die irrefĂŒhrendste und am wenigsten dokumentierte Komponente.

K8s nutzt den CFS-Quota-Mechanismus zur Umsetzung von Limits. Ihre Einstellungen werden in den Dateien cfs_period_us und cfs_quota_us im cgroup-Verzeichnis festgelegt (dort befindet sich auch die Datei cpu.share).

Im Gegensatz zu cpu.share, basiert die Quote auf der Zeitperiode, nicht auf der verfĂŒgbaren Prozessorleistung. cfs_period_us legt die Dauer der Periode (Epoche) fest – dies sind immer 100.000 ”s (100 ms). In K8s gibt es die Möglichkeit, diesen Wert zu Ă€ndern, allerdings ist dies bislang nur in der Alpha-Version verfĂŒgbar. Der Scheduler verwendet die Epoche, um verbrauchte Quoten zurĂŒckzusetzen. Die zweite Datei, cfs_quota_us, legt die verfĂŒgbare Zeit (Quote) in jeder Epoche fest. Beachten Sie, dass sie ebenfalls in Mikrosekunden angegeben wird. Die Quote kann die Dauer der Epoche ĂŒberschreiten; mit anderen Worten, sie kann mehr als 100 ms betragen.

Lassen Sie uns zwei Szenarien auf 16-Kern-Maschinen (die hÀufigste Art von Computern bei uns in Omio) betrachten:

CPU-Grenzen und aggressives Throttling in Kubernetes
Szenario 1: 2 Threads und ein Limit von 200 ms. Ohne Drosselung

CPU-Grenzen und aggressives Throttling in Kubernetes
Szenario 2: 10 Threads und ein Limit von 200 ms. Die Drosselung beginnt nach 20 ms, der Zugriff auf die CPU-Ressourcen wird nach weiteren 80 ms wiederhergestellt.

Angenommen, Sie haben das CPU-Limit auf 2 Kerne festgelegt; Kubernetes wandelt diesen Wert in 200 ms um. Das bedeutet, dass der Container maximal 200 ms CPU-Zeit ohne Drosselung verwenden kann.

Und hier wird es interessant. Wie oben erwĂ€hnt, betrĂ€gt die verfĂŒgbare Quote 200 ms. Wenn Sie gleichzeitig zehn Threads auf einer 12-Kern-Maschine (siehe Abbildung zu Szenario 2) ausfĂŒhren, wĂ€hrend alle anderen Pods untĂ€tig sind, wird die Quote bereits nach 20 ms erschöpft sein (da 10 * 20 ms = 200 ms), und alle Threads dieses Pods werden «drosseln» (throttle) auf die nĂ€chsten 80 ms. Die Situation wird durch den bereits erwĂ€hnten Planer-Bug, aufgrund dessen ĂŒbermĂ€ĂŸiges Throttling auftritt und der Container nicht einmal das vorhandene Kontingent nutzen kann.

Wie bewertet man das Throttling in Pods?

Treten Sie einfach in den Pod ein und fĂŒhren Sie cat /sys/fs/cgroup/cpu/cpu.stat.

  • nr_periods — die Gesamtzahl der Planungsperioden;
  • nr_throttled — die Anzahl der gedrosselten Perioden; nr_periods;
  • throttled_time — die kumulierte Throttled-Zeit in Nanosekunden.

CPU-Grenzen und aggressives Throttling in Kubernetes

Was passiert eigentlich?

Am Ende haben wir ein hohes Throttling in allen Anwendungen. Manchmal ist es um das 1,5-fache stÀrker als berechnet!

Dies fĂŒhrt zu verschiedenen Fehlern – Problemen mit der BereitschaftsprĂŒfung, hĂ€ngenbleibenden Containern, VerbindungsabbrĂŒchen und ZeitĂŒberschreitungen innerhalb der Dienstaufrufe. Letztendlich Ă€ußert sich dies in einer erhöhten Verzögerung und einer steigenden Anzahl von Fehlern.

Die Lösung und die Konsequenzen

Hier ist alles ganz einfach. Wir haben auf CPU-Limits verzichtet und die Betriebssystem-Kernel in den Clustern auf die neueste Version aktualisiert, in der der Bug behoben wurde. Die Anzahl der Fehler (HTTP 5xx) in unseren Diensten ist sofort deutlich gesunken:

HTTP 5xx Fehler

CPU-Grenzen und aggressives Throttling in Kubernetes
HTTP 5xx Fehler eines kritischen Dienstes

Antwortzeit p95

CPU-Grenzen und aggressives Throttling in Kubernetes
Verzögerung bei Anfragen eines kritischen Dienstes, 95. Perzentil

Betriebskosten

CPU-Grenzen und aggressives Throttling in Kubernetes
Anzahl der verbrauchten Instanzstunden

Wo liegt der Haken?

Wie zu Beginn des Artikels erwÀhnt:

Man kann eine Analogie zu einer Wohngemeinschaft ziehen
 Kubernetes fungiert als Immobilienverwalter. Aber wie kann man die Mieter daran hindern, Konflikte miteinander zu haben? Was, wenn einer von ihnen beschließt, das Badezimmer fĂŒr einen halben Tag zu besetzen?

Hier liegt das Problem. Ein nachlĂ€ssiger Container kann alle verfĂŒgbaren CPU-Ressourcen des Systems aufbrauchen. Wenn Sie einen soliden Anwendungsstapel haben (zum Beispiel richtig konfigurierte JVM, Go, Node VM), ist das kein Problem: Unter solchen Bedingungen kann man lange Zeit arbeiten. Wenn die Anwendungen jedoch schlecht oder gar nicht optimiert sind (FROM java:latest), kann die Situation außer Kontrolle geraten. Bei uns in Omio haben wir automatisierte Basis-Dockerfiles mit angemessenen Standardeinstellungen fĂŒr die wichtigsten Programmiersprachen, sodass dieses Problem nicht aufgetreten ist.

Wir empfehlen, die Metriken zu ĂŒberwachen. USE (Nutzung, Saturierung und Fehler), API-Verzögerungen und HĂ€ufigkeit des Auftretens von Fehlern. Achten Sie darauf, dass die Ergebnisse Ihren Erwartungen entsprechen.

Links

Das ist unsere Geschichte. Die folgenden Materialien haben sehr geholfen, zu verstehen, was passiert:

Fehlerberichte zu Kubernetes:

Hatten Sie Ă€hnliche Probleme in Ihrer Praxis oder verfĂŒgen Sie ĂŒber Erfahrungen im Zusammenhang mit Throttling in containerisierten Produktionsumgebungen? Teilen Sie Ihre Geschichte in den Kommentaren!

P.S. vom Übersetzer

Lesen Sie auch in unserem Blog:

Quelle: habr.com

ZuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz kaufen, VPS VDS Server đŸ”„ ZuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz kaufen, VPS VDS Server - ProHoster