Les services peuvent présenter toutes sortes de fiabilité et de résilience intégrées, mais afin que la fiabilité soit effective, ils doivent également traiter les pannes prévisibles lorsqu'elles se produisent. Chez Amazon, nous créons des services horizontalement évolutifs et redondants afin de pallier l'obsolescence programmée du matériel. Tous les disques durs ont une durée de vie maximale estimée et tout logiciel est susceptible de planter tôt ou tard. On pourrait croire que l'état d'un serveur est binaire : soit il fonctionne, soit il ne fonctionne pas du tout et n'est plus utilisable. Malheureusement, ce n'est pas le cas. Nous avons découvert qu'au lieu de simplement s'arrêter, les serveurs en panne peuvent endommager les systèmes de façon inattendue et parfois disproportionnée. Les vérifications de l'état détectent et résolvent automatiquement ces types de problèmes.

Cet article explique comment nous utilisons les vérifications de l'état pour détecter et traiter chaque panne de serveur, ce qu'il se passe lorsque les vérifications de l'état ne sont pas utilisées et comment les systèmes qui surréagissent aux pannes de vérification de l'état peuvent transformer de petits problèmes en interruptions totales. Il fournit également des informations sur notre expérience chez Amazon en ce qui concerne l'équilibre des compromis entre les différents types d'implémentation des vérifications de l'état.

Les petites pannes ayant des incidences disproportionnées

Lors de mes débuts en tant que développeur logiciel chez Amazon, je travaillais sur le groupe de rendu de site Web derrière Amazon.com. Alors que je travaillais sur une modification pour ajouter une instrumentation et obtenir une visibilité sur l'exécution effective du logiciel, j'ai malencontreusement écrit un bogue. Le bogue se déclenchait rarement, mais lorsqu'il se déclenchait, un serveur Web donné rendait des pages d'erreur vides à chaque requête. La seule manière de résoudre le problème était de redémarrer le processus du serveur Web. Nous avons détecté le bogue et restauré la modification rapidement, ajouté de nombreux tests et amélioré les processus pour rattraper les situations comme celle-ci à l'avenir. Mais alors que le bogue était en production, quelques serveurs d'une grande flotte ont fini par ne plus fonctionner.
 
Le bogue était particulièrement difficile à trouver parce que le serveur ne comprenait pas vraiment qu'il était en mauvais état. En outre, le serveur avait perdu sa capacité à transmettre des rapports de son état aux systèmes de surveillance, c'est pourquoi il n'a pas automatiquement été mis hors service et n'a pas déclenché ses alarmes habituelles. Pire encore, le serveur était devenu très rapide et commençait à produire des pages d'erreur blanches beaucoup plus rapidement que ses « serveurs en bon état » homologues ne rendaient des pages de sites Web normales. La technologie d'équilibrage de charge que nous utilisions à l'époque privilégiait les serveurs rapides par rapport aux serveurs lents, et redirigeait donc une quantité de trafic disproportionnée vers les serveurs en mauvais état, empirant ainsi davantage l'impact.

Étant donné que la surveillance implique la mesure des taux d'erreurs et de la latence depuis différents points du système, d'autres alarmes se déclenchaient. Alors que ces types de systèmes de surveillance et de processus opérationnels peuvent servir de barrières pour contenir le problème, des vérifications de l'état appropriées peuvent minimiser l'impact de cette classe entière d'erreurs de manière significative en détectant les pannes et en prenant des mesures rapidement.

Les compromis des vérifications de l'état

Les vérifications de l'état permettent de demander un service sur un serveur précis, qu'il soit en mesure ou non d'effectuer les tâches avec succès. Les équilibreurs de charge envoient cette question régulièrement à chaque serveur pour déterminer vers quels serveurs le trafic peut être directement redirigé sans danger. Un service qui interroge des messages d'une file d'attente peut se demander si elle est en bon état avant d'interroger davantage la file d'attente. Les agents de surveillance (qui s'exécutent sur chaque serveur ou sur une flotte de surveillance externe) peuvent demander aux serveurs s'ils sont en bon état afin de pouvoir déclencher une alarme ou traiter automatiquement les serveurs qui sont en panne.

Comme nous l'avons vu dans mon exemple de bogue de site Web, lorsqu'un serveur en mauvais état reste en service, il peut diminuer la disponibilité du service dans son ensemble de manière disproportionnée. Avec une flotte de 10 serveurs, lorsqu'un serveur est en mauvais état, la disponibilité de la flotte passe à 90 % ou moins. Pire encore, certains algorithmes d'équilibrage de charge, tels que « le moins de requêtes », donnent plus de travail aux serveurs les plus rapides. Lorsqu'un serveur tombe en panne, il commence souvent à ne plus répondre aux requêtes rapidement, créant ainsi un « trou noir » dans la flotte de service en attirant plus de requêtes que les serveurs en bon état. Dans certains cas, nous ajoutons une protection supplémentaire pour empêcher les trous noirs en ralentissant les requêtes des serveurs en panne pour que leur latence corresponde à la latence moyenne des requêtes réussies. Toutefois, dans d'autres scénarios, comme avec les interrogateurs de files d'attente, ce problème est bien plus difficile à résoudre. Par exemple, si un interrogateur de file d'attente interroge des messages aussi vite qu'il les reçoit, un serveur en panne deviendra alors un trou noir également. Avec un ensemble d'environnements aussi varié pour distribuer le travail, nous envisageons la protection des serveurs partiellement en panne de manières différentes d'un système à l'autre.

