Implémenter de la résilience dans une Web API ASP.NET Core avec Polly
Publié le 22/05/2023
Par  Alexandre CASTRO

Dans cet article nous allons aborder comment implémenter de la résilience dans un Web API ASP.NET.

 

Qu'est-ce que la résilience en informatique ?

Avant tout nous allons définir ce qu'est la résilience.

La résilience, en informatique, est la capacité d’un système à continuer de fonctionner en cas de panne, d’incidents intentionnels ou non et/ou de sollicitations extrêmes.

La résilience tout d'abord doit s'appuyer sur :

  • Les tests : les tests nous donnent des métriques en termes de qualité car c'est l'idée que tout le monde se fait des tests mais ils servent aussi à tester la résilience de votre application.
  • La sécurité : c'est une composante essentielle dans la résilience car elle répond au besoin d'empêcher des incidents intentionnels comme des attaques ou des intrusions.
  • L’architecture : Typiquement une architecture de SI peut vous permettre aussi de vous protéger contre des attaques ou des intrusions aussi.

Par exemple le classique Hub-and-spoke :

Le fait d'être sur des private networks et de communiquer via des private endpoints vous protège et vous rend plus résilient.

 

Et maintenant ? Quel est le rapport avec ASP.NET Core Web API ?

Et bien vous aussi en tant que développeur .NET, même dans le cas d'une architecture parfaite avec des test, vous devez gérer les incidents de la bonne manière.

En effet, en dehors de la gestion de la gestion des exceptions, il va falloir gérer proprement certains codes HTTP.

Et c'est la que Polly va nous aider. Polly est une librairie .NET qui va vous aider à implémenter plusieurs patterns de résilience connus.

Voici l'adresse du package Nuget : https://www.nuget.org/packages/polly/

Comme évoqué au dessus, il n'est pas nécessaire de gérer toutes les erreurs mais dans la plupart des cas, nous allons gérer ces erreurs en priorité :

  • Problèmes de communication réseau avec des services externes par exemple
  • les erreurs serveur (donc les codes HTTP 5XX )
  • les timeout de requêtes (donc les codes HTTP 408)

Comme je le disais Polly permet d'implémenter plusieurs pattern pour améliorer la résilience.

Tout d'abord le pattern "Retry".

Généralement, ces échecs transitoires se corrigent automatiquement et la requête est redéclenchée après un certain intervalle, ce qui entraîne son succès. Ex : Service de base de données qui traite un grand nombre de requêtes simultanées pouvant rejeter d'autres requêtes jusqu'à ce que la charge de travail soit allégée.En gros nous allons avoir des échecs parce qu'il y a trop de requêtes simultanées mais nous avons besoin de relancer nos requêtes en échec dès que la charge serveur est redevenue normale.

Pour cela nous allons utiliser le pattern Retry :

Par exemple lorsque nous ajoutons un service externe à notre application :

builder.Services.AddHttpClient<IWeatherService, WeatherService>(client =>
 {
     client.BaseAddress = new Uri("https://localhost:7054");
     client.DefaultRequestHeaders.Add("Accept", "application/json");
 })
 .AddTransientHttpErrorPolicy(policy => policy.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(2)));

Ici le retry est implémenté dans le code suivant :

.AddTransientHttpErrorPolicy(policy => policy.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(2)));

le code ci-dessus va retenter les requêtes 3 fois dans un interval de 2 secondes pour chaque requête en échec.

 

Ensuite le pattern "CircuitBreaker" ou Disjoncteur :

Pour certaines ressources et en fonction de certains imprévus, l’indisponibilité peut durer plus longtemps et toute nouvelle tentative est vouée à l’échec. La solution consiste alors à libérer la ressource, d’où le nom de pattern Circuit Breaker

Il existe un très bon article très détaillé sur le site de Microsoft : https://learn.microsoft.com/fr-fr/azure/architecture/patterns/circuit-breaker

Pour information, le CircuitBreaker a 3 états :

  • Closed : Lorsque tout est normal, le circuit reste à l'état fermé et le débit redevient normal. Lorsque le nombre de pannes dépasse le seuil déterminé alors il passe dans un état ouvert
  • Open: Le disjoncteur renvoie immédiatement une erreur sans appeler les systèmes en amont.
  • Half open : Une fois la période de temporisation configurée atteinte, le disjoncteur passe à l'état semi-ouvert et valide si l'appel au système en amont fonctionne sans défaillance. En cas de panne, le disjoncteur repasse à l'état ouvert. Cependant, en cas de succès, le disjoncteur le réinitialise à l'état fermé.

Polly va nous permettre d'implémenter ce pattern en complément du pattern Retry vu précédemment :

builder.Services.AddHttpClient<IWeatherService, WeatherService>(client =>
 {
     client.BaseAddress = new Uri("https://localhost:7054");
     client.DefaultRequestHeaders.Add("Accept", "application/json");
 })
    .AddTransientHttpErrorPolicy(policy => policy.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(2)))
    .AddTransientHttpErrorPolicy(policy => policy.CircuitBreakerAsync(5, TimeSpan.FromSeconds(10)))

Nous avons donc rajouté le "CircuitBreaker" à notre code ici :

.AddTransientHttpErrorPolicy(policy => policy.CircuitBreakerAsync(5, TimeSpan.FromSeconds(10)))

Dans notre cas, le CircuitBreaker passe à l'état ouvert après que l'API WeatherService échoue 5 fois, et la période d'attente pour passer le disjoncteur à un état semi-ouvert est de 10 secondes.

 

Est-ce que ces deux patterns suffisent pour atteindre une résilience suffisante ? Et bien non 😥

Prenons un exemple, supposons qu'il existe 3 services - le service A, le service B et le service C. Le service A agit comme une passerelle vers les services B et C. Supposons que le service A ne peut gérer que 5 requêtes simultanées et que le service B nécessite plus temps de traitement pour terminer l'exécution.

Maintenant, imaginez 10 déclencheurs de requêtes simultanées vers le service A


5 appels au service B
5 appels au service C
Dans le cas où les demandes sont transmises au service B (ce qui prend du temps), il y a de fortes chances de timeout pour le service C.

Pour cela nous pouvons tenter d'équilibrer les requêtes envoyées en fonction à améliorer l'expérience utilisateur , éviter les timeouts etc mais pour cela, il faut connaitre son applicatif en ayant effectué des tests de charge par exemple.

Cela permet aussi de renvoyer des résultats partiels et non pas totalement défaillants en cloisonnant les services par exemple ou en cloisonnant les requêtes.

Pour implémenter ce scénario d'équilibrage des requêtes, vous pouvez utiliser le pattern Bulk-Head

le nom de ce pattern provient du cloisonnement des conteneurs au sein d'un navire, comme ça lorsque la coque est endommagée, seul certains conteneurs sont perdus., d'ou le nom de BulkHead ou cloison.

Voici un exemple de ce que pourrait être le pattern BulkHead si vous aviez plusieurs services :

Ce pattern a réellement pour but d'éviter la propagation des erreurs et des exceptions en cloisonnant les appels de service et il est très souvent utilisé dans le cadre de microservices.

Dans le cadre de notre exemple, il est compliqué d'implémenter ce pattern mais nous allons voir un exemple qui utilise ce pattern :

var bulk = Policy.BulkheadAsync<HttpResponseMessage>(3, 5, x =>
{
    Console.WriteLine("rejected"+x.OperationKey);
    return Task.CompletedTask;
});
builder.Services.AddHttpClient<IWeatherService, WeatherService>(client =>
 {
     client.BaseAddress = new Uri("https://localhost:7054");
     client.DefaultRequestHeaders.Add("Accept", "application/json");
 })
    .AddTransientHttpErrorPolicy(policy => policy.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(2)))
    .AddTransientHttpErrorPolicy(policy => policy.CircuitBreakerAsync(5, TimeSpan.FromSeconds(10)))
    .AddPolicyHandler(policy => bulk);

Ici nous avons créer un bulkhead (un cloisonnement) qui a paramétré le MaxParallelism a 3 ce qui veut dire que le nombre de requêtes concurentes maximum est géré à travers cette stratégie.

Et nous avons aussi paramétré le nombre maximum de MaxQueuingAction, ce qui veut dire que le nombre maximum de requêtes en attente est de 5.

 

Voila vous voyez maintenant l'important d'implémenter de la résilience dans vos applicatifs.

Je vous conseille l'article suivant sur le site de Microsoft qui complète ce que nous venons de voir :

https://learn.microsoft.com/fr-fr/dotnet/architecture/microservices/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly

Happy coding 😎

Alexandre CASTRO

Alexandre CASTRO

.NET et Azure