Kubernetes Tipps & Tricks: Besonderheiten des graceful Shutdowns in NGINX und PHP-FPM

Standardanforderung bei der Durchführung von CI/CD in Kubernetes: Die Anwendung muss in der Lage sein, vor der vollständigen Abschaltung keine neuen Kundenanfragen mehr anzunehmen und, was noch wichtiger ist, bereits bestehende Anfragen erfolgreich abzuschließen.

Kubernetes Tipps & Tricks: Besonderheiten eines sanften Herunterfahrens in NGINX und PHP-FPM

Die Einhaltung dieser Anforderung ermöglicht es, eine Null-Ausfallzeit während des Deployments zu erreichen. Allerdings können selbst bei der Verwendung sehr beliebter Kombinationen (wie NGINX und PHP-FPM) Probleme auftreten, die zu einem Anstieg von Fehlern bei jedem Deployment führen...

Theorie. Wie lebt ein Pod

Über den Lebenszyklus eines Pods haben wir bereits veröffentlicht diesen Artikel. Im Kontext des behandelten Themas interessiert uns Folgendes: In dem Moment, in dem der Pod in den Status wechselt Wird beendet, werden keine neuen Anfragen mehr an ihn gesendet (der Pod wird entfernt aus der Liste der Endpunkte für den Dienst). Um also eine Ausfallzeit während des Deployments zu vermeiden, reicht es aus, das Problem der ordnungsgemäßen Abschaltung der Anwendung zu lösen.

Es ist auch zu beachten, dass der Grace-Period standardmäßig 30 Sekunden beträgt: Nach dieser Zeit wird der Pod beendet und die Anwendung sollte alle Anfragen bis zu diesem Zeitraum bearbeitet haben. Hinweis: Obwohl jede Anfrage, die länger als 5-10 Sekunden dauert, bereits problematisch ist, wird ein sanftes Herunterfahren nicht helfen…

Um besser zu verstehen, was passiert, wenn ein Pod heruntergefahren wird, genügt es, das folgende Schema zu betrachten:

Kubernetes Tipps & Tricks: Besonderheiten eines sanften Herunterfahrens in NGINX und PHP-FPM

A1, B1 — Empfang von Änderungen zum Status des Pods
A2 — Versand von SIGTERM
B2 — Entfernen des Pods aus Endpunkten
B3 — Empfang von Änderungen (die Liste der Endpunkte hat sich geändert)
B4 — Aktualisierung der iptables-Regeln

Hinweis: Das Entfernen des Endpoint-Pods und das Senden von SIGTERM geschieht nicht sequentiell, sondern parallel. Aufgrund der Tatsache, dass Ingress die aktualisierte Liste der Endpunkte nicht sofort erhält, werden neue Anfragen von Clients an den Pod gesendet, was während der Termination des Pods zu 500-Fehlern führen kann. (detaillierteres Material zu diesem Thema haben wir übersetzt). Diese Probleme sollten wie folgt angegangen werden:

  • Im Antwort-Header Connection: close senden (wenn es sich um eine HTTP-Anwendung handelt).
  • Wenn keine Änderungen im Code vorgenommen werden können, beschreibt der folgende Artikel eine Lösung, die es ermöglichen wird, Anfragen bis zum Ende der sanften Periode zu bearbeiten.

Theorie. Wie NGINX und PHP-FPM ihre Prozesse beenden.

NGINX

Lassen Sie uns mit NGINX beginnen, da es hier relativ klar ist. Wenn wir in die Theorie eintauchen, erfahren wir, dass NGINX einen Master-Prozess und mehrere „Worker“ hat – das sind die Kindprozesse, die die Kundenanfragen bearbeiten. Es gibt eine praktische Möglichkeit: mit dem Befehl nginx -s können Prozesse entweder im Schnellabbau oder im sanften Shutdown beendet werden. Offensichtlich interessiert uns letzterer.

Der nächste Schritt ist einfach: Fügen Sie im preStop-Hook einen Befehl hinzu, der das Signal für einen sanften Shutdown sendet. Dies kann im Deployment im Containerblock geschehen:

       lifecycle:
          preStop:
            exec:
              command:
              - /usr/sbin/nginx
              - -s
              - quit

Jetzt sehen wir im Container-Log von NGINX beim Herunterfahren des Pods Folgendes:

2018/01/25 13:58:31 [notice] 1#1: signal 3 (SIGQUIT) received, shutting down
2018/01/25 13:58:31 [notice] 11#11: gracefully shutting down

Und das bedeutet genau das, was wir wollen: NGINX wartet, bis alle Anfragen bearbeitet sind, bevor der Prozess beendet wird. Allerdings wird im Folgenden auch ein verbreitetes Problem behandelt, bei dem selbst mit dem Befehl nginx -s quit der Prozess nicht korrekt beendet wird.

