Как минимизировать время простоя (downtime) приложения Node.js во время деплоя?

Ответ

Чтобы Node.js-приложение оставалось доступным пользователям во время обновления, я использую стратегии, которые позволяют запустить новую версию до отключения старой.

Основные стратегии и их реализация в Node.js-окружении:

  1. Blue-Green Deployment:

    • Суть: Разворачиваются два идентичных окружения — «Blue» (текущая версия) и «Green» (новая версия). Весь трафик идёт на Blue.
    • Деплой: Новая версия разворачивается в Green. После успешного запуска и health-чеков трафик с балансировщика нагрузки (Nginx, HAProxy, облачный LB) мгновенно переключается на Green.
    • Откат: В случае проблем — переключение обратно на Blue занимает секунды.
  2. Canary Releases:

    • Суть: Новая версия разворачивается для небольшого процента пользователей (например, 5%), затем доля постепенно увеличивается.
    • Реализация: Используются возможности балансировщиков (например, веса в Nginx weight) или feature flags на уровне приложения.
      # Пример конфигурации Nginx для canary
      upstream backend {
      server old_app:3000 weight=95;
      server new_app:3000 weight=5;
      }
  3. Rolling Update (в оркестраторах, например, Kubernetes):

    • Суть: Новые поды (контейнеры) с обновлённым приложением создаются по одному/несколько, а старые удаляются после их готовности.
    • Настройка: В манифесте Kubernetes задаются strategy.type: RollingUpdate и параметры maxUnavailable (сколько подов может быть недоступно) и maxSurge (сколько дополнительных подов можно создать).

Технические практики для Node.js приложения:

  • Graceful Shutdown: Приложение должно корректно завершать работу, дожидаясь завершения текущих запросов.
    process.on('SIGTERM', () => {
      server.close(() => {
        console.log('Server closed');
        db.disconnect(() => process.exit(0));
      });
    });
  • Health Checks: Добавление эндпоинтов /health и /ready для проверки балансировщиком состояния приложения и его зависимостей (БД, кэш).
  • Миграции БД: Выполняются до переключения трафика на новую версию и должны быть обратно совместимы со старой версией кода (например, добавление nullable-колонки).

Ответ 18+ 🔞

Э, слушай, тут один вопрос меня просто выносит постоянно — как обновлять продакшен-приложение на Node.js, чтобы пользователи даже не заметили, что ты там что-то ковыряешь. Ну, чтобы не было этой дичи, когда сайт на пять минут ложится, а все начинают орать «чё за хуйня, ничего не работает!». Так вот, есть несколько проверенных способов, которые реально работают.

Первая стратегия — Blue-Green, или «синий-зелёный», ёпта.
Представь, у тебя есть два абсолютно одинаковых стенда. Один, условно, синий — на нём крутится твоя текущая, рабочая версия. Весь трафик идёт туда. Ты спокойно, без спешки, раскатываешь новую версию на зелёный стенд. Поднимаешь, проверяешь, что всё живое. А потом, одним движением рубильника на балансировщике (Nginx, HAProxy или что у тебя там), просто переключаешь весь поток запросов на зелёный. Бах — и новая версия в работе. Если вдруг что-то пошло не так — охуеть, да похуй! — ты просто переключаешь рубильник обратно на синий. Откат занимает секунды. Это как иметь запасной выход, который всегда открыт.

Вторая — Canary Releases, или «канареечное развёртывание». Тут идея хитрая, как жопа. Ты не сразу всех пользователей кидаешь на новую версию, а начинаешь с малого. Скажем, 5% трафика отправляешь на обновлённое приложение, а остальные 95% пусть себе работают на старой, проверенной. Смотришь метрики: не падают ли ошибки, не полетела ли производительность. Если всё ок — постепенно увеличиваешь процент, скажем, до 20%, потом до 50%, и так далее, пока все не переедут. В Nginx это делается через веса (weight), выглядит просто, но работает, блядь, как часы.

upstream backend {
  server old_app:3000 weight=95;
  server new_app:3000 weight=5;
}

Если что-то пошло не так — ты быстро локализовал проблему на маленькой группе, а не устроил пиздец всем сразу. Умно, да?

Третья — Rolling Update, особенно если ты в Kubernetes.
Тут оркестратор сам всё делает, красота. Он по одному или по несколько создаёт новые поды (контейнеры) с новой версией твоего приложения. Каждый новый под проходит health-чеки. Как только он готов принимать трафик, оркестратор убивает один старый под. И так потихоньку, пока все поды не обновятся. В манифесте просто прописываешь strategy.type: RollingUpdate и настраиваешь, сколько максимум может быть недоступно (maxUnavailable) и сколько дополнительных подов можно создать сверх плана (maxSurge). Главное — терпения ебать ноль, но процесс идёт сам.

А теперь, чувак, ключевые технические штуки, без которых нихуя не получится:

  • Graceful Shutdown — корректное завершение работы. Это когда приложение получает сигнал «пора на покой» (например, SIGTERM от оркестратора), оно не тупо умирает, а говорит: «Окей, я закончу обрабатывать текущие запросы, закрою соединения с базой, и только потом выключусь». Иначе пользователи получат ошибки в самый неподходящий момент. Делается это так:

    process.on('SIGTERM', () => {
      server.close(() => {
        console.log('Server closed');
        db.disconnect(() => process.exit(0));
      });
    });

    Без этого — манда с ушами, а не деплой.

  • Health Checks — проверки здоровья. Ты должен сделать в приложении эндпоинты типа /health и /ready. Балансировщик или оркестратор будут постоянно стучаться туда и спрашивать: «Ты живой? Ты готов работать?». Если приложение отвечает «я болен» (например, БД недоступна), его исключат из пула получателей трафика. Доверия ебать ноль — только холодная проверка статуса.

  • Миграции базы данных — самая опасная часть. Их нужно выполнять до того, как ты переключишь трафик на новую версию. И они должны быть обратно совместимыми со старой версией кода! Представь: ты добавил новую обязательную колонку в таблицу, а старый код про неё не знает и пытается вставить запись — будет тебе хиросима и нигерсраки. Поэтому правило: сначала миграция (например, добавление nullable-колонки), потом деплой нового кода, который уже умеет с этой колонкой работать. И только потом, когда все переехали, можно делать следующую миграцию, которая, например, сделает колонку NOT NULL. Если проебешь этот момент — сам от себя охуеешь.

В общем, суть в том, чтобы никогда не останавливать систему целиком. Всегда должен быть работающий экземпляр, который отвечает на запросы. А новые версии подключаются и отключаются плавно, как в хорошем театре. Ну, или почти как.