Trois étapes clés sont d'une importance capitale pour prévenir les pannes dans les applications microservices, en particulier celles qui dépendent des services cloud : Identifier les causes potentielles de défaillance du système, s'y préparer et tester les contre-mesures avant que la panne ne se produise.
Parce que l'infrastructure et les applications complexes de DoorDash sont susceptibles de se briser, nous devons comprendre quelles sont les défaillances du système qui causent les pannes. L'idéal est de soumettre les services backend à des défaillances intentionnelles simulées dans un environnement de test, où le comportement de l'application en cas de défaillance peut être observé. Ce n'est qu'à l'issue de ce processus que nous pourrons concevoir des contre-mesures appropriées pour garantir que les pannes futures ne deviennent pas des pannes à part entière qui affectent l'expérience des clients de DoorDash.
Dans ce billet, nous discutons des approches naissantes et plus traditionnelles de l'ingénierie de la résilience et de leur application aux applications microservices à grande échelle. Nous présentons également une nouvelle approche - les tests d'injection de fautes au niveau des services - qui a été explorée sur la plateforme DoorDash au cours de mon récent stage de recherche.
Voici Filibuster : Un outil automatisé de test de résilience
En tant qu'étudiant en doctorat à l'université Carnegie Mellon, je travaille depuis deux ans au développement d'un outil automatisé de test de résilience appelé Filibuster au sein du Composable Systems Lab de l'Institute of Software Research de l'université. La conception de Filibuster repose principalement sur l'étude de la littérature grise - présentations de conférences, articles de blog, livres blancs et autres documents non produits par des éditeurs commerciaux - afin d'identifier les bogues de résilience qui ont causé des pannes, afin de mieux comprendre comment les éviter à l'avenir. Bien que Filibuster ait été conçu dans un esprit pratique, l'objectif ultime de la recherche universitaire est de pouvoir adapter et appliquer les idées de recherche dans un cadre industriel.
En raison de l'intérêt croissant de DoorDash pour l'amélioration de la fiabilité de la plateforme, j'ai rejoint DoorDash en tant que stagiaire pendant l'été 2021 pour tester l'applicabilité de Filibuster à la plateforme DoorDash. Mon travail a produit des résultats préliminaires positifs dans ce sens, tout en me donnant l'occasion d'étendre les algorithmes de base de Filibuster et de mettre en œuvre la prise en charge de nouveaux langages de programmation et cadres RPC. L'accès à l'application industrielle réelle de microservices de DoorDash a été extrêmement précieux, à la fois pour s'assurer que la conception de Filibuster correspond à la façon dont les applications de microservices sont développées aujourd'hui et pour influencer les fonctionnalités futures que je n'aurais pas identifiées dans un environnement de laboratoire. De telles expériences sont bénéfiques à la fois pour la communauté des chercheurs et pour l'entreprise d'accueil !
Contexte : Les microservices et leur complexité
Les services dorsaux de DoorDash sont écrits à l'aide d'une architecture microservice, un style qui permet à des équipes indépendantes, concentrées sur un composant spécifique de l'entreprise, de fournir des fonctionnalités à leur propre rythme, puis de faire évoluer ces services de manière indépendante pour répondre à la demande. Comme c'est le cas chez DoorDash, les architectures de microservices sont généralement adoptées pour augmenter la productivité des développeurs et améliorer la livraison des fonctionnalités lorsque l'entreprise s'agrandit. Les architectures de microservices ne sont généralement pas adoptées uniquement pour des raisons techniques ; elles prennent une conception d'application monolithique simpliste et facile à tester et la convertissent en un système distribué plus difficile à tester et à raisonner.
Les systèmes distribués sont réputés pour leur complexité. Une fois qu'une conception monolithique a été décomposée en ses services constitutifs, le traitement d'une seule demande d'utilisateur de bout en bout à partir d'une application mobile peut impliquer des dizaines, voire des centaines de services différents travaillant de concert. Cette décomposition oblige les développeurs à prendre en compte un nouveau type de complexité applicative : la défaillance partielle, dans laquelle un ou plusieurs des services dont dépend un processus unique se trouvent dans un état de défaillance. Plus concrètement, cela oblige les développeurs de ces applications à se poser des questions telles que les suivantes : Que se passe-t-il lorsque l'un des services nécessaires tombe en panne ou devient indisponible ? Que se passe-t-il si un service nécessaire est lent ? Que se passe-t-il si l'un des services requis est déployé avec un bogue et commence à renvoyer des erreurs lorsqu'il est appelé ?
Malheureusement, les développeurs doivent avoir des réponses à toutes ces questions. Les fournisseurs de services en nuage n'offrent pas de garanties de disponibilité à 100 %, car des défaillances se produisent ; les ingénieurs en logiciel ne sont pas parfaits et, par conséquent, ils écrivent des bogues qui passent parfois à travers les processus de test manuel. Même s'il était possible d'effectuer des tests de pré-production parfaits pour s'assurer que les bogues n'atteignent jamais la production, DoorDash s'appuie sur un certain nombre de services tiers où des bogues peuvent exister. En bref : les pannes se produisent.
L'échec n'est pas une chose que l'on peut éviter totalement. Il est inévitable et doit être anticipé et planifié.
L'ingénierie du chaos et son utilisation en production
Au cours des dix dernières années, l'ingénierie du chaos s'est imposée comme la principale discipline permettant de résoudre ce problème de fiabilité. Elle reconnaît d'abord que les défaillances se produisent et se concentre ensuite sur les réponses organisationnelles et/ou techniques nécessaires lorsque les services tombent inévitablement en panne.
L'ingénierie du chaos a été inaugurée par Netflix lorsqu'elle est passée des serveurs physiques à AWS. La première instanciation de l'ingénierie du chaos a été Chaos Monkey, un outil permettant d'arrêter automatiquement et aléatoirement des instances EC2 afin de vérifier que Netflix pouvait résister à de telles défaillances en production. Elle s'est depuis étendue à toute une série d'outils. Par exemple, Chaos Gorilla simule la défaillance d'une zone de disponibilité entière dans AWS et Chaos Kong simule la défaillance d'une région AWS entière. Netflix a depuis consacré beaucoup de temps à l'automatisation de l'injection de fautes ; des clusters de production entiers peuvent désormais être configurés pour exécuter une expérience de chaos sur un très petit pourcentage du trafic de production de Netflix et peuvent ensuite être démantelés automatiquement par les systèmes d'injection de fautes CHaP et Monocle de l'entreprise. D'anciens employés de Netflix ont récemment créé Gremlin, une société qui propose des services d'ingénierie du chaos, déjà utilisés par de grands sites de commerce électronique.
Netflix prône la valeur de ce type d'ingénierie du chaos directement dans la production. Certains utilisateurs de Netflix peuvent ne pas se soucier de devoir rafraîchir la page d'accueil de Netflix lorsqu'ils tombent dans un groupe expérimental d'ingénierie du chaos et qu'ils rencontrent un bogue. Mais certains utilisateurs s'en soucient ; Netflix leur permet de se retirer du groupe expérimental. DoorDash, cependant, ne peut pas s'attendre à ce qu'un client qui essaie de passer une commande tolère une défaillance aléatoire de l'application, surtout pas au cours d'une expérience d'ingénierie du chaos. Au lieu de simplement rafraîchir la page comme dans le cas de Netflix, cet utilisateur de DoorDash serait probablement frustré et cesserait d'utiliser la plateforme. En outre, bien qu'il existe des programmes de fidélisation comme Dash Pass, le coût de changement est encore faible pour les consommateurs. C'est pourquoi il est essentiel d'offrir une expérience client de qualité pour ne pas perdre des clients au profit de concurrents sur le long terme.
Par conséquent, l'expérimentation du chaos n'est pas pratique pour des applications et des entreprises comme DoorDash.
Qu'en est-il de l'ingénierie du chaos en phase d'essai ou pendant le développement ?
Par ailleurs, le même type d'expériences sur le chaos pourrait être réalisé dans un environnement de développement local ou en phase d'essai. C'est précisément ce que Gremlin et les développeurs d'outils d'ingénierie du chaos en local - par exemple, Chaos Toolkit - recommandent comme point de départ. Les expériences de chaos dans l'environnement de développement peuvent être utilisées avec succès pour tester la réponse d'une organisation à une défaillance - par exemple, la réponse à l'appel ou la validation des runbooks. Mais d'un point de vue technique, l'utilisation de l'ingénierie du chaos à ce stade met en évidence certains des principaux inconvénients de l'approche globale de l'ingénierie du chaos.
Tout d'abord, nous ne nous intéressons pas seulement aux défaillances qui rendent un service indisponible ou lent. Il s'agit également de comprendre le comportement de l'ensemble de l'application lorsqu'un service particulier commence à renvoyer une erreur. La nouvelle erreur peut être causée par la défaillance d'un service dépendant plus bas dans la chaîne, ou l'erreur peut s'être produite parce qu'un service dépendant a été redéployé avec un bogue. Ces types de défaillances sont généralement plus difficiles à générer par les approches traditionnelles d'ingénierie du chaos qui reposent uniquement sur l'épuisement de la mémoire des instances de service, leur indisponibilité sur le réseau ou leur plantage pur et simple. Les défaillances qui nous intéressent se produisent au niveau de l'application, et non au niveau de l'infrastructure. Si un bogue est déployé dans un service en aval, nous souhaitons renforcer la mise en œuvre des services individuels afin de rendre notre application globale plus résiliente. Pour qu'une approche d'injection de fautes soit la plus utile possible, elle doit également prendre en compte les défaillances au niveau de l'application.
Deuxièmement, sans une approche systématique de l'injection de fautes, il est difficile de garantir la fiabilité globale de l'application ; l'expérimentation aléatoire peut manquer la défaillance particulière qui entraînera une panne. La plupart des approches et des outils d'ingénierie du chaos utilisés aujourd'hui reposent sur des configurations d'expériences spécifiées manuellement. Il incombe donc au développeur de concevoir et de spécifier manuellement les scénarios de défaillance possibles qu'il souhaite tester - par exemple, Gremlin ou LinkedOut - et de ne pas rechercher systématiquement ou exhaustivement l'espace des défaillances possibles. Pour garantir qu'un service agira d'une certaine manière lorsqu'une défaillance se produit, nous devons tester ce service pour cette défaillance. Par conséquent, nous pensons que toute approche d'injection de fautes devrait générer et exécuter automatiquement les configurations de fautes afin de fournir ces garanties de résilience.
L'ingénierie du chaos en tant que discipline est née des préoccupations concernant la fiabilité de l'infrastructure et les réponses techniques et organisationnelles au manque inhérent de fiabilité. Mais la fiabilité des applications de microservices s'étend bien au-delà du niveau de l'infrastructure, jusqu'aux défauts logiciels de l'application et à la garantie de la résilience contre ces défauts. Si l'ingénierie du chaos est une technique extrêmement utile pour identifier les défauts au niveau de l'infrastructure et tester la réponse de l'organisation, il est important d'identifier les effets potentiels de ces défauts sur l'ensemble de l'application dès le début du processus de développement en construisant des services de manière fiable et sans défaut.
Filibuster : Automatiser les tests de résilience des applications microservices
Filibuster est un outil conçu pour tester automatiquement la résilience des applications microservices. Filibuster part du principe que tout problème de résilience du système de bas niveau, comme les pannes de service ou les dépassements de délai, se manifestera dans la couche applicative sous forme d'erreurs ou d'exceptions, parallèlement à toute défaillance au niveau de l'application lors de l'émission de RPC interservices. Pour identifier les problèmes de résilience d'une application microservice, il suffit donc d'énumérer ces erreurs, de les synthétiser dans l'application, puis de vérifier le comportement de l'application en cas de défaillance par le biais d'une exploration systématique.
Pour ce faire, Filibuster s'appuie sur un certain nombre de techniques qui fonctionnent de concert :
- Tout d'abord, une analyse statique identifie les éventuelles erreurs visibles - qu'elles soient lancées ou renvoyées - à chaque site d'appel RPC au sein de l'application.
- Ensuite, Filibuster injecte systématiquement ces erreurs, d'abord une par une, puis en combinaisons, tout en exécutant de manière répétée un test fonctionnel de bout en bout de l'application pour s'assurer qu'elle continue à se comporter de manière souhaitable jusqu'à ce que l'espace des défaillances possibles soit épuisé. Cette approche évite aux développeurs d'avoir à écrire manuellement des tests unitaires ou d'intégration contenant des simulacres pour tous les échecs et toutes les combinaisons.
- Étant donné que les tests fonctionnels rédigés par les développeurs ne contiennent généralement pas d'oracle de test prenant en compte la défaillance d'un ou de plusieurs services dépendants, les développeurs sont contraints de réfléchir à ce que le système doit faire lorsque l'un des services dépendants tombe inévitablement en panne et qu'une assertion de leur test fonctionnel échoue.
- Une fois que les développeurs savent quel est le comportement souhaité en cas d'erreur, ils peuvent coder ce comportement directement dans le test fonctionnel en utilisant les assertions conditionnelles fournies par Filibuster. Par exemple, l'application renvoie 200 lorsque tous les services sont en ligne ; si le service X est en panne, l'application renvoie le code d'erreur Y.
- Ce processus itératif peut être assimilé à un coaching en résilience. Grâce à Filibuster, les développeurs sont informés d'un scénario de défaillance qu'ils n'ont pas anticipé et sont obligés de réfléchir à ce que l'application devrait faire avant d'écrire une assertion pour capturer ce comportement.
Nous aimons considérer ce processus comme un développement axé sur la résilience.
En bref, Filibuster permet d'identifier plus tôt, au moment du développement, un grand nombre des problèmes de résilience technique qui existent dans les applications, sans qu'il soit nécessaire de procéder à des tests en production comme l'exigent les techniques plus traditionnelles d'ingénierie du chaos.
Deux caractéristiques essentielles de l'obstruction parlementaire
Parce qu'il n'existe pas de modèle unique convenant à toutes les organisations, Filibuster a été conçu en tenant compte de la configuration. Sans couvrir toutes les configurations possibles de Filibuster, nous soulignons ce que nous pensons être deux caractéristiques clés pour les organisations soucieuses d'adopter des tests de résilience : la relecture déterministe, une technique qui permet aux développeurs d'écrire des tests de régression pour les pannes ou les défaillances antérieures du système, et la sélection des failles d'exécution, un moyen d'utiliser Filibuster de manière incrémentale pour augmenter la couverture des tests de résilience au fur et à mesure que le code évolue dans un pipeline d'intégration continue.
- Relecture déterministe : Lorsque les développeurs déboguent une défaillance particulière, ils peuvent, par exemple, rejouer la défaillance tout en utilisant le débogueur interactif de Java pour suivre pas à pas la défaillance d'un service particulier. Lorsque des échecs se produisent lors de tests avec Filibuster, celui-ci produit également un fichier de contre-exemple sur le système de fichiers. Ce fichier de contre-exemple peut être utilisé pour écrire des tests unitaires, d'intégration ou fonctionnels afin de mettre le système dans un état d'échec et d'écrire des tests qui simulent des pannes passées afin de construire une suite de régression de pannes ou de pannes.
- Sélection des fautes d'exécution : Filibuster peut être entièrement configuré pour que certaines failles soient testées dans le cadre d'un environnement de développement local tandis que d'autres failles sont testées dans le cadre de l'intégration continue. Par exemple, pour que l'environnement de développement local reste rapide, seuls les défauts courants sont testés pour chaque service et une liste complète des défaillances est utilisée à chaque validation d'une branche ou dans le cadre d'une version de nuit. Tout cela peut être spécifié dans le cadre d'un fichier de configuration fourni à Filibuster pour permettre la flexibilité de l'environnement de test.
Adapter Filibuster à DoorDash
Au début du stage, Filibuster a été conçu pour tester uniquement des microservices implémentés en Python, utilisant Flask, qui communiquaient strictement par HTTP pour les RPC. DoorDash utilise Kotlin sur la JVM pour l'implémentation des services et utilise à la fois HTTP et GRPC pour les RPC interservices.
Pour adapter Filibuster à DoorDash, il a fallu procéder comme suit :
- Tout d'abord, Filibuster a dû être étendu pour prendre en charge Kotlin et GRPC. Alors que l'extension à GRPC était simple, l'extension à Kotlin et à la JVM s'est avérée plus difficile en raison des primitives de concurrence qu'ils fournissaient. En fait, cela a entraîné plusieurs modifications des algorithmes de base de Filibuster pour prendre en charge les RPC qui se produisent dans un code concurrent, ce qui n'était pas une préoccupation importante en Python en raison de son manque de véritable concurrence et de parallélisme.
- Deuxièmement, Filibuster s'est appuyé sur l'instrumentation des services au niveau de l'application pour prendre en charge l'injection de fautes. Mais la modification du code au niveau de l'application n'était pas une option viable pour DoorDash, à la fois en raison de l'effort requis et de la surcharge qu'impliquerait le maintien d'un ensemble de modifications uniquement pour les tests de résilience. J'ai donc travaillé avec les ingénieurs de DoorDash pour concevoir une stratégie permettant d'ajouter dynamiquement cette instrumentation au moment de l'exécution, sans modification du code, afin de prendre en charge l'injection de fautes requise par Filibuster.
Pour voir comment Filibuster peut être utilisé sur une application Java, regardez cette vidéo YouTube, qui démontre un résultat concret du stage.
DoorDash n'en est qu'à ses débuts en matière de résilience avec Filibuster. À la fin de l'été, un prototype de Filibuster fonctionnait sur deux des services de DoorDash exécutés dans l'environnement de développement local, démontrant la viabilité de l'approche pour le code des microservices du monde réel. DoorDash prévoit d'étendre la portée de Filibuster dans les mois à venir pour continuer à améliorer la résilience de leur application.
Pour en savoir plus sur Filibuster, regardez cette vidéo (ou lisez notre article) qui présente une vue d'ensemble de la technique d'injection de fautes au niveau des services.
Pour en savoir plus sur la recherche en matière d'injection de fautes, suivez Christopher Meiklejohn sur Twitter ou sur son blog.
Vous trouvez ce travail intéressant et passionnant ? Nous recrutons dans le domaine de l'ingénierie de la résilience chez DoorDash ! Postulez ici.