{ "index": 121, "slug": "editorial-2024-08-field-feature-flags", "title": "Feature flag после rollout: как отделить rollback от cleanup", "excerpt": "Выключенный feature flag прекращает выдачу нового поведения, но не удаляет ветки, конфигурацию и несовместимые данные. Разбираем безопасный rollout, проверяемый rollback и отдельный cleanup gate.", "contentHtml": "
Новый экран работает у сотрудников, но часть клиентов внезапно видит его после релиза. Команда ставит feature flag в false. Ошибка исчезает, график успокаивается, change закрывают. Через месяц в коде всё ещё живут две ветки, в конфигурации лежит старый ключ, а тесты проверяют два ответа. Любая случайная смена окружения может вернуть candidate-путь. Цена ошибки — повторный инцидент, долгий поиск владельца и риск для данных, которые новый путь уже успел записать.
Обратный случай не безопаснее. Rollout достиг 100 процентов, и это объявили завершением. Но 100 процентов означает только текущий результат правила для объявленной аудитории. Это не доказывает совместимость данных, исправность recovery path и право удалить fallback. Feature flag — не одно переключение, а временный контракт: кто получает вариант, кто его вычисляет, что считается остановкой и когда старый путь перестаёт быть нужен.
\nТезис. Rollout, rollback и cleanup нужно проводить разными изменениями. Rollout расширяет аудиторию. Rollback останавливает candidate и возвращает fallback. Cleanup удаляет лишнюю ветку после выбора итогового поведения. Если смешать эти операции, команда теряет причинную связь и принимает отключение за исправление.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Флаг выключен, но в коде остались две ветки | Rollback смешали с удалением реализации | Найти evaluator, branches, config references и тесты по ключу | Оставить fallback исполняемым, открыть отдельный cleanup change |
| 100% rollout считают доказательством готовности | Процент подменяет проверку контракта и данных | Сверить cohort key, effective config version, stop condition и recovery path | Заморозить процент и провести решение о final variant |
| После возврата к fallback часть запросов получает ошибку | Candidate записал состояние, которое fallback не читает | Проверить read/write compatibility и миграционный контракт | Остановить расширение; исправить совместимость отдельно от flag value |
| После удаления ключ снова появляется в UI | Остался client check, stale cache или старый payload | Проверить server response, client bundle, cache key и config | Удалить presentation toggle и оставить только итоговый UI contract |
| Никто не может назвать дату удаления | У флага нет owner, final variant и cleanup gate | Запросить карточку решения с evidence и списком references | Не расширять rollout; назначить владельца и новый change |
Сначала evaluator получает ключ флага и контекст. Контекст должен однозначно описывать subject, окружение и другие поля, которые участвуют в targeting. Для процентного rollout особенно важен стабильный cohort key. Если сегодня используется user id, а завтра случайный request id, один пользователь будет переходить между вариантами. Это уже не gradual rollout, а непредсказуемое распределение.
\nEvaluator возвращает выбранный вариант и, если система это поддерживает, детали оценки: key, value, variant, reason и версию конфигурации. Приложение применяет результат в одной точке. Второй слой не должен пересчитывать то же правило с другим контекстом. Иначе сервер отдаст новый ответ, а клиент решит, что пользователь относится к fallback-группе.
\nПроцент не описывает весь жизненный цикл. Он не говорит, обновился ли client cache, дошёл ли запрос до нового обработчика и можно ли откатить данные. Для каждого этапа нужны вопрос наблюдения и заранее выбранное действие. При остановке меняют одну существенную переменную: аудиторию, правило или код. Одновременная смена процента, evaluator и схемы данных уничтожает полезность сравнения.
\ntype FlagDecision = {\n key: string;\n variant: 'fallback' | 'candidate';\n cohortKey: string;\n configVersion: string;\n};\n\nfunction renderCheckout(decision: FlagDecision, order: Order) {\n if (decision.variant === 'candidate') {\n return renderNewCheckout(order);\n }\n\n return renderLegacyCheckout(order);\n}\nЭтот фрагмент — учебный. Он показывает границу между результатом оценки и применением варианта. В нём нет настоящего flag provider, хранилища, телеметрии, миграции или команды rollback. В рабочей системе нужно дополнительно определить формат контекста, источник версии конфигурации, правила кеширования и поведение при ошибке evaluator.
\nЕсли candidate доставляется отдельным Kubernetes Deployment, сначала проверьте историю и состояние rollout. Эти команды требуют настроенного кластера, namespace и права чтения. Значение 7 — пример ревизии: его нужно заменить результатом первой команды.
kubectl rollout history deployment/checkout -n shop\nkubectl rollout status deployment/checkout -n shop --timeout=60s\n\n# Изменение состояния: выполняйте только по утверждённому stop condition\nkubectl rollout undo deployment/checkout -n shop --to-revision=7\nkubectl rollout status deployment/checkout -n shop --timeout=60s\nrollout undo меняет состояние Deployment, поэтому это операционная команда, а не безопасная проверка «на всякий случай». Kubernetes откатывает Pod template к выбранной ревизии; он не возвращает записи в базе, события и внешние побочные эффекты. Если команда относится только к конфигурации feature flag, применяйте аналогичный runbook вашего провайдера и сохраняйте effective version.
| Этап | Вопрос | Стоп-условие | Действие |
|---|---|---|---|
| 0% | Fallback выполняет согласованный smoke scenario? | Старый путь уже не работает | Не включать candidate; исправить базовый контракт |
| 5% | Одна стабильная cohort получает candidate в объявленной версии? | Сигнал нарушает заранее записанную границу | Вернуть аудиторию к 0%, сохранить конфигурацию и вопрос расследования |
| 25% | Candidate можно сопоставить с fallback в одном окне? | Версия или источник сигнала неизвестны | Остановить расширение и не менять правило вместе с кодом |
| 100% | Вся объявленная аудитория получает выбранный вариант? | Final variant не утверждён или fallback нужен для recovery | Не удалять флаг; открыть решение о завершении |
| Cleanup gate | Код и конфигурация больше не требуют alternate branch? | Осталась runtime reference или зависимость данных | Вернуть cleanup в доработку |
Значения 0, 5, 25 и 100 процентов в таблице — фиксированный учебный пример. Они не являются рекомендацией, нормативом или результатом измерения. Реальный шаг выбирают по размеру риска, качеству сигнала, обратимости и размеру cohort. Если команда не может назвать population, control, окно и владельца решения, процент не даёт полезной информации.
\nПри stop condition минимальное действие — прекратить дальнейшее расширение candidate. Для этого возвращают аудиторию к fallback или к заранее определённому safe value. Записывают effective config version и причину остановки. Не нужно в тот же момент удалять ветки, менять evaluator и переписывать миграцию. Иначе нельзя будет понять, что именно остановило симптом.
\nRollback значения флага не равен rollback данных. Если candidate уже создал записи, отправил события или изменил схему ответа, старый путь должен уметь прочитать это состояние. Фича-флаг не добавляет backward compatibility автоматически. Если fallback не понимает результат candidate, ответом должен стать отдельный migration contract, а не уверенность, что false решает всё.
Также не стоит называть rollback мгновенным. Сервис, edge-кеш и браузер могут получить конфигурацию в разное время. В карточке изменения укажите, где вычисляется вариант, сколько живёт кеш и что происходит с активной сессией. Разница версий допустима только тогда, когда она входит в контракт и не ломает recovery path.
\nCleanup начинается после выбора единственного поведения. Сначала владелец фиксирует final variant. Затем команда перестаёт добавлять новые условия и открывает отдельное изменение удаления. В его области должны быть server branches, client checks, configuration, permissions, tests, документация и миграционные заметки.
\nПроверка cleanup строится на отрицательных утверждениях. Runtime больше не вызывает evaluator для ключа. Клиент не переключает UI по старому payload. Конфигурация не содержит targeting rules и environment overrides, нужные только флагу. Тесты проверяют итоговый business contract, а не сохранение двух boolean-веток. Документ объясняет принятое поведение, но не предлагает включить исчезнувший путь.
\nГлобальный поиск по имени ключа полезен, но недостаточен. Ключ может быть собран из частей, сохранён в типе, скрыт в конфиге или пришит к кешу. Ищите вызовы evaluator, schema fields, token scopes, generated client types и тестовые данные. Если reference нужна для обратимой миграции, cleanup ещё не завершён: у неё должен быть owner и срок следующей проверки.
\nЭтот подход не выбирает конкретный SDK, размер cohort, TTL кеша, SLO или схему миграции. OpenFeature даёт общий API для evaluation context и результата, но не задаёт правила вашей платформы; при ошибке базовой оценки клиент возвращает переданное default value. Kubernetes rollback относится к версии Deployment и его Pod template; он не доказывает обратимость бизнес-данных. Система флагов также не доказывает качество эксперимента: для этого нужны отдельные метрики, контрольная группа и методика измерения.
\nНе следует удалять fallback только потому, что candidate получил 100 процентов. Не следует сохранять flag навсегда только потому, что когда-то существовал инцидент. Если данные, кеш или долгоживущий consumer не описаны, это блокер cleanup. Если evaluator недоступен, нужен явный default и проверяемая политика ошибки. Если неизвестно, какой вариант является итоговым, сначала принимают это решение, а потом удаляют код.
\nCleanup готов, если независимая проверка отвечает «да» на все вопросы: final variant записан владельцем; fallback больше не нужен для recovery или миграции; runtime не зависит от ключа; client и server не пересчитывают старое правило; configuration и permissions не содержат флаговые остатки; тесты сохраняют итоговое поведение; обычный релизный rollback не требует возвращать удалённый flag.
\nДо этого момента выключенный флаг нужно считать остановленным, но не удалённым. Такое различие сохраняет причину, границу действия и следующий шаг. Оно также не даёт спокойному графику скрыть технический долг, который снова станет пользовательской ошибкой.
\nOpenFeature: Evaluation Context — официальный материал о контексте оценки, targeting key и рисках передачи персональных данных.
\nOpenFeature: Evaluation API — официальное описание результата оценки и его деталей.
\nKubernetes: Update a Deployment Without Downtime — официальная документация о проверке rollout, возврате Deployment к ревизии и ограничении rollback только Pod template.
" }