Protégez vos API partenaires avec Circuit Breaker - partie 1
Publié le 23 octobre 2024
Lorsque vous concevez un projet, vous devez effectuer des appels HTTP à une API ou, plus généralement, à un web service. Comment vous assurer que votre application est bien résiliente ? Vous vérifiez alors que la réponse reçue est valide, que les en-têtes sont corrects, et que les valeurs du payload sont exactes et n'ont pas changé.
Imaginons que soudainement, vous recevez des erreurs 5xx 😱 !
- 500 (Internal Server Error)
- 502 (Bad Gateway or Proxy Error)
- 503 (Service Unavailable)
- 504 (Gateway Time-out)
- 505 (HTTP Version not supported)
Quelle qu'en soit la raison, ce n'est ni une bonne nouvelle pour nous, ni pour les mainteneurs du service. Il y a de fortes chances que l'API soit en difficulté, et si elle est déjà sur-sollicitée, ce n'est pas lui rendre service de la bombarder de requêtes jusqu'à recevoir une réponse valide. Cela devient un véritable goulot d'étranglement et une source de dysfonctionnement pour votre application. C'est là qu'intervient le Circuit Breaker.
Si vous êtes familier·e avec Circuit Breaker, vous pouvez aller directement à l'usage de la librairie. À la suite de cet article, vous serez capable d'ajouter la fonctionnalité sur le client HTTP de votre choix avec un attribut PHP à l'aide de l'injection de dépendance et la décoration de service. Et si la théorie ne vous intéresse pas, patientez jusqu'à la publication de la seconde partie de cet article :
Note: Cet article met en avant un exemple d'implémentation, pas une librairie clé en main.
Le patron de conception Circuit Breaker
Le patron de conception Circuit Breaker est le reflet d'un comportement d'un circuit électrique. Lorsque le circuit est fermé, l'électricité circule. Lorsque le circuit est ouvert, l'électricité ne peut plus joindre l'autre extrémité : on coupe le circuit (lire le patron de conception en détail).
Ici, lorsque l'on détecte que le service est inaccessible, nous coupons le circuit. Au même moment, le système déclenche un compte à rebours pour tester régulièrement si le service ne serait pas de nouveau disponible. Si oui, on referme le circuit pour laisser circuler les requêtes. Sinon, on relance un nouveau compte à rebours.
Simple et efficace pour laisser le temps au service en défaut de se remettre à flot. Ce qui est particulièrement pertinent lorsque le service subit une charge qu'il n'est pas capable de supporter.
Cela dit, ce n'est pas suffisant. Nous devons également réagir en conséquence pour fournir à nos utilisateurs un message cohérent en réponse !
Les librairies PHP
Ne pas avoir à ré-écrire ce genre de librairie, c'est mieux. Voici mes critères : interopérable, maintenu récemment, éprouvé, et basé sur HttpClientInterface.
Je vous recommande l'excellente conférence de Nicolas Grekas lors de l'API Platform Conference 2024 durant laquelle il a notamment expliqué comment configurer son projet avec Composer pour que les services restent agnostiques en termes de client HTTP, afin que l’utilisateur final puisse choisir son client HTTP préféré s’il le souhaite, voire que le bundle sélectionne lui-même un client HTTP par défaut qui correspond à ses besoins.
Sa conférence devrait être disponible en rediffusion sur notre chaine YouTube en novembre 2024. En attendant, vous pouvez aller lire notre article résumant la conférence.
Faisons le tour des possibilités :
Les deux dernières librairies proviennent du même groupe de personnes, dont l'excellent Mickael Andrieu qui était venu présenter la librairie lors du Forum PHP 2019. Il y a aussi Matthieu Ferment, qui avait présenté CQRS à l'AFUP Day 2021 et reviendra à Lille dévoiler les nouveautés de Prestashop 9 ce 29 octobre 2024. Bref, c'est une librairie candidate, tout comme Ganesha.
L'une ou l'autre, c'est mécanique ici. Plus d'utilisateurs, plus de contributeurs, plus d'activités chez Ganesha. C'est la librairie que j'ai retenu, mais attention, il est tout à fait possible de faire la même chose avec PrestaShop/circuit-breaker ou loveOss/resiliency.
D'ailleurs, pour avoir échangé brièvement avec son auteur, c'est vrai qu'avec un peu de temps et d'amour, il serait possible de raviver le dépôt.
À vos claviers !
Usage du patron de conception avec Ganesha
Il existe cinq grandes stratégies de surveillance. Ganesha en implémente deux. Rate
et Count
.
Avec cette stratégie de surveillance, nous allons observer le nombre de requêtes en erreur pendant un temps, à partir de la première erreur reçue.
Si le seuil minimum d'erreurs est atteint, alors on "ouvre" le circuit et empêche les futurs appels, et ce pendant un délai. Une fois ce délai passé, le circuit passe en "semi-ouvert", et la prochaine requête passera. Selon la réponse, on referme le circuit, ou le ré-ouvre.
Ce mécanisme bénéficie lui aussi de deux variations.
Dès qu'une réponse nous revient en erreur, nous allons vérifier les seuils déclencheurs (ex: 30% de requêtes en erreur avec minimum 50 requêtes en erreur, dans les 30 dernières secondes). Si ces seuils sont dépassés, le circuit s'ouvre. La mise en sécurité est déclenchée.
Sur ce schéma, au cours des dix dernières secondes, il y a eu quatre requêtes en erreur, une seule lors des dix secondes précédentes, puis trois, et enfin cinq. Un seuil inférieur ou égal à treize requêtes en erreurs est suffisant pour déclencher la mise en sécurité. Si, dans les secondes qui suivent, une seule requête échoue, alors le seuil ne sera plus atteint et le circuit pourra se refermer.
Avec cette variation, le temps est divisé en segments avec trois propriétés principales :
- qui ne se répètent pas
- qui ne se superpose pas
- ne peuvent pas être plus longs que la fenêtre surveillée
Beaucoup plus statique, cette stratégie ne demande qu'un seuil à atteindre. Pour chaque requête en erreur, on augmente le compteur, et pour chacune revenant avec succès, le compteur est décrémenté.
Tous les modes de stockage des événements ne sont pas disponibles pour toutes les stratégies.
La fenêtre coulissante est supportée par Redis et MongoDB.
La fenêtre en cascade est supportée par APCu et Memcached (pas Memcache).
La stratégie Count est supportée par les quatre moyens de stockage.
Utiliser Ganesha
Ajoutez Ganesha à votre projet :
# Installez Composer si vous ne l'avez pas encore
$ curl -sS https://getcomposer.org/installer | php
$ php composer.phar require ackintosh/ganesha
Pour son usage en PHP, Ganesha offre un builder pour chacune des stratégies à utiliser. Il faudra également appeler les méthodes de configuration de chacune des stratégies.
$ganesha = Ackintosh\Ganesha\Builder::withRateStrategy();
$ganesha = Ackintosh\Ganesha\Builder::withCountStrategy();
Il sera ensuite possible de vérifier si un service est disponible ou non, déclarer un événement en succès ou en erreur.
$ganesha->isAvailable($service);
$ganesha->success($service);
$ganesha->failure($service);
Pour une stratégie Rate
, il va falloir une connexion à un serveur Redis, par exemple, et la donner à l'Adapter
adéquat.
$redis = new \Redis();
$redis->connect('localhost');
$adapter = new Ackintosh\Ganesha\Storage\Adapter\Redis($redis);
Mis bout à bout, ça pourrait ressembler à ceci.
La variable $service
sert à identifier le service web que l'on essaie de contacter lorsque vous discutez avec plusieurs API, c'est ce qui permettra de les distinguer en stockant des clés différentes.
$ganesha = Ackintosh\Ganesha\Builder::withRateStrategy()
->adapter($adapter)
->failureRateThreshold(50)
->intervalToHalfOpen(10)
->minimumRequests(10)
->timeWindow(30)
->build();
$service = 'external_api';
if (!$ganesha->isAvailable($service)) {
// TODO une jolie erreur pour faire patienter nos utilisateurs
}
try {
// TODO envoyer une requête à l'API
$ganesha->success($service);
} catch (Exception $e) {
// On enregistre l'erreur pour le service
$ganesha->failure($service);
// TODO une jolie erreur pour faire patienter nos utilisateurs
}
Pour chaque appel effectué à Ganesha, il est possible d'écouter un événement, souvent dans le but de recevoir des alertes techniques ou d'envoyer un signal à vos clients/front-end afin de mettre en place un mode dégradé durant la récupération du service.
$ganesha->subscribe(function ($event, $service, $message) {
switch ($event) {
case Ganesha::EVENT_TRIPPED:
$logger->warning(
"Une erreur est survenue pour {$service}. {$message}."
);
break;
case Ganesha::EVENT_CALMED_DOWN:
$logger->warning(
"Le service {$service} est revenu :). {$message}."
);
break;
case Ganesha::EVENT_STORAGE_ERROR:
$logger->critical($message);
break;
default:
break;
}
});
Et pour terminer ce tour d'horizon, si Ganesha est désactivé, peu importe le nombre d'erreurs enregistrées, Ganesha retournera toujours que le service est disponible.
En cas de modification de votre configuration, il est souvent nécessaire de remettre les compteurs à zéro.
Ackintosh\Ganesha::disable();
$ganesha->failure($service);
var_dump($ganesha->isAvailable($service)); // true
$ganesha->reset();
La librairie est assez complète, donc je n'ai mentionné que l'essentiel pour la comprendre, ainsi que le code que je vais produire pour l'intégrer dans Symfony. Si vous êtes curieux·ses de découvrir tout ce qu'elle propose, je vous recommande vivement de consulter la documentation du projet.
Nous avons désormais tout ce qu'il faut pour protéger les échanges avec nos web services. Il est temps de l'intégrer dans Symfony. Nous verrons cela dans un prochain article !