Pour faire face aux défaillances dans un système de microservices, des mécanismes d'atténuation localisés tels que le délestage et les disjoncteurs ont toujours été utilisés, mais ils peuvent ne pas être aussi efficaces qu'une approche plus globale. Ces mécanismes localisés(comme le démontre une étude systématique sur le sujet publiée à SoCC 2022) sont utiles pour empêcher que des services individuels ne soient surchargés, mais ils ne sont pas très efficaces pour traiter les défaillances complexes qui impliquent des interactions entre les services, ce qui est caractéristique des défaillances des microservices.
Une nouvelle façon de gérer ces défaillances complexes consiste à adopter une vision globale du système : lorsqu'un problème survient, un plan d'atténuation global est automatiquement activé et coordonne les actions d'atténuation entre les services. Dans ce billet, nous évaluons le projet open-source Aperture et la manière dont il permet de mettre en place un plan global d'atténuation des défaillances pour nos services. Nous décrivons tout d'abord les types de pannes les plus courants que nous avons rencontrés chez DoorDash. Ensuite, nous nous penchons sur les mécanismes existants qui nous ont aidés à pallier les défaillances. Nous expliquerons pourquoi les mécanismes localisés ne sont peut-être pas la solution la plus efficace et nous plaiderons en faveur d'une approche globale d'atténuation des défaillances. En outre, nous partagerons nos premières expériences avec Aperture, qui offre une approche globale pour relever ces défis.
Catégories de défaillances de l'architecture des microservices
Before we explain what we have done to deal with failures, let's explore the types of microservice failures that organizations experience. We will discuss four types of failures that DoorDash and other enterprises have encountered.
Chez DoorDash, nous considérons chaque échec comme une opportunité d'apprentissage et nous partageons parfois nos idées et les leçons apprises dans des articles de blog publics pour montrer notre engagement à la fiabilité et au partage des connaissances. Dans cette section, nous aborderons quelques modèles de défaillance courants que nous avons connus. Chaque section est accompagnée de pannes réelles tirées de nos anciens articles de blog qui peuvent être explorées plus en détail.
Voici les défaillances que nous allons détailler :
- Défaillance en cascade: réaction en chaîne de différents services interconnectés qui tombent en panne.
- Tempête de tentatives : lorsque les tentatives exercent une pression supplémentaire sur un service dégradé.
- Spirale de la mort : certains nœuds tombent en panne, ce qui entraîne l'acheminement d'une plus grande quantité de trafic vers les nœuds sains, qui tombent à leur tour en panne.
- Metastable failure: an overarching term that describes failures that can't self-recover because of the existence of a positive feedback loop
Défaillance en cascade
La défaillance en cascade désigne le phénomène selon lequel la défaillance d'un seul service entraîne une réaction en chaîne de défaillances dans d'autres services. Nous avons documenté une panne grave de ce type dans notre blog. Dans ce cas, la chaîne de défaillances est partie d'une maintenance apparemment anodine de la base de données, qui a augmenté la latence de la base de données. Cette latence s'est ensuite répercutée sur les services en amont, provoquant des erreurs dues à des dépassements de délai et à l'épuisement des ressources. L'augmentation des taux d'erreur a déclenché un disjoncteur mal configuré, qui a interrompu le trafic entre un grand nombre de services non liés, ce qui a entraîné une panne avec un large rayon d'action.
La défaillance en cascade décrit un phénomène général où la défaillance se propage à travers les services, et il y a un large éventail de façons dont une défaillance peut se transmettre à une autre. La tempête de tentatives est un mode de transmission courant parmi d'autres, que nous examinerons plus loin.
Réessayer la tempête
En raison de la nature peu fiable des appels de procédure à distance (RPC), les sites d'appel RPC sont souvent instrumentés avec des délais d'attente et des tentatives pour augmenter les chances de succès de chaque appel. Réessayer une requête est très efficace lorsque l'échec est transitoire. Cependant, ces tentatives aggravent le problème lorsque le service en aval est indisponible ou lent, car dans ce cas, la plupart des requêtes sont relancées plusieurs fois et finissent toujours par échouer. Ce scénario, dans lequel des tentatives excessives et inefficaces sont effectuées, s'appelle l'amplification de la charge de travail, et il entraîne une dégradation supplémentaire d'un service déjà dégradé. À titre d'exemple, ce type de panne s'est produit à un stade précoce de notre transition vers les microservices : une augmentation soudaine de la latence de notre service de paiement a entraîné un comportement de relance de l'application Dasher et de son système dorsal, ce qui a exacerbé la situation.
La spirale de la mort
Les défaillances peuvent fréquemment se propager verticalement dans un graphe d'appels RPC entre les services, mais elles peuvent également se propager horizontalement entre les nœuds qui appartiennent au même service. Une spirale de la mort est une panne qui commence par un modèle de trafic qui fait qu'un nœud tombe en panne ou devient très lent, de sorte que l'équilibreur de charge achemine les nouvelles demandes vers les nœuds sains restants, ce qui les rend plus susceptibles de tomber en panne ou d'être surchargés. Ce billet de blog décrit une panne qui a commencé par l'échec de certains pods à la sonde de préparation et qui a donc été retirée du cluster, et les nœuds restants sont tombés en panne parce qu'ils n'étaient pas en mesure de gérer seuls les charges massives.
Défaillances métastables
A recent paper proposes a new framework to study distributed system failures, which is called a "metastable failure." Many of the outages we experienced belong to this category. This type of failure is characterized by a positive feedback loop within the system that provides a sustaining high load because of work amplification, even after the initial trigger (e.g., bad deployment; a surge of users) is gone. Metastable failure is especially bad because it will not self-recover, and engineers need to step in to stop the positive feedback loop, which increases the time it takes to recover.
Contre-mesures locales
Toutes les défaillances documentées dans la section ci-dessus sont des types de contre-mesures qui tentent de limiter l'impact de la défaillance localement au sein d'une instance d'un service, mais aucune de ces solutions ne permet une atténuation coordonnée entre les services pour assurer le rétablissement global du système. Pour le démontrer, nous allons nous pencher sur chaque mécanisme d'atténuation existant que nous avons déployé, puis nous discuterons de leurs limites.
Les contre-mesures dont nous parlerons sont les suivantes :
- Le délestage : qui empêche les services dégradés d'accepter davantage de demandes.
- Disjoncteur : qui arrête les demandes sortantes en cas de dégradation
- Auto scaling: that can help with handling high load at peak traffic, but it's only useful if it's configured to be predictive rather than reactive
Nous expliquerons ensuite le fonctionnement de toutes ces stratégies de tolérance aux pannes, puis nous discuterons de leurs inconvénients et de leurs compromis.
Délestage de charge
Load shedding is a reliability mechanism that rejects incoming requests at the service entrance when the number of in-flight or concurrent requests exceeds a limit. By rejecting only some traffic, we maximize the goodput of the service, instead of allowing the service to be completely overloaded where it would no longer be able to do any useful work. At DoorDash, we instrumented each server with an "adaptive concurrency limit" from the Netflix library concurrency-limit. It works as a gRPC interceptor and automatically adjusts the maximum number of concurrent requests according to the change in the latency it observes: when the latency rises, the library reduces the concurrency limit to give each request more compute resources. Additionally, the load shedder can be configured to recognize priorities of requests from their header and only accept high priority ones during a period of overload.
Le délestage peut être efficace pour éviter qu'un service ne soit surchargé. Cependant, comme le délesteur est installé au niveau local, il ne peut gérer que les pannes de service locales. Comme nous l'avons vu dans la section précédente, les défaillances dans un système de microservices résultent souvent d'une interaction entre les services. Par conséquent, il serait avantageux de disposer d'une solution coordonnée en cas de panne. Par exemple, lorsqu'un service aval important (A) devient lent, un service amont (B) devrait commencer à bloquer les demandes avant qu'elles n'atteignent A. Cela empêche la latence accrue de A de se propager à l'intérieur du sous-graphe, ce qui pourrait provoquer une défaillance en cascade.
Besides the limitation of the lack of coordination, load shedding is also hard to configure and test. Properly configuring a load shedder requires carefully orchestrated load testing to understand a service's optimal concurrency limit, which is not an easy task because in the production environment, some requests are more expensive than others, and some requests are more important to the system than others. As an example of a misconfigured load shedder, we once had a service whose initial concurrency limit was set too high, which resulted in a temporary overload during the service's startup time. Although the load shedder was able to tune down the limit eventually, the initial instability was bad and showed how important it is to correctly configure the load shedder. Nevertheless, engineers often leave these parameters to their default values, which is often not optimal for individual services' characteristics.
Disjoncteur
Alors que le délestage est un mécanisme qui permet de rejeter le trafic entrant, un disjoncteur rejette le trafic sortant, mais, comme le délestage, il n'a qu'une vue localisée. Les coupe-circuits sont généralement mis en œuvre sous la forme d'un proxy interne qui gère les demandes sortantes vers les services en aval. Lorsque le taux d'erreur du service en aval dépasse un certain seuil, le coupe-circuit s'ouvre et rejette rapidement toutes les demandes vers le service en difficulté sans amplifier le travail. Au bout d'un certain temps, le disjoncteur laisse progressivement passer plus de trafic, pour finalement revenir à un fonctionnement normal. Chez DoorDash, nous avons intégré un coupe-circuit dans notre client interne gRPC.
Dans les situations où le service en aval subit une défaillance mais a la capacité de se rétablir si le trafic est réduit, un coupe-circuit peut être utile. Par exemple, lors d'une spirale de la mort dans la formation, les nœuds malsains sont remplacés par des nœuds nouvellement démarrés qui ne sont pas prêts à prendre le trafic, de sorte que le trafic est acheminé vers les nœuds sains restants, ce qui les rend plus susceptibles d'être surchargés. Dans ce cas, un disjoncteur ouvert donne du temps et des ressources supplémentaires à tous les nœuds pour qu'ils redeviennent sains.
Circuit breakers have the same tuning problem as load shedding: there is no good way for service authors to determine the tripping threshold. Many online sources on this subject use a "50% error rate" as a rule of thumb. However, for some services 50% error rate may be tolerable. When a called service returns an error, it might be because the service itself is unhealthy, or it might be because a service further downstream is having problems. When a circuit breaker opens, the service behind it will become effectively unreachable for a period of time, which may be deemed even less desirable. The tripping threshold depends on the SLA of the service and the downstream implications of the requests, which must all be considered carefully.
Restez informé grâce aux mises à jour hebdomadaires
Abonnez-vous à notre blog d'ingénierie pour recevoir régulièrement des informations sur les projets les plus intéressants sur lesquels notre équipe travaille.
Please enter a valid email address.
Merci de vous être abonné !
Mise à l'échelle automatique
All cluster orchestrators can be configured with autoscaling to handle increases in load. When it's turned on, a controller periodically checks each node's resource consumption (e.g. CPU or memory), and when it detects high usage, it launches new nodes to distribute the workload. While this feature may seem appealing, at DoorDash we recommend that teams do not use reactive auto-scaling (which scales up the cluster in real time during a load peak). Since this is counterintuitive, we list the drawback of reactive auto-scaling below.
- Les nœuds nouvellement lancés ont besoin de temps pour s'échauffer (remplir les caches, compiler le code, etc.) et présenteront une latence plus élevée, ce qui réduit temporairement la capacité de la grappe. En outre, les nouveaux nœuds exécutent des tâches de démarrage coûteuses, telles que l'ouverture de connexions à des bases de données et le déclenchement de protocoles d'adhésion. Ces comportements sont peu fréquents, de sorte qu'une augmentation soudaine peut entraîner des résultats inattendus.
- Lors d'une panne impliquant une charge élevée, l'augmentation de la capacité d'un service ne fera souvent que déplacer le goulot d'étranglement vers un autre endroit. Cela ne résout généralement pas le problème.
- L'auto-scaling réactif rend plus difficile l'analyse post-mortem, car la chronologie des mesures s'ajuste de diverses manières à l'incident, aux mesures prises par les humains pour l'atténuer et à l' auto-scaler.
Therefore, we advise teams to avoid using reactive auto-scaling, preferring instead to use predictive auto-scaling such as KEDA's cron that adjusts a cluster's size based on expected traffic levels throughout the day.
Tous ces mécanismes localisés sont efficaces pour traiter les différents types de défaillance. Cependant, la localisation a ses propres inconvénients. Nous allons maintenant examiner les raisons pour lesquelles les solutions localisées n'ont qu'une portée limitée et pourquoi une observation et une intervention à l'échelle mondiale seraient préférables.
Lacunes des contre-mesures existantes
Toutes les techniques de fiabilité que nous employons ont une structure similaire composée de trois éléments : la mesure des conditions opérationnelles, l'identification des problèmes par le biais de règles et de paramètres, et les mesures à prendre lorsque des problèmes surviennent. Par exemple, dans le cas du délestage, les trois composantes sont les suivantes :
- Mesure : calcule l'historique récent de la latence du service ou des erreurs.
- Identifier : utilise des formules mathématiques et des paramètres prédéfinis pour déterminer si le service risque d'être surchargé.
- Action : refuse les demandes entrantes excessives
Pour les disjoncteurs, il s'agit de
- Measure: evaluates downstream service's error rate
- Identifier : vérifie s'il dépasse un seuil
- Action : arrête tout le trafic sortant vers ce service
Cependant, les mécanismes localisés existants souffrent de lacunes similaires :
- Ils utilisent les paramètres locaux du service pour mesurer les conditions d'exploitation ; cependant, de nombreuses catégories de pannes impliquent une interaction entre de nombreux composants, et il est nécessaire d'avoir une vue d'ensemble du système pour prendre de bonnes décisions sur la manière d'atténuer les effets d'une condition de surcharge.
- Ils utilisent des heuristiques très générales pour déterminer l'état du système, ce qui n'est souvent pas assez précis. Par exemple, la latence seule ne permet pas de savoir si un service est surchargé ; une latence élevée peut être due à un service lent en aval.
- Leurs actions de remédiation sont limitées. Comme les mécanismes sont instrumentés localement, ils ne peuvent prendre que des mesures locales. Les actions locales ne sont généralement pas optimales pour rétablir le système dans un état sain, car la véritable source du problème peut se trouver ailleurs.
Nous allons examiner comment surmonter ces lacunes et rendre l'atténuation plus efficace.
Utilisation de contrôles globalisés : Aperture pour la gestion de la fiabilité
Un projet qui va au-delà des contre-mesures locales pour mettre en œuvre un contrôle de charge globalisé est mis en œuvre par Aperture, un système de gestion de la fiabilité à code source ouvert. Il fournit une couche d'abstraction de fiabilité qui facilite la gestion de la fiabilité dans une architecture distribuée de microservices. Contrairement aux mécanismes de fiabilité existants qui ne peuvent réagir qu'à des anomalies locales, Aperture offre un système centralisé de gestion de la charge qui lui permet de coordonner de nombreux services en réponse à une panne en cours.
Aperture's design
Comme les contre-mesures existantes, Aperture surveille et contrôle la fiabilité du système à l'aide de trois éléments clés.
- Observer: Aperture recueille des mesures liées à la fiabilité de chaque nœud et les regroupe dans Prometheus.
- Analyser: Un contrôleur Aperture indépendant surveille en permanence les paramètres et suit les écarts par rapport au SLO.
- Actionner: En cas d'anomalie, le contrôleur Aperture activera les politiques correspondant au modèle observé et appliquera des actions à chaque nœud, comme le délestage ou la limitation du débit distribué.
Notre expérience de l'utilisation d'Aperture
Aperture est hautement configurable dans sa manière de détecter et d'agir face aux anomalies du système. Il prend en compte des politiques écrites dans des fichiers YAML qui guident ses actions pendant une panne. Par exemple, le code ci-dessous, extrait de la documentation d' Aperture et simplifié, calcule la moyenne mobile exponentielle (EMA) de la latence. Il utilise les mesures de latence de Prometheus et déclenche une alerte lorsque la valeur calculée est supérieure à un seuil.
circuit:
components:
- promql:
evaluation_interval: 1s
out_ports:
output:
signal_name: LATENCY
query_string:
# OMITTED
- ema:
ema_window: 1500s
in_ports:
input:
signal_name: LATENCY
out_ports:
output:
signal_name: LATENCY_EMA
warm_up_window: 10s
- decider:
in_ports:
lhs:
signal_name: LATENCY
rhs:
signal_name: LATENCY_SETPOINT
operator: gt
out_ports:
output:
signal_name: IS_OVERLOAD_SWITCH
- alerter:
alerter_config:
alert_name: overload
severity: crit
in_ports:
signal:
signal_name: IS_OVERLOAD_SWITCH
evaluation_interval: 0.5s
Lorsqu'une alerte est déclenchée, Aperture exécute automatiquement des actions en fonction des politiques configurées. Parmi les actions qu'il propose actuellement, on peut citer la limitation du débit distribué et la limitation de la concurrence (ou délestage). Le fait qu'Aperture dispose d'une vue et d'un contrôle centralisés de l'ensemble du système ouvre de nombreuses possibilités pour atténuer les pannes. Par exemple, il est possible de configurer une politique de délestage sur un service en amont lorsqu'un service en aval est surchargé, ce qui permet aux demandes excessives d'échouer avant d'atteindre le sous-graphe problématique, ce qui rend le système plus réactif et permet d'économiser des coûts.
To test out Aperture's capability, we ran a deployment of Aperture and integrated it into one of our primary services, all within a testing environment and found it to be an effective load shedder. As we increased the RPS of the artificial requests sent to the service, we observed that the error rate increased, but the goodput remained steady. On a second run, we reduced the compute capacity of the service, and this time we observed that the goodput reduced, but the latency only increased slightly. Behind the scenes of both runs, the Aperture controller noticed an increase in latency and decided to reduce the concurrency limit. Consequently, our API integration in our application code rejected some of the incoming requests, which is reflected by an increased error rate. The reduced concurrency limit ensures that each accepted request gets enough compute resources, so the latency is only slightly affected.
Avec cette configuration simple, Aperture agit essentiellement comme un délesteur, mais il est plus configurable et plus convivial que nos solutions existantes. Nous sommes en mesure de configurer Aperture avec un algorithme sophistiqué de limitation de la concurrence qui minimise l'impact d'une charge ou d'une latence inattendue. Aperture offre également un tableau de bord Grafana tout-en-un utilisant les métriques Prometheus, qui donne un aperçu rapide de la santé de nos services.
Nous n'avons pas encore essayé les fonctionnalités plus avancées d'Aperture, notamment la possibilité de coordonner les actions d'atténuation entre les services et la possibilité d'avoir des politiques d'escalade dans lesquelles l'autoscaling est déclenché après une charge soutenue. L'évaluation de ces fonctionnalités nécessite des configurations plus élaborées. Cela dit, il est préférable de tester une solution de fiabilité dans l'environnement de production, où se produisent de véritables pannes, toujours imprévisibles.
Détails de l'intégration d'Aperture
It's worth a deeper dive into how Aperture is integrated into an existing system. A deployment of Aperture consists of the following components:
- Contrôleur ApertureCe module est le cerveau du système Aperture. Il surveille en permanence les mesures de fiabilité et décide du moment où il convient d'exécuter un plan d'atténuation. Lorsqu'un plan est déclenché, il envoie les actions appropriées (par exemple, le délestage) à l'agent Aperture.
- Aperture agent : chaque cluster Kubernetes fait tourner une instance de l'agent Aperture, qui est chargé de suivre et d'assurer la santé des nœuds fonctionnant dans le même cluster. Lorsqu'une demande arrive dans un service, elle est interceptée par un point d'intégration qui transmet les métadonnées correspondantes à un agent Aperture. L'agent Aperture enregistre les métadonnées et répond en décidant d'accepter ou non la demande. Cette décision est basée sur les informations fournies par le contrôleur Aperture.
- Point d'intégration : les services qui souhaitent bénéficier d'une gestion centralisée de la fiabilité peuvent s'intégrer à Aperture de trois manières. Si les services sont construits sur un maillage de services (actuellement seulement Envoy), Aperture peut être déployé sur le maillage de services directement sans changer le code de l'application. Il existe également des SDK Aperture que l'on peut utiliser pour intégrer le code de l'application aux points d'extrémité Aperture. Pour les applications Java, il est également possible d'utiliser Java Agent pour injecter automatiquement l'intégration d'Aperture dans Netty. Pour illustrer le rôle de cette intégration, voici un extrait de code qui montre comment utiliser le SDK Aperture en Java.
- Prometheus & etcd : il s'agit de bases de données qui stockent les mesures de fiabilité et qui sont interrogées par le contrôleur Aperture pour obtenir une mesure de l'état de fonctionnement actuel.
private String handleSuperAPI(spark.Request req, spark.Response res) {
Flow flow = apertureSDK.startFlow(metadata);
if (flow.accepted()) {
res.status(202);
work(req, res);
flow.end(FlowStatus.OK);
} else {
res.status(403);
flow.end(FlowStatus.Error);
}
return "";
}
Conclusion
Existing reliability mechanisms are instrumented at the local level of individual services, and we have shown that globalized mechanisms work better at dealing with outages. In this blog, we showed why keeping a microservice system running reliably is a challenging problem. We also give an overview of our current countermeasures. These existing solutions effectively prevent many outages, but engineers often poorly understand their inner workings and don't configure them optimally. Additionally, they can only observe and act inside each service, which limits their effectiveness in mitigating outages in a distributed system.
Pour tester l'idée d'utiliser des mécanismes globalisés pour atténuer les pannes, nous avons étudié le projet de gestion de la fiabilité Aperture. Ce projet élève la gestion de la fiabilité au rang de composante principale du système en centralisant les responsabilités de surveillance et de contrôle plutôt que de les confier à des services individuels. Ce faisant, Aperture permet de mettre en œuvre des méthodes automatisées, efficaces et rentables pour remédier aux pannes. Nous avons eu une expérience positive lors de notre essai initial et nous sommes enthousiasmés par son potentiel.