- Graceful Shutdown (geordnetes Herunterfahren) besteht aus dem Blockieren neuer Anfragen, dem Abschließen laufender Anfragen und dem Aufräumen von Ressourcen, nachdem die Anwendung ein Beendigungssignal erhalten hat
- In Go lassen sich mit dem Paket
os/signal Beendigungssignale wie SIGINT und SIGTERM direkt verarbeiten; mit signal.NotifyContext ist auch eine kontextbasierte Steuerung des Shutdowns möglich
- Beim Beenden eines HTTP-Servers ist es stabiler, vor dem Aufruf von
Server.Shutdown() den Traffic über eine fehlschlagende Readiness-Probe zu blockieren und den Shutdown erst nach einigen Sekunden durchzuführen
- Alle Handler sollten das Beendigungssignal im Context erkennen und sauber abbrechen können; das lässt sich zentral über
BaseContext oder Middleware umsetzen
- Nach Empfang des Beendigungssignals sollten externe Ressourcen wie Datenbanken, Message Broker und Caches gezielt bereinigt werden; mit
defer lässt sich die Shutdown-Reihenfolge leichter steuern
Was ist Graceful Shutdown?
- Graceful Shutdown ist der Prozess, bei dem eine Anwendung beim Beenden neue Anfragen blockiert, auf den Abschluss laufender Anfragen wartet und Ressourcen bereinigt
- Dieser Artikel behandelt hauptsächlich HTTP-Server und Container-Umgebungen, das ist jedoch ein Konzept, das sich auf alle Anwendungen anwenden lässt
1. Beendigungssignale verarbeiten
- Auf Unix-artigen Systemen werden SIGTERM, SIGINT und SIGHUP als Beendigungssignale verwendet
- Die Go-Runtime beendet die Anwendung beim Empfang von
SIGTERM oder SIGINT standardmäßig, mit os/signal.Notify kann man diese Signale jedoch selbst verarbeiten
- Mit einem gepufferten Channel (Kapazität 1) lässt sich verhindern, dass Signale während der Initialisierung verloren gehen
- Seit Go 1.16 vereinfacht
signal.NotifyContext die signalgesteuerte Steuerung über Context
2. Die verfügbare Shutdown-Zeit berücksichtigen
- In Kubernetes gibt es standardmäßig eine Grace-Periode von 30 Sekunden für das Beenden (
terminationGracePeriodSeconds)
- Für ein sicheres Herunterfahren ist es sinnvoll, 20 % Reserve einzuplanen und die Beendigungsarbeiten innerhalb von 25 Sekunden abzuschließen
3. Keine neuen Anfragen mehr annehmen
http.Server.Shutdown() blockiert neue Verbindungen und wartet, bis bestehende Anfragen abgeschlossen sind
- In Kubernetes sollte man zunächst die Readiness-Probe fehlschlagen lassen, um den eingehenden Traffic zu stoppen, dann kurz warten und erst danach den Shutdown ausführen
- Im Readiness-Handler kann anhand einer globalen Variable der Shutdown-Status geprüft werden, um HTTP 503 zurückzugeben
4. Anfrageverarbeitung sauber abschließen
- Für den Shutdown-Context sollte ein geeignetes Timeout gesetzt werden (
context.WithTimeout)
- Wenn der Shutdown-Context abläuft, werden verbleibende Verbindungen zwangsweise beendet
- Alle Handler sollten mit
context.Context so entworfen sein, dass sie ein Beendigungssignal erkennen und ihre Arbeit abbrechen können
- Dazu kann man über Middleware oder
BaseContext den Shutdown-Context in alle Anfragen injizieren
5. Ressourcen aufräumen
- Werden Ressourcen sofort nach dem Empfang des Beendigungssignals geschlossen, kann das Probleme für noch laufende Handler verursachen
- Erst nach abgeschlossenem Shutdown sollten Datenbankverbindungen, Message Broker, Caches usw. bereinigt werden
- Mit Go-
defer lassen sich Shutdown-Routinen in umgekehrter Initialisierungsreihenfolge ausführen, was das Management von Abhängigkeiten erleichtert
- Neben Ressourcen wie Speicher oder File Descriptors, die das Betriebssystem automatisch aufräumt, gibt es auch Ressourcen, die ein explizites Beenden erfordern, etwa Daten-Flushes oder Transaction-Rollbacks
Zusammenfassung des vollständigen Beispiels
- Beendigungssignale mit
signal.NotifyContext empfangen
/healthz-Readiness-Endpunkt implementieren
- Mit
BaseContext den Shutdown-Context in alle Anfragen injizieren
- Nach 5 Sekunden Readiness-Wartezeit den Shutdown ausführen
- Fallback für erzwungenes Beenden enthalten, falls
server.Shutdown fehlschlägt
Referenzen und verwandte Ressourcen
1 Kommentare
Hacker-News-Kommentare
In Kubernetes dauert es manchmal lange, bis die Ziel-IP des Load Balancers aktualisiert wird. 90 % des Problems bestehen darin sicherzustellen, dass der Traffic tatsächlich gedraint wird.
preStop-Hook wurde die HTTP-503-Rate deutlich verbessert.SIGTERM, was die Verarbeitung in der Anwendung vereinfacht.Bei der Verwendung von
log.Fatalwird der Inhalt indefernicht ausgeführt.log.Fatalruftos.Exitauf und beendet das Programm sofort.panicwird derdefer-Inhalt ausgeführt.Wenn der Prometheus-Endpunkt
/metricsperiodisch gescraped wird, werden Metriken, die zwischen dem letzten Scrape und dem Beenden des Prozesses aufgezeichnet wurden, möglicherweise nicht weitergegeben.Wenn ein verteiltes System auf einen sauberen Shutdown des Clients angewiesen ist, kann das System schwerwiegend ausfallen.
Es fehlt eine Erklärung dazu, wie man eine Anwendung ohne Unterbrechung von Verbindungen neu startet, wenn eine neue Service-Instanz Sockets von der vorherigen Instanz übernimmt.
Es fehlt an einer Diskussion über Liveness.
Wenn ein Programm Befehle wie Ctrl-C nicht sauber verarbeiten kann, ist es schlecht geschrieben.
Elixir entwirft Prozesse als kleine VM-Prozesse, sodass bewusst keine Routinen für einen sauberen Shutdown nötig sind.
Es wurde eine kleine Bibliothek erstellt, um in Projekten einen sauberen Shutdown zu behandeln.
Nach dem Aktualisieren der Readiness-Probe sollte man einige Sekunden warten, damit das System keine neuen Anfragen mehr sendet.
SIGTERMkann es noch ein kleines Zeitfenster geben, aber das ist kein großes Problem.