TL;DR: Описание клиент-серверной архитектуры нашей внутренней системы управления конфигурацией сети, QControl. В основе лежит двухуровневый транспортный протокол, работающий с упакованными в gzip сообщениями без декомпрессии между эндпойнтами. Распределенные роутеры и эндпойнты получают конфигурационные апдейты, а сам протокол позволяет установку локализованных промежуточных релеев. Система построена по принципу дифференциального бэкапа (“recent-stable”, объясняется ниже) и задействует язык запросов JMESpath вместе с шаблонизатором Jinja для рендера конфигурационных файлов.
Qrator Labs управляет глобально распределенной сетью нейтрализации атак. Наша сеть работает по принципу anycast, а подсети анонсируются посредством BGP. Будучи BGP anycast-сетью, физически расположенной в нескольких регионах Земли мы можем обработать и отфильтровать нелегитимный трафик ближе к ядру интернета — Tier-1 операторам.
С другой стороны, быть географически распределенной сетью непросто. Коммуникация между сетевыми точками присутствия критически важна для провайдера услуг безопасности, для того чтобы иметь согласованную конфигурацию всех узлов сети, обновляя их своевременным образом. Поэтому с целью предоставить максимальный возможный уровень основной услуги для потребителя нам необходимо было найти способ надежно синхронизировать конфигурационные данные между континентами.
В начале было Слово. Оно быстро стало коммуникационным протоколом, нуждающимся в апдейте.
Краеугольным камнем существования QControl и одновременно основной причиной траты значительного количества времени и ресурсов на построение такого рода протокола является необходимость получить единый авторитетный источник конфигурации и, в конечном счете, синхронизировать с ним наши точки присутствия. Само хранилище было лишь одним из нескольких требований в ходе разработки QControl. Помимо этого, нам также были необходимы интеграции с существующими и планируемыми сервисами на точках присутствия (ТП), умные (и настраиваемые) способы валидации данных, а также разграничение доступа. Помимо этого, мы также хотели управлять такой системой с помощью команд, а не внесением модификаций в файлы. До QControl, данные отправлялись на точки присутствия практически в ручном режиме. Если одна из точек присутствия была недоступна, и мы забывали обновить ее позже, конфигурация оказывалась рассинхронизированной — приходилось тратить время на возвращение ее в строй.
В итоге мы придумали следующую схему:
Конфигурационный сервер ответственен за валидацию данных и хранилище, у роутера есть несколько эндпойнтов, получающих и транслирующих конфигурационный апдейты от клиентов и команды поддержки на сервер, а с сервера на точки присутствия.
Качество интернет-соединения все еще значительно различается в разных уголках планеты — для иллюстрации данного тезиса, давайте посмотрим на простой MTR из Праги, Чешской Республики в Сингапур и Гонконг.
Высокие задержки означают меньшую скорость. Помимо этого, есть потеря пакетов. Ширина каналов не компенсирует эту проблему, которая должна всегда приниматься во внимание при построении децентрализованных систем.
Полная конфигурация точки присутствия — это значительный объем данных, который необходимо переслать многим получателям по ненадежным соединениям. К счастью, хотя конфигурация и меняется постоянно, это происходит небольшими порциями.
Дизайн recent-stable
Можно сказать, что построение распределенной сети по принципу инкрементальных обновлений является достаточно очевидным решением. Но с диффами связано большое количество проблем. Нам необходимо сохранять все диффы между опорными точками, а также уметь досылать их в случае, если кто-то упустил часть данных. Каждая точка назначения должна применять их в строго заданной последовательности. Обычно в случае нескольких точек назначения подобная операция может занимать продолжительное время. Получатель также должен быть в состоянии запросить упущенные части и, конечно же, центральная часть должна ответить на такой запрос корректно, посылая только пропущенные данные.
В итоге, мы пришли к довольно интересному решению — у нас есть только один опорный слой, фиксированный, назовем его stable, и только один дифф для него — recent. Каждый recent основан на последнем сформированном stable и является достаточным для перестроения конфигурационных данных. Как только свежий recent доезжает до места назначения, старый уже не нужен.
Остается только время от времени отправлять свежую stable конфигурацию, например из-за того, что recent стал слишком большим. Также важным здесь является и то, что мы рассылаем все эти обновления в режиме бродкаста/мультикаста, не беспокоясь об отдельных получателях и их способности собрать кусочки данных вместе. Как только мы убедились в том, что у всех находится корректный stable — мы отправляем только новые recent. Стоит ли уточнять, что это работает? Работает. Stable кэшируется на конфигурационном сервере и получателях, recent создается по необходимости.
Архитектура двухуровневого транспорта
Почему мы построили наш транспорт на двух уровнях? Ответ достаточно прост — мы хотели отделить маршрутизацию от высокоуровневой логики, черпая вдохновение в модели OSI с ее транспортным уровнем и уровнем приложений. На роль транспортного протокола мы взяли Thrift, а для высокоуровневого формата управляющих сообщений — формат сериализации msgpack. Именно поэтому роутер (выполняющий multicast/broadcast/relay) не смотрит внутрь msgpack, не распаковывает и не упаковывает содержимое обратно и выполняет только пересылку данных.
Thrift (с англ. — «бережливость», произносится как [θrift]) — язык описания интерфейсов, который используется для определения и создания служб под разные языки программирования. Является фреймворком к удалённому вызову процедур (RPC). Сочетает в себе программный конвейер с движком генерации кода для разработки служб, в той или иной степени эффективно и легко работающих между языками.
Мы взяли фреймворк Thrift из-за RPC и поддержки многих языков. Как обычно, легкими частями стали клиент и сервер. Однако, роутер оказался крепким орешком, отчасти из-за отсутствия готового решения во время нашей разработки.
Существуют и другие опции, типа protobuf / gRPC, однако, когда мы начинали наш проект, gRPC был достаточно молодым и мы не решились взять его на борт.
Конечно, мы могли (и на самом деле, так и стоило поступить) создать собственный велосипед. Было бы проще создать протокол для того, что нам нужно, потому что клиент-серверная архитектура является относительно прямолинейной в реализации по сравнению с тем, чтобы построить роутер на Thrift. Так или иначе, к самописным протоколам и реализациям популярных библиотек (не зря) существует традиционное предвзятое отношение, кроме того, при обсуждении всегда поднимается вопрос: «А как мы будем это портировать на другие языки?». Поэтому мы сразу же выкинули идеи о велосипеде.
Msgpack — аналог JSON, но быстрее и меньше. Это двоичный формат сериализации данных, позволяющий обмениваться данными между множеством языков.
На первом уровне у нас находится Thrift с минимум необходимой роутеру информации для пересылки сообщения. На втором уровне — упакованные структуры msgpack.
Мы выбрали msgpack потому что оно быстрее и компактнее в сравнении с JSON. Но что еще важнее, он поддерживает кастомные типы данных, позволяя нам использовать крутые фичи типа передачи сырых бинарников или специальных объектов обозначающих отсутствие данных, что было важно для нашей схемы “recent-stable”.
JMESPath
JMESPath это язык запросов к JSON.
Именно так выглядит описание, которое мы получаем из официальной документации JMESPath, но на самом деле, он дает гораздо больше. JMESPath позволяет искать и фильтровать поддеревья в произвольной древовидной структуре, а также применять изменения к данным на лету. А ещё он позволяет добавлять специальные фильтры и процедуры преобразования данных. Хотя он, конечно, требует напряжения головного
Jinja
Для некоторых потребителей, нам необходимо превратить конфигурацию в файл — поэтому мы используем шаблонный движок и Jinja является очевидным выбором. С ее помощью мы генерируем конфигурационный файл из шаблона и данных, полученных в точке назначения.
Для генерации файла конфигурации нам нужен запрос JMESPath, шаблон для местоположения файла в ФС, шаблон для самого конфига. Также на этом этапе неплохо уточнить права доступа к файлу. Все это получилось удачным образом скомбинировать в одном файле — перед началом шаблона конфигурации мы ставим заголовок в формате YAML, описывающий остальное. Например:
---
selector: "[@][?@.fft._meta.version == `42`] | items([0].fft_config || `{}`)"
destination_filename: "fft/{{ match[0] }}.json"
file_mode: 0644
reload_daemons: [fft]
...
{{ dict(match[1]) | json(indent=2, sort_keys=True) }}
Для того чтобы сделать конфигурационный файл для нового сервиса мы добавляем только новый файл шаблона. Не требуются никакие изменения исходного кода или ПО на точках присутствия.
Что изменилось после ввода QControl в операционную деятельность?
Первое и самое важное — согласованная и надежная доставка обновлений конфигурации по всем узлам сети. Второе — получение мощного инструмента проверки конфигурации и внесения в нее изменений нашей командой поддержки, а также потребителями услуги.
Все это нам удалось сделать задействуя схему обновлений recent-stable для упрощения коммуникации между конфигурационным сервером и получателями конфигурации. Используя двухуровневый протокол для поддержки независимого от содержимого способа маршрутизации данных. Успешно интегрировав основанный на Jinja движок генерации конфигурации в распределенную сеть фильтрации. Данная система поддерживает широкий набор способов конфигурации для нашей распределенной и разношерстной периферии.
За помощь в написании материала спасибо VolanDamrod, serenheit, NoN.
Английская версия поста.
Автор: Shapelez