Envoy. 1. Introducere

Bună! Acesta este un scurt articol care răspunde la întrebările: "ce este envoy?", "de ce este necesar?" și "de unde să încep?".

Ce este

Envoy este un echilibror de încărcare L4-L7 scris în C++, dedicat performanței ridicate și disponibilității. Pe de o parte, este oarecum similar cu nginx și haproxy, comparabil cu acestea în ceea ce privește performanța. Pe de altă parte, este mai orientat spre arhitectura microserviciilor și dispune de funcționalități cel puțin la fel de bune ca echilibratoarele scrise în java și go, precum zuul sau traefik.

Tabel comparativ haproxy/nginx/envoy, nu pretinde a fi o adevărată autoritate, dar oferă o imagine de ansamblu.

nginx
haproxy
envoy
traefik

stele pe github
11.2k/mirror
1.1k/mirror
12.4k
27.6k

scris în
C
C
C++
go

API
nu
socket only/push
dataplane/pull
pull

verificare activă a sănătății
nu
da
da
da

Open tracing
plugin extern
nu
da
da

JWT
plugin extern
nu
da
nu

Extensie
Lua/C
Lua/C
Lua/C++
nu

De ce

Este un proiect tineresc, îi lipsesc multe lucruri, unele sunt în stadiul de alfa timpurie. Dar envoy, datorită tineretii, se dezvoltă rapid și deja are multe capacități interesante: configurare dinamică, multe filtre gata făcute, o interfață simplă pentru scrierea propriilor filtre.
De aici rezultă domeniile de aplicație, dar pentru început, 2 antipattern-uri:

  • Servirea staticelor.

Ceea ce se întâmplă este că în prezent nu există suport pentru caching. envoy Cei de la google încearcă să facă acest lucru remediat. Ideea este de a implementa o dată în envoy toate subtilitățile (zoo de headere) conforme RFC, iar pentru implementările specifice, să creeze o interfață. Dar deocamdată, aceasta este chiar mai puțin decât alfa, arhitectura este în discuție, PR deschis (între timp, pe când scriam articolul, PR-ul a fost fuzionat, dar acest punct este încă relevant).

Între timp, folosiți nginx pentru statice.

  • Configurare statică.

Poate fi utilizată, dar envoy nu a fost creată pentru aceasta. Capacitățile în configurația statică nu vor fi dezvăluite. Sunt multe aspecte:

Editând configurația în yaml, veți face greșeli, veți înjura dezvoltatorii pentru verbose și veți crede că config-urile nginx/haproxy, deși mai puțin structurate, sunt mai concise. Asta este esența. Configurația Nginx și Haproxy a fost creată pentru editare manuală, în timp ce envoy a fost creată pentru generarea din cod. Toată configurația este descrisă în protobuf, generându-se prin fișiere proto, este mult mai greu să greșești.

Scenariile canary, de desplatare b/g și multe altele, sunt realizate bine doar în configurația dinamică. Nu spun că nu se poate face în statică, facem toate acestea. Dar pentru asta trebuie să te înfășori în soluții de tip workaround, în oricare dintre echilibratoarele disponibile, în envoy inclusiv.

Sarcinile în care Envoy este indispensabil:

  • Îmbinarea traficului în sisteme complexe și dinamice. Aici intră și service mesh, dar nu se limitează doar la acesta.
  • Necesitatea funcționalității de urmărire distribuită, autorizare complexă sau altceva care există în envoy modul standard sau este ușor de implementat, în timp ce în nginx/haproxy trebuie să ne bazăm pe lua și pluginuri discutabile.

Ambele sunt necesare pentru a asigura o performanță ridicată.

Cum funcționează

Envoy este distribuit în binare doar ca imagine docker. În imagine există deja un exemplu de configurație statică. Dar noi suntem interesați de asta doar pentru a înțelege structura.

envoy.yaml configurație statică

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
    # Comentarii asupra următoarei linii pentru a testa pe rețele 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

Configurație dinamică

Ce problemă căutăm să rezolvăm? Nu putem pur și simplu să reluăm configurația echilibrorului de sarcină sub sarcină, vor apărea "probleme mici":

  • Validarea configurației.

Configurația poate fi mare, poate fi foarte mare, iar dacă o reîncărcăm pe toată deodată, șansele de a avea o eroare undeva cresc.

  • Conexiuni de lungă durată.

