Всем привет! Я Павел, тимлид команды 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-тестов
ссылка на оригинал статьи https://habr.com/ru/company/avito/blog/686814/
Добавить комментарий