Tipps und Tricks zu Kubernetes: Funktionen für das ordnungsgemäße Herunterfahren in NGINX und PHP-FPM

Eine typische Bedingung bei der Implementierung von CI/CD in Kubernetes: Die Anwendung muss in der Lage sein, keine neuen Client-Anfragen anzunehmen, bevor sie vollständig gestoppt wird, und vor allem bestehende Anfragen erfolgreich abzuschließen.

Tipps und Tricks zu Kubernetes: Funktionen für das ordnungsgemäße Herunterfahren in NGINX und PHP-FPM

Durch die Einhaltung dieser Bedingung können Sie während der Bereitstellung keine Ausfallzeiten erreichen. Allerdings können selbst bei der Verwendung sehr beliebter Bundles (wie NGINX und PHP-FPM) Schwierigkeiten auftreten, die bei jeder Bereitstellung zu einer Flut von Fehlern führen ...

Theorie. Wie Pod lebt

Über den Lebenszyklus eines Pods haben wir bereits ausführlich publiziert dieser Artikel. Im Kontext des betrachteten Themas interessiert uns Folgendes: in dem Moment, in dem der Pod in den Zustand eintritt Beenden, werden keine neuen Anfragen mehr an ihn gesendet (pod gelöscht aus der Liste der Endpunkte für den Dienst). Um Ausfallzeiten während der Bereitstellung zu vermeiden, reicht es für uns aus, das Problem des ordnungsgemäßen Stoppens der Anwendung zu lösen.

Sie sollten auch bedenken, dass die standardmäßige Kulanzfrist beträgt 30 Sekunden: Danach wird der Pod beendet und die Anwendung muss vor diesem Zeitraum Zeit haben, alle Anfragen zu verarbeiten. Beachten: Obwohl jede Anfrage, die länger als 5-10 Sekunden dauert, bereits problematisch ist und ein ordnungsgemäßes Herunterfahren nicht mehr hilft ...

Um besser zu verstehen, was passiert, wenn ein Pod beendet wird, schauen Sie sich einfach das folgende Diagramm an:

Tipps und Tricks zu Kubernetes: Funktionen für das ordnungsgemäße Herunterfahren in NGINX und PHP-FPM

A1, B1 – Empfangen von Änderungen über den Zustand des Herdes
A2 – Abfahrt SIGTERM
B2 – Entfernen eines Pods von Endpunkten
B3 – Empfangen von Änderungen (die Liste der Endpunkte hat sich geändert)
B4 – Iptables-Regeln aktualisieren

Bitte beachten Sie: Das Löschen des Endpunkt-Pods und das Senden von SIGTERM erfolgen nicht nacheinander, sondern parallel. Und da Ingress die aktualisierte Liste der Endpunkte nicht sofort erhält, werden neue Anfragen von Clients an den Pod gesendet, was bei der Pod-Beendigung zu einem 500-Fehler führt (Für ausführlicheres Material zu diesem Thema wenden Sie sich bitte an uns übersetzt). Dieses Problem muss auf folgende Weise gelöst werden:

  • Verbindung senden: In Antwortheadern schließen (wenn es sich um eine HTTP-Anwendung handelt).
  • Wenn es nicht möglich ist, Änderungen am Code vorzunehmen, wird im folgenden Artikel eine Lösung beschrieben, die es Ihnen ermöglicht, Anfragen bis zum Ende der Kulanzfrist zu bearbeiten.

Theorie. Wie NGINX und PHP-FPM ihre Prozesse beenden

NGINX

Beginnen wir mit NGINX, da bei ihm alles mehr oder weniger offensichtlich ist. Wenn wir uns mit der Theorie befassen, erfahren wir, dass NGINX einen Master-Prozess und mehrere „Worker“ hat – das sind untergeordnete Prozesse, die Kundenanfragen verarbeiten. Es gibt eine praktische Möglichkeit: die Verwendung des Befehls nginx -s <SIGNAL> Beenden Sie Prozesse entweder im Modus „Schnelles Herunterfahren“ oder „Graceful Shutdown“. Offensichtlich ist es die letztere Option, die uns interessiert.

Dann ist alles ganz einfach: Sie müssen hinzufügen preStop-Haken ein Befehl, der ein ordnungsgemäßes Abschaltsignal sendet. Dies kann im Deployment im Containerblock erfolgen:

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

Wenn der Pod nun heruntergefahren wird, sehen wir Folgendes in den NGINX-Containerprotokollen:

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, was wir brauchen: NGINX wartet auf den Abschluss der Anforderungen und bricht dann den Prozess ab. Im Folgenden betrachten wir jedoch auch ein häufiges Problem, aufgrund dessen auch mit dem Befehl nginx -s quit Der Prozess wird fehlerhaft beendet.

Und zu diesem Zeitpunkt sind wir mit NGINX fertig: Zumindest aus den Protokollen kann man erkennen, dass alles so funktioniert, wie es sollte.

Was hat es mit PHP-FPM auf sich? Wie geht es mit dem ordnungsgemäßen Herunterfahren um? Lass es uns herausfinden.

PHP-FPM

Im Fall von PHP-FPM gibt es etwas weniger Informationen. Wenn Sie sich darauf konzentrieren offizielles Handbuch Laut PHP-FPM heißt es, dass die folgenden POSIX-Signale akzeptiert werden:

  1. SIGINT, SIGTERM — schnelles Herunterfahren;
  2. SIGQUIT — ordnungsgemäßes Herunterfahren (was wir brauchen).

Die übrigen Signale werden für diese Aufgabe nicht benötigt, daher verzichten wir auf ihre Analyse. Um den Prozess korrekt zu beenden, müssen Sie den folgenden preStop-Hook schreiben:

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

Auf den ersten Blick ist dies alles, was erforderlich ist, um in beiden Containern ein ordnungsgemäßes Herunterfahren durchzuführen. Allerdings ist die Aufgabe schwieriger als es scheint. Im Folgenden sind zwei Fälle aufgeführt, in denen das ordnungsgemäße Herunterfahren nicht funktionierte und dazu führte, dass das Projekt während der Bereitstellung kurzfristig nicht verfügbar war.

Üben. Mögliche Probleme beim ordnungsgemäßen Herunterfahren

NGINX

Zunächst ist es nützlich, sich daran zu erinnern: zusätzlich zur Ausführung des Befehls nginx -s quit Es gibt noch eine weitere Phase, die es wert ist, beachtet zu werden. Wir sind auf ein Problem gestoßen, bei dem NGINX immer noch SIGTERM anstelle des SIGQUIT-Signals sendete, was dazu führte, dass Anfragen nicht korrekt abgeschlossen wurden. Ähnliche Fälle finden sich z.B. hier. Leider konnten wir den konkreten Grund für dieses Verhalten nicht ermitteln: Es bestand ein Verdacht bezüglich der NGINX-Version, der sich jedoch nicht bestätigte. Das Symptom bestand darin, dass in den NGINX-Containerprotokollen folgende Meldungen beobachtet wurden: „Öffnen Sie die Buchse Nr. 10 links in Anschluss 5“, woraufhin die Kapsel stoppte.

Ein solches Problem können wir beispielsweise anhand der von uns benötigten Antworten auf den Ingress beobachten:

Tipps und Tricks zu Kubernetes: Funktionen für das ordnungsgemäße Herunterfahren in NGINX und PHP-FPM
Indikatoren für Statuscodes zum Zeitpunkt der Bereitstellung

In diesem Fall erhalten wir lediglich einen 503-Fehlercode von Ingress selbst: Es kann nicht auf den NGINX-Container zugegriffen werden, da dieser nicht mehr zugänglich ist. Wenn Sie sich die Containerprotokolle mit NGINX ansehen, enthalten diese Folgendes:

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

Nach dem Wechsel des Stoppsignals beginnt der Container korrekt zu stoppen: Dies wird dadurch bestätigt, dass der Fehler 503 nicht mehr beobachtet wird.

Wenn Sie auf ein ähnliches Problem stoßen, ist es sinnvoll herauszufinden, welches Stoppsignal im Container verwendet wird und wie der preStop-Hook genau aussieht. Es ist durchaus möglich, dass der Grund genau darin liegt.

PHP-FPM... und mehr

Das Problem mit PHP-FPM wird trivial beschrieben: Es wartet nicht auf den Abschluss von untergeordneten Prozessen, sondern beendet diese, weshalb beim Deployment und anderen Vorgängen 502-Fehler auftreten. Seit 2005 gibt es auf bugs.php.net mehrere Fehlerberichte (z. B hier и hier), der dieses Problem beschreibt. In den Protokollen werden Sie jedoch höchstwahrscheinlich nichts sehen: PHP-FPM meldet den Abschluss seines Prozesses ohne Fehler oder Benachrichtigungen Dritter.

Es sollte klargestellt werden, dass das Problem selbst in geringerem oder größerem Maße von der Anwendung selbst abhängen kann und sich beispielsweise nicht in der Überwachung manifestiert. Wenn Sie darauf stoßen, fällt Ihnen zunächst eine einfache Problemumgehung ein: Fügen Sie einen preStop-Hook mit hinzu sleep(30). Damit können Sie alle vorherigen Anfragen abschließen (und wir akzeptieren keine neuen, da pod bereits in der Lage zu Beenden), und nach 30 Sekunden endet der Pod selbst mit einem Signal SIGTERM.

Es stellt sich heraus, dass lifecycle denn der Container sieht so aus:

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

Allerdings aufgrund der 30-Sekunden sleep wir сильно Wir verlängern die Bereitstellungszeit, da jeder Pod beendet wird Minimum 30 Sekunden, was schlecht ist. Was kann man dagegen tun?

Wenden wir uns an den Verantwortlichen für die direkte Ausführung des Antrags. In unserem Fall ist es so PHP-FPMDie überwacht standardmäßig nicht die Ausführung seiner untergeordneten Prozesse: Der Masterprozess wird sofort beendet. Sie können dieses Verhalten mithilfe der Direktive ändern process_control_timeout, der die Zeitlimits angibt, die untergeordnete Prozesse auf Signale vom Master warten sollen. Wenn Sie den Wert auf 20 Sekunden festlegen, deckt dies die meisten im Container ausgeführten Abfragen ab und stoppt den Masterprozess, sobald diese abgeschlossen sind.

Mit diesem Wissen kehren wir zu unserem letzten Problem zurück. Wie bereits erwähnt ist Kubernetes keine monolithische Plattform: Die Kommunikation zwischen den verschiedenen Komponenten dauert einige Zeit. Dies gilt insbesondere, wenn wir den Betrieb von Ingresses und anderen zugehörigen Komponenten berücksichtigen, da es aufgrund einer solchen Verzögerung zum Zeitpunkt der Bereitstellung leicht zu einem Anstieg von 500 Fehlern kommt. Beispielsweise kann beim Senden einer Anfrage an einen Upstream ein Fehler auftreten, die „Zeitverzögerung“ der Interaktion zwischen Komponenten ist jedoch recht kurz – weniger als eine Sekunde.

Somit zusammen mit der bereits erwähnten Richtlinie process_control_timeout Sie können die folgende Konstruktion verwenden für lifecycle:

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

In diesem Fall gleichen wir die Verzögerung mit dem Befehl aus sleep und wir erhöhen die Bereitstellungszeit nicht wesentlich: Schließlich ist der Unterschied zwischen 30 Sekunden und einer Sekunde spürbar? process_control_timeoutUnd lifecycle Wird nur als „Sicherheitsnetz“ im Falle einer Verzögerung verwendet.

Generell gesagt, Das beschriebene Verhalten und der entsprechende Workaround gelten nicht nur für PHP-FPM. Eine ähnliche Situation kann auf die eine oder andere Weise bei der Verwendung anderer Sprachen/Frameworks auftreten. Wenn Sie das ordnungsgemäße Herunterfahren nicht auf andere Weise beheben können – beispielsweise durch Umschreiben des Codes, damit die Anwendung Beendigungssignale korrekt verarbeitet – können Sie die beschriebene Methode verwenden. Es ist vielleicht nicht das Schönste, aber es funktioniert.

Üben. Lasttests, um die Funktion des Pods zu überprüfen

Lasttests sind eine Möglichkeit, die Funktionsweise des Containers zu überprüfen, da er durch dieses Verfahren den realen Kampfbedingungen näher kommt, wenn Benutzer die Website besuchen. Um die oben genannten Empfehlungen zu testen, können Sie verwenden Yandex.Tankom: Es deckt alle unsere Bedürfnisse perfekt ab. Im Folgenden finden Sie Tipps und Empfehlungen zur Durchführung von Tests anhand eines klaren Beispiels aus unserer Erfahrung dank der Diagramme von Grafana und Yandex.Tank selbst.

Das Wichtigste hier ist Überprüfen Sie die Änderungen Schritt für Schritt. Nachdem Sie einen neuen Fix hinzugefügt haben, führen Sie den Test aus und prüfen Sie, ob sich die Ergebnisse im Vergleich zum letzten Lauf geändert haben. Andernfalls wird es schwierig, ineffektive Lösungen zu identifizieren, und auf lange Sicht kann es nur schaden (z. B. die Bereitstellungszeit verlängern).

Eine weitere Nuance besteht darin, sich die Containerprotokolle während der Beendigung anzusehen. Werden dort Informationen zum ordnungsgemäßen Herunterfahren aufgezeichnet? Gibt es Fehler in den Protokollen beim Zugriff auf andere Ressourcen (z. B. auf einen benachbarten PHP-FPM-Container)? Fehler in der Anwendung selbst (wie im oben beschriebenen Fall bei NGINX)? Ich hoffe, dass Ihnen die einleitenden Informationen aus diesem Artikel dabei helfen, besser zu verstehen, was mit dem Container während seiner Beendigung passiert.

Der erste Testlauf fand also ohne statt lifecycle und ohne zusätzliche Anweisungen für den Anwendungsserver (process_control_timeout in PHP-FPM). Der Zweck dieses Tests bestand darin, die ungefähre Anzahl der Fehler zu ermitteln (und festzustellen, ob es welche gibt). Aus zusätzlichen Informationen sollten Sie außerdem wissen, dass die durchschnittliche Bereitstellungszeit für jeden Pod etwa 5–10 Sekunden betrug, bis er vollständig bereit war. Die Ergebnisse sind:

Tipps und Tricks zu Kubernetes: Funktionen für das ordnungsgemäße Herunterfahren in NGINX und PHP-FPM

Das Yandex.Tank-Informationsfeld zeigt einen Anstieg von 502 Fehlern, die zum Zeitpunkt der Bereitstellung auftraten und im Durchschnitt bis zu 5 Sekunden anhielten. Vermutlich lag das daran, dass bestehende Anfragen an den alten Pod beendet wurden, als dieser beendet wurde. Danach traten 503 Fehler auf, was auf einen gestoppten NGINX-Container zurückzuführen war, der aufgrund des Backends auch Verbindungen abbrach (was Ingress daran hinderte, eine Verbindung zu ihm herzustellen).

Mal sehen, wie process_control_timeout in PHP-FPM hilft uns, auf den Abschluss untergeordneter Prozesse zu warten, d. h. Korrigieren Sie solche Fehler. Führen Sie die erneute Bereitstellung mithilfe dieser Anweisung durch:

Tipps und Tricks zu Kubernetes: Funktionen für das ordnungsgemäße Herunterfahren in NGINX und PHP-FPM

Beim 500. Einsatz gibt es keine Fehler mehr! Die Bereitstellung ist erfolgreich, das ordnungsgemäße Herunterfahren funktioniert.

Es lohnt sich jedoch, sich an das Problem mit Ingress-Containern zu erinnern, bei denen wir möglicherweise aufgrund einer Zeitverzögerung einen kleinen Prozentsatz an Fehlern erhalten. Um sie zu vermeiden, muss nur noch eine Struktur hinzugefügt werden sleep und wiederholen Sie die Bereitstellung. In unserem speziellen Fall waren jedoch keine Änderungen sichtbar (auch hier keine Fehler).

Abschluss

Um den Prozess ordnungsgemäß zu beenden, erwarten wir von der Anwendung das folgende Verhalten:

  1. Warten Sie einige Sekunden und akzeptieren Sie dann keine neuen Verbindungen mehr.
  2. Warten Sie, bis alle Anforderungen abgeschlossen sind, und schließen Sie alle Keepalive-Verbindungen, die keine Anforderungen ausführen.
  3. Beenden Sie Ihren Prozess.

Allerdings können nicht alle Anwendungen auf diese Weise funktionieren. Eine Lösung für das Problem in der Kubernetes-Realität ist:

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

Das Beispiel mit NGINX macht deutlich, dass selbst eine Anwendung, die ursprünglich Beendigungssignale verarbeiten sollte, dies möglicherweise nicht richtig verarbeiten kann. Daher ist es wichtig, während der Anwendungsbereitstellung auf 500 Fehler zu prüfen. Dadurch können Sie das Problem auch umfassender betrachten und sich nicht auf einen einzelnen Pod oder Container konzentrieren, sondern die gesamte Infrastruktur als Ganzes betrachten.

Als Testtool können Sie Yandex.Tank in Verbindung mit jedem Überwachungssystem verwenden (in unserem Fall wurden für den Test Daten aus Grafana mit einem Prometheus-Backend übernommen). Probleme mit dem ordnungsgemäßen Herunterfahren sind bei starker Belastung, die der Benchmark erzeugen kann, deutlich sichtbar, und die Überwachung hilft, die Situation während oder nach dem Test detaillierter zu analysieren.

Als Antwort auf das Feedback zum Artikel: Erwähnenswert ist, dass hier die Probleme und Lösungen in Bezug auf NGINX Ingress beschrieben werden. Für andere Fälle gibt es andere Lösungen, die wir in den folgenden Materialien der Serie betrachten können.

PS

Weiteres aus der K8s-Tipps- und Trickserie:

Source: habr.com

Kommentar hinzufügen