Typische Bedingung bei der Implementierung von CI/CD in Kubernetes: Die Anwendung muss in der Lage sein, vor der vollständigen Stilllegung keine neuen Kundenanfragen anzunehmen und, was noch wichtiger ist, bereits bestehende Anfragen erfolgreich abzuschließen.

Die Einhaltung dieser Bedingung ermöglicht es, eine nullzeitliche Ausfallzeit während des Deployments zu erreichen. Allerdings kann man auch bei der Verwendung sehr beliebter Kombinationen (wie NGINX und PHP-FPM) auf Schwierigkeiten stoßen, die bei jedem Deployment zu einem Anstieg der Fehler führen können…
Theorie. Wie ein Pod lebt
Wir haben bereits ausführlich über den Lebenszyklus eines Pods veröffentlicht. Im Kontext des betrachteten Themas interessiert uns Folgendes: In dem Moment, in dem der Pod in den Zustand Terminating, hört man auf, neue Anfragen an ihn zu senden (der Pod aus der Endpunktliste für den Dienst entfernt). Somit müssen wir, um Ausfallzeiten während des Deployments zu vermeiden, das Problem der ordnungsgemäßen Stilllegung der Anwendung lösen.
Es ist auch wichtig zu beachten, dass die Grace-Periode standardmäßig : Nachdem diese Zeit abgelaufen ist, wird der Pod terminiert, und die Anwendung muss alle Anfragen bis zu diesem Zeitpunkt bearbeitet haben. Hinweis: Obwohl jede Anfrage, die länger als 5-10 Sekunden dauert, bereits problematisch ist und eine sanfte Abschaltung nicht mehr helfen kann…
Um besser zu verstehen, was passiert, wenn ein Pod seine Arbeit beendet, genügt es, das folgende Schema zu betrachten:

A1, B1 — Empfang von Änderungen zum Zustand des Pods
A2 — Senden von SIGTERM
B2 — Entfernen des Pods aus den Endpunkten
B3 — Empfang von Änderungen (die Liste der Endpunkte hat sich geändert)
B4 — Aktualisierung der iptables-Regeln
Bitte beachten: Das Entfernen des Endpunkts des Pods und das Senden von SIGTERM erfolgen nicht hintereinander, sondern parallel. Und da Ingress die aktualisierte Liste der Endpunkte nicht sofort erhält, werden dem Pod weiterhin neue Anfragen von Clients gesendet, was während der Terminierung des Pods zu 500-Fehlern führen wird. (Ausführlicheres Material zu diesem Thema haben wir ). Diese Probleme sollten auf folgende Weise gelöst werden:
- Im Antwort-Header Connection: close senden (wenn es sich um eine HTTP-Anwendung handelt).
- Wenn es nicht möglich ist, Änderungen am Code vorzunehmen, wird in dem folgenden Artikel eine Lösung beschrieben, die es ermöglicht, die Anfragen bis zum Ende der Grace-Periode zu bearbeiten.
Theorie. Wie NGINX und PHP-FPM ihre Prozesse beenden
NGINX
Lass uns mit NGINX beginnen, da dies mehr oder weniger offensichtlich ist. Wenn wir in die Theorie eintauchen, erfahren wir, dass NGINX einen Master-Prozess und mehrere „Worker“ hat – das sind die Kindprozesse, die die Client-Anfragen bearbeiten. Eine praktische Möglichkeit ist vorgesehen: mit Hilfe des Befehls nginx -s kann man Prozesse entweder im Modus des schnellen Herunterfahrens oder im sanften Herunterfahren beenden. Offensichtlich interessiert uns genau die letztere Option.
Dann ist alles einfach: wir müssen in den einen Befehl hinzufügen, der ein Signal für ein sanftes Herunterfahren sendet. Das kann im Deployment im Containerblock gemacht werden:
lifecycle:
preStop:
exec:
command:
- /usr/sbin/nginx
- -s
- quitJetzt werden wir beim Beenden des Pods in den Logs des NGINX-Containers Folgendes sehen:
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 wird bedeuten, was wir brauchen: NGINX wartet darauf, dass Anfragen abgeschlossen werden, bevor der Prozess beendet wird. Übrigens wird im Folgenden auch ein häufiges Problem behandelt, bei dem selbst bei Vorhandensein des Befehls nginx -s quit der Prozess nicht korrekt beendet wird.
An diesem Punkt haben wir NGINX abgehakt: zumindest aus den Logs ist ersichtlich, dass alles funktioniert, wie es soll.
Wie sieht es mit PHP-FPM aus? Wie verarbeitet es das sanfte Herunterfahren? Lassen Sie uns das herausfinden.
PHP-FPM
Im Falle von PHP-FPM gibt es etwas weniger Informationen. Wenn wir auf das von PHP-FPM schauen, erfahren wir, dass die folgenden POSIX-Signale akzeptiert werden:
-
SIGINT,SIGTERM— schnelles Herunterfahren; -
SIGQUIT— sanftes Herunterfahren (das brauchen wir).
Die anderen Signale sind für diese Aufgabe nicht nötig, daher lassen wir deren Analyse weg. Für ein korrektes Beenden des Prozesses muss der folgende preStop-Hook geschrieben werden:
lifecycle:
preStop:
exec:
command:
- /bin/kill
- -SIGQUIT
- "1"Auf den ersten Blick ist das alles, was für ein sanftes Herunterfahren in beiden Containern erforderlich ist. Dennoch ist die Aufgabe komplizierter, als es scheint. Im Folgenden werden zwei Fälle behandelt, in denen das sanfte Herunterfahren nicht funktionierte und während des Deployments zu einer vorübergehenden Unzugänglichkeit des Projektes führte.
Praxis. Mögliche Probleme mit dem sanften Herunterfahren
NGINX
Zunächst ist es hilfreich, sich daran zu erinnern: neben der Ausführung des Befehls nginx -s quit Es gibt noch einen weiteren Punkt, auf den man achten sollte. Wir hatten das Problem, dass NGINX anstelle des Signals SIGQUIT dennoch SIGTERM gesendet hat, wodurch die Anfragen nicht korrekt abgeschlossen wurden. Ähnliche Fälle sind zum Beispiel zu finden, . Leider konnten wir den spezifischen Grund für dieses Verhalten nicht bestimmen: Es gab den Verdacht, dass es an der NGINX-Version liegt, aber dieser bestätigte sich nicht. Die Symptome bestanden darin, dass in den Logs des NGINX-Containers folgende Nachrichten auftauchten, „open socket #10 left in connection 5“, woraufhin der Pod gestoppt wurde.
Wir können ein solches Problem zum Beispiel anhand der Antworten auf dem gewünschten Ingress beobachten:

Statuscode-Zahlen 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 in die Logs des NGINX-Containers schauen, finden wir Folgendes:
[alert] 13939#0: *154 open socket #3 left in connection 16
[alert] 13939#0: *168 open socket #6 left in connection 13Nach der Änderung des Stop-Signals beginnt der Container, korrekt gestoppt zu werden: Das wird dadurch bestätigt, dass kein 503-Fehler mehr auftritt.
Wenn Sie auf ein ähnliches Problem stoßen, lohnt es sich herauszufinden, welches Stop-Signal im Container verwendet wird und wie genau der preStop-Hook aussieht. Es ist durchaus möglich, dass die Ursache genau darin liegt.
PHP-FPM… und nicht nur
Das Problem mit PHP-FPM beschreibt sich trivial: Es wartet nicht auf den Abschluss der Unterprozesse und terminiert sie, wodurch während des Deployments und anderer Operationen 502-Fehler entstehen. Auf bugs.php.net gibt es seit 2005 mehrere Fehlerberichte (zum Beispiel, und ), in denen dieses Problem beschrieben wird. In den Logs werden Sie höchstwahrscheinlich nichts sehen: PHP-FPM meldet das Ende seines Prozesses ohne Fehler oder andere Benachrichtigungen.
Es ist zu beachten, dass das Problem in unterschiedlichem Maße von der Anwendung selbst abhängt und beispielsweise in der Überwachung möglicherweise nicht sichtbar ist. Wenn Sie dennoch darauf stoßen, kommt einem zunächst eine einfache Übergangslösung in den Sinn: einen preStop-Hook mit sleep(30). Er ermöglicht es, alle vorhergehenden Anfragen abzuschließen (und neue werden nicht angenommen, da sich der Pod bereits im Zustand Terminating), und nach Ablauf von 30 Sekunden wird der Pod selbst mit dem Signal beendet. SIGTERM.
Es stellt sich heraus, dass Der lifecycle für den Container würde folgendermaßen aussehen:
lifecycle:
preStop:
exec:
command:
- /bin/sleep
- "30" Allerdings könnte die Angabe von 30 Sekunden sleep haben wir stark Wir werden die Bereitstellungszeit erhöhen, da jeder Pod beendet wird. Mindestsatz 30 Sekunden, was schlecht ist. Was kann man dagegen tun?
Wir wenden uns an die Seite, die für die direkte Ausführung der Anwendung verantwortlich ist. In unserem Fall ist das PHP-FPM, der standardmäßig nicht für die Ausführung 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 angibt. Wenn Sie den Wert auf 20 Sekunden einstellen, werden die meisten Anfragen abgedeckt, die im Container ausgeführt werden, und nach deren Abschluss wird der Masterprozess gestoppt.
Mit diesem Wissen kehren wir zu unserem letzten Problem zurück. Wie bereits erwähnt, ist Kubernetes keine monolithische Plattform: Die Interaktion zwischen ihren verschiedenen Komponenten benötigt einige Zeit. Dies ist besonders relevant, wenn wir die Funktionsweise von Ingress und anderen verknüpften Komponenten betrachten, da durch solche Verzögerungen während der Bereitstellung leicht ein Anstieg von 500-Fehlern auftreten kann. Zum Beispiel kann der Fehler auf der Stufe der Anfrage an das Upstream auftreten, aber der „zeitliche Rückstand“ in der Interaktion zwischen den Komponenten ist ziemlich kurz — weniger als eine Sekunde.
Deshalb insgesamt kann man mit der bereits genannten Direktive process_control_timeout die folgende Konstruktion verwenden für Der lifecycle:
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 die Bereitstellungszeit nicht erheblich: denn ist der Unterschied zwischen 30 Sekunden und einer bemerkbar?.. Eigentlich übernimmt genau das die "Hauptarbeit" process_control_timeout, und Der lifecycle wird nur als "Sicherheitsmaßnahme" für den Fall eines Lags verwendet.
Im Allgemeinen gilt, dass das beschriebene Verhalten und die entsprechende Lösung nicht nur PHP-FPM betreffen.Eine ähnliche Situation kann auch bei der Verwendung anderer Programmiersprachen/Frameworks auftreten. Wenn es nicht möglich ist, das graceful shutdown auf andere Weise zu beheben — zum Beispiel, indem der Code so umgeschrieben wird, dass die Anwendung Signale zum Beenden korrekt verarbeitet — kann die beschriebene Methode angewendet werden. Auch wenn sie nicht die schönste ist, funktioniert sie.
Praxis. Lasttests zur Überprüfung der Funktionsweise des Pods.
Lasttest ist eine Methode zur Überprüfung, wie ein Container funktioniert, da dieses Verfahren realistischen Bedingungen nahekommt, wenn Benutzer auf die Website zugreifen. Um die oben genannten Empfehlungen zu testen, kann man : es deckt all unsere Bedürfnisse hervorragend ab. Im Folgenden finden Sie Tipps und Empfehlungen zur Durchführung von Tests mit anschaulichen – dank der Grafiken von Grafana und Yandex.Tank – Beispielen aus unserer Erfahrung.
Das Wichtigste hier ist Änderungen schrittweise zu überprüfen. Nach dem Hinzufügen eines neuen Fixes sollten Sie die Tests starten und beobachten, ob sich die Ergebnisse im Vergleich zum vorherigen Durchlauf geändert haben. Andernfalls wird es schwierig sein, ineffiziente Lösungen zu identifizieren, und in der Zukunft könnte man sogar nur schaden (z.B. die Deploy-Zeit erhöhen).
Ein weiterer Aspekt – beobachten Sie die Logs des Containers während seiner Terminierung. Wird dort Informationen über einen sanften Shutdown festgehalten? Gibt es Fehler in den Logs beim Zugriff auf andere Ressourcen (z.B. auf einen benachbarten PHP-FPM-Container)? Fehler der Anwendung selbst (wie im zuvor beschriebenen Fall mit NGINX)? Ich hoffe, dass die Einführung in diesen Artikel hilft, besser zu verstehen, was mit dem Container während seiner Terminierung passiert.
Also, der erste Testlauf fand ohne Der lifecycle und ohne zusätzliche Direktiven für den Anwendungsserver (process_control_timeout in PHP-FPM) statt. Ziel dieses Tests war es, die ungefähre Anzahl von Fehlern zu identifizieren (und ob sie überhaupt existieren). Zudem sollten Sie aus den Zusatzinformationen wissen, dass die durchschnittliche Deploy-Zeit jedes Pods etwa 5-10 Sekunden bis zur vollen Betriebsbereitschaft betrug. Die Ergebnisse sind wie folgt:

Auf dem Dashboard von Yandex.Tank ist ein Anstieg von 502-Fehlern zu sehen, der während des Deployments auftrat und im Durchschnitt bis zu 5 Sekunden andauerte. Vermutlich wurden bestehende Anfragen an den alten Pod unterbrochen, als dieser terminierte. Danach traten 503-Fehler auf, die das Ergebnis des gestoppten NGINX-Containers waren, der ebenfalls Verbindungen aufgrund von Backend-Problemen abbrach (weshalb Ingress sich nicht verbinden konnte).
Lassen Sie uns sehen, wie process_control_timeout in PHP-FPM uns dabei helfen kann, auf den Abschluss von Child-Prozessen zu warten, d.h. solche Fehler zu beheben. Ein erneutes Deployment bereits unter Verwendung dieser Direktive:

Beim Deployment gibt es keine 500-Fehler mehr! Das Deployment verläuft erfolgreich, der graceful shutdown funktioniert.
Es ist jedoch wichtig, sich an den Moment mit den Ingress-Containern zu erinnern, bei denen wir einen kleinen Prozentsatz von Fehlern aufgrund vorübergehender Lags erhalten können. Um diese zu vermeiden, bleibt nur die Konstruktion mit sleep hinzuzufügen und das Deployment zu wiederholen. In unserem speziellen Fall waren jedoch keine Änderungen sichtbar (wieder keine Fehler).
Fazit
Für einen korrekten Abschluss des Prozesses erwarten wir von der Anwendung folgendes Verhalten:
- Einige Sekunden warten, dann aufhören, neue Verbindungen anzunehmen.
- Warten, bis alle Anfragen abgeschlossen sind, und alle keepalive-Verbindungen schließen, die keine Anfragen ausführen.
- Den eigenen Prozess beenden.
Nicht alle Anwendungen können jedoch so arbeiten. Eine Lösung für dieses Problem in Kubernetes ist:
- Hinzufügen eines pre-stop Hooks, der einige Sekunden warten wird;
- Überprüfung der Konfigurationsdatei unseres Backends auf entsprechende Parameter.
Das Beispiel mit NGINX zeigt, dass selbst eine Anwendung, die ursprünglich korrekt auf Abschaltsignale reagieren sollte, dies möglicherweise nicht tut. Daher ist es entscheidend, das Vorhandensein von 500-Fehlern während des Deployments der Anwendung zu überprüfen. Dies ermöglicht es auch, das Problem umfassender zu betrachten und sich nicht nur auf einen einzelnen Pod oder Container zu konzentrieren, sondern die gesamte Infrastruktur im Blick zu behalten.
Als Testwerkzeug kann Yandex.Tank in Verbindung mit jedem Überwachungssystem verwendet werden (in unserem Fall stammen die Testdaten aus Grafana mit Prometheus als Backend). Probleme mit graceful shutdown sind bei hohen Lasten, wie sie ein Benchmark erzeugen kann, deutlich sichtbar, und die Überwachung hilft, die Situation während oder nach dem Test detailliert zu analysieren.
In Bezug auf das Feedback zum Artikel ist zu erwähnen, dass die Probleme und Lösungen hier im Kontext von NGINX Ingress beschrieben werden. Für andere Fälle gibt es andere Lösungen, die wir möglicherweise in zukünftigen Beiträgen der Reihe behandeln werden.
P.S.
Noch etwas aus der Reihe K8s Tipps & Tricks:
- «»;
- «»;
- «»;
- «».
Quelle: habr.com
