Всем привет! Я Павел, тимлид команды SLA, и занимаюсь оценкой надёжности Авито. В своей прошлой статье я рассказал про стратегии ветвления и Trunk Based Development. Если не читали, переходите по ссылке. А сейчас я хочу рассказать про фича-флаги, которые появляются именно в контексте TBD. 

Что такое фича-флаг

В Авито в рамках TBD-подхода мы создаём фичи, которые показывают готовность проекта к релизу. Нам важно, чтобы они не были видны пользователю, поэтому мы закрываем их защитным тогглом.

Самый простой вариант: if (false):

func (s *Service) Handle {
...
  // мы не хотим, чтобы фича работала пока не готова
  // @see <link to task> is in active development
  if (false) {
    // Добавим функционал со списком покупок, но список всегда пустой
    result["purchases"] = []Purchase{}
  }  
...
}

Согласно TBD-подходу, каждую итерацию фича расширяется и обрастает «мясом»:

func (s *Service) Handle {
...
  // мы не хотим, чтобы фича работала пока не готова
  // @see <link to task> is in active development
  if (false) {
    // делаем запрос в сервис, за списокм покупок
    purchases := s.purchasesRepository.GetLast(limit, userID)
    // Добавим список покупок к ответу
    result["purchases"] = purchases
  }  
...
}

И в момент, например, когда фича частично или полностью работоспособна, мы меняем false на динамический фича-флаг:

const PurchaseListFeature = "some-string"
func (s *Service) Handle {
...
  // проверим, включена ли фича
  if (s.FeatureFlags.IsEnable(PurchaseListFeature)) {
    // делаем запрос в сервис, за списокм покупок
    purchases := s.purchasesRepository.GetLast(limit, userID)
    // Добавим список покупок к ответу
    result["purchases"] = purchases
  }  
...
}

Теперь мы можем динамически включать и выключать фичу. Например, для проведения регрессионных тестов или экстренного выключения, если рискованный функционал начнет «стрелять». Но сами тогглы могут применяться и в отрыве от TBD-процессов.

Типы тогглов

Мартин Фаулер выделяет 4 типа тогглов, исходя из срока их жизни и частоты изменений:

  1. Release Toggles: самые важные тогглы. Они нужны для включения фичей, которые сделаны в TBD-подходе или не прошли процесс регрессионного тестирования.

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

  3. Permission Toggles: тогглы для определённых групп пользователей, которым включается новый функционал. Например для сотрудников или тестировщиков.

  4. Experiment Toggles: тогглы для A/B-тестов.

Типы тогглов по Мартину Фаулеру
Типы тогглов по Мартину Фаулеру
Важно!

Типы тогглов по Мартину Фаулеру — это не изолированные категории. Ops-тоггл можно совместить, например, с Experiment-тогглом.

Способы реализации динамических тогглов

Bool-toggles — простые тогглы-константы. 

Константы можно зашивать прямо в код, положить в конфиг, или даже класть в Redis\BD и управлять ими из админки. В них важна простота и только два стейта: true\false.

Они хорошо подходят для Realease-тогглов и для Ops-тогглов:

type Toggles struct {
  constToggles map[string]bool
}

func (t *Toggles) IsEnable(toggleName string) bool {
  return t.constToggles[toggleName]
}

...
if (toggleService.IsEnable(Feature)) {
  // здесь описываем функционал под тогглом
}
...

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

Значение процента вероятности можно также хранить в виде константы в коде, конфиге или Redis\BD для управления из админки.

type Toggles struct 
  percentToggles map[string]int
}

func (t *Toggles) IsEnable(toggleName string) bool {
  // значение в диапазоне [0, 100]
  percent := max(0, min(100, t.percentToggles[toggleName]))
  return percent >= rand.Intn(100)+1
}

...
if (toggleService.IsEnable(Feature)) {
  // здесь описываем функционал под тогглом
}
...

Обратите внимание, что «бросок кубиков» — random. То есть, каждый вызов будет приводить к новому результату и будет сходиться к нужной нам вероятности.

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

Idempotent Percent Toggles такие же, как процентные тогглы. Но их поведение не меняется с обновлением страницы для одного пользователя. Они подходят для Release-, Ops-, Experiment-тогглов.

Значение процента вероятности можно также хранить в виде константы в коде, конфиге или Redis\BD для управления из админки. А вот критерий разбиения лучше хранить в коде, и не делать конфигурированным. Это сильно унифицирует способы задания параметров тоггла — ровно один скаляр.

type Toggles struct 
  idempotentPercentToggles map[string]int
}

func (t *Toggles) IsEnable(toggleName string) bool {
  // значение в диапазоне [0, 100]
  percent := max(0, min(100, t.idempotentPercentToggles[toggleName]))
  // cчитаем md5 от критерия
  // превращаем  в число
  // считаем остаток от деления на 100
  // ВАЖНО - что для одного и того-же критерия тоггл имеет одно и то-же значение
  return percent >= t.getMd5Mod100(criteria)
}

...
if (toggleService.IsEnable(Feature)) {
  // здесь описываем функционал под тогглом
}
...

Во всех этих типах важно использовать устойчивый критерий, который будет редко меняться. Им могут стать DeviceID, Fingerprint, UserID. Если критерий неустойчивый, то «кубики» будут бросаться каждый раз, и каждый раз поведение для пользователя будет определяться заново.

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

Фича статистики закрыта обычным процентным тогглом
Фича статистики закрыта обычным процентным тогглом

Тогглы в микросервисах

«Куда выносить тогглы?» — частый вопрос, особенно при распиле монолита. Часто бывает, что один тоггл определяет поведение, которое выносится в несколько разных сервисов. К примеру, тоггл «показать статистику» есть и в сервисе для мобильных приложений, и в сервисе для десктопа. Мы в Авито много думали над этим вопросом.

И вариантов тут всего два:

Синхронные тогглы

Первая идея, которая приходит в голову — сделать сервис тогглов и вынести их туда. Тогда микросервисы будут получать состояние тогглов из него. 

Это не лучший выбор, потому что:

  • нет явной связи между сервисами;

  • может получиться асинхронный монолит, а не микросервисы;

  • придётся ждать релиза всех сервисов, где есть тоггл, прежде чем его включать;

  • включение и выключение — всегда огромный риск: не предугадаешь, где и что бомбанёт.

Отдельные тогглы на каждый микросервис

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

Этот подход работает лучше, потому что: 

  • Каждый сервис изначально не рассчитывает на то, что тогглы будут включены одновременно;

  • Нет неявных связей между сервисами;

  • Тестирование тоггла будет происходить внутри одного сервиса и влиять только на него.

  • Ваши сервисы не превращаются в распределенный монолит.

A/B-тесты и Experiment-toggles

Тогглы для проведения A/B-тестов мы обычно реализуем отдельно. По своему поведению они похожи на процентные идемпотентные.

Важно использовать разные механизмы для обычных тогглов и A/B-тестов, потому что A/B-тогглы:

  • всегда обкладываются тонной избыточной аналитики, в отличие от обычных;

  • имеют множество настроек, например: «только платящим», «только для Android»;

  • должны иметь отдельный интерфейс для работы аналитиков и продактов.

Если использовать один и тот же механизм:

  • вы никогда не отделите просто тогглы от A/B,

  • будет сложнее удалять устаревший код,

  • будут постоянные риски создать ОГРОМНУЮ и лишнюю нагрузку на системы аналитики. Например, процентный тоггл легко раскатить на 100%, и начать слать вагон аналитики в 100% случаев вместо каноничных 2%.

Проблемы в использовании фича-тогглов

У тогглов есть не только плюсы, есть и ворох проблем. Но если знать про них, знать подходы к решению, то последствия легко минимизировать.

Накопление тогглов

Со временем количество тогглов может стать просто огромным! Они будут встречаться в самых разных кусочках приложения. Я как-то видел тоггл, который был не актуален более 5 лет, но до сих пор существовал.

Последствия:

  • сложно проводить рефакторинг;

  • будут появляться вложенные друг в друга тогглы;

  • ошибки после включения или отключения не того тоггла: из-за схожих названий, ошибочных описаний.

Решение: нужно перестраивать процессы работы в компании. Появление любого нового тоггла должно приводить и к появлению задачи на его удаление.

Тестирование

У вас же есть тесты, да? А в каком состоянии тогглов вы тестируете? А все состояния тогглов вы проверяете? Привет комбинаторному взрыву :)

Последствия:

  • разное состояние тогглов на prod и dev;

  • тестируешь совсем не то, что будет на проде;

  • комбинаторный взрыв вариативности тогглов — проверить все сочетания просто невозможно

Решение: dev-среда должна синхронизироваться с продом. Самые важные тогглы могут иметь отдельные автотесты, проверяющие их поведение во включенном и выключенном состоянии.

Тогглы неизвестного происхождения

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

Последствия:

  • старые тогглы: неясно, зачем они нужны;

  • новые тогглы: неясно, как они работают.

Решение в интерфейсе управления тогглами нужно отражать весь цикл жизни тоггла: 

  • задача, в которой он добавлен;

  • задача в которой он будет удалён;

  • описание;

  • владелец.

Выводы

Тогглы в концепции фича-флагов необходимы для быстрой скорости Delivery и Trunk Based Development. В современной разработке с её гибкими практиками без этого не обойтись. Это позволяет нам реализовывать TBD-подходы, плавно управлять нагрузкой. А ещё быть гибче, смелее катить новые фичи, зная что они закрыты тогглом. 

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

Предыдущая статья: Критерий Манна-Уитни — самый главный враг A/B-тестов

Комментарии (2)


  1. Hixon10
    28.01.2023 01:50

    Вы не пробовали реализовать нечто подобное, что делает убер с автоматическим удалением фича тоглов?

    github.com/uber/piranha


  1. TyVik
    28.01.2023 21:08

    В одной из компаний, где я работал, ПМы настолько любили фиче-тоглы, что из стало огромное количество. Многие были раскатаны и забыты. А в коде оставались и смущали разработчиков. В итоге договорились о правиле, что при добавлении нового сразу ставится таска на его выпиливание.

    Пользовались, кстати, тогда split.io.