Деплой без простою на одному сервері: що насправді потрібно
Більшість статей про деплой без простою починаються з кластера на п’ять нод. Але 90% продуктів живуть на одному сервері з docker compose, і саме там простій болить найбільше — бо нікому підхопити трафік. Хороша новина: на одному сервері zero-downtime теж досяжний, і головна робота там не в оркестраторі, а в тому, як ви пишете міграції.
Чому падає навіть простий деплой
Класичний сценарій: docker compose up -d --build. Старий контейнер зупиняється, новий піднімається за 8–20 секунд, поки Nuxt прогріває свій кеш. Усі запити в цьому вікні отримують 502 від nginx. Якщо в цей момент користувач відправляв форму — заявка втрачена, і ви про це ніколи не дізнаєтесь.
Друга причина гірша: міграція. Ви перейменували колонку, застосували міграцію, і поки новий образ збирається, старий код продовжує писати в колонку, якої вже немає. Помилки в логах, дані втрачені.
Міграції, сумісні в обидва боки
Це найважливіша частина, і вона не про інфраструктуру. Правило одне: міграція має працювати і зі старим кодом, і з новим. З цього випливає expand/contract у три релізи:
- Expand. Додаєте нову колонку, nullable, з дефолтом. Старий код її не бачить і не ламається. Новий код пише в обидві.
- Migrate. Бекфіл даних фоновою задачею, не в міграції. Мільйон рядків у міграції — це блокування таблиці й той самий простій, тільки під іншою назвою.
- Contract. Наступним релізом прибираєте стару колонку, коли жоден інстанс уже її не читає.
Три деплої замість одного здаються надмірними, поки ви вперше не відкотите реліз о другій ночі. Перейменування колонки одним кроком — це деплой, який неможливо відкотити: міграцію вниз ніхто ніколи не тестував.
Міграція, яку не можна відкотити, — це не міграція, а односторонні двері. Проходьте в них свідомо, а не випадково.
Правильний порядок кроків
Порядок має значення більше, ніж інструмент:
1. build зібрати новий образ (старий продовжує обслуговувати трафік)
2. migrate застосувати сумісні міграції
3. start підняти новий контейнер поруч зі старим
4. wait чекати, поки /health/ready віддасть 200
5. switch перемкнути nginx upstream
6. drain дати старому 10 секунд дообслужити запити
7. stop зупинити старий контейнерКроки 3–5 — це те, що docker compose сам не робить. Найпростіший спосіб отримати їх без Kubernetes: два сервіси в compose (app_blue і app_green) і nginx, який дивиться на upstream з файлу, що підмінюється й перечитується через nginx -s reload. Reload у nginx не рве наявні з’єднання — старі воркери дообслуговують, нові беруть нову конфігурацію.
Health-check, який каже правду
Найпоширеніша помилка — health-check, який повертає 200 одразу після старту процесу. Тоді ви перемикаєте трафік на контейнер, який ще не має з’єднання з базою, і отримуєте той самий простій, тільки тепер він виглядає як 500 замість 502.
Розділяйте два ендпоїнти:
/health/live— процес живий. Якщо не відповідає, контейнер треба перезапустити./health/ready— з’єднання з базою є, Redis відповідає, черга доступна. Якщо не відповідає, контейнер треба прибрати з балансувальника, але не перезапускати.
Плутанина між ними — причина каскадних падінь: база гальмує, readiness падає, оркестратор перезапускає всі контейнери, і при старті вони одночасно б’ють у базу, яка вже й так гальмувала.
Прогрів
SSR-застосунок після старту компілює маршрути на першому запиті, а кеш контенту порожній. Перший користувач після деплою отримує сторінку за 3 секунди замість 300 мілісекунд. Формально простою немає — фактично сайт повільний рівно тоді, коли на нього дивляться.
Лікується це десятьма рядками: після того як контейнер відповів на readiness, але до перемикання трафіку, зробіть кілька запитів до найважливіших сторінок і до ендпоїнтів, які наповнюють кеш. Прогрів кешу — це частина деплою, а не оптимізація на потім.
Відкат, який справді працює
Відкат — це не «повернути попередній образ». Це «повернути попередній образ і переконатися, що він працює з поточною схемою бази». Саме тому expand/contract важливіший за скрипт деплою: якщо міграції сумісні в обидва боки, відкат — це одна команда. Якщо ні — це відновлення з бекапу.
Мінімум, який варто мати: тег образу з SHA коміту (не latest), збережений попередній тег, і смоук-тест після перемикання. Якщо смоук-тест червоний — перемикаємо upstream назад, поки старий контейнер ще живий. Тому крок 7 і стоїть в кінці.
Що зробити цього тижня
- Розділіть liveness і readiness, якщо у вас один
/health. - Перевірте останні п’ять міграцій: чи пережив би їх старий код?
- Додайте прогрів після старту й смоук-тест після перемикання.
- Замініть
latestна тег із SHA коміту.
Це чотири невеликі зміни, і разом вони дають більше, ніж перехід на оркестратор, який доведеться ще й обслуговувати.
- Zero-downtime визначається міграціями, а не оркестратором
- Expand / migrate / contract — три релізи замість одного ризикованого
- Liveness і readiness — різні ендпоїнти з різними наслідками
- Прогрів кешу — частина деплою, інакше перший користувач бачить повільний сайт
Так. Потрібні два контейнери, які працюють паралельно кілька секунд, nginx з reload замість restart і readiness-check, який чекає на базу. Оркестратор дає це «з коробки», але сам механізм не залежить від нього.
Скрипт деплою з blue/green і прогрівом — приблизно день роботи. Привести міграції до expand/contract — це зміна звички команди, а не разова задача: перші кілька релізів здаються повільнішими, далі це стає нормою.
