Heim >Backend-Entwicklung >Golang >Leistungsschalter in Go: Stoppen Sie kaskadierende Ausfälle

Leistungsschalter in Go: Stoppen Sie kaskadierende Ausfälle

WBOY
WBOYOriginal
2024-07-17 17:41:111009Durchsuche

Circuit Breakers in Go: Stop Cascading Failures

Leistungsschalter

Ein Leistungsschalter erkennt Ausfälle und kapselt die Logik zur Behandlung dieser Ausfälle so ein, dass verhindert wird, dass der Fehler ständig erneut auftritt. Sie sind beispielsweise nützlich, wenn Sie Netzwerkaufrufe an externe Dienste, Datenbanken oder eigentlich jeden Teil Ihres Systems verarbeiten, der vorübergehend ausfallen könnte. Durch den Einsatz eines Leistungsschalters können Sie kaskadierende Ausfälle verhindern, vorübergehende Fehler verwalten und bei einem Systemausfall ein stabiles und reaktionsfähiges System aufrechterhalten.

Kaskadierende Fehler

Kaskadierende Ausfälle treten auf, wenn ein Ausfall in einem Teil des Systems Ausfälle in anderen Teilen auslöst, was zu weitreichenden Störungen führt. Ein Beispiel wäre, wenn ein Mikrodienst in einem verteilten System nicht mehr reagiert, was dazu führt, dass abhängige Dienste eine Zeitüberschreitung erleiden und schließlich ausfallen. Abhängig vom Umfang der Anwendung können die Auswirkungen dieser Fehler katastrophal sein, was zu einer Verschlechterung der Leistung und wahrscheinlich sogar zu einer Beeinträchtigung des Benutzererlebnisses führt.

Leistungsschaltermuster

Ein Leistungsschalter selbst ist eine Technik/ein Muster und es gibt drei verschiedene Betriebszustände, über die wir sprechen werden:

  1. Geschlossener Zustand: Im geschlossenen Zustand ermöglicht der Leistungsschalter, dass alle Anforderungen wie gewohnt an den Zieldienst weitergeleitet werden. Sind die Anfragen erfolgreich, bleibt der Stromkreis geschlossen. Wenn jedoch eine bestimmte Fehlerschwelle erreicht wird, geht der Stromkreis in den offenen Zustand über. Stellen Sie sich das wie einen voll funktionsfähigen Dienst vor, bei dem sich Benutzer problemlos anmelden und auf Daten zugreifen können. Alles läuft reibungslos.

Circuit Breakers in Go: Stop Cascading Failures

2. Offener Zustand: Im offenen Zustand schlägt der Leistungsschalter alle eingehenden Anforderungen sofort fehl, ohne zu versuchen, den Zieldienst zu kontaktieren. Der Zustand wird eingegeben, um eine weitere Überlastung des ausgefallenen Dienstes zu verhindern und ihm Zeit zur Wiederherstellung zu geben. Nach einer vordefinierten Zeitspanne geht der Leistungsschalter in den halboffenen Zustand. Ein nachvollziehbares Beispiel ist dieses; Stellen Sie sich vor, in einem Online-Shop tritt plötzlich ein Problem auf, bei dem jeder Kaufversuch fehlschlägt. Um eine Überlastung des Systems zu vermeiden, nimmt das Geschäft vorübergehend keine neuen Kaufanfragen mehr an.

Circuit Breakers in Go: Stop Cascading Failures

3. Halboffener Zustand: Im halboffenen Zustand lässt der Leistungsschalter eine (konfigurierbare) begrenzte Anzahl von Testanforderungen an den Zieldienst durch. Und wenn diese Anfragen erfolgreich sind, geht der Schaltkreis wieder in den geschlossenen Zustand über. Wenn sie ausfallen, kehrt der Stromkreis in den offenen Zustand zurück. Im Beispiel des Online-Shops, den ich oben im geöffneten Zustand gegeben habe, beginnt der Online-Shop hier, einige Kaufversuche zuzulassen, um zu sehen, ob das Problem behoben wurde. Wenn diese wenigen Versuche erfolgreich sind, wird das Geschäft seinen Service wieder vollständig öffnen, um neue Kaufanfragen anzunehmen.

Dieses Diagramm zeigt, wenn der Leistungsschalter versucht zu sehen, ob Anforderungen an Dienst B erfolgreich sind, und dann scheitert/bricht er ab:

Circuit Breakers in Go: Stop Cascading Failures

Das Folgediagramm zeigt dann, wenn die Testanfragen an Dienst B erfolgreich sind, der Stromkreis geschlossen ist und alle weiteren Anrufe wieder an Dienst B weitergeleitet werden:

Circuit Breakers in Go: Stop Cascading Failures

Hinweis: Zu den wichtigsten Konfigurationen für einen Leistungsschalter gehören der Fehlerschwellenwert (Anzahl der Ausfälle, die zum Öffnen des Stromkreises erforderlich sind), das Timeout für den offenen Zustand und die Anzahl der Testanforderungen im halboffenen Zustand Zustand.

Implementierung von Leistungsschaltern in Go

Es ist wichtig zu erwähnen, dass Vorkenntnisse in Go erforderlich sind, um diesem Artikel folgen zu können.

Wie jedes Software-Engineering-Muster können Leistungsschalter in verschiedenen Sprachen implementiert werden. Dieser Artikel konzentriert sich jedoch auf die Implementierung in Golang. Für diesen Zweck stehen zwar mehrere Bibliotheken zur Verfügung, wie z. B. goresilience, go-resiliency und gobreaker, wir werden uns jedoch speziell auf die Verwendung der gobreaker-Bibliothek konzentrieren.

Profi-Tipp: Sie können die interne Implementierung des Gobreaker-Pakets hier sehen.

Betrachten wir eine einfache Golang-Anwendung, in der ein Leistungsschalter implementiert ist, um Aufrufe an eine externe API zu verarbeiten. Dieses einfache Beispiel zeigt, wie ein externer API-Aufruf mit der Circuit-Breaker-Technik umschlossen wird:

Lass uns auf ein paar wichtige Dinge eingehen:

  1. Die Funktion gobreaker.NewCircuitBreaker initialisiert den Leistungsschalter mit unseren benutzerdefinierten Einstellungen
  2. Die cb.Execute-Methode umschließt die HTTP-Anfrage und verwaltet automatisch den Schaltkreisstatus.
  3. MaximumRequests ist die maximale Anzahl von Anfragen, die passieren dürfen, wenn der Status halboffen ist
  4. Intervall ist die zyklische Periode des geschlossenen Zustands, in der der Leistungsschalter die internen Zählwerte löscht
  5. Timeout ist die Dauer vor dem Übergang vom offenen in den halboffenen Zustand.
  6. ReadyToTrip wird mit einer Kopie der Zählungen aufgerufen, wenn eine Anfrage im geschlossenen Zustand fehlschlägt. Wenn ReadyToTrip „true“ zurückgibt, wird der Leistungsschalter in den geöffneten Zustand versetzt. In unserem Fall hier wird true zurückgegeben, wenn Anfragen mehr als drei Mal hintereinander fehlgeschlagen sind.
  7. OnStateChange wird immer dann aufgerufen, wenn sich der Zustand des Leistungsschalters ändert. Normalerweise möchten Sie hier die Metriken der Zustandsänderung sammeln und diese an einen beliebigen Metriksammler Ihrer Wahl melden.

