Приветствую! Это небольшая статья, отвечающая на вопросы: "что такое 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 образ. В образе уже есть пример статической конфигурации. Но нам он интересен только для понимания структуры.
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 в которой достаточно только заполнить объекты из своего источника.
Конфигурация приобретает следующий вид:
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 минут.
Продолжение следует.
Автор: Мирослав Шевалдин