Всем привет! Я Павел, тимлид команды 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)
TyVik
28.01.2023 21:08В одной из компаний, где я работал, ПМы настолько любили фиче-тоглы, что из стало огромное количество. Многие были раскатаны и забыты. А в коде оставались и смущали разработчиков. В итоге договорились о правиле, что при добавлении нового сразу ставится таска на его выпиливание.
Пользовались, кстати, тогда split.io.
Hixon10
Вы не пробовали реализовать нечто подобное, что делает убер с автоматическим удалением фича тоглов?
github.com/uber/piranha