Nous pensons que les serveurs tombent en panne de manière indépendante pour plusieurs raisons, y compris les disques qui deviennent protégés en écriture et qui entraînent l'échec immédiat des requêtes, l'asymétrie soudaine des horloges qui entraînent des appels aux dépendances pour échec d'authentification, les serveurs qui ne parviennent pas à récupérer le matériel cryptographique mis à jour et qui entraînent l'échec du déchiffrage et du chiffrement, le plantage des processus de prise en charge essentiels à cause de leurs propres bogues, les fuites de mémoire et les verrous qui figent le traitement.

Les serveurs tombent également en panne pour des raisons corrélées qui entraînent l'échec simultané de plusieurs ou de tous les serveurs d'une flotte. Ces raisons corrélées peuvent être, par exemple, la panne d'une dépendance partagée ou des problèmes de réseau à grande échelle. La vérification de l'état idéale teste chaque aspect de l'état du serveur et de l'application, parfois même en vérifiant que les processus de prise en charge non essentiels sont bien en cours d'exécution. Cependant, des problèmes peuvent survenir lorsque la vérification de l'état échoue pour une raison non critique ou lorsque la panne est corrélée entre les serveurs. Si l'automatisation supprime les serveurs d'un service alors qu'ils auraient encore pu effectuer un travail utile, l'automatisation est plus dommageable qu'utile.

La difficulté avec les vérifications de l'état réside dans la tension entre, d'un côté, les avantages de vérifications de l'état complètes et la minimisation rapide des pannes de serveurs uniques et, de l'autre côté, les dommages causés par une panne qui est un faux positif dans la flotte tout entière. Ainsi, l'une des difficultés de la création d'une bonne vérification de l'état est de se prémunir contre les faux positifs. En général, cela signifie que l'automatisation autour des vérifications de l'état devrait arrêter la redirection du trafic vers un seul serveur en mauvais état, mais continuer de permettre la redirection du trafic si la flotte tout entière semble rencontrer un problème.

Méthode d'évaluation de l'état

Les raisons pour lesquelles un serveur peut tomber en panne sont nombreuses, de même que les endroits dans nos systèmes où nous mesurons l'état des serveurs. Certaines vérifications de l'état peuvent signaler la panne indépendante d'un serveur avec certitude, tandis que d'autres sont plus floues et signalent des faux positifs en cas de pannes corrélées. Certaines vérifications de l'état sont difficiles à implémenter. D'autres sont implémentées lors de la configuration avec des services comme Amazon Elastic Compute Cloud (Amazon EC2) et Elastic Load Balancing. Chaque type de vérification de l'état a ses avantages.

Vérifications de présence

Les vérifications de présence testent la connectivité de base à un service et la présence d'un processus de serveur. Elles sont souvent réalisées par un équilibreur de charge ou un agent de surveillance externe, et ne savent pas comment fonctionnent les applications. Les vérifications de présence sont généralement incluses avec le service et ne nécessitent pas d'auteur d'application pour implémenter quoi que ce soit. Voici certains exemples de vérifications de présence que nous utilisons au sein d'Amazon :

• Les tests qui confirment qu'un serveur écoute sur son port prévu et accepte les nouvelles connexions TCP ;
• Les tests qui effectuent des requêtes HTTP de base et vérifient que le serveur répond bien à un code d'état 200 ;
• Les vérifications de l'état pour Amazon EC2 qui testent des aspects de base nécessaires au fonctionnement de tout système, tels que l'accessibilité du réseau.

Vérifications de l'état locales

Les vérifications de l'état locales vont plus loin que les vérifications de présence et vérifient que l'application devrait être en mesure de fonctionner. Ces vérifications de l'état testent des ressources qui ne sont pas partagées avec les homologues du serveur. Par conséquent, elles sont peu susceptibles d'échouer sur plusieurs serveurs de la flotte simultanément. Ces vérifications de l'état testent les éléments suivants :

• L'incapacité à écrire ou à lire le disque : le serveur essaye peut-être de penser qu'un service sans état ne nécessite pas de disque en écriture. Cependant, chez Amazon, nos services utilisent généralement leurs disques pour les actions telles que la surveillance, la connexion et la publication de données de mesure asynchrones.
• Les plantages ou les pannes de processus essentiels : certains services prennent les requêtes à l'aide d'un proxy sur serveur (comme NGINX) et effectuent leur logique métier dans un autre processus de serveur. Une vérification de présence peut seulement vérifier si le processus de proxy est en cours d'exécution. Un processus de vérification de l'état local peut passer du proxy à l'application pour vérifier que les deux sont en cours d'exécution et qu'ils répondent correctement aux requêtes. Fait intéressant, dans l'exemple de site Web de début d'article, la vérification de l'état existante était assez approfondie pour garantir que le processus de rendu s'exécutait et répondait, mais pas assez approfondie pour garantir qu'il répondait correctement.
• Les processus de prise en charge manquants : les hôtes qui n'ont pas leurs démons de surveillance peuvent laisser les opérateurs « à l'aveugle » et ignorants de l'état de leurs services. Les autres processus de prise en charge poussent les enregistrements d'utilisation de mesure et de facturation ou reçoivent des mises à jour d'informations d'identification. Les serveurs avec des processus de prise en charge en panne mettent les fonctionnalités en danger de manière subtile, difficile à détecter.

Vérifications de l'état de la dépendance

Les vérifications de l'état de la dépendance sont un contrôle approfondi de la capacité d'une application à interagir avec ses systèmes adjacents. Ces vérifications décèlent parfaitement les problèmes locaux du serveur, tels que les informations d'identification expirées, qui l'empêchent d'interagir avec une dépendance. Mais elles peuvent également présenter des faux positifs lorsque les problèmes concernent la dépendance elle-même. À cause de ces faux positifs, nous devons faire preuve de prudence lorsque nous réagissons aux échecs de vérification de l'état de la dépendance. Les vérifications de l'état de la dépendance peuvent tester les éléments suivants :

• Une mauvaise configuration ou des métadonnées obsolètes : si un processus cherche des mises à jour des métadonnées ou de la configuration de manière asynchrone, mais que le mécanisme de mise à jour est en panne sur un serveur, le serveur peut se désynchroniser de manière significative avec ses homologues et avoir un comportement défectueux, imprévisible et non testé. Cependant, lorsqu'un serveur ne voit pas une mise à jour pendant un certain temps, il ne sait pas si le mécanisme de mise à jour est en panne ou si le système de mise à jour central a arrêté de publier des mises à jour pour tous les serveurs.
• L'incapacité à communiquer avec des dépendances ou des serveurs pairs : on sait qu'un comportement de réseau étrange affecte la capacité d'un sous-ensemble de serveurs d'une flotte à communiquer avec les dépendances, mais n'affecte pas la capacité d'envoi du trafic vers ce serveur. Les problèmes de logiciel, tels que les verrous ou les bogues liés aux groupes, peuvent également entraver la communication réseau.
• D'autres bogues de logiciel inhabituels qui nécessitent un retour au processus : les verrous, les fuites de mémoire et les bogues de corruption de l'état peuvent entraîner l'obtention d'erreurs de la part du serveur. 

Détection des anomalies

La détection des anomalies vérifie tous les serveurs d'une flotte pour déterminer si l'un d'eux se comporte de manière étrange par rapport à ses homologues. En rassemblant les données de surveillance par serveur, nous pouvons comparer en continu les taux d'erreurs, les données de latence et d'autres attributs afin de trouver les serveurs anormaux et les mettre automatiquement hors service. La détection des anomalies peut trouver des divergences au sein d'une flotte que les serveurs ne peuvent pas détecter pour eux-mêmes, par exemple :

• Les décalages d'horloge : nous savons que les horloges peuvent se décaler de manière soudaine et drastique, en particulier lorsque les serveurs sont sous forte charge. Les mesures de sécurité, telles que celles utilisées pour évaluer les requêtes signées pour AWS, nécessitent que l'heure de l'horloge du client affiche moins de cinq minutes de décalage par rapport à l'heure réelle. Dans le cas contraire, les requêtes ne parviennent pas aux services AWS.
• Un code ancien : si un serveur est déconnecté du réseau ou éteint pendant longtemps et qu'il se reconnecte, il peut exécuter un code dangereusement obsolète qui est incompatible avec le reste de la flotte.
• Tout mode de défaillance inattendue : il arrive que l'échec d'un serveur entraîne le renvoi d'une erreur que le serveur identifie comme provenant du client au lieu de lui-même (HTTP 400 au lieu de 500). Les serveurs peuvent ralentir au lieu de planter ou répondre plus rapidement que leurs homologues, signe qu'ils renvoient des réponses erronées à leurs mandataires. La détection des anomalies est un formidable passe-partout pour les modes de défaillance inattendue.

Pour que la détection des anomalies fonctionne dans la pratique, les points suivants doivent être vérifiés :

• Les serveurs doivent effectuer à peu près la même action : lorsque nous acheminons explicitement différents types de trafic vers différents types de serveurs, les serveurs peuvent se comporter assez différemment pour détecter les cas particuliers. Cependant, lorsque nous utilisons des équilibreurs de charge pour diriger le trafic vers les serveurs, ils répondent généralement de la même manière.
• Les flottes doivent être relativement homogènes : dans les flottes qui incluent différents types d'instances, certaines instances peuvent être plus lentes que les autres, pouvant ainsi déclencher à tort une détection passive de serveurs en mauvais état. Pour contourner ce scénario, nous recueillons des métriques par type d'instance.
• Les erreurs ou les différences de comportement doivent être signalées : étant donné que nous comptons sur les serveurs eux-mêmes pour signaler les erreurs, que se passe-t-il lorsque leurs systèmes de surveillance sont aussi en panne ? Heureusement, le client d'un service est un excellent moyen d'ajouter une instrumentation. Les équilibreurs de charge comme l'Équilibreur de charge d'application publient des journaux d'accès qui indiquent quel serveur backend a été contacté à chaque requête, le temps de réponse et si la requête a réussi ou échoué. 

Réagir en toute sécurité aux échecs de vérification de l'état

Lorsqu'un serveur détermine qu'il est en mauvais état, il peut effectuer deux types d'actions. Dans les cas les plus extrêmes, il peut décider localement qu'il ne devrait pas recevoir de travail et se mettre lui-même hors service en faisant échouer une vérification de l'état de l'équilibreur de charge ou en arrêtant l'interrogation d'une file d'attente. Le serveur peut également réagir en informant une autorité centrale qu'il a un problème et en laissant le système central décider comment gérer le problème. Le système central peut traiter le problème en toute sécurité sans laisser l'automatisation mettre hors service la flotte tout entière.

Il existe différentes façons d'implémenter les vérifications de l'état et d'y répondre. Cette section décrit quelques schémas que nous utilisons au sein d'Amazon.

« Échec ouvert »

Certains équilibreurs de charge agissent comme des autorités centrales intelligentes. En cas d'échec de la vérification de l'état d'un serveur, l'équilibreur de charge arrête de lui envoyer du trafic. Mais si les vérifications de l'état de tous les serveurs échouent en même temps, l'équilibreur de charge échoue et reste ouvert, permettant ainsi le trafic vers tous les serveurs. Nous pouvons utiliser les équilibreurs de charge pour prendre en charge l'implémentation sécurisée d'une vérification de l'état de la dépendance, peut-être en incluant un équilibreur de charge qui interroge sa base de données et ses vérifications pour s'assurer que ses processus de prise en charge non essentiels s'exécutent.