An dieser Stelle sind wir mit NGINX fertig: Anhand der Logs können wir sehen, dass alles wie gewünscht funktioniert.

Wie steht es um PHP-FPM? Wie verarbeitet es das graceful shutdown? Lass uns das herausfinden.

PHP-FPM

Im Fall von PHP-FPM gibt es etwas weniger Informationen. Wenn wir uns an die offizielle Dokumentation von PHP-FPM halten, wird beschrieben, dass die folgenden POSIX-Signale akzeptiert werden:

  1. SIGINT, SIGTERM — schnelles Herunterfahren;
  2. SIGQUIT — sanftes Herunterfahren (das ist, was wir brauchen).

Die anderen Signale sind für diese Aufgabe nicht erforderlich, daher lassen wir deren Analyse weg. Für einen korrekten Abschluss des Prozesses müssen wir den folgenden preStop-Hook schreiben:

        lifecycle:
          preStop:
            exec:
              command:
              - /bin/kill
              - -SIGQUIT
              - "1"

Auf den ersten Blick scheint das alles zu sein, was für das sanfte Herunterfahren in beiden Containern benötigt wird. Dennoch ist die Aufgabe komplexer als sie scheint. Im Folgenden werden zwei Fälle beschrieben, in denen das graceful shutdown nicht funktionierte und während des Deployments zu kurzzeitigen Ausfällen des Projekts führte.

Praxis. Mögliche Probleme mit dem graceful shutdown

NGINX

Zunächst ist es wichtig zu beachten: Neben der Ausführung des Befehls nginx -s quit Es gibt noch einen weiteren Punkt, den es wert ist, beachtet zu werden. Wir hatten das Problem, dass NGINX statt des SIGQUIT-Signals dennoch SIGTERM gesendet hat, was dazu führte, dass die Anfragen nicht korrekt abgeschlossen wurden. Ähnliche Fälle sind zum Beispiel zu finden, hier. Leider konnten wir den genauen Grund für dieses Verhalten nicht ermitteln: Verdacht fiel auf die NGINX-Versionen, aber das bestätigte sich nicht. Die Symptome deuteten darauf hin, dass in den Logs des NGINX-Containers Nachrichten „open socket #10 left in connection 5“, zu sehen waren, nach denen das Pod gestoppt wurde.

Wir können solch ein Problem zum Beispiel an den Antworten auf unserem benötigten Ingress beobachten:

Kubernetes Tipps & Tricks: Besonderheiten eines sanften Herunterfahrens in NGINX und PHP-FPM
Die Statuscode-Metriken zum Zeitpunkt des Deployments

In diesem Fall erhalten wir genau den 503-Fehlercode vom Ingress selbst: Er kann nicht auf den NGINX-Container zugreifen, da dieser bereits nicht mehr verfügbar ist. Wenn wir die Logs des NGINX-Containers betrachten, sehen wir folgendes:

[alert] 13939#0: *154 open socket #3 left in connection 16
[alert] 13939#0: *168 open socket #6 left in connection 13

Nach der Änderung des Stop-Signals beginnt der Container korrekt zu stoppen: Dies wird dadurch bestätigt, dass der 503-Fehler nicht mehr auftritt.

Wenn Sie auf ein ähnliches Problem gestoßen sind, kann es sinnvoll sein, herauszufinden, welches Stop-Signal im Container verwendet wird und wie der preStop-Hook genau aussieht. Möglicherweise liegt das Problem genau darin.

PHP-FPM… und mehr

Das Problem mit PHP-FPM lässt sich einfach zusammenfassen: Es wartet nicht auf den Abschluss der Kindprozesse, sondern beendet diese, was während des Deployments und anderer Vorgänge zu 502-Fehlern führt. Seit 2005 gibt es auf bugs.php.net mehrere Fehlermeldungen (zum Beispiel, hier und hier), die dieses Problem beschreiben. In den Logs werden Sie wahrscheinlich nichts sehen: PHP-FPM meldet den Abschluss seines Prozesses ohne Fehler oder weitere Benachrichtigungen.

Es ist wichtig zu beachten, dass das Problem in unterschiedlichem Maße vom eigentlichen Programm abhängen kann und beispielsweise im Monitoring nicht sichtbar sein kann. Sollten Sie dennoch damit konfrontiert werden, fällt einem zunächst eine einfache Lösung ein: Fügen Sie einen preStop-Hook mit sleep(30)hinzu. Dieser ermöglicht es, alle vorherigen Anfragen abzuschließen (neue nehmen wir nicht an, da sich der Pod bereits im Zustand Wird beendet), und nach Ablauf von 30 Sekunden wird der Pod selbst mit einem Signal beendet. SIGTERM.

Es stellt sich heraus, dass Der lifecycle für den Container würde folgendermaßen aussehen: für den Container wird folgendermaßen aussehen:

    Lebenszyklus:
      preStop:
        exec:
          command:
          - /bin/sleep
          - "30"

Allerdings führt die Vorgabe von 30 Sekunden dazu, sleep verwenden wir stark die Deploy-Zeit zu verlängern, da jeder Pod terminieren wird mindestens 30 Sekunden, was ungünstig ist. Was kann man dagegen tun?

Betrachten wir die Komponente, die für die tatsächliche Ausführung der Anwendung verantwortlich ist. In unserem Fall ist das PHP-FPM, der standardmäßig nicht für die Überwachung seiner Kindprozesse zuständig: Der Masterprozess wird sofort beendet. Dieses Verhalten kann durch die Direktive process_control_timeoutgeändert werden, die Zeitlimits für das Warten auf Signale vom Master durch die Kindprozesse festlegt. Wenn ein Wert von 20 Sekunden festgelegt wird, deckt dies die meisten Anfragen ab, die im Container ausgeführt werden, und nach deren Abschluss wird der Masterprozess gestoppt.

Mit diesem Verständnis kehren wir zu unserem letzten Problem zurück. Wie bereits erwähnt, ist Kubernetes keine monolithische Plattform: Die Interaktion zwischen ihren verschiedenen Komponenten erfordert etwas Zeit. Dies ist besonders relevant, wenn wir die Funktionsweise von Ingress und anderen verwandten Komponenten betrachten, da aufgrund dieser Verzögerung beim Deployment leicht ein Anstieg von 500-Fehlern auftreten kann. Beispielsweise kann der Fehler während der Anfrage an den Upstream auftreten, doch der "zeitliche Verzögerung" in der Interaktion zwischen den Komponenten ist ziemlich kurz – weniger als eine Sekunde.

Deshalb, insgesamt mit der bereits erwähnten Direktive process_control_timeout kann folgende Konstruktion für Der lifecycle für den Container würde folgendermaßen aussehen::

lifecycle:
  preStop:
    exec:
      command: ["/bin/bash","-c","/bin/sleep 1; kill -QUIT 1"]

In diesem Fall kompensieren wir die Verzögerung mit dem Befehl sleep und erhöhen nicht erheblich die Deployment-Zeit: es ist ein spürbarer Unterschied zwischen 30 Sekunden und einer?... Im Grunde genommen übernimmt die "Hauptarbeit" genau process_control_timeout, und Der lifecycle für den Container würde folgendermaßen aussehen: wird lediglich als "Absicherung" im Falle eines Lags verwendet.

Im Allgemeinen das beschriebene Verhalten und die entsprechende Problemlösung betrifft nicht nur PHP-FPM. Eine ähnliche Situation kann in der einen oder anderen Hinsicht auch bei der Verwendung anderer Programmiersprachen oder Frameworks auftreten. Wenn es auf andere Weise nicht gelingt, den graceful shutdown zu beheben – zum Beispiel, indem der Code so umgeschrieben wird, dass die Anwendung die Abschluss-Signale korrekt verarbeitet – kann die beschriebene Methode angewendet werden. Auch wenn sie nicht die eleganteste ist, funktioniert sie.

Praxis. Lasttest zur Überprüfung der Funktionsweise eines Pods.

Lasttests sind eine Möglichkeit, die Leistung eines Containers zu überprüfen, da dieses Verfahren realen Bedingungen näherkommt, wenn Benutzer die Website besuchen. Um die oben genannten Empfehlungen zu testen, kann man auf Yandex.Tank: es erfüllt all unsere Anforderungen hervorragend. Im Folgenden finden Sie Tipps und Empfehlungen zur Durchführung von Tests mit einem anschaulichen Beispiel – dank der Grafiken von Grafana und Yandex.Tank – aus unserer Erfahrung.

Das Wichtigste hier ist Änderungen schrittweise zu überprüfen.. Nach Hinzufügen einer neuen Korrektur sollten Sie die Tests erneut durchführen und prüfen, ob sich die Ergebnisse im Vergleich zum vorherigen Lauf geändert haben. Andernfalls wird es schwierig, ineffiziente Lösungen zu identifizieren, und langfristig kann es sogar schädlich sein (zum Beispiel die Deploy-Zeit verlängern).

Ein weiterer Punkt ist, die Protokolle des Containers während seiner Beendigung zu beobachten. Wird dort Informationen über ein sanftes Herunterfahren festgehalten? Gibt es Fehlerprotokolle beim Zugriff auf andere Ressourcen (zum Beispiel auf den benachbarten PHP-FPM-Container)? Gibt es Fehler der Anwendung selbst (wie im oben beschriebenen Fall mit NGINX)? Ich hoffe, dass die einleitenden Informationen aus diesem Artikel helfen, besser zu verstehen, was während der Beendigung des Containers geschieht.

Der erste Testlauf fand also ohne Der lifecycle für den Container würde folgendermaßen aussehen: und ohne zusätzliche Anweisungen für den Anwendungsserver (process_control_timeout in PHP-FPM) statt. Ziel dieses Tests war es, die ungefähre Anzahl der Fehler zu ermitteln (und ob diese überhaupt vorhanden sind). Außerdem sollte die zusätzliche Information bekannt sein, dass die durchschnittliche Deploy-Zeit jedes Pods etwa 5-10 Sekunden bis zur vollständigen Bereitschaft betrug. Die Ergebnisse sind wie folgt:

Kubernetes Tipps & Tricks: Besonderheiten eines sanften Herunterfahrens in NGINX und PHP-FPM

Auf dem Dashboard von Yandex.Tank ist ein Anstieg von 502-Fehlern sichtbar, der während des Deployments auftrat und im Durchschnitt bis zu 5 Sekunden andauerte. Es wird vermutet, dass bestehende Anfragen an das alte Pod abgebrochen wurden, als es terminiert wurde. Danach traten 503-Fehler auf, was das Ergebnis eines gestoppten NGINX-Containers war, der ebenfalls Verbindungen aufgrund des Backends abbrach (was dazu führte, dass sich Ingress nicht verbinden konnte).

Schauen wir uns an, wie process_control_timeout uns PHP-FPM hilft, auf den Abschluss von Child-Prozessen zu warten, d.h. solche Fehler zu beheben. Ein erneutes Deployment mit dieser Direktive:

Kubernetes Tipps & Tricks: Besonderheiten eines sanften Herunterfahrens in NGINX und PHP-FPM

Während des Deployments gibt es keine 500-Fehler mehr! Das Deployment verläuft erfolgreich, und der graceful shutdown funktioniert.

Es ist jedoch wichtig, an den Punkt mit den Ingress-Containern zu denken, bei dem wir einen kleinen Prozentsatz von Fehlern aufgrund eines temporären Lags erhalten können. Um diese zu vermeiden, bleibt es, eine Konstruktion mit sleep hinzuzufügen und das Deployment zu wiederholen. In unserem speziellen Fall waren jedoch keine sichtbaren Änderungen zu erkennen (Fehler sind weiterhin nicht vorhanden).

Fazit

Für einen ordnungsgemäßen Abschluss des Prozesses erwarten wir vom Anwendung folgende Verhaltensweise:

  1. Ein paar Sekunden zu warten, bevor neue Verbindungen nicht mehr angenommen werden.
  2. Warten Sie, bis alle Anfragen abgeschlossen sind, und schließen Sie alle Keepalive-Verbindungen, die keine Anfragen ausführen.
  3. Beenden Sie Ihren Prozess.

Allerdings können nicht alle Anwendungen so arbeiten. Eine Lösung für dieses Problem in der Kubernetes-Welt ist:

  • das Hinzufügen eines Pre-Stop-Hooks, der einige Sekunden wartet;
  • die Überprüfung der Konfigurationsdatei unseres Backends auf die entsprechenden Parameter.

Ein Beispiel mit NGINX zeigt, dass selbst eine Anwendung, die ursprünglich korrekt auf Stoppsignale reagieren soll, dies möglicherweise nicht tut. Daher ist es entscheidend, das Auftreten von 500-Fehlern während des Deployments der Anwendung zu überprüfen. Dies ermöglicht auch einen breiteren Blick auf das Problem und verhindert, dass man sich nur auf einen einzelnen Pod oder Container konzentriert, sondern die gesamte Infrastruktur im Blick behält.

Als Testwerkzeug kann Yandex.Tank zusammen mit jedem Überwachungssystem verwendet werden (in unserem Fall wurden die Daten aus Grafana mit Prometheus als Backend für den Test herangezogen). Probleme mit dem Graceful Shutdown sind bei hohen Lasten, die durch Benchmarks erzeugt werden können, gut sichtbar, und die Überwachung hilft, die Situation während oder nach dem Test detaillierter zu analysieren.

In Bezug auf das Feedback zu dem Artikel sollte erwähnt werden, dass die Probleme und deren Lösungen hier für NGINX Ingress beschrieben werden. Für andere Szenarien gibt es unterschiedliche Lösungen, die wir möglicherweise in künftigen Beiträgen der Serie behandeln werden.

P.S.

Weitere aus der Reihe K8s Tipps & Tricks:

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster