Points Clés
- Lorsqu’on gère les dépendances entre des microservices distribués, plusieurs types de croissance doivent être envisagés lorsque le produit évolue : par exemple le nombre d’utilisateurs, leur comportement ou encore les interactions entre services et sous-systèmes.
- Les services sans état sont souvent plus faciles à gérer que les services avec état.
- Regrouper les composants de services permet d’obtenir de meilleures performances, d’isoler plus efficacement les pannes et aligner les objectifs de niveaux de service (SLO).
- Isoler les couches de service peut éviter une panne globale du service. Cela inclut également les dépendances pour un service hébergé sur le Cloud ou par un tiers.
- Certaines stratégies d’architecture peuvent permettre une dégradation limitée du service durant une panne plutôt que de renvoyer immédiatement des erreurs à l’utilisateur (en mettant un cache entre l’API et la base de données par exemple).
- Afin de définir un SLO, il est nécessaire de prendre en compte le SLO de chaque service et de chaque parcours utilisateurs, en incluant les cas aux limites (réponses en erreur, expérience dégradée).
- Travailler avec les responsables des différents composants et prévoir du temps supplémentaire pour l’allocation de ressources et les changements d’architecture.
Durant QCon Plus l’année dernière, j’ai partagé certains pièges et patterns dans la gestion des dépendances dans une architecture microservices que j’ai pu rencontrer en travaillant chez Google ces 10 dernières années. Plutôt que de me concentrer sur une équipe ou un produit particulier, j’ai préféré partager au travers de cette présentation mes propres expériences et apprentissages en tant qu’ingénieure logiciel chez Google.
Dans chacun des scénarios que j’ai présentés, il y a eu ces moments de révélation qui m’ont permis de réaliser l’importance de différents aspects inhérents aux architectures microservices. Il y a également eu des moments d’échec ou de moins bien. Je me suis assurée d’identifier ces complications et de documenter mes actions pour éviter des situations similaires dans le futur, et que vous puissiez chercher les signes annonciateurs d’une potentielle panne de même type dans vos propres environnements.
Tous ces scénarios sont survenus alors que je travaillais avec des personnes occupant des rôles différents. Pour l’un je travaillais comme ingénieure logiciel avec un chef de projet, pour l’autre j’étais ingénieure de fiabilité des sites (SRE) travaillant avec d’autres développeurs. Dans le dernier scénario, je travaillais globalement avec toute l’équipe dans le but de construire des services fiables.
Chacun de ces scénarios peut s‘avérer utile pour différents rôles d’une même équipe : le succès (ou l’échec) de la construction d’une architecture microservices fiable ne dépend pas seulement d’une seule personne ou d’un seul rôle.
Chaque fois que vous changez un système, ce changement affectera plusieurs autres parties et plusieurs autres composants de votre produit dans sa globalité - ces composants pouvant être opérés par votre entreprise, dans le cloud ou par un prestataire. Ces modifications du système peuvent avoir des répercussions jusqu’au client, cet utilisateur que vous devriez toujours garder en tête. Pour cette raison, vous devez avoir une vision globale de vos systèmes - ce qui est aussi ce que j’ai essayé de transmettre durant ma conférence.
Avant d’aller plus loin, rappelons-nous rapidement comment l’industrie est passée des monolithes aux microservices, et faisons un dernier saut pour inclure les services opérés dans le cloud. Nous aborderons également les patterns et augmentations de trafic, l’isolation des pannes et comment nous pouvons planifier des objectifs de niveaux de service (SLOs) raisonnables dans un monde où les composants sont distribués.
Monolithes, Microservices, et l’hébergement Cloud
Notre voyage (pour lequel nous utiliserons un service générique) débute avec un fichier binaire unique, qui finira (rapidement) par évoluer pour inclure des fonctionnalités plus complexes comme une base de données, de l’authentification, du contrôle de flux, de la supervision et une API HTTP (pour que nos clients puissent nous appeler). Au départ il était prévu que ce binaire soit exécuté sur une seule machine, mais la croissance de l’activité a rapidement exigé sa réplication dans plusieurs régions du monde, permettant entre autres d’augmenter le trafic sur l’application.
Peu de temps après avoir répliqué notre monolithe, nous avons dû le découper en plusieurs binaires pour différentes raisons que vous pourriez reconnaître. Une de ses raisons est liée à la complexité du binaire : en y ajoutant des fonctionnalités plus complexes, le code source est devenu presque impossible à maintenir (sans parler d’ajouter de nouvelles fonctionnalités). L’expression de besoins particuliers pour des composants logiques indépendants est une autre raison pour laquelle on pourrait découper un monolithe en plusieurs binaires. Il pourrait par exemple être nécessaire d’augmenter les ressources machines pour un composant spécifique sans impacter les performances des autres composants.
Ces types de scénarios ont poussé à l’émergence des microservices: un ensemble de services faiblement couplés, déployables indépendamment, hautement maintenables et organisés pour former (ou servir) une application complexe. En pratique, cela implique de déployer plusieurs binaires communiquant sur le réseau, où chaque binaire implémente un microservice différent mais qui tous ensemble servent et représentent un seul produit. Le schéma 1 illustre l’exemple d’un produit API découpé en cinq microservices indépendants (API, Authentification, Contrôle, Données et Opérations) qui communiquent ensemble par le réseau. Dans une architecture microservice, le réseau est un composant à part entière du produit qui doit toujours être gardé en tête. Chaque service - désormais un binaire unique et un composant de notre application - peut désormais augmenter ses ressources individuellement, et leur cycle de vie est facilement géré par les équipes de développement.
Schéma 1: Un produit API découpé en cinq microservices indépendants
Avantages des Microservices
Développer un produit selon une architecture microservices offre plusieurs avantages. Globalement, la possibilité de déployer des binaires faiblement couplés sur différents serveurs permet aux Responsables Produit (PO) de choisir entre des scénarios de déploiement hautement disponibles ou peu couteux, et d’héberger chaque service soit sur le Cloud soit sur des machines propres. Cela permet également leur mise à l’échelle de façon indépendante : horizontale en augmentant les ressources systèmes pour chaque composant ou verticale en répliquant les composants individuellement, et permet aussi de déployer sur des régions indépendantes.
La gestion du cycle de développement est un autre avantage de cette architecture. Puisque chaque service est logiquement découpé des autres et présente peu de complexité interne, les développeurs peuvent plus facilement réfléchir aux impacts des changements d’implémentation et garantir que les nouvelles fonctionnalités auront des résultats prévisibles. Cela signifie aussi des développements indépendants pour chaque composant, permettant ainsi des modifications locales sur un ou plusieurs services sans effet de bord sur les autres. Les versions peuvent être livrées ou annulées indépendamment et ainsi offrir une réaction rapide en cas de pannes et des modifications en production plus ciblées.
Défis des microservices
Malgré les bénéfices apportés, une architecture microservice peut aussi complexifier certains processus. Dans les paragraphes suivants, je vais présenter les scénarios que j’ai mentionnés plus haut (pour lesquels certains noms ont été modifiés). Je présenterai chaque scénario en détail en présentant les écueils mémorables liés à la gestion des microservices, telle que la difficulté de corréler l’augmentation de trafic et de ressources entre les serveurs frontaux et les composants de service. J’aborderai également la gestion des pannes et le calcul des SLOs du produit en prenant en compte les SLOs de l’ensemble des microservices. Enfin, je partagerai quelques conseils utiles qui, je l’espère, vous feront gagner du temps et vous éviteront de possibles pannes clients.
Scénario #1: PetPic
Le premier scénario s’articule autour d’un produit fictif appelé PetPic. Comme montré sur le schéma 2, PetPic est un service mondial qui propose des photos de chiens dans deux régions géographiques distinctes : Happytails et Furland. Le service a actuellement 100 clients dans chaque région, pour un total de 200 clients. L’API frontale tourne sur des machines indépendantes localisées dans chacune des régions. En tant que service complexe, PetPic est constitué de plusieurs composants mais pour les besoins de ce premier cas d’usage, nous ne considérerons que la base de données. La base de données est déployée dans le cloud sur une région globale et sert ainsi nos deux régions : Happytails et Furland.
Schéma 2: Le service mondial PetPic
>Problématique : Corréler les croissances de trafic
La base de données utilise 50% de ses ressources en pic de charge. En prenant ceci en compte, le PO a décidé d’implémenter une nouvelle fonctionnalité dans PetPic afin de proposer des photos de chats à ses clients. Une fois la fonctionnalité développée, l’équipe a décidé de déployer la nouvelle fonctionnalité d’abord dans la région Happytails. Ainsi, ils pourront surveiller si le trafic augmente de façon inattendue suite à un engouement des clients ou si l’utilisation des ressources est impactée avant de généraliser l’ouverture du service. Partant du principe que la base d’utilisateurs est la même dans les deux régions, cela semblait être une bonne stratégie.
Pour se préparer à la mise en production, les ingénieurs ont doublé les ressources de traitement du service API dans Happytails et augmenté les ressources de la base de données de 10%. Le lancement a été un succès, et 10% de clients supplémentaires ont rejoint PetPic prouvant ainsi que les amateurs de chats s’étaient connectés à la plateforme. La base de données consomme toujours 50% de ses ressources au pic d’utilisation, validant ainsi la nécessité des ressources supplémentaires allouées.
Tout indique qu’une croissance du nombre d’utilisateurs de 10% demande une augmentation équivalente des ressources de la base de données. En préparation du lancement sur Furland, les ingénieurs PetPic ont donc ajouté 10% de ressources en plus sur la base de données. Ils ont encore une fois doublé les ressources API pour faire face aux nouvelles requêtes. Ce sont exactement les mêmes changements réalisés que pour la région Happytails.
La nouvelle fonctionnalité a été lancée pour les utilisateurs de Furland un mercredi. Durant le déjeuner, les ingénieurs se sont mis à recevoir de nombreuses alertes : les utilisateurs recevaient des codes erreur HTTP 500 - c’est à dire qu’ils ne pouvaient pas utiliser le service. Par rapport au lancement d’Happytails, le résultat était complètement différent. A ce moment-là, l’équipe base de données a indiqué aux ingénieurs que les ressources utilisées par la base avaient dépassé les 80% deux heures auparavant (peu de temps après le lancement). Ils avaient essayé d’allouer plus de CPU pour gérer le trafic supplémentaire mais le changement ne pourrait probablement pas se faire dans la journée. Dans le même temps, l’équipe API avait vérifié l’augmentation du nombre d’utilisateurs mais sans remarquer d’écarts par rapport à ce qui était attendu : 220 utilisateurs étaient inscrits sur la plateforme. Puisque personne n’était capable de comprendre les raisons de la panne, ils décidèrent d’arrêter le lancement de la fonctionnalité pour Furland et de faire un retour arrière.
Schéma 3: Répercussions inattendues de l’augmentation du trafic
Le lancement pour Happytails a montré qu’une croissance du nombre d'utilisateurs de 10% était corrélée à une croissance de 10% du trafic vers la base de données. Cependant, après analyse des logs suite au lancement de Furland, l’équipe a observé une augmentation de trafic vers la base de données de 60% sans qu’un seul nouvel utilisateur se soit inscrit. Suite au retour arrière, les utilisateurs pressés de voir des photos de chats pendant leur pause déjeuner et mécontents ont ouvert de nombreux tickets au service client. Les ingénieurs apprirent ainsi que les utilisateurs de Furland étaient passionnés de chats et n’interagissaient que peu avec PetPic lorsqu'il n’y avait que des photos de chiens.
>Astuces
Ce scénario nous montre que la mise en place des photos de chats a été un immense succès dans Furland, mais que la stratégie de déploiement des nouvelles fonctionnalités n’aurait jamais pu prévoir un tel engouement. Une leçon importante à retenir : tous les produits connaissent des types de croissances différents. Dans notre cas de figure, la croissance du nombre d’utilisateurs est différente de la hausse d'intérêt des utilisateurs existants - et les différents types de croissance ne sont pas toujours corrélés. Les ressources systèmes nécessaires pour traiter les requêtes des utilisateurs varient en fonction des comportements, qui varient eux-mêmes selon de nombreux facteurs - comme la région géographique par exemple.
Afin de se préparer au lancement d’un nouveau produit dans plusieurs régions, il est utile de tester cette nouvelle fonctionnalité sur toutes les régions afin d’avoir une vision complète des impacts sur les comportements utilisateurs (et donc de l’utilisation des ressources). De même lorsqu’un lancement demande un ajout de ressources machines, il est important de donner du temps aux responsables des serveurs pour allouer les ressources nécessaires. Allouer une nouvelle machine nécessite de passer par une demande d’achats, du transport, et une installation physique du matériel. La stratégie de déploiement doit prendre en compte ce temps supplémentaire.
Scénario #2: Isolation des pannes
D’un point de vue architectural, le premier scénario a impliqué un service mondial déployé en tant que point de défaillance unique, ainsi qu’un déploiement local qui a provoqué une panne dans plusieurs régions distinctes. Dans le monde des monolithes, il est difficile, voire impossible, d’isoler les pannes entre les composants. La principale raison à cette difficulté provient du fait que tous les composants logiques coexistent dans le même binaire et donc dans le même environnement d’exécution. L’énorme avantage de travailler avec des microservices est de pouvoir isoler les pannes de chaque composant logique indépendant, et ainsi les empêcher de se répandre à travers le système et d’affecter les autres composants. Le processus d’analyse des pannes de service en vue de les contenir est souvent appelé l’isolation des pannes.
Dans notre exemple, PetPic est déployé indépendamment dans deux régions : Happytails et Furland. Cependant, les performances de ces deux régions sont fortement liées aux performances de la base de données globale commune aux deux régions. Comme observé jusqu’à présent, les clients d’Happytails et de Furland ont des intérêts distincts, rendant difficile l’ajustement de la base de données afin de répondre efficacement à leurs deux besoins. Une modification des interactions des utilisateurs d’une région avec la base de données peut affecter négativement l’expérience des utilisateurs d’une autre région, et vice versa.
Il est possible d’éviter ces problèmes, en utilisant par exemple un cache local comme illustré sur le schéma 4. Le cache local peut améliorer l’expérience utilisateur puisqu’il réduit également le délai de réponse et l’utilisation des ressources de la base de données. La taille du cache peut lui être adapté au trafic local plutôt qu’à l’usage mondial. Il peut également servir des données enregistrées dans le cas d’une panne des serveurs, permettant ainsi une dégradation moins brutale du service.
Le cache peut tout de même introduire des problématiques pour l’application ou pour respecter les besoins métiers - par exemple, si vous avez besoin de données mises à jour fréquemment ou de mise à l’échelle. Parmi les problèmes fréquents, on retrouve des temps de latence croissants à cause des restrictions sur les ressources et de cohérence lorsque plusieurs caches sont interrogés. Les services quant à eux ne devraient pas reposer sur du contenu en cache pour réaliser leurs opérations.
Schéma 4: Isolation de panne en utilisant un cache local dédié
Que dire des autres composants de notre architecture Produit ? Est-ce raisonnable de tout mettre en cache ? Peut-on isoler des services hébergés dans le Cloud sur des régions spécifiques ? La réponse à ces deux questions est oui, et il faut implémenter ces stratégies lorsque cela est possible. Utiliser un service dans le Cloud ne l’empêche pas d’être la cause d’une panne globale. Un service déployé dans plusieurs régions du cloud peut aussi se comporter comme un service global et être un point unique de défaillance. Isoler la panne d’un service à un domaine donné est une décision architecturale, et ne peut être garantie que par l’infrastructure sous-jacente.
Considérons un scénario différent avec PetPic, mais en se concentrant cette fois sur le composant de contrôle. Ce composant effectue une série de vérifications sur la qualité du contenu. L’équipe de développement a récemment intégré un module de détection automatique des abus basé sur de l’apprentissage automatique (ML) à ce composant, qui permet de valider chaque nouvelle photo dès qu’elle est téléchargée. Les problèmes commencent lorsqu’un utilisateur d’Happytails démarre le téléchargement d’un grand nombre de photos de différents animaux dans PetPic, alors que la plateforme ne permet que des photos de chiens et de chats. L’afflux de téléchargements active la détection automatique des abus, mais le nouvel algorithme ne peut suivre le nombre de requêtes.
Le composant s’exécute sur un pool de 1000 threads et limite à la moitié le nombre de threads alloués à la détection d’abus (500 dans notre cas). Ceci devait empêcher une surutilisation des threads si, comme dans notre exemple, de nombreuses et longues requêtes étaient reçues en même temps. Mais ce qui n’avait pas été prévu, c’est que la moitié des threads finiraient par consommer toute la mémoire et le CPU disponibles, causant ainsi de fortes latences pour les utilisateurs des deux régions qui souhaitent télécharger des images sur PetPic.
Comment peut-on éviter la gêne occasionnée dans ce scénario ? En isolant les opérations du composant de contrôle à une seule région, il est possible de restreindre ce type de situations. Même si un service est hébergé dans le Cloud, s’assurer que chaque région possède sa propre instance de contrôle assure que seuls les clients d’Happytails seront impactés par cet afflux de téléchargements. Les services sans état peuvent facilement être isolés. Isoler les bases de données n’est en revanche pas toujours possible, mais un bon compromis serait d’effectuer des requêtes de lecture depuis le cache et de mettre en place une cohérence inter-région à terme. Plus la couche de traitement est géographiquement isolée, mieux c’est.
>Astuces
Pour empêcher des pannes répandues, il est préférable de conserver tous les services d’une même couche colocalisés et restreints à une même zone de panne. Isoler des services sans état est souvent plus facile que d’isoler des services avec état. S’il n’est pas possible d’éviter une communication inter-région, il faut réfléchir à des stratégies de dégradation minimale et de cohérence à terme.
Scénario #3: Prévoir des objectifs de niveaux de service (SLO)
Dans ce dernier scénario, nous regarderons les Objectifs de niveaux de service (SLOs) de PetPic, et vérifierons les SLOs de chacun des composants. En résumé, les SLOs sont les objectifs de service attendus qui peuvent être transformés contractuellement en accords de niveaux de service (SLA) avec nos clients. Regardons le schéma 5:
Schéma 5: Objectifs de niveaux de service du produit PetPic
Ce tableau montre que l’équipe SLO pense être capable de fournir une bonne expérience utilisateur de PetPic. On peut également y voir les SLOs proposés par chaque composant interne. A noter, les SLOs de l’API doivent être déterminés en fonction des SLOs de chaque composant de service (comme les composants de Contrôle et de base de données). Si de meilleurs SLOs pour l’API sont attendus mais impossibles à obtenir, nous devrions considérer un changement d’architecture du produit et travailler avec les responsables de composants pour fournir de meilleures performances et disponibilité. En prenant en compte notre dernière architecture pour PetPic, regardons si les SLOs pour l’API ont un sens.
Commençons avec notre serveur opérationnel (Ops), qui est un composant permettant de collecter plusieurs métriques à propos des APIs PetPic. Le service API appelle Ops uniquement pour fournir des données de supervision autour des requêtes, erreurs et temps de traitement des opérations. Toutes les écritures pour le composant Ops sont faites de façon asynchrone, et les erreurs n’impactent pas la qualité de service de l’API. En gardant ceci en tête, nous pouvons donc exclure les SLOs Ops du calcul des SLOs globaux pour PetPic.
Schéma 6: Corréler les SLOs de lecture à la Base de données
Maintenant, étudions le parcours utilisateur pour afficher une image de PetPic. La qualité du contenu n’est vérifiée qu’au téléchargement de l’image sur PetPic, donc les lectures ne seront pas affectées par les performances du service de contrôle. En plus de récupérer les informations de l’image, le service API doit traiter la requête ce qui prend environ 30 ms d’après notre benchmark. Une fois que l’image est prête à être affichée, l’API construit une réponse, ce qui prend environ 20 ms en moyenne. Ce qui nous fait 50ms de traitement au total pour la seule API.
Si nous pouvons garantir qu’au moins la moitié des requêtes trouveront un résultat dans le cache, alors cibler un SLO de 100 ms au 50è percentile est plutôt raisonnable. Notons que sans le cache local, la latence de la requête serait au moins de 150 ms. Pour toutes les autres requêtes, l’image doit être récupérée depuis la base de données. La base demande environ 100 à 240 ms pour répondre, et peut ne pas être colocalisée avec le service API. La latence réseau est en moyenne de 100 ms. En considérant le pire scénario, le plus long délai que pourrait prendre une requête serait de 50ms (traitement API) + 10ms (échec du cache) + 100ms (réseau) + 240 ms (base de données), soit un total de 400 ms. En regardant le SLO dans la colonne de gauche du schéma 6, nous pouvons voir que les chiffres sont bien en phase avec l’architecture de l’API.
Schéma 7: Corréler les SLOs d’écriture à la Base de données et au Contrôle
En suivant la même logique, vérifions les SLOs pour télécharger une image sur le serveur. Quand un utilisateur envoie une image à PetPic, l’API doit demander au composant de Contrôle de vérifier le contenu, ce qui prend entre 150 et 800 ms. En plus de contrôler le contenu abusif, le composant vérifie également si l’image existe déjà dans la base de données. Les images existantes sont considérées comme vérifiées (et n’ont pas besoin d’être revérifiées). Les données montrent que les utilisateurs de Furland et Happytails ont tendance à télécharger les mêmes images dans les deux régions. Quand une image est déjà présente en base de données, le composant de contrôle peut créer un nouvel identifiant pour elle sans dupliquer de données, ce qui prend environ 50 ms. Ce parcours correspond à environ la moitié des requêtes en écriture, avec une latence totale au 50è percentile de 250 ms.
Les images avec du contenu abusif prennent généralement plus de temps. La limite de temps du composant de Contrôle pour retourner une réponse est de 800 ms. De plus, si une image de chien ou de chat est valide, et n’existe pas dans la base de données, le composant Data peut prendre jusqu'à 1000 ms pour l’enregistrer. En prenant tout en considération, le pire scénario pourrait prendre jusqu’à presque 2000 ms pour retourner une réponse. Comme montré sur le schéma 7, 2000 ms est bien au-delà des SLOs actuels imaginés pour les écritures, ce qui indique que les scénarios en erreur ont probablement été oubliés au moment de proposer les objectifs. Pour corriger la situation, vous pourriez proposer de mettre un SLO avec la limite maximale au 99è percentile. Cette situation pourrait toutefois mener à de mauvaises performances de notre service. Par exemple, la base de données pourrait finir d’écrire l’image seulement après que l’API ait retourné un message d’erreur pour cause de délai dépassé, ce qui porterait à confusion pour l’utilisateur. Dans ce cas, la meilleure stratégie est de travailler avec l’équipe base de données pour améliorer les performances de la base ou pour ajuster les SLOs d’écriture pour PetPic.
>Astuces
Il est important de s’assurer que vos produits distribués offrent les bons SLOs aux clients. En donnant un SLO global, il est nécessaire de prendre en compte les SLOs de tous les composants. Considérez tous les parcours utilisateurs possibles et les différents chemins qu’une requête peut parcourir pour générer une réponse. Si de meilleurs SLOs sont attendus, il vous faudra changer l’architecture de vos services ou travailler avec les responsables des composants pour améliorer le service. Conserver les composants colocalisés facilite grandement la garantie de SLOs cohérents.
A propos des Auteurs
Silvia Esparrachiari a été ingénieure logiciel chez Google pendant 11 ans, où elle a travaillé en Confidentialité des Données, Spam et Prévention des Abus, et plus récemment chez Google Cloud SRE. Elle possède un Bachelor en Sciences Moléculaires et un Master en Vision par Ordinateur et Interactions Homme-Machine. Chez Google, elle se concentre actuellement à promouvoir un environnement de respect et de diversité où les personnes peuvent développer leurs compétences techniques.
Betsy Beyer est Rédactrice Technique pour Google à New York spécialisée en Ingénierie de Fiabilité des Sites (SRE). Elle a coécrit Site Reliability Engineering: How Google Runs Production Systems (2016), The Site Reliability Workbook: Practical Ways to Implement SRE (2018), and Building Secure and Reliable Systems (2020). Pour en arriver là, Betsy a étudié les relations internationales et la littérature anglaise, et a des diplômes de Stanford et Tulane.