![Search icon](https://hackernoon.imgix.net/search-new.png?w=19&h=19)
Haben Sie schon einmal aus Frust das Netzkabel aus Ihrem Computer gerissen? Das mag zwar wie eine schnelle Lösung erscheinen, kann aber zu Datenverlust und Systeminstabilität führen. In der Softwarewelt gibt es ein ähnliches Konzept: das harte Herunterfahren. Dieses abrupte Beenden kann genau wie sein physisches Gegenstück Probleme verursachen. Zum Glück gibt es eine bessere Möglichkeit: das ordnungsgemäße Herunterfahren.
Durch die Integration eines ordnungsgemäßen Herunterfahrens benachrichtigen wir den Dienst im Voraus. Dadurch kann er laufende Anfragen abschließen, Statusinformationen ggf. auf der Festplatte speichern und letztendlich Datenbeschädigungen beim Herunterfahren vermeiden.
In diesem Handbuch geht es um die Welt des ordnungsgemäßen Herunterfahrens, wobei der Schwerpunkt insbesondere auf der Implementierung in Go-Anwendungen liegt, die auf Kubernetes ausgeführt werden.
Eines der wichtigsten Tools zum Erreichen eines reibungslosen Herunterfahrens in Unix-basierten Systemen ist das Konzept der Signale. Vereinfacht ausgedrückt handelt es sich dabei um eine einfache Möglichkeit, einem Prozess eine bestimmte Sache von einem anderen Prozess aus mitzuteilen. Wenn wir verstehen, wie Signale funktionieren, können wir sie nutzen, um kontrollierte Beendigungsverfahren in unseren Anwendungen zu implementieren und so einen reibungslosen und datensicheren Herunterfahrvorgang sicherzustellen.
Es gibt viele Signale und Sie können sie hier finden, aber uns interessieren nur die Abschaltsignale:
Diese Signale können vom Benutzer (Strg+C / Strg+D), von einem anderen Programm/Prozess oder vom System selbst (Kernel / Betriebssystem) gesendet werden. Beispielsweise wird ein SIGSEGV (auch bekannt als Segmentierungsfehler) vom Betriebssystem gesendet.
Um die Welt der ordnungsgemäßen Herunterfahren in einer praktischen Umgebung zu erkunden, erstellen wir einen einfachen Dienst, mit dem wir experimentieren können. Dieser „Versuchskaninchen“-Dienst verfügt über einen einzelnen Endpunkt, der einige reale Aufgaben simuliert (wir fügen eine leichte Verzögerung hinzu), indem er den INCR -Befehl von Redis aufruft. Wir stellen auch eine grundlegende Kubernetes-Konfiguration bereit, um zu testen, wie die Plattform mit Beendigungssignalen umgeht.
Das ultimative Ziel: sicherzustellen, dass unser Dienst Herunterfahren ordnungsgemäß bewältigt, ohne dass Anfragen/Daten verloren gehen. Indem wir die Anzahl der parallel gesendeten Anfragen mit dem endgültigen Zählerwert in Redis vergleichen, können wir überprüfen, ob unsere Implementierung des ordnungsgemäßen Herunterfahrens erfolgreich ist.
Wir werden nicht näher auf die Einrichtung des Kubernetes-Clusters und von Redis eingehen, aber Sie finden das vollständige Setup in unserem Github-Repository .
Der Verifizierungsprozess läuft wie folgt ab:
Beginnen wir mit unserem grundlegenden Go HTTP-Server.
Hard-Shutdown/main.go
package main import ( "net/http" "os" "time" "github.com/go-redis/redis" ) func main() { redisdb := redis.NewClient(&redis.Options{ Addr: os.Getenv("REDIS_ADDR"), }) server := http.Server{ Addr: ":8080", } http.HandleFunc("/incr", func(w http.ResponseWriter, r *http.Request) { go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) server.ListenAndServe() } func processRequest(redisdb *redis.Client) { // simulate some business logic here time.Sleep(time.Second * 5) redisdb.Incr("counter") }
Wenn wir unser Überprüfungsverfahren mit diesem Code ausführen, sehen wir, dass einige Anfragen fehlschlagen und der Zähler unter 1000 liegt (die Zahl kann bei jedem Durchlauf variieren).
Das bedeutet eindeutig, dass wir während des Rolling Updates einige Daten verloren haben. 😢
Go bietet ein Signalpaket , mit dem Sie Unix-Signale verarbeiten können. Es ist wichtig zu beachten, dass SIGINT- und SIGTERM-Signale standardmäßig dazu führen, dass das Go-Programm beendet wird. Und damit unsere Go-Anwendung nicht so abrupt beendet wird, müssen wir eingehende Signale verarbeiten.
Hierzu gibt es zwei Möglichkeiten.
Kanal verwenden:
c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGTERM)
Kontext verwenden (heutzutage bevorzugter Ansatz):
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop()
NotifyContext gibt eine Kopie des übergeordneten Kontexts zurück, die als „erledigt“ markiert ist (sein „Erledigt“-Kanal ist geschlossen), wenn eines der aufgelisteten Signale eintrifft, wenn die zurückgegebene Funktion stop() aufgerufen wird oder wenn der „Erledigt“-Kanal des übergeordneten Kontexts geschlossen wird, je nachdem, was zuerst eintritt.
Es gibt einige Probleme mit unserer aktuellen Implementierung des HTTP-Servers:
Lass es uns neu schreiben.
ordnungsgemäßes Herunterfahren/main.go
package main // imports var wg sync.WaitGroup func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop() // redisdb, server http.HandleFunc("/incr", func(w http.ResponseWriter, r *http.Request) { wg.Add(1) go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) // make it a goroutine go server.ListenAndServe() // listen for the interrupt signal <-ctx.Done() // stop the server if err := server.Shutdown(context.Background()); err != nil { log.Fatalf("could not shutdown: %v\n", err) } // wait for all goroutines to finish wg.Wait() // close redis connection redisdb.Close() os.Exit(0) } func processRequest(redisdb *redis.Client) { defer wg.Done() // simulate some business logic here time.Sleep(time.Second * 5) redisdb.Incr("counter") }
Hier ist die Zusammenfassung der Updates:
Wenn wir nun unseren Verifizierungsprozess wiederholen, werden wir sehen, dass alle 1000 Anfragen korrekt verarbeitet wurden. 🎉
Frameworks wie Echo, Gin, Fiber und andere erzeugen für jede eingehende Anfrage eine Goroutine, geben ihr einen Kontext und rufen dann Ihre Funktion/Ihren Handler auf, je nachdem, für welches Routing Sie sich entschieden haben. In unserem Fall wäre das die anonyme Funktion, die HandleFunc für den Pfad „/incr“ gegeben wird.
Wenn Sie ein SIGTERM- Signal abfangen und Ihr Framework auffordern, ordnungsgemäß herunterzufahren, passieren (vereinfacht ausgedrückt) zwei wichtige Dinge:
Hinweis: Kubernetes beendet auch die Weiterleitung des eingehenden Datenverkehrs vom Loadbalancer zu Ihrem Pod, sobald es diesen als „Wird beendet“ gekennzeichnet hat.
Das Beenden eines Prozesses kann komplex sein, insbesondere wenn viele Schritte erforderlich sind, wie z. B. das Schließen von Verbindungen. Um einen reibungslosen Ablauf zu gewährleisten, können Sie ein Timeout festlegen. Dieses Timeout fungiert als Sicherheitsnetz und beendet den Prozess ordnungsgemäß, wenn er länger als erwartet dauert.
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() go func() { if err := server.Shutdown(shutdownCtx); err != nil { log.Fatalf("could not shutdown: %v\n", err) } }() select { case <-shutdownCtx.Done(): if shutdownCtx.Err() == context.DeadlineExceeded { log.Fatalln("timeout exceeded, forcing shutdown") } os.Exit(0) }
Da wir Kubernetes zur Bereitstellung unseres Dienstes verwendet haben, wollen wir uns genauer ansehen, wie Kubernetes die Pods beendet. Sobald Kubernetes beschließt, den Pod zu beenden, finden die folgenden Ereignisse statt:
Wie Sie sehen, kann es bei einem Beendigungsprozess mit langer Ausführungsdauer erforderlich sein, die Einstellung „terminationGracePeriodSeconds“ zu erhöhen, um Ihrer Anwendung ausreichend Zeit für ein ordnungsgemäßes Herunterfahren zu geben.
Ordentliches Herunterfahren schützt die Datenintegrität, sorgt für ein nahtloses Benutzererlebnis und optimiert die Ressourcenverwaltung. Mit seiner umfangreichen Standardbibliothek und dem Schwerpunkt auf Parallelität ermöglicht Go Entwicklern die mühelose Integration von Verfahren für ordnungsgemäßes Herunterfahren – eine Notwendigkeit für Anwendungen, die in containerisierten oder orchestrierten Umgebungen wie Kubernetes bereitgestellt werden.
Den Go-Code und die Kubernetes-Manifeste finden Sie in unserem Github-Repository .