La inițierea unui nou listener, trebuie să ne preocupăm de conexiunile care funcționează pe vechiul listener, dacă modificările apar frecvent și sunt conexiuni de lungă durată, va trebui să găsim un compromis. Salut, ingress kubernetes pe nginx.

  • Verificări active de sănătate.

Dacă avem verificări active de sănătate, ar trebui să le verificăm toate pe noua configurație înainte de a trimite traficul. Dacă sunt multe upstream-uri, aceasta necesită timp. Salut, haproxy.

Cum se rezolvă aceasta în envoy, încărcând dinamic configurația, pe baza modelului de pool, aceasta poate fi împărțită în părți separate și nu este necesară reinițializarea acelei părți care nu s-a schimbat. De exemplu, un listener, care are costuri de reinițializare mari, se schimbă rar.

Configurație envoy (din fișierul de mai sus) are următoarele entități:

  • listener — un listener care ascultă pe un anumit ip/port
  • virtual host — un host virtual pe baza numelui de domeniu
  • route — regulă de balansare
  • cluster — grup de upstream-uri cu parametrii de balansare
  • endpoint — adresa instanței upstream-ului

Fiecare dintre aceste entități, plus unele altele, pot fi completate dinamic; pentru aceasta, în configurație se specifică adresa serviciului de la care va fi obținută configurația. Serviciul poate fi REST sau gRPC, preferabil este să se utilizeze gRPC.

Serviciile se numesc corespunzător: LDS, VHDS, RDS, CDS și EDS. Se poate combina configurația statică și dinamică, cu restricția că resursa dinamică nu poate fi specificată în static.

Pentru majoritatea sarcinilor, este suficient să se implementeze ultimele trei servicii, acestea se numesc ADS (Aggregated Discovery Service), pentru java și go există o implementare pregătită gRPC dataplane în care este suficient să completați doar obiectele din sursa proprie.

Configurația devine următoarea:

envoy.yaml configurație dinamică

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

La pornire envoy cu această configurație, se va conecta la control-plane și va încerca să solicite configurația RDS, CDS și EDS. Cum se desfășoară procesul de interacțiune este descris aici.

Pe scurt, envoy trimite o cerere, indicând tipul de resursă solicitat, versiunea și parametrii nodului. În răspuns, primește resursa și versiunea; dacă versiunea pe control-plane nu s-a schimbat, nu răspunde.
Există 4 variante de interacțiune:

  • Un singur stream gRPC pentru toate tipurile de resurse, se trimite starea completă a resursei.
  • Fluxuri separate, stare completă.
  • Flux unic, stare incrementală.
  • Fluxuri separate, stare incrementală.

xDS incremental permite reducerea traficului între control-plane și envoy, acest lucru fiind relevant pentru configurări mari. Însă complica interacțiunea, transmitând în cerere o listă de resurse pentru dezabonare și abonare.

În exemplul nostru, utilizăm ADS — un flux pentru RDS, CDS, EDS și modul non-incremental. Pentru a activa modul incremental, trebuie specificat api_type: DELTA_GRPC

Deoarece cererea conține parametrii nodurilor, putem trimite resurse diferite pentru diferite instanțe la control-plane envoy, ceea ce este convenabil pentru construirea unei rețele de servicii.

Încălzire

Pe envoy la pornire sau atunci când se primește o nouă configurație de la control-plane, se pornește procesul de încălzire a resurselor. Acesta este împărțit în încălzirea listener-ului și încălzirea cluster-ului. Prima se activează la schimbări în RDS/LDS, a doua în CDS/EDS. Aceasta înseamnă că, dacă se schimbă doar upstream-urile, listener-ul nu este recreat.

În timpul procesului de încălzire, se așteaptă resursele dependente de la control-plane timp de un timeout. Dacă timeout-ul expiră, inițializarea nu va fi reușită, noul listener nu va începe să asculte portul.
Ordinea inițializării: EDS, CDS, health check activ, RDS, LDS. Cu health check-urile active activate, traficul va merge la upstream, doar după un health check reușit.

Dacă listener-ul a fost recreat, cel vechi trece în stare DRAIN și va fi eliminat după ce toate conexiunile sunt închise sau după expirarea timeout-ului --drain-time-s, implicit 10 minute.

Continuarea urmează.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster