SRE и надёжность Безопасные релизы: флаги, канареечные выкатки, откат
0%

Безопасные релизы: флаги, канареечные выкатки, откат

Безопасные релизы: флаги, канареечные выкатки, откат

Вторник, 14:10. Выкатывается версия с новым способом сериализации ответа корзины. Rolling update на сорок подов идёт двенадцать минут, к 14:22 везде новый код. В 14:31 срабатывает алерт по бюджету ошибок. В 14:36 дежурный подтверждает: доля 5xx на /cart выросла с 0,08 % до 4,7 %. В 14:41 начинается спор, откатывать или чинить: автор изменения уверен, что «там одна строчка», и просит десять минут. В 14:58 правка не помогает. В 15:02 решают откатывать — но артефакт предыдущей версии вытеснен политикой хранения реестра, и его собирают заново, одиннадцать минут. В 15:14 трафик возвращается на здоровую версию.

Шестьдесят четыре минуты после полной выкатки. На само исправление ушло ноль: помогло возвращение старого кода, доступного всё это время в git. Остальное — обнаружение, спор и логистика.

Глава не про то, как писать код без ошибок. Ошибки будут. Она про то, как устроить процесс, чтобы ошибка в релизе стоила две минуты, а не час, — и сколько это стоит в инфраструктуре, деньгах и человеческом внимании. Инструментальная сторона (конвейеры, GitOps, стратегии деплоя) разобрана в треке devops; здесь нас интересует арифметика риска и граница, за которой инженерное решение упирается в организационное.

Изменение — главная причина отказов, и это хорошая новость

Разметьте постмортемы за год по триггерам. В типичной продуктовой команде картина такая:

Триггер инцидента Доля Отличительная черта
Выкатка кода 35–45 % момент известен с точностью до минуты
Изменение конфигурации, флага, правила 15–25 % часто вообще не считается релизом
Рост нагрузки, исчерпание ёмкости 10–15 % предсказуемо, см. главу про ёмкость
Отказ инфраструктуры или зависимости 10–20 % вне вашего контроля
Истёкшие сертификаты, квоты, лимиты 5–10 % детерминированные бомбы замедленного действия

Доли у всех свои, порядок устойчив: больше половины инцидентов запускает изменение, сделанное вами и в известный момент. Google в SRE-книге даёт близкую оценку — около 70 % сбоев связаны с изменением живой системы.

Это хорошая новость. Изменение происходит в момент, который вы выбираете. Оно вносится порциями, размер которых вы контролируете. И у него есть предыдущее состояние, к которому можно вернуться, — чего нет ни у роста нагрузки, ни у отказа зоны доступности. Три этих свойства и есть весь инструментарий главы: время, доля и обратимость.

Три разных слова, которые все зовут «релизом»

  • Deploy (развёртывание) — новый код физически оказался на серверах. Пользователь ничего не заметил.
  • Release (включение) — новое поведение стало доступно какой-то части пользователей.
  • Rollout (раскатка) — эта часть растёт от нуля до всех.

В схеме «собрали, выкатили, работает у всех» три события слиты в одно, и единственный рычаг снижения риска — реже выкатывать. Как только они разделены, рычагов четыре: когда выкатывать, кому включать, как быстро расширять и как быстро выключить.

Три стрелки, ведущие назад в «развёрнут», и есть безопасность релиза: выключение поведения не требует ни сборки, ни деплоя, ни рестарта — только изменения значения флага. Секунды вместо минут. Состояние done не менее важно: флаг, который не удалили, остаётся в системе навсегда и однажды сработает.

Арифметика радиуса поражения

Сервис со средней нагрузкой 500 rps, SLO 99,9 % по доле успешных ответов, окно — 30 дней. Механика бюджета разобрана в главе про бюджет ошибок, здесь нужен результат.

окно            30 дней = 43 200 минут
бюджет времени  0,1 % × 43 200 = 43,2 минуты полной недоступности в месяц
месячный объём  500 rps × 2 592 000 с = 1,296 млрд запросов
бюджет событий  0,1 % × 1,296 млрд = 1 296 000 неуспешных запросов

Плохой релиз описывается тремя числами: f — доля трафика на новой версии, Δp — насколько выросла доля отказов, T — сколько минут это длилось.

burn = f × Δp / (1 − SLO)                  скорость сжигания бюджета
доля бюджета = burn × T / 43 200
Сценарий f Δp T burn Доля месячного бюджета
Полная выкатка, полный отказ 1,0 1,0 40 мин 1000 92,6 %
Полная выкатка, деградация 5 % 1,0 0,05 40 мин 50 4,6 %
Канарейка 1 %, полный отказ 0,01 1,0 10 мин 10 0,23 %
Канарейка 1 %, деградация 5 % 0,01 0,05 10 мин 0,5 0,012 %

