[Français] Modèle de thread Envoy

Traduction de l'article: Modèle de thread Envoy - https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

J'ai trouvé cet article assez intéressant, et comme Envoy est le plus souvent utilisé dans le cadre de « istio » ou simplement comme « contrôleur d'entrée » de kubernetes, la plupart des gens n'ont pas la même interaction directe avec lui que, par exemple, avec un Installations Nginx ou Haproxy. Cependant, si quelque chose se brise, il serait bon de comprendre comment cela fonctionne de l’intérieur. J'ai essayé de traduire autant de texte que possible en russe, y compris des mots spéciaux ; pour ceux qui trouvent cela pénible de regarder cela, j'ai laissé les originaux entre parenthèses. Bienvenue au chat.

La documentation technique de bas niveau pour la base de code Envoy est actuellement assez rare. Pour remédier à cela, je prévois de faire une série d'articles de blog sur les différents sous-systèmes d'Envoy. Puisqu'il s'agit du premier article, n'hésitez pas à me faire savoir ce que vous en pensez et ce qui pourrait vous intéresser dans les prochains articles.

L'une des questions techniques les plus courantes que je reçois à propos d'Envoy consiste à demander une description de bas niveau du modèle de thread qu'il utilise. Dans cet article, je décrirai comment Envoy mappe les connexions aux threads, ainsi que le système de stockage local Thread qu'il utilise en interne pour rendre le code plus parallèle et plus performant.

Présentation du filetage

[Français] Modèle de thread Envoy

Envoy utilise trois types de flux différents :

  • Principal: Ce thread contrôle le démarrage et l'arrêt des processus, tous les traitements de l'API XDS (xDiscovery Service), y compris le DNS, la vérification de l'état, la gestion générale du cluster et du runtime, la réinitialisation des statistiques, l'administration et la gestion générale des processus - signaux Linux, redémarrage à chaud, etc. ce qui se passe dans ce fil est asynchrone et "non bloquant". En général, le thread principal coordonne tous les processus de fonctionnalités critiques qui ne nécessitent pas une grande quantité de CPU pour s'exécuter. Cela permet à la plupart du code de contrôle d'être écrit comme s'il s'agissait d'un seul thread.
  • Ouvrier: Par défaut, Envoy crée un thread de travail pour chaque thread matériel du système, cela peut être contrôlé à l'aide de l'option --concurrency. Chaque thread de travail exécute une boucle d'événements « non bloquante », qui est responsable de l'écoute de chaque écouteur ; au moment de la rédaction (29 juillet 2017), il n'y a pas de partitionnement de l'écouteur, d'acceptation de nouvelles connexions, d'instanciation d'une pile de filtres pour la connexion et traiter toutes les opérations d'entrée/sortie (IO) pendant la durée de vie de la connexion. Encore une fois, cela permet d'écrire la plupart du code de gestion des connexions comme s'il s'agissait d'un seul thread.
  • Videur de fichiers : Chaque fichier écrit par Envoy, principalement les journaux d'accès, possède actuellement un thread de blocage indépendant. Cela est dû au fait que l'écriture dans les fichiers mis en cache par le système de fichiers même lors de l'utilisation de O_NONBLOCK peut parfois se bloquer (soupir). Lorsque les threads de travail doivent écrire dans un fichier, les données sont en fait déplacées vers un tampon en mémoire où elles sont finalement vidées via le thread. fichier vide. Il s'agit d'une zone de code dans laquelle techniquement tous les threads de travail peuvent bloquer le même verrou tout en essayant de remplir une mémoire tampon.

Gestion des connexions

Comme indiqué brièvement ci-dessus, tous les threads de travail écoutent tous les écouteurs sans aucun partitionnement. Ainsi, le noyau est utilisé pour envoyer gracieusement les sockets acceptées aux threads de travail. Les noyaux modernes sont généralement très bons dans ce domaine, ils utilisent des fonctionnalités telles que l'augmentation de la priorité d'entrée/sortie (IO) pour essayer de remplir un thread de travail avant de commencer à utiliser d'autres threads qui écoutent également sur le même socket, et qui n'utilisent pas non plus le round robin. verrouillage (Spinlock) pour traiter chaque demande.
Une fois qu'une connexion est acceptée sur un thread de travail, elle ne quitte jamais ce thread. Tout traitement ultérieur de la connexion est entièrement géré dans le thread de travail, y compris tout comportement de transfert.

