Envoy. 1. Introducción

¡Hola! Este es un breve artículo que responde a las preguntas: "¿Qué es envoy?", "¿Para qué se necesita?" y "¿Por dónde empezar?".

Qué es

Envoy es un equilibrador de carga L4-L7 escrito en C++, enfocado en alto rendimiento y disponibilidad. Por un lado, es un análogo de nginx y haproxy, comparable en rendimiento. Por otro lado, está más orientado a arquitecturas de microservicios y cuenta con funcionalidades no inferiores a las de equilibradores en Java y Go, como zuul o traefik.

Tabla comparativa de haproxy/nginx/envoy, que no pretende ser una verdad absoluta, pero ofrece una visión general.

nginx
haproxy
envoy
traefik

estrellas en github
11.2k/mirror
1.1k/mirror
12.4k
27.6k

escrito en
C
C
C++
go

API
no
socket solo/push
dataplane/pull
pull

verificación de salud activa
no
sí
sí
sí

Open tracing
plugin externo
no
sí
sí

JWT
plugin externo
no
sí
no

Extensión
Lua/C
Lua/C
Lua/C++
no

¿Por qué?

Es un proyecto joven, le falta mucho, muchas cosas están en una versión temprana alfa. Pero envoy, gracias a su juventud, está evolucionando rápidamente y ya tiene muchas funcionalidades interesantes: configuración dinámica, muchos filtros listos, una interfaz sencilla para escribir tus propios filtros.
De esto derivan las áreas de aplicación, pero primero, 2 anti-patrones:

  • Entrega de contenido estático.

El hecho es que en este momento envoy no hay soporte para cacheo. Los chicos de Google están intentando solucionarlo.. La idea es implementar una vez por todas en envoy todas las sutilezas (zoológico de encabezados) de conformidad con RFC, y para implementaciones concretas crear una interfaz. Pero por ahora, esto ni siquiera es alfa, la arquitectura está en discusión, PR está abierto (mientras escribía el artículo, se aceptó un PR, pero este punto sigue siendo relevante).

Y mientras tanto, utiliza nginx para contenido estático.

  • Configuración estática.

Se puede usar, pero envoy no fue creado para eso. Las capacidades en la configuración estática no se desplegarán. Hay muchos puntos:

Al editar la configuración en yaml, cometerás errores, maldecirás a los desarrolladores por su verbosidad y pensarás que las configuraciones de nginx/haproxy, aunque menos estructuradas, son más concisas. Esa es la esencia. La configuración de Nginx y Haproxy fue creada para ser editada a mano, mientras que la de envoy fue diseñada para generarse a partir de código. Toda la configuración está descrita en protobuf, generándola a partir de archivos proto es mucho más difícil cometer errores.

Los escenarios canary, el despliegue b/g y muchas otras cosas se implementan de manera adecuada solo en la configuración dinámica. No estoy diciendo que no se pueda hacer en estático, todos lo hacemos. Pero para eso, hay que lidiar con soluciones improvisadas, en cualquiera de los equilibradores, en envoy también.

Tareas en las que Envoy es indispensable:

  • Balanceo de tráfico en sistemas complejos y dinámicos. Esto incluye service mesh, pero no es exclusivamente eso.
  • La necesidad de funcionalidades de trazabilidad distribuida, autorización compleja u otros aspectos que están en envoy el paquete o que se implementan fácilmente, mientras que en nginx/haproxy hay que recurrir a lua y a plugins cuestionables.

Ambos son necesarios para asegurar un alto rendimiento cuando se requiere.

¿Cómo funciona?

Envoy se distribuye en binarios solo como imagen de docker. La imagen ya contiene un ejemplo de configuración estática. Pero nos interesa solo para entender la estructura.

envoy.yaml configuración estática

static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address:
        protocol: TCP
        address: 0.0.0.0
        port_value: 10000
    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match:
                  prefix: "\/"
                route:
                  host_rewrite: www.google.com
                  cluster: service_google
          http_filters:
          - name: envoy.router
  clusters:
  - name: service_google
    connect_timeout: 0.25s
    type: LOGICAL_DNS
    # Comentar la siguiente línea para probar en redes v6
    dns_lookup_family: V4_ONLY
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: service_google
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: www.google.com
                port_value: 443
    transport_socket:
      name: envoy.transport_sockets.tls
      typed_config:
        "@type": type.googleapis.com/envoy.api.v2.auth.UpstreamTlsContext
        sni: www.google.com

Configuración dinámica

¿Qué problema estamos tratando de resolver? No se puede simplemente reiniciar la configuración del balanceador bajo carga, surgirán "pequeños" problemas:

  • Validación de la configuración.

La configuración puede ser grande, puede ser muy grande, si la recargamos toda a la vez, aumentan las probabilidades de que haya un error en alguna parte.

  • Conexiones de larga duración.

