Индекс журналаDockup / заметка с места
Note / deploy-succeeded-but-site-is-down

Деплой выполнен успешно, но сайт недоступен

В панели указано, что приложение запущено, а пользователи видят ошибку. Узнайте, почему успешный деплой и работоспособность приложения — разные сигналы, и как сделать так, чтобы зелёный статус деплоя действительно означал, что приложение отвечает.

Есть особый вид неудачного утра, которое начинается с зелёной галочки. Деплой выполнен успешно, но сайт недоступен, в панели указано запущено, а кто-то присылает вам скриншот с ошибкой 502.

Это не редкий edge case. Это закономерный результат того, что платформа сообщает об одном, а измеряет другое. Важно точно понимать, почему так происходит, потому что решение — не в том, чтобы «проверять тщательнее», а в изменении смысла слова запущено.

Три разных вопроса и один индикатор статуса

Когда платформа сообщает, что сервис запущен, она может отвечать на любой из следующих вопросов:

  1. Запустился ли контейнер? Процесс существует и не завершился.
  2. Открыт ли порт? В нужном месте что-то принимает соединения.
  3. Корректно ли отвечает приложение? На запрос приходит ответ, означающий, что приложение готово работать.

Это совершенно разные гарантии, и большинство инцидентов такого типа возникает потому, что панель отвечает на вопрос 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 запускаются не по одному и тому же расписанию.

Если сайт уже недоступен

Если вы читаете это во время инцидента, следующий порядок действий поможет устранить проблему быстрее всего:

  1. Проверьте, отвечает ли приложение напрямую, в обход домена. Если оно отвечает на своём порту, но не через домен, проблема связана с маршрутизацией, а не с приложением, поэтому дальнейшую отладку кода нужно прекратить.
  2. Изучите runtime logs, а не логи сборки. Сборка завершилась успешно — это исходная предпосылка. Вам нужно понять, что процесс делал после запуска.
  3. Сначала выполните 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-проверка, которая должна выполняться до переключения трафика.

Как полностью предотвратить падение сайта из-за неудачного деплоя? Переключайте трафик только после того, как новая версия ответит на реальный запрос, и сохраняйте предыдущую версию до подтверждения переключения. Тогда неудачный релиз просто не состоится, вместо того чтобы привести к недоступности сайта.