Задача 2. Под удалён, трафик идёт: анатомия graceful shutdown

При удалении пода часть запросов падает с ошибкой, хотя приложение корректно обрабатывает SIGTERM. Разбираем, почему удаление из endpoints и остановка контейнера идут параллельно, а не последовательно.
Опубликовано:

Задача

Приложение аккуратно обрабатывает SIGTERM: перестаёт принимать новые соединения, дожидается завершения текущих запросов, закрывается. terminationGracePeriodSeconds — стандартные 30 секунд.

Во время rolling update мониторинг стабильно показывает всплеск ошибок 502/connection refused — небольшой, но заметный.

Вопрос: почему запросы попадают в под, который уже завершается, если приложение корректно реагирует на сигнал?

Что кажется очевидным

«Kubernetes сначала убирает под из балансировки, потом посылает SIGTERM. Раз ошибки есть — значит, приложение обрабатывает сигнал неправильно».

Это разумное предположение, и оно неверно в главном.

Как это работает на самом деле

Ключевой факт: при удалении пода Kubernetes запускает два независимых процесса параллельно, а не последовательно.

  sequenceDiagram
    participant API as API server
    participant EP as Endpoint controller
    participant KP as kube-proxy на нодах
    participant KL as kubelet
    participant P as Под

    API->>API: под помечен на удаление<br/>(deletionTimestamp)
    par Ветка 1: убрать из балансировки
        API->>EP: под удаляется
        EP->>API: обновить EndpointSlice
        API->>KP: рассылка изменений
        KP->>KP: обновить iptables/IPVS<br/>на КАЖДОЙ ноде
    and Ветка 2: остановить контейнер
        API->>KL: остановить под
        KL->>P: preStop hook
        KL->>P: SIGTERM
    end
    Note over KP,P: Гонка: контейнер может<br/>остановиться раньше,<br/>чем обновятся правила

Обе ветки стартуют одновременно. И вот в чём проблема: вторая ветка обычно быстрее первой.

Остановка контейнера — это локальное действие на одной ноде. А удаление из балансировки должно:

  1. дойти до контроллера endpoints;
  2. обновить объект EndpointSlice в API;
  3. разослать изменение всем kube-proxy в кластере;
  4. каждый kube-proxy должен перестроить правила iptables/IPVS на своей ноде.

На кластере в сотню нод это занимает заметное время. Плюс за пределами кластера могут быть ingress-контроллеры и облачные балансировщики со своими интервалами обновления.

Итог: приложение уже получило SIGTERM и закрыло слушающий сокет, а правила на части нод ещё направляют туда трафик. Запрос приходит в никуда — connection refused.

Ответ

Дело не в обработке SIGTERM. Причина — гонка между удалением из балансировки и остановкой контейнера: они идут параллельно, и остановка успевает раньше распространения правил.

Что с этим делать

Стандартное решение — задержать начало остановки, чтобы правила успели обновиться. Делается это через preStop hook:

YAML
spec:
  terminationGracePeriodSeconds: 60
  containers:
    - name: api
      lifecycle:
        preStop:
          exec:
            command: ["sleep", "10"]
Нажмите, чтобы развернуть и увидеть больше

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

Важные детали:

Порядок, который стоит держать в голове

  1. Под помечается на удаление, deletionTimestamp установлен.
  2. Параллельно: начинается удаление из endpoints и kubelet запускает preStop.
  3. После preStopSIGTERM главному процессу.
  4. Приложение завершает текущие запросы.
  5. Если не уложилось в grace period — SIGKILL.

Проверка

Убедиться, что настройка работает, проще всего нагрузочным тестом во время выкатки: запустите постоянный поток запросов через Service и сделайте kubectl rollout restart. При правильных preStop и grace period количество ошибок должно быть нулевым.


Дальше: Задача 3. Requests, limits и три класса QoS.

Начать поиск

Введите ключевые слова для поиска статей

↑↓
ESC
⌘K Горячая клавиша