Lassen Sie uns einige Komponententests schreiben, um die Implementierung unseres Leistungsschalters zu überprüfen. Ich werde nur die kritischsten Komponententests erläutern, um sie zu verstehen. Den vollständigen Code finden Sie hier.

  1. Wir werden einen Test schreiben, der aufeinanderfolgende fehlgeschlagene Anforderungen simuliert und prüft, ob der Leistungsschalter in den offenen Zustand wechselt. Im Wesentlichen gehen wir davon aus, dass der Leistungsschalter nach drei Ausfällen auslöst (öffnet), wenn der vierte Fehler auftritt, da unser Zustand zählt. Aufeinanderfolgende Ausfälle > 3 . So sieht der Test aus:
 t.Run("FailedRequests", func(t *testing.T) {
         // Override callExternalAPI to simulate failure
         callExternalAPI = func() (int, error) {
             return 0, errors.New("simulated failure")
         }

         for i := 0; i < 4; i++ {
             _, err := cb.Execute(func() (interface{}, error) {
                 return callExternalAPI()
             })
             if err == nil {
                 t.Fatalf("expected error, got none")
             }
         }

         if cb.State() != gobreaker.StateOpen {
             t.Fatalf("expected circuit breaker to be open, got %v", cb.State())
         }
     })
  1. Wir werden die offene > halb - offen > geschlossene Zustände. Aber wir werden zunächst einen offenen Stromkreis simulieren und eine Auszeit aufrufen. Nach einer Zeitüberschreitung müssen wir mindestens eine Erfolgsanforderung stellen, damit die Schaltung in den halboffenen Zustand übergeht. Nach dem halboffenen Zustand müssen wir eine weitere Erfolgsanforderung stellen, damit der Stromkreis wieder vollständig geschlossen wird. Wenn aus irgendeinem Grund keine Erfolgsanfrage für den Fall vorliegt, wird der Fall wieder geöffnet. So sieht der Test aus:
     //Simulates the circuit breaker being open, 
     //wait for the defined timeout, 
     //then check if it closes again after a successful request.
         t.Run("RetryAfterTimeout", func(t *testing.T) {
             // Simulate circuit breaker opening
             callExternalAPI = func() (int, error) {
                 return 0, errors.New("simulated failure")
             }
    
             for i := 0; i < 4; i++ {
                 _, err := cb.Execute(func() (interface{}, error) {
                     return callExternalAPI()
                 })
                 if err == nil {
                     t.Fatalf("expected error, got none")
                 }
             }
    
             if cb.State() != gobreaker.StateOpen {
                 t.Fatalf("expected circuit breaker to be open, got %v", cb.State())
             }
    
             // Wait for timeout duration
             time.Sleep(settings.Timeout + 1*time.Second)
    
             //We expect that after the timeout period, 
             //the circuit breaker should transition to the half-open state. 
    
             // Restore original callExternalAPI to simulate success
             callExternalAPI = func() (int, error) {
                 resp, err := http.Get(server.URL)
                 if err != nil {
                     return 0, err
                 }
                 defer resp.Body.Close()
                 return resp.StatusCode, nil
             }
    
             _, err := cb.Execute(func() (interface{}, error) {
                 return callExternalAPI()
             })
             if err != nil {
                 t.Fatalf("expected no error, got %v", err)
             }
    
             if cb.State() != gobreaker.StateHalfOpen {
                 t.Fatalf("expected circuit breaker to be half-open, got %v", cb.State())
             }
    
             //After verifying the half-open state, another successful request is simulated to ensure the circuit breaker transitions back to the closed state.
             for i := 0; i < int(settings.MaxRequests); i++ {
                 _, err = cb.Execute(func() (interface{}, error) {
                     return callExternalAPI()
                 })
                 if err != nil {
                     t.Fatalf("expected no error, got %v", err)
                 }
             }
    
             if cb.State() != gobreaker.StateClosed {
                 t.Fatalf("expected circuit breaker to be closed, got %v", cb.State())
             }
         })
    
    1. Testen wir die ReadyToTrip-Bedingung, die nach zwei aufeinanderfolgenden Fehleranforderungen ausgelöst wird. Wir werden eine Variable haben, die aufeinanderfolgende Fehler verfolgt. Der ReadyToTrip-Rückruf wird aktualisiert, um zu prüfen, ob der Leistungsschalter nach zwei Ausfällen auslöst ( counts.ConsecutiveFailures > 2). Wir werden einen Test schreiben, der Ausfälle simuliert und die Anzahl überprüft sowie überprüft, ob der Leistungsschalter nach der angegebenen Anzahl von Ausfällen in den geöffneten Zustand übergeht.
       t.Run("ReadyToTrip", func(t *testing.T) {
               failures := 0
               settings.ReadyToTrip = func(counts gobreaker.Counts) bool {
                   failures = int(counts.ConsecutiveFailures)
                   return counts.ConsecutiveFailures > 2 // Trip after 2 failures
               }
      
               cb = gobreaker.NewCircuitBreaker(settings)
      
               // Simulate failures
               callExternalAPI = func() (int, error) {
                   return 0, errors.New("simulated failure")
               }
               for i := 0; i < 3; i++ {
                   _, err := cb.Execute(func() (interface{}, error) {
                       return callExternalAPI()
                   })
                   if err == nil {
                       t.Fatalf("expected error, got none")
                   }
               }
      
               if failures != 3 {
                   t.Fatalf("expected 3 consecutive failures, got %d", failures)
               }
               if cb.State() != gobreaker.StateOpen {
                   t.Fatalf("expected circuit breaker to be open, got %v", cb.State())
               }
           })
      

      Fortgeschrittene Strategien

      Wir können noch einen Schritt weiter gehen, indem wir unserer Leistungsschalterimplementierung eine exponentielle Backoff-Strategie hinzufügen. Wir werden diesen Artikel einfach und prägnant halten, indem wir ein Beispiel der exponentiellen Backoff-Strategie demonstrieren. Es gibt jedoch noch weitere erwähnenswerte fortgeschrittene Strategien für Leistungsschalter, wie z. B. Lastabwurf, Schottung, Fallback-Mechanismen, Kontext und Löschung. Diese Strategien verbessern grundsätzlich die Robustheit und Funktionalität von Leistungsschaltern. Hier ist ein Beispiel für die Verwendung der exponentiellen Backoff-Strategie:

      Exponentielles Backoff

      Leistungsschalter mit exponentiellem Backoff

      Lassen Sie uns ein paar Dinge klarstellen:

      Benutzerdefinierte Backoff-Funktion: Die exponentialBackoff-Funktion implementiert eine exponentielle Backoff-Strategie mit einem Jitter. Grundsätzlich wird die Backoff-Zeit basierend auf der Anzahl der Versuche berechnet und sichergestellt, dass die Verzögerung mit jedem Wiederholungsversuch exponentiell zunimmt.

      Verarbeitung von Wiederholungsversuchen: Wie Sie im /api-Handler sehen können, enthält die Logik jetzt eine Schleife, die versucht, die externe API bis zu einer bestimmten Anzahl von Versuchen aufzurufen (Versuche := 5). Nach jedem fehlgeschlagenen Versuch warten wir eine durch die exponentialBackoff-Funktion bestimmte Dauer ab, bevor wir es erneut versuchen.

      Ausführung des Leistungsschalters: Der Leistungsschalter wird innerhalb der Schleife verwendet. Wenn der externe API-Aufruf erfolgreich ist ( err == nil), wird die Schleife unterbrochen und das erfolgreiche Ergebnis zurückgegeben. Wenn alle Versuche fehlschlagen, wird ein HTTP 503-Fehler (Dienst nicht verfügbar) zurückgegeben.

      Die Integration einer benutzerdefinierten Backoff-Strategie in eine Leistungsschalterimplementierung zielt tatsächlich darauf ab, vorübergehende Fehler eleganter zu behandeln. Die zunehmenden Verzögerungen zwischen den Wiederholungsversuchen tragen dazu bei, die Belastung ausgefallener Dienste zu verringern und ihnen Zeit für die Wiederherstellung zu geben. Wie aus unserem obigen Code hervorgeht, wurde unsere exponentialBackoff-Funktion eingeführt, um Verzögerungen zwischen Wiederholungsversuchen beim Aufruf einer externen API hinzuzufügen.

      Darüber hinaus können wir Metriken und Protokollierung integrieren, um Zustandsänderungen von Leistungsschaltern zu überwachen, indem wir Tools wie Prometheus für die Echtzeitüberwachung und -warnung nutzen. Hier ist ein einfaches Beispiel:

      Implementierung eines Leistungsschaltermusters mit erweiterten Strategien in go

      Wie Sie sehen werden, haben wir jetzt Folgendes getan:

      1. In L16–21 definieren wir einen Prometheus-Zählervektor, um die Anzahl der Anfragen und deren Status (Erfolg, Fehler, Zustandsänderungen des Leistungsschalters) zu verfolgen.
      2. In L25–26 werden die definierten Metriken bei Prometheus in der Init-Funktion registriert.

      Profi-Tipp: Die Init-Funktion in Go wird verwendet, um den Status eines Pakets zu initialisieren, bevor die Hauptfunktion oder ein anderer Code im Paket ausgeführt wird. In diesem Fall registriert die Init-Funktion die requestCount-Metrik bei Prometheus. Und dies stellt im Wesentlichen sicher, dass Prometheus diese Metrik kennt und mit der Datenerfassung beginnen kann, sobald die Anwendung ausgeführt wird.

      1. Wir erstellen den Leistungsschalter mit benutzerdefinierten Einstellungen, einschließlich der ReadyToTrip-Funktion, die den Fehlerzähler erhöht und bestimmt, wann der Stromkreis ausgelöst werden soll.

      2. OnStateChange, um Zustandsänderungen zu protokollieren und die entsprechende Prometheus-Metrik zu erhöhen

      3. Wir stellen die Prometheus-Metriken am /metrics-Endpunkt zur Verfügung

      Zusammenfassung

      Zum Abschluss dieses Artikels hoffe ich, dass Sie gesehen haben, wie Leistungsschalter eine große Rolle beim Aufbau belastbarer und zuverlässiger Systeme spielen. Indem sie kaskadierende Ausfälle proaktiv verhindern, stärken sie die Zuverlässigkeit von Microservices und verteilten Systemen und sorgen so für ein nahtloses Benutzererlebnis auch in schwierigen Situationen.

      Denken Sie daran, dass jedes System, das auf Skalierbarkeit ausgelegt ist, Strategien zur reibungslosen Bewältigung von Ausfällen und zur schnellen Wiederherstellung beinhalten muss -  Oluwafemi, 2024

      Ursprünglich veröffentlicht unter https://oluwafemiakinde.dev am 7. Juni 2024.

      Das obige ist der detaillierte Inhalt vonLeistungsschalter in Go: Stoppen Sie kaskadierende Ausfälle. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Vorheriger Artikel:Pipeline-KonzeptNächster Artikel:Pipeline-Konzept