Приветствую! Это небольшая статья, отвечающая на вопросы: "что такое envoy?", "зачем он нужен?" и "с чего начать?".


Что это


Envoy — это L4-L7 балансировщик написанный на С++, ориентированный на высокую производительность и доступность. С одной стороны, это в некотором роде аналог nginx и haproxy, соизмеримый с ними по производительности. С другой, он больше ориентирован под микросервисную архитектуру и обладает функционалом не хуже балансировщиков на java и go, таких как zuul или traefik.


Таблица сравнения haproxy/nginx/envoy, она не претендует на абсолютную истину, но дает общую картину.


nginx haproxy envoy traefik
звезд на github 11.2k/mirror 1.1k/mirror 12.4k 27.6k
написан на C C C++ go
API нет socket only/push dataplane/pull pull
active healthcheck нет да да да
Open tracing внешний плагин нет да да
JWT внешний плагин нет да нет
Расширение Lua/C Lua/C Lua/C++ нет

Зачем


Это молодой проект, в нем много чего нет, что-то в ранней альфе. Но envoy, в том числе за счет молодости, быстро развивается и уже сейчас имеет много интересных возможностей: динамическую конфигурацию, много готовых фильтров, простой интерфейс для написания своих фильтров.
Из этого вытекают области применения, но для начала 2 антипаттерна:


  • Отдача статики.

Дело в том, что на данный момент в envoy нет поддержки кэширования. Ребята из google пытаются это исправить. Идея единожды реализовать в на envoy всё тонкости(зоопарк хедеров) соответствия RFC, а для конкретных реализаций сделать интерфейс. Но пока это даже не альфа, архитектура в обсуждении, PR открыт (пока я писал статью PR вмержили, но этот пункт еще актуален).


А пока используйте для статики nginx.


  • Статическая конфигурация.

Можно ее использовать, но envoy был создан не для этого. Возможности в статической конфигурации не будут раскрыты. Моментов много:


Редактируя конфигурацию в yaml, Вы будете ошибаться, материть разработчиков за многословность и думать, что конфиги nginx/haproxy, пусть менее структурированы, но лаконичнее. В этом и суть. Конфигурация Nginx и Haproxy создавалась под редактирование руками, а у envoy под генерацию из кода. Вся конфигурация описана в protobuf, генерируя её по proto файлам ошибиться гораздо сложнее.


Сценарии canary, b/g деплоя и много другое, нормально реализуются только в динамической конфигурации. Я не говорю что это нельзя сделать в статике, мы все это делаем. Но для этого нужно обложиться костылями, в любом из балансеров, в envoy в том числе.


Задачи в которых Envoy незаменим:


  • Балансировка трафика в сложных и динамичных системах. Сюда попадает service mesh, но это не обязательно только он.
  • Необходимость функционала распределенной трассировки, сложной авторизации или другого, который есть в envoy из коробки или удобно реализовывается, а в nginx/haproxy нужно обложиться lua и сомнительными плагинами.

И то и другое при необходимости обеспечить высокую производительность.


Как это работает


Envoy распространяется в бинарниках только как docker образ. В образе уже есть пример статической конфигурации. Но нам он интересен только для понимания структуры.


envoy.yaml статическая конфигурация
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
    # Comment out the following line to test on v6 networks
    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

Динамическая конфигурация


Решение какой проблемы мы ищем? Нельзя просто так взять и перезагрузить конфигурацию балансировщика под нагрузкой, возникнут "небольшие" проблемы:


  • Валидация конфигурации.

Конфиг может быть большой, может быть очень большой, если мы перегружаем его весь разом, шансы что где-то ошибка возрастают.


  • Долгоживущие соединения.

При инициализации нового листенера, нужно позаботиться о соединениях работающих на старом, если изменения происходят часто и есть долгоживущие соединения, придется искать компромисс. Привет, kubernetes ingress на nginx.


  • Активные хелсчеки.

Если у нас есть активные хелсчеки, надо бы их все перепроверить на новом конфиге до того как послать трафик. Если апстримов много, это требует время. Привет, haproxy.


Как это решается в envoy, подгружая конфиг динамически, по пул модели, можно его поделить на отдельные части и не переинициализировать ту часть которая не менялась. Например листенер, который переинициализировать дорого, а меняется он редко.


Конфигурация envoy (из файла выше) имеет следующие сущности:


  • listener — листенер висящий на определенном ip/порту
  • virtual host — виртуальный хост по имени домена
  • route — правило балансировки
  • cluster — группа апстримов с параметрами балансировки
  • endpoint — адрес инстанса апстрима

Каждую из этих сущностей плюс некоторые другие можно заполнить динамически, для этого в конфигурации указывается адрес сервиса от куда будет получен конфиг. Сервис может быть REST либо gRPC, предпочтительнее использовать gRPC.


Сервисы называются соответственно: LDS, VHDS, RDS, CDS и EDS. Можно комбинировать статическую и динамическую конфигурацию, с ограничением, что динамический ресурс нельзя указать в статическом.


Для большинства задач достаточно реализовать последние три сервиса, они называются ADS (Aggregated Discovery Service), для java и go имеется готовая имплементация gRPC dataplane в которой достаточно только заполнить объекты из своего источника.


Конфигурация приобретает следующий вид:


envoy.yaml динамическая конфигурация
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

При запуске envoy с этим конфигом, он подключится к control-plane и попробует запросить конфигурацию RDS, CDS и EDS. Как происходит процесс взаимодействия описано здесь.


Если кратко, envoy шлет запрос, с указанием типа запрашиваемого ресурса, версией и параметрами ноды. В ответ получает ресурс и версию, если на control-plane версия не поменялась, он не отвечает.
Есть 4 варианта взаимодействия:


  • Один gRPC стрим на все типы ресурсов, присылается полное состояние ресурса.
  • Раздельные стримы, полное состояние.
  • Один стрим, инкрементальное состояние.
  • Раздельные стримы, инкрементальное состояние.

Incremental xDS позволяет уменьшить трафик между control-plane и envoy, это актуально для больших конфигураций. Но усложняет взаимодействие, в запросе передается список ресурсов для отписки и подписки.


В нашем примере используется ADS — один стрим для RDS, CDS, EDS и не инкрементальный режим. Для включения инкрементального режима, нужно указать api_type: DELTA_GRPC


Так как в запросе есть параметры ноды, мы можем на control-plane присылать разные ресурсы для разных инстансов envoy, это удобно для построения service mesh.


Warmup


На envoy при старте или при получении новой конфигурации от control-plane запускается процесс warmup ресурсов. Он разделен, на listener warmup и cluster warmup. Первый запускается при изменениях в RDS/LDS, второй при CDS/EDS. Это значит, что если меняются только апстримы, листенер не пересоздается.


В процессе прогрева, ожидаются зависимые ресурсы от control-plane в течении таймаута. Если таймаут вышел, инициализация не будет успешной, новый листенер не начнет слушать порт.
Порядок инициализации: EDS, CDS, active health check, RDS, LDS. При включенных активных хелсчеках, трафик пойдет на апстрим, только после одного успешного хелсчека.


Если пересоздавался листенер, старый переходит в состояние DRAIN, и будет удален после закрытия всех соединений или истечении таймаута --drain-time-s, по умолчанию 10 минут.


Продолжение следует.