Инженерная практика
Feature flags: расцепить деплой и релиз, не утонув в долге флагов
1 августа 2012 года Knight Capital задеплоил новый код на 7 из 8 своих торговых серверов. Новый код переиспользовал старый флаг с именем Power Peg — тестовую фичу 2003 года, мёртвую годами. На восьмом, незапатченном сервере переключение этого флага разбудило мёртвый код. За 45 минут он выстрелил 4 миллионами ошибочных сделок по 154 акциям и потерял примерно $440 миллионов — больше рыночной капитализации фирмы. Устаревший флаг, оставленный в кодбазе и переиспользованный, прикончил компанию из 1400 человек.
Деплой — не релиз
Первый рефрейм сеньора: задеплоить код и зарелизить фичу — два разных события. Без флагов они сварены вместе — мерж, попавший в main, и есть момент, когда пользователи видят изменение, так что каждый рискованный запуск становится высокоставочным деплоем в 2 ночи под взглядом всей команды. Feature flag их разделяет. Ты деплоишь код «тёмным» (присутствует в проде, обёрнут в if (flags.newCheckout) {...} и возвращает false для всех), потом релизишь позже переключением флага — без пересборки, без редеплоя.
Это разделение меняет то, как команды шипят. Код мержится непрерывно и мелко (trunk-based development перестаёт зависеть от долгоживущих веток), незаконченные фичи безопасно сидят за выключенным флагом, а релиз становится изменением конфига, что SDK подхватывает за секунды. Клиентский спек Unleash поллит состояние флага на дефолтном интервале 15 секунд; LaunchDarkly стримит обновления по SSE, так что тумблер распространяется на работающие серверы заметно меньше чем за секунду. Релиз перестаёт быть деплоем и становится решением.
Четыре типа флагов — и почему их жизненные циклы различны
«Feature flag» прячет четыре разных вида, и ошибка сеньора — обращаться со всеми одинаково. Тип диктует, как долго флаг должен жить и кто им владеет.
| Тип | Назначение | Срок жизни | Владелец |
|---|---|---|---|
release | Гейтит фичу в процессе; постепенный выкат | Дни–недели — удалить после 100% | Команда разработки |
ops / kill-switch | Отключить подсистему под нагрузкой или в аварии | Постоянный — держат нарочно | SRE / on-call |
experiment | Отдавать A/B-варианты, мерить метрику | Один цикл эксперимента — потом удалить | Продукт / данные |
permission | Гейтить по плану, роли или праву | Долгоживущий — привязан к модели продукта | Продукт |
Release-флаг, которого никогда не удалили, тихо стал долгом флагов. Kill-switch, который кто-то «прибрал», потому что он выглядел устаревшим, убрал твою страховочную сетку. Тот же механизм, противоположная правильная судьба — ровно поэтому тип надо записывать, не угадывать.
Постепенный выкат и мгновенный откат
Суперсила release-флага — процентный выкат. Вместо 0% → 100% ты разгоняешь: 1% → 5% → 25% → 100%, следя за частотой ошибок и латентностью на каждом шаге. Если новый путь ломается на 5%, ты выключаешь флаг, и радиус поражения был 5% трафика, восстановлен за секунды — без отката коммита, без пайплайна хотфикса, без редеплоя. Вот настоящий аргумент за флаги: откат перестаёт быть инженерным событием и становится тумблером конфига.
Механизм важен для корректности. Хороший выкат липкий: тот же пользователь должен продолжать получать тот же вариант между запросами, иначе твой UI мерцает, а данные эксперимента — мусор. SDK делают это, хешируя стабильный ключ (userId плюс groupId) в корзину 0–99; «выкат 25%» значит, что корзины 0–24 включены. Хеш детерминирован и считается локально, так что оценка субмиллисекундна и не требует сетевого вызова на проверку — SDK держит весь набор правил в памяти и обновляет его в фоне.
Почему это работает
Зачем хешировать локально вместо запроса к серверу на каждую оценку? На масштабе флаг проверяется тысячи раз на путь запроса. Сетевой round-trip на проверку добавил бы латентность и жёсткую зависимость: если сервис флагов лежит, твоё приложение лежит. Локальная in-memory оценка с фоновой синхронизацией означает, что проверка флага — это lookup в хешмапе, а падение сервиса флагов деградирует до «последнего известного конфига», а не в твою аварию.
Каждый флаг — ветка в проде
Вот цена, что сеньор взвешивает против всей этой скорости. Каждый живой флаг — это if/else, оба пути которого работают в проде одновременно. Десять независимых булевых флагов — это 2^10 = 1024 возможных рантайм-конфигурации — ты не можешь протестировать их все, и комбинация, в которую пользователь реально попадает в проде, может быть той, что не задействовал ни один тест. Флаги умножают пространство состояний твоей системы. Они ещё и гниют: флаг, оставленный на 100% месяцами, — мёртвый конфиг, который всё ещё оценивается, всё ещё захламляет код веткой, которую никто не читает, и — урок Knight Capital — может быть переиспользован, разбудив код, о котором все забыли.
Это долг флагов, и он не гипотетический. Собственное руководство LaunchDarkly определяет устаревший флаг как тот, что отдаёт один и тот же вариант всем больше ~30 дней, и рекомендует архивировать по расписанию; инструменты вроде Piranha от Uber существуют ровно для того, чтобы AST-парсить кодбазы и автогенерить pull request, удаляющий флаг и его мёртвую ветку. Дисциплина — это вся игра: release-флаг должен нести срок годности и Jira-тикет на удаление, kill-switch’и должны быть помечены постоянными, чтобы никто их не «прибрал», а удаление — часть definition of done фичи, а не когда-нибудь-может-быть.
Новый путь checkout собран и протестирован в staging. Хочешь зашипить его в прод сегодня, но снизить риск запуска. Выбери подход к выкату.
Что на самом деле значит расцепить деплой и релиз?
Release-флаг стоит на выкате 100% три месяца, и никто его не трогал. Каков ход сеньора?
Расставь жизненный цикл release-флага от создания до вывода из эксплуатации:
- 1 Создай флаг, дефолт выключен; задеплой код тёмным в прод
- 2 Релизь 1–5% пользователей, липко по userId; следи за частотой ошибок и латентностью
- 3 Расширь выкат до 25% → 100%, пока метрики чисты (или kill-switch выключи, если нет)
- 4 Стабильно на 100% — убери флаг и удали теперь мёртвую ветку else
- 5 Подтверди, что ссылок не осталось в коде или конфиге; закрой тикет на чистку
- 01Объясни, как feature flags расцепляют деплой и релиз, и почему это меняет то, как команда шипит.
- 02Что такое долг флагов, почему инцидент Knight Capital — канонический пример, и какая дисциплина его предотвращает?
Feature flags расцепляют деплой и релиз: код шипится в прод тёмным, а рантайм-тумблер — подхваченный SDK за секунды — решает экспозицию, так что релиз становится решением, а не высокоставочным деплоем. Это покупает постепенный процентный выкат (1% → 5% → 25% → 100%, липко по userId, чтобы варианты не мерцали) и мгновенный откат kill-switch, при оценке локально как субмиллисекундный lookup хеша, так что падение сервиса флагов деградирует мягко. Но четыре типа флагов — release, ops/kill-switch, experiment, permission — имеют противоположные правильные сроки жизни, и каждый живой флаг — ветка в проде, так что N флагов значат 2^N конфигураций, что не протестировать целиком. Режим отказа — долг флагов: устаревшие или забытые флаги, что всё ещё оцениваются, захламляют код и могут быть переиспользованы — ровно механизм, стоивший Knight Capital ~$440M за 45 минут в 2012. Дисциплина сеньора — жизненный цикл: типизируй каждый флаг, давай release-флагам срок годности и тикет на удаление, помечай kill-switch’и постоянными, чтобы никто не удалил страховочную сетку, и делай чистку флагов частью «готово».