Cuando se inicializa un nuevo listener, es necesario ocuparse de las conexiones que están trabajando en el anterior. Si los cambios ocurren con frecuencia y hay conexiones de larga duración, hay que buscar un compromiso. Hola, kubernetes ingress en nginx.

  • Health checks activos.

Si tenemos health checks activos, sería bueno verificarlos todos en la nueva configuración antes de enviar tráfico. Si hay muchos upstreams, esto requiere tiempo. Hola, haproxy.

¿Cómo se resuelve esto en envoy, cargando la configuración dinámicamente, a partir del grupo de modelos, se puede dividir en partes separadas y no reinicializar la parte que no cambió. Por ejemplo, un listener, que es costoso de reinicializar, pero cambia raramente.

Configuración envoy (del archivo anterior) tiene las siguientes entidades:

  • listener — un listener que está vinculado a una IP/puerto específicos
  • virtual host — un host virtual por nombre de dominio
  • route — regla de balanceo
  • cluster — grupo de upstreams con parámetros de balanceo
  • endpoint — dirección de la instancia del upstream

Cada una de estas entidades, además de algunas otras, se puede llenar dinámicamente, para ello en la configuración se indica la dirección del servicio desde el cual se obtendrá la configuración. El servicio puede ser REST o gRPC, preferiblemente se debe usar gRPC.

Los servicios se denominan respectivamente: LDS, VHDS, RDS, CDS y EDS. Se puede combinar configuración estática y dinámica, con la limitación de que un recurso dinámico no puede ser indicado en estático.

Para la mayoría de las tareas, es suficiente implementar los últimos tres servicios, que se llaman ADS (Aggregated Discovery Service), para java y go tienen una implementación lista de gRPC dataplane en la que solo es necesario llenar los objetos desde su fuente.

La configuración adquiere el siguiente formato:

envoy.yaml configuración dinámica

dynamic_resources:
  ads_config:
    api_type: GRPC
    grpc_services:
      envoy_grpc:
        cluster_name: xds_clr
  cds_config:
    ads: {}
static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address:
        protocol: TCP
        address: 0.0.0.0
        port_value: 10000
    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
          stat_prefix: ingress_http
          rds:
            route_config_name: local_route
            config_source:
              ads: {}
          http_filters:
          - name: envoy.router
  clusters:
  - name: xds_clr
    connect_timeout: 0.25s
    type: LOGICAL_DNS
    dns_lookup_family: V4_ONLY
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: xds_clr
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: xds
                port_value: 6565

Al iniciar envoy con esta configuración, se conectará al control-plane y tratará de solicitar la configuración de RDS, CDS y EDS. Cómo ocurre el proceso de interacción se describe aquí.

En resumen, envoy envía una solicitud, indicando el tipo de recurso solicitado, la versión y los parámetros del nodo. En respuesta recibe el recurso y la versión; si en el control-plane la versión no ha cambiado, no responde.
Hay 4 variantes de interacción:

  • Un stream de gRPC para todos los tipos de recursos, se envía el estado completo del recurso.
  • Streams separados, estado completo.
  • Un stream, estado incremental.
  • Streams separados, estado incremental.

xDS incremental permite reducir el tráfico entre el control y el plano envoy, esto es relevante para configuraciones grandes. Sin embargo, complica la interacción, ya que se envía una lista de recursos para suscribirse y cancelarse en la solicitud.

En nuestro ejemplo, se usa ADS: un stream para RDS, CDS, EDS y modo no incremental. Para habilitar el modo incremental, se debe especificar api_type: DELTA_GRPC

Dado que en la solicitud hay parámetros de nodo, podemos enviar diferentes recursos al control-plane para diferentes instancias envoy, lo que es conveniente para construir una malla de servicios.

Calentamiento

En envoy al iniciar o al recibir una nueva configuración del control-plane, se inicia el proceso de calentamiento de recursos. Está dividido en calentamiento de listener y calentamiento de clúster. El primero se inicia con cambios en RDS/LDS, el segundo en CDS/EDS. Esto significa que si solo cambian los upstreams, el listener no se recrea.

Durante el calentamiento, se esperan recursos dependientes del control-plane dentro del tiempo de espera. Si el tiempo de espera se agota, la inicialización no será exitosa, y el nuevo listener no comenzará a escuchar el puerto.
Orden de inicialización: EDS, CDS, verificación de salud activa, RDS, LDS. Con las verificaciones de salud activas habilitadas, el tráfico fluirá hacia el upstream solo después de una verificación de salud exitosa.

Si se recrea el listener, el antiguo pasa al estado DRAIN y será eliminado después de cerrar todas las conexiones o al expirar el tiempo de espera --drain-time-s, por defecto 10 minutos.

Continuará.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster