Деплой и инфра
Стратегии выката: размен радиуса поражения на стоимость ресурсов и скорость отката
Rolling update выкатывается в 14:00. Дашборды зелёные — Deployment рапортует «available», все реплики подняты. И тут 30% запросов начинают отдавать 502. Новые поды загрузились, Kubernetes пометил их ready в тот же миг, как стартанул контейнер, и трафик пошёл туда раньше, чем приложение подключилось к базе. Readiness-пробы не было. Выкат «успешно» зашёл прямо в тихий частичный отказ, и понадобилось 20 нервных минут, чтобы понять — причина в деплое.
Четыре стратегии и чего стоит каждая
Релиз может принять ровно четыре формы, и выбор одной — это инженерный размен, а не дефолт.
Recreate убивает все старые инстансы, потом стартует новые. Просто, без пересечения версий — что важно, когда две версии действительно не могут сосуществовать (несовместимое in-place изменение схемы, singleton-джоба). Цена — downtime: окно, когда ничего не обслуживает. Допустимо только для не-HA сервисов или окон обслуживания.
Rolling update — дефолт Kubernetes. Заменяет поды постепенно — подними несколько новых, слей несколько старых, повтори — так сервис никогда не падает полностью. Настраивается двумя ручками: maxSurge (сколько лишних подов сверх желаемого количества может существовать в процессе выката) и maxUnavailable (сколько может отсутствовать). Оба по умолчанию 25%.
Blue-green держит два полных окружения. «Blue» обслуживает прод, пока «green» получает новую версию, полностью развёрнутую и прогретую; затем ты атомарно переключаешь роутер. Откат мгновенный — переключи обратно. Цена — примерно двойные ресурсы во время переключения, плюс проблема совместимости с базой (ниже).
Canary направляет небольшой срез трафика — обычно 5%, потом 25%, потом 50% — на новую версию, смотрит метрики на каждом шаге и наращивает только если error rate и latency остаются здоровыми. Самый маленький радиус поражения, но нужны traffic-shaping и реальная observability. Инструменты вроде Argo Rollouts и Flagger автоматизируют шаги и метрические гейты (progressive delivery).
Rolling update: readiness-проба не опциональна
Отказ из Hook — каноничный провал rolling update. Kubernetes отправляет трафик в новый под в тот момент, когда он ready — а по умолчанию под считается ready, как только стартует процесс контейнера. Если приложению нужны три секунды, чтобы открыть соединения с БД и прогреть кэш, это трёхсекундное окно на каждый под, в котором ты направляешь живой трафик в процесс, который не может обслуживать.
Корректная readiness-проба закрывает это окно: Kubernetes придерживает трафик, пока проба не пройдёт (и не истёк minReadySeconds). Без неё maxUnavailable: 25% тебя не защищает — четверть мощности может быть одновременно «ready» и мёртвой. Безопасный для сеньора конфиг для zero-downtime: maxSurge: 1, maxUnavailable: 0 плюс настоящая readiness-проба — никогда не падать ниже полной мощности и никогда не слать трафик в под, который не доказал, что может отвечать.
Почему это работает
Readiness-проба и liveness-проба — не одно и то же, и путаница вызывает отказы. Liveness перезапускает под, который считает мёртвым — наведи её на медленную зависимость, и икота бэкенда вызовет шторм рестартов. Readiness лишь убирает под из ротации балансировщика. Во время выката трафик гейтит именно readiness; liveness-проба, делающая двойную работу, радостно поубивает поды, которые были просто заняты.
Blue-green: мгновенный откат, но база не переключается
Главный козырь blue-green — атомарное переключение и мгновенный откат, который оно даёт. Ловушка в том, что ты переключаешь приложение атомарно, но база общая — она не переключается. В день, когда green выкатывает миграцию, дропающую или переименовывающую колонку, blue (всё ещё твоя цель отката) теперь сломан против живой схемы. Переключишь обратно — упадёт. Твой «мгновенный откат» испарился ровно тогда, когда он нужен.
Фикс — паттерн expand-contract (parallel change): никогда не делай ломающее изменение схемы за один шаг.
| Фаза | Действие со схемой | Почему обе версии выживают |
|---|---|---|
| Expand | Добавь новую колонку/таблицу; старую оставь | Старый код игнорирует новое поле; новый читает оба — аддитивное изменение обратносовместимо |
| Migrate | Бэкфилл данных; dual-write старое + новое | Обе формы заполнены, поэтому откат к старому коду всё ещё находит свои данные |
| Contract | Дропни старую колонку — в более позднем деплое | Только после того, как старая версия полностью выведена и отката к ней не будет |
Это и значит «обратносовместимые изменения схемы» на практике, и это применимо к rolling и canary тоже — любая стратегия с двумя живыми версиями приложения требует схему, которую читают обе.
Выбор: радиус поражения vs стоимость ресурсов vs скорость отката
Универсально лучшей стратегии нет; ты взвешиваешь три оси. Радиус поражения — скольких пользователей задевает плохой релиз до того, как ты его поймал: у canary самый маленький, у recreate — тотальный. Стоимость ресурсов — blue-green платит за два полных окружения; canary и rolling переиспользуют один пул; recreate дешевле всех. Скорость отката — blue-green и canary почти мгновенны (переключи роутер / уведи вес в ноль); rolling приходится катить назад под за подом; recreate — это второй downtime.
Решающий фактор — обычно качество твоей observability. Крошечный радиус поражения canary бесполезен, если ты не можешь по метрикам понять, что 5%-й срез падает — ты накатишь его до 100% вслепую. Без крепких дашбордов и алертов на основе SLO хорошо настроенный rolling update с readiness-пробой бьёт canary, который ты не можешь прочитать.
Платёжный API на Kubernetes выкатывает рискованный рефакторинг. У тебя есть дашборды Prometheus и SLO-алерты, достаточная ёмкость кластера и нужно минимальное возможное влияние на пользователей, если что-то пойдёт не так. Выбери стратегию выката.
Rolling update рапортует «available», но ~30% запросов отдают 502 сразу после деплоя. Какая самая вероятная корневая причина?
Ты используешь blue-green и хочешь, чтобы откат оставался безопасным. Релиз green переименовывает колонку. Что делать?
Расставь шаги безопасного canary-выката рискованного изменения:
- 1 Выкати обратносовместимое (expand) изменение схемы, чтобы старая и новая версии обе работали
- 2 Направь 5% трафика на новую версию
- 3 Следи за error-rate и latency против SLO на этом шаге
- 4 Если здорово — наращивай 25% → 50% → 100%; если нет — верни вес в 0 (откат)
- 5 Когда полностью на новой версии и стабильно — contract: дропни старую схему в более позднем деплое
- 01Rolling update рапортует успех, но пользователи видят периодические 502. Объясни почему и какое одно изменение это чинит.
- 02Почему «мгновенный откат» blue-green ломается, когда релиз меняет схему, и как expand-contract его восстанавливает?
Четыре формы выката, один размен. Recreate прост, но берёт downtime — годится только для не-HA сервисов. Rolling update — дефолт Kubernetes, настраивается через maxSurge и maxUnavailable (оба по умолчанию 25%), и он безопасен только с корректной readiness-пробой: без неё трафик идёт в поды, которые стартанули, но обслуживать не могут, давая «успешный» деплой прямо в тихий частичный отказ. Blue-green даёт атомарное переключение и мгновенный откат ценой двойных ресурсов — но общая база не переключается, поэтому ломающая миграция убивает твою цель отката, если не использовать expand-contract (добавь, dual-write, дропни позже). У canary самый маленький радиус поражения — 5% → 25% → 50% с гейтом по метрикам, автоматизируется через Argo Rollouts или Flagger — но он хорош ровно настолько, насколько хороша observability, читающая canary-срез. Выбирай, взвешивая радиус поражения против стоимости ресурсов против скорости отката, и помни несущий пререквизит под всеми четырьмя: health-чеки, план отката и обратносовместимые изменения схемы.