Cela a plusieurs conséquences importantes :

  • Tous les pools de connexions dans Envoy sont attribués à un thread de travail. Ainsi, bien que les pools de connexions HTTP/2 n'établissent qu'une seule connexion à chaque hôte en amont à la fois, s'il y a quatre threads de travail, il y aura quatre connexions HTTP/2 par hôte en amont dans un état stable.
  • La raison pour laquelle Envoy fonctionne de cette façon est qu'en gardant tout sur un seul thread de travail, presque tout le code peut être écrit sans blocage et comme s'il s'agissait d'un seul thread. Cette conception facilite l’écriture de beaucoup de code et s’adapte incroyablement bien à un nombre presque illimité de threads de travail.
  • Cependant, l'un des principaux points à retenir est que, du point de vue du pool de mémoire et de l'efficacité de la connexion, il est en réalité très important de configurer le --concurrency. Avoir plus de threads de travail que nécessaire gaspillera de la mémoire, créera davantage de connexions inactives et réduira le taux de regroupement de connexions. Chez Lyft, nos conteneurs side-car envoyés fonctionnent avec une très faible concurrence, de sorte que les performances correspondent à peu près aux services à côté desquels ils se trouvent. Nous exécutons Envoy en tant que proxy Edge uniquement avec une concurrence maximale.

Que signifie non-blocage ?

Le terme « non bloquant » a été utilisé à plusieurs reprises jusqu'à présent pour décrire le fonctionnement des threads principal et de travail. Tout le code est écrit en supposant que rien n'est jamais bloqué. Cependant, ce n’est pas tout à fait vrai (qu’est-ce qui n’est pas tout à fait vrai ?).

Envoy utilise plusieurs longs verrous de processus :

  • Comme indiqué, lors de l'écriture des journaux d'accès, tous les threads de travail acquièrent le même verrou avant que le tampon du journal en mémoire ne soit rempli. Le temps de maintien du verrou doit être très faible, mais il est possible que le verrou soit contesté avec une concurrence élevée et un débit élevé.
  • Envoy utilise un système très complexe pour gérer les statistiques locales au thread. Ce sera le sujet d’un article séparé. Cependant, je mentionnerai brièvement que dans le cadre du traitement local des statistiques des threads, il est parfois nécessaire d'acquérir un verrou sur un "magasin de statistiques" central. Ce verrouillage ne devrait jamais être requis.
  • Le thread principal doit périodiquement se coordonner avec tous les threads de travail. Cela se fait en « publiant » du thread principal vers les threads de travail, et parfois des threads de travail vers le thread principal. L'envoi nécessite un verrou afin que le message publié puisse être mis en file d'attente pour une livraison ultérieure. Ces verrous ne devraient jamais être sérieusement contestés, mais ils peuvent toujours techniquement être bloqués.
  • Lorsque Envoy écrit un journal dans le flux d'erreurs système (erreur standard), il acquiert un verrou sur l'ensemble du processus. En général, la journalisation locale d'Envoy est considérée comme terrible du point de vue des performances, donc peu d'attention a été accordée à son amélioration.
  • Il existe quelques autres verrous aléatoires, mais aucun d'entre eux n'est critique en termes de performances et ne doit jamais être contesté.

Stockage local des threads

En raison de la manière dont Envoy sépare les responsabilités du thread principal de celles du thread de travail, il est nécessaire qu'un traitement complexe puisse être effectué sur le thread principal, puis fourni à chaque thread de travail de manière hautement concurrente. Cette section décrit le stockage local Envoy Thread (TLS) à un niveau élevé. Dans la section suivante, je décrirai comment il est utilisé pour gérer un cluster.
[Français] Modèle de thread Envoy

