Fünf Fehler beim Bereitstellen der ersten Anwendung auf Kubernetes

Fünf Fehler beim Bereitstellen der ersten Anwendung auf KubernetesFehler von Aris-Dreamer

Viele glauben, dass es ausreicht, die Anwendung auf Kubernetes zu migrieren (entweder mit Helm oder manuell) – und alles wird gut. Aber so einfach ist es nicht.

Team Mail.ru Cloud Solutions Sie hat den Artikel des DevOps-Ingenieurs Julian Gindi übersetzt. Er erzählt, mit welchen Problemen sein Unternehmen während der Migration konfrontiert war, damit Sie nicht dieselben Fehler machen.

Schritt eins: Anfragen von Pods und Limits konfigurieren

Lass uns mit der Einrichtung einer sauberen Umgebung beginnen, in der unsere Pods arbeiten werden. Kubernetes meistert die Planung von Pods und die Handhabung von Ausfallzuständen hervorragend. Aber es stellte sich heraus, dass der Scheduler manchmal nicht in der Lage ist, einen Pod zu platzieren, wenn er Schwierigkeiten hat, die benötigten Ressourcen für das erfolgreiche Funktionieren abzuschätzen. Hier kommen die Ressourcenanfragen und Limits ins Spiel. Es gibt viel Diskussionen über den besten Ansatz zur Konfiguration von Anfragen und Limits. Manchmal scheint es wirklich eher eine Kunst als eine Wissenschaft zu sein. Hier ist unser Ansatz.

Anfragen von Pods (pod requests) sind der Hauptwert, den der Scheduler für die optimale Platzierung eines Pods verwendet.

Aus Kubernetes-Dokumentation: In der Filterstufe wird eine Menge von Knoten bestimmt, auf denen ein Pod geplant werden kann. Zum Beispiel überprüft der Filter PodFitsResources, ob genügend Ressourcen auf dem Knoten vorhanden sind, um die spezifischen Ressourcenanfragen des Pods zu erfüllen.

Wir verwenden die Anforderungen für Anwendungen so, dass wir abschätzen können, wie viele Ressourcen wirklich die Anwendung für einen reibungslosen Betrieb benötigt. So kann der Scheduler die Knoten realistisch platzieren. Ursprünglich wollten wir die Anfragen großzügig festlegen, um genug Ressourcen für jeden Pod zu garantieren, stellten jedoch fest, dass die Planungszeit erheblich zunahm und einige Pods gar nicht vollständig geplant wurden, als wären keine Ressourcenanfragen für sie eingegangen.

In diesem Fall "drückte" der Scheduler oft Pods heraus und konnte sie wegen der unklaren Resource-Anforderungen, die die Verwaltungsebene nicht beurteilen konnte, nicht erneut planen. Das ist ein kritischer Bestandteil des Planungsalgorithmus.

Limits von Pods (pod limits) sind eine genauere Einschränkung für den Pod. Sie stellen das maximale Maß an Ressourcen dar, das der Cluster dem Container zuteilt.

Wiederum, aus offiziellen Dokumentation: Wenn für den Container ein Speicherkontingent von 4 GiB festgelegt ist, wird kubelet (und die Containerlaufzeit) ihn zwangsweise begrenzen. Die Laufzeit erlaubt es dem Container nicht, mehr als das festgelegte Ressourcenkontingent zu verwenden. Zum Beispiel, wenn ein Prozess im Container versucht, mehr als die erlaubte Menge an Speicher zu verwenden, beendet der Kernel das Prozess mit dem Fehler „out of memory“ (OOM).

Ein Container kann immer mehr Ressourcen verwenden, als im Ressourcenantrag angegeben, aber niemals mehr, als im Limit angegeben ist. Es ist schwierig, diesen Wert richtig festzulegen, aber er ist sehr wichtig.

Idealerweise möchten wir, dass die Ressourcenanforderungen des Pods sich während des Lebenszyklus des Prozesses ändern, ohne andere Prozesse im System zu stören — das ist das Ziel der Festlegung von Limits.

Leider kann ich keine konkreten Anweisungen geben, welche Werte festzulegen sind, aber wir halten uns an die folgenden Regeln:

  1. Durch den Einsatz eines Lasttestwerkzeugs simulieren wir ein Basis-Traffic-Niveau und beobachten die Ressourcennutzung des Pods (Speicher und CPU).
  2. Wir setzen die Anforderungen des Pods auf einen willkürlich niedrigen Wert (mit einem Ressourcengrenzwert, der etwa 5-mal so hoch ist wie die Anforderungswerte) und beobachten. Wenn die Anforderungen zu niedrig sind, kann der Prozess nicht starten, was oft zu mysteriösen Laufzeitfehlern in Go führt.

Ich möchte anmerken, dass höhere Ressourcengrenzen die Planung komplizierter machen, da der Pod einen Zielknoten mit ausreichenden verfügbaren Ressourcen benötigt.

Stellen Sie sich vor, Sie haben einen schlanken Webserver mit sehr hohen Ressourcengrenzen, beispielsweise 4 GB RAM. Wahrscheinlich muss dieser Prozess horizontal skaliert werden, und jedes neue Modul muss auf einem Knoten mit mindestens 4 GB verfügbarem Speicher geplant werden. Wenn ein solcher Knoten nicht existiert, muss der Cluster einen neuen Knoten einführen, um diesen Pod zu verarbeiten, was einige Zeit in Anspruch nehmen kann. Es ist wichtig, eine minimale Differenz zwischen den Ressourcenanforderungen und den Limits zu erreichen, um eine schnelle und reibungslose Skalierung sicherzustellen.

Schritt zwei: Konfigurieren von Liveness- und Readiness-Tests

Dies ist ein weiteres feines Thema, das oft in der Kubernetes-Community diskutiert wird. Es ist wichtig, sich gut mit den Liveness- und Readiness-Tests auszukennen, da sie einen Mechanismus für einen stabilen Betrieb der Software bieten und die Ausfallzeiten minimieren. Sie können jedoch die Leistung Ihrer Anwendung erheblich beeinträchtigen, wenn sie nicht richtig konfiguriert sind. Im Folgenden finden Sie eine kurze Zusammenfassung dessen, was beide Prüfungen darstellen.

Lebensfähigkeit zeigt, ob der Container funktioniert. Wenn er ausfällt, killt kubelet den Container, und die Neustart-Policy wird aktiviert. Wenn der Container nicht mit einem Liveness-Test ausgestattet ist, wird der Standardzustand als erfolgreich betrachtet – das steht in der Kubernetes-Dokumentation.

Liveness-Tests sollten kostengünstig sein, das heißt, sie sollten nicht viele Ressourcen verbrauchen, da sie häufig ausgeführt werden und Kubernetes informieren müssen, dass die Anwendung läuft.

Wenn Sie den Parameter so einstellen, dass er jede Sekunde ausgeführt wird, erhöht sich die Anzahl der Anfragen um 1 pro Sekunde. Berücksichtigen Sie, dass für die Verarbeitung dieses Datenverkehrs zusätzliche Ressourcen erforderlich sind.

In unserem Unternehmen überprüfen die Liveness-Tests die grundlegenden Komponenten der Anwendung, selbst wenn bestimmte Daten (zum Beispiel aus einer entfernten Datenbank oder einem Cache) nicht vollständig verfügbar sind.

Wir haben in den Anwendungen einen 'Health'-Endpunkt konfiguriert, der einfach den HTTP-Statuscode 200 zurückgibt. Das ist ein Indikator dafür, dass der Prozess läuft und in der Lage ist, Anfragen zu bearbeiten (aber noch nicht den Datenverkehr).

Prüfung Bereitstellung zeigt an, ob der Container bereit ist, Anfragen zu bearbeiten. Wenn der Readiness-Test ausfällt, entfernt der Endpoint-Controller die IP-Adresse des Pods aus den Endpunkten aller Dienste, die dem Pod entsprechen. Das steht auch in der Kubernetes-Dokumentation.

Readiness-Tests verbrauchen mehr Ressourcen, da sie so in das Backend gelangen müssen, dass die Bereitschaft der Anwendung, Anfragen entgegenzunehmen, angezeigt wird.

In der Community gibt es viele Diskussionen darüber, ob man direkt auf die Datenbank zugreifen sollte. Angesichts der Nebenkosten (Überprüfungen werden häufig durchgeführt, können aber reguliert werden) haben wir entschieden, dass für einige Anwendungen die Bereitschaft, den Verkehr zu bedienen, erst nach der Überprüfung gilt, dass Datensätze aus der Datenbank zurückgegeben werden. Gut durchdachte Bereitschaftsprüfungen haben ein höheres Maß an Verfügbarkeit gewährleistet und Ausfallzeiten während der Bereitstellung beseitigt.

Wenn Sie eine Abfrage an die Datenbank zur Überprüfung der Bereitschaft der Anwendung durchführen, stellen Sie sicher, dass sie so kostengünstig wie möglich ist. Nehmen wir eine solche Abfrage:

SELECT small_item FROM table LIMIT 1

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

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

Es können einige zusätzliche Konfigurationsparameter hinzugefügt werden:

  • initialDelaySeconds — wie viele Sekunden vergehen zwischen dem Start des Containers und dem Beginn der Prüfungen.
  • periodSeconds — der Wartezeitraum zwischen den Prüfungen.
  • timeoutSeconds — die Anzahl der Sekunden, nach denen ein Pod als fehlerhaft gilt. Ein typisches Timeout.
  • failureThreshold — die Anzahl der fehlgeschlagenen Prüfungen, bevor ein Neustart-Signal an den Pod gesendet wird.
  • successThreshold — die Anzahl der erfolgreichen Prüfungen, bevor der Pod in den Bereitschaftszustand wechselt (nach einem Fehlversuch, wenn der Pod neu gestartet oder wiederhergestellt wird).

Schritt drei: Konfigurieren der standardmäßigen Netzwerkrichtlinien des Pods

In Kubernetes gibt es eine 'flache' Netzwerk-Topologie, in der alle Pods standardmäßig direkt miteinander kommunizieren. In einigen Fällen ist dies unerwünscht.

Ein potenzielles Sicherheitsproblem besteht darin, dass ein Angreifer eine einzige verwundbare Anwendung nutzen kann, um Verkehr an alle Pods im Netzwerk zu senden. Wie in vielen Bereichen der Sicherheit gilt auch hier das Prinzip der geringsten Privilegien. Idealerweise sollten Netzwerkrichtlinien ausdrücklich festlegen, welche Verbindungen zwischen Pods erlaubt sind und welche nicht.

Zum Beispiel, hier ist eine einfache Richtlinie, die sämtlichen eingehenden Verkehr für einen bestimmten Namespace verbietet:

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

Visualisierung dieser Konfiguration:

Fünf Fehler beim Bereitstellen der ersten Anwendung auf Kubernetes
(https://miro.medium.com/max/875/1*-eiVw43azgzYzyN1th7cZg.gif)
Genauer gesagt hier.

Schritt vier: Unkonventionelles Verhalten mit Hooks und Init-Containern

Eine unserer Hauptaufgaben war es, Deployments in Kubernetes ohne Ausfallzeiten für die Entwickler zu gewährleisten. Das ist schwierig, da es viele Möglichkeiten gibt, wie Anwendungen beendet werden und die verwendeten Ressourcen freigeben.

Besondere Schwierigkeiten gab es mit Nginx. Wir bemerkten, dass bei der sequentiellen Bereitstellung dieser Pods aktive Verbindungen unterbrochen wurden, bevor sie erfolgreich abgeschlossen waren.

Nach umfangreicher Recherche im Internet stellte sich heraus, dass Kubernetes nicht wartet, bis Nginx-Verbindungen beendet sind, bevor es den Pod herunterfährt. Mit Hilfe des Pre-Stop-Hooks implementierten wir diese Funktionalität und beseitigten vollständig die Ausfallzeiten:

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

Hier ist 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 zur Verwaltung des Starts bestimmter Anwendungen. Dies ist besonders nützlich, wenn Sie einen ressourcenintensiven Datenbankmigrationprozess haben, der vor dem Start der Anwendung ausgeführt werden soll. Für diesen Prozess können Sie auch eine höhere Ressourcengrenze festlegen, ohne diese Grenze für die Hauptanwendung festzulegen.

Ein weiteres gängiges Schema besteht darin, auf Geheimnisse im Init-Container zuzugreifen, der diese Anmeldedaten dem Hauptmodul zur Verfügung stellt, wodurch unbefugter Zugriff auf die Geheimnisse aus dem Hauptmodul der Anwendung verhindert wird.

Wie gewohnt, ein Zitat aus der Dokumentation: Init-Container führen sicher Benutzercode oder -dienstprogramme aus, die andernfalls die Sicherheit des Anwendungscontainer-Images verringern würden. Indem Sie unnötige Werkzeuge separat aufbewahren, verringern Sie die Angriffsfläche des Anwendungscontainer-Images.

Schritt fünf: Kernel-Konfiguration

Zuletzt berichten wir über eine fortgeschrittene Technik.

Kubernetes ist eine äußerst flexible Plattform, die es ermöglicht, Workloads so auszuführen, wie Sie es für richtig halten. Wir haben eine Reihe hocheffizienter Anwendungen, die extrem ressourcenintensiv sind. Nach umfangreichen Lasttests stellten wir fest, dass eine der Anwendungen Schwierigkeiten hatte, mit der erwarteten Verkehrslast umzugehen, wenn die Standard-Kubernetes-Einstellungen galten.

Kubernetes ermöglicht jedoch das Starten eines privilegierten Containers, der die Kernel-Parameter nur für ein bestimmtes Pod ändert. Hier ist, was wir verwendet haben, um die maximale Anzahl an offenen 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 erforderlich ist. Aber wenn Ihre Anwendung bei hoher Last kaum zurechtkommt, könnten Sie versuchen, einige dieser Parameter anzupassen. Nähere Informationen zu diesem Prozess und zur Konfiguration verschiedener Werte finden Sie wie immer in der offiziellen Dokumentation..

Abschließend

Obwohl Kubernetes wie eine sofort einsatzbereite Lösung erscheinen mag, sind einige wesentliche Schritte erforderlich, um die reibungslose Funktion der Anwendungen zu gewährleisten.

Während der gesamten Migration zu Kubernetes ist es wichtig, dem "Zyklus des Lasttests" zu folgen: Sie starten die Anwendung, testen sie unter Last, überwachen die Metriken und das Verhalten beim Skalieren, passen die Konfiguration basierend auf diesen Daten an und wiederholen dann diesen Zyklus.

Schätzen Sie den erwarteten Traffic realistisch ein und versuchen Sie, darüber hinauszugehen, um zu sehen, welche Komponenten zuerst ausfallen. Mit diesem iterativen Ansatz könnte es ausreichen, nur einige der genannten Empfehlungen zu befolgen. Es kann jedoch auch erforderlich sein, eine tiefere Anpassung vorzunehmen.

Stellen Sie sich immer solche Fragen:

  1. Wie viel Ressourcen verbrauchen die Anwendungen und wie wird sich dieser Bedarf ändern?
  2. Was sind die tatsächlichen Anforderungen an die Skalierung? Wie viel Traffic wird die Anwendung im Durchschnitt verarbeiten? Und wie sieht es mit dem Spitzen-Traffic aus?
  3. Wie oft benötigt der Dienst horizontale Skalierung? Wie schnell müssen neue Pods bereitgestellt werden, um den Traffic zu übernehmen?
  4. Wie korrekt wird die Arbeit der Pods beendet? Ist das überhaupt notwendig? Kann ein Deployment ohne Ausfallzeit erreicht werden?
  5. Wie lassen sich Sicherheitsrisiken minimieren und die Schäden durch kompromittierte Pods begrenzen? Haben einige Dienste Berechtigungen oder Zugänge, die sie nicht benötigen?

Kubernetes bietet eine beeindruckende Plattform, die es ermöglicht, bewährte Praktiken für die Bereitstellung von Tausenden von Diensten in einem Cluster zu nutzen. Dennoch sind alle Anwendungen unterschiedlich. Manchmal erfordert die Implementierung etwas mehr Arbeit.

Glücklicherweise bietet Kubernetes die erforderlichen Einstellungen, um alle technischen Ziele zu erreichen. Durch die Kombination von Ressourcenanfragen und -limits, Liveness- und Readiness-Probes, Init-Containern, Netzwerk-Richtlinien und benutzerdefinierter Kernel-Konfiguration können Sie hohe Leistung bei gleichzeitiger Ausfallsicherheit und schneller Skalierbarkeit erzielen.

Was Sie sonst noch lesen sollten:

  1. Best Practices und Empfehlungen für den Betrieb von Containern und Kubernetes in Produktionsumgebungen.
  2. 90+ nützliche Tools für Kubernetes: Deployment, Management, Monitoring, Sicherheit und mehr.
  3. Unser Kanal Rund um Kubernetes auf Telegram.

Quelle: habr.com

60GB SSD 8Gb DDR4