Всем привет! Я Павел, тимлид команды 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 типа тогглов, исходя из срока их жизни и частоты изменений:
- Release Toggles: самые важные тогглы. Они нужны для включения фичей, которые сделаны в TBD-подходе или не прошли процесс регрессионного тестирования. 
- Ops Toggles: закрывают функциональные блоки, которые описывают Ops-составляющую. Например включение и выключение режима осады, переключение типа капчи, переключение нагрузки с одной базы на другую. 
- Permission Toggles: тогглы для определённых групп пользователей, которым включается новый функционал. Например для сотрудников или тестировщиков. 
- 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)
 - TyVik28.01.2023 21:08- В одной из компаний, где я работал, ПМы настолько любили фиче-тоглы, что из стало огромное количество. Многие были раскатаны и забыты. А в коде оставались и смущали разработчиков. В итоге договорились о правиле, что при добавлении нового сразу ставится таска на его выпиливание. - Пользовались, кстати, тогда split.io. 
 
           
 
Hixon10
Вы не пробовали реализовать нечто подобное, что делает убер с автоматическим удалением фича тоглов?
github.com/uber/piranha