Comme déjà décrit, le thread principal gère pratiquement toutes les fonctionnalités du plan de gestion et de contrôle dans le processus Envoy. Le plan de contrôle est un peu surchargé ici, mais lorsque vous l'examinez dans le processus Envoy lui-même et que vous le comparez au transfert effectué par les threads de travail, cela a du sens. La règle générale est que le processus du thread principal effectue un certain travail, puis il doit mettre à jour chaque thread de travail en fonction du résultat de ce travail. dans ce cas, le thread de travail n'a pas besoin d'acquérir un verrou à chaque accès.

Le système TLS (Thread local storage) d'Envoy fonctionne comme suit :

  • Le code exécuté sur le thread principal peut allouer un emplacement TLS pour l'ensemble du processus. Bien que cela soit abstrait, il s’agit en pratique d’un index dans un vecteur, fournissant un accès O(1).
  • Le thread principal peut installer des données arbitraires dans son emplacement. Lorsque cela est fait, les données sont publiées sur chaque thread de travail en tant qu'événement de boucle d'événements normal.
  • Les threads de travail peuvent lire à partir de leur emplacement TLS et récupérer toutes les données locales du thread qui y sont disponibles.

Bien qu'il s'agisse d'un paradigme très simple et incroyablement puissant, il est très similaire au concept de blocage RCU (Read-Copy-Update). Essentiellement, les threads de travail ne voient jamais de modifications de données dans les emplacements TLS pendant l'exécution du travail. Le changement ne se produit que pendant la période de repos entre les événements professionnels.

Envoy l'utilise de deux manières différentes :

  • En stockant différentes données sur chaque thread de travail, les données sont accessibles sans aucun blocage.
  • En conservant un pointeur partagé vers les données globales en mode lecture seule sur chaque thread de travail. Ainsi, chaque thread de travail possède un nombre de références de données qui ne peut pas être décrémenté pendant l'exécution du travail. Ce n’est que lorsque tous les travailleurs se seront calmés et auront téléchargé de nouvelles données partagées que les anciennes données seront détruites. Ceci est identique à RCU.

Threading de mise à jour du cluster

Dans cette section, je vais décrire comment TLS (Thread local storage) est utilisé pour gérer un cluster. La gestion du cluster inclut le traitement de l'API xDS et/ou du DNS, ainsi que la vérification de l'état.
[Français] Modèle de thread Envoy

La gestion des flux de cluster comprend les composants et étapes suivants :

  1. Le Cluster Manager est un composant d'Envoy qui gère tous les clusters connus en amont, l'API Cluster Discovery Service (CDS), les API Secret Discovery Service (SDS) et Endpoint Discovery Service (EDS), le DNS et les contrôles externes actifs. Il est chargé de créer une vue « finalement cohérente » de chaque cluster en amont, qui inclut les hôtes découverts ainsi que l'état de santé.
  2. Le vérificateur de santé effectue une vérification de l'état active et signale les modifications de l'état de santé au gestionnaire de cluster.
  3. CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS sont effectués pour déterminer l'appartenance au cluster. Le changement d'état est renvoyé au gestionnaire de cluster.
  4. Chaque thread de travail exécute en permanence une boucle d'événements.
  5. Lorsque le gestionnaire de cluster détermine que l'état d'un cluster a changé, il crée un nouvel instantané en lecture seule de l'état du cluster et l'envoie à chaque thread de travail.
  6. Au cours de la prochaine période de repos, le thread de travail mettra à jour l'instantané dans l'emplacement TLS alloué.
  7. Lors d'un événement d'E/S censé déterminer l'hôte à équilibrer de charge, l'équilibreur de charge demandera un emplacement TLS (Thread local storage) pour obtenir des informations sur l'hôte. Cela ne nécessite pas de verrous. Notez également que TLS peut également déclencher des événements de mise à jour afin que les équilibreurs de charge et autres composants puissent recalculer les caches, les structures de données, etc. Cela dépasse le cadre de cet article, mais est utilisé à divers endroits dans le code.

