Maison  >  Article  >  développement back-end  >  Disjoncteurs en marche : arrêtez les pannes en cascade

Disjoncteurs en marche : arrêtez les pannes en cascade

WBOY
WBOYoriginal
2024-07-17 17:41:11804parcourir

Circuit Breakers in Go: Stop Cascading Failures

Disjoncteurs

Un disjoncteur détecte les pannes et encapsule la logique de gestion de ces pannes de manière à empêcher la panne de se reproduire constamment. Par exemple, ils sont utiles pour gérer les appels réseau vers des services externes, des bases de données ou, en réalité, toute partie de votre système susceptible de tomber en panne temporairement. En utilisant un disjoncteur, vous pouvez éviter les pannes en cascade, gérer les erreurs temporaires et maintenir un système stable et réactif en cas de panne du système.

Pannes en cascade

Les pannes en cascade se produisent lorsqu'une panne dans une partie du système déclenche des pannes dans d'autres parties, entraînant une perturbation généralisée. Un exemple est lorsqu'un microservice dans un système distribué ne répond plus, ce qui entraîne l'expiration du délai d'attente des services dépendants et finalement leur échec. Selon l'ampleur de l'application, l'impact de ces pannes peut être catastrophique, ce qui va dégrader les performances et probablement même avoir un impact sur l'expérience utilisateur.

Modèles de disjoncteurs

Un disjoncteur lui-même est une technique/un modèle et il fonctionne dans trois états différents dont nous parlerons :

  1. État fermé : Dans un état fermé, le disjoncteur permet à toutes les demandes de transiter vers le service cible normalement comme elles le feraient. Si les requêtes aboutissent, le circuit reste fermé. Cependant, si un certain seuil de défaillances est atteint, le circuit passe à l’état ouvert. Considérez-le comme un service entièrement opérationnel où les utilisateurs peuvent se connecter et accéder aux données sans problème. Tout se passe bien.

Circuit Breakers in Go: Stop Cascading Failures

2. État ouvert : Dans un état ouvert, le disjoncteur fait immédiatement échouer toutes les demandes entrantes sans tenter de contacter le service cible. L'état est saisi pour éviter une surcharge supplémentaire du service défaillant et lui donner le temps de récupérer. Après un délai d'attente prédéfini, le disjoncteur passe à l'état semi-ouvert. Un exemple pertinent est le suivant : Imaginez qu'une boutique en ligne rencontre un problème soudain où chaque tentative d'achat échoue. Pour éviter de surcharger le système, le magasin cesse temporairement d'accepter toute nouvelle demande d'achat.

Circuit Breakers in Go: Stop Cascading Failures

3. État semi-ouvert : Dans l'état semi-ouvert, le disjoncteur permet à un nombre limité (configurable) de demandes de tests de transiter vers le service cible. Et si ces requêtes aboutissent, le circuit revient à l’état fermé. S'ils échouent, le circuit revient à l'état ouvert. Dans l'exemple de la boutique en ligne que j'ai donné à l'état ouvert ci-dessus, c'est là que la boutique en ligne commence à autoriser quelques tentatives d'achat pour voir si le problème a été résolu. Si ces quelques tentatives aboutissent, le magasin rouvrira entièrement son service pour accepter de nouvelles demandes d'achat.

Ce diagramme montre quand le disjoncteur essaie de voir si les demandes au Service B réussissent, puis il échoue/se casse :

Circuit Breakers in Go: Stop Cascading Failures

Le diagramme de suivi montre ensuite lorsque les demandes de test au Service B réussissent, le circuit est fermé et tous les autres appels sont à nouveau acheminés vers le Service B :

Circuit Breakers in Go: Stop Cascading Failures

Remarque : Les configurations clés pour un disjoncteur incluent le seuil de défaillance (nombre de défaillances nécessaires pour ouvrir le circuit), le délai d'attente pour l'état ouvert et le nombre de demandes de test en semi-ouverture état.

Implémentation de disjoncteurs dans Go

Il est important de mentionner qu’une connaissance préalable de Go est requise pour suivre cet article.

Comme tout modèle de génie logiciel, les disjoncteurs peuvent être implémentés dans différents langages. Cependant, cet article se concentrera sur la mise en œuvre dans Golang. Bien qu'il existe plusieurs bibliothèques disponibles à cet effet, telles que goresilience, go-resiliency et gobreaker, nous nous concentrerons spécifiquement sur l'utilisation de la bibliothèque gobreaker.

Astuce de pro : Vous pouvez voir l'implémentation interne du package gobreaker, vérifiez ici.

Considérons une application Golang simple dans laquelle un disjoncteur est implémenté pour gérer les appels vers une API externe. Cet exemple de base montre comment encapsuler un appel d'API externe avec la technique du disjoncteur :

Parlons de quelques points importants :

  1. La fonction gobreaker.NewCircuitBreaker initialise le disjoncteur avec nos paramètres personnalisés
  2. La méthode cb.Execute encapsule la requête HTTP, gérant automatiquement l'état du circuit.
  3. MaximumRequests est le nombre maximum de requêtes autorisées à passer lorsque l'état est semi-ouvert
  4. Intervalle est la période cyclique de l'état fermé pendant laquelle le disjoncteur efface les comptes internes
  5. Timeout est la durée avant la transition de l'état ouvert à semi-ouvert.
  6. ReadyToTrip est appelé avec une copie des décomptes chaque fois qu'une requête échoue à l'état fermé. Si ReadyToTrip renvoie vrai, le disjoncteur sera placé à l'état ouvert. Dans notre cas ici, cela renvoie vrai si les requêtes ont échoué plus de trois fois consécutives.
  7. OnStateChange est appelé chaque fois que l'état du disjoncteur change. Vous souhaiterez généralement collecter les métriques du changement d'état ici et en faire rapport à n'importe quel collecteur de métriques de votre choix.

Écrivons quelques tests unitaires pour vérifier la mise en œuvre de notre disjoncteur. Je n'expliquerai que les tests unitaires les plus critiques à comprendre. Vous pouvez consulter ici le code complet.

  1. Nous allons écrire un test qui simule des demandes consécutives ayant échoué et vérifie si le disjoncteur se déclenche à l'état ouvert. Essentiellement, après 3 pannes, lorsque la quatrième panne se produit, nous nous attendons à ce que le disjoncteur se déclenche (s'ouvre) puisque notre condition indique que cela compte.ConsecutiveFailures > 3 . Voici à quoi ressemble le test :
 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. Nous allons tester le ouvert > moitié - ouvert> États fermés. Mais nous allons d’abord simuler un circuit ouvert et appeler un timeout. Après un délai d'attente, nous devons faire au moins une demande de réussite pour que le circuit passe à moitié ouvert. Après l’état semi-ouvert, nous devons faire une autre demande de réussite pour que le circuit soit à nouveau complètement fermé. Si, pour une raison quelconque, il n’y a aucune trace d’une demande réussie dans le cas, celui-ci redeviendra ouvert. Voici à quoi ressemble le test :
     //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. Testons la condition ReadyToTrip qui se déclenche après 2 demandes d'échec consécutives. Nous aurons une variable qui suivra les échecs consécutifs. Le rappel ReadyToTrip est mis à jour pour vérifier si le disjoncteur se déclenche après 2 pannes ( counts.ConsecutiveFailures > 2). Nous allons écrire un test qui simule les pannes et vérifie le décompte et que le disjoncteur passe à l'état ouvert après le nombre de pannes spécifié.
       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())
               }
           })
      

      Stratégies avancées

      Nous pouvons aller encore plus loin en ajoutant une stratégie d'attente exponentielle à la mise en œuvre de notre disjoncteur. Nous allons garder cet article simple et concis en démontrant un exemple de stratégie d'attente exponentielle. Cependant, il existe d'autres stratégies avancées pour les disjoncteurs qui méritent d'être mentionnées, telles que le délestage, le cloisonnement, les mécanismes de secours, le contexte et l'annulation. Ces stratégies améliorent essentiellement la robustesse et la fonctionnalité des disjoncteurs. Voici un exemple d'utilisation de la stratégie d'intervalle exponentiel :

      Retard exponentiel

      Disjoncteur avec recul exponentiel

      Mettons certaines choses au clair :

      Fonction d'attente personnalisée : La fonction exponentialBackoff implémente une stratégie d'attente exponentielle avec une gigue. Il calcule essentiellement le temps d'attente en fonction du nombre de tentatives, garantissant que le délai augmente de façon exponentielle à chaque nouvelle tentative.

      Gestion des tentatives : Comme vous pouvez le voir dans le gestionnaire /api, la logique inclut désormais une boucle qui tente d'appeler l'API externe jusqu'à un nombre spécifié de tentatives ( tentatives := 5). Après chaque tentative échouée, nous attendons une durée déterminée par la fonction exponentialBackoff avant de réessayer.

      Exécution du disjoncteur : Le disjoncteur est utilisé dans la boucle. Si l'appel d'API externe réussit ( err == nil), la boucle est interrompue et le résultat réussi est renvoyé. Si toutes les tentatives échouent, une erreur HTTP 503 (Service non disponible) est renvoyée.

      L'intégration d'une stratégie d'attente personnalisée dans la mise en œuvre d'un disjoncteur vise en effet à gérer les erreurs transitoires avec plus de grâce. Les délais croissants entre les tentatives contribuent à réduire la charge sur les services défaillants, leur laissant ainsi le temps de récupérer. Comme le montre notre code ci-dessus, notre fonction exponentialBackoff a été introduite pour ajouter des délais entre les tentatives lors de l'appel d'une API externe.

      De plus, nous pouvons intégrer des métriques et une journalisation pour surveiller les changements d'état des disjoncteurs à l'aide d'outils comme Prometheus pour la surveillance et les alertes en temps réel. Voici un exemple simple :

      Mise en œuvre d'un modèle de disjoncteur avec des stratégies avancées en go

      Comme vous le verrez, nous avons maintenant effectué ce qui suit :

      1. Dans L16-21, nous définissons un vecteur de compteur Prometheus pour suivre le nombre de requêtes et leur état (succès, échec, changements d'état du disjoncteur).
      2. Dans L25-26, les métriques définies sont enregistrées auprès de Prometheus dans la fonction init.

      Astuce de pro : La fonction init dans Go est utilisée pour initialiser l'état d'un package avant que la fonction principale ou tout autre code du package ne soit exécuté. Dans ce cas, la fonction init enregistre la métrique requestCount auprès de Prometheus. Et cela garantit essentiellement que Prometheus est conscient de cette métrique et peut commencer à collecter des données dès que l'application démarre.

      1. Nous créons le disjoncteur avec des paramètres personnalisés, y compris la fonction ReadyToTrip qui augmente le compteur de pannes et détermine quand déclencher le circuit.

      2. OnStateChange pour enregistrer les changements d'état et incrémenter la métrique prometheus correspondante

      3. Nous exposons les métriques Prometheus au point de terminaison /metrics

      Conclusion

      Pour conclure cet article, j'espère que vous avez vu à quel point les disjoncteurs jouent un rôle important dans la construction de systèmes résilients et fiables. En empêchant de manière proactive les pannes en cascade, ils renforcent la fiabilité des microservices et des systèmes distribués, garantissant une expérience utilisateur transparente même face à l'adversité.

      Gardez à l'esprit que tout système conçu pour l'évolutivité doit intégrer des stratégies pour gérer les pannes avec élégance et récupérer rapidement —  Oluwafemi, 2024

      Publié à l'origine sur https://oluwafemiakinde.dev le 7 juin 2024.

      Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Article précédent:Concept de pipelineArticle suivant:Concept de pipeline