← Паттерны приложений
11 мин · средне · Урок 1 из 5

Graceful shutdown: SIGTERM и дренаж соединений

Почему приложение должно корректно завершаться по SIGTERM, как дослуживать запросы и не рвать соединения при деплое.

Что происходит при деплое

Любой выкат из прошлого трека (rolling, blue-green) останавливает старую копию приложения. Оркестратор не убивает процесс сразу: он шлёт SIGTERM и ждёт. Если приложение умеет на это реагировать, оно успевает доделать текущие запросы. Если нет, оркестратор через таймаут шлёт SIGKILL, и все открытые соединения рвутся вместе с недообработанными запросами.

Graceful shutdown значит завершиться аккуратно: перестать брать новое, доделать начатое, освободить ресурсы.

Сигналы из прошлого урока

SIGTERM (15) — вежливая просьба завершиться, её можно перехватить. SIGKILL (9) перехватить нельзя, процесс умирает мгновенно. Оркестратор и docker stop всегда шлют сначала SIGTERM, ждут (дефолт зависит от системы: docker stop около 10 секунд, Kubernetes 30 секунд), и только потом SIGKILL.

Задача приложения: поймать SIGTERM и за отведённое время свернуться правильно.

Порядок корректного завершения

Когда пришёл SIGTERM, делают по шагам:

  1. Перестать принимать новое. Закрыть приёмный сокет, чтобы новые запросы не приходили.
  2. Дослужить начатое. Дать текущим запросам завершиться (это и есть дренаж, draining).
  3. Закрыть ресурсы. Соединения с базой, файлы, продюсеры очередей.
  4. Выйти с кодом 0.

Схематично, на псевдокоде:

on SIGTERM:
    server.stop_accepting()      # п.1
    server.wait_active_requests(timeout=25s)  # п.2
    db.close(); queue.close()    # п.3
    exit(0)                      # п.4

Откуда берутся 502 при «правильном» завершении

Первый шаг звучит безобидно, но именно на нём чаще всего и ловят ошибки. Логика «закрою сокет, а балансировщик сам уберёт копию из ротации» не работает, потому что порядок событий обратный.

В Kubernetes при удалении Pod запускаются два процесса параллельно, и никто их не синхронизирует:

Первое занимает миллисекунды, второе — от сотен миллисекунд до нескольких секунд, в зависимости от размера кластера. Получается окно, в котором приложение уже закрыло сокет, а трафик на него ещё идёт. Каждый такой запрос упирается в connection refused, и клиент видит 502 или 503 на ровном деплое.

Лечится это не в коде, а паузой перед завершением. Приложение должно продолжать обслуживать запросы ещё несколько секунд после SIGTERM — ровно столько, сколько расходится информация об удалении эндпоинта (propagation delay).

Проще всего добавить хук preStop. Kubernetes выполняет его до SIGTERM, а удаление из эндпоинтов в это время уже идёт:

lifecycle:
  preStop:
    exec:
      command: ["sleep", "10"]
terminationGracePeriodSeconds: 45

Контейнер эти 10 секунд работает как обычно и отвечает на запросы. К моменту, когда придёт SIGTERM, балансировщики уже знают, что копии нет, и новых соединений не пришлют.

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

Про terminationGracePeriodSeconds не забудь: пауза preStop считается внутри него. Десять секунд сна плюс двадцать пять на дренаж не влезут в дефолтные тридцать, и приложение прибьют на середине.

Таймаут на стороне приложения и оркестратора

Здесь есть две связанные величины. Оркестратор ждёт SIGTERM-таймаут (grace period) перед SIGKILL. Приложение должно уложить свой дренаж в это же время. Если приложению нужно 25 секунд, а grace period 10, оркестратор прибьёт его на середине. Поэтому grace period настраивают под самый долгий запрос.

В Kubernetes это terminationGracePeriodSeconds, в systemd TimeoutStopSec, в docker stop флаг -t.

Частая ошибка с PID 1

В контейнере приложение часто запущено как PID 1. Из урока про процессы: PID 1 особенный, и сигналы ему доставляются иначе. Если приложение запущено через шелл (sh -c "myapp"), SIGTERM поймает шелл, а не само приложение, и graceful shutdown не сработает. Поэтому в контейнере приложение запускают exec-формой (CMD ["myapp"]), чтобы оно стало PID 1 и получало сигналы напрямую.

Что важно запомнить

Оркестратор шлёт SIGTERM, ждёт grace period и только потом SIGKILL. Приложение должно за это время перестать принимать новое, дослужить начатое, закрыть ресурсы и выйти с кодом 0.

Дренаж приложения согласуют с grace period оркестратора, иначе его прибьют на середине. В контейнере приложение запускают exec-формой, чтобы оно было PID 1 и получало сигналы напрямую.

Закрывать приёмный сокет сразу по SIGTERM нельзя: в Kubernetes удаление эндпоинта идёт параллельно сигналу и доезжает до балансировщиков позже. Пауза через preStop: sleep (или сон в самом обработчике) закрывает это окно и убирает 502 при деплое.

В следующем уроке разберём health-эндпоинты: как оркестратор и балансировщик понимают, что приложение живо и готово.

Прошли материал до конца? Отметьте урок пройденным.