[Traducción] Modelo de rosca Envoy

Tradución do artigo: Modelo de subproceso Envoy: https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Pareceume bastante interesante este artigo, e dado que Envoy úsase con máis frecuencia como parte do "istio" ou simplemente como o "controlador de entrada" de kubernetes, a maioría da xente non ten a mesma interacción directa con el que, por exemplo, co típico Instalacións de Nginx ou Haproxy. Non obstante, se algo se rompe, sería bo entender como funciona desde dentro. Tentei traducir o máximo posible do texto ao ruso, incluídas palabras especiais; para aqueles que lles resulte doloroso mirar isto, deixei os orixinais entre parénteses. Benvido ao gato.

A documentación técnica de baixo nivel para a base de código Envoy é actualmente bastante escasa. Para remediar isto, penso facer unha serie de publicacións no blog sobre os distintos subsistemas de Envoy. Xa que este é o primeiro artigo, por favor, avísame o que pensas e o que che pode interesar nos próximos artigos.

Unha das preguntas técnicas máis comúns que recibo sobre Envoy é pedir unha descrición de baixo nivel do modelo de fíos que usa. Nesta publicación, describirei como Envoy asigna as conexións aos fíos, así como o sistema de almacenamento local Thread que usa internamente para facer que o código sexa máis paralelo e de alto rendemento.

Visión xeral do threading

[Traducción] Modelo de rosca Envoy

Envoy usa tres tipos diferentes de fluxos:

  • Principal: Este fío controla o inicio e a terminación do proceso, todo o procesamento da API XDS (xDiscovery Service), incluído o DNS, a comprobación de saúde, a xestión xeral do clúster e do tempo de execución, o restablecemento das estatísticas, a administración e a xestión xeral do proceso: sinais de Linux, reinicio en quente, etc. ocorre neste fío é asíncrono e "non bloquea". En xeral, o fío principal coordina todos os procesos de funcionalidade críticos que non requiren unha gran cantidade de CPU para executarse. Isto permite que a maioría do código de control se escriba coma se fose un só fío.
  • Traballador: De forma predeterminada, Envoy crea un fío de traballo para cada fío de hardware do sistema, isto pódese controlar mediante a opción --concurrency. Cada fío de traballo executa un bucle de eventos "non bloqueador", que se encarga de escoitar a cada oínte; no momento da escritura (29 de xullo de 2017) non hai fragmentación do oínte, aceptando novas conexións, instanciando unha pila de filtros para a conexión e procesando todas as operacións de entrada/saída (IO) durante a vida útil da conexión. De novo, isto permite que a maioría do código de manexo de conexións se escriba coma se fose un só fío.
  • Lavado de ficheiros: Cada ficheiro que escribe Envoy, principalmente rexistros de acceso, ten actualmente un fío de bloqueo independente. Isto débese ao feito de escribir en ficheiros almacenados na caché polo sistema de ficheiros mesmo cando se usa O_NONBLOCK ás veces pode bloquearse (suspiro). Cando os fíos de traballo necesitan escribir nun ficheiro, os datos móvense realmente a un búfer na memoria onde finalmente se lavan a través do fío. descarga de ficheiros. Esta é unha área de código onde tecnicamente todos os fíos de traballo poden bloquear o mesmo bloqueo mentres intentan encher un búfer de memoria.

Manexo de conexións

Como se comentou brevemente anteriormente, todos os fíos de traballo escoitan a todos os oíntes sen ningún fragmento. Así, o núcleo úsase para enviar con gracia os sockets aceptados aos fíos de traballo. Os núcleos modernos son xeralmente moi bos nisto, usan funcións como o aumento da prioridade de entrada/saída (IO) para tentar encher un fío con traballo antes de comezar a usar outros fíos que tamén están escoitando no mesmo socket, e tampouco usar round robin. bloqueo (Spinlock) para procesar cada solicitude.
Unha vez que se acepta unha conexión nun fío de traballo, nunca abandona ese fío. Todo o procesamento posterior da conexión manéxase integramente no fío de traballo, incluído calquera comportamento de reenvío.

Isto ten varias consecuencias importantes:

  • Todos os grupos de conexións en Envoy están asignados a un fío de traballo. Polo tanto, aínda que os conxuntos de conexións HTTP/2 só fan unha conexión a cada host upstream á vez, se hai catro fíos de traballo, haberá catro conexións HTTP/2 por host upstream en estado estacionario.
  • A razón pola que Envoy funciona deste xeito é que ao manter todo nun único fío de traballo, case todo o código pódese escribir sen bloquear e coma se fose un só fío. Este deseño facilita a escritura de moito código e escala incriblemente ben a un número case ilimitado de fíos de traballo.
  • Non obstante, unha das principais conclusións é que desde o punto de vista da agrupación de memoria e da eficiencia da conexión, en realidade é moi importante configurar o --concurrency. Ter máis fíos de traballo dos necesarios desperdiciará memoria, creará máis conexións inactivas e reducirá a taxa de agrupación de conexións. En Lyft, os contedores sidecar do noso enviado funcionan cunha concorrencia moi baixa para que o rendemento coincida aproximadamente cos servizos aos que se atopan. Executamos Envoy como un proxy de borde só na máxima simultaneidade.

Que significa non bloquear?

O termo "non bloquear" utilizouse varias veces ata agora cando se discute como funcionan os fíos de traballo principal e de traballo. Todo o código está escrito asumindo que nunca se bloquea nada. Non obstante, isto non é totalmente certo (que non é totalmente certo?).

Envoy usa varios bloqueos de proceso longo:

  • Como se comentou, ao escribir rexistros de acceso, todos os fíos de traballo adquiren o mesmo bloqueo antes de que se enche o búfer de rexistro en memoria. O tempo de retención do bloqueo debe ser moi baixo, pero é posible que o bloqueo se dispute con alta concorrencia e alto rendemento.
  • Envoy usa un sistema moi complexo para xestionar estatísticas que son locais do fío. Este será o tema dunha publicación separada. Non obstante, mencionarei brevemente que como parte do procesamento local de estatísticas de fíos, ás veces é necesario adquirir un bloqueo nunha "tenda de estatísticas" central. Este bloqueo nunca debería ser necesario.
  • O fío principal debe coordinarse periodicamente con todos os fíos de traballo. Isto faise "publicando" desde o fío principal ata os fíos de traballo, e ás veces desde os fíos de traballo de volta ao fío principal. O envío require un bloqueo para que a mensaxe publicada poida quedar en cola para a súa entrega posterior. Estes bloqueos nunca deben ser impugnados seriamente, pero aínda poden ser bloqueados tecnicamente.
  • Cando Envoy escribe un rexistro no fluxo de erros do sistema (erro estándar), adquire un bloqueo en todo o proceso. En xeral, o rexistro local de Envoy considérase terrible desde o punto de vista do rendemento, polo que non se prestou moita atención a melloralo.
  • Hai algúns outros bloqueos aleatorios, pero ningún deles é crítico para o rendemento e nunca debe ser desafiado.

Almacenamento local de fíos

Debido á forma en que Envoy separa as responsabilidades do fío principal das responsabilidades do fío de traballo, existe o requisito de que se poida realizar un procesamento complexo no fío principal e, a continuación, proporcionarse a cada fío de traballo dunha forma moi concorrente. Esta sección describe Envoy Thread Local Storage (TLS) a un alto nivel. Na seguinte sección describirei como se usa para xestionar un clúster.
[Traducción] Modelo de rosca Envoy

Como xa se describiu, o fío principal xestiona practicamente todas as funcións do plano de xestión e control no proceso Envoy. O plano de control está un pouco sobrecargado aquí, pero cando o miras dentro do propio proceso Envoy e o comparas co reenvío que fan os fíos de traballo, ten sentido. A regra xeral é que o proceso de fío principal fai algún traballo e, a continuación, ten que actualizar cada fío de traballo segundo o resultado dese traballo. neste caso, o fío de traballo non precisa adquirir un bloqueo en cada acceso.

O sistema TLS (almacenamento local de subprocesos) de Envoy funciona do seguinte xeito:

  • O código que se executa no fío principal pode asignar un slot TLS para todo o proceso. Aínda que isto é abstraído, na práctica é un índice nun vector, proporcionando acceso a O(1).
  • O fío principal pode instalar datos arbitrarios no seu slot. Cando se fai isto, os datos publícanse en cada fío de traballo como un evento de bucle de eventos normal.
  • Os fíos de traballo poden ler desde o seu slot TLS e recuperar os datos locais dispoñibles alí.

Aínda que é un paradigma moi sinxelo e incriblemente poderoso, é moi semellante ao concepto de bloqueo RCU (Read-Copy-Update). Esencialmente, os fíos de traballo nunca ven ningún cambio de datos nos slots TLS mentres o traballo está en execución. O cambio ocorre só durante o período de descanso entre eventos laborais.

Envoy usa isto de dúas formas diferentes:

  • Ao almacenar datos diferentes en cada fío de traballo, pódese acceder aos datos sen ningún bloqueo.
  • Ao manter un punteiro compartido a datos globais en modo de só lectura en cada fío de traballo. Así, cada fío de traballo ten un reconto de referencias de datos que non se pode diminuír mentres se executa o traballo. Só cando todos os traballadores se calmen e carguen novos datos compartidos, os datos antigos serán destruídos. Isto é idéntico ao RCU.

Subproceso de actualización do clúster

Nesta sección, describirei como se usa TLS (almacenamento local de subprocesos) para xestionar un clúster. A xestión de clústeres inclúe o procesamento de API e/ou DNS xDS, así como a comprobación do estado.
[Traducción] Modelo de rosca Envoy

A xestión do fluxo de clústeres inclúe os seguintes compoñentes e pasos:

  1. O xestor de clústeres é un compoñente de Envoy que xestiona todos os clústeres ascendentes coñecidos, a API do servizo de descubrimento de clústeres (CDS), as API do servizo de descubrimento secreto (SDS) e do servizo de descubrimento de puntos finais (EDS), DNS e comprobacións externas activas. É responsable de crear unha vista "eventualmente coherente" de cada clúster ascendente, que inclúe os hosts descubertos así como o estado de saúde.
  2. O comprobador de saúde realiza unha comprobación de estado activa e informa dos cambios de estado de saúde ao xestor do clúster.
  3. CDS (Servizo de descubrimento de clúster) / SDS (Servizo de descubrimento secreto) / EDS (Servizo de descubrimento de punto final) / DNS realízanse para determinar a pertenza ao clúster. O cambio de estado devólvese ao xestor de clúster.
  4. Cada fío de traballo executa continuamente un bucle de eventos.
  5. Cando o xestor do clúster determina que o estado dun clúster cambiou, crea unha nova instantánea de só lectura do estado do clúster e envíaa a cada fío de traballo.
  6. Durante o seguinte período de silencio, o fío de traballo actualizará a instantánea no slot TLS asignado.
  7. Durante un evento de E/S que se supón que determina o host para equilibrar a carga, o equilibrador de carga solicitará un slot TLS (almacenamento local de subprocesos) para obter información sobre o host. Isto non require bloqueos. Teña en conta tamén que TLS tamén pode activar eventos de actualización para que os equilibradores de carga e outros compoñentes poidan volver calcular cachés, estruturas de datos, etc. Isto está fóra do alcance desta publicación, pero úsase en varios lugares do código.

Usando o procedemento anterior, Envoy pode procesar todas as solicitudes sen ningún bloqueo (excepto o descrito anteriormente). Ademais da complexidade do propio código TLS, a maior parte do código non precisa entender como funciona o multithreading e pódese escribir nun só fío. Isto fai que a maior parte do código sexa máis fácil de escribir ademais dun rendemento superior.

Outros subsistemas que fan uso de TLS

TLS (almacenamento local de fíos) e RCU (actualización de lectura de copias) úsanse amplamente en Envoy.

Exemplos de uso:

  • Mecanismo para cambiar a funcionalidade durante a execución: A lista actual de funcións activadas calcúlase no fío principal. A continuación, cada fío de traballo recibe unha instantánea de só lectura mediante a semántica RCU.
  • Substitución das táboas de rutas: Para as táboas de rutas proporcionadas por RDS (Route Discovery Service), as táboas de rutas créanse no fío principal. A instantánea de só lectura proporcionarase posteriormente a cada fío de traballo mediante a semántica RCU (Read Copy Update). Isto fai que cambiar as táboas de rutas sexa atomicamente eficiente.
  • Caché de cabeceira HTTP: Polo que resulta, calcular a cabeceira HTTP para cada solicitude (mentres se executa ~ 25K+ RPS por núcleo) é bastante caro. Envoy calcula a cabeceira de forma centralizada aproximadamente cada medio segundo e ofrécelle a cada traballador a través de TLS e RCU.

Hai outros casos, pero os exemplos anteriores deberían proporcionar unha boa comprensión de para que se usa TLS.

Trampas de rendemento coñecidas

Aínda que Envoy funciona bastante ben en xeral, hai algunhas áreas notables que requiren atención cando se usa con concorrencia e rendemento moi altos:

  • Como se describe neste artigo, actualmente todos os fíos de traballo adquiren un bloqueo ao escribir no búfer da memoria do rexistro de acceso. Cunha alta simultaneidade e alto rendemento, terás que agrupar os rexistros de acceso para cada fío de traballo a costa da entrega fóra de pedido ao escribir no ficheiro final. Alternativamente, pode crear un rexistro de acceso separado para cada fío de traballo.
  • Aínda que as estatísticas están moi optimizadas, a concorrencia e rendemento moi altos probablemente haxa unha disputa atómica sobre as estatísticas individuais. A solución a este problema son os contadores por fío de traballo con reinicio periódico dos contadores centrais. Isto será discutido nunha publicación posterior.
  • A arquitectura actual non funcionará ben se Envoy se implanta nun escenario onde hai moi poucas conexións que requiren recursos de procesamento significativos. Non hai garantía de que as conexións se distribúan uniformemente entre os fíos de traballo. Isto pódese solucionar implementando o equilibrio de conexións dos traballadores, que permitirá o intercambio de conexións entre fíos de traballo.

Conclusión

O modelo de fíos de Envoy está deseñado para proporcionar facilidade de programación e paralelismo masivo a costa de posibles desperdicios de memoria e conexións se non se configuran correctamente. Este modelo permítelle funcionar moi ben con un número de fíos e un rendemento moi elevados.
Como mencionei brevemente en Twitter, o deseño tamén se pode executar enriba dunha pila de rede en modo usuario completo, como DPDK (Data Plane Development Kit), o que pode dar lugar a que os servidores convencionais xestionen millóns de solicitudes por segundo cun procesamento L7 completo. Será moi interesante ver o que se constrúe nos próximos anos.
Un último comentario rápido: Preguntáronme moitas veces por que escollemos C++ para Envoy. O motivo segue sendo a única linguaxe de grao industrial moi utilizada na que se pode construír a arquitectura descrita neste post. C++ definitivamente non é axeitado para todos ou incluso para moitos proxectos, pero para certos casos de uso segue a ser a única ferramenta para facer o traballo.

Ligazóns ao código

Ligazóns a ficheiros con interfaces e implementacións de cabeceiras que se comentan nesta publicación:

Fonte: www.habr.com

Engadir un comentario