Первая строка: 1000 × 40 / 43 200 = 0,926. Третья: 10 × 10 / 43 200 = 0,0023. В последней строке скорость сжигания меньше единицы — то есть меньше, чем бюджет позволяет тратить равномерно: такой релиз буквально не виден на графике.

Вторая строка отдельно поучительна. Умеренная регрессия на полном трафике почти терпима — и именно поэтому живёт часами: она не будит дежурного, но накапливается.

Сравнение площади ущерба: полная выкатка против канареечной

Между первой и третьей строкой — множитель 400, раскладывающийся на два независимых сомножителя: 100 по доле трафика и 4 по времени экспозиции. Это важно, потому что рычаги стоят по-разному:

Рычаг Чем достигается Цена
Уменьшить f канарейка, раскатка по когортам, флаг с процентом маршрутизация, второй набор дашбордов, +5–10 % ёмкости
Уменьшить T обнаружения алерт по скорости сжигания на канареечной группе, аннотации деплоя настройка алертов, разметка метрик по версии
Уменьшить T решения политика «откат без обсуждения» ноль денег, много разговоров
Уменьшить T исполнения прошлый артефакт под рукой, флаг вместо деплоя хранение артефактов, дисциплина совместимости

Самая дешёвая строка — третья, и она же чаще всего отсутствует. В истории из начала главы спор занял двадцать одну минуту из шестидесяти четырёх.

Сколько трафика нужно канарейке, чтобы что-то увидеть

«Выкатим на 1 % на десять минут и посмотрим» звучит осторожно, но статистически это утверждение либо тривиально истинно, либо бессмысленно — в зависимости от размера поломки.

Правило трёх. Если новая версия отказывает с вероятностью p, вероятность не увидеть ни одного отказа за n запросов равна (1 − p)^n. Чтобы она упала ниже 5 %:

n ≥ ln(0,05) / ln(1 − p) ≈ 3 / p        для малых p

Стоит помнить наизусть: чтобы с уверенностью 95 % не пропустить событие частотой p, нужно около 3/p наблюдений. Поломка на 5 % запросов — 60 запросов через канарейку. Поломка на 0,1 % — три тысячи.

Это нижняя граница «увидели хотя бы одну ошибку». Чтобы утверждать, что новая версия хуже старой, нужно сравнение двух долей:

n = (z_α/2 + z_β)² × p1 × (1 − p1) / (p1 − p0)²

z_α/2 = 1,96 (значимость 5 %), z_β = 0,8416 (мощность 80 %), сумма в квадрате = 7,849
from math import ceil, log

Z = 1.959964 + 0.841621          # значимость 5 % + мощность 80 % = 2,8016


def canary_sample_size(p0: float, p1: float) -> int:
    """Сколько запросов нужно пропустить через канарейку, чтобы отличить
    долю отказов p1 от базовой p0. Контрольная группа считается много
    большей канареечной — это теоретический пол, меньше не хватит никогда.
    O(1) по времени и памяти.
    """
    return ceil(Z * Z * p1 * (1 - p1) / (p1 - p0) ** 2)


def three_rule(p: float) -> int:
    """Минимум запросов, чтобы с вероятностью 95 % увидеть хоть один отказ."""
    return ceil(log(0.05) / log(1 - p))

Подставляем: 500 rps, канарейка 1 % — это 5 запросов в секунду.

Что ищем p0p1 Нужно запросов При 5 rps это
Грубая поломка 0,1 % → 5 % 156 31 секунда
Заметная регрессия 0,1 % → 1 % 960 3,2 минуты
Удвоение отказов 0,1 % → 0,2 % 15 668 52 минуты
Рост на 20 % 0,1 % → 0,12 % 235 192 13 часов

Разброс — четыре порядка, и отсюда два честных вывода.

Канареечная выкатка отлично ловит катастрофы и почти не ловит регрессии. Тридцать одна секунда против пятидесяти двух минут — при том что десятиминутная пауза в конвейере кажется «достаточно осторожной». Её хватает ровно на первую строку.

Канарейка — не тест, а ограничитель ущерба. Её задача не «проверить, что версия хорошая», а «сделать так, чтобы плохая версия успела навредить малому числу людей». Проверка гипотез — работа продуктовых экспериментов с недельной статистикой (MVP и эксперименты) и долгого наблюдения за SLI (SLI и SLO).

Для небольшого сервиса — 20 rps, канарейка 5 %, то есть 1 rps — грубая поломка обнаруживается за 2,6 минуты, а удвоение отказов за 15 668 секунд, то есть 4,4 часа. Что с этим делать, разбирается ниже.

