[Traducció] Model d'enfilament d'Envoy

Traducció de l'article: Model de fils d'Envoy - https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Aquest article m'ha semblat força interessant, i com que Envoy s'utilitza més sovint com a part del "istio" o simplement com a "controlador d'entrada" de kubernetes, la majoria de la gent no té la mateixa interacció directa amb ell que, per exemple, amb el típic Instal·lacions de Nginx o Haproxy. Tanmateix, si alguna cosa es trenca, seria bo entendre com funciona des de dins. He intentat traduir la major part del text al rus possible, incloses paraules especials; per a aquells que els resulta dolorós mirar-ho, he deixat els originals entre parèntesis. Benvingut al gat.

La documentació tècnica de baix nivell per a la base de codi Envoy és actualment força escassa. Per solucionar-ho, tinc la intenció de fer una sèrie de publicacions al bloc sobre els diferents subsistemes d'Envoy. Com que aquest és el primer article, si us plau, feu-me saber què us sembla i què us pot interessar en els propers articles.

Una de les preguntes tècniques més habituals que rebo sobre Envoy és demanar una descripció de baix nivell del model de fil que utilitza. En aquesta publicació, descriuré com Envoy mapa les connexions als fils, així com el sistema Thread Local Storage que utilitza internament per fer que el codi sigui més paral·lel i d'alt rendiment.

Visió general del fil

[Traducció] Model d'enfilament d'Envoy

Envoy utilitza tres tipus diferents de fluxos:

  • Principal: Aquest fil controla l'inici i la finalització del procés, tot el processament de l'API XDS (xDiscovery Service), inclòs el DNS, la comprovació de salut, la gestió general del clúster i el temps d'execució, el restabliment d'estadístiques, l'administració i la gestió general del procés: senyals de Linux, reinici en calent, etc. que passa en aquest fil és asíncron i "no bloqueja". En general, el fil principal coordina tots els processos de funcionalitat crític que no requereixen una gran quantitat de CPU per executar-se. Això permet que la majoria del codi de control s'escrigui com si es tractés d'un sol fil.
  • Treballador: Per defecte, Envoy crea un fil de treball per a cada fil de maquinari del sistema, això es pot controlar mitjançant l'opció --concurrency. Cada fil de treball executa un bucle d'esdeveniments "no bloquejant", que s'encarrega d'escoltar a cada oient; en el moment d'escriure aquest escrit (29 de juliol de 2017) no hi ha fragmentació de l'oient, acceptant noves connexions, creant una pila de filtres per a la connexió i processant totes les operacions d'entrada/sortida (IO) durant la vida útil de la connexió. De nou, això permet que la majoria del codi de gestió de connexions s'escrigui com si es tractés d'un sol fil.
  • Flux de fitxers: Cada fitxer que escriu Envoy, principalment registres d'accés, actualment té un fil de bloqueig independent. Això es deu al fet que l'escriptura en fitxers guardats a la memòria cau pel sistema de fitxers fins i tot quan s'utilitza O_NONBLOCK de vegades es pot bloquejar (sospir). Quan els fils de treball han d'escriure en un fitxer, les dades es mouen realment a una memòria intermèdia a la memòria on finalment s'encarreguen del fil. esborrar el fitxer. Aquesta és una àrea del codi on tècnicament tots els fils de treball poden bloquejar el mateix bloqueig mentre intenten omplir un buffer de memòria.

Gestió de la connexió

Com s'ha comentat breument anteriorment, tots els fils de treball escolten tots els oients sense cap fragmentació. Així, el nucli s'utilitza per enviar amb gràcia els sòcols acceptats als fils de treball. Els nuclis moderns són generalment molt bons en això, utilitzen funcions com l'augment de la prioritat d'entrada/sortida (IO) per intentar omplir un fil amb treball abans de començar a utilitzar altres fils que també escolten al mateix sòcol, i tampoc no utilitzen round robin. bloqueig (Spinlock) per processar cada sol·licitud.
Un cop acceptada una connexió en un fil de treball, mai no abandona aquest fil. Tot el processament posterior de la connexió es gestiona completament al fil de treball, inclòs qualsevol comportament de reenviament.

