[Traducción] Modelo de subprocesamiento Envoy

Traducción del artículo: Modelo de subprocesamiento de Envoy: https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Este artículo me pareció bastante interesante y, dado que Envoy se utiliza con mayor frecuencia como parte del "istio" o simplemente como el "controlador de ingreso" de Kubernetes, la mayoría de las personas no tienen la misma interacción directa con él que, por ejemplo, con el típico Instalaciones Nginx o Haproxy. Sin embargo, si algo se rompe, sería bueno entender cómo funciona desde dentro. Intenté traducir la mayor cantidad posible del texto al ruso, incluidas palabras especiales; para aquellos a quienes les resulte doloroso mirar esto, dejé los originales entre paréntesis. Bienvenido al gato.

La documentación técnica de bajo nivel para el código base de Envoy es actualmente bastante escasa. Para remediar esto, planeo hacer una serie de publicaciones de blog sobre los distintos subsistemas de Envoy. Dado que este es el primer artículo, déjeme saber lo que piensa y lo que podría interesarle en artículos futuros.

Una de las preguntas técnicas más comunes que recibo sobre Envoy es solicitar una descripción de bajo nivel del modelo de subprocesos que utiliza. En esta publicación, describiré cómo Envoy asigna conexiones a subprocesos, así como el sistema de almacenamiento local de subprocesos que utiliza internamente para hacer que el código sea más paralelo y de alto rendimiento.

Descripción general del subproceso

[Traducción] Modelo de subprocesamiento Envoy

Envoy utiliza tres tipos diferentes de transmisiones:

  • Principal: Este hilo controla el inicio y la terminación del proceso, todo el procesamiento de la API XDS (xDiscovery Service), incluido DNS, verificación de estado, administración general del clúster y del tiempo de ejecución, restablecimiento de estadísticas, administración y administración general de procesos: señales de Linux, reinicio en caliente, etc. Lo que sucede en este hilo es asincrónico y "sin bloqueo". En general, el hilo principal coordina todos los procesos de funcionalidad críticos que no requieren una gran cantidad de CPU para ejecutarse. Esto permite que la mayoría del código de control se escriba como si fuera de un solo subproceso.
  • Obrero: De forma predeterminada, Envoy crea un subproceso de trabajo para cada subproceso de hardware en el sistema, esto se puede controlar usando la opción --concurrency. Cada hilo de trabajo ejecuta un bucle de eventos "sin bloqueo", que es responsable de escuchar a cada oyente; al momento de escribir este artículo (29 de julio de 2017) no hay fragmentación del oyente, no se aceptan nuevas conexiones, no se crea una instancia de una pila de filtros para la conexión y procesar todas las operaciones de entrada/salida (IO) durante la vida útil de la conexión. Nuevamente, esto permite que la mayor parte del código de manejo de conexiones se escriba como si fuera de un solo subproceso.
  • Descargador de archivos: Cada archivo que escribe Envoy, principalmente registros de acceso, actualmente tiene un hilo de bloqueo independiente. Esto se debe al hecho de que escribir en archivos almacenados en caché por el sistema de archivos incluso cuando se usa O_NONBLOCK A veces puede bloquearse (suspiro). Cuando los subprocesos de trabajo necesitan escribir en un archivo, los datos en realidad se mueven a un búfer en la memoria donde finalmente se vacían a través del subproceso. descarga de archivos. Esta es un área del código donde técnicamente todos los subprocesos de trabajo pueden bloquear el mismo bloqueo mientras intentan llenar un búfer de memoria.

Manejo de conexión

Como se analizó brevemente anteriormente, todos los subprocesos de trabajo escuchan a todos los oyentes sin ningún tipo de fragmentación. Por lo tanto, el kernel se utiliza para enviar correctamente los sockets aceptados a los subprocesos de trabajo. Los kernels modernos generalmente son muy buenos en esto, usan características como el aumento de prioridad de entrada/salida (IO) para intentar llenar un subproceso con trabajo antes de comenzar a usar otros subprocesos que también están escuchando en el mismo socket, y tampoco usan round robin. bloqueo (Spinlock) para procesar cada solicitud.
Una vez que se acepta una conexión en un subproceso de trabajo, nunca abandona ese subproceso. Todo el procesamiento posterior de la conexión se maneja íntegramente en el subproceso de trabajo, incluido cualquier comportamiento de reenvío.

