paint-brush
Ordentliches Herunterfahren in Go meistern: Ein umfassender Leitfaden für Kubernetesvon@gopher

Ordentliches Herunterfahren in Go meistern: Ein umfassender Leitfaden für Kubernetes

von Alex6m2024/08/14
Read on Terminal Reader

Zu lang; Lesen

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.
featured image - Ordentliches Herunterfahren in Go meistern: Ein umfassender Leitfaden für Kubernetes
Alex HackerNoon profile picture

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.

Signale in Unix-Systemen

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:

  • SIGTERM – wird an einen Prozess gesendet, um dessen Beendigung anzufordern. Wird am häufigsten verwendet und wir werden uns später darauf konzentrieren.
  • SIGKILL – „sofort beenden“, kann nicht beeinflusst werden.
  • SIGINT - Unterbrechungssignal (z. B. Strg+C)
  • SIGQUIT - Beendigungssignal (z. B. Strg+D)


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.


Unser Meerschweinchen-Service

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:

  1. Stellen Sie Redis- und Go-Anwendungen in Kubernetes bereit.
  2. Verwenden Sie Vegeta , um 1000 Anfragen zu senden (25/s über 40 Sekunden).
  3. Initialisieren Sie, während Vegeta ausgeführt wird, ein Kubernetes Rolling Update, indem Sie den Image-Tag aktualisieren.
  4. Stellen Sie eine Verbindung zu Redis her, um den „Zähler“ zu überprüfen. Er sollte 1000 betragen.


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. 😢

Signalbehandlung in Go

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:

  1. Wir haben eine langsame ProcessRequest-Goroutine, und da wir das Beendigungssignal nicht behandeln, wird das Programm automatisch beendet, was bedeutet, dass alle laufenden Goroutinen ebenfalls beendet werden.
  2. Das Programm schließt keine Verbindungen.


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:

  • signal.NotifyContext hinzugefügt, um auf das SIGTERM-Beendigungssignal zu warten.
  • Einführung einer sync.WaitGroup zur Verfolgung laufender Anfragen (processRequest-Goroutinen).
  • Habe den Server in eine Goroutine gepackt und „server.Shutdown “ mit Kontext verwendet, um die Annahme neuer Verbindungen ordnungsgemäß zu beenden.
  • Verwendet wurde wg.Wait(), um sicherzustellen, dass alle laufenden Anfragen (ProcessRequest-Goroutinen) abgeschlossen werden, bevor fortgefahren wird.
  • Ressourcenbereinigung: redisdb.Close() hinzugefügt, um die Redis-Verbindung vor dem Beenden ordnungsgemäß zu schließen.
  • Sauberer Ausgang: Verwendet os.Exit(0), um eine erfolgreiche Beendigung anzuzeigen.

Wenn wir nun unseren Verifizierungsprozess wiederholen, werden wir sehen, dass alle 1000 Anfragen korrekt verarbeitet wurden. 🎉


Web-Frameworks/HTTP-Bibliothek

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:

  • Ihr Framework akzeptiert keine eingehenden Anfragen mehr
  • Es wartet, bis alle vorhandenen eingehenden Anfragen abgeschlossen sind (und wartet implizit auf das Ende der Goroutinen).


Hinweis: Kubernetes beendet auch die Weiterleitung des eingehenden Datenverkehrs vom Loadbalancer zu Ihrem Pod, sobald es diesen als „Wird beendet“ gekennzeichnet hat.

Optional: Shutdown-Timeout

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) }

Kubernetes-Beendigungslebenszyklus

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:

  1. Pod wird auf den Status „Wird beendet“ gesetzt und aus der Endpunktliste aller Dienste entfernt.
  2. Der preStop- Hook wird ausgeführt, falls definiert.
  3. Das SIGTERM- Signal wird an den Pod gesendet. Aber hey, jetzt weiß unsere Anwendung, was zu tun ist!
  4. Kubernetes wartet eine Nachfrist ( terminationGracePeriodSeconds ), die standardmäßig 30 Sekunden beträgt.
  5. Das SIGKILL- Signal wird an den Pod gesendet und der Pod wird entfernt.

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.

Abschluss

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 .

Ressourcen