Par exemple, l'équilibreur de charge Network Load Balancer AWS échoue et reste ouvert si aucun serveur n'est considéré comme étant en bon état. Il échoue également pour les zones de disponibilité en mauvais état si tous les serveurs d'une zone de disponibilité sont signalés comme étant en mauvais état. (Pour plus d'informations sur l'utilisation d'équilibreurs de charge Network Load Balancer pour les vérifications de l'état, consultez la documentation Elastic Load Balancing.) Notre Équilibreur de charge d'application prend également en charge ce mode, tout comme Amazon Route 53. (Pour plus d'informations sur la configuration des vérifications de l'état à l'aide de Route 53, consultez la documentation de Route 53.)

Lorsque nous comptons sur ce comportement, nous nous assurons de tester les modes de défaillance de la vérification de l'état de la dépendance. Par exemple, imaginons un service où les serveurs se connectent à un magasin de données partagé. Si ce magasin de données devient lent ou répond avec un taux d'erreurs plus bas, les vérifications de l'état de la dépendance des serveurs peuvent échouer de temps à autre. Dans ce scénario, les serveurs passent de en service à hors service, mais le seuil « échec ouvert » n'est pas déclenché. Il est important de réfléchir et de tester les défaillances partielles des dépendances avec ces vérifications de l'état pour éviter que les vérifications de l'état approfondies n'aggravent la situation.

Alors que le comportement « échec ouvert » est utile, nous avons tendance, chez Amazon, à nous méfier des cas qui ne nous permettent pas de réfléchir correctement ou que nous ne pouvons pas tester dans toutes les situations. Nous n'avons pas encore trouvé de preuves générales selon lesquelles le comportement « échec ouvert » se déclenche comme nous le prévoyons pour tous les types de surcharge, de défaillance partielle ou de défaillance gray dans un système ou dans les dépendances du système. À cause de cette limite, les équipes d'Amazon ont tendance à restreindre leurs vérifications de l'état de l'équilibreur de charge à action rapide aux vérifications de l'état locales et à compter sur la réaction vigilante des systèmes centralisés aux vérifications de l'état de la dépendance plus approfondies. Il ne s'agit pas de dire que nous n'utilisons pas le comportement « échec ouvert » ou de prouver qu'il fonctionnent dans des cas particuliers, mais que lorsque la logique peut agir sur de nombreux serveurs rapidement, nous sommes extrêmement prudents quant à cette logique.

Vérifications de l'état sans disjoncteur

Permettre aux serveurs de réagir à leurs propres problèmes peut sembler constituer la solution la plus rapide et la plus simple. Toutefois, c'est également la solution la plus risquée si le serveur se trompe sur son état ou s'il n'a pas une vue d'ensemble de ce qu'il se passe dans toute la flotte. Lorsque tous les serveurs de la flotte prennent la même et mauvaise décision en même temps, ils peuvent entraîner des défaillances en cascade à travers les services adjacents. Ce risque nous met face à un compromis. En cas d'écart entre la vérification de l'état et la surveillance , un serveur peut réduire la disponibilité d'un service jusqu'à ce que le problème soit détecté. Cependant, ce scénario évite une panne de service totale en raison d'un comportement de vérification de l'état inattendu dans la flotte tout entière.

Voici les bonnes pratiques que nous suivons pour implémenter les vérifications de l'état lorsque nous n'avons pas de disjoncteur intégré :

• Configurer le producteur de travail (équilibreur de charge, thread d'interrogation de file d'attente) pour effectuer les vérifications de l'état de présence et locales. Les serveurs sont mis automatiquement hors service par l'équilibreur de charge uniquement s'ils ont un problème qui leur est clairement propre, tel qu'un disque défectueux.
• Configurer d'autres systèmes de surveillance externes pour qu'ils effectuent des vérifications de l'état de la dépendance et des détections d'anomalies. Ces systèmes peuvent tenter de résilier automatiquement des instances ou des alarmes ou d'impliquer un opérateur.

Lorsque nous construisons des systèmes qui réagissent automatiquement aux échecs de vérifications de l'état de la dépendance, nous devons définir correctement les seuils pour empêcher le système automatisé de prendre des mesures drastiques de manière inattendue. Les équipes Amazon qui utilisent des serveurs avec état comme Amazon DynamoDB, Amazon S3 et Amazon Relational Database Service (Amazon RDS) ont des exigences importantes de durabilité en matière de remplacement de serveurs. Elles ont également construit des limitations de vitesse et des boucles de rétroaction de contrôle prudentes pour que l'automatisation s'arrête et implique des humains lorsque des seuils sont franchis. Lorsque nous construisons cette automatisation, nous devons nous assurer de bien pouvoir remarquer les échecs de vérifications de l'état de la dépendance. Pour certaines métriques, nous comptons sur les serveurs pour qu'ils auto-signalent leur propre état à un système de surveillance central. Pour compenser les situations où le serveur est tellement défaillant qu'il ne peut pas signaler son propre état, nous les contactons activement pour vérifier leur état. 

Privilégier votre état

Il est important que les serveurs privilégient leurs vérifications de l'état par rapport à leur travail habituel, surtout lorsqu'ils sont en surcharge. Dans ces cas, l'échec des vérifications de l'état ou une réponse lente à celles-ci peut empirer une situation de baisse de tension déjà mauvaise. 

Lorsqu'un serveur échoue une vérification de l'état de l'équilibreur de charge, il demande à l'équilibreur de charge de le mettre immédiatement hors service pour une durée non négligeable. Lorsqu'un serveur seul échoue, ce n'est pas un problème, mais en cas de pic de trafic vers le service, la dernière chose que nous voulons est diminuer la taille du service. La mise hors service de serveurs pendant une surcharge peut entraîner un cercle vicieux. Si les serveurs restants sont forcés de prendre encore plus de trafic, leur surcharge peut augmenter et ils peuvent échouer une vérification de l'état, diminuant ainsi la flotte de plus belle.

Le problème, c'est que lorsqu'ils sont surchargés, les serveurs retournent des erreurs indiquant que les serveurs ne répondent pas à la requête de ping de l'équilibreur de charge à temps. Après tout, les vérifications de l'état de l'équilibreur de charge sont configurées avec des délais, comme n'importe quel autre appel de service à distance. Les serveurs en baisse de tension répondent lentement pour plusieurs raisons, y compris la contention élevée du processeur, les cycles de nettoyage de mémoire longs ou le simple manque de threads de travail. Les services doivent être configurés pour définir des ressources supplémentaires pour répondre aux vérifications de l'état en temps voulu au lieu de prendre trop de requêtes supplémentaires.

Heureusement, il existe quelques bonnes pratiques de configuration simples que nous suivons pour mieux empêcher ce type de cercle vicieux. Les outils comme les iptables, et même certains équilibreurs de charge, prennent en charge la notion de « nombre maximal de connexions ». Dans ce cas, le système d'exploitation (ou l'équilibreur de charge) limite le nombre de connexions au serveur pour que le processus du serveur ne soit pas submergé de requêtes simultanées qui le ralentiraient.

Lorsqu'un service est géré par un proxy ou un équilibreur de charge qui prend en charge le nombre maximal de connexions, il semble logique de faire correspondre le nombre de threads de travail sur le serveur HTTP au nombre maximal de connexions dans le proxy. Toutefois, avec cette configuration, le service tomberait dans un cercle vicieux en cas de baisse de tension. Les vérifications de l'état du proxy nécessitent également des connexions, c'est pourquoi il est important qu'un groupe de travail de serveur soit assez grand pour répondre à des requêtes de vérification de l'état supplémentaires. Les threads de travail inactifs sont peut coûteux, c'est pourquoi nous avons tendance à en configurer davantage : une poignée de threads de travail supplémentaires pour doubler le nombre de connexions maximales de proxy configuré.

Nous utilisons également une autre stratégie pour privilégier les vérifications de l'état qui consiste à configurer les serveurs pour qu'ils implémentent leur propre application de requêtes simultanées maximales. Dans ce cas, les vérifications de l'état de l'équilibreur de charge sont toujours autorisées, mais les requêtes normales sont rejetées si le serveur travaille déjà sur un seuil. Les implémentations autour d'Amazon vont de simples sémaphores dans Java aux analyses de tendances d'utilisation de CPU les plus complexes.

Un autre moyen de mieux s'assurer que les services répondent en temps voulu à une demande de ping de vérification de l'état consiste à effectuer la logique de vérification de l'état de la dépendance dans un thread en arrière-plan et de mettre à jour un indicateur isHealthy qui sera vérifié par la logique de ping. Dans ce cas, les serveurs répondent rapidement aux vérifications de l'état et la vérification de l'état de la dépendance produit une charge prévisible sur le système externe avec lequel elle interagit. Lorsque l'équipe procède ainsi, elle fait très attention à la détection d'une défaillance du thread de vérification de l'état. Si le thread en arrière-plan existe, le serveur ne détecte pas de défaillance de serveur à venir (ou de reprise !).

Équilibrer les vérifications de l'état de la dépendance avec la portée de l'impact

Les vérifications de l'état de la dépendance sont intéressantes parce qu'elles agissent comme des tests approfondis de l'état des serveurs. Malheureusement, elles peuvent être dangereuses parce qu'une dépendance peut entraîner une défaillance en cascade dans l'ensemble du système.

Nous pouvons trouver des idées pour la gestion des dépendances de vérification de l'état en regardant notre architecture Amazon axée sur les services. Chez Amazon, chaque service est conçu pour effectuer une petite quantité de tâches. Il n'y a pas de monolithe qui s'occupe de tout. Nous aimons construire des services de cette façon pour de nombreuses raisons, y compris l'innovation plus rapide avec de petites équipes et une portée de l'impact réduite en cas de problème avec un service. Cette conception architecturale peut s'appliquer également aux vérifications de l'état.

Lorsqu'un service appelle un autre service, il prend une dépendance sur ce service. Si un service n'appelle la dépendance que de temps en temps, nous pouvons considérer la dépendance comme une « dépendance légère », puisque le service ne peut effectuer que certains types de tâches, même s'il peut communiquer avec la dépendance. Sans protection « échec ouvert », implémenter une vérification de l'état qui teste une dépendance transforme cette dépendance en « dépendance dure ». Si la dépendance est hors service, le service se met hors service également, créant ainsi une défaillance en cascade avec une portée de l'impact augmentée.

Même si nous divisons les fonctionnalités en différents services, chaque service servira certainement plusieurs API. Parfois, les API du service ont leurs propres dépendances. Si une API est affectée, nous préférons que le service continue de servir les autres API. Par exemple, un service peut être à la fois un plan de contrôle (comme les API parfois appelées CRUD sur les ressources ayant une longue durée de vie) et un plan de données (API indispensables aux entreprises à débit élevé). Nous souhaitons que les API de plans de données continuent de fonctionner même si les API de plan de contrôle ont du mal à communiquer avec leurs dépendances.

Parallèlement, même une seule API peut se comporter différemment en fonction de l'entrée ou de l'état des données. Il est courant qu'une API de lecture interroge une base de données, mais met en cache les réponses localement pendant un certain temps. Si la base de données est hors service, le service peut toujours servir des lectures en cache jusqu'à ce que la base de données soit de nouveau en ligne. Les échecs de vérifications de l'état dues à un seul chemin de code en mauvais état augmentent la portée de l'impact d'un problème de communication avec une dépendance.

Cette question de savoir quelle dépendance doit être vérifiée soulève une question intéressante sur les compromis entre les microservices et les services relativement monolithiques. Il existe rarement une règle claire du nombre d'unités ou de points de terminaison déployables dans lesquels interrompre un service, mais les questions de savoir pour quelles dépendances effectuer une vérification de l'état et de savoir si un échec augmente la portée de l'impact ou non permettent de déterminer si un service doit être un microservice ou un macroservice et dans quelle mesure. 