Esto tiene varias consecuencias importantes:

  • Todos los grupos de conexiones en Envoy están asignados a un subproceso de trabajo. Por lo tanto, aunque los grupos de conexiones HTTP/2 solo realizan una conexión a cada host ascendente a la vez, si hay cuatro subprocesos de trabajo, habrá cuatro conexiones HTTP/2 por host ascendente en un estado estable.
  • La razón por la que Envoy funciona de esta manera es que al mantener todo en un único subproceso de trabajo, casi todo el código se puede escribir sin bloqueos y como si fuera de un solo subproceso. Este diseño facilita la escritura de una gran cantidad de código y se adapta increíblemente bien a una cantidad casi ilimitada de subprocesos de trabajo.
  • Sin embargo, una de las principales conclusiones es que desde el punto de vista del grupo de memoria y la eficiencia de la conexión, en realidad es muy importante configurar el --concurrency. Tener más subprocesos de trabajo de los necesarios desperdiciará memoria, creará más conexiones inactivas y reducirá la tasa de agrupación de conexiones. En Lyft, nuestros contenedores sidecar enviados funcionan con muy baja concurrencia, por lo que el rendimiento coincide aproximadamente con los servicios que los rodean. Ejecutamos Envoy como proxy perimetral solo con la máxima simultaneidad.

¿Qué significa no bloqueo?

El término "sin bloqueo" se ha utilizado varias veces hasta ahora cuando se analiza cómo funcionan los subprocesos principal y de trabajo. Todo el código se escribe bajo el supuesto de que nunca se bloquea nada. Sin embargo, esto no es del todo cierto (¿qué no es del todo cierto?).

Envoy utiliza varios bloqueos de procesos largos:

  • Como se mencionó, al escribir registros de acceso, todos los subprocesos de trabajo adquieren el mismo bloqueo antes de que se llene el búfer de registro en memoria. El tiempo de retención del bloqueo debe ser muy bajo, pero es posible impugnar el bloqueo con alta concurrencia y alto rendimiento.
  • Envoy utiliza un sistema muy complejo para manejar estadísticas locales del hilo. Este será tema de un post aparte. Sin embargo, mencionaré brevemente que como parte del procesamiento local de estadísticas de subprocesos, a veces es necesario adquirir un bloqueo en un "almacén de estadísticas" central. Este bloqueo nunca debería ser necesario.
  • El hilo principal necesita coordinarse periódicamente con todos los hilos de trabajo. Esto se hace "publicando" desde el hilo principal a los hilos de trabajo y, a veces, desde los hilos de trabajo al hilo principal. El envío requiere un bloqueo para que el mensaje publicado pueda ponerse en cola para su entrega posterior. Estos bloqueos nunca deberían cuestionarse seriamente, pero técnicamente aún pueden bloquearse.
  • Cuando Envoy escribe un registro en el flujo de errores del sistema (error estándar), adquiere un bloqueo en todo el proceso. En general, el registro local de Envoy se considera terrible desde el punto de vista del rendimiento, por lo que no se ha prestado mucha atención a mejorarlo.
  • Hay algunos otros bloqueos aleatorios, pero ninguno de ellos es crítico para el rendimiento y nunca debe cuestionarse.

Almacenamiento local de subprocesos

Debido a la forma en que Envoy separa las responsabilidades del subproceso principal de las responsabilidades del subproceso de trabajo, existe el requisito de que se pueda realizar un procesamiento complejo en el subproceso principal y luego proporcionarlo a cada subproceso de trabajo de una manera altamente concurrente. Esta sección describe el almacenamiento local de subprocesos (TLS) de Envoy en un nivel alto. En la siguiente sección describiré cómo se utiliza para administrar un clúster.
[Traducción] Modelo de subprocesamiento Envoy