En utilisant la procédure ci-dessus, Envoy peut traiter chaque demande sans aucun blocage (sauf comme décrit précédemment). Outre la complexité du code TLS lui-même, la plupart du code n'a pas besoin de comprendre le fonctionnement du multithreading et peut être écrit en monothread. Cela rend la plupart du code plus facile à écrire en plus de performances supérieures.

Autres sous-systèmes utilisant TLS

TLS (Thread local storage) et RCU (Read Copy Update) sont largement utilisés dans Envoy.

Exemples d'utilisation:

  • Mécanisme de modification des fonctionnalités pendant l'exécution : La liste actuelle des fonctionnalités activées est calculée dans le thread principal. Chaque thread de travail reçoit ensuite un instantané en lecture seule utilisant la sémantique RCU.
  • Remplacement des tables de routage: Pour les tables de routage fournies par RDS (Route Discovery Service), les tables de routage sont créées sur le thread principal. L'instantané en lecture seule sera ensuite fourni à chaque thread de travail à l'aide de la sémantique RCU (Read Copy Update). Cela rend la modification des tables de routage atomiquement efficace.
  • Mise en cache des en-têtes HTTP : Il s'avère que calculer l'en-tête HTTP pour chaque requête (tout en exécutant environ 25 XNUMX RPS par cœur) est assez coûteux. Envoy calcule l'en-tête de manière centralisée environ toutes les demi-secondes et le fournit à chaque travailleur via TLS et RCU.

Il existe d'autres cas, mais les exemples précédents devraient permettre de bien comprendre à quoi sert TLS.

Pièges connus en matière de performances

Bien qu'Envoy fonctionne globalement assez bien, quelques domaines notables nécessitent une attention particulière lorsqu'il est utilisé avec une simultanéité et un débit très élevés :

  • Comme décrit dans cet article, actuellement tous les threads de travail acquièrent un verrou lors de l'écriture dans la mémoire tampon du journal d'accès. En cas de concurrence élevée et de débit élevé, vous devrez regrouper par lots les journaux d'accès pour chaque thread de travail au détriment d'une livraison dans le désordre lors de l'écriture dans le fichier final. Vous pouvez également créer un journal d'accès distinct pour chaque thread de travail.
  • Bien que les statistiques soient hautement optimisées, avec une concurrence et un débit très élevés, il y aura probablement des conflits atomiques sur les statistiques individuelles. La solution à ce problème réside dans les compteurs par thread de travail avec réinitialisation périodique des compteurs centraux. Cela sera discuté dans un prochain article.
  • L'architecture actuelle ne fonctionnera pas bien si Envoy est déployé dans un scénario où il y a très peu de connexions nécessitant des ressources de traitement importantes. Il n'y a aucune garantie que les connexions seront réparties uniformément entre les threads de travail. Ce problème peut être résolu en implémentant l'équilibrage des connexions de travail, qui permettra l'échange de connexions entre les threads de travail.

Conclusion

Le modèle de thread d'Envoy est conçu pour offrir une facilité de programmation et un parallélisme massif au détriment d'un gaspillage potentiel de mémoire et de connexions s'il n'est pas configuré correctement. Ce modèle lui permet de très bien fonctionner avec un nombre de threads et un débit très élevés.
Comme je l'ai brièvement mentionné sur Twitter, la conception peut également fonctionner sur une pile réseau complète en mode utilisateur telle que DPDK (Data Plane Development Kit), ce qui peut donner lieu à des serveurs conventionnels traitant des millions de requêtes par seconde avec un traitement L7 complet. Il sera très intéressant de voir ce qui sera construit dans les prochaines années.
Un dernier commentaire rapide : on m'a demandé à plusieurs reprises pourquoi nous avions choisi le C++ pour Envoy. La raison demeure qu’il s’agit toujours du seul langage de qualité industrielle largement utilisé dans lequel l’architecture décrite dans cet article peut être construite. Le C++ n'est certainement pas adapté à tous, ni même à de nombreux projets, mais pour certains cas d'utilisation, il reste le seul outil permettant de faire le travail.

Liens vers le code

Liens vers des fichiers avec des interfaces et des implémentations d'en-tête discutées dans cet article :

Source: habr.com

Ajouter un commentaire