Fünf Fehler bei der Bereitstellung der ersten Anwendung auf Kubernetes

Fünf Fehler bei der Bereitstellung der ersten Anwendung auf KubernetesScheitern von Aris Dreamer

Viele Leute denken, dass es ausreicht, die Anwendung auf Kubernetes zu übertragen (entweder mit Helm oder manuell) – und sie werden glücklich sein. Aber nicht alles ist so einfach.

Team Mail.ru Cloud-Lösungen übersetzte einen Artikel des DevOps-Ingenieurs Julian Gindy. Er erzählt, mit welchen Fallstricken sein Unternehmen während des Migrationsprozesses konfrontiert war, damit Sie nicht auf die gleiche Rechenschaft treten.

Schritt eins: Pod-Anfragen und -Limits einrichten

Beginnen wir mit der Einrichtung einer sauberen Umgebung, in der unsere Pods ausgeführt werden. Kubernetes eignet sich hervorragend für Pod-Planung und Failover. Es stellte sich jedoch heraus, dass der Planer manchmal einen Pod nicht platzieren kann, wenn es schwierig ist abzuschätzen, wie viele Ressourcen er benötigt, um erfolgreich zu arbeiten. Hier tauchen Anfragen nach Ressourcen und Limits auf. Es gibt viele Debatten über den besten Ansatz zur Festlegung von Anforderungen und Grenzwerten. Manchmal scheint es, dass dies eher eine Kunst als eine Wissenschaft ist. Hier ist unser Ansatz.

Pod-Anfragen ist der Hauptwert, den der Planer verwendet, um den Pod optimal zu platzieren.

Von Kubernetes-Dokumentation: Der Filterschritt definiert eine Reihe von Knoten, auf denen ein Pod geplant werden kann. Beispielsweise prüft der PodFitsResources-Filter, ob ein Knoten über genügend Ressourcen verfügt, um bestimmte Ressourcenanforderungen eines Pods zu erfüllen.

Wir nutzen Bewerbungsanfragen so, dass wir abschätzen können, wie viele Ressourcen vorhanden sind tatsächlich Die Anwendung benötigt es, um ordnungsgemäß zu funktionieren. Auf diese Weise kann der Planer die Knoten realistisch platzieren. Ursprünglich wollten wir Anfragen überplanen, um sicherzustellen, dass für jeden Pod genügend Ressourcen vorhanden sind. Wir stellten jedoch fest, dass sich die Planungszeit deutlich verlängerte und einige Pods nicht vollständig geplant waren, als ob es keine Ressourcenanfragen für sie gäbe.

In diesem Fall „verdrängte“ der Planer die Pods oft und konnte sie nicht neu planen, da die Steuerungsebene keine Ahnung hatte, wie viele Ressourcen die Anwendung benötigen würde, was eine Schlüsselkomponente des Planungsalgorithmus darstellt.

Pod-Grenzwerte ist eine klarere Grenze für einen Pod. Es stellt die maximale Menge an Ressourcen dar, die der Cluster dem Container zuweist.

Nochmals von amtliche Dokumentation: Wenn ein Container ein Speicherlimit von 4 GiB hat, wird es vom Kubelet (und der Containerlaufzeit) erzwungen. Die Laufzeit verhindert, dass der Container mehr als das angegebene Ressourcenlimit verwendet. Wenn beispielsweise ein Prozess in einem Container versucht, mehr als die zulässige Menge an Speicher zu verwenden, beendet der Systemkernel den Prozess mit der Fehlermeldung „Out of Memory“ (OOM).

Ein Container kann immer mehr Ressourcen nutzen, als in der Ressourcenanforderung angegeben ist, aber nie mehr als das Limit. Es ist schwierig, diesen Wert richtig einzustellen, aber er ist sehr wichtig.

Idealerweise möchten wir, dass sich die Ressourcenanforderungen eines Pods während des Lebenszyklus eines Prozesses ändern, ohne andere Prozesse im System zu beeinträchtigen – das ist der Zweck der Festlegung von Grenzwerten.

Leider kann ich keine konkreten Anweisungen geben, welche Werte eingestellt werden müssen, wir selbst halten uns jedoch an folgende Regeln:

  1. Mit einem Lasttest-Tool simulieren wir ein Basis-Traffic-Niveau und beobachten die Nutzung der Pod-Ressourcen (Speicher und Prozessor).
  2. Setzen Sie die Pod-Anfragen auf einen beliebig niedrigen Wert (mit einem Ressourcenlimit von etwa dem Fünffachen des Anfragewerts) und beobachten Sie. Wenn die Anforderungen zu niedrig sind, kann der Prozess nicht gestartet werden, was häufig zu kryptischen Go-Laufzeitfehlern führt.