Como ya se describió, el hilo principal maneja prácticamente todas las funciones del plano de gestión y control en el proceso Envoy. El plano de control está un poco sobrecargado aquí, pero cuando lo observa dentro del proceso Envoy y lo compara con el reenvío que realizan los subprocesos de trabajo, tiene sentido. La regla general es que el proceso del hilo principal realiza algún trabajo y luego necesita actualizar cada hilo de trabajo de acuerdo con el resultado de ese trabajo. en este caso, el hilo de trabajo no necesita adquirir un bloqueo en cada acceso.

El sistema TLS (almacenamiento local de subprocesos) de Envoy funciona de la siguiente manera:

  • El código que se ejecuta en el hilo principal puede asignar una ranura TLS para todo el proceso. Aunque esto es abstracto, en la práctica es un índice en un vector, que proporciona acceso O(1).
  • El hilo principal puede instalar datos arbitrarios en su ranura. Cuando se hace esto, los datos se publican en cada subproceso de trabajo como un evento de bucle de eventos normal.
  • Los subprocesos de trabajo pueden leer desde su ranura TLS y recuperar cualquier dato local del subproceso disponible allí.

Aunque es un paradigma muy simple e increíblemente poderoso, es muy similar al concepto de bloqueo RCU (Leer-Copiar-Actualizar). Básicamente, los subprocesos de trabajo nunca ven ningún cambio de datos en las ranuras TLS mientras se ejecuta el trabajo. El cambio ocurre sólo durante el período de descanso entre eventos laborales.

Envoy usa esto de dos maneras diferentes:

  • Al almacenar diferentes datos en cada subproceso de trabajo, se puede acceder a los datos sin ningún bloqueo.
  • Manteniendo un puntero compartido a datos globales en modo de solo lectura en cada subproceso de trabajo. Por lo tanto, cada subproceso de trabajo tiene un recuento de referencias de datos que no se puede disminuir mientras se ejecuta el trabajo. Solo cuando todos los trabajadores se calmen y carguen nuevos datos compartidos se destruirán los datos antiguos. Esto es idéntico a RCU.

Subprocesos de actualización del clúster

En esta sección, describiré cómo se utiliza TLS (almacenamiento local de subprocesos) para administrar un clúster. La gestión de clústeres incluye API xDS y/o procesamiento DNS, así como verificación de estado.
[Traducción] Modelo de subprocesamiento Envoy

La gestión del flujo del clúster incluye los siguientes componentes y pasos:

  1. El Administrador de clústeres es un componente dentro de Envoy que administra todos los flujos ascendentes de clústeres conocidos, la API del Servicio de descubrimiento de clústeres (CDS), las API del Servicio de descubrimiento secreto (SDS) y del Servicio de descubrimiento de puntos finales (EDS), DNS y comprobaciones externas activas. Es responsable de crear una vista "eventualmente coherente" de cada clúster ascendente, que incluye los hosts descubiertos y el estado de salud.
  2. El verificador de estado realiza una verificación de estado activa e informa los cambios de estado al administrador del clúster.
  3. CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS se realizan para determinar la membresía del clúster. El cambio de estado se devuelve al administrador del clúster.
  4. Cada hilo de trabajo ejecuta continuamente un bucle de eventos.
  5. Cuando el administrador del clúster determina que el estado de un clúster ha cambiado, crea una nueva instantánea de solo lectura del estado del clúster y la envía a cada subproceso de trabajo.
  6. Durante el próximo período de silencio, el subproceso de trabajo actualizará la instantánea en la ranura TLS asignada.
  7. Durante un evento de E/S que se supone debe determinar el host a equilibrar la carga, el balanceador de carga solicitará una ranura TLS (almacenamiento local de subprocesos) para obtener información sobre el host. Esto no requiere cerraduras. Tenga en cuenta también que TLS también puede desencadenar eventos de actualización para que los balanceadores de carga y otros componentes puedan recalcular cachés, estructuras de datos, etc. Esto está más allá del alcance de esta publicación, pero se usa en varios lugares del código.

Utilizando el procedimiento anterior, Envoy puede procesar cada solicitud sin ningún bloqueo (excepto lo descrito anteriormente). Aparte de la complejidad del código TLS en sí, la mayor parte del código no necesita comprender cómo funciona el subproceso múltiple y se puede escribir en un solo subproceso. Esto hace que la mayor parte del código sea más fácil de escribir además de un rendimiento superior.

Otros subsistemas que hacen uso de TLS

TLS (almacenamiento local de subprocesos) y RCU (actualización de copia de lectura) se utilizan ampliamente en Envoy.

Ejemplos de uso:

  • Mecanismo para cambiar la funcionalidad durante la ejecución: La lista actual de funciones habilitadas se calcula en el hilo principal. Luego, a cada subproceso de trabajo se le proporciona una instantánea de solo lectura utilizando la semántica de RCU.
  • Reemplazo de tablas de ruta: Para las tablas de rutas proporcionadas por RDS (Servicio de descubrimiento de rutas), las tablas de rutas se crean en el hilo principal. Posteriormente, la instantánea de solo lectura se proporcionará a cada subproceso de trabajo mediante la semántica RCU (Read Copy Update). Esto hace que el cambio de tablas de rutas sea atómicamente eficiente.
  • Almacenamiento en caché de encabezados HTTP: Resulta que calcular el encabezado HTTP para cada solicitud (mientras se ejecuta ~25K+ RPS por núcleo) es bastante costoso. Envoy calcula de forma centralizada el encabezado aproximadamente cada medio segundo y se lo proporciona a cada trabajador a través de TLS y RCU.

Hay otros casos, pero los ejemplos anteriores deberían proporcionar una buena comprensión de para qué se utiliza TLS.

Errores de rendimiento conocidos

Si bien Envoy funciona bastante bien en general, hay algunas áreas notables que requieren atención cuando se utiliza con muy alta simultaneidad y rendimiento:

  • Como se describe en este artículo, actualmente todos los subprocesos de trabajo adquieren un bloqueo cuando escriben en el búfer de memoria de registro de acceso. Con alta simultaneidad y alto rendimiento, necesitará agrupar los registros de acceso para cada subproceso de trabajo a expensas de una entrega desordenada al escribir en el archivo final. Como alternativa, puede crear un registro de acceso independiente para cada subproceso de trabajo.
  • Aunque las estadísticas están altamente optimizadas, con una simultaneidad y un rendimiento muy altos probablemente habrá una contienda atómica en las estadísticas individuales. La solución a este problema son los contadores por subproceso de trabajo con reinicio periódico de los contadores centrales. Esto se discutirá en una publicación posterior.
  • La arquitectura actual no funcionará bien si Envoy se implementa en un escenario donde hay muy pocas conexiones que requieran importantes recursos de procesamiento. No hay garantía de que las conexiones se distribuyan uniformemente entre los subprocesos de trabajo. Esto se puede solucionar implementando el equilibrio de conexiones de trabajadores, que permitirá el intercambio de conexiones entre subprocesos de trabajadores.

Conclusión

El modelo de subprocesos de Envoy está diseñado para facilitar la programación y un paralelismo masivo a expensas de memoria y conexiones potencialmente desperdiciadoras si no se configura correctamente. Este modelo le permite funcionar muy bien con un número de subprocesos y un rendimiento muy elevados.
Como mencioné brevemente en Twitter, el diseño también puede ejecutarse sobre una pila de red completa en modo de usuario, como DPDK (Kit de desarrollo de plano de datos), lo que puede dar como resultado que servidores convencionales manejen millones de solicitudes por segundo con procesamiento L7 completo. Será muy interesante ver qué se construirá en los próximos años.
Un último comentario rápido: me han preguntado muchas veces por qué elegimos C++ para Envoy. La razón sigue siendo que sigue siendo el único lenguaje de grado industrial ampliamente utilizado en el que se puede construir la arquitectura descrita en esta publicación. Definitivamente, C++ no es adecuado para todos o incluso para muchos proyectos, pero para ciertos casos de uso sigue siendo la única herramienta para realizar el trabajo.

Enlaces al código

Enlaces a archivos con interfaces e implementaciones de encabezados discutidos en esta publicación:

Fuente: habr.com

Añadir un comentario