Això té diverses conseqüències importants:

  • Tots els grups de connexions d'Envoy s'assignen a un fil de treball. Així, tot i que els grups de connexions HTTP/2 només fan una connexió a cada amfitrió amunt a la vegada, si hi ha quatre fils de treball, hi haurà quatre connexions HTTP/2 per host amunt en estat estacionari.
  • La raó per la qual Envoy funciona d'aquesta manera és que mantenint-ho tot en un sol fil de treball, gairebé tot el codi es pot escriure sense bloquejar i com si fos un sol fil. Aquest disseny fa que sigui fàcil escriure molt de codi i s'ajusta increïblement bé a un nombre gairebé il·limitat de fils de treball.
  • Tanmateix, una de les principals conclusions és que, des del punt de vista de l'agrupació de memòria i de l'eficiència de la connexió, en realitat és molt important configurar el --concurrency. Tenir més fils de treball del necessari malgastarà memòria, crearà més connexions inactives i reduirà la taxa d'agrupació de connexions. A Lyft, els nostres contenidors sidecar enviats funcionen amb una concurrència molt baixa, de manera que el rendiment coincideix aproximadament amb els serveis als quals s'asseuen. Executem Envoy com a servidor intermediari només amb la màxima concurrència.

Què vol dir no bloquejar?

El terme "sense bloqueig" s'ha utilitzat diverses vegades fins ara quan es parla de com funcionen els fils de treball principal i de treball. Tot el codi s'escriu en el supòsit que mai no es bloqueja res. Tanmateix, això no és del tot cert (què no ho és del tot?).

Envoy utilitza diversos bloquejos de procés llarg:

  • Com s'ha comentat, quan escriuen registres d'accés, tots els fils de treball adquireixen el mateix bloqueig abans que s'ompli la memòria intermèdia de registre a la memòria. El temps de retenció del bloqueig hauria de ser molt baix, però és possible que el bloqueig es disputi amb una concurrència elevada i un alt rendiment.
  • Envoy utilitza un sistema molt complex per gestionar les estadístiques que són locals al fil. Aquest serà el tema d'una publicació a part. Tanmateix, esmentaré breument que com a part del processament local d'estadístiques de fils, de vegades és necessari adquirir un bloqueig en una "botiga d'estadístiques" central. Aquest bloqueig no hauria de ser mai necessari.
  • El fil principal s'ha de coordinar periòdicament amb tots els fils de treball. Això es fa "publicant" des del fil principal als fils de treball i, de vegades, des dels fils de treball al fil principal. L'enviament requereix un bloqueig perquè el missatge publicat es pugui posar a la cua per a un lliurament posterior. Aquests bloquejos mai s'han de contestar seriosament, però encara es poden bloquejar tècnicament.
  • Quan l'Envoy escriu un registre al flux d'errors del sistema (error estàndard), adquireix un bloqueig a tot el procés. En general, el registre local d'Envoy es considera terrible des del punt de vista del rendiment, de manera que no s'ha prestat molta atenció a millorar-lo.
  • Hi ha uns quants altres bloquejos aleatoris, però cap d'ells és crític de rendiment i no s'ha de desafiar mai.

Emmagatzematge local del fil

A causa de la forma en què Envoy separa les responsabilitats del fil principal de les responsabilitats del fil de treball, hi ha un requisit que es pugui fer un processament complex al fil principal i després proporcionar-los a cada fil de treball d'una manera molt concurrent. Aquesta secció descriu Envoy Thread Local Storage (TLS) a un alt nivell. A la següent secció descriuré com s'utilitza per gestionar un clúster.
[Traducció] Model d'enfilament d'Envoy

