Перейти к содержанию
Innovialabs

Деплой без простоя: техники и пошаговая настройка

Практика4 мин

Коротко

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

Что на самом деле нужно для деплоя без простоя

Релиз без простоя — это не один приём, а четыре правила, которые работают вместе:

  • Поэтапная замена: новые экземпляры запускаются раньше, чем удаляются старые, поэтому мощность никогда не падает до нуля.
  • Health checks: трафик идёт только на экземпляры, которые сообщают о готовности.
  • Graceful shutdown и слив соединений: старый экземпляр перестаёт принимать новые запросы, дорабатывает текущие и только потом завершается.
  • Обратно совместимые миграции: во время выкатки старый и новый код работают параллельно с одной базой, поэтому схема должна подходить обоим.

Если нет хотя бы одного пункта, пользователи будут видеть ошибки при каждом деплое — даже на идеальной инфраструктуре.

Rolling restart

При rolling update оркестратор (Kubernetes, Docker Swarm, Nomad или скрипт за балансировщиком) заменяет экземпляры небольшими партиями:

  1. Запускает один новый экземпляр.
  2. Ждёт, пока он пройдёт readiness-проверку.
  3. Добавляет его в балансировщик.
  4. Убирает один старый экземпляр из балансировщика, даёт ему слить соединения и останавливает.
  5. Повторяет, пока все экземпляры не перейдут на новую версию.

Ключевые настройки: сколько лишних экземпляров допустимо во время обновления и сколько может быть недоступно. Для нулевого простоя — ноль недоступных и минимум один лишний.

Альтернативы с той же целью: blue-green (два полных окружения, трафик переключается разом) и canary (сначала на новую версию идёт небольшая доля трафика). Обе тоже опираются на health checks и слив соединений.

Health checks: liveness и readiness

Эти проверки нельзя смешивать:

  • Readiness отвечает на вопрос «можно ли мне сейчас получать трафик?». Она должна падать во время старта, прогрева кэшей и при остановке.
  • Liveness отвечает на вопрос «я завис и меня нужно перезапустить?». Держите её простой и не проверяйте в ней базу, иначе сбой базы перезапустит все экземпляры разом.

Readiness-эндпоинт, который всегда возвращает 200, — самая частая причина, почему «бесшовный» деплой всё равно теряет запросы.

Graceful shutdown и слив соединений

Останавливая контейнер, оркестратор отправляет SIGTERM, ждёт grace period и затем отправляет SIGKILL. Приложение должно использовать это окно. Минимальный пример на Node.js:

const express = require('express');
const app = express();
let shuttingDown = false;

app.get('/healthz/ready', (req, res) => {
  res.status(shuttingDown ? 503 : 200).end();
});

const server = app.listen(3000);

process.on('SIGTERM', () => {
  shuttingDown = true;
  setTimeout(() => {
    server.close(() => process.exit(0));
  }, 5000);
});

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

Рабочий пример для Kubernetes

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  template:
    metadata:
      labels:
        app: api
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: api
          image: registry.example.com/api:1.4.2
          ports:
            - containerPort: 3000
          readinessProbe:
            httpGet:
              path: /healthz/ready
              port: 3000
            periodSeconds: 5
            failureThreshold: 2

maxUnavailable: 0 сохраняет полную мощность, maxSurge: 1 добавляет по одному поду, а grace period должен быть больше задержки слива плюс самого долгого запроса.

Обратно совместимые миграции базы данных

Используйте паттерн expand and contract. Пример: переименование name в full_name.

РелизБаза данныхКод
1. ExpandДобавить full_name, nullableПисать в обе колонки, читать name
2. BackfillСкопировать данные пачкамиБез изменений
3. SwitchБез измененийЧитать и писать только full_name
4. ContractУдалить nameБез изменений

Отсюда правила:

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

Частые ошибки

  • Readiness-проверка не учитывает состояние остановки.
  • Grace period короче самого долгого запроса или задачи.
  • Разрушающая миграция выкатывается вместе с кодом.
  • Сессии хранятся в памяти экземпляра, и пользователей разлогинивает при его остановке.
  • Не проверен откат: предыдущая версия тоже должна работать с новой схемой.

Частые вопросы

Нет. Те же шаги работают с nginx или облачным балансировщиком и двумя и более экземплярами. Kubernetes просто автоматизирует rolling update и проверки.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.