Exemples pratiques de mauvaises expériences avec les vérifications de l'état

Tout cela semble logique dans la théorie, mais qu'arrive-t-il en réalité aux systèmes lorsqu'ils ne reçoivent pas les vérifications de l'état correctement ? Nous avons cherché des exemples d'expérience de clients AWS et d'Amazon pour mieux illustrer la situation globale. Nous avons également recherché des facteurs de compensation, autrement dit des sortes de « ceintures et bretelles » que les équipes implémentent pour empêcher la faiblesse d'une vérification de l'état d'engendrer un problème général.

Déploiements

L'un des problèmes relatifs à la vérification de l'état concerne les déploiements. Les systèmes de déploiement tels qu'AWS CodeDeploy poussent le nouveau code à un sous-ensemble de flotte à la fois et attendent qu'une vague de déploiement se termine avant de passer à la suivante. Ce processus repose sur le signalement renvoyé au système de déploiement par les serveurs une fois qu'ils fonctionnent avec le nouveau code. Si aucun signalement n'est renvoyé, le système de déploiement voit que quelque chose ne va pas avec le nouveau code et restaure le déploiement.

Le script de déploiement de démarrage de service le plus basique dupliquerait simplement le processus de serveur et répondrait immédiatement « déploiement effectué » au système de déploiement. Cependant, cette pratique est dangereuse parce qu'elle peut engendrer de nombreux problèmes avec le nouveau code. Par exemple, ce dernier pourrait planter tout de suite après le démarrage, être coincé et ne pas commencer à écouter sur un socket serveur, ne pas charger la configuration nécessaire pour traiter les requêtes avec succès ou rencontrer un bogue. Lorsqu'un système de déploiement n'est pas configuré pour tester une vérification de l'état de la dépendance, il ne se rend pas compte qu'il pousse un mauvais déploiement. Il met tous les serveurs hors service l'un après l'autre.

Heureusement, dans la pratique, les équipes Amazon implémentent plusieurs systèmes d'atténuation empêchant une telle situation de mettre hors service leurs flottes tout entières. Cette atténuation consiste à configurer des alarmes qui se déclenchent lorsque la taille globale d'une flotte est trop petite ou qu'elle est surchargée, ou en cas de latence ou de taux d'erreur élevé. Si l'une de ces alarmes se déclenchent, le système de déploiement arrête et restaure le déploiement.

Un autre type d'atténuation consiste à utiliser des déploiements progressifs. Au lieu de déployer la flotte tout entière en un seul déploiement, le service peut être configuré pour déployer un sous-ensemble, par exemple une zone de disponibilité, avant de faire une pause et d'exécuter une série complète de tests d'intégration sur cette zone. Cet alignement déploiement/zone de disponibilité est pratique parce que les services sont déjà désignés comme pouvant continuer de fonctionner en cas de problèmes avec une zone de disponibilité donnée.

En outre, avant le déploiement en production, les équipes Amazon poussent bien entendu ces modifications dans les environnements de test et exécutent automatiquement des tests d'intégration qui détecteraient ce type de défaillance. Cependant, des différences subtiles entre les environnements de production et de test sont inévitables, c'est pourquoi il est important de combiner de nombreuses couches de sécurité de déploiement pour détecter tous les types de problèmes avant qu'ils ne se répercutent sur la production. Bien que les vérifications de l'état soient indispensables pour prémunir les services contre les mauvais déploiements, nous ne nous arrêtons pas là. Nous réfléchissons aux approches de « ceintures et bretelles » qui servent de barrières pour protéger les flottes contre ces erreurs et bien d'autres.

Processeurs asynchrones

Un autre exemple de défaillance concerne le traitement de messages asynchrones, par exemple un service qui obtient son travail en interrogeant une file d'attente Amazon SQS ou un flux Amazon Kinesis. Contrairement aux systèmes qui récupèrent les requêtes auprès des équilibreurs de charge, ici, rien n'effectue de vérifications de l'état de manière automatique pour mettre les serveurs hors service.

Lorsque les vérifications de l'état des services ne sont pas assez approfondies, les serveurs individuels de travail de file d'attente peuvent présenter des défaillances comme des disques remplis ou un manque de descripteurs de fichiers. Ce problème n'empêche pas le serveur de retirer le travail de la file d'attente, mais il l'empêche de traiter les messages avec succès. Ce problème entraîne un retard de traitement des messages, avec des serveurs en mauvais état qui retirent le travail de la file d'attente rapidement et ne réussissent pas à le traiter.

Dans ce type de situation, il existe souvent des facteurs de compensation qui aident à limiter l'impact. Par exemple, si un serveur ne parvient pas à traiter le message qu'il retire de la file d'attente Amazon SQS, la file d'attente renvoie ce message à un autre serveur après un délai de visibilité de message configuré. La latence de bout en bout augmente, mais les messages ne sont pas supprimés. Un autre facteur de compensation est une alarme qui se déclenche lorsqu'il y a trop de messages de traitement d'erreurs et qui avertit un opérateur pour qu'il enquête.