Ich stelle fest, dass höhere Ressourcenlimits die Planung schwieriger machen, da der Pod einen Zielknoten mit ausreichend verfügbaren Ressourcen benötigt.

Stellen Sie sich eine Situation vor, in der Sie über einen leichtgewichtigen Webserver mit einer sehr hohen Ressourcenbeschränkung, beispielsweise 4 GB Arbeitsspeicher, verfügen. Dieser Prozess muss wahrscheinlich horizontal skaliert werden und jeder neue Pod muss auf einem Knoten mit mindestens 4 GB verfügbarem Speicher geplant werden. Wenn kein solcher Knoten vorhanden ist, muss der Cluster einen neuen Knoten einführen, um diesen Pod zu verarbeiten, was einige Zeit dauern kann. Es ist wichtig, einen minimalen Unterschied zwischen Ressourcenanforderungen und -limits zu erreichen, um eine schnelle und reibungslose Skalierung zu gewährleisten.

Schritt zwei: Richten Sie Lebendigkeits- und Bereitschaftstests ein

Dies ist ein weiteres heikles Thema, das in der Kubernetes-Community häufig diskutiert wird. Es ist wichtig, die Liveness- und Readiness-Tests gut zu verstehen, da sie einen Mechanismus für den stabilen Betrieb der Software bieten und Ausfallzeiten minimieren. Sie können jedoch die Leistung Ihrer Anwendung erheblich beeinträchtigen, wenn sie nicht richtig konfiguriert werden. Nachfolgend finden Sie eine Zusammenfassung der beiden Beispiele.

Lebendigkeit Zeigt an, ob der Container ausgeführt wird. Wenn dies fehlschlägt, beendet das Kubelet den Container und die Neustartrichtlinie wird für ihn aktiviert. Wenn der Container nicht mit einer Lebendigkeitssonde ausgestattet ist, ist der Standardstatus „Erfolgreich“ – wie in angegeben Kubernetes-Dokumentation.

Liveness-Probes sollten kostengünstig sein, d. h. nicht viele Ressourcen verbrauchen, da sie häufig ausgeführt werden und Kubernetes darüber informieren sollten, dass die Anwendung ausgeführt wird.

Wenn Sie die Option „Ausführung jede Sekunde“ festlegen, wird eine Anfrage pro Sekunde hinzugefügt. Beachten Sie daher, dass für die Verarbeitung dieses Datenverkehrs zusätzliche Ressourcen erforderlich sind.

Bei uns testen Liveness-Tests die Kernkomponenten einer Anwendung, auch wenn die Daten (z. B. aus einer entfernten Datenbank oder einem Cache) nicht vollständig verfügbar sind.

Wir haben in den Anwendungen einen „Gesundheits“-Endpunkt eingerichtet, der einfach den Antwortcode 200 zurückgibt. Dies ist ein Hinweis darauf, dass der Prozess ausgeführt wird und Anfragen verarbeiten kann (aber noch keinen Datenverkehr).

Probe Bereitschaft Gibt an, ob der Container bereit ist, Anfragen zu bedienen. Wenn die Bereitschaftsprüfung fehlschlägt, entfernt der Endpunkt-Controller die IP-Adresse des Pods von den Endpunkten aller Dienste, die mit dem Pod übereinstimmen. Dies steht auch in der Kubernetes-Dokumentation.

Bereitschaftsprüfungen verbrauchen mehr Ressourcen, da sie das Backend so erreichen müssen, dass sie zeigen, dass die Anwendung bereit ist, Anfragen anzunehmen.

In der Community gibt es viele Diskussionen darüber, ob direkt auf die Datenbank zugegriffen werden soll. In Anbetracht des Mehraufwands (die Prüfungen erfolgen häufig, können aber kontrolliert werden) haben wir entschieden, dass bei einigen Anwendungen die Bereitschaft zur Bereitstellung von Datenverkehr erst gezählt wird, nachdem überprüft wurde, ob Datensätze aus der Datenbank zurückgegeben werden. Gut konzipierte Bereitschaftstests stellten eine höhere Verfügbarkeit sicher und verhinderten Ausfallzeiten während der Bereitstellung.

Wenn Sie sich entscheiden, die Datenbank abzufragen, um die Einsatzbereitschaft Ihrer Anwendung zu testen, stellen Sie sicher, dass dies so kostengünstig wie möglich ist. Nehmen wir diese Abfrage:

SELECT small_item FROM table LIMIT 1

