Двигайтесь быстро и (не) ломайте вещи

Почему именно динамическая конфигурация?

Ваш код часто основан на множестве констант и конфигураций. Представьте, что вы передаете эту статическую конфигурацию вашему коду:

Config {
 whitelisted_customers: [customer_1, customer_2],
 feature_1_turn_on_percentage: 0.1,
}

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

  • Добавление клиента к новой альфа-функции
  • Удалите клиента из указанной альфа-версии, чтобы отладить сбои запросов
  • Ограничьте скорость конкретного клиента, чтобы избежать ресурсного голодания
  • Тщательное развертывание функции в системе с отслеживанием состояния
  • Изменение тайм-аутов, количества повторных попыток, ограничений скорости и т. Д.

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

Система динамической конфигурации позволяет разработчикам обновлять конфигурации без повторного развертывания своих сервисов. Такие компании, как Facebook и Twitter, создали довольно сложные системы именно для этой цели. В Mixpanel мы решили написать небольшой способ управления динамической конфигурацией для наших сервисов Golang, построенный на Kubernetes.

Требования к динамическим конфигам

  1. Быстро. После внесения изменений в конфигурацию все связанные развернутые службы должны увидеть эффект менее чем за минуту.
  2. Гибкость: конфигурации должны храниться в формате, допускающем произвольные значения.
  3. Безопасно: в конфигурациях должна быть возможность писать валидацию.
  4. Контроль версий: как естественное расширение, должно быть легко вернуться к более старой версии конфигурации.
  5. Простота в использовании: должны быть простые в использовании SDK для служб, которые создают впечатление, будто они просто получают обычные значения конфигурации.

Как Mixpanel управляет динамической конфигурацией

Федерация

Помня об этих принципах дизайна, мы решили быстро написать что-то, что охватит 90% наших сценариев использования. Mixpanel работает на GKE, и большинство наших сервисов написано на Go. В Kubernetes уже есть нечто, называемое configmaps, которое позволяет разработчикам использовать SDK Kube для создания, обновления и применения конфигураций к службам или развертываниям.

Монтирование configmap просто помещает конфигурации в файлы (ключ - имя файла, значение - содержимое) в файловой системе модуля. Любые изменения в configmap выполняются так же, как и любые другие изменения в объекте kube. Это надежно решает проблему доставки конфигураций в модули согласованным образом и в режиме реального времени.

Контроль версий и проверка

Наши конфигурации записываются в файлы YAML, как описано в разделе ниже. Эти файлы регистрируются в нашем репозитории git, и изменения в них следуют тому же рабочему процессу разработки, что и изменения в коде. Таким образом, все наши конфигурации имеют контроль версий и проходят процесс проверки + CI.

Область конфигурации

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

Позже мы разделили конфигурации и сервисы (хотя они часто были тесно связаны) по следующим причинам:

  1. Некоторые конфигурации (например, белые списки) поддерживали большую функцию, охватывающую несколько служб.
  2. Две разные команды, работающие в одном сервисе, хотят обновлять конфигурации независимо друг от друга, без лишних умственных затрат на рассуждения о конфигурациях друг друга.

Сегодня большинство конфигураций основаны на продуктовой вертикали, а не на отдельных услугах.

Схема объекта конфигурации

Вот пример того, как выглядит наша конфигурационная карта:

Data
====
configs.json:
- — — 
[
  {
    "key": "feature_enabled_customers",
    "value": {
      "123": {},
      "456": {}
    }
  },
  {
    "key": "scaling_percentage",
    "value": 1
  },
  {
    "key": "timeout_secs",
    "value": 5
  }
]

В приведенном выше примере мы смонтируем configmap и создадим файл с именем configs.json с содержимым JSON внутри него. Вы можете заметить, что каждая конфигурация

{ 
 “key”: “key_name”,
 “value”: <JSON object, string, bool, number … >
}

SDK

На данный момент у нас есть только один SDK, написанный на Go. SDK делает следующее:

  1. Загружать содержимое конфигов из файла configs.json в память при его изменении. Это достигается с помощью fsnotify.
  2. Предоставьте интерфейс для получения конфигурации по имени конфигурации. На момент написания интерфейс SDK выглядел так:

Этот SDK имеет открытый исходный код! Вы можете найти его на https://github.com/mixpanel/configmanager.

Изменение конфигурации

Kubernetes позволяет разработчикам описывать ресурсы (сервисы, задания, конфигурации) как декларативный YAML. В нашем масштабе услуг мы обнаружили, что рукописный текст YAML дублируется и подвержен ошибкам. Вместо этого мы пишем Jsonnet, который затем компилируется для генерации файлов YAML. И Jsonnet, и YAML файлы проверяются в нашем репо.

Чтобы сделать изменения конфигурации декларативными и простыми для наших разработчиков, мы написали следующую библиотеку Jsonnet.

Учитывая такой синтаксический сахар, мы можем написать наш образец конфигурации:

Компиляция этого приведет к приведенному выше примеру YAML.

В дикой природе

Configmanager (так мы творчески называем эту библиотеку) широко используется в наших серверных службах Mixpanel. Вот лишь небольшая часть его использования:

  1. Хранилище: кнопка паузы и возобновления для функций, интенсивно использующих ЦП, таких как выполнение запросов на удаление GDPR.
  2. Прием: развертывание важных функций, таких как дедупликация событий и управление идентификацией, на нашем уровне приема. Configmanager использовался для добавления и удаления клиентов из белого списка и постепенного увеличения трафика.
  3. Запрос: недействительность кешей для отдельного клиента или глобально в случае изменения формата кеширования или ошибок.
  4. Везде: в различных частях нашего стека он используется для управления степенью параллелизма, доступной для обработки нашей рабочей нагрузки. Другой распространенный вариант использования - изменение лимитов скорости.

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

Будущая работа

Мы очень довольны гибкостью и ускорением разработчика, обеспечиваемыми configmanager. Тем не менее, есть возможности для улучшения; вот что мы планируем изменить в будущем:

Добавление пользовательского интерфейса

В настоящее время разработчики используют свой любимый редактор и Kubernetes CLI для просмотра и редактирования конфигураций. Хотя они просты и выполняют свою работу, для гигантских файлов конфигурации может быть проще работать с пользовательским интерфейсом, чем с одним массивным файлом JSON. Вы можете редактировать карты конфигурации kube в облаке Google, но это не лучший опыт.

Более мощный язык конфигурации

В настоящее время конфигурации представляют собой простые пары ключ-значение. Любая дополнительная логика должна быть закодирована клиентом. Вот пример гипотетического конфигурационного ключа route_to_queue:

if {customer: c1, event: alias_call} route to high priority queue
if {customer: c1, event: track_call, lib_version: v2.1} route to fail queue
...

Чтобы выполнить этот вид маршрутизации в стиле оператора переключения, вы можете сохранить объект JSON и написать некоторый код, который исключает объект JSON, и записать оператор переключения в коде. В качестве альтернативы вы можете выразить сами значения конфигурации в гибкой грамматике и закодировать эту логику переключения в самих конфигурациях. Это также избавляет нас от необходимости писать одноразовые методы, такие как IsCustomerWhitelisted.

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

Вот пост, который я написал по этому поводу. В какой-то момент, если в Mixpanel это будет достаточно, мы можем расширить configmanager таким образом.

использованная литература

  1. Https://medium.com/google-cloud/kubernetes-configmaps-and-secrets-68d061f7ab5b
  2. Https://jsonnet.org/
  3. Https://github.com/mixpanel/configmanager
  4. Https://medium.com/@nikunjyadav/generic-rules-engine-in-golang-using-antlr-d30a0d0bb565