Розгортання завершилося успішно, але сайт недоступний
На панелі показано статус «працює», а користувачі бачать помилку. Дізнайтеся, чому успішне розгортання та працездатність застосунку — це різні сигнали, і як зробити так, щоб зелене розгортання означало, що застосунок справді відповідає.
Є особливий різновид невдалого ранку, який починається із зеленої позначки. Розгортання завершилося успішно, але сайт недоступний, на панелі показано працює, а хтось надсилає вам скриншот із помилкою 502.
Це не рідкісний крайній випадок. Це передбачуваний результат того, що платформа повідомляє про одне, а вимірює інше. І це варто чітко розуміти, адже рішення полягає не в тому, щоб «перевіряти ретельніше», — потрібно змінити значення слова працює.
Три різні запитання, але один індикатор стану
Коли платформа повідомляє, що сервіс працює, вона може відповідати на будь-яке з таких запитань:
- Чи запустився контейнер? Процес існує і не завершив роботу.
- Чи відкритий порт? На очікуваній платформою адресі щось прослуховує з’єднання.
- Чи правильно відповідає застосунок? На запит надходить відповідь, яка означає, що застосунок готовий до роботи.
Це кардинально різні гарантії, і більшість інцидентів такого типу виникає через те, що панель відповідає на запитання 1, тоді як ви припускали відповідь на запитання 3.
Процес Node, який запускається, не може під’єднатися до бази даних і застрягає в циклі повторних спроб, безкінечно відповідає вимогам запитання 1. Він не аварійно завершився. Він ніколи не обслуговуватиме запит. Контейнер «працює» в усіх значеннях, які важливі для оркестратора.
Проміжок, у якому ховається збій
Небезпечне вікно виникає між моментами «нова версія запустилася» та «нова версія може працювати». У цей проміжок наївна платформа вже перенаправила трафік, оскільки вимірювала лише факт запуску.
Це гірше за звичайний збій, бо ускладнює відкат. Цикл аварійних завершень одразу привертає увагу: контейнер завершує роботу, перезапускається, знову завершує роботу, і платформа зрештою це помічає. Запуск із зависанням відбувається безшумно. Нічого не перезапускається, сповіщення не надходять, а попередню робочу версію зазвичай уже видалено.
Остання частина і є справжньою шкодою. Стара версія працювала нормально. Її видалили, бо нова запустилася, а запуск помилково сприйняли як працездатність.
Як працює справжня перевірка готовності
Рішення має структурний, а не процедурний характер. Трафік не слід перемикати, доки нова версія не відповість на запит.
У Dockup випуск відбувається так: нову версію збирають ізольовано, запускають паралельно з версією, яка обслуговує запити, а потім ставлять їй запитання. Лише після відповіді домен спрямовується на неї. Якщо відповіді немає, випуск зупиняється, а попередня версія продовжує обслуговувати запити — ніхто за межами вашої панелі навіть не дізнається, що було здійснено спробу розгортання.
Саме тому невдале розгортання в Dockup не є простоєм. Старий контейнер не видаляли, покладаючись на те, що новий працюватиме належним чином.
# The health gate is per-service configuration, not a platform default you inherit
dockup info my-project/my-api --json
Блок healthCheck у цьому виводі — це весь контракт: який шлях запитується, скільки чекати на відповідь, скільки разів повторювати спробу та який інтервал витримувати між спробами.
Налаштуйте перевірку так, щоб вона відповідала на запитання 3
Ендпоінт перевірки стану, який безумовно повертає 200, гірший за повну його відсутність, оскільки перетворює справжню перевірку на формальність. Сенс перевірки в тому, щоб вона завершувалася помилкою, коли застосунок не може виконувати свою роботу.
Корисний ендпоінт готовності перевіряє те, без чого застосунок не може працювати:
// Not this — it proves only that the process is alive
app.get('/healthz', (req, res) => res.send('ok'))
// This — it proves the app can actually serve a request
app.get('/healthz', async (req, res) => {
try {
await db.query('select 1') // the dependency that is usually the problem
if (!cacheReady) throw new Error('cache warming')
res.status(200).json({ ok: true })
} catch (err) {
res.status(503).json({ ok: false, reason: err.message })
}
})
На практиці це забезпечують два правила:
Перевіряйте лише залежності, без яких ви не можете обслуговувати запити. Якщо застосунок може коректно працювати в обмеженому режимі, коли пошуковий індекс недоступний, не вважайте пошуковий індекс причиною неготовності — інакше ви блокуватимете розгортання через те, що не є простоєм.
Не ускладнюйте перевірку. Ендпоінт викликається багаторазово під час кожного випуску. Перевірка готовності, яка виконує дорогий запит, створює додаткове навантаження власними руками.
Дайте йому достатньо часу, але не безмежно
Два параметри визначають, чи допоможе перевірка, чи зашкодить:
- Тайм-аут однієї спроби має перевищувати тривалість найповільнішого нормального холодного запуску. Застосунок, якому потрібно вісім секунд для підключення до бази даних і прогрівання кешу, щоразу не проходитиме перевірку з тайм-аутом у три секунди, і ви «вирішите» проблему, вимкнувши перевірку — тобто повернетеся до початкової ситуації.
- Кількість повторних спроб має охоплювати загальний час запуску, а не тривалість однієї спроби. Інтервал × кількість спроб — це реальний доступний бюджет часу.
У Dockup це healthCheckInterval, healthCheckTimeout і healthCheckRetries. Вони налаштовуються для кожного сервісу окремо, оскільки моноліт на Rails і sidecar на Go запускаються не за однаковим розкладом.
Якщо сайт уже недоступний
Якщо ви читаєте це під час інциденту, найшвидше його вирішити в такому порядку:
- Перевірте, чи відповідає застосунок безпосередньо, в обхід домену. Якщо через порт він відповідає, а через домен — ні, це проблема маршрутизації, а не застосунку, тож припиніть налагоджувати код.
- Перегляньте runtime-логи, а не логи збірки. Збірка завершилася успішно — це наша вихідна умова. Потрібно з’ясувати, що процес робив після запуску.
- Виконайте відкат до діагностики. Діагностувати проблему дешевше, коли ніхто не спостерігає.
dockup logs my-project/my-api --follow # what the running process is saying
dockup deployments my-project/my-api # what was live before this
dockup rollback <deployment-id> my-project/my-api # put that back
У Dockup відкат — це перемикання, а не повторна збірка, оскільки попередня версія все ще зберігається на диску. О третій ночі це важливо: найшвидше відновлення — те, для якого не потрібно нічого компілювати.
Яке запитання слід поставити платформі
Коли ви обираєте, де запускати продакшн, варто навмисно перевірити це: розгорніть застосунок, який успішно запускається, але не може під’єднатися до бази даних. Подивіться, що покаже панель.
Якщо вона покаже працює, тепер ви точно знаєте, чого буде варте це слово під час наступного інциденту.
Поширені запитання
Чому на панелі показано «працює», коли сайт недоступний? Тому що «працює» зазвичай означає, що процес контейнера існує, а не те, що застосунок може обслуговувати запит. Процес, який застряг у повторних спробах підключення до бази даних, безкінечно відповідає цьому визначенню.
Чи має перевірка стану звертатися до бази даних? Так, якщо без неї застосунок не може обслуговувати запити. Перевіряйте залежності, які справді необхідні, і не перевіряйте ті, без яких застосунок може працювати в обмеженому режимі.
У чому різниця між liveness і readiness? Liveness визначає, чи потрібно перезапустити процес. Readiness визначає, чи можна спрямовувати на нього трафік. Перевірка, яка запобігає цій проблемі, — це readiness, і її потрібно виконати до перемикання трафіку.
Як взагалі не допустити, щоб невдале розгортання поклало сайт? Перемикайте трафік лише після того, як нова версія відповість на справжній запит, і зберігайте попередню версію, доки перемикання не буде підтверджено. Тоді невдалий випуск буде випуском, якого ніколи не сталося, а не простоєм.
