Безопасные релизы: флаги, канареечные выкатки, откат
Вторник, 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 запросов в секунду.
| Что ищем | p0 → p1 |
Нужно запросов | При 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 мин |
Строка «решение» стоит ноль денег и требует одного абзаца в регламенте.
что менялось за последние два часа?"] --> B{"Есть изменение,
совпадающее по времени?"} B -->|нет| C["Обычный сценарий
диагностики"] B -->|да| D{"Изменение
обратимо?"} D -->|"да — код, флаг, конфиг"| E["ОТКАТ. Без обсуждения,
без гипотез, без
'сейчас поправлю одну строчку'"] D -->|"нет — миграция, письма,
записанные данные"| F{"Есть kill switch
на затронутую функцию?"} F -->|да| G["Выключить функцию,
деградировать явно"] --> I F -->|нет| H["Чиним вперёд. Дорогой путь:
время не ограничено сверху"] --> J E --> I{"Помогло?"} I -->|да| J["Стабилизировались.
Разбор в постмортеме"] I -->|нет| K["Изменение не было причиной.
Гипотеза закрыта за две минуты"] --> C
Ветка «не помогло» — не провал, а результат: откат за две минуты, который не помог, стоит дешевле часового обсуждения, помог бы он или нет.
Что делает откат невозможным
Откат возвращает код. Он не возвращает всё остальное.
- Несовместимые миграции схемы. Колонка удалена — старый код падает на
SELECT. - Данные в новом формате. Старый код их не разберёт.
- Необратимые побочные эффекты. Письма отправлены, платежи проведены, вебхуки доставлены (см. идемпотентность и доставку).
- Изменённый контракт сообщений. Потребитель старой версии не понимает событий, уже лежащих в топике.
- Клиенты вне вашего контроля. Мобильное приложение откатить нельзя: релиз в сторе занимает дни, а обновятся не все и никогда. Единственный рычаг — серверный kill switch и версионирование API, поэтому для мобильных клиентов флаг обязателен, а не желателен.
Всё это сводится к одному правилу: соседние версии должны уметь работать с данными друг друга. Версия N обязана переваривать записанное N+1, и наоборот. Иначе «откат» — эвфемизм для «восстановление из бэкапа».
Expand / contract: как сделать миграцию обратимой
Приём известен как parallel change (описание Данило Сато): изменение схемы разбивается на релизы так, что между любыми двумя соседними откат безопасен.
-- Релиз 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 и мониторинг.
Дежурного информируют, а не будят. OC->>CI: v42 помечена плохой, конвейер заблокирован
Автоматический откат снижает срочность: пользователи в порядке, будить некого. Это одно из немногих мест, где автоматизация напрямую уменьшает нагрузку на дежурство (дежурство). Два предохранителя обязательны. Защита от цикла: конвейер, автоматически повторяющий выкатку после отката, будет колотить прод бесконечно, поэтому после автоотката версия помечается плохой и повтор требует человека. Защита от ложного срабатывания: на 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 % сложности. Это тема заключительной главы трека.
Чек-лист внедрения
Дешёвое и обязательное — сверху; каждый следующий пункт имеет смысл только после предыдущих.
- Предыдущий артефакт всегда доступен в реестре, и это проверено попыткой его развернуть.
- Аннотация каждого деплоя на графиках метрик: версия, время, автор.
- Записанная политика: дежурный откатывает без согласования, доказательства не требуются.
- Раннбук отката: одна команда, проверенная руками, а не описанная по памяти.
- Kill switch на самые тяжёлые и самые новые функции.
- Release toggle с процентом и датой истечения; линтер на просроченные флаги.
- Expand / contract для любой миграции продовой таблицы.
- Канарейка в контроллере выкатки — сначала с ручным подтверждением каждого шага, автоматический анализ только после расчёта, что трафика хватает.
- Учение: намеренно выкатить заведомо плохую версию в рабочее время и замерить реальный 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, политика отката. Не переносится автоматический статистический анализ канарейки: для него нужен трафик, которого у вас нет.
Источники
- Jez Humble, David Farley. Continuous Delivery, Addison-Wesley, 2010 — исходная книга про конвейер развёртывания и обратимость.
- Nicole Forsgren, Jez Humble, Gene Kim. Accelerate, IT Revolution, 2018; актуальные отчёты — dora.dev/research.
- Pete Hodgson. Feature Toggles (aka Feature Flags) — классификация флагов и практики их удаления.
- Danilo Sato. ParallelChange; Martin Fowler. BlueGreenDeployment, CanaryRelease.
- Google. The Site Reliability Workbook, ch. 16: Canarying Releases — размер канарейки, длительность окна, метрики анализа; SRE book, ch. 8: Release Engineering.
- AWS Builders’ Library. Ensuring rollback safety during deployments — почему двухфазные изменения обязательны; Automating safe, hands-off deployments.
- Netflix. Automated canary analysis with Kayenta.
- Argo Rollouts, Flagger, Spinnaker — контроллеры выкатки; OpenFeature и Unleash — флаги.
- SEC. Administrative Proceeding 34-70694, In the Matter of Knight Capital Americas LLC, 2013 — первоисточник по инциденту 1 августа 2012 года.
- Cloudflare. Details of the Cloudflare outage on July 2, 2019; CrowdStrike. Falcon content update remediation and guidance hub, 2024.
- Charity Majors. Friday deploy freezes are exactly like murdering puppies — полемично, но по существу про доверие к средствам отката.
- Sam Newman. Building Microservices, 2nd ed., O’Reilly, 2021 — совместимость версий и независимая выкатываемость сервисов.
Что дальше
Мы разобрали, как сделать изменение дешёвым: разделить деплой и включение, ограничить радиус, довести откат до одной кнопки, разбить миграцию на обратимые шаги. Всё это работает, пока кто-то выполняет процедуры, — а процедуры накапливаются и однажды съедают рабочее время команды целиком. Следующая глава — про то, как отличить рутину, которую нужно автоматизировать, от рутины, которую нужно просто перестать делать, и почему автоматизация ради автоматизации создаёт новую рутину.