Hier ist ein Beispiel dafür, wie wir diese beiden Werte in Kubernetes konfigurieren:

livenessProbe: 
 httpGet:   
   path: /api/liveness    
   port: http 
readinessProbe:  
 httpGet:    
   path: /api/readiness    
   port: http  periodSeconds: 2

Sie können einige zusätzliche Konfigurationsoptionen hinzufügen:

  • initialDelaySeconds - Wie viele Sekunden vergehen zwischen dem Start des Containers und dem Start der Sonden?
  • periodSeconds — Warteintervall zwischen Probenläufen.
  • timeoutSeconds — die Anzahl der Sekunden, nach denen der Pod als Notfall gilt. Normales Timeout.
  • failureThreshold ist die Anzahl der Testfehler, bevor ein Neustartsignal an den Pod gesendet wird.
  • successThreshold ist die Anzahl erfolgreicher Versuche, bevor der Pod in den Bereitschaftszustand übergeht (nach einem Fehler beim Starten oder Wiederherstellen des Pods).

Schritt drei: Festlegen der Standardnetzwerkrichtlinien des Pods

Kubernetes verfügt über eine „flache“ Netzwerktopographie, standardmäßig kommunizieren alle Pods direkt miteinander. In manchen Fällen ist dies nicht wünschenswert.

Ein potenzielles Sicherheitsrisiko besteht darin, dass ein Angreifer eine einzige anfällige Anwendung verwenden könnte, um Datenverkehr an alle Pods im Netzwerk zu senden. Wie in vielen Bereichen der Sicherheit gilt auch hier das Prinzip der geringsten Privilegien. Im Idealfall sollten Netzwerkrichtlinien explizit angeben, welche Verbindungen zwischen Pods erlaubt sind und welche nicht.

Das Folgende ist beispielsweise eine einfache Richtlinie, die den gesamten eingehenden Datenverkehr für einen bestimmten Namespace ablehnt:

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:  
 name: default-deny-ingress
spec:  
 podSelector: {}  
 policyTypes:  
   - Ingress

Visualisierung dieser Konfiguration:

Fünf Fehler bei der Bereitstellung der ersten Anwendung auf Kubernetes
(https://miro.medium.com/max/875/1*-eiVw43azgzYzyN1th7cZg.gif)
Ausführlicher hier.

Schritt vier: Benutzerdefiniertes Verhalten mit Hooks und Init-Containern

Eines unserer Hauptziele war es, Bereitstellungen in Kubernetes ohne Ausfallzeiten für Entwickler bereitzustellen. Dies ist schwierig, da es viele Möglichkeiten gibt, Anwendungen herunterzufahren und ihre genutzten Ressourcen freizugeben.

Besondere Schwierigkeiten ergaben sich bei Nginx. Wir haben festgestellt, dass bei der Bereitstellung dieser Pods nacheinander aktive Verbindungen unterbrochen wurden, bevor sie erfolgreich abgeschlossen wurden.

Nach umfangreichen Recherchen im Internet stellte sich heraus, dass Kubernetes nicht darauf wartet, dass die Nginx-Verbindungen erschöpft sind, bevor es den Pod herunterfährt. Mit Hilfe des Pre-Stop-Hooks haben wir die folgende Funktionalität implementiert und die Ausfallzeit vollständig beseitigt:

lifecycle: 
 preStop:
   exec:
     command: ["/usr/local/bin/nginx-killer.sh"]

und hier nginx-killer.sh:

#!/bin/bash
sleep 3
PID=$(cat /run/nginx.pid)
nginx -s quit
while [ -d /proc/$PID ]; do
   echo "Waiting while shutting down nginx..."
   sleep 10
done

Ein weiteres äußerst nützliches Paradigma ist die Verwendung von Init-Containern für den Start spezifischer Anwendungen. Dies ist besonders nützlich, wenn Sie einen ressourcenintensiven Datenbankmigrationsprozess haben, der vor dem Start der Anwendung ausgeführt werden muss. Sie können für diesen Prozess auch ein höheres Ressourcenlimit festlegen, ohne ein solches Limit für die Hauptanwendung festzulegen.

Ein weiteres gängiges Schema besteht darin, auf Geheimnisse im Init-Container zuzugreifen, der diese Anmeldeinformationen dem Hauptmodul bereitstellt, wodurch ein unbefugter Zugriff auf Geheimnisse vom Hauptanwendungsmodul selbst verhindert wird.

Wie üblich ein Zitat aus der Dokumentation: Init-Container führen Benutzercode oder Dienstprogramme sicher aus, die andernfalls die Sicherheit des Container-Images der Anwendung gefährden würden. Indem Sie unnötige Tools getrennt halten, begrenzen Sie die Angriffsfläche des Container-Images der Anwendung.

Schritt fünf: Kernel-Konfiguration

Lassen Sie uns abschließend über eine fortgeschrittenere Technik sprechen.

Kubernetes ist eine äußerst flexible Plattform, die es Ihnen ermöglicht, Workloads so auszuführen, wie Sie es für richtig halten. Wir verfügen über eine Reihe hocheffizienter Anwendungen, die äußerst ressourcenintensiv sind. Nach umfangreichen Lasttests stellten wir fest, dass es einer der Anwendungen schwerfiel, mit der erwarteten Verkehrslast Schritt zu halten, wenn die Standardeinstellungen von Kubernetes in Kraft waren.

Mit Kubernetes können Sie jedoch einen privilegierten Container ausführen, der nur Kernel-Parameter für einen bestimmten Pod ändert. Folgendes haben wir verwendet, um die maximale Anzahl offener Verbindungen zu ändern:

initContainers:
  - name: sysctl
     image: alpine:3.10
     securityContext:
         privileged: true
      command: ['sh', '-c', "sysctl -w net.core.somaxconn=32768"]

Dies ist eine fortgeschrittenere Technik, die oft nicht benötigt wird. Wenn Ihre Anwendung jedoch Probleme mit der hohen Auslastung hat, können Sie versuchen, einige dieser Einstellungen zu optimieren. Weitere Informationen zu diesem Vorgang und dem Einstellen verschiedener Werte – wie immer in der offiziellen Dokumentation.

Abschließend

Auch wenn Kubernetes wie eine sofort einsatzbereite Lösung erscheint, müssen einige wichtige Schritte unternommen werden, damit Anwendungen reibungslos laufen.

Während der Migration zu Kubernetes ist es wichtig, den „Lasttestzyklus“ einzuhalten: Führen Sie die Anwendung aus, testen Sie sie unter Last, beobachten Sie die Metriken und das Skalierungsverhalten, passen Sie die Konfiguration basierend auf diesen Daten an und wiederholen Sie diesen Zyklus dann erneut.

Seien Sie realistisch hinsichtlich des erwarteten Datenverkehrs und versuchen Sie, darüber hinauszugehen, um zu sehen, welche Komponenten zuerst kaputt gehen. Bei diesem iterativen Vorgehen können bereits wenige der aufgeführten Empfehlungen ausreichen, um einen Erfolg zu erzielen. Oder es erfordert möglicherweise eine tiefergehende Anpassung.

Stellen Sie sich immer diese Fragen:

  1. Wie viele Ressourcen verbrauchen Anwendungen und wie wird sich dieser Betrag ändern?
  2. Was sind die tatsächlichen Skalierungsanforderungen? Wie viel Traffic verkraftet die App im Durchschnitt? Wie sieht es mit dem Spitzenverkehr aus?
  3. Wie oft muss der Dienst skaliert werden? Wie schnell müssen neue Pods betriebsbereit sein, um Datenverkehr zu empfangen?
  4. Wie ordnungsgemäß werden Pods heruntergefahren? Ist es überhaupt notwendig? Ist eine Bereitstellung ohne Ausfallzeiten möglich?
  5. Wie können Sicherheitsrisiken minimiert und Schäden durch kompromittierte Pods begrenzt werden? Verfügen Dienste über Berechtigungen oder Zugriffe, die sie nicht benötigen?

Kubernetes bietet eine unglaubliche Plattform, die es Ihnen ermöglicht, mithilfe von Best Practices Tausende von Diensten in einem Cluster bereitzustellen. Allerdings sind alle Anwendungen unterschiedlich. Manchmal erfordert die Umsetzung etwas mehr Arbeit.

Glücklicherweise bietet Kubernetes die notwendigen Einstellungen, um alle technischen Ziele zu erreichen. Durch die Verwendung einer Kombination aus Ressourcenanforderungen und -limits, Liveness- und Readiness-Prüfungen, Init-Containern, Netzwerkrichtlinien und benutzerdefinierter Kernel-Optimierung können Sie eine hohe Leistung bei gleichzeitiger Fehlertoleranz und schneller Skalierbarkeit erreichen.

Was es sonst noch zu lesen gibt:

  1. Best Practices und Best Practices für die Ausführung von Containern und Kubernetes in Produktionsumgebungen.
  2. Über 90 nützliche Tools für Kubernetes: Bereitstellung, Verwaltung, Überwachung, Sicherheit und mehr.
  3. Unser Kanal Rund um Kubernetes in Telegram.

Source: habr.com

Kommentar hinzufügen