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

Graceful shutdown в контейнерах: SIGTERM, дренаж запросов и таймауты

Продвинутое3 мин

Коротко

По SIGTERM приложение должно перестать принимать новую работу, завершить текущие запросы и выйти до конца grace period; убедитесь, что сигнал доходит до процесса как PID 1, и добавьте короткий preStop в Kubernetes.

Коротко

Каждый деплой, уменьшение реплик или drain ноды останавливает контейнеры. Если приложение умирает мгновенно, текущие запросы падают с ошибками, а фоновые задачи обрываются на середине. Graceful shutdown означает:

  1. сигнал действительно доходит до процесса;
  2. процесс перестаёт принимать новые соединения и задачи;
  3. завершает начатое, закрывает соединения с базой и сбрасывает буферы;
  4. выходит раньше, чем платформа потеряет терпение и пришлёт SIGKILL.

Как Docker и Kubernetes останавливают контейнеры

Docker: docker stop отправляет SIGTERM (или сигнал из STOPSIGNAL) процессу PID 1 контейнера, ждёт таймаут (по умолчанию 10 секунд, меняется через --stop-timeout или -t) и затем отправляет SIGKILL.

Kubernetes делает больше, причём частично параллельно:

  • pod помечается Terminating и убирается из endpoints сервиса;
  • выполняется preStop hook, если он задан;
  • затем контейнер получает SIGTERM;
  • если по истечении terminationGracePeriodSeconds (по умолчанию 30) он ещё жив, приходит SIGKILL. Время preStop входит в этот период.

Важная деталь: удаление из endpoints доходит до kube-proxy, ingress-контроллеров и балансировщиков асинхронно. Несколько мгновений после SIGTERM трафик ещё может приходить. Приложение, которое сразу закрывает listener, отклонит эти запросы.

PID 1 и проброс сигналов

Процесс с PID 1 в контейнере ведёт себя особенно: ядро не применяет к нему стандартные действия сигналов. Если у PID 1 нет обработчика SIGTERM, сигнал просто игнорируется, и контейнер ждёт SIGKILL.

Типичные ловушки:

  • Shell form CMD npm start запускает /bin/sh -c, который становится PID 1 и не пробрасывает сигналы приложению.
  • npm, yarn и похожие обёртки могут передавать сигналы ненадёжно. Запускайте runtime напрямую.
  • Entrypoint-скрипты должны заканчиваться exec "$@", чтобы приложение заменило shell.
# exec form: PID 1 — сам node
CMD ["node", "server.js"]

Если процесс не умеет обрабатывать сигналы или порождает дочерние процессы, используйте маленький init: docker run --init или tini в entrypoint. Он пробрасывает сигналы и убирает зомби-процессы.

Обработка SIGTERM в коде

Node.js: останавливаем HTTP-сервер, ждём открытые запросы, закрываем ресурсы. Жёсткий таймаут страхует от зависших keep-alive соединений.

process.on("SIGTERM", () => {
  server.close(async () => {
    await db.end();
    process.exit(0);
  });
  setTimeout(() => process.exit(1), 25000).unref();
});

Python: серверы вроде Gunicorn и Uvicorn уже корректно обрабатывают SIGTERM — главное, чтобы они были PID 1. В собственном цикле воркера ставим флаг и доделываем текущую задачу:

import signal

stopping = False

def on_sigterm(signum, frame):
    global stopping
    stopping = True

signal.signal(signal.SIGTERM, on_sigterm)

while not stopping:
    process_next_job()
close_connections()

Go: http.Server.Shutdown закрывает listeners и ждёт активные запросы, пока не истечёт контекст.

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
defer stop()

go srv.ListenAndServe()
<-ctx.Done()

shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
srv.Shutdown(shutdownCtx)

preStop и таймауты в Kubernetes

Чтобы закрыть гонку с обновлением endpoints, добавьте короткую паузу перед SIGTERM:

spec:
  terminationGracePeriodSeconds: 45
  containers:
    - name: api
      lifecycle:
        preStop:
          exec:
            command: ["sleep", "5"]

Для exec в образе нужен бинарник sleep; в новых версиях Kubernetes есть и встроенное действие sleep для preStop. Составьте бюджет времени: пауза preStop + самый долгий обычный запрос + очистка должны укладываться в terminationGracePeriodSeconds, а жёсткий таймаут внутри приложения должен срабатывать раньше SIGKILL от Kubernetes.

Чек-лист

  • Exec form в CMD/ENTRYPOINT, exec в shell-скриптах или --init.
  • Обработчик SIGTERM: прекратить приём, дренировать, выйти с кодом 0.
  • Readiness probe начинает падать с началом остановки.
  • Пауза в preStop на распространение endpoints.
  • Grace period длиннее времени дренажа.
  • Консьюмеры очередей подтверждают сообщение только после обработки, чтобы прерванная задача пришла заново.
  • Долгие соединения (WebSocket, стриминг) получают сообщение о закрытии, чтобы клиенты переподключились к другому экземпляру.

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

Подайте на сервис постоянную нагрузку и одновременно сделайте rolling deploy. Любой 5xx или сброс соединения во время выкатки указывает на дыру: нет обработчика, проблема с PID 1 или слишком короткий preStop.

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

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

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

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

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

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