CPU-Limits und aggressive Drosselung in Kubernetes

Hinweis.: 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. Solche Fallstudien erweitern nicht nur den Horizont, sondern helfen auch, unauffällige Probleme zu vermeiden.

CPU-Limits und aggressive Drosselung in Kubernetes

Hatten Sie schon einmal mit einer Anwendung zu kämpfen, die 'hängengeblieben' ist und nicht mehr auf die Status-Anfragen (Health Checks) reagierte, sodass Sie die Ursache für solches Verhalten nicht verstehen konnten? Eine mögliche Erklärung dafür hängt mit den CPU-Ressourcenkontingenten zusammen. Darüber wird in diesem Artikel gesprochen.

TL;DR:
Wir empfehlen dringend, von CPU-Limits in Kubernetes abzusehen (oder die CFS-Quoten im Kubelet zu deaktivieren), wenn eine fehlerhafte Linux-Kernel-Version mit einem CFS-Quota-Bug verwendet wird. Wir haben Es handelt sich um einen bekannten Fehler, der zu übermäßigem Throttling und Verzögerungen führt.
.

Bei Omio wird die gesamte Infrastruktur von Kubernetes verwaltet.. Alle unsere stateful- und stateless-Anwendungen laufen ausschließlich auf Kubernetes (wir verwenden Google Kubernetes Engine). In den letzten sechs Monaten haben wir zufällige Verzögerungen beobachtet. Anwendungen frieren ein oder reagieren nicht mehr auf Health Checks, verlieren die Netzwerkverbindung usw. Verhalten dieser Art hat uns lange Zeit baff gemacht, und schließlich haben wir uns entschlossen, das Problem intensiver anzugehen.

Kurzzusammenfassung des Artikels:

  • Einige Worte über Container und Kubernetes;
  • Wie CPU-Anfragen und -Limits implementiert sind;
  • Wie das CPU-Limit in Umgebungen mit mehreren Kernen funktioniert;
  • Wie man das CPU-Throttling überwacht;
  • Die Lösung des Problems und wichtige Details.

Einige Worte über Container und Kubernetes

Kubernetes ist im Grunde der moderne Standard in der Infrastrukturwelt. Seine Hauptaufgabe ist die Orchestrierung von Containern.

Container

Früher mussten wir Artefakte wie Java JARs/WARs, Python Eggs oder ausführbare Dateien erstellen, die anschließend auf Servern gestartet wurden. Um sie jedoch zum Laufen zu bringen, war zusätzlicher Aufwand erforderlich: die Ausführungsumgebung (Java/Python) zu installieren, die notwendigen Dateien an die richtigen Stellen zu platzieren und die Kompatibilität mit spezifischen Betriebssystemversionen sicherzustellen, usw. Mit anderen Worten, wir mussten der Konfigurationsverwaltung besondere Aufmerksamkeit widmen, was oft zu Spannungen zwischen Entwicklern und Systemadministratoren führte.

Container haben alles verändert. Heute ist das Artefakt ein Container-Image. Man kann sich das wie eine erweiterte ausführbare Datei vorstellen, die nicht nur das Programm, sondern auch eine vollständige Ausführungsumgebung (Java/Python/…) sowie die notwendigen Dateien/Pakete enthält, die vorinstalliert und bereit zum Starten sind. Container können auf verschiedenen Servern bereitgestellt und ausgeführt werden, ohne dass zusätzliche Maßnahmen erforderlich sind.

Darüber hinaus arbeiten Container in einer eigenen Sandbox-Umgebung. Jeder Container verfügt über einen eigenen virtuellen Netzwerkadapter, ein eigenes Dateisystem mit eingeschränktem Zugriff, eine eigene Prozesshierarchie, spezifische CPU- und RAM-Beschränkungen usw. Dies wird durch ein besonderes Subsystem des Linux-Kernels, die Namespaces, realisiert.

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 Instanz und halte sie am Laufen!'. Kubernetes kümmert sich um den Rest. Er findet freie Kapazitäten, startet die Container und startet sie bei Bedarf neu, führt Upgrades bei Versionswechsel durch usw. Im Wesentlichen ermöglicht Kubernetes die Abstraktion von der Hardware und macht alle Arten von Systemen für die Bereitstellung und Ausführung von Anwendungen geeignet.

CPU-Limits und aggressive Drosselung in Kubernetes
Kubernetes aus der Sicht des Durchschnittsnutzers

Was sind Requests und Limits in Kubernetes

Okay, wir haben uns mit Containern und Kubernetes beschäftigt. Wir wissen auch, dass mehrere Container auf einer Maschine laufen können.

Man kann dies mit einer Wohngemeinschaft vergleichen. Ein geräumiger Raum (Maschinen/Knoten) wird an mehrere Mieter (Container) vermietet. Kubernetes agiert dabei als Immobilienmakler. Die Frage ist, wie man Konflikte zwischen den Mietern vermeiden kann? Was passiert, wenn einer von ihnen beschließt, das Badezimmer für einen halben Tag in Beschlag zu nehmen?

Hier kommen Requests und Limits ins Spiel. CPU Request dient ausschließlich der Planung. Es ist so etwas wie eine "Wunschliste" des Containers und wird verwendet, um den geeignetsten Knoten auszuwählen. Gleichzeitig Limit kann man mit einem Mietvertrag vergleichen — sobald wir einen Knoten für den Container ausgewählt haben, kann dieser die festgelegten Grenzen nicht überschreiten. Und hier entsteht das Problem…

Wie sind Requests und Limits in Kubernetes implementiert?

Kubernetes nutzt einen im Kernel integrierten Drosselungsmechanismus zur Implementierung von CPU-Limits. Wenn eine Anwendung das Limit überschreitet, wird die Drosselung aktiviert (d.h. sie erhält weniger CPU-Takte). Anfragen und Limits für den Speicher sind anders organisiert, sodass sie leichter zu erkennen sind. Dafür reicht es aus, den letzten Status des Pod-Neustarts zu überprüfen: Ist er nicht „OOMKilled“. Bei der CPU-Drosselung ist dies komplizierter, da K8s nur Nutzungsmetriken und keine cgroups zur Verfügung stellt.

CPU-Anfrage

CPU-Limits und aggressive Drosselung in Kubernetes
Wie die CPU-Anfrage umgesetzt wird

Zur Vereinfachung betrachten wir den Prozess anhand eines Beispiels mit einer Maschine mit einem 4-Kern-CPU.

K8s verwendet Mechanismen von Kontrollgruppen (cgroups), um die Ressourcenzuteilung (Speicher und Prozessor) zu steuern. Es gibt ein hierarchisches Modell, bei dem ein Kind die Limits der übergeordneten Gruppe erbt. Details zur Zuteilung werden im virtuellen Dateisystem (/sys/fs/cgroup). Im Falle des Prozessors ist das /sys/fs/cgroup/cpu,cpuacct/*.

K8s verwendet die Datei cpu.share zur Verteilung von CPU-Ressourcen. In unserem Fall erhält die Root-Control-Gruppe 4096 Anteile der CPU-Ressourcen – 100% der verfügbaren Prozessorkapazität (1 Kern = 1024; dies ist ein fester Wert). Die Root-Gruppe verteilt die Ressourcen proportional basierend auf den Anteilen der Nachkommen, die in cpu.share, und diese verfahren ihrerseits ebenso mit ihren Nachkommen usw. In einem typischen Kubernetes-Knoten hat die Root-Control-Gruppe drei Nachkommen: system.slice, user.slice und kubepods. Die ersten beiden Untergruppen werden verwendet, um Ressourcen zwischen kritischen Systemlasten und Benutzeranwendungen außerhalb von K8s zu verteilen. Die Letzte – kubepods – wird von Kubernetes erstellt, um Ressourcen zwischen Pods zu verteilen.

Im obigen Diagramm ist zu sehen, dass die erste und die zweite Untergruppe jeweils 1024 Anteile erhalten haben, während der Untergruppe kubepod 4096 Anteile zugewiesen wurden. Wie ist das möglich: Die Root-Gruppe hat insgesamt nur 4096 Anteile zur Verfügung, während die Summe der Anteile ihrer Nachkommen deutlich höher ist (6144)? Es ist wichtig zu verstehen, dass der Wert eine logische Bedeutung hat, weshalb der Linux-Scheduler (CFS) ihn für die proportionale Verteilung der 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 keine zugewiesenen Ressourcen nutzen.

Glücklicherweise verfügt der Scheduler über einen Mechanismus, der den Verlust ungenutzter CPU-Ressourcen verhindert. Er überträgt die „leerlaufenden“ Kapazitäten in einen globalen Pool, aus dem sie an Gruppen verteilt werden, die zusätzliche Prozessorkapazitäten benötigen (die Übertragung erfolgt in Paketen, um Verluste durch Rundungsfehler zu vermeiden). Ein ähnliches Verfahren wird auch auf alle Nachkommen angewandt.

Dieser Mechanismus gewährleistet eine faire Verteilung der Prozessorkapazitäten und stellt sicher, dass kein Prozess Ressourcen von anderen „stehlen“ kann.

CPU-Limit

Obwohl die Konfigurationen von Limits und Requests in K8s ähnlich aussehen, ist ihre Implementierung grundlegend unterschiedlich: es ist der irreführendste und am wenigsten dokumentierte Teil.

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

Im Gegensatz zu cpu.share, basiert das Kontingent auf Zeitperioden, nicht auf der verfügbaren Rechenleistung. cfs_period_us legt die Dauer der Periode (Epoche) fest — dies sind immer 100000 µs (100 ms). In K8s gibt es die Möglichkeit, diesen Wert zu ändern, jedoch ist dies bisher nur in der Alpha-Version verfügbar. Der Scheduler verwendet die Epoche, um die verwendeten Kontingente zurückzusetzen. Die zweite Datei, cfs_quota_us, legt die verfügbare Zeit (Kontingent) in jeder Epoche fest. Beachten Sie, dass sie ebenfalls in Mikrosekunden angegeben wird. Das Kontingent kann die Dauer der Epoche überschreiten; mit anderen Worten, es kann mehr als 100 ms betragen.

Schauen wir uns zwei Szenarien auf 16-Kern-Maschinen an (der am häufigsten verwendete Computertyp bei uns in Omio):

CPU-Limits und aggressive Drosselung in Kubernetes
Szenario 1: 2 Threads und ein Limit von 200 ms. Ohne Drosselung.

CPU-Limits und aggressive Drosselung in Kubernetes
Szenario 2: 10 Threads und ein Limit von 200 ms. Drosselung beginnt nach 20 ms, der Zugriff auf die Rechenressourcen wird nach weiteren 80 ms wiederhergestellt.

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

Hier beginnt das Interessanteste. Wie bereits erwähnt, beträgt das verfügbare Kontingent 200 ms. Wenn Sie parallel zehn Threads auf einer 12-Kern-Maschine ausführen (siehe Abbildung zum Szenario 2), während alle anderen Pods im Leerlauf sind, wird das Kontingent bereits nach 20 ms erschöpft sein (da 10 * 20 ms = 200 ms), und alle Threads dieses Pods werden "eingeschränkt" (throttle) für die nächsten 80 ms. Verschärft wird die Situation durch den bereits erwähnten Fehler im Scheduler,der zu übermäßiger Drosselung führt und der Container nicht einmal das vorhandene Kontingent nutzen kann.

Wie kann man die Drosselung in Pods bewerten?

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

  • nr_periods — die Gesamtzahl der Scheduler-Zeiträume;
  • nr_throttled — die Anzahl der gedrosselten Zeiträume innerhalb von nr_periods;
  • throttled_time — die gesamte gedrosselte Zeit in Nanosekunden.

CPU-Limits und aggressive Drosselung in Kubernetes

Was passiert tatsächlich?

Am Ende erfahren wir eine hohe Drosselung in allen Anwendungen. Manchmal ist sie um anderthalb Mal stärker als erwartet!

Dies führt zu verschiedenen Fehlern – Ausfällen bei der Bereitschaftskontrolle, Container-Hängern, Netzwerkausfällen und Timeouts bei Serviceaufrufen. Letztendlich äußert sich dies in erhöhten Latenzzeiten und einer Zunahme der Fehleranzahl.

Lösungen und Folgen

Hier ist alles einfach. Wir haben die CPU-Limits abgeschafft und das Betriebssystemkern-Update in den Clustern auf die neueste Version durchgeführt, in der der Fehler behoben wurde. Die Anzahl der Fehler (HTTP 5xx) in unseren Diensten ist sofort deutlich gesunken:

HTTP 5xx Fehler

CPU-Limits und aggressive Drosselung in Kubernetes
HTTP 5xx Fehler eines kritischen Dienstes

p95 Antwortzeiten

CPU-Limits und aggressive Drosselung in Kubernetes
Latenz der Anfragen an den kritischen Dienst, 95. Perzentil

Betriebskosten

CPU-Limits und aggressive Drosselung in Kubernetes
Anzahl der aufgewendeten Instanzstunden

Was ist der Haken?

Wie zu Beginn des Artikels erwähnt:

Man kann es mit einer Wohngemeinschaft vergleichen… Kubernetes fungiert als Immobilienverwalter. Aber wie kann man die Mieter davon abhalten, Konflikte miteinander zu haben? Was ist, wenn einer von ihnen zum Beispiel beschließt, das Badezimmer für einen halben Tag zu besetzen?

Hier liegt der Haken. Ein einzelner schlecht konfigurierter Container kann alle verfügbaren CPU-Ressourcen auf dem Server aufbrauchen. Wenn Sie jedoch einen soliden Anwendungsstack haben (zum Beispiel korrekt konfigurierte JVMs, Go oder Node-VMs), stellt das kein Problem dar: In solchen Bedingungen kann man über längere Zeiträume arbeiten. Ist die Anwendungsoptimierung hingegen unzulänglich oder ganz fehlend, kann die Situation schnell außer Kontrolle geraten.FROM java:latestIn unserem Unternehmen, Omio, verfügen wir über automatisierte Basis-Dockerfiles mit adäquaten Voreinstellungen für die Hauptprogrammiersprachen, sodass solche Probleme in der Regel nicht auftreten.

Wir empfehlen, die Metriken USE (Nutzung, Saturation und Fehler), API-Latenzen und Häufigkeit von Fehlern im Auge zu behalten. Stellen Sie sicher, dass die Ergebnisse Ihren Erwartungen entsprechen.

Links

Das ist unsere Geschichte. Die folgenden Materialien haben erheblich dazu beigetragen, zu verstehen, was vor sich geht:

Kubernetes Fehlerberichte:

Hatten Sie ähnliche Probleme in Ihrer Praxis oder Erfahrungen 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 Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen 🔥 Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen | ProHoster