Remplissage de disques

Un autre type de défaillance que nous notons est le remplissage de disques sur serveurs, qui entraîne l'échec du à la fois du traitement et de la connexion. Cette défaillance engendre un écart de visibilité de la surveillance, étant donné que le serveur ne peut pas forcément signaler ses défaillances au système de surveillance.

Encore une fois, plusieurs contrôles d'atténuation empêchent les services d'être « à l'aveugle » et limitent l'impact rapidement. Les systèmes gérés par un proxy tels qu'un Équilibreur de charge d'application ou une passerelle d'API ont des métriques de taux d'erreurs et de latence produits par ce proxy. Dans ce cas, les alarmes se déclenchent même si le serveur ne les signale pas. Pour les systèmes basés sur une file d'attente, les services comme Amazon Simple Queue Service (Amazon SQS) signalent les métriques indiquant que le processus est retardé pour certains messages.

Le point commun de ces solutions est qu'elles présentent plusieurs couches de surveillance. Le serveur lui-même signale les erreurs, mais un système externe le fait aussi. Ce principe est également important pour les vérifications de l'état. Un système externe peut tester l'état d'un système donné de façon plus précise qu'il ne peut le faire pour lui-même. Pour cette raison, avec AWS Auto Scaling, les équipes configurent un équilibreur de charge pour effectuer les vérifications de l'état de ping externes.

Les équipes écrivent également leur propre système de vérification de l'état personnalisé pour demander régulièrement à chaque serveur s'ils sont en bon état et signaler les serveurs en mauvais état à AWS Auto Scaling. L'implémentation commune de ce système implique l'exécution continue d'une fonction Lambda pour tester l'état de chaque serveur. Ces vérifications de l'état peuvent même enregistrer leur état entre chaque exécution dans des services tels que DynamoDB pour qu'ils ne marquent pas par erreur trop de services d'un coup comme étant en mauvais état.

Zombies

Les serveurs zombies sont un autre exemple de problème. En effet, les serveurs peuvent se déconnecter du réseau pour un certain temps, mais fonctionner quand même ou s'éteindre pour des périodes plus longues puis redémarrer.

Lorsqu'ils redémarrent, les serveurs zombies peuvent être considérablement désynchronisés par rapport au reste de la flotte, ce qui peut engendrer de graves problèmes. Par exemple, si un serveur zombie exécute une version de logiciel incompatible, car beaucoup plus ancienne, il peut entraîner des défaillances lorsqu'il tente d'interagir avec une base de données ayant un schéma différent ou encore utiliser la mauvaise configuration.

Pour gérer les serveurs zombies, les systèmes répondent souvent aux vérifications de l'état avec leur version de logiciel qu'ils utilisent actuellement. Un agent de surveillance central compare alors les réponses au sein de la flotte pour voir si des serveurs exécutent inopinément une version obsolète et empêche ces serveurs de revenir en service.

Conclusion

Les serveurs et les logiciels qu'ils exécutent peuvent tomber en panne pour une variété de raisons inattendues. Le matériel finit alors par se casser physiquement. En tant que développeurs logiciels, il nous arrive d'écrire des bogues comme celui décrit plus haut, qui mettent les logiciels en état de panne. Plusieurs couches de vérifications, des vérifications de la présence légères à la surveillance passive des métriques par serveur, sont nécessaires pour déceler tout type de mode de défaillance inattendue.

Lorsque ces défaillances se produisent, il est important de les détecter et de mettre rapidement les serveurs affectés hors service. Cependant, comme pour l'automatisation des flottes, nous ajoutons des limitations de vitesse, des seuils et des disjoncteurs qui arrêtent l'automatisation et impliquent des humains dans les cas d'incertitude ou extrêmes. Le comportement « échec ouvert » et la construction d'acteurs centralisés sont des stratégies permettant de tirer avantage de la vérification de l'état approfondie avec la sécurité d'une automatisation à vitesse limitée.

Atelier pratique

Testez certains des principes que vous avez appris ici avec un laboratoire pratique.


À propos de l'auteur

David Yanacek est ingénieur en chef senior et travaille sur AWS Lambda. David est développeur logiciel chez Amazon depuis 2006. Il a auparavant travaillé sur Amazon DynamoDB et AWS IoT, ainsi que sur les cadres de service Web internes et les systèmes d'automatisation d'opération des flottes. L'une des activités professionnelles préférées de David est d'effectuer des analyses de journaux et de parcourir des métriques opérationnelles dans le but de trouver des solutions pour que les systèmes s'exécutent de mieux en mieux au fil du temps.

Délais d'expiration, nouvelles tentatives et interruption avec instabilité