8 lines
21 KiB
JSON
8 lines
21 KiB
JSON
{
|
||
"index": 121,
|
||
"slug": "editorial-2024-08-field-feature-flags",
|
||
"title": "Feature flag после rollout: как отделить rollback от cleanup",
|
||
"excerpt": "Выключенный feature flag прекращает выдачу нового поведения, но не удаляет ветки, конфигурацию и несовместимые данные. Разбираем безопасный rollout, проверяемый rollback и отдельный cleanup gate.",
|
||
"contentHtml": "<p>Новый экран работает у сотрудников, но часть клиентов внезапно видит его после релиза. Команда ставит feature flag в <code>false</code>. Ошибка исчезает, график успокаивается, change закрывают. Через месяц в коде всё ещё живут две ветки, в конфигурации лежит старый ключ, а тесты проверяют два ответа. Любая случайная смена окружения может вернуть candidate-путь. Цена ошибки — повторный инцидент, долгий поиск владельца и риск для данных, которые новый путь уже успел записать.</p>\n<p>Обратный случай не безопаснее. Rollout достиг 100 процентов, и это объявили завершением. Но 100 процентов означает только текущий результат правила для объявленной аудитории. Это не доказывает совместимость данных, исправность recovery path и право удалить fallback. Feature flag — не одно переключение, а временный контракт: кто получает вариант, кто его вычисляет, что считается остановкой и когда старый путь перестаёт быть нужен.</p>\n<p><strong>Тезис.</strong> Rollout, rollback и cleanup нужно проводить разными изменениями. Rollout расширяет аудиторию. Rollback останавливает candidate и возвращает fallback. Cleanup удаляет лишнюю ветку после выбора итогового поведения. Если смешать эти операции, команда теряет причинную связь и принимает отключение за исправление.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>Флаг выключен, но в коде остались две ветки</td><td>Rollback смешали с удалением реализации</td><td>Найти evaluator, branches, config references и тесты по ключу</td><td>Оставить fallback исполняемым, открыть отдельный cleanup change</td></tr><tr><td>100% rollout считают доказательством готовности</td><td>Процент подменяет проверку контракта и данных</td><td>Сверить cohort key, effective config version, stop condition и recovery path</td><td>Заморозить процент и провести решение о final variant</td></tr><tr><td>После возврата к fallback часть запросов получает ошибку</td><td>Candidate записал состояние, которое fallback не читает</td><td>Проверить read/write compatibility и миграционный контракт</td><td>Остановить расширение; исправить совместимость отдельно от flag value</td></tr><tr><td>После удаления ключ снова появляется в UI</td><td>Остался client check, stale cache или старый payload</td><td>Проверить server response, client bundle, cache key и config</td><td>Удалить presentation toggle и оставить только итоговый UI contract</td></tr><tr><td>Никто не может назвать дату удаления</td><td>У флага нет owner, final variant и cleanup gate</td><td>Запросить карточку решения с evidence и списком references</td><td>Не расширять rollout; назначить владельца и новый change</td></tr></tbody></table>\n<h2>Как работает решение</h2>\n<p>Сначала evaluator получает ключ флага и контекст. Контекст должен однозначно описывать subject, окружение и другие поля, которые участвуют в targeting. Для процентного rollout особенно важен стабильный cohort key. Если сегодня используется user id, а завтра случайный request id, один пользователь будет переходить между вариантами. Это уже не gradual rollout, а непредсказуемое распределение.</p>\n<p>Evaluator возвращает выбранный вариант и, если система это поддерживает, детали оценки: key, value, variant, reason и версию конфигурации. Приложение применяет результат в одной точке. Второй слой не должен пересчитывать то же правило с другим контекстом. Иначе сервер отдаст новый ответ, а клиент решит, что пользователь относится к fallback-группе.</p>\n<p>Процент не описывает весь жизненный цикл. Он не говорит, обновился ли client cache, дошёл ли запрос до нового обработчика и можно ли откатить данные. Для каждого этапа нужны вопрос наблюдения и заранее выбранное действие. При остановке меняют одну существенную переменную: аудиторию, правило или код. Одновременная смена процента, evaluator и схемы данных уничтожает полезность сравнения.</p>\n<pre><code>type 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}</code></pre>\n<p>Этот фрагмент — учебный. Он показывает границу между результатом оценки и применением варианта. В нём нет настоящего flag provider, хранилища, телеметрии, миграции или команды rollback. В рабочей системе нужно дополнительно определить формат контекста, источник версии конфигурации, правила кеширования и поведение при ошибке evaluator.</p>\n<h2>Команды для проверки ревизии</h2>\n<p>Если candidate доставляется отдельным Kubernetes Deployment, сначала проверьте историю и состояние rollout. Эти команды требуют настроенного кластера, namespace и права чтения. Значение <code>7</code> — пример ревизии: его нужно заменить результатом первой команды.</p>\n<pre><code>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</code></pre>\n<p><code>rollout undo</code> меняет состояние Deployment, поэтому это операционная команда, а не безопасная проверка «на всякий случай». Kubernetes откатывает Pod template к выбранной ревизии; он не возвращает записи в базе, события и внешние побочные эффекты. Если команда относится только к конфигурации feature flag, применяйте аналогичный runbook вашего провайдера и сохраняйте effective version.</p>\n<h2>Учебная лестница rollout</h2>\n<table><thead><tr><th>Этап</th><th>Вопрос</th><th>Стоп-условие</th><th>Действие</th></tr></thead><tbody><tr><td>0%</td><td>Fallback выполняет согласованный smoke scenario?</td><td>Старый путь уже не работает</td><td>Не включать candidate; исправить базовый контракт</td></tr><tr><td>5%</td><td>Одна стабильная cohort получает candidate в объявленной версии?</td><td>Сигнал нарушает заранее записанную границу</td><td>Вернуть аудиторию к 0%, сохранить конфигурацию и вопрос расследования</td></tr><tr><td>25%</td><td>Candidate можно сопоставить с fallback в одном окне?</td><td>Версия или источник сигнала неизвестны</td><td>Остановить расширение и не менять правило вместе с кодом</td></tr><tr><td>100%</td><td>Вся объявленная аудитория получает выбранный вариант?</td><td>Final variant не утверждён или fallback нужен для recovery</td><td>Не удалять флаг; открыть решение о завершении</td></tr><tr><td>Cleanup gate</td><td>Код и конфигурация больше не требуют alternate branch?</td><td>Осталась runtime reference или зависимость данных</td><td>Вернуть cleanup в доработку</td></tr></tbody></table>\n<p>Значения 0, 5, 25 и 100 процентов в таблице — фиксированный учебный пример. Они не являются рекомендацией, нормативом или результатом измерения. Реальный шаг выбирают по размеру риска, качеству сигнала, обратимости и размеру cohort. Если команда не может назвать population, control, окно и владельца решения, процент не даёт полезной информации.</p>\n<figure><img src=\"/assets/editorial/2024/feature-flags-2024-cleanup-gate.svg\" alt=\"Rollout переходит к rollback и cleanup gate только после отдельного решения о финальном варианте\"><figcaption>Rollout расширяет аудиторию, rollback возвращает fallback, cleanup удаляет alternate branch. Проценты на схеме относятся к учебной модели.</figcaption></figure>\n<h2>Rollback возвращает поведение, но не переписывает историю</h2>\n<p>При stop condition минимальное действие — прекратить дальнейшее расширение candidate. Для этого возвращают аудиторию к fallback или к заранее определённому safe value. Записывают effective config version и причину остановки. Не нужно в тот же момент удалять ветки, менять evaluator и переписывать миграцию. Иначе нельзя будет понять, что именно остановило симптом.</p>\n<p>Rollback значения флага не равен rollback данных. Если candidate уже создал записи, отправил события или изменил схему ответа, старый путь должен уметь прочитать это состояние. Фича-флаг не добавляет backward compatibility автоматически. Если fallback не понимает результат candidate, ответом должен стать отдельный migration contract, а не уверенность, что <code>false</code> решает всё.</p>\n<p>Также не стоит называть rollback мгновенным. Сервис, edge-кеш и браузер могут получить конфигурацию в разное время. В карточке изменения укажите, где вычисляется вариант, сколько живёт кеш и что происходит с активной сессией. Разница версий допустима только тогда, когда она входит в контракт и не ломает recovery path.</p>\n<h2>Cleanup — отдельное решение</h2>\n<p>Cleanup начинается после выбора единственного поведения. Сначала владелец фиксирует final variant. Затем команда перестаёт добавлять новые условия и открывает отдельное изменение удаления. В его области должны быть server branches, client checks, configuration, permissions, tests, документация и миграционные заметки.</p>\n<p>Проверка cleanup строится на отрицательных утверждениях. Runtime больше не вызывает evaluator для ключа. Клиент не переключает UI по старому payload. Конфигурация не содержит targeting rules и environment overrides, нужные только флагу. Тесты проверяют итоговый business contract, а не сохранение двух boolean-веток. Документ объясняет принятое поведение, но не предлагает включить исчезнувший путь.</p>\n<p>Глобальный поиск по имени ключа полезен, но недостаточен. Ключ может быть собран из частей, сохранён в типе, скрыт в конфиге или пришит к кешу. Ищите вызовы evaluator, schema fields, token scopes, generated client types и тестовые данные. Если reference нужна для обратимой миграции, cleanup ещё не завершён: у неё должен быть owner и срок следующей проверки.</p>\n<h2>Порядок действий</h2>\n<ol><li><strong>Опишите контракт.</strong> Назовите owner, evaluator, cohort key, fallback, config version и данные, которые меняет candidate.</li><li><strong>Проверьте fallback.</strong> Выполните smoke scenario и убедитесь, что старый путь доступен до включения нового.</li><li><strong>Запишите stop condition.</strong> Свяжите её с конкретным сигналом, окном, источником и действием на остановке.</li><li><strong>Запускайте один этап.</strong> Не меняйте одновременно аудиторию, правило, код и схему данных.</li><li><strong>При проблеме остановите расширение.</strong> Верните cohort к fallback, сохраните effective config version и отделите mitigation от поиска причины.</li><li><strong>Проверьте совместимость данных.</strong> Убедитесь, что fallback читает состояние, которое оставил candidate, либо оформите отдельную миграцию.</li><li><strong>После выбора final variant заморозьте flag policy.</strong> Не добавляйте новые условия и не продлевайте флаг без новой причины.</li><li><strong>Проведите cleanup search.</strong> Проверьте server, client, config, кеши, права, тесты и документацию.</li><li><strong>Удалите артефакты отдельным change.</strong> Сохраните тест итогового поведения и обычный rollback plan для самого cleanup-релиза.</li><li><strong>Закройте решение.</strong> Зафиксируйте final variant, owner, причину удаления и evidence отсутствия runtime-зависимости.</li></ol>\n<h2>Ограничения</h2>\n<p>Этот подход не выбирает конкретный SDK, размер cohort, TTL кеша, SLO или схему миграции. OpenFeature даёт общий API для evaluation context и результата, но не задаёт правила вашей платформы; при ошибке базовой оценки клиент возвращает переданное default value. Kubernetes rollback относится к версии Deployment и его Pod template; он не доказывает обратимость бизнес-данных. Система флагов также не доказывает качество эксперимента: для этого нужны отдельные метрики, контрольная группа и методика измерения.</p>\n<p>Не следует удалять fallback только потому, что candidate получил 100 процентов. Не следует сохранять flag навсегда только потому, что когда-то существовал инцидент. Если данные, кеш или долгоживущий consumer не описаны, это блокер cleanup. Если evaluator недоступен, нужен явный default и проверяемая политика ошибки. Если неизвестно, какой вариант является итоговым, сначала принимают это решение, а потом удаляют код.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Cleanup готов, если независимая проверка отвечает «да» на все вопросы: final variant записан владельцем; fallback больше не нужен для recovery или миграции; runtime не зависит от ключа; client и server не пересчитывают старое правило; configuration и permissions не содержат флаговые остатки; тесты сохраняют итоговое поведение; обычный релизный rollback не требует возвращать удалённый flag.</p>\n<p>До этого момента выключенный флаг нужно считать остановленным, но не удалённым. Такое различие сохраняет причину, границу действия и следующий шаг. Оно также не даёт спокойному графику скрыть технический долг, который снова станет пользовательской ошибкой.</p>\n<h2>Проверяемые источники</h2>\n<p><a href=\"https://openfeature.dev/docs/reference/concepts/evaluation-context/\" target=\"_blank\" rel=\"noopener\">OpenFeature: Evaluation Context</a> — официальный материал о контексте оценки, targeting key и рисках передачи персональных данных.</p>\n<p><a href=\"https://openfeature.dev/docs/reference/concepts/evaluation-api/\" target=\"_blank\" rel=\"noopener\">OpenFeature: Evaluation API</a> — официальное описание результата оценки и его деталей.</p>\n<p><a href=\"https://kubernetes.io/docs/tasks/run-application/update-deployment-rolling/\" target=\"_blank\" rel=\"noopener\">Kubernetes: Update a Deployment Without Downtime</a> — официальная документация о проверке rollout, возврате Deployment к ревизии и ограничении rollback только Pod template.</p>"
|
||
}
|