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.
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.
Un disjoncteur lui-même est une technique/un modèle et il fonctionne dans trois états différents dont nous parlerons :
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.
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 :
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 :
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.
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 :
É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.
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()) } })
//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()) } })
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()) } })
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 :
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.
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.
OnStateChange pour enregistrer les changements d'état et incrémenter la métrique prometheus correspondante
Nous exposons les métriques Prometheus au point de terminaison /metrics
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!