Как мы в Dropbox перешли с Nginx на Envoy

в 19:52, , рубрики: bazel, devops, DropBox, envoy, golang, nginx, Блог компании Southbridge, Серверное администрирование, системное администрирование

В этой статье мы будем говорить о нашей старой инфраструктуре, основанной на Nginx, ее болячках, а также выгоде, которую мы получили после миграции на Envoy. Мы сравним Nginx и Envoy различными способами. Также кратко коснемся процесса миграции, текущего состояния, а также проблем, возникших при переходе.

Как мы в Dropbox перешли с Nginx на Envoy - 1

Когда мы переключили большую часть трафика на Envoy — у нас получилось бесшовно мигрировать систему, обрабатывающую десятки миллионов открытых соединений, миллионы запросов в секунду и терабиты пропускной способности. По факту мы стали одним из крупнейших пользователей Envoy в мире.

Отказ от ответственности: мы пытаемся оставаться объективными, достаточно много сравнений относятся только к Dropbox и нашим принципам разработки ПО: мы ставим на Bazel, gRPC, С++ и Golang.

Просим обратить внимание, что мы рассмотрим версию Nginx с открытым исходным кодом, а не коммерческую с дополнительными функциями.

Наша старая инфраструктура, основанная на Nginx

Наши настройки Nginx были статичными, обновлялись с помощью комбинации Python2, Jinja и YAML. Любое изменение требовало полной раскатки с нуля. Все динамические части, к примеру управление upstream и экспорт статистики, были написаны на Lua. Любая достаточно сложная логика была перемещена на следующий уровень проксирования, написанный на Go. Наша статья также имеет раздел, посвященный нашей старой инфраструктуре на Nginx.

Nginx служил нам верой и правдой уже порядка десяти лет. Но он перестал устраивать наши лучшие практики по разработке:

  • Наши внутренние и (закрытые) внешние API постепенно переходили с REST на gRPC, который требует кучу различных перекодировок от прокси.
  • Protocol buffers стали стандартом de facto для определения сервисов и настроек
  • Все программное обеспечение, независимо от языка программирования, собирается и тестируется с помощью Bazel.
  • Большая вовлеченность наших инженеров ключевых инфраструктурных проектов в качестве участников сообществ ПО с открытым исходным кодом.

Также Nginx был достаточно сложным в плане обслуживания:

  • Сборка конфигурационных файлов была слишком гибкой и была разбита между YAML, Jinja2 и Python.
  • Мониторинг был смесью Lua, анализа журналов, и мониторингом системного уровня
  • Повышенная зависимость от сторонних модулей влияла на стабильность, производительность и стоимость последующих обновлений.
  • Раскатка и управление процессом сильно отличался от остальных сервисов, поскольку он достаточно сильно зависел от настройки других систем: syslog, logrotate и прочих, в отличие от того, чтобы быть независимым от основной системы.

При этом мы в первый раз начали искать потенциальную замену Nginx.

Почему на Bandaid?

Мы часто говорим, что внутри инфраструктуры мы значительно полагаемся на Bandaid, прокси-сервер, написанный на Go. Он прекрасно внедрен в инфраструктуру Dropbox, поскольку основан на обширнейней экосистеме внутренних библиотек Go: мониторинг, обнаружение сервисов, ограничение скорости и т.п. Мы рассматривали подобный переход с Nginx, однако есть несколько проблем, мешающих нам это сделать:

  • ПО на Golang требуется больше ресурсов, чем ПО на C++. Низкое потребление ресурсов особенно важно для нашего Edge, поскольку мы не можем легко "автоматически масштабировать" там наши сервисы.
    • дополнительное потребление процессорного времени в основном связано с сборщиком мусора (GC), парсером HTTP и TLS, причем последнее менее оптимизировано, чем BoringSSL, используемый NginxEnvoy.
    • Модель "goroutine-per-request" и издержки GC значительно увеличивают требования к оперативной памяти в сервисах с большим числом соединений, как у нас.
  • Нет поддержки FIPS для Golang TLS
  • У Bandaid нет поддержки сообществом вне Dropbox, что означает, что мы сможем полагаться только на себя при разработке.

С учетом вышенаписанного мы решили начать перенос нашей транспортной инфраструктуры на Envoy.

Наша новая инфраструктура, основанная на Envoy

Давайте посмотрим на основные характерные черты разработки и сопровождения подробнее, чтобы увидеть, почему мы думаем, что Envoy лучший выбор для нас, а также что мы получим при переходе с Nginx на Envoy.

Производительность

Архитектура Nginx является событийной и многопроцессной. Она поддерживает SO_REUSEPORT, EPOLLEXCLUSIVE, а также привязку обработчиков к процессорным ядрам. Однако несмотря на то, что она событийная, она не полностью неблокирующая, что означает, что некоторые операции, например открытие файла или журналирование, потенциально может вызвать приостановку обслуживания (даже с aio, aio_write, включенными thread pools). Это приведет к увеличению задержек, которые могут достигать несколько секунд на дисках с шпинделями.

у Envoy схожая событийная архитектура, но вместо процессов используются потоки. Также есть поддержка SO_REUSEPORT (с поддержкой фильтрации BPF) и зависимость от libevent для обработки цикла событий (другими словами — нету крутых фишек epoll(2), к примеру EPOLLEXCLUSIVE). В цикле событий Envoy нет каких-либо блокирующих операций ввода-вывода. Даже журналирование сделано неблокирующим, так что оно не может вызвать подвисание.

Похоже, что теоретически по производительности Nginx и Envoy будут близки. Надеятся не в наших правилах, поэтому в первую очередь мы выполнили набор различных тестов на аналогично настроенных Nginx и Envoy. Если вы интересуетесь настройкой производительности, мы описали наши типовые руководства по настройке здесь, начиная с подбора оборудования и заканчивая оптимизацией операционной системы, выбора библиотек и настроек вебсервера.

Тесты показали схожую производительность на большинстве тестовых нагрузок: высокий RPS, высокая пропускная способность, и смешанное проксирование gRPC с низкими задержками и высокой пропускной способностью. Достаточно сложно сделать хороший тест производительности. у Nginx есть руководства, но они беспорядочные. У Envoy также есть руководство по нагрузочному тестированию, а также инструменты в проекте envoy-perf, но они, к сожалению, выглядят неподдерживаемыми. Мы стали использовать наш внутренний инструмент по имени "hulk", потому что он "ломать" наши сервисы.

Тем не менее были и заметные различия в результатах:

  • Nginx показал бОльшие задержки на долгоживущих соединениях. По большей части это связано с подвисанием цикла обработки событий при интенсивном вводе-выводе, особенно было заметно при работе с SO_REUSEPORT, поскольку в этом случае соединения могут приниматься от заблокированного в данный момент обработчика.
  • Производительность Nginx без сборщика статистики схожа с таковой для Envoy, активация сборщика на Lua замедлила Nginx в тесте с высоким RPS в три раза. Предсказуемо, с учетом зависимости от lua_shared_dict, который синхронизируется между обработчиками с помощью mutex. Мы понимаем, насколько неэффективным был наш сбор статистики. Мы попробовали сделать что-то подобное на counter(9) из FreeBSD, но только в пространстве пользователя: привязка к процессорному ядру, а также счетчики без блокировки на каждый обработчик вместе с функцией сбора данных, которая проходит по всем обработчикам и возвращает объединенную статистику. Но мы отказались от этой затеи, поскольку если бы мы завязались на внутренности Nginx (включая обработку ошибок, например), то нам пришлось бы поддерживать огроменнный патч, превращающий последующие обновления в настоящий ад.

У Envoy не было ни одной из этих проблем, а после перехода на него мы бы смогли освободить до 60% серверов, ранее занятых только под Nginx.

Наблюдаемость

Наблюдаемость одна из наиболее основных нужд при сопровождении любого продукта, а особенно лдя такой основной части инфраструктуры, как прокси. Это еще более важно в период перехода, чтобы любая проблема была обнаружена системой мониторинга, а не сообщена разочарованными пользователями.

Некоммерческий Nginx имеет модуль "stub status", в котором есть семь характеристик:

Active connections: 291
server accepts handled requests
 16630948 16630948 31070465
Reading: 6 Writing: 179 Waiting: 106

Этого однозначно не достаточно для нас, так что мы добавили простой обработчик log_by_lua, который добавляет статистику по каждому запросу, основанному на заголовках ипеременных, доступных в Lua: код статуса, размеры, попадание в кэш и т.п. Здесь пример простейшей функции, которая выдает характеристики:

function _M.cache_hit_stats(stat)
    if _var.upstream_cache_status then
        if _var.upstream_cache_status == "HIT" then
            stat:add("upstream_cache_hit")
        else
            stat:add("upstream_cache_miss")
        end
    end
end

В довесок к характеристикам, собираемым с каждого запроса, мы добавили очень хрупкий анализатор error.log, который отвечает классификацию ошибок upstream, http, Lua и TLS.

Поверх всего у нас работал отдельный обработчик для сбора внутреннего состояния Nginx: время с момента последней перезагрузки, число обработчиков, размеры RSSVMS, сроки жизни сертификатов TLS и проч.

Типовая установка Envoy предоставляет нам тысячи отдельных метрик (в формате Prometheus), описывая как проксируемый трафик, так и внутреннее состояние сервера:

$ curl -s http://localhost:3990/stats/prometheus | wc -l
14819

Тут включено множество статистики с различной агрегацией:

  • Статистика по кластерупо upstreamпо виртуальному хосту, включая информацию о пуле соединений и различные временные ряды.
  • Статистика для каждого слушающего сокета: TCPHTTPTLS
  • Различная внутренняя статистика, начиная от версии и времени работы, и заканчивая статистикой выделения памяти и устаревшей функцией счетчиков использования.

Особый привет интерфейсу администратора Envoy. Он не только предоставляет отдельную статистику через /certs, /clusters и config_dump, но также имеет важнейшие для сопровождения особенности:

  • Способность менять на лету уровень журналирования через /logging, что позволило нам решать достаточно сложные проблемы за считанные минуты.
  • Точки входа /cpuprofiler, /heapprofiler, /contention, которые безусловно были весьма нужными при устранении проблем с производительностью
  • /runtime_modify, с которой можно изменять параметры конфигурации без применения новой конфигурации, что можно использовать при подборе функций и т.п.

В дополнение к статистике Envoy также поддерживает подключаемые механизмы отладки. Это используется не только нашей командой, ответственной за трафик и работающей с несколькими уровнями балансировки нагрузки, но и разработчиками приложений, желающими проверить сквозные задержки между Edge и серверами приложений. Технически у Nginx есть поддержка отладки через стороннюю программу OpenTracing, однако она вяло развивается.

Вишенкой на торте у Envoy идет способность отправлять журналы по gRPC, что позволило убрать мосты syslog-to-hive с команды по трафику. Кроме прочего на боевых серверах гораздо удобнее (и безопаснее!) добавить еще один общий сервис gRPC, чем добавлять отдельный слушающий сокет TCPUDP.

Настройка журналирования доступа в Envoy, как и прочие вещи, производится через сервис управления gRPС: Access Log Service (ALS). Сервисы управления являются стандартным способом интеграции Envoy data plane с различными сервисами, что подводит нас к следующей тема.

Интеграция

Способность Nginx к интеграции проще всего описать как "юниксовая". Конфигурация очень статичная. Есть сильная зависимость от файлов (собственно конфигурационный файл, сертификаты, белыечерные списки и т.п.) и широко известных производственных стандартов (журналирование в syslog, авторизация подзапросов через HTTP). Простота и обратная совместимость хорошая штука для небольших установок, потому что Nginx может быть достаточно просто автоматизирован парой shell-скриптов. Но по мере роста масштаба системы тестируемость и стандартизация становятся более важными.

Envoy гораздо более продвинут в том, как data plane трафика должен быть интегрирован с control plane, а, следовательно, и с остальной инфраструктурой. У него есть поддержка protobuf и gRPC, предоставляемая через стабильный API, называемый xDS. Envoy производит обнаружение своих динамических ресурсов запрашивая свои (одну или несколько) службы xDS. В настоящее время эти интерфейсы развиваются за пределы Envoy, перед UDPA (universal data plane interface) разработчики ставит амбициозную цель: стать стандартом "de facto" в мире балансировщиков L4L7. Из нашего опыта — это работает. Мы уже используем ORCA (Open Request Cost Agregation) для внутреннего тестирования нагрузки и рассматриваем возможности UDPA для наших балансировщиков, не связанных с Envoy, например на основе Katran, балансировщика eBPFXDP L4.

Это весьма хорошо для Dropbox, поскольку все сервисы взаимодействуют между собой через API на основе gRPC. Мы внедрили собственную xDS control plane, которая объединяет Envoy с нашей системой управления конфигурацией, обнаружением сервисов, управлением секретами и информацией о маршрутах. Если хотите знать больше о Dropbox RPC — можете почитать здесь, мы подробно описали интеграцию обнаружения сервисов, управление секретами, статистику, отладку и прочие вещи с gRPC.

Опишем несколько доступных сервисов xDS, их альтернативы для Nginx, а также примеры того, как мы их используем:

  • Access Log Service (ALS), как уже писали выше, позволяет нам динамически настраивать цели для журналов доступа, кодировки и форматы. Попробуйте представить себе динамическую версию log_format и access_log для Nginx
  • Endpoint discovery service (EDS), предоставляет информацию о членах кластера. Это аналог динамически обновляемого списка блоков upstream для секций server (например для Lua это будет balancer_by_lua_block) в конфигурационном файле Nginx. В нашем случае мы проксируем это на наш внутренний сервис обнаружения.
  • Secret discovery service (SDS), предоставляет различную информацию о TLS, которая в Nginx работает по директивам ssl_* (ну и ssl_*_by_lua_block соответственно). Мы взяли его вкачестве нашего сервиса распостранения секретов.
  • Runtime discovery service (RTDS), предоставляет runtime флаги. Наша реализация этой функции на Nginx была довольно топорной, основанной на проверке существования различных файлов в Lua. С таким подходом сервера быстро стали несовместимыми по настройкам. В Envoy это также реализовано на файлах по-умолчанию, однако мы вместо этого подключили сервис с RTDS API к нашему распределенному хранилищу конфигурации, так что мы можем управлять целыми кластерами (инструментом, с интерфейсом, похожим на sysctl), а случайные несоотвествия между различными серверами исключены.
  • Route discovery service (RDS): выполняет сопоставление маршрутов с виртуальными хостами и позволяет выполнять дополнительную настройку заголовков и фильтров. В терминах Nginx это аналог динамического блока location c set_header```proxy_set_headerproxy_pass```. На нижних уровнях проксирования мы автоматически создаем их из наших конфигураионных файлов для определения сервисов.

В качестве примера интеграции Envoy в существующей боевой системе обычно приводят этот. Есть также и другие реализации control plane для Envoy, например Istio и менее сложная go-control-plane. Наша собственная платформа управления Envoy реализует все больше интерфейсов API xDS. Она развернута как обычный gRPC сервис и выступает в качестве промежуточного звена для наших инфраструктурных строительных блоков. Все это делается с помощью общих библиотек Golang для общения с внутренними сервисами и их предоставления для Envoy через интерфейсы API xDS. Сам процесс не включает вызовы файловой системы, подачу сигналов, обработку cronlogrotatesyslogпарсеров и т.п.

Настройка

У Nginx есть неоспоримое преимущество в виде простой и удобочитаемой конфигурации. Но это все становится неважным, как только конфигурационный файл становится сложным и начинает создаваться программными средствами. Как мы уже писали — наша конфигурация создается с помощью Python2, Jinja2 и YAML. Некоторые из вас возможно видели, или даже писали такое для erb, pug, Text::Template или даже m4 (А то! прим. переводчика):

{% for server in servers %}
server {
    {% for error_page in server.error_pages %}
    error_page {{ error_page.statuses|join(' ') }} {{ error_page.file }};
    {% endfor %}
    ...
    {% for route in service.routes %}
    {% if route.regex or route.prefix or route.exact_path %}
    location {% if route.regex %}~ {{route.regex}}{%
            elif route.exact_path %}= {{ route.exact_path }}{%
            else %}{{ route.prefix }}{% endif %} {
        {% if route.brotli_level %}
        brotli on;
        brotli_comp_level {{ route.brotli_level }};
        {% endif %}
        ...

С таким подходом создания конфигурации Nginx была огромная проблема: все используемые инструменты позволяли подстановку иили логику. У YAML есть anchors, у Jinja2 — циклы, условия и макросы, ну а Python вообще обладает полнотой по Тьюрингу. Без чистой модели данных сложность быстро расползлась по всем трем инструментам. Это можно было бы решить, но дополнительно было еще:

  • Нет декларативного описания формата настроек. Если бы мы хотели программно создавать и проверять конфигурацию — нам пришлось бы его изобрести.
  • Синтаксически правильная конфигурация может все еще быть недействительной с точки зрения кода на C. Например, некоторые связанные с буфером переменные имеют ограничения по значениям, ограничения по выравниванию и взаимозависимости с другими переменными. Для проверки конфигурации нам нужно было бы запустить ее через nginx -t.

Envoy наоборот имеет унифицированную модель данных для настройки: всё определяется через Protocol Buffers. Это не только решает вопрос с моделированием, но также добавляет информацию о типе к значениям конфигурации. С учетом этого, а также того, что мы уже давно работаем с protobuf в других сервисах, а также общего способа описаниянастройки сервисов — значительно упрощается интеграция.

Наш новый генератор конфигурации для Envoy основан на protobuf и Python3. Моделирование выполняется в файлах proto, а вся логика реализована на Python. Например:

from dropbox.proto.envoy.extensions.filters.http.gzip.v3.gzip_pb2 import Gzip
from dropbox.proto.envoy.extensions.filters.http.compressor.v3.compressor_pb2 import Compressor

def default_gzip_config(
    compression_level: Gzip.CompressionLevel.Enum = Gzip.CompressionLevel.DEFAULT,
    ) -> Gzip:
        return Gzip(
            # Envoy's default is 6 (Z_DEFAULT_COMPRESSION).
            compression_level=compression_level,
            # Envoy's default is 4k (12 bits). Nginx uses 32k (MAX_WBITS, 15 bits).
            window_bits=UInt32Value(value=12),
            # Envoy's default is 5. Nginx uses 8 (MAX_MEM_LEVEL - 1).
            memory_level=UInt32Value(value=5),
            compressor=Compressor(
                content_length=UInt32Value(value=1024),
                remove_accept_encoding_header=True,
                content_type=default_compressible_mime_types(),
            ),
        )

Обратите внимание на аннотации типов Python3 в этом кусочке кода! В сочетании с расширением mypy-protobuf обеспечивается сквозная типизация внутри генератора. Любая совместимая IDE сразу же выявит проблемы при несоотвествии. Все еще есть варианты, когда проверяемый тип в protobuf может быть неверным. В примере выше window_bits для Gzip может принимать значения от 9 до 15. Этот тип ограничений легко может быть задан с помощью расширения protoc-gen-validate:

google.protobuf.UInt32Value window_bits = 9 [(validate.rules).uint32 = {lte: 15 gte: 9}];

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

// Value from 1 to 9 that controls the amount of internal memory used by zlib. Higher values.
// use more memory, but are faster and produce better compression results. The default value is 5.
google.protobuf.UInt32Value memory_level = 1 [(validate.rules).uint32 = {lte: 9 gte: 1}];

Тем, кто думает об использовании protobuf на боевых серверах, но беспокоится, что может не хватить представления без схемы, стоит ознакомиться с статьей от Harvey Tuch, ключевого разработчика Envoy.

Расширяемость

Расширение Nginx куда-либо дальше, чем разрешают настройки, обычно возможно путем написания модуля на C. Руководство разработчика предоставляет наиболее полное введение для доступных строительных блоков. Там же сказано, что это относительно тяжелый способ. На практике потребуется наличие серьезного сеньора для безопасного написания модуля Nginx. С точки зрения инфраструктуры, доступной для разработчиков модулей, есть наличие базовых контейнеров: hash-таблицы, очереди, красно-черные деревья, управление памятью (не RAII), а также перехват всех этапов обработки запроса HTTP. Есть и внешние библиотеки, pcre, zlib, openssl и конечно же libc.

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

Наиболее часто используемый способ расширения от сообщества основан на стороннем lua-nginx-module и различных библиотеках от OpenResty. Так можно подключиться на любом этапе обработки запроса. Мы использовали log_by_lua для сбора статистики и balancer_by_luaдля динамической перенастройки backend.

В теории Nginx предоставляет способность разработки модулей на C++, на практике же отсутствуют нужные интерфейсы и обертки над всеми примитивами, чтобы сделать это стоящим делом. Но тем не менее сообщество пытается что-то делать. Конечно же, они все еще не дошли до внедрения на боевых системах.

Основной механизм расширения для Envoy — написание расширений на C++. Процесс не так хорошо описан как в случае с Nginx, однако тут все проще. Это частично из-за того, что:

  • Чистие и хорошо прокомментированные интерфейсы. Классы — точки расширения и документирования.
  • Стандартная библиотека и язык C++14. Начиная от базовых языковых функций, например шаблонов и лямбда-функций, заканчивая типобезопасными контейнерами и алгоритмами. В целом писать на C++14 так же просто как на Golang или с небольшой натяжкой — Python. (провокационно! прим. переводчика)
  • Расширения C++14 и его стандартной библиотеки. Предоставляются библиотекой abseil, в которой собраны из более новых стандартов C++, например mutex с встроенным обнаружением взаимоблокировки, поддержка отладки, дополнительныеболее эффективные контейнеры, и многое другое.

Мы смогли объединить Envoy вместе с Vortex2 (наш framework для мониторинга) написав всего 200 строчек кода для реализации интерфейса stats.

Envoy также поддерживает Lua через moonjit, форк LuaJIT c улучшенной поддержкой Lua 5.2. Однако по сравнению с сторонней интеграцией Lua в Nginx у нее гораздо меньше возможностей и преимуществ, что делает Lua в Envoy гораздо менее привлекательным из-за дополнительных сложностей в разработке, тестировании и отладке интерпретируемого кода. Компании, специализирующиеся на Lua, могут не согласиться, но в нашем случае проще было избежать Lua и использовать исключительно C++ для написания расширений для Envoy.

чем Envoy отличается от других вебсерверов, так это появившейся поддержкой WebAssembly (WASM) — быстрого, переносимого и безопасного механизма для расширений. WASM предназначен не для непосредственного использования. а в качестве цели компиляции любого языка программирования общего назначения. Envoy реализует спецификацию WebAssembly for Proxies (включая эталонные SDK для C++ и Rust), которая описывает границы между кодом WASM и универсальным прокси L4L7. Такой способ разделения между прокси и кодом расширения обеспечивает безопасную изолированную среду, а низкоуровневый компактный бинарный формат WASM обеспечивает производительность практически близкую к оборудованию. Кроме того, расширения proxy-wasm уже интегрированы в xDS, что позволяет динамические обновления и даже потенциально AB тестирование. В презентации с Kubecon'19 (вы таки помните, что были не виртуальные конференции?) есть хороший обзор WASM в Envoy и его потенциальных применениях. Также на ней было сказано об производительности порядка 60-70% от кода на C++.

Вместе с WASM поставщики услуг получают безопасный и эффективный способ выполнения кода клиентов на своей стороне. Клиенты получают переносимость, поскольку их расширения смогут работать в любом облаке, реализующем proxy-wasm ABI. Кроме прочего он позволяет использовать вашим пользователям любой язык, компилирующийся в WebAssembly. Это позволяет им безопасно и эффективно использовать большой набор библиотек, не связанных с C++.

Разработчики Istio также вкладывают кучу ресурсов в разработку WebAssembly, у них уже есть экспериментальная версия расширения телеметрии и сообщество WebAssemblyHub для обмена расширениями. Можно почитать об этом подробнее здесь.

Мы в Dropbox в настоящее время не используем WebAssembly, но все может поменяться, когда станедо доступен proxy-wasm SDK для Go.

Сборка и тестирование

Для сборки Nginx применяется особая система конфигурации на основе shell-скриптов, а также система сборки на основе make. Просто и утонченно, но заняло слишком много времени для интеграции его в монорепо Bazel для получения преимуществ в виде инкрементных, распределенных, герметичных и воспроизводимых сборок. Google открыл исходный код своей версии Nginx для Bazel, которая состоит из Nginx, BoringSSL, PCRE, ZLIB и Brotli.

Что касается тестирования, то у Nginx есть набор интеграционных тестов в отдельном репозитории и нет unit-тестов.

С учетом интенсивного использования Lua и отсутствия встроенной инфраструктуры модульного тестирования мы перешли к тестированию на основе макетных настроек и простого тестового драйвера на Python:

class ProtocolCountersTest(NginxTestCase):
    @classmethod
    def setUpClass(cls):
        super(ProtocolCountersTest, cls).setUpClass()
        cls.nginx_a = cls.add_nginx(
            nginx_CONFIG_PATH, endpoint=["in"], upstream=["out"],
        )
        cls.start_nginxes()

    @assert_delta(lambda d: d == 0, get_stat("request_protocol_http2"))
    @assert_delta(lambda d: d == 1, get_stat("request_protocol_http1"))
    def test_http(self):
        r = requests.get(self.nginx_a.endpoint["in"].url("/"))
        assert r.status_code == requests.codes.ok

Также мы проверяем синтаксическую правильность всех созданных конфигурационных файлов с их предварительной обработкой (например меняем ip-адреса на 127.0.0.1/8, переключаем на самоподписанные сертификаты ну и т.п.) запуская nginx -c.

Если смотреть на Envoy, то его система сборки уже Bazel, так что интеграция в наш монорепо была простейшей: Bazel позволяет легко добавлять внешние зависимости. Мы также использовали скрипты copybara для синхронизации protobuf как для Envoy, так и для UDPA. Это удобо, если нужно сделать простые преобразования без необходимости поддержки большого набора патчей.

Для Envoy есть возможность использовать либо unit-тесты (на основе gtestgmock) с предварительно написанными макетами, либо интегрированный фреймворк для тестирования, либо оба варианта сразу. Больше не нужно полагаться на медленные сквозные интеграционные тесты, запускаемые на каждое мелкое изменение.

При разработке Envoy с открытым исходным кодом требуется 100% покрытие unit-тестами. Тесты запускаются автоматически через конвейер CI в Azure при каждом запросе на слияние.

Кроме этого обычной практикой является обработка чувствительного к скорости работы кода с помощью googlebenchmark:

$ bazel run --compilation_mode=opt test/common/upstream:load_balancer_benchmark -- --benchmark_filter=".*LeastRequestLoadBalancerChooseHost.*"
BM_LeastRequestLoadBalancerChooseHost/100/1/1000000          848 ms          449 ms            2 mean_hits=10k relative_stddev_hits=0.0102051 stddev_hits=102.051
...

После перехода на Envoy мы стали полагаться исключительно на unit-тесты при разработке наших внутренних модулей:

TEST_F(CourierClientIdFilterTest, IdentityParsing) {
  struct TestCase {
    std::vector<std::string> uris;
    Identity expected;
  };
  std::vector<TestCase> tests = {
    {{"spiffe://prod.dropbox.com/service/foo"}, {"spiffe://prod.dropbox.com/service/foo", "foo"}},
    {{"spiffe://prod.dropbox.com/user/boo"}, {"spiffe://prod.dropbox.com/user/boo", "user.boo"}},
    {{"spiffe://prod.dropbox.com/host/strange"}, {"spiffe://prod.dropbox.com/host/strange", "host.strange"}},
    {{"spiffe://corp.dropbox.com/user/bad-prefix"}, {"", ""}},
  };
  for (auto& test : tests) {
    EXPECT_CALL(*ssl_, uriSanPeerCertificate()).WillOnce(testing::Return(test.uris));
    EXPECT_EQ(GetIdentity(ssl_), test.expected);
  }
}

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

Bazel — одна из лучших вещей, которые когда-либо случались с нашими разработчиками. У него очень крутая кривая обучения и большие первоначальные вложения, но у него также очень высокая отдача: инкрементные сборки, удаленное кэширование, распределенные сборки / тесты и т.д.

Одним из менее обсуждаемых преимуществ Bazel является то, что он дает нам возможность запрашивать и даже расширять граф зависимостей. Программный интерфейс к графу зависимостей в сочетании с общей системой сборки для всех языков является очень мощной функцией. Его можно использовать в качестве основного строительного блока линтеров, генерации кода, отслеживания уязвимостей, системы развертывания и т.д.

Безопасность

Кодовая база Nginx весьма небольшая, с минимальными внешними зависимостями. Обычно можно увидеть только три внешних зависимости полученного бинарного файла: zlib (или более быстрый вариант), какая-либо библиотека TLS и PCRE. В Nginx реализованы все парсеры протоколов, библиотеки для работы с событиями. а также разработчики дошли до того, что повторно написали некоторые функции из libc.

Некоторое время Nginx считался настолько безопасным, что использовался в качестве вебсервера по умолчанию в OpenBSD. Однако после конфликта двух сообществ разработчики OpenBSD начали разработку httpd. Более подробно можно почитать в докладе с BSDCon.

Минимализм окупился на практике, у Nginx было всего 30 известных уязвимостей за последниее 11 лет.

У Envoy кода гораздо больше, особенно если учитывать, что код C++ более плотный, чем код на C, используемый в Nginx. Также сюда включены миллионы строк из внешних зависимостей. Все, начиная от уведомлений от событий, до парсеров протоколов выделяется в сторонние библиотеки. Это увеличивает плоскость атаки и раздувает результирующий бинарный файл.

Для противостояния Envoy в значительной степени опирается на современные методы обеспечения безопасности. Для этого используются AddressSanitizer, ThreadSanitizer и MemorySanitizer. Также разработчики пошли дальше и стали использовать fuzzing.

Любой проект с открытым исходным кодом, который имеет решающее значение для глобальной инфраструктуры IT, может быть принят в OSS-Fuzz, бесплатную платформу для автоматического fuzzing. Узнать больше можно здесь.

На практике, однако, все эти меры предосторожности не в полной мере успевают за ростом кодовой базы. Так за последние два года было найдено 22 уязвимости.

Для Envoy подробно описана политика безопасности для выпусков, также есть описание и для отдельных уязвимостей. Envoy является участником Google's Vulnerability Reward Program (VRP). Google по этой программе, открытой для всех исследователей, предоставляет вознаграждения за обнаруженные уязвимости и сообщает о них в соотвествии с их правилами.

Примером того, как некоторые уязвимости потенциально могут быть использованы, служит CVE-2019–18801

Для противодействия рискам уязвимостей мы применяем лучшие методы защиты от наших поставщиков операционных систем Ubuntu и Debian, а именно специальный hardened профиль для всех наших бинарных файлов, работающих на Edge. Он включает в себя ASLR, защиту стека и таблицы символов:

build:hardened --force_pic
build:hardened --copt=-fstack-clash-protection
build:hardened --copt=-fstack-protector-strong
build:hardened --linkopt=-Wl,-z,relro,-z,now

Вебсервера с использованием fork, такие как Nginx, в большинстве окружений имеют проблемы с защитой стека, поскольку основной и рабочие процессы разделяют одно и то же значение для переменной-канарейки в стеке, а поскольку при проверке этой переменной сбойный рабочий процесс убивается — значение этой переменной можно перебрать бит за битом примерно за 1000 попыток. У Envoy, который работает с потоками, не подвержен этой атаке.

Аналогично мы усиливаем сторонние зависимости при сборке, где это возможно. Так мы применяем BoringSSL в режиме FIPS, который включает в себя самопроверку при запуске и проверку целостности бинарного файла. Мы также рассматриваем возможность запуска бинарников с поддержкой ASAN на некоторых наших канареечных серверах на Edge.

Возможности

Наиболее спорная часть этой статьи, держите себя в руках.

Nginx начинался как вебсервер, который специализируется на обслуживании статических файлов с минимальным потреблением ресурсов. Его функции по порядку: работа с статикой, кэширование (включая защиту при скачках трафика), кэширование диапазона.

Со стороны прокси у Nginx отсутствуют функции, необходимые современной инфраструктуре. Например нету поддержки HTTP/2 для бэкэндов, проксирование gRPC есть, но без мультиплексирования соединений. Отсутствует поддержка транскодирования gRPC. Кроме того при использовании модели "открытое ядро" есть ограничения по возможностям, которые входят в версию с открытым исходным кодом. В результате некоторые из важных функций, например статистика, недоступны в версии, поддерживаемой сообществом.

Envoy наоборот развивался как ingressegress прокси, чаще всего используемый для окружений с использованием gRPC и в хвост и в гриву. Его функции в качестве вебсервера рудиментарная: нету раздачи файлов, кэширование все еще не реализовано до конца, нету поддержки сжатия. Для подобных случаев мы все еще держим запасные Nginx, которые используются Envoy в качестве upstream кластера.

Когда кэширование в Envoy будет реализовано, мы сможем перенести на Envoy большинство сценариев раздачи статических файлов с использованием S3 вместо файловой системы в качестве хранилища. Можно почитать больше про eCache, кэш HTTP с несколькими бэкэндами для Envoy.

Также у Envoy уже есть встроенная поддержка многих возможностей, связанных с gRPC:

  • Проксирование gRPC, базовая способность, которая позволила нам использовать gRPC в наших приложениях (например клиент Dropbox для настольных компьютеров)
  • Поддержка HTTP/2 для бэкэндов, позволила нам значительно сократить число соединений TCP между уровнями трафика, уменьшая потребление памяти и поддерживающий трафик.
  • Мосты gRPC->HTTP (и обратно), с помощью которых мы можем публиковать старые приложения HTTP/1 с использованием современного стека gRPC.
  • gRPC->WEB, с помощью которого мы применяем сквозной gRPC там, где промежуточные узлы (firewall, IDS и прочие) еще не умеют работать с HTTP/2.
  • gRPC JSON transcoder, с которым мы смогли перекодировать весь входящий трафик, включая публичные API Dropbox, из REST в gRPC.

Кроме прочего Envoy может работать и как исходящий прокси, так мы его используем в дополнение в следующих случаях:

  • Egress прокси, поскольку Envoy умеет в метод HTTP CONNECT — его можно использовать в качестве замены Squid прокси. Мы уже начали заменять наши сервера с Squid на Envoy, поскольку это не только значительно улучшает видимость, но также и сокращает трудоемкость за счет общего стека с data plane и мониторингом (не надо парсить журналы для статистики).
  • Обнаружение сторонних сервисов: в наших программах мы используем библиотеки Courier gRPC вместо использования Envoy в качестве service mesh. Но мы так используем Envoy в разовых случаях, когда надо подключить с минимальными затратами сервис с открытым исходным кодом к нашему обнаружению сервисов. К примеру мы применяем Envoy в качестве дополнительного модуля обнаружения сервисов в нашем стеке аналитики. Hadoop может динамически запрашивать свое имя и узлы для журналирования. Superset может запрашивать свои бэкэнды airflow, presto и hive. Grafanа может обнаруживать свою базу MySQL.

Сообщество

Разработка Nginx по большей части централизована, большинство разработок ведется за закрытыми дверями. Есть немного внешней активности в списке рассылок, а также иногда случаются дискуссии о дальнейшем развитии в официальном bug tracker. Есть канал #nginx в IRC сети FreeNode, можно свободно присоединиться для более интерактивной связи с сообществом.

Разработка Envoy открыта и децентрализована: ведется с использованием проблемзапросов на слияние на GitHub, через список рассылки и собрания сообщества (собираются в Zoom раз в две недели, прим. переводчика). Есть также достаточно активности и в Slack, приглашение можно получить здесь.

Трудно дать объективную оценку стилям разработки и сообществу, поэтому давайте посмотрим пример разработки HTTP/3.

Реализация QUIC и HTTP/3 для Nginx была недавно представлена F5. Код чистый, без внешних зависимостей. Сам процесс был довольно непрозрачный, а за полгода до этого Cloudflare разработала свою реализацию. В результате у сообщества есть две отдельных экспериментальных версии для Nginx (а как по мне — так это же круто, есть что сравнить! прим. переводчика)

В случае Envoy реализация все еще в процессе разработки, основывается на библиотеке quiche. Отслеживание статуса ведется в этой проблеме, документация по архитектуре была готова еще до готовности патчей, остальная работа, для которой требуется сообщество, помечена "Требуется помощь".

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

Текущее состояние перехода

Более полугода Nginx и Envoy работали вместе, мы послепенно переключали трафик с первого на второй с помощью DNS. На нынешний момент мы перенести на Envoy самые разные рабочие нагрузки:

  • Сервисы Ingress с высокой пропускной способностью. Все данные файлов из клиентов Dropbox для настольных компьютеров обрабатываются через сквозной gRPC с помощью Envoy. С переходом на Envoy мы также немного улучшили производительность для пользователей благодаря улучшенному повторному использованию соединений на Edge.
  • Сервисы Ingress с высоким RPS. Это клиентские метаданные, получили те же преимущества сквозного gRPC, а также из-за удаление пула соединений мы не ограничены одним запросом в одном соединении.
  • Сервисы уведомлений и телеметрии. Здесь ведется обработка всех уведомлений в режиме реального времени, поэтому на эти сервера приходят миллионы соединений по HTTP (одно на каждого активного клиента). Сервисы уведомлений теперь могут быть сделаны на потоках gRPC вместо затратного способа с long-polling.
  • Смешанные сервисы с высокой пропускной способностью и высоким RPC. Тут идет трафик API (метаданные и данные). Это позволило нам задуматься о доступных API gRPC. Мы даже можем переключиться к перекодированию наших сущетвующих API на основе REST прямо на Edge.
  • Сервисы Egress, высокопроизводительные прокси. В нашем случае — связь с AWS, в основном S3. В конечном итоге это позволит нам окончательно удалить все сервера с Squid из боевой сети, оставляя единственную L4L7 data plane.

Одна из последних вещей, которые надо перенести, сам сайт www.dropbox.com. После переноса сайта мы сможем начать вывод из эксплуатации Edge серверов с Nginx. Эпоха закончится.

Проблемы, которые у нас возникли

Переход не был полностью гладким. Но это не привело к каким-либо заметным сбоям. Труднее всего было с нашими сервисами API. Различные устройства взаимодействуют с Dropbox через наш общедоступный API, от скриптов с curlwget и встроенных устройств с особыми стеками HTTP/1.0 до всяких возможных библиотек HTTP. Nginx это испытанный промышленный стандарт "de facto", понятно, что большинство библиотек неявно зависят от его поведения. Наряду с некоторым числом несоответствий между поведением Nginx и Envoy, от которых зависят наши пользователи API, в Envoy и его библиотеках было несколько ошибок. Все они были быстро решены нами с помощью сообщества, а наши решения были приняты разработчиками.

Вот лишь несколько из необычных/ не по RFС поведений:

  • Слияние слешей в URL'ах. Слияние и нормализация — распостраненная функция для прокси, Nginx разрешает по умолчанию нормализацию и слияние, а вот Envoy не умел последнее. Мы предоставили патч, который добавляет такую функцию, и позволяет пользователям активизировать слияние с помощью параметра merge_slashes.
  • Порты в именах виртуальных хостов. Nginx может получать значение заголовка Host в обоих вариантах: example.com и example.com:port. У нас была часть пользователей, которые привыкли к такому поведению. Сначала мы работали над этим дублируя виртуальные хосты в нашей конфигурации (с портом и без него), а потом реализовали возможность игнорирования порта на стороне Envoy: [strip_matching_host_port](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#envoy-v3-api-field-extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-strip-matching-host-port).
  • Чувствительность к регистру. Небольшая часть клиентов API по какой-то неизвестной причине использовала заголовок Transfer-Encoding: Chunked (обратите внимание на большую C). Технически это правильно, поскольку RFC7230 утверждает, что заголовки Transfer-Encoding/TE нечувствительны к регистру. Исправление было простейшим и было отправлено разработчикам.
  • Запрос, у которого есть и Content-Length и Transfer-Encoding: chunked. Эти запросы работали на Nginx, но были сломаны при переходе на Envoy. Согласно RFC7230 тут немного сложнее, но общая идея заключается в том, что вебсервера должны выдавать ошибку на такие запросы, поскольку запросы вероятно "нелегальные". С другой стороны следующее же предложение говорит о том, что прокси могут просто удалить заголовок Content-Length и переслать запрос. Мы расширили http-parse, чтобы пользователи библиотеки смогли работать с такими запросами, а сейчас работаем над поддержкой в самом Envoy.

Также наверное стоит упомянуть о некоторых распостраненных проблемах с настройкой, с которыми мы столкнулись:

  • Нерабочий circuit-breaking. По нашему опыту при использовании Envoy в качестве входящего прокси, особенно если смешать HTTP/1 и HTTP/2, неправильная настройка circuit breakers может привести к неожиданным простоям во время скачков трафика или отключении бэкэнда. Стоит отметить, что по умолчанию ограничения на отключение достаточно жесткие, поэтому стоит их ослабить если вы не используете Envoy в качестве mesh прокси.
  • Буферизация. Nginx разрешает буферизацию запросов на диск, что может быть полезным в окружениях, где есть устаревшие бэкэнды HTTP/1.0, которые не понимают передачу chunked. Nginx умеет преобразовывать такие запросы в запросы с Content-Length, сохраняя их на диске. У Envoy есть фильтр Buffer, но без возможности хранения данных на диске есть ограничения по оперативной памяти.

Если будете использовать Envoy в качестве пограничного прокси — будет полезно почитать эту статью. Существуют ограничения безопасности и ресурсов, которые хотелось бы иметь в наиболее открытой части вашей инфраструктуры.

Что далее?

  • Грядет HTTP/3. Поддержка уже добавлена в наиболее популярные браузеры. Его экспериментальная поддержка в Envoy уже доступна. После обновления ядра Linux для поддержки ускорения UDP мы будем проводить эксперименты на нашем Edge.
  • Внутренний балансировщик на основе xDS и обнаружение выбросов. В настоящее время мы смотрим комбинацию Load Reporting service (LRS) и Endpoint discovery service (EDS) в качестве строительных блоков для создания общего сбалансированного по нагрузке балансировщика как для Envoy, так и для gRPC.
  • Расширения для Envoy на основе WASM. Когда станет доступным Golang proxy-wasm SDK — мы сможем начать писать расширения Envoy на Go, который даст нам доступ к широкому набору внутренних библиотек Golang.
  • Замена Bandaid. Объединение всех уровней проксирования Dropbox под одним data plane — звучит весьма убедительно. Чтобы это случилось, нам надо перенести множество функций Bandaid (особенно насчет балансировки нагрузки) на Envoy. Долгий путь, но это наш нынешний план.
  • Envoy mobile. Наконец, мы хотим использовать Envoy в наших мобильных приложения. С точки зрения трафика очень важно поддерживать единый стек с общим мониторингом и современными возможностями (HTTP/3, gRPC, TLS 1.3 и т.п.) на всех мобильных платформах.

Благодарности

Переход прошел благодаря командному усилию. Возглавляли команды Traffic и Runtime, но другие сотрудники также внесли большой вклад: Agata Cieplik, Jeffrey Gensler, Konstantin Belyalov, Louis Opter, Naphat Sanguansin, Nikita V. Shirokov, Utsav Shah, Yi-Shu Tai, и конечно же потрясающее сообщество Envoy, которое помогало нам на протяжении всего этого путешествия.

Хотим также выразить признательность техническому руководителю команды Runtime Ruslan Nigmatullin, чьи действия в качестве евангелиста Envoy, автора Envoy MVP, а также основного разработчика позволили этому проекту осуществиться.

Автор: Павел

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js