[Traduction] Modèle de flux Envoy (Envoy threading model)

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

Cet article me semble assez intéressant, et comme Envoy est le plus souvent utilisé comme partie d'«istio» ou simplement comme «ingress controller» pour Kubernetes, la plupart des gens n'ont pas le même type d'interaction directe avec lui que, par exemple, lors des installations typiques de Nginx ou Haproxy. Cependant, si quelque chose se casse, il serait bon de comprendre comment cela fonctionne de l'intérieur. J'ai essayé de traduire autant de texte que possible en français, y compris des termes techniques, pour ceux qui trouvent cela difficile, j'ai laissé les originaux entre parenthèses. Bienvenue sous le cut.

La documentation technique de bas niveau sur la base de code d'Envoy est actuellement assez limitée. Pour y remédier, je prévois de faire une série d'articles de blog sur différents sous-systèmes d'Envoy. Comme ceci est le 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.

Une des questions techniques les plus courantes que je reçois à propos d'Envoy est la demande d'une description à bas niveau du modèle de threading utilisé. Dans ce post, je décrirai comment Envoy associe les connexions aux threads, ainsi qu'une description du système de stockage local de threads (Thread Local Storage) utilisé en interne pour rendre le code plus parallèle et hautement performant.

Aperçu des threads

[Traduction] Modèle de flux Envoy (Envoy threading model)

Envoy utilise trois types de threads différents :

  • Principal (Main) : Ce thread gère le démarrage et l'arrêt du processus, tout le traitement API XDS (xDiscovery Service), y compris DNS, vérification de l'état (health checking), gestion générale du cluster et du processus de service (runtime), réinitialisation des statistiques, administration et gestion générale des processus — signaux Linux, redémarrage à chaud (hot restart), etc. Tout ce qui se passe dans ce thread est asynchrone et «non-bloquant». Dans l'ensemble, le thread principal coordonne tous les processus critiques de fonctionnalité pour lesquels une grande quantité de CPU n'est pas requise. Cela permet d'écrire la plupart du code de gestion comme s'il était mono-thread.
  • Travailleur (Worker) : Par défaut, Envoy crée un thread de travail (worker thread) pour chaque thread matériel dans le 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 » (event loop) qui est responsable de l'écoute de chaque écouteur (listener). Au moment de la rédaction de cet article (29 juillet 2017), il n'y a pas de segmentation (sharding) des écouteurs (listeners), l'acceptation de nouvelles connexions, la création d'une instance de la pile de filtres pour les connexions et le traitement de toutes les opérations d'entrée-sortie (IO) pendant toute la durée de la connexion. Encore une fois, cela permet d’écrire la plupart du code de traitement des connexions comme s'il s'agissait d'un environnement mono-thread.
  • Flush du fichier (File flusher) : Chaque fichier que rédige Envoy, principalement les journaux d'accès (access logs), dispose actuellement d'un thread bloquant indépendant. Cela est dû au fait que l'écriture dans des fichiers mis en cache par le système de fichiers, même en utilisant O_NONBLOCK , peut parfois être bloquée (soupir). Lorsque les threads de travail doivent écrire dans un fichier, les données sont en fait déplacées dans un tampon en mémoire, où elles finissent par être vidées via le flux file flush. C'est l'un des domaines du code où techniquement tous les threads de travail (worker threads) peuvent bloquer (block) le même verrou (lock) en essayant de remplir le tampon de mémoire.

Gestion des connexions (Connection handling)

Comme mentionné brièvement ci-dessus, tous les threads de travail écoutent tous les écouteurs (listeners) sans aucune segmentation. Ainsi, le noyau est utilisé pour acheminer intelligemment les sockets acceptés vers les threads de travail. Les noyaux modernes sont généralement très bons dans ce domaine, ils utilisent des fonctionnalités telles que la priorité d'entrée-sortie (IO) pour tenter de remplir un thread de travail avant de commencer à utiliser d'autres threads qui écoutent également le même socket, et évitent d'utiliser un verrouillage cyclique (Spinlock) pour traiter chaque demande.
Une fois qu'une connexion est acceptée sur un thread de travail (worker thread), elle ne quitte jamais ce thread. Tout le traitement ultérieur de la connexion est entièrement géré dans le thread de travail (worker thread), y compris tout comportement de transfert (forwarding behavior).

Cela a plusieurs conséquences importantes :

  • Tous les pools de connexions dans Envoy se rapportent à un flux de travail. Ainsi, bien que les pools de connexions HTTP/2 n'établissent qu'une seule connexion par hôte au niveau supérieur à la fois, s'il y a quatre flux de travail, il y aura quatre connexions HTTP/2 à l'hôte au niveau supérieur dans un état stable.
  • La raison pour laquelle Envoy fonctionne de cette manière est que, en conservant tout dans un seul flux de travail, presque tout le code peut être écrit sans verrouillage et comme s'il était à thread unique. Ce design simplifie l'écriture d'une grande quantité de code et évolue incroyablement bien pour un nombre presque illimité de flux de travail.
  • Cependant, l'un des principaux enseignements est qu'en termes d'efficacité, le pool de mémoire et de connexions doit vraiment être configuré correctement. --concurrencyAvoir plus de flux de travail que nécessaire entraînera une perte de mémoire, la création de connexions inactives supplémentaires et une diminution de la rapidité d'accès au pool de connexions. Chez Lyft, nos conteneurs envoyés sidecar fonctionnent avec un niveau de parallélisme très faible, de sorte que les performances correspondent à peu près aux services avec lesquels ils coexistent. Nous exécutons Envoy en tant que proxy frontal uniquement lors du parallélisme maximal.

Ce que signifie le mode non-bloquant

Le terme « non-bloquant » a été utilisé plusieurs fois lors de la discussion sur le fonctionnement des threads principaux et de travail. Tout le code est écrit avec l'hypothèse que rien ne sera jamais bloqué. Cependant, ce n'est pas tout à fait vrai.

Envoy utilise plusieurs verrous de processus prolongés :

  • Comme déjà mentionné, lors de l'enregistrement des journaux d'accès, tous les flux de travail obtiennent le même verrou avant de remplir le tampon journal en mémoire. Le temps de maintien du verrou doit être très faible, mais il est possible que ce verrou soit contesté en cas de parallélisme élevé et de forte bande passante.
  • Envoy utilise un système très complexe pour traiter les statistiques, qui est local au flux. Ce sera l'objet d'un article séparé. Cependant, je vais brièvement mentionner qu'en tant que partie de la gestion locale des statistiques de flux, il est parfois nécessaire d'obtenir un verrou pour le « stockage central des statistiques ». Ce verrou ne devrait jamais être requis.
  • Le flux principal a périodiquement besoin de coordination avec tous les flux de travail. Cela se fait par la « publication » depuis le flux principal vers les flux de travail, et parfois des flux de travail vers le flux principal. Pour l'envoi, un verrou est nécessaire pour que le message publié puisse être mis en file d'attente pour une livraison ultérieure. Ces verrous ne devraient jamais faire l'objet d'une concurrence sérieuse, mais ils peuvent tout de même être techniquement bloqués.
  • Lorsque Envoy écrit dans le flux d'erreurs système (erreur standard), il obtient un verrou de l'ensemble du processus. En général, la journalisation locale d'Envoy est considérée comme horrible en termes de performances, donc peu d'attention est accordée à son amélioration.
  • Il existe quelques autres blocages occasionnels, mais aucun d'entre eux n'est critique pour les performances et ne devrait jamais être contesté.

Stockage local au thread (Thread local storage)

En raison de la manière dont Envoy sépare les responsabilités entre le flux principal et le flux de travail, il existe une exigence selon laquelle un traitement complexe peut être réalisé dans le flux principal, puis fourni à chaque flux de travail avec un degré élevé de parallélisme. Cette section décrit le système de stockage local des threads (TLS) d'Envoy à un niveau élevé. Dans la section suivante, je décrirai comment il est utilisé pour gérer le cluster.
[Traduction] Modèle de flux Envoy (Envoy threading model)

Comme déjà décrit, le flux principal gère pratiquement toutes les fonctions de gestion et la fonctionnalité du plan de contrôle dans le processus d'Envoy. Le plan de contrôle ici est un peu surchargé, mais si l'on considère cela dans le cadre du processus même d'Envoy et en le comparant au transfert effectué par les flux de travail, cela semble logique. En règle générale, le processus du flux principal effectue un certain travail, puis doit mettre à jour chaque flux de travail selon le résultat de ce travail. Dans ce cas, le thread de travail n'a pas besoin de verrouiller à chaque accès..

Le système TLS (Thread local storage) d'Envoy fonctionne de la manière suivante :

  • Le code s'exécutant dans le thread principal peut allouer un emplacement TLS pour l'ensemble du processus. Bien que cela soit abstrait, en pratique, il s'agit d'un index dans un vecteur permettant un accès O(1).
  • Le thread principal peut placer des données arbitraires dans son emplacement. Une fois cela fait, les données sont publiées dans chaque thread de travail comme un événement normal de la boucle d'événements.
  • Les threads de travail peuvent lire à partir de leur emplacement TLS et extraire toutes les données locales de threads disponibles là.

Bien que ce soit une paradigme très simple et incroyablement puissant, similaire au concept de verrouillage RCU (Read-Copy-Update). Essentiellement, les threads de travail ne voient jamais les changements de données dans les emplacements TLS pendant qu'ils exécutent du travail. Le changement ne se produit que pendant le repos entre les événements de travail.

Envoy utilise cela de deux manières différentes :

  • En conservant différentes données dans chaque thread de travail, l'accès à ces données se fait sans aucun verrou.
  • En conservant un pointeur commun vers des données globales en mode « lecture seule » dans chaque thread de travail. Ainsi, chaque thread de travail a un compteur de références sur les données qui ne peut pas être réduit pendant l'exécution du travail. Ce n'est que lorsque tous les travailleurs sont tranquilles et chargent de nouvelles données partagées que les anciennes données seront détruites. Cela est identique à RCU.

Fil de mise à jour du cluster (Cluster update threading)

Dans cette section, je vais décrire comment le TLS (Thread local storage) est utilisé pour gérer le cluster. La gestion du cluster comprend le traitement de l'API xDS et / ou DNS, ainsi que les vérifications de l'état de santé.
[Traduction] Modèle de flux Envoy (Envoy threading model)

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

  1. Le gestionnaire de cluster est un composant à l’intérieur d’Envoy qui gère tous les upstreams connus du cluster, l'API CDS (Cluster Discovery Service), les API SDS (Secret Discovery Service) et EDS (Endpoint Discovery Service), DNS et les vérifications de l'état de santé externes actives. Il est responsable de la création d'une représentation « finalement cohérente » (eventually consistent) de chaque upstream du cluster, qui inclut les hôtes découverts ainsi que l'état de santé.
  2. Le vérificateur de fonctionnement (health checker) effectue une vérification active de l'état de fonctionnement et informe le gestionnaire de cluster des changements d'état.
  3. CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS sont exécuté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 de traitement des événements.
  5. Lorsque le gestionnaire de cluster détermine qu'il y a un changement d'état pour le cluster, il crée un nouvel instantané de l'état du cluster, disponible en lecture seule, et l'envoie à chaque thread de travail.
  6. Pendant la prochaine période d'inactivité, le thread de travail mettra à jour l'instantané dans l'emplacement TLS désigné.
  7. Lors d'un événement d'entrée-sortie qui doit déterminer l'hôte pour l'équilibrage de charge, l'équilibreur de charge demandera un emplacement TLS (Thread local storage) pour obtenir des informations sur l'hôte. Aucun verrou n'est nécessaire pour cela. Notez également que TLS peut également initier des événements lors de mises à jour, afin que les sous-systèmes d'équilibrage de charge et d'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.

Utilisant la procédure décrite ci-dessus, Envoy peut traiter chaque demande sans aucun verrou (hormis ceux décrits précédemment). En dépit de la complexité du code TLS lui-même, la majeure partie du code n'a pas besoin de comprendre comment fonctionne le multi-threading et peut être écrite en mode mono-thread. Cela facilite l'écriture de la plus grande partie du code tout en offrant d'excellentes performances.

Autres sous-systèmes utilisant TLS (Other subsystems that make use of TLS)

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

Exemples d'utilisation :

  • Mécanisme de changement de fonctionnalité en cours d'exécution : La liste actuelle des fonctionnalités activées est calculée dans le thread principal. Ensuite, chaque thread de travail reçoit un instantané en lecture seule en 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 dans le thread principal. Un instantané en lecture seule sera ensuite fourni à chaque thread de travail en utilisant 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 : Comme il s'avère, le calcul de l'en-tête HTTP pour chaque requête (en exécutant ~25K+ RPS par cœur) est assez coûteux. Envoy calcule en centralisé l'en-tête 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 donner une bonne compréhension de l'utilisation de TLS.

Pièges de performance connus (Known performance pitfalls)

Bien qu'Envoy fonctionne globalement assez bien, il existe quelques domaines connus qui nécessitent de l'attention lorsqu'il est utilisé avec un très haut parallèle et une large bande passante :

  • Comme déjà décrit dans cet article, tous les threads de travail obtiennent actuellement un verrou lors de l'écriture dans le buffer mémoire du journal d'accès. Avec un haut degré de parallélisme et une large bande passante, il sera nécessaire d'effectuer un regroupement des journaux d'accès pour chaque thread de travail au prix d'une livraison désordonnée lors de l'écriture dans le fichier final. En alternative, un journal d'accès séparé peut être créé pour chaque thread de travail.
  • Bien que les statistiques soient très optimisées, avec un très haut degré de parallélisme et une large bande passante, il est probable qu'il y ait une concurrence atomique pour les statistiques individuelles. La solution à ce problème consiste à utiliser des compteurs pour un seul thread de travail avec des réinitialisations périodiques des compteurs centraux. Cela sera abordé dans un post ultérieur.
  • L'architecture existante 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 significatives pour le traitement. Il n'y a aucune garantie que les connexions soient uniformément réparties entre les threads de travail. Cela peut être résolu en mettant en œuvre un équilibrage des connexions de travail, où il sera possible d'échanger des connexions entre les threads de travail.

Conclusion (Conclusion)

Le modèle de threading d'Envoy est conçu pour assurer une programmation simple et un parallélisme massif grâce à une utilisation potentiellement excessive de la mémoire et des connexions, si elles ne sont pas correctement configurées. Ce modèle lui permet de fonctionner très efficacement avec un très grand nombre de threads et une large bande passante.
Comme je l'ai brièvement mentionné sur Twitter, le design peut également fonctionner au-dessus d'une pile réseau entièrement fonctionnelle en mode utilisateur, comme DPDK (Data Plane Development Kit), ce qui pourrait permettre à des serveurs ordinaires de traiter des millions de requêtes par seconde avec un traitement complet L7. 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 fois pourquoi nous avons choisi C++ pour Envoy. La raison est toujours que c'est le seul langage industriel largement utilisé qui permet de construire l'architecture décrite dans cet article. C++ n'est certainement pas adapté à tous les projets ou même à beaucoup d'entre eux, mais pour certains cas d'utilisation, c'est encore le seul outil capable de mener à bien la tâche.

Liens vers le code

Liens vers les fichiers d'interfaces et d'implémentation des en-têtes discutés dans cet article :

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster