L'année dernière, l'équipe Uber Engineering a publié un article sur son nouveau mécanisme de délestage conçu pour son architecture de microservices.
Cet article est très intéressant à différents points de vue. J'ai donc pris quelques notes pendant que je le lisais pour capturer ma compréhension et écrire des choses que j'aimerais approfondir plus tard si je n'obtiens pas les réponses à la fin. J'ai découvert à plusieurs reprises que c'était la meilleure façon pour moi d'apprendre de nouvelles choses.
Ce qui m'a séduit dès le départ, c'est la référence aux principes centenaires utilisés pour construire cette solution. C'est quelque chose que j'aime : emprunter des concepts/idées à différents domaines et les adapter pour résoudre un problème dans un domaine différent.
Si la résilience et la stabilité du système vous intéressent, je vous recommande également de lire l'excellent livre « Release It ! » par Michael T. Nygard.
C'est un livre ancien mais bon : un livre qui explore les stratégies, les modèles et les conseils pratiques pour créer des systèmes logiciels résilients et stables, en mettant l'accent sur la manière de gérer efficacement les pannes.
Uber a mis en œuvre une nouvelle solution de délestage appelée Cinnamon qui tire parti d'un contrôleur PID (le mécanisme vieux de plusieurs siècles) pour décider quelles demandes doivent être traitées ou rejetées par un service en fonction de la charge actuelle du service et de la priorité de la demande.
Elle n'implique aucun réglage au niveau du service (même si j'avais une question à ce sujet), est automatiquement adaptable et bien plus efficace que leur précédente solution QALM. Rappelons également que l'architecture de microservices d'Uber n'est pas pour les âmes sensibles…
Un contrôleur PID est un instrument utilisé dans les applications de contrôle industriel pour réguler la température, le débit, la pression, la vitesse et d'autres variables de processus. Les contrôleurs PID (proportionnel intégral dérivé) utilisent un mécanisme de retour de boucle de contrôle pour contrôler les variables du processus et sont les contrôleurs les plus précis et les plus stables.
Si vous souhaitez plus d’informations sur ce concept vieux de plusieurs siècles, rendez-vous sur Wikipédia.
Maintenant, revenons à l'article. PID signifie Proportionnel, Intégral et Dérivé. Dans leur cas, ils utilisent un composant appelé contrôleur PID pour surveiller la santé d'un service (demandes d'entrée) en fonction de trois composants (ou mesures).
Le terme « proportionnel » indique que l'action entreprise est proportionnelle à l'erreur actuelle. En termes simples, cela signifie que la correction appliquée est directement proportionnelle à la différence entre l’état souhaité et l’état réel. Si l’erreur est importante, l’action corrective est proportionnellement importante.
Lorsqu'un point de terminaison est surchargé, la goroutine d'arrière-plan commence à surveiller les entrées et sorties de requêtes dans la file d'attente prioritaire.
Ainsi, le composant proportionnel (P) du délesteur de charge ajuste le taux de délestage en fonction de la distance entre la taille actuelle de la file d'attente et la taille de file d'attente cible ou souhaitée. Si la file d'attente est plus grande que souhaité, une perte plus importante se produit ; si elle est plus petite, la perte est réduite.
C'est ce que je comprends.
Le travail du contrôleur PID est de minimiser le nombre de requêtes en file d'attente, tandis que le travail de l'auto-tuner est de maximiser le débit du service, sans sacrifier (trop) les latences de réponse.
Bien que le texte ne mentionne pas explicitement « Intégrale (I) » dans le contexte de la taille de la file d'attente, il indique que le rôle du contrôleur PID est de minimiser le nombre de requêtes en file d'attente. La minimisation des requêtes en file d'attente s'aligne sur l'objectif du composant Integral de traiter les erreurs accumulées au fil du temps.
Pour déterminer si un point de terminaison est surchargé, nous gardons une trace de la dernière fois que la file d'attente des requêtes était vide, et si elle n'a pas été vidée au cours des 10 dernières secondes, nous considérons que le point de terminaison est surchargé (inspiré de Facebook).
Dans le délesteur de charge, il peut être associé à des décisions liées au comportement historique de la file d'attente de requêtes, comme le temps écoulé depuis sa dernière vide.
Honnêtement, ce n'est pas tout à fait clair pour moi. C'est un peu frustrant, je dois dire. Bien qu’ils mentionnent l’exploitation d’un mécanisme vieux de plusieurs siècles, il aurait été utile qu’ils indiquent explicitement quelle partie correspond à quoi et comment il fonctionne. Je ne veux pas diminuer la valeur de leur incroyable article. C'est juste mon coup de gueule ici… Après tout, je suis français… ;)
Je pense que celui-ci est plus facile à identifier.
Dans un contrôleur PID (Proportionnel-Intégral-Dérivé) classique, l'action « Dérivée (D) » est particulièrement utile lorsque vous souhaitez que le contrôleur anticipe le comportement futur du système en fonction du taux de variation actuel de l'erreur. Il permet d'amortir les oscillations et d'améliorer la stabilité du système.
Dans le contexte du délesteur de charge et du contrôleur PID mentionnés dans l'article, le composant Derivative est probablement utilisé pour évaluer la vitesse à laquelle la file d'attente des requêtes se remplit. Ce faisant, il aide à prendre des décisions visant à maintenir un système stable et à prévenir des changements soudains ou imprévisibles.
Le composant de rejet a deux responsabilités : a) déterminer si un point de terminaison est surchargé et b), si un point de terminaison est surchargé, éliminer un pourcentage des requêtes pour s'assurer que la file d'attente des requêtes est aussi petite que possible. Lorsqu'un point de terminaison est surchargé, la goroutine d'arrière-plan commence à surveiller les entrées et sorties de requêtes dans la file d'attente prioritaire. Sur la base de ces chiffres, il utilise un contrôleur PID pour déterminer un ratio de demandes à éliminer. Le contrôleur PID est très rapide (car très peu d'itérations sont nécessaires) pour trouver le niveau correct et une fois la file d'attente des requêtes vidée, le PID garantit que nous ne réduisons que lentement le rapport.
Dans le contexte mentionné, le contrôleur PID est utilisé pour déterminer le ratio de requêtes à éliminer lorsqu'un point de terminaison est surchargé, et il surveille l'entrée et la sortie des requêtes. Le composant dérivé du contrôleur PID, qui répond au taux de changement, est implicitement impliqué dans l'évaluation de la vitesse à laquelle la file d'attente des requêtes est remplie ou vidée. Cela aide à prendre des décisions dynamiques pour maintenir la stabilité du système.
Dans le contexte de la détermination de la surcharge, le composant intégral peut être associé au suivi de la durée pendant laquelle la file d'attente de requêtes est restée dans un état non vide. Cela correspond à l’idée d’accumuler l’intégrale du signal d’erreur au fil du temps.
« Intégral — en fonction de la durée pendant laquelle la demande est restée dans la file d'attente… »
La composante dérivée, quant à elle, est liée au taux de variation. Il répond à la rapidité avec laquelle l'état de la file d'attente des requêtes change.
"Dérivé - rejet basé sur la vitesse à laquelle la file d'attente se remplit..."
Le composant Integral met l'accent sur la durée de l'état non vide, tandis que le composant Derivative prend en compte la vitesse à laquelle la file d'attente change.
À la fin du jeu, ils utilisent ces trois mesures pour déterminer la marche à suivre pour une demande.
La question que je me pose est de savoir comment ils combinent ces trois éléments, voire pas du tout. Je suis également curieux de comprendre comment ils les surveillent.
Néanmoins, je pense avoir compris l'idée…
Le point de terminaison dans le Edge est annoté avec la priorité de la requête et celle-ci est propagée du Edge à toutes les dépendances en aval via Jaeger . En propageant ces informations, tous les services de la chaîne de requête connaîtront l'importance de la requête et à quel point elle est critique pour nos utilisateurs.
La première pensée qui me vient à l’esprit est qu’il s’intégrerait de manière transparente dans une architecture de services maillé.
J'apprécie le concept d'utilisation du traçage de services distribués et des en-têtes pour propager la priorité des demandes. Dans ce sens, pourquoi opter pour une bibliothèque partagée avec cette dépendance ajoutée à chaque microservice, au lieu de la placer en dehors du service, peut-être sous forme de plugin Istio ? Compte tenu des avantages qu’il offre : cycles de publication/déploiement indépendants, support polyglotte, etc.
Voici quelques réflexions supplémentaires :
Eh bien, je suis partial, car je ne suis pas un grand fan des bibliothèques partagées, ne serait-ce que parce que je pense qu'elles compliquent le processus de publication/déploiement. Cependant, je ne sais pas s'il existe un aspect de configuration spécifique au service à prendre en compte. Peut-être configurent-ils combien de temps le service doit attendre pour commencer à traiter une requête et la terminer ?
Un aspect qui mérite d’être testé est peut-être le processus de prise de décision de l’éjecteur.
D'après ce que j'ai compris, il détermine s'il faut rejeter une demande en fonction du contrôleur PID, qui est localisé dans le service. Existe-t-il une option pour une approche plus globale ? Par exemple, si l'on sait que l'un des services en aval du pipeline est surchargé (en raison de son propre contrôleur PID), un service en amont pourrait-il décider de rejeter la demande avant d'atteindre ce service surchargé (ce qui pourrait être n étapes plus loin dans le pipeline) ? chemin)?
Cette décision pourrait être basée sur la valeur renvoyée par le contrôleur PID ou le réglage automatique du service en aval.
Maintenant, je réfléchis à divers aspects mentionnés alors qu'ils terminent l'article et fournissent quelques chiffres pour montrer l'efficacité de leur système, ce qui est assez impressionnant.
Ils mentionnent à un moment donné que « Chaque demande a un délai d'attente de 1 seconde ».
Nous effectuons des tests de 5 minutes, au cours desquels nous envoyons un montant fixe de RPS (par exemple, 1 000), où 50 % du trafic est de niveau 1 et 50 % est de niveau 5. Chaque requête a un délai d'attente de 1 seconde.
Il est courant dans les systèmes distribués d'associer une demande à un délai d'expiration ou une date limite spécifique, chaque service le long du chemin de traitement étant responsable du respect de ce délai. Si le délai d'expiration est atteint avant que la demande ne soit terminée, n'importe quel service de la chaîne a la possibilité d'abandonner ou de rejeter la demande.
Je suppose que ce délai d'attente d'une seconde est attaché à la demande, et chaque service, selon où nous en sommes dans ce délai, peut décider d'abandonner la demande. Il s’agit d’une mesure globale car agrégée au travers des services. Je pense que cela rejoint le point que je faisais plus tôt sur la nécessité d'avoir une vue globale de l'état complet du système ou de ses dépendances pour décider d'abandonner la demande dès que possible si elle n'a pas la possibilité de se terminer en raison de l'un des services en aval. chemin.
La « santé » des services en aval (comprenant les données de leurs contrôleurs PID locaux) pourrait-elle être renvoyée sous forme d'en-têtes attachés aux réponses et utilisée pour créer un mécanisme de disjoncteur/délestage préemptif plus évolué ?
Enfin, je suis curieux d'en savoir plus sur l'approche précédente car, sur la base de la description donnée dans cet article, elle semble solide.
Lorsque vous examinez les mesures du goodput et des latences, il n’y a aucun doute quant à savoir lequel, QALM ou Cinnamon, est le plus performant. Notez qu'ils mentionnent un lien vers l'approche QALM dans l'article. Il faudrait probablement commencer par là ;)
Comme toujours, ces approches ne conviennent pas à tout le monde. L'architecture et la charge d'Uber sont en quelque sorte uniques. J'ai en fait hâte de lire les prochains articles de cette série, notamment pour en savoir plus sur le contrôleur PID et l'auto-tuner.
Avec Cinnamon, nous avons construit un délesteur de charge efficace qui utilise des techniques centenaires pour définir dynamiquement des seuils de rejet et d'estimation de la capacité des services. Cela résout les problèmes que nous avons remarqués avec QALM (et donc tout délesteur de charge basé sur CoDel), à savoir que Cinnamon est capable de :
- Trouver rapidement un taux de rejet stable
- Ajuster automatiquement la capacité du service
- Être utilisé sans définir de paramètres de configuration
- Encourir des frais généraux très faibles
Ce qui est intéressant dans cette approche, c'est qu'ils considèrent toutes les requêtes à traiter pour décider quoi faire pour chaque nouvelle requête d'entrée, car ils utilisent une file d'attente (prioritaire). Comme mentionné, je suis curieux de savoir si le mécanisme pourrait également prendre en compte la santé de tous les services dépendants sur la base des mêmes mesures PID…
Il y a d’autres aspects intéressants dans cet article, comme la façon dont ils mesurent l’effet de leurs stratégies et la comparaison avec l’approche précédente. Cependant, cela ne nécessite pas de ma part des notes plus détaillées que celles déjà présentées. Je vous encourage donc fortement à lire l’article original .
Vous avez trouvé cet article utile ? Suivez-moi sur Linkedin , Hackernoon et Medium ! S'il vous plaît 👏 cet article pour le partager !
Également publié ici.