Com ja s'ha descrit, el fil principal gestiona pràcticament totes les funcionalitats del pla de gestió i control del procés d'Envoy. El pla de control està una mica sobrecarregat aquí, però quan el mireu dins del propi procés d'Envoy i el compareu amb el reenviament que fan els fils de treball, té sentit. La regla general és que el procés del fil principal fa una mica de feina i després ha d'actualitzar cada fil de treball segons el resultat d'aquest treball. en aquest cas, el fil de treball no necessita adquirir un bloqueig a cada accés.

El sistema TLS (emmagatzematge local de fils) d'Envoy funciona de la següent manera:

  • El codi que s'executa al fil principal pot assignar una ranura TLS per a tot el procés. Tot i que això s'abstraeix, a la pràctica és un índex en un vector, proporcionant accés O(1).
  • El fil principal pot instal·lar dades arbitràries a la seva ranura. Quan això es fa, les dades es publiquen a cada fil de treball com a esdeveniment de bucle d'esdeveniments normal.
  • Els fils de treball poden llegir des de la seva ranura TLS i recuperar les dades locals dels fils disponibles allà.

Tot i que és un paradigma molt senzill i increïblement potent, és molt semblant al concepte de bloqueig RCU (Read-Copy-Update). Bàsicament, els fils de treball no veuen mai cap canvi de dades a les ranures TLS mentre s'està treballant. El canvi només es produeix durant el període de descans entre esdeveniments laborals.

Envoy ho fa servir de dues maneres diferents:

  • En emmagatzemar dades diferents a cada fil de treball, es pot accedir a les dades sense cap bloqueig.
  • En mantenir un punter compartit a les dades globals en mode de només lectura a cada fil de treball. Així, cada fil de treball té un recompte de referència de dades que no es pot disminuir mentre s'executa el treball. Només quan tots els treballadors es calmen i pugin noves dades compartides, les dades antigues es destruiran. Això és idèntic a RCU.

Fils d'actualització del clúster

En aquesta secció, descriuré com s'utilitza TLS (emmagatzematge local de fils) per gestionar un clúster. La gestió del clúster inclou l'API xDS i/o el processament de DNS, així com la comprovació de l'estat.
[Traducció] Model d'enfilament d'Envoy

La gestió del flux de clúster inclou els components i passos següents:

  1. El gestor de clúster és un component d'Envoy que gestiona tots els clústers coneguts amunt, l'API del servei de descoberta de clúster (CDS), les API del servei de descoberta secreta (SDS) i del servei de descoberta de punts finals (EDS), DNS i comprovacions externes actives. És responsable de crear una visió "eventualment coherent" de cada clúster aigües amunt, que inclou els amfitrions descoberts i l'estat de salut.
  2. El verificador de salut realitza una comprovació de salut activa i informa dels canvis d'estat de salut al gestor de clúster.
  3. CDS (Servei de descoberta de clúster) / SDS (Servei de descoberta secreta) / EDS (Servei de descoberta de punts finals) / DNS es realitzen per determinar la pertinença al clúster. El canvi d'estat es retorna al gestor de clúster.
  4. Cada fil de treball executa contínuament un bucle d'esdeveniments.
  5. Quan el gestor del clúster determina que l'estat d'un clúster ha canviat, crea una nova instantània de només lectura de l'estat del clúster i l'envia a cada fil de treball.
  6. Durant el proper període de silenci, el fil de treball actualitzarà la instantània a la ranura TLS assignada.
  7. Durant un esdeveniment d'E/S que se suposa que determina l'amfitrió per equilibrar la càrrega, l'equilibrador de càrrega sol·licitarà una ranura TLS (emmagatzematge local de fils) per obtenir informació sobre l'amfitrió. Això no requereix panys. Tingueu en compte també que TLS també pot activar esdeveniments d'actualització perquè els equilibradors de càrrega i altres components puguin tornar a calcular la memòria cau, les estructures de dades, etc. Això està fora de l'abast d'aquesta publicació, però s'utilitza en diversos llocs del codi.

Mitjançant el procediment anterior, Envoy pot processar totes les sol·licituds sense cap tipus de bloqueig (excepte com s'ha descrit anteriorment). A part de la complexitat del codi TLS en si, la majoria del codi no necessita entendre com funciona el multithreading i es pot escriure amb un sol fil. Això fa que la major part del codi sigui més fàcil d'escriure a més d'un rendiment superior.

Altres subsistemes que fan ús de TLS

TLS (emmagatzematge local de fils) i RCU (Actualització de la còpia de lectura) s'utilitzen àmpliament a Envoy.

Exemples d'ús:

  • Mecanisme per canviar la funcionalitat durant l'execució: La llista actual de funcionalitats habilitades es calcula al fil principal. A continuació, a cada fil de treball se li dóna una instantània de només lectura mitjançant la semàntica RCU.
  • Substitució de taules de ruta: Per a les taules de ruta proporcionades per RDS (Route Discovery Service), les taules de ruta es creen al fil principal. La instantània de només lectura es proporcionarà posteriorment a cada fil de treball mitjançant la semàntica RCU (Read Copy Update). Això fa que el canvi de taules de ruta sigui atòmicament eficient.
  • Emmagatzematge a la memòria cau de la capçalera HTTP: Com a resultat, calcular la capçalera HTTP per a cada sol·licitud (mentre s'executa ~ 25K + RPS per nucli) és bastant car. Envoy calcula centralment la capçalera aproximadament cada mig segon i la proporciona a cada treballador mitjançant TLS i RCU.

Hi ha altres casos, però els exemples anteriors haurien de proporcionar una bona comprensió de per a què s'utilitza TLS.

Errors de rendiment coneguts

Tot i que Envoy funciona força bé en general, hi ha algunes àrees notables que requereixen atenció quan s'utilitza amb concurrència i rendiment molt alts:

  • Tal com es descriu en aquest article, actualment tots els fils de treball adquireixen un bloqueig quan escriuen a la memòria intermèdia del registre d'accés. Amb una concurrència alta i un alt rendiment, haureu de agrupar els registres d'accés per a cada fil de treball a costa de l'entrega fora de comanda quan escriviu al fitxer final. Alternativament, podeu crear un registre d'accés independent per a cada fil de treball.
  • Tot i que les estadístiques estan molt optimitzades, a una concurrència i un rendiment molt alts probablement hi haurà una disputa atòmica sobre les estadístiques individuals. La solució a aquest problema són comptadors per fil de treball amb restabliment periòdic dels comptadors centrals. Això es comentarà en una publicació posterior.
  • L'arquitectura actual no funcionarà bé si Envoy es desplega en un escenari on hi ha molt poques connexions que requereixen recursos de processament importants. No hi ha cap garantia que les connexions es distribueixin uniformement entre els fils de treball. Això es pot resoldre implementant l'equilibri de connexions de treballador, que permetrà l'intercanvi de connexions entre fils de treball.

Conclusió

El model de fils d'Envoy està dissenyat per proporcionar facilitat de programació i paral·lelisme massiu a costa de la memòria i les connexions potencialment malbaratas si no es configura correctament. Aquest model li permet funcionar molt bé amb un nombre de fils i un rendiment molt elevats.
Com he esmentat breument a Twitter, el disseny també es pot executar a sobre d'una pila de xarxa en mode d'usuari complet, com ara DPDK (Data Plane Development Kit), que pot provocar que els servidors convencionals gestionen milions de sol·licituds per segon amb un processament L7 complet. Serà molt interessant veure què es construirà en els propers anys.
Un últim comentari ràpid: m'han preguntat moltes vegades per què vam triar C++ per a Envoy. El motiu segueix sent que encara és l'únic llenguatge de grau industrial àmpliament utilitzat en el qual es pot construir l'arquitectura descrita en aquesta publicació. Definitivament, C++ no és adequat per a tots o fins i tot per a molts projectes, però per a determinats casos d'ús segueix sent l'única eina per fer la feina.

Enllaços al codi

Enllaços a fitxers amb interfícies i implementacions de capçalera que es discuteixen en aquesta publicació:

Font: www.habr.com

Afegeix comentari