awesome-everything EN
↑ Обратно к восхождению

Деплой и инфра

Capstone: деплой — это цепочка, а авария живёт в стыке

Суть Каждая стадия деплоя верна по отдельности, а релиз падает там, где две собираются неправильно: rolling update без readiness-пробы, blue-green-флип поверх несовместимой миграции, L4 без draining. Клей — health-чеки, expand-contract и метрики раскатки.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 17 min

Все блоки в диаграмме релиза были зелёными. Образ собрался и запушился. K8s-Deployment применился без ошибок. Rolling update отрапортовал успех за 40 секунд. И тут запищал пейджер: 30% запросов отдают 502 две минуты. Никто не выкатил баг. Образ был в порядке, манифест в порядке, миграция в порядке — по отдельности. У Deployment не было readinessProbe, поэтому k8s объявлял каждый новый под «готовым» в тот миг, когда стартовал процесс контейнера, ещё до того, как приложение открыло пул к БД. Rolling update послушно перенаправил трафик на поды, которые были живыми, но ещё не обслуживали. Авария не жила ни в одной стадии. Она жила в стыке между двумя верными.

Пайплайн — это один собранный объект, а не семь шагов

Весь трек деплоя шёл по одной стадии за раз: собрать лёгкий multi-stage образ, запушить в реестр, объявить k8s-объекты, выбрать стратегию раскатки, поставить перед сервисом балансировщик, инжектить секреты, закодировать всё как infrastructure-as-code. Озарение capstone в том, что ни одна из этих стадий сама по себе не выкатывает софт. Релиз — это композиция всех их, а у композиции есть эмерджентные режимы отказа, которые не ловят тесты ни одной отдельной стадии.

Думай об этом как о цепочке, где каждое звено передаёт артефакт следующему:

СтадияПроизводитСтык, который она может сломать
Multi-stage сборкаНеизменяемый образ + digestBuild-зависимости утекают в runtime → раздутость, CVE
Push в реестрТегнутый артефакт, готовый к pullИзменяемый тег :latest → какая сборка реально live?
k8s-объектыDeployment + Service + Ingress + Config/SecretНет пробы → «готов» врёт раскатке
Стратегия раскаткиСдвиг трафика старое → новоеФлип поверх несовместимой схемы → старый код 500-ит
БалансировщикТрафик клиента → здоровые бэкендыL4 не умеет draining → запросы в полёте обрываются
Секреты при деплоеКонфиг инжектится в runtimeСекрет вшит в образ → утёк + не ротируется
IaCВоспроизводимое состояние кластераDrift → окружение, что тестил ≠ что выкатил

Каждая колонка «стык» — это реальная авария, которую кто-то выкатил. Каждая предполагает, что предыдущая стадия отработала — и всё равно сломалась, потому что контракт между стадиями никто не проверял.

Health-чеки — это клей между «запущен» и «обслуживает»

Самый несущий кусок клея — readiness-проба, потому что она задаёт контракт, на который опирается раскатка. Вся работа rolling update такая: поднять новый под, дождаться, пока он готов, затем погасить старый, по maxUnavailable за раз. Слово «готов» делает тут гигантскую работу. Без readinessProbe k8s рапортует под готовым в тот миг, когда стартовал главный процесс контейнера, — а это до того, как прогрелась JVM, открылся пул соединений, прогрелись кэши. Rolling update видит зелёное, добавляет под в эндпоинты Service, и трафик льётся в процесс, который отдаёт 502 / connection-refused.

Фикс — проба, которая реально дёргает зависимости, плюс surge-арифметика, которая никогда не падает ниже ёмкости:

readinessProbe:
  httpGet: { path: /healthz/ready, port: 8080 }
  initialDelaySeconds: 5
  periodSeconds: 5
strategy:
  rollingUpdate:
    maxUnavailable: 0   # никогда ниже желаемого числа реплик
    maxSurge: 1         # добавить новый под до удаления старого

Две пробы делают две разные работы, и их смешение — отдельная авария. Проба readiness управляет трафиком — провалил её, и под убирают из Service, но оставляют работать. Проба liveness управляет рестартами — провалил её, и kubelet убивает и пересоздаёт контейнер. Заведи медленную зависимость (флапающую БД) в liveness-пробу — и получишь шторм рестартов: каждый под, который не достучался до БД, убивают, что делает проблему БД хуже, а не лучше. Liveness должна проверять «я в дедлоке?», а не «весь ли мой мир здоров?».

Почему это работает

Тонкая ловушка: проба на path: /healthz, которая отдаёт 200, если поднят веб-сервер, не говорит раскатке ничего полезного, потому что веб-сервер поднимается почти мгновенно. Проба обязана падать, пока то, что реально нужно запросу — пул к БД, соединение с кэшем, downstream-клиент — ещё инициализируется. Health-чек, который не может покраснеть во время старта, — это украшение, а не клей.

Стратегия раскатки и миграция — это одно решение

Самый дорогой стык во всей цепочке — между стратегией раскатки и базой данных, потому что флип балансировщика обратим, а изменение схемы обычно нет. Представь blue-green деплой: blue live, green — новая версия, ты флипаешь LB и мгновенно откатываешься, если green ведёт себя плохо. Теперь допустим, релиз green «прибрал» схему — удалил колонку, которую blue всё ещё читает. Флип проходит, green обслуживает нормально. Потом green кидает ошибку, и ты откатываешься на blue — и blue немедленно 500-ит на каждом запросе, потому что нужной колонки больше нет. Твой «мгновенный откат» теперь односторонняя авария.

Дисциплина, которая делает раскатку и миграцию композируемыми, — expand-contract (он же «parallel change»): никогда не выкатывай изменение схемы, несовместимое с кодом, который сейчас работает. Ты разбиваешь одно логическое изменение на отдельные деплои, каждый держит N-1 совместимость (новая схема работает со старым кодом и наоборот):

  1. Expand — добавь новую колонку/таблицу; оставь старую. Обе версии работают.
  2. Migrate — перелей данные в новую форму; пиши из приложения в обе (dual-write).
  3. Contract — только после того, как весь трафик на новом коде, удали старую колонку. Отдельный, более поздний деплой.

Цена реальна: переименование колонки, которое «должно» быть одной миграцией, становится тремя деплоями, размазанными по релизам, плюс dual-write код, который надо не забыть удалить. Выигрыш в том, что любое промежуточное состояние безопасно для отката, поэтому твоя стратегия раскатки сохраняет обратимость, которую обещала.

Выбери лучший вариант

Нужно переименовать горячую колонку в таблице на 200M строк, сохранив раскатки zero-downtime обратимыми. Выбери подход.

Draining и observability замыкают цепочку

Даже с пробами и безопасными миграциями cutover может убить запросы, которые уже были в полёте. Когда под завершается, k8s убирает его из эндпоинтов Service и шлёт SIGTERM — но эти два события гоняются, и LB может ещё пару мгновений роутить запросы на умирающий под. L7-балансировщик умеет draining: перестаёт слать новые запросы и ждёт настраиваемый таймаут (хороший дефолт — 1.5–2× твоего p99 времени запроса), пока завершатся те, что в полёте. Наивная L4-схема, которая просто рвёт на уровне соединения, может оборвать запрос посреди ответа. Сторона приложения обязана сотрудничать: ловить SIGTERM, перестать принимать новую работу, доделать запросы в полёте, затем выйти — всё внутри terminationGracePeriodSeconds (дефолт 30s), с preStop-паузой, чтобы покрыть гонку с удалением эндпоинта.

И единственная причина, по которой ты узнал, что авария readiness-пробы в Hook была про 502, а не про «успех», — это observability. Exit-код раскатки говорит, что манифест применился; он не говорит про error rate, p99 latency или saturation на новых подах. Несущий сигнал — сравнение golden-метрик новой версии с baseline старой во время раскатки — ровно то, что canary делает автоматически: сдвинуть 5% трафика, понаблюдать error rate и latency пару минут, продвинуть или авто-откатить по метрике, а не по возвращаемому значению команды деплоя.

Викторина

Rolling update рапортует успех за 40 секунд, но потом 30% запросов 502-ят две минуты. Образ, манифест и миграция по отдельности в порядке. Какая причина наиболее вероятна?

Викторина

Почему blue-green флип опасен в паре с миграцией, которая удаляет колонку?

Расставь шаги по порядку

Расставь безопасный релиз переименования колонки так, чтобы каждое промежуточное состояние было безопасным для отката:

  1. 1 Деплой 1 — Expand: добавь новую колонку, оставь старую на месте
  2. 2 Деплой 2 — выкати код, который dual-write пишет в обе колонки и backfill-ит существующие строки
  3. 3 Проверь, что все работающие поды на новом коде и читают новую колонку
  4. 4 Деплой 3 — Contract: удали старую колонку, раз её никто не читает
  5. 5 Убери теперь мёртвый dual-write код отдельным деплоем
Вспомните перед уходом
  1. 01
    Проведи коллегу через то, почему идеально верный rolling update всё равно может вызвать аварию, и какой один кусок конфигурации это предотвращает.
  2. 02
    Объясни expand-contract и почему именно он делает стратегию раскатки и миграцию базы безопасно композируемыми.
Итог

Деплой — это не семь независимых шагов; это один собранный объект, и аварии живут в стыках, где две по отдельности верные стадии встречаются под контрактом, который никто не проверял. Readiness-проба — это контракт между «процесс работает» и «приложение может обслуживать», и без неё безупречный rolling update роутит трафик на мёртвые поды. Стратегия раскатки и миграция базы — одно решение: обратимый флип LB в композиции с деструктивным, необратимым изменением схемы даёт состояние, на которое нельзя откатиться, и поэтому expand-contract — держать каждую схему N-1 совместимой через отдельные деплои expand, migrate и contract — это клей, который позволяет им сосуществовать. L7-балансировщик, который умеет draining, и приложение, которое обрабатывает SIGTERM внутри terminationGracePeriodSeconds, держат запросы в полёте живыми сквозь cutover, а метрики раскатки (error rate и p99 против baseline, как автоматически проверяет canary) — это то, что делает «успех» здоровьем, а не просто «применилось». Собери образ лёгким, запушь неизменяемый digest, объяви объекты, инжекть секреты в runtime и закодируй всё как IaC, чтобы окружение, которое ты тестил, было тем, что ты выкатил — тогда цепочка держится, потому что ты заинженерил стыки, а не только звенья.

Продолжить восхождение ↑Капстоун deployment: вопросы с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.