Что канарейка не ловит

  • Эффекты масштаба. Утечка соединений к базе проявится, когда новая версия займёт все сорок подов; cache stampede начнётся при переключении большой доли трафика. Класс отказов из главы про каскады и насыщение: на 1 % трафика система линейна, на 100 % — нет.
  • Отложенные эффекты. Утечка памяти видна через шесть часов, крон срабатывает раз в сутки, фоновая миграция портит данные к утру.
  • Систематическая ошибка когорты. Липкое распределение по хешу означает, что в канарейку всегда попадают одни и те же пользователи; если это сотрудники или один регион, профиль нагрузки не репрезентативен.
  • Взаимодействие выкаток и изменения не в коде. Две канарейки в соседних сервисах дают четыре комбинации версий, из которых проверяется одна. А конфигурация, правило WAF или версия модели вообще не проходят через контроллер выкатки.

Ни один пункт не отменяет канарейку. Все означают, что за ней нужен второй эшелон: алерты по бюджету на длинном окне (алерты), нагрузочные проверки (нагрузочное тестирование) и учения (проверка отказом).

Флаги: код в проде не равно функция включена

Фича-флаг — условие в коде, значение которого приходит извне и меняется без деплоя. Классификация Пита Ходжсона (martinfowler.com) полезна тем, что связывает тип со сроком жизни, а срок жизни определяет цену.

Тип Зачем Срок жизни Кто меняет Динамичность
Release toggle отделить деплой от включения дни–недели, затем удалить команда разработки статичный
Ops toggle / kill switch выключить тяжёлую функцию под нагрузкой месяцы–годы дежурный меняется в инциденте
Experiment toggle A/B-эксперимент недели, до решения продукт и аналитика распределение по когортам
Permission toggle доступ по подписке или роли постоянно бизнес-логика на каждый запрос

Смешивание типов — источник большинства проблем. Release toggle, проживший год, превращается в неуправляемую ветку кода. Kill switch, который меняет разработчик мимо дежурного, — сюрприз в три часа ночи. Experiment toggle, влияющий на доступность, ломает канареечный анализ.

import hashlib

BUCKETS = 10_000


def bucket(flag_key: str, unit_id: str) -> int:
    """Корзина 0..9999 для пары «флаг + единица распределения». Соль из ключа
    флага обязательна: без неё в канарейку каждого флага попадает один и тот же
    хвост пользователей. O(1) по времени и памяти.
    """
    digest = hashlib.sha256(f"{flag_key}:{unit_id}".encode()).digest()
    return int.from_bytes(digest[:4], "big") % BUCKETS


def is_enabled(flag_key: str, unit_id: str, snapshot: dict) -> bool:
    """Чистая функция от снимка конфигурации в памяти: сетевого вызова
    в горячем пути нет и быть не может."""
    rule = snapshot.get(flag_key)
    if rule is None or rule["kill_switch"]:
        return False                      # неизвестный флаг и рубильник — выключено
    if unit_id in rule["allowlist"]:
        return True                       # свои и отладка
    return bucket(flag_key, unit_id) < rule["percent"] * 100

Единица распределения — отдельное решение. По user_id пользователь видит согласованный интерфейс, но один тяжёлый клиент перекашивает метрики. По request_id статистика чище и набирается быстрее, зато на одной странице человек получит два разных поведения. Для канарейки инфраструктурного изменения единицей обычно служит под или узел, а не пользователь.

Флаг не должен становиться зависимостью. Сервис флагов, опрашиваемый на каждый запрос, — новое обязательное звено цепочки доступности со всеми последствиями из главы про деградацию. Рабочая схема: конфигурация доставляется в процесс потоком и живёт в памяти; вычисление флага — чистая функция без ввода-вывода; при потере связи используется последний известный снимок, а не дефолт из кода; при старте без связи — зашитые дефолты и громкая запись в лог. Дефолт всегда «старое поведение». Если сеть отвалилась, вы хотите систему, которая ведёт себя как вчера, а не как непроверенное завтра.

Флаги — это долг. Десять независимых булевых флагов дают 2^10 = 1024 комбинации поведения. Тестируются обычно две: всё выключено и всё включено. Остальные 1022 существуют в проде и не проверялись никогда. Это не аргумент против флагов, а цена, которую надо назвать вслух. Минимум гигиены: у флага есть владелец и дата истечения в коде; CI ругается на release toggle старше 60–90 дней; удаление флага входит в определение готовности задачи; флаг никогда не переиспользуется под новый смысл.

Последнее правило написано кровью. 1 августа 2012 года Knight Capital выкатывала код на восемь серверов; на одном обновление не применилось. Новый код переиспользовал флаг, включавший мёртвую с 2003 года функцию Power Peg. Когда пошли аномальные сделки, команда откатила новый код на всех восьми серверах — и тем самым включила мёртвый код на семи исправных. За 45 минут компания потеряла около 460 млн USD и как самостоятельный бизнес перестала существовать. Первоисточник — постановление SEC 34-70694. Три урока в одной истории: переиспользованный флаг, неверифицированная неполная выкатка и откат вслепую.

Стратегии выкатки: сравнение и цена

Стратегия Радиус в момент сбоя Скорость отката Цена Где ломается
Recreate 100 % плюс простой как деплой ноль простой на каждой выкатке
Rolling update растёт от 1/N до 100 % как деплой, минуты ноль в проде две версии, совместимость обязательна
Blue-green 100 % мгновенно секунды, переключение балансировщика вторая копия окружения схема БД общая, откат кода не откатывает данные
Canary заданная доля секунды–минуты маршрутизация, анализ, +5–10 % ёмкости нужен трафик для статистики
Feature flag заданная доля секунды, без деплоя долг флагов, два кода в проде не покрывает инфраструктурные изменения
Shadow / dark launch 0 %, ответ не отдаётся не нужен двойная нагрузка на зависимости побочные эффекты дублировать нельзя

Blue-green и canary часто противопоставляют, хотя они решают разные задачи: blue-green даёт мгновенный откат ценой второй копии, canary — малый радиус ценой сложности маршрутизации. В Kubernetes оба реализуются одним контроллером поверх обычного Deployment (см. Kubernetes и Helm и GitOps).

Откат — первое действие, а не последнее

Доступность через среднюю наработку на отказ и среднее время восстановления: A = MTBF / (MTBF + MTTR). Пусть плохой релиз случается раз в месяц, а восстановление занимает час:

как есть:  MTBF = 43 200, MTTR = 60  →  60 / 43 260 = 0,001387  →  59,9 мин/мес
вариант A: вдвое реже ломаться       →  MTBF = 86 400, MTTR = 60  →  30,0 мин/мес
вариант B: вдвенадцатеро быстрее     →  MTBF = 43 200, MTTR =  5  →   5,0 мин/мес

Бюджет 99,9 % — 43,2 минуты, то есть в исходное состояние мы не влезаем с одним-единственным событием.

Вариант A — программа повышения качества: больше тестов, дольше ревью, тщательнее стенды. Полугодовая работа всей команды, результат не гарантирован, и он даёт 30 минут — впритык к бюджету, без запаса на всё остальное. Вариант B — хранение прошлого артефакта, аннотация деплоя на дашборде и написанное правило «дежурный откатывает без согласования». Недели работы одного человека, 5 минут на выходе и 38 минут свободного бюджета на нерелизные инциденты.

MTTR — почти всегда более дешёвый рычаг, чем MTBF. Из этого не следует, что тестами можно пренебречь (стратегия автоматизации, тесты в CI). Следует другое: при ограниченном бюджете начинать надо со скорости возврата.

MTTR — не одно число, а сумма четырёх, и лечатся они разным:

Слагаемое Было Стало Чем
Обнаружение 25 мин 2 мин алерт по скорости сжигания на канареечной группе, а не на общем графике
Решение «откатывать?» 15 мин 0 мин политика: при связи с релизом откат выполняется, разбор потом
Исполнение 12 мин 1 мин прошлый артефакт в реестре либо переключение флага
Распространение 8 мин 2 мин сброс кэша CDN, короткие TTL, быстрый rolling back
Итого 60 мин 5 мин

Строка «решение» стоит ноль денег и требует одного абзаца в регламенте.

Ветка «не помогло» — не провал, а результат: откат за две минуты, который не помог, стоит дешевле часового обсуждения, помог бы он или нет.

Что делает откат невозможным

Откат возвращает код. Он не возвращает всё остальное.

  • Несовместимые миграции схемы. Колонка удалена — старый код падает на SELECT.
  • Данные в новом формате. Старый код их не разберёт.
  • Необратимые побочные эффекты. Письма отправлены, платежи проведены, вебхуки доставлены (см. идемпотентность и доставку).
  • Изменённый контракт сообщений. Потребитель старой версии не понимает событий, уже лежащих в топике.
  • Клиенты вне вашего контроля. Мобильное приложение откатить нельзя: релиз в сторе занимает дни, а обновятся не все и никогда. Единственный рычаг — серверный kill switch и версионирование API, поэтому для мобильных клиентов флаг обязателен, а не желателен.

Всё это сводится к одному правилу: соседние версии должны уметь работать с данными друг друга. Версия N обязана переваривать записанное N+1, и наоборот. Иначе «откат» — эвфемизм для «восстановление из бэкапа».

Expand / contract: как сделать миграцию обратимой

Приём известен как parallel change (описание Данило Сато): изменение схемы разбивается на релизы так, что между любыми двумя соседними откат безопасен.

Фазы миграции expand-contract и окно безопасного отката

