Образ собран, тесты прошли — остался последний шаг: новый код встречается с реальными пользователями. Именно здесь и прячется риск. Стратегии релиза отвечают на два вопроса: сколько пользователей заметят проблему и как быстро вы вернётесь назад.
Цена отката — это высота линии (сколько людей уже на новой версии) и длина пунктира (сколько идёт возврат). На шестой минуте сбой застаёт rolling и blue-green на всей аудитории, но blue-green возвращается переключением, а rolling ползёт ступенями ещё четыре минуты. Canary к этому моменту показал новую версию четверти. Feature flags — единственная стратегия, где доля и цена отката не связаны: флаг гасится за секунды при любой доле.
Раньше деплоили «всё или ничего»
Исторически деплой означал: остановить сервер, залить новую версию, запустить. Пока идёт замена — сервис недоступен. Если что-то пошло не так — откат занимает столько же времени, сколько сам деплой. Все пользователи видят проблему одновременно.
У этой стратегии есть имя, и она никуда не делась: в Kubernetes она так и называется — Recreate. Сначала гасятся все старые реплики, потом поднимаются новые; перерыв между этими двумя моментами и есть простой. Для сервиса, который отвечает пользователям, это плохо, а вот для фонового обработчика — иногда единственный правильный вариант: две версии, одновременно разбирающие одну очередь, натворят хуже, чем минута простоя.
Современные стратегии решают это по-разному. Разберём каждую с нуля.
Rolling update — менять по одной реплике
Проблема: при полной замене сервис на время недоступен, а на большом парке серверов это неприемлемо.
Решение: реплики заменяются по очереди, а не все сразу. Пока одна реплика перезапускается с новой версией, остальные продолжают обслуживать запросы — сервис не пропадает целиком ни на секунду.
Kubernetes делает это из коробки: он не пустит трафик на новую реплику, пока та не пройдёт readiness-проверку. Если новая реплика стартует с ошибками — выкат встаёт: старые реплики остаются на месте и обслуживают запросы, новая крутится в перезапусках, а через progressDeadlineSeconds выкат помечается как несостоявшийся.
Но «встал» — это не «откатился». Сам назад к прежней версии Kubernetes не вернёт: кластер так и останется в половинчатом состоянии, пока откат не запустит человек командой kubectl rollout undo или оператор GitOps по откаченному коммиту. Ждать, что всё починится само, не стоит.
Совсем бесплатным rolling тоже не выходит. В Kubernetes по умолчанию на время выката разрешено поднять четверть реплик сверх штата (maxSurge 25%) и одновременно увести в перезапуск четверть штатных (maxUnavailable 25%). То есть либо нужен запас мощности под лишнюю четверть, либо какое-то время сервис работает меньшим числом реплик. Оба числа настраиваются: maxSurge: 1, maxUnavailable: 0 держит полную ёмкость ценой одной дополнительной реплики.
Что rolling не умеет:
- мгновенный откат (откат — это такой же rolling в обратную сторону, занимает минуты);
- защита от ошибок, которые видны только под полным трафиком;
- контроль «кто именно увидит новую версию».
Важное условие: во время rolling update работают две версии одновременно. Это значит, что схема базы данных и формат сообщений должны понимать обе версии. Если новая версия добавила колонку, а старая её не знает — это не проблема; если старая версия сломается при виде новых данных — это проблема.
Rolling update — разумный выбор по умолчанию для большинства изменений.
Почему выкат роняет запросы
Rolling выглядит так, будто запросы не теряются по определению: реплики меняются по очереди, сервис всё время жив. На практике первый же выкат на двадцати копиях даёт всплеск пятисотых ошибок, и это не ошибка стратегии — это то, что стратегия не делает за вас. Разберём, куда деваются запросы.
Копию убивают, пока она отвечает. Остановка начинается с сигнала SIGTERM процессу. Если процесс на него не реагирует, через положенный срок (в Kubernetes по умолчанию тридцать секунд) приходит SIGKILL, и всё, что было в работе, обрывается: незавершённые ответы, транзакции, отправка сообщений. Первое требование, без которого дальше говорить не о чем, — процесс на SIGTERM перестаёт брать новые запросы и досчитывает начатые.
Балансировщик узнаёт об остановке позже, чем процесс. Это главная и самая неочевидная причина. Удаление копии из списка получателей трафика — не мгновенная операция: сведения проходят через несколько слоёв (список готовых адресов, правила на узлах, таблицы самого балансировщика), и это занимает секунды. За эти секунды копия, которая уже начала останавливаться, продолжает получать новые соединения — и отказывает по ним.
Лечится это не ускорением, а паузой в правильном месте: перед остановкой процесса делают задержку (в Kubernetes это preStop с ожиданием, у балансировщиков — настройка вывода копии из оборота). Сначала копию перестают считать готовой, ждут, пока это разойдётся по всем слоям, и только потом гасят процесс. Порядок именно такой: пауза сначала, остановка потом.
Соединение живёт дольше запроса. Клиенты и обратные прокси держат постоянные соединения. Копия, которая закрыла соединение резко, отдаёт клиенту ошибку вместо ответа; правильный способ — сообщить, что соединение больше не используется (Connection: close в последнем ответе или штатное завершение потока протокола), и дать клиенту переоткрыть его к живой копии. Для долгоживущих соединений (веб-сокеты, поток событий) не спасает и это: их придётся закрывать, а клиент обязан уметь переподключаться.
Новая копия объявила себя готовой раньше времени. Обратная сторона: проверка готовности отвечает «да», пока прогрев не закончен — не поднялись соединения с базой, не заполнились кеши. Трафик приходит на непрогретую копию, и первые запросы отвечают долго или падают. Поэтому проверка готовности отвечает по факту готовности, а не по факту старта, и у выката есть выдержка: копия считается успешной, только если продержалась готовой некоторое время (minReadySeconds).
Что из этого следует для стратегий. Rolling, blue-green и canary одинаково зависят от корректной остановки: blue-green переключает трафик целиком, и незавершённые запросы на старом окружении обрываются так же. Проверяется это не глазами, а нагрузкой: держите на сервис ровный поток запросов и выкатывайтесь — если в отчёте появились ошибки или обрывы, дело не в стратегии, а в остановке. Механика по слоям — в разделе про корректную остановку.
Blue-green — мгновенный переключатель
Проблема: откат занимает минуты, а некоторые ошибки критичны и каждая секунда на счету.
Решение: держать два полных окружения. Одно (blue) несёт боевой трафик, в другом (green) разворачивается новая версия. Когда green готово и проверено — трафик переключается целиком. Откат: переключить трафик обратно на blue. Занимает секунды.
Цвета здесь — просто ярлыки, чтобы не путаться, какое окружение сейчас живое. Но порядок общепринятый: боевой трафик держит blue, новую версию поднимают в green. В чужой документации встречается и обратное, поэтому первым делом смотрите, какое окружение у авторов живое, а не на сам цвет.
Что blue-green даёт:
- новую версию можно проверить в продакшн-окружении до того, как её увидят пользователи;
- откат — переключение, а не новый деплой.
За что платите:
- двойная ёмкость на время выкатки;
- база данных общая для обоих окружений, поэтому миграции схемы должны понимать обе версии;
- фоновые обработчики (воркеры, потребители очередей) нельзя запускать в двух версиях одновременно — их переключают, не дублируют.
Blue-green оправдан, когда цена ошибки высока, а релизы редкие: платёжные системы, критичные API, системы с SLA.
Canary — выкатка на процент
Проблема: даже хорошо протестированный код может вести себя иначе под реальным трафиком и реальными данными.
Решение: новая версия сначала получает маленькую долю трафика — например, 1%. Если метрики (ошибки, время ответа, бизнес-показатели) не ухудшаются, доля растёт: 1% → 5% → 25% → 100%. При деградации — автоматический откат. Проблему увидела малая часть аудитории, а не все.
Название — от шахтёрской практики брать канарейку в шахту: если птица гибнет, шахтёры уходят. Новая версия — канарейка; если она «гибнет» на малом проценте, остальные пользователи в безопасности.
Что нужно для canary:
- инфраструктура для маршрутизации с весами: без весов на балансировщике новой версии можно дать только целую реплику, а это не 1 %, а одна N-я. Веса умеет service mesh, а двигать их по ступеням и откатывать по метрикам — контроллеры прогрессивной выкатки Argo Rollouts и Flagger;
- надёжные метрики — без них canary превращается в rolling с лишними шагами; продвижение по ступеням должно быть по формальным критериям, не «посмотрим глазами»;
- липкость маршрута — процент трафика это не процент пользователей. Если каждый запрос бросать монеткой, один и тот же человек попадёт то в старую версию, то в новую, увидит сломанную страницу через раз, и «пострадавших» окажется куда больше объявленного процента. Поэтому выбор версии привязывают к пользователю — по идентификатору, cookie или консистентному хешу, — чтобы человек весь выкат оставался на одной стороне;
- время — выкатка растягивается на часы, и всё это время в проде живут две версии со всеми следствиями для схемы базы и контрактов.
Canary оправдан при большом трафике и зрелой наблюдаемости. Считать тут надо не долю, а сколько запросов успеет пройти через новую версию до того, как придётся решать. Пусть на сервис идёт 100 запросов в секунду: 1% — это один запрос в секунду, и за пятнадцать минут наблюдения через канарейку пройдёт около девятисот обращений. Ошибка, которая случается раз на тысячу, в такую выборку может просто не попасть — и вы двинете выкат на следующую ступень, ничего не заметив. При 2000 запросов в секунду тот же процент и то же окно дают восемнадцать тысяч обращений, и редкий сбой становится виден. То есть не хватает не трафика вообще, а трафика внутри окна, за которое надо принять решение.
И одно различение, без которого путаются даже опытные команды: canary — не продуктовый эксперимент, хотя техника раскатки на процент у них общая. Canary отвечает на вопрос «работает ли новая версия так же хорошо, как старая» — её цель увидеть поломку и откатиться, метрики техничные (ошибки, задержка, насыщение), длительность минуты и часы, а нормальный исход — «разницы нет, катим дальше». Эксперимент отвечает на вопрос «какой вариант лучше для пользователя» — его цель измерить разницу, метрики продуктовые (конверсия, удержание, средний чек), длительность недели, и нормальный исход — статистически значимое различие. Отсюда практическое следствие: заканчивать эксперимент раскаткой победившего варианта на всех — правильно, а принимать продуктовое решение по пятнадцатиминутной ступени canary — нет, потому что за это время не наберётся выборки ни по одной продуктовой метрике. И наоборот: ждать неделю, чтобы понять, не выросла ли доля ошибок, — это не осторожность, а неделя выката ради того, что видно за полчаса.
Зеркалирование трафика — проверка без риска для пользователя
Проблема: canary уже показывает новую версию настоящим пользователям, пусть и одному проценту. Иногда даже этого нельзя: переписали расчёт цены, и ошибка в нём — это неправильные счёта, а не просто пятисотая ошибка.
Решение: копию боевого трафика отправляют в новую версию, а её ответы выбрасывают. Пользователь получает ответ от старой версии и вообще не участвует в проверке. Новая версия при этом работает на настоящих запросах: с настоящими данными, настоящим их разнообразием и настоящей нагрузкой.
Что это даёт такого, чего не дают тесты: настоящие запросы содержат то, чего нет ни в одном наборе тестовых данных — пустые поля, которых «не бывает», старые форматы, аномально большие корзины, нагрузку с настоящей формой. Половина ошибок, которые ловит canary, ловится зеркалированием — только без пострадавших.
Что здесь обязательно учесть. Побочные действия удваиваются. Если новая версия при обработке запроса пишет в базу, отправляет сообщение или дёргает платёжного провайдера — зеркалированный запрос сделает это второй раз. Поэтому зеркалируют либо только чтение, либо пишут в отдельную базу и отправляют в отдельную очередь, либо новая версия в этом режиме побочные действия не выполняет вовсе (её запускают с настройкой «только считать»). Пропустить этот пункт — значит списать деньги дважды. Нагрузка тоже удваивается — на соседей, а не только на ваш сервис: зеркалированные запросы ходят в те же базы и те же внешние службы. Ответы надо сравнивать: смысл упражнения в том, чтобы увидеть, где новая версия ответила иначе, — иначе вы просто прогреваете её впустую. Сравнивают долю расхождений и сами расхождения по образцам. Понадобится инструмент: зеркалирование умеет сетевая прослойка (service mesh) и часть обратных прокси одной настройкой.
Когда брать: расчёты и преобразования данных, переписанный сервис с сохранением контракта, миграция на другое хранилище (сравниваете ответы старого и нового). Когда не брать: запросы с побочными действиями, которые нельзя отключить; удвоенная нагрузка, которую не выдержат соседи; сервис, где ответы законно различаются между вызовами (вернул время, случайный порядок, выборку по свежести) — сравнивать будет нечего.
Feature flags — деплой и релиз как разные события
Проблема: иногда код готов, но включать его для всех страшно. Или нужно проверить фичу на 10% пользователей без изменений в инфраструктуре.
Решение: развести два события — доставку кода и его включение. Код едет в продакшн выключенным, а включается позже — без деплоя. Флаг можно включить для всех, для процента, для конкретного сегмента (например, только бета-пользователи).
Логика одна во всех языках: взять идентификатор (пользователя, заказа), спросить сервис флагов, выбрать путь:
// Java — Unleash
UnleashContext ctx = UnleashContext.builder()
.userId(customerId)
.build();
if (unleash.isEnabled("new-pricing", ctx)) {
return newPricingPolicy.calculate(order);
}
return currentPricingPolicy.calculate(order);
// Go — Unleash
import unleashctx "github.com/Unleash/unleash-client-go/v3/context"
ctx := unleashctx.Context{UserId: customerId}
if unleash.IsEnabled("new-pricing", unleash.WithContext(ctx)) {
return newPricingPolicy.Calculate(order)
}
return currentPricingPolicy.Calculate(order)
// Node/TypeScript — Unleash
const enabled = isEnabled("new-pricing", { userId: customerId });
if (enabled) {
return newPricingPolicy.calculate(order);
}
return currentPricingPolicy.calculate(order);
# Python — Unleash
if client.is_enabled("new-pricing", {"userId": customer_id}):
return new_pricing_policy.calculate(order)
return current_pricing_policy.calculate(order)
Что флаги меняют в процессе:
- деплой становится скучным — выключенный код не несёт риска;
- включение обратимо за секунды;
- постепенная раскатка по пользователям без инфраструктуры весов.
Главная ловушка — вечные флаги. Флаг без даты удаления через год становится частью архитектуры, которую никто не понимает. Заведите правило: у каждого флага есть владелец и дата, когда он должен исчезнуть. После полной раскатки — удалить.
Для начала не нужен специальный сервис: таблица в PostgreSQL с кешем вполне достаточна. Unleash, LaunchDarkly и другие решения нужны, когда таблицы начинает не хватать.
Три технические вещи, о которых узнают обычно после первой аварии.
Где лежит значение и кто его знает. Флаг в переменной окружения — это не флаг: чтобы его переключить, нужен перезапуск, то есть выкат, а мы разделяли выкат и включение ровно для того, чтобы его не делать. Значение должно жить снаружи процесса: в таблице, в сервисе флагов, в хранилище настроек. Дальше важно, как приложение его узнаёт. Спрашивать при каждом вызове — дорого; правильная схема — приложение держит локальную копию всех флагов в памяти и обновляет её раз в несколько секунд (или получает уведомления об изменении). Отсюда и честный ответ на вопрос «за сколько включается флаг»: не мгновенно, а за интервал обновления, и об этом надо знать, когда флагом гасят аварию.
Что будет, если источник флагов недоступен. Это самое опасное место: сервис флагов становится зависимостью на горячем пути, и его недоступность не должна ронять ваш сервис. Отсюда два правила. Первое — у каждого флага есть безопасное значение по умолчанию, которое применяется, когда узнать настоящее не удалось; по умолчанию это «выключено», то есть старое поведение, а для флага, которым что-то отключают, наоборот. Второе — локальная копия переживает недоступность: сервис продолжает работать с последними известными значениями и складывает их на диск, чтобы перезапуск во время аварии не сбросил всё к умолчаниям. Проверяется это учением: погасите источник флагов на стенде и посмотрите, что сервис жив.
Проверка флага стоит времени. Вызов «спросить, включён ли флаг» обязан быть обращением в память: сравнение строки и, если флаг раскатывается по процентам, вычисление хеша от идентификатора. Если в этом месте окажется сетевой вызов или запрос в базу, вы добавили сетевую задержку на каждый запрос — а флаги любят ставить как раз в самых горячих местах. Признак, что всё сделано правильно: проверка флага не видна в трассировке запроса.
Совместимость схемы: как менять базу, пока работают две версии
«Схема должна понимать обе версии» звучит как принцип, а нужна последовательность шагов, потому что все дорогие аварии выката — это нарушенный порядок. Разберём на самом частом: переименовать колонку phone в phone_number.
Соблазн сделать это одной миграцией — ALTER TABLE ... RENAME COLUMN. Она проходит мгновенно и ломает всё: старая версия, которая ещё работает (при rolling — минуты, при откате — часы), ищет колонку phone и получает ошибку на каждом запросе. Откат кода не помогает: колонки больше нет.
Правильно это делается за шесть шагов и три выката, и ни один из них не ломает работающую версию.
- Добавить новое, не трогая старое. Миграция создаёт колонку
phone_number, допускающую пустое значение. Старый код её не видит и работать продолжает. Мгновенно и безопасно. - Писать в оба места. Новая версия кода при записи заполняет и
phone, иphone_number, а читает пока из старой колонки. Выкатывается обычным rolling: обе версии живут спокойно, потому что старая пишет только вphone, а новая — в обе. - Перелить накопленное. Отдельная задача заполняет
phone_numberу старых строк — порциями, с паузами, и её можно останавливать и запускать заново. Это не миграция: она идёт минуты или часы и не держит блокировок на всю таблицу. В конце — проверка, что незаполненных строк не осталось. - Переключить чтение. Следующая версия читает из
phone_number, а писать продолжает в оба. Вот здесь и появляется страховка: если новая версия окажется плохой, откат к предыдущей просто вернёт чтение изphone, где данные на месте. - Перестать писать в старое. Ещё один выкат: запись только в
phone_number. С этого моментаphoneотстаёт от жизни, и обратной дороги без потери данных больше нет — поэтому этот шаг делают, когда предыдущая версия уже точно никуда не откатится. - Удалить старое. Отдельной миграцией, отдельным выпуском, спустя время (обычно выпуск-два):
DROP COLUMN phone. Если поторопиться и сделать это вместе с шагом 5, откат кода снова упрётся в отсутствующую колонку.
Та же схема работает для любого несовместимого изменения. Удалить колонку: сначала перестать её читать, выкатить, потом перестать писать, выкатить, и только потом удалять, — иначе работающая версия падает на INSERT без этой колонки. Сузить тип или добавить обязательность: сначала новая колонка или проверка без принудительного применения, потом привести данные, потом включить требование. Разделить таблицу: то же двойное чтение и двойная запись, просто дольше.
Два числа, которые стоит держать в голове. Между шагами нужен зазор, а не последовательность выкатов в течение часа: шаг 5 безопасен только тогда, когда версия из шага 4 отработала достаточно, чтобы вы перестали рассматривать откат к шагу 3, — обычно это день или выпуск. И срок жизни двойной записи: пара недель, которую никто не заводит специально, превращается в полгода, и в проекте остаются колонки-двойники, про которые никто не помнит, какая из них правда. Поэтому шаги с 4 по 6 заводят задачами сразу, вместе с шагом 1, а не «потом уберём».
Признак, что вы всё сделали верно, один: в любой момент последовательности можно откатить код на предыдущую версию, ничего не делая с базой. Если для отката нужна обратная миграция — порядок нарушен.
Сравнение стратегий
| Rolling | Blue-green | Canary | + Feature flags | |
|---|---|---|---|---|
| Радиус ошибки | Все, постепенно | Все, мгновенно | Малый процент | Управляемый сегмент |
| Скорость отката | Минуты | Секунды | Автоматика | Секунды |
| Дополнительные затраты | Запас на время выката | Двойная ёмкость | Маршрутизация + метрики | Дисциплина флагов |
| Когда применять | По умолчанию | Дорогая ошибка | Большой трафик, зрелые метрики | Всегда полезны |
Практичная связка для большинства команд: rolling + feature flags. Canary — когда трафик и метрики дозрели. Blue-green — точечно для самых критичных систем.
Как выбрать и когда не брать
Таблица сравнивает, а выбирать по ней всё равно трудно. Правило короткое: начните с rolling и флагов и добавляйте остальное только под названную причину. Причина формулируется как вопрос к себе, и он один: что случится, если новая версия окажется плохой?
- Ответ «увидим по метрикам и откатимся за минуты, потерь нет» — rolling, и ничего больше не нужно.
- «Минуты отката — это уже деньги или испорченные данные» — blue-green: платите двойной ёмкостью за откат переключателем.
- «Поломка может быть такой, что по стенду и препроду её не видно, а трафика хватит, чтобы увидеть быстро» — canary.
- «Ответы можно сравнить, а пользователю ошибку показывать нельзя» — зеркалирование.
- «Включение надо развести с выкатом по времени, по сегментам или уметь выключить мгновенно» — флаги, они складываются с любой из стратегий.
Обратная сторона, которую в таблицах не пишут, — когда стратегию брать не надо.
Rolling не берут для фоновых обработчиков, где две версии одновременно разбирают одну очередь по-разному, и там, где новая версия несовместима со старой по данным на уровне, который не лечится совместимой миграцией, — тогда короткий простой честнее.
Blue-green не берут для сервиса с состоянием на локальном диске (второе окружение его не увидит, а переключение потеряет), при нехватке мощности на двойную ёмкость и там, где переключение «целиком» означает обрыв долгоживущих соединений — сначала научитесь их закрывать корректно.
Canary не берут при малом трафике: при десяти запросах в секунду один процент — это один запрос в десять секунд, и за час наблюдения не набирается выборки ни для одного вывода. Не берут и без метрик с меткой версии. И не берут для операций, которые случаются редко и не попадут в окно наблюдения.
Флаги не берут внутри библиотек, которыми пользуются другие команды: флаг становится частью контракта, и выключить его нельзя, не зная, кто на нём. Не берут для того, что можно просто выкатить: флаг на каждое изменение — это проект, в котором через год никто не понимает, какие ветки кода живые. И не берут как замену отката: флаг гасит ровно то, что за ним спрятано, а ошибка может оказаться в коде, который исполняется до проверки флага.
Зеркалирование не берут для запросов с побочными действиями, которые нельзя отключить, и когда соседи не выдержат удвоенного потока.
Типичные ошибки
Canary без формальных критериев. «Посмотрим в Grafana» — это не canary, это надежда. Если нет порогов для автоматического продвижения или отката, canary не работает как защита.
Blue-green с несовместимой миграцией схемы. Переключили на green, мигрировали схему — старый blue больше не работает с новой схемой. Откат за секунды превращается в восстановление из резервной копии.
Вечные флаги. Сотня флагов, переплетённых условиями, сложнее любого монолита. Флаги — временный инструмент.
Глубже: откат и разбор неудачного выката: что, в каком порядке, кто решаетрасширенное
«Откат за секунды» обещан трижды, а процедуры нет, и в реальном инциденте первый вопрос не «как», а «что именно откатывать». У выката четыре слоя, и откатывают их в обратном порядке зависимости.
Флаг. Если новое поведение спрятано за флагом, откат это выключение флага: секунды, без выката, без потери данных. Поэтому рискованное прячут за флаги заранее, о чём раздел выше. Конфигурация. Изменённая настройка, лимит, адрес соседа: откатывается возвратом коммита в репозитории конфигурации. Образ. Предыдущая версия по дайджесту, тем же механизмом выката в обратную сторону: у rolling это минуты, у blue-green переключение. Данные. Не откатываются почти никогда: миграция совместима с предыдущей версией, поэтому старый код работает с новой схемой, а вот данные, записанные новой версией в новом формате, старый код обязан понимать (терпимый читатель). Если новая версия успела записать то, что старая не прочитает, откат образа невозможен, и остаётся исправление вперёд.
Кто решает. Критерии отката записаны до выката: доля ошибок выше порога, задержка выше бюджета, падение конверсии на ступени canary, и при их срабатывании откат делает дежурный или автоматика, не собирая совещание. Правило: сначала откат, потом разбор; попытка «быстро починить на живом» в момент инцидента почти всегда длиннее отката. Владелец выпуска объявляет откат в канале инцидента, чтобы никто параллельно не выкатывал ещё раз.
Когда катят вперёд. Откат невозможен (схема или данные ушли вперёд), откат дольше исправления (однострочная правка конфигурации), или откат вернёт другой инцидент, который новая версия чинила. Исправление вперёд идёт по короткой процедуре: минимальный диф, ревью дежурного, полный конвейер без пропуска проверок; «горячий фикс мимо конвейера» это способ получить второй инцидент.
Разбор после. Выкат, потребовавший отката, это событие для разбора, как инцидент: что не поймали проверки, что не показал препрод, почему критерий сработал поздно; итог это новый тест или новая проверка в конвейере, а не «быть внимательнее», о чём статья про разбор аварий в разделе распределённых систем.
Глубже: чем мерить canary: выдержка ступени и сравнение с контрольной группойрасширенное
Canary описан как механизм, а без измерения он превращается в rolling с лишними шагами. Измерение состоит из трёх частей.
Метрики с меткой версии. Все ключевые сигналы (доля ошибок, задержка по процентилям, насыщение, и одна-две бизнес-метрики вроде доли завершённых оформлений) снимаются с меткой версии, чтобы график новой версии лежал рядом со старой, а не растворялся в общем. Журналы и трассы получают ту же метку, и по ней читают ошибки только новой версии.
Контрольная группа. Сравнивать новую версию нужно не с историей, а с такой же долей старой версии в тот же момент: одна новая копия против одной старой при одинаковом трафике. Иначе всплеск нагрузки или сбой соседа выглядят как вина новой версии. Критерий это разница: доля ошибок новой не выше старой более чем на заданную величину, p99 не выше более чем на столько-то, и на выборке достаточной длины, потому что на десяти запросах в минуту статистики нет. Инструменты автоматического анализа (у Argo Rollouts это шаблоны анализа с запросами к Prometheus) считают это сами и останавливают выкат при провале.
Выдержка ступени. Каждая ступень (1 %, 10 %, 50 %) держится заданное время до перехода к следующей: столько, чтобы набрать выборку и чтобы проявились медленные проблемы, утечка памяти или деградация под кешем. Обычно десять-тридцать минут на ступень, для редких операций дольше; ступень, которая длится секунды, ничего не проверяет. Автоматический откат при провале критерия и ручное подтверждение перед последней ступенью, если сервис критичный.
Чего не покрывает canary. Ошибки, которые проявляются у одного процента пользователей, на одном проценте трафика не видны статистически; редкие сценарии (месячный отчёт) не попадают в окно; изменения схемы данных видны всем версиям сразу. Для этого остаются флаги с включением по группам пользователей и тесты до выката.
Коротко
- Стратегии релиза отличаются радиусом ошибки и скоростью отката. Rolling — реплики меняются по одной; откат занимает минуты; выбор по умолчанию.
- Blue-green — два окружения, трафик переключается целиком; откат за секунды; цена — двойная ёмкость.
- Canary — новая версия получает малый процент трафика и растёт по метрикам; требует маршрутизации и надёжных метрик. Canary мерят метриками с меткой версии против контрольной группы той же доли в тот же момент, с выдержкой ступени в десятки минут и автоматическим откатом по разнице ошибок и задержки.
- Feature flags — деплой и включение функции разделены; флаги включают/выключают за секунды без деплоя.
- При rolling и canary две версии работают одновременно — схема БД и контракты должны понимать обе.
- Флаги — временный инструмент: заводить с датой удаления и убирать после раскатки.
- Откатывают по слоям в обратном порядке: флаг, конфигурация, образ по дайджесту; данные не откатывают, а держат совместимыми; критерии записаны заранее, решает дежурный, сначала откат, потом разбор; вперёд катят, когда откат невозможен или дольше.
- Выкат роняет запросы не из-за стратегии, а из-за остановки: копия получает трафик несколько секунд после начала остановки, поэтому сначала пауза и вывод из оборота, потом остановка процесса, а новая копия объявляет готовность после прогрева.
- Несовместимое изменение схемы делается за шесть шагов и три выката: добавить, писать в оба, перелить, переключить чтение, перестать писать в старое, удалить, — критерий верности в том, что код откатывается без обратной миграции.
- Зеркалирование трафика проверяет новую версию на боевых запросах без риска для пользователя, но удваивает нагрузку и требует отключить побочные действия.
Что почитать дальше
- Деплой в Kubernetes — механика rolling update и совместимые миграции.
- Принципы конвейера — деплой и релиз как разные события.
- Ветки и релизный цикл — как код доезжает до этих стратегий.