3 Punkte von GN⁺ 2025-05-06 | 1 Kommentare | Auf WhatsApp teilen
  • 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

 
GN⁺ 2025-05-06
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.

    • Durch das Hinzufügen einer Wartezeit von 15 Sekunden zum globalen preStop-Hook wurde die HTTP-503-Rate deutlich verbessert.
    • Dadurch entsteht Zeit zwischen der Deregistrierung beim Load Balancer und der Zustellung von SIGTERM, was die Verarbeitung in der Anwendung vereinfacht.
  • Bei der Verwendung von log.Fatal wird der Inhalt in defer nicht ausgeführt.

    • log.Fatal ruft os.Exit auf und beendet das Programm sofort.
    • Bei Verwendung von panic wird der defer-Inhalt ausgeführt.
  • Wenn der Prometheus-Endpunkt /metrics periodisch gescraped wird, werden Metriken, die zwischen dem letzten Scrape und dem Beenden des Prozesses aufgezeichnet wurden, möglicherweise nicht weitergegeben.

    • Beim Beenden eines Dienstes können die Logs der letzten Sekunden verloren gehen.
    • Wenn eine Logdatei von einem Sidecar-Prozess überwacht wird, kann es zu einer Race Condition kommen.
  • 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.

    • Unter systemd ist das vergleichsweise einfach umzusetzen.
    • nginx unterstützt dies seit über 20 Jahren.
    • Kubernetes und Docker unterstützen dies nicht.
  • Es fehlt an einer Diskussion über Liveness.

    • Es wurden mehrfach Apps gesehen, die denselben Endpunkt für Liveness und Readiness verwenden.
  • 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.

    • Sie bietet eine API, um Services mit unterschiedlichen Start- und Shutdown-Mechanismen zu vereinheitlichen.
  • Nach dem Aktualisieren der Readiness-Probe sollte man einige Sekunden warten, damit das System keine neuen Anfragen mehr sendet.

    • Ein Pod, der beendet wird, ist nicht bereit.
    • Der Service markiert den Endpunkt als wird beendet.
    • Auch nach SIGTERM kann es noch ein kleines Zeitfenster geben, aber das ist kein großes Problem.
    • Wichtig ist, keine neuen Verbindungen anzunehmen und bestehende Verbindungen sauber zu beenden.