Деплой выполнен успешно, но сайт недоступен
В панели указано, что приложение запущено, а пользователи видят ошибку. Узнайте, почему успешный деплой и работоспособность приложения — разные сигналы, и как сделать так, чтобы зелёный статус деплоя действительно означал, что приложение отвечает.
Есть особый вид неудачного утра, которое начинается с зелёной галочки. Деплой выполнен успешно, но сайт недоступен, в панели указано запущено, а кто-то присылает вам скриншот с ошибкой 502.
Это не редкий edge case. Это закономерный результат того, что платформа сообщает об одном, а измеряет другое. Важно точно понимать, почему так происходит, потому что решение — не в том, чтобы «проверять тщательнее», а в изменении смысла слова запущено.
Три разных вопроса и один индикатор статуса
Когда платформа сообщает, что сервис запущен, она может отвечать на любой из следующих вопросов:
- Запустился ли контейнер? Процесс существует и не завершился.
- Открыт ли порт? В нужном месте что-то принимает соединения.
- Корректно ли отвечает приложение? На запрос приходит ответ, означающий, что приложение готово работать.
Это совершенно разные гарантии, и большинство инцидентов такого типа возникает потому, что панель отвечает на вопрос 1, а вы предполагаете ответ на вопрос 3.
Процесс Node, который запускается, не может подключиться к базе данных и застревает в цикле повторных попыток, бесконечно удовлетворяет условию из вопроса 1. Он не завершился с ошибкой. Он никогда не обработает запрос. Контейнер «запущен» во всех смыслах, которые важны для оркестратора.
Где возникает недоступность
Опасное окно находится между моментом, когда «запустилась новая версия», и моментом, когда «новая версия может работать». В этот промежуток наивная платформа уже переключила трафик, потому что измеряла только факт запуска.
Ситуация с запуском и зависанием хуже обычного падения из-за особенностей rollback. Цикл падений заметен: контейнер завершается, перезапускается, снова завершается, и платформа в конце концов это обнаруживает. Запуск с последующим зависанием происходит незаметно. Ничего не перезапускается, тревоги не срабатывают, а предыдущая рабочая версия обычно уже удалена.
Именно это наносит настоящий ущерб. Старая версия работала. Её удалили, потому что запустилась новая, а запуск приняли за работоспособность.
Что делает настоящая health gate
Решение носит структурный, а не процедурный характер. Трафик нельзя переключать, пока новая версия не ответит на запрос.
В Dockup релиз проходит так: новая версия собирается изолированно, запускается рядом с текущей обслуживающей версией, а затем ей задают вопрос. Только после ответа домен переключается на неё. Если ответа нет, релиз останавливается, а предыдущая версия продолжает обслуживать запросы — никто за пределами вашей панели даже не узнает, что была предпринята попытка деплоя.
Поэтому неудачный деплой в Dockup не приводит к недоступности сайта. Старый контейнер не удаляется в расчёте на то, что новая версия будет работать.
# The health gate is per-service configuration, not a platform default you inherit
dockup info my-project/my-api --json
Блок healthCheck в этом выводе — это весь контракт: какой путь запрашивать, сколько ждать ответа, сколько раз повторять попытку и какой интервал выдерживать между попытками.
Настройте проверку так, чтобы она отвечала на вопрос 3
Health endpoint, который безусловно возвращает 200, хуже, чем полное отсутствие проверки, потому что он превращает настоящий контроль в формальную отметку. Смысл проверки в том, чтобы она завершалась ошибкой, когда приложение не может выполнять свою работу.
Полезный readiness endpoint проверяет то, без чего приложение не может работать:
// 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 })
}
})
На практике достаточно соблюдать два правила:
Проверяйте только те зависимости, без которых приложение не может обслуживать запросы. Если приложение может работать в ограниченном режиме при недоступности поискового индекса, не включайте поисковый индекс в readiness-проверку — иначе деплои будут блокироваться из-за ситуации, которая не является недоступностью сервиса.
Проверка должна быть дешёвой. Этот endpoint вызывается многократно во время каждого релиза. Readiness-проверка, выполняющая дорогой запрос, создаёт дополнительную нагрузку своими же силами.
Дайте приложению достаточно времени, но не бесконечное
Два параметра определяют, принесёт ли gate пользу или вред:
- Timeout для одной попытки должен быть больше максимального допустимого времени холодного старта. Приложение, которому требуется восемь секунд на подключение к базе данных и прогрев кеша, будет каждый раз проваливать проверку с трёхсекундным таймаутом. В итоге вы «исправите» проблему, отключив gate, и вернётесь к исходной ситуации.
- Количество повторных попыток должно покрывать общее время запуска, а не одну попытку. Интервал × количество попыток — это фактический доступный бюджет времени.
В Dockup за это отвечают healthCheckInterval, healthCheckTimeout и healthCheckRetries. Они настраиваются для каждого сервиса отдельно, потому что монолит на Rails и sidecar на Go запускаются не по одному и тому же расписанию.
Если сайт уже недоступен
Если вы читаете это во время инцидента, следующий порядок действий поможет устранить проблему быстрее всего:
- Проверьте, отвечает ли приложение напрямую, в обход домена. Если оно отвечает на своём порту, но не через домен, проблема связана с маршрутизацией, а не с приложением, поэтому дальнейшую отладку кода нужно прекратить.
- Изучите runtime logs, а не логи сборки. Сборка завершилась успешно — это исходная предпосылка. Вам нужно понять, что процесс делал после запуска.
- Сначала выполните rollback, а потом разбирайтесь в причинах. Диагностировать проблему дешевле, когда никто не наблюдает за происходящим.
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 rollback — это переключение, а не повторная сборка, потому что предыдущая версия всё ещё хранится на диске. В три часа ночи это особенно важно: самое быстрое восстановление — то, которому не нужно ничего компилировать.
Какой вопрос стоит задать платформе
Выбирая, где запускать production, стоит намеренно проверить это поведение: задеплойте приложение, которое успешно запускается, но не может подключиться к базе данных. Посмотрите, что покажет панель.
Если в ней указано запущено, теперь вы точно знаете, чего стоит это слово во время следующего инцидента.
Часто задаваемые вопросы
Почему в панели указано, что приложение запущено, когда сайт недоступен? Потому что «запущено» обычно означает лишь наличие процесса контейнера, а не способность приложения обработать запрос. Процесс, застрявший на повторных попытках подключения к базе данных, может бесконечно соответствовать этому определению.
Должна ли health check обращаться к базе данных? Да, если без неё приложение не может обрабатывать запросы. Проверяйте действительно необходимые зависимости и пропускайте те, без которых приложение может работать в ограниченном режиме.
В чём разница между liveness и readiness? Liveness проверяет, нужно ли перезапустить процесс. Readiness проверяет, можно ли направлять на него трафик. Gate, предотвращающий эту проблему, — это readiness-проверка, которая должна выполняться до переключения трафика.
Как полностью предотвратить падение сайта из-за неудачного деплоя? Переключайте трафик только после того, как новая версия ответит на реальный запрос, и сохраняйте предыдущую версию до подтверждения переключения. Тогда неудачный релиз просто не состоится, вместо того чтобы привести к недоступности сайта.