-- Релиз 1 (expand): только добавление, старый код о колонке не знает.
-- NULL обязателен: NOT NULL с DEFAULT на большой таблице в старых версиях
-- PostgreSQL и в MySQL переписывает всю таблицу под блокировкой.
ALTER TABLE orders ADD COLUMN total_minor BIGINT NULL;

-- Релиз 2 (двойная запись): пишем в обе колонки, читаем старую.
-- Откат к релизу 1 безопасен: total_amount остаётся источником истины.

-- Релиз 3 (бэкфилл): фоном, порциями, с паузами — не одной транзакцией.
UPDATE orders SET total_minor = CAST(ROUND(total_amount * 100) AS BIGINT)
 WHERE total_minor IS NULL AND id BETWEEN :lo AND :hi;

-- Релиз 4 (переключение чтения): читаем total_minor, пишем по-прежнему в обе.
-- Откат безопасен: старая колонка актуальна.

-- Релиз 5 (contract): точка невозврата. Делается, когда версии, знающей про
-- total_amount, не осталось нигде: ни воркеров, ни крона, ни долгих соединений.
ALTER TABLE orders DROP COLUMN total_amount;

Пять релизов вместо одного — дороже. Оплачивается тем, что в любой момент между ними инцидент лечится откатом за минуту, а не восстановлением базы за часы. Для таблицы на десять строк приём избыточен; для таблицы под продовым трафиком — обязателен.

Конфигурация — тоже релиз

Самая частая слепая зона. Код проходит ревью, тесты, канарейку и постепенную раскатку. Конфигурация, правило фильтрации, содержимое справочника, версия модели, DNS-запись — выкатываются мгновенно и глобально, потому что «это же просто настройка».

2 июля 2019 года Cloudflare выкатила правило WAF с регулярным выражением, дающим катастрофический бэктрекинг. CPU всех машин глобальной сети ушёл в 100 %, значительная часть интернета перестала отвечать примерно на полчаса. В разборе компания прямо пишет: правила WAF считались конфигурацией и выкатывались глобально за секунды, минуя поэтапную раскатку, давно применявшуюся к коду.

19 июля 2024 года CrowdStrike выкатила обновление файла контента для агента Falcon — не код, а данные для установленного драйвера. Обновление ушло на все машины сразу и уронило порядка 8,5 млн Windows-хостов. В разборе первым пунктом плана исправлений идёт поэтапная выкатка контента.

Вывод переносится на любой масштаб: если изменение попадает в прод, оно проходит тот же конвейер, что и код — версионирование, ревью, раскатка, возможность отката. Инфраструктура как код и GitOps существуют ровно для этого: превратить конфигурацию в объект, у которого есть история и предыдущее состояние.

Автоматика: канареечный анализ и автооткат

Когда трафика хватает, шаги «посмотреть на метрики» и «нажать откат» автоматизируются. Ниже типовая форма для Argo Rollouts; у Flagger, Spinnaker и облачных сервисов деплоя логика та же.

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata: {name: checkout}
spec:
  replicas: 40
  strategy:
    canary:
      canaryService: checkout-canary
      stableService: checkout-stable
      analysis:
        templates: [{templateName: slo-guard}]
        startingStep: 1              # анализ включается со второго шага
      steps:                         # 20 минут при 5 rps — около 6000 запросов
        - {setWeight: 1}
        - {pause: {duration: 20m}}
        - {setWeight: 5}
        - {pause: {duration: 20m}}
        - {setWeight: 25}
        - {pause: {duration: 30m}}
        - {setWeight: 50}
        - {pause: {duration: 1h}}    # длинная пауза ловит отложенные эффекты
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata: {name: slo-guard}
spec:
  metrics:
    - name: error-rate
      interval: 2m
      failureLimit: 0                # одного превышения достаточно для отката
      successCondition: result[0] <= 0.01
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            sum(rate(http_requests_total{job="checkout",version="canary",code=~"5.."}[2m]))
            / sum(rate(http_requests_total{job="checkout",version="canary"}[2m]))            
    - name: orders-per-minute        # бизнес-метрика: 200 без заказа — тоже отказ
      interval: 5m
      failureLimit: 1
      successCondition: result[0] >= 40
      provider: {prometheus: {address: "http://prometheus.monitoring:9090",
        query: 'sum(rate(orders_created_total{version="canary"}[5m])) * 60'}}

Вторая метрика здесь не для полноты. Технические сигналы ловят падения; они не ловят версию, которая исправно отдаёт 200 на кнопку «Оплатить», не создавая заказа. Про выбор таких показателей — SLI и SLO и мониторинг.

Автоматический откат снижает срочность: пользователи в порядке, будить некого. Это одно из немногих мест, где автоматизация напрямую уменьшает нагрузку на дежурство (дежурство). Два предохранителя обязательны. Защита от цикла: конвейер, автоматически повторяющий выкатку после отката, будет колотить прод бесконечно, поэтому после автоотката версия помечается плохой и повтор требует человека. Защита от ложного срабатывания: на 0,2 rps одна ошибка даёт 100 % за двухминутное окно, поэтому минимальное число запросов в окне обязательно — иначе вы построили генератор случайных откатов.

Где инженерное упирается в организационное

Заморозки и согласования

Заморозка релизов перед высоким сезоном звучит осмотрительно. Пусть вероятность того, что отдельное изменение окажется плохим, равна 1 %.

20 изменений в одном релизе:  P(релиз плохой) = 1 − 0,99²⁰ = 18,2 %
20 релизов по одному:         ожидание плохих = 20 × 0,01 = 0,2
                              P(хотя бы один) = 1 − 0,99²⁰ = 18,2 %

Числа одинаковые. Заморозка не уменьшает количество дефектов — она их накапливает. Меняется другое:

  • Локализация. В релизе из двадцати изменений виновника ищут бинарным поиском: log₂(20) ≈ 4,3, то есть пять итераций «выкатить половину и посмотреть» по пять минут — 25 минут только на поиск. При одном изменении — ноль.
  • Цена отката. Откат релиза с двадцатью изменениями отменяет девятнадцать хороших, включая чужие. Появляется сопротивление: «давайте не откатывать всё, а починим то одно». Это те самые двадцать одна минута спора.
  • Пик после разморозки. Накопленное выкатывается одновременно, обычно когда команда выдохнула после сезона.

Чем крупнее партия, тем выше MTTR, а MTTR стоит дороже частоты. Механизм тот же, что в петлях обратной связи с задержкой: чем позже сигнал доходит до управляющего, тем сильнее перерегулирование (подробно — глава про задержки). Осмысленный вариант заморозки — не «нельзя релизить», а «нельзя релизить необратимое»: миграции схемы, contract-фазы, изменения контрактов. Обратимые релизы под флагом продолжают идти.

Частота против качества: считаем ущерб

Команда A:   4 релиза/мес, доля неудачных 25 %, MTTR 60 мин → 1,0 × 60 = 60 мин
Команда B: 100 релизов/мес, доля неудачных  5 %, MTTR  5 мин → 5,0 ×  5 = 25 мин

У команды B в пять раз больше поломок и в 2,4 раза меньше ущерба. При бюджете 43,2 минуты первая не укладывается, вторая укладывается с запасом в 18 минут. Исследование DORA (dora.dev, книга «Accelerate») даёт тот же вывод на большой выборке: частота развёртываний и стабильность не противоречат друг другу. Там же — про внешние согласования: одобрение изменения комитетом, не работающим с сервисом ежедневно, не улучшает стабильность и заметно удлиняет цикл. Работающая замена — ревью в момент изменения плюс автоматические проверки в конвейере (см. стратегии ветвления).

Пятница, ночь и право откатывать

«Не релизим в пятницу». Проблема не в дне недели, а в том, что происходит после релиза. Если поломка обнаруживается за две минуты и лечится откатом за одну, пятница безопасна. Если обнаруживается в понедельник силами клиента — опасен и вторник. Правило «не в пятницу» — симптом: команда не доверяет своим средствам обнаружения и отката. Честная промежуточная формулировка: в пятницу выкатываем только то, что откатывается кнопкой, и не выкатываем миграции и contract-фазы.

«Релизим ночью, когда мало пользователей». Три проблемы, и все считаемые. Первая: при ночных 50 rps канарейка на 1 % — это 0,5 rps, и на обнаружение удвоения отказов нужно 15 668 / 0,5 = 8,7 часа; ночная выкатка не проверяется в принципе. Вторая: проблема проявится утром, при полной нагрузке, когда её никто не ждёт и никто не свяжет с ночным релизом. Третья: человек, разбирающийся в три часа ночи, ошибается чаще — и ошибается в момент, когда действие необратимо. Аргумент «ночью меньше пострадает» верен арифметически, но только при условии, что поломка обнаружится ночью, — а это условие ночная выкатка и нарушает. Рабочий компромисс: первая половина рабочего дня, когда трафик уже вырос, а команда ещё свежая и вся на месте.

Право откатывать. Политика формулируется одним абзацем и стоит ноль денег:

Дежурный имеет право откатить любой релиз без согласования с автором. Откат не требует доказательства причинно-следственной связи — достаточно совпадения по времени. Ошибочный откат не является ошибкой дежурного.

Последнее предложение несущее. Без него дежурный будет искать доказательства перед откатом, потому что откат чужого релиза «просто так» — социально дорогое действие. Именно так пятнадцать минут спора попадают в MTTR. Та же логика работает в обратную сторону: если за плохой релиз наказывают, инженеры релизят реже и крупнее — то есть дороже. Механизм тот же, что в главе про постмортемы: поиск виноватого меняет не количество ошибок, а количество информации о них. Организационная сторона разобрана в главе про дежурства и инциденты трека engineering-leadership.

Пять девяток и цена штатной выкатки

Возьмём ничем не примечательную rolling-выкатку, при которой 5 % запросов две минуты ловят 502 из-за перезапуска подов без корректного дренирования соединений. Не инцидент — фоновый шум, который во многих командах даже не замечают.

SLO 99,9 %:   burn = 0,05 / 0,001   =   50  →  0,23 % за выкатку  →  4,6 % за 20
SLO 99,99 %:  burn = 0,05 / 0,0001  =  500  →   2,3 % за выкатку  →   46 % за 20
SLO 99,999 %: burn = 0,05 / 0,00001 = 5000  →    23 % за выкатку  →  463 % за 20

Одна и та же техническая мелочь стоит 4,6 % бюджета на трёх девятках и четыре с половиной бюджета — на пяти. Пять девяток — это 25,9 секунды недоступности в месяц; человек в цикле принятия решений отсутствует физически, потому что пока дежурный читает алерт, месячный бюджет уже кончился. Это требование не к старательности команды, а к архитектуре: активно-активные регионы, дублирование запросов, автоматическое переключение без участия людей, полностью автоматический откат. Стоимость всего этого обычно превышает стоимость продукта.

Вопрос, который стоит задать до того, как девятки попадут в договор: сколько стоит минута простоя? Разница между 99,9 % и 99,99 % — 39 минут в месяц. При 200 USD за минуту простоя это 7 800 USD в месяц. Если переход стоит второго региона за 15 000 USD плюс двух инженеров, ответ получен без споров.

Что переносится из больших компаний, а что нет

Литература по канареечным выкаткам написана в основном Google и Netflix, и это стоит держать в голове при чтении.

Что Переносится Почему
Разделение deploy / release / rollout да, целиком дисциплина, а не инфраструктура
Флаг с процентом и kill switch да сотни строк кода либо готовая библиотека
Хранение прошлого артефакта, политика «откат без обсуждения» да политика реестра и абзац текста
Expand / contract для схемы да дисциплина, ноль инструментов
Канарейка как ограничитель ущерба да есть в любом контроллере выкатки
Автоматический статистический анализ нет при малом трафике нужны десятки тысяч запросов на группу
Многорегиональная раскатка, отдельная команда release engineering нет вы не в тридцати регионах, и у вас нет десятков людей на инструментарий

Ограничение по трафику жёсткое: при 20 rps канареечный анализ по частоте ошибок не работает для регрессий тоньше катастрофы, и никакая настройка этого не изменит. Что делать вместо:

  • Канарейка по времени, а не по доле. Один под из десяти, но смотрим час, а не десять минут: набирается больше запросов на группу.
  • Канарейка на 25–50 %. Радиус больше, статистика набирается за минуты. При хорошем автооткате это разумный размен: 50 % × 3 мин даёт тот же ущерб, что 1 % × 150 мин, но узнаёте вы за три минуты.
  • Опора на грубые сигналы и на быстрый откат. Правило трёх: 60 запросов достаточно, чтобы поймать поломку на 5 %, — а такие поломки и составляют большинство релизных аварий. Всё, что тоньше, всё равно придёт от алерта по SLO или от клиента, поэтому инвестировать надо в исполнение отката, а не в точность обнаружения.
  • Флаг плюс внутренние пользователи. Allowlist на команду, затем 10 % — ручная лестница без анализатора.

И честное замечание про инструменты. Argo Rollouts, Flagger, Spinnaker с Kayenta, OpenFeature, Unleash — рабочие вещи, но каждая добавляет систему, которую нужно обслуживать, обновлять и чинить в три часа ночи. Команде из пяти человек флаг в виде записи в базе с кэшем в памяти и kubectl rollout undo в раннбуке закрывают 90 % ценности за 1 % сложности. Это тема заключительной главы трека.

Чек-лист внедрения

Дешёвое и обязательное — сверху; каждый следующий пункт имеет смысл только после предыдущих.

  1. Предыдущий артефакт всегда доступен в реестре, и это проверено попыткой его развернуть.
  2. Аннотация каждого деплоя на графиках метрик: версия, время, автор.
  3. Записанная политика: дежурный откатывает без согласования, доказательства не требуются.
  4. Раннбук отката: одна команда, проверенная руками, а не описанная по памяти.
  5. Kill switch на самые тяжёлые и самые новые функции.
  6. Release toggle с процентом и датой истечения; линтер на просроченные флаги.
  7. Expand / contract для любой миграции продовой таблицы.
  8. Канарейка в контроллере выкатки — сначала с ручным подтверждением каждого шага, автоматический анализ только после расчёта, что трафика хватает.
  9. Учение: намеренно выкатить заведомо плохую версию в рабочее время и замерить реальный MTTR (проверка отказом).

Без последнего пункта всё, начиная с четвёртого, остаётся описанием намерений.

Типичные ошибки

  • Канарейка как галочка. Десять минут на 1 % при 500 rps — 3000 запросов; этого хватает на поломки порядка 1 % и крупнее, и ни на что тоньше. Пауза считается из нужного числа запросов, а не берётся из примера в документации.
  • Анализ без минимума запросов в окне. На 0,2 rps одна ошибка даёт 100 % за окно — генератор ложных откатов.
  • Автооткат без блокировки повтора. Конвейер откатывает и тут же выкатывает снова, циклически.
  • Флаг вычисляется сетевым вызовом в горячем пути. Сервис флагов стал обязательным звеном доступности, причём самым новым и наименее надёжным. Отдельно: дефолт флага «включено» — потеряли связь с конфигурацией и включили непроверенное поведение везде.
  • Флаг живёт год либо переиспользуется под новый смысл. Первое — мёртвая ветка, которую однажды включат; второе стоило Knight Capital 460 млн USD.
  • Миграция схемы в одном коммите с кодом. Откат кода не откатывает DROP COLUMN. И blue-green тут не спасает: код переключается мгновенно, но база общая.
  • Конфигурация мимо конвейера. Правила, справочники, модели и лимиты выкатываются глобально и мгновенно — самый частый источник крупных аварий.
  • Ночные выкатки «чтобы никто не заметил». Никто и не заметит, включая вас, до утра.
  • Заморозка вместо обратимости. Накопленная партия превращает пятиминутный откат в двадцатипятиминутный бинарный поиск.
  • Откат в последнюю очередь. Сначала гипотезы, потом «сейчас поправлю», потом откат. Порядок ровно обратный правильному.

Мини-итог

  • Больше половины инцидентов запускает изменение, сделанное вами в известный момент. Это самый управляемый класс отказов: у него есть время, доля и предыдущее состояние. Deploy, release и rollout — три разных события; пока они слиты, единственный рычаг — релизить реже.
  • Ущерб — это площадь: burn = f × Δp / (1 − SLO). Полная выкатка с полным отказом на 40 минут сжигает 92,6 % месячного бюджета 99,9 %; канарейка 1 % с автооткатом за 10 минут — 0,23 %. Множитель 400 складывается из 100 по доле и 4 по времени.
  • Канарейка ловит катастрофы за секунды и не ловит регрессии вовсе: 156 запросов на поломку 5 % против 15 668 на удвоение с 0,1 % до 0,2 %; при 5 rps это 31 секунда против 52 минут. Канарейка — ограничитель ущерба, а не тест. Правило трёх: чтобы с уверенностью 95 % увидеть событие частотой p, нужно 3/p наблюдений.
  • MTTR дешевле MTBF: снижение восстановления с 60 до 5 минут даёт 5 минут недоступности в месяц против 30 при удвоении наработки на отказ. Из шестидесяти минут пятнадцать обычно занимает спор — строка, стоящая ноль денег.
  • Откат — первое действие, а не последнее: ошибочный откат за две минуты закрывает гипотезу дешевле часового обсуждения. Но он не возвращает данные, письма и мобильных клиентов — поэтому expand / contract, где между соседними релизами откат безопасен, а contract — точка невозврата.
  • Флаги стоят денег: десять флагов — 1024 непроверенные комбинации. Дата истечения, владелец и запрет на переиспользование обязательны.
  • Конфигурация — тоже релиз. Cloudflare 2019 и CrowdStrike 2024 — обе аварии про изменение, которое не считалось кодом и выкатывалось глобально сразу.
  • Заморозка не уменьшает число дефектов (18,2 % в обоих режимах), но увеличивает MTTR: бинарный поиск по партии плюс сопротивление откату девятнадцати хороших изменений.
  • Пять девяток превращают штатную двухминутную просадку выкатки в 23 % месячного бюджета. Это требование не к команде, а к архитектуре, и стоит оно обычно дороже продукта.
  • Из практики больших компаний переносится дисциплина — разделение понятий, флаги, expand / contract, политика отката. Не переносится автоматический статистический анализ канарейки: для него нужен трафик, которого у вас нет.

Источники

Что дальше

Мы разобрали, как сделать изменение дешёвым: разделить деплой и включение, ограничить радиус, довести откат до одной кнопки, разбить миграцию на обратимые шаги. Всё это работает, пока кто-то выполняет процедуры, — а процедуры накапливаются и однажды съедают рабочее время команды целиком. Следующая глава — про то, как отличить рутину, которую нужно автоматизировать, от рутины, которую нужно просто перестать делать, и почему автоматизация ради автоматизации создаёт новую рутину.

Рутина и автоматизация: что автоматизировать, а что убрать

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов