Когда Kubernetes перезапускает ваш под — и когда нет

Проблема терминологии

Инженеры говорят «под перезапустился», когда имеют в виду четыре совершенно разные вещи. Ошибки в этом понимании ведут к дефектным ранбукам и неверным решениям во время дежурств (on-call).
Практический тест: изменился ли UID пода? Если да — это пересоздание (recreation), а не перезапуск контейнера. Счетчик перезапусков сбрасывается в ноль. Если нет — объект пода тот же самый, внутри него перезапустился процесс контейнера.

Главный инсайт: за чем на самом деле следит kubelet

kubelet следит за спецификацией пода (pod spec) — не за ConfigMap, не за Secrets, не за CRD-ресурсами Istio. Если pod spec не изменился, kubelet никогда не сработает. Этот единственный факт объясняет большинство расследований в продакшене на тему «почему мой конфиг не обновился?».

Mutating admission webhooks могут изменять pod spec в момент создания, но никогда после допуска (admission) — они не могут инициировать перезапуски контейнеров после создания.

Матрица решений

Диаграмма ниже переводит эту же матрицу в путь принятия решений, по которому вы можете пройти даже в 2 часа ночи во время инцидента.

Сценарий 1: ConfigMap — почему одно и то же изменение имеет два разных поведения

ConfigMap как переменная окружения vs volume mount — под с переменной окружения «заморожен», под с volume автосинхронизируется через подмену симлинков kubelet

  • Режим переменных окружения (envFrom / valueFrom): ядро копирует переменные окружения в /proc/<pid>/environ при вызове execve(). Эта память принадлежит процессу — ни одна внешняя система не может её изменить. Обновите ConfigMap, и kubelet не увидит изменений в pod spec, а значит ничего не сделает. Процесс будет бесконечно хранить старые значения.
  • Режим Volume Mount: kubelet синхронизирует данные через атомарную подмену символической ссылки (symlink swap), а не через прямую запись в файл:
/etc/config/
├── ..2025_12_19_11_30_00/   ← NEW data dir (kubelet creates this)
│   └── APP_COLOR            ← "red"
├── ..data ─────────────────▶ ..2025_12_19_11_30_00/  ← symlink SWAPPED atomically
└── APP_COLOR ──────────────▶ ..data/APP_COLOR
Подмена симлинка генерирует событие IN_CREATE на директорию ..data — но НЕ IN_MODIFY на сам файл. Приложения, отслеживающие IN_MODIFY на открытом файловом дескрипторе, полностью упускают это из виду. Именно поэтому nginx не перезагружает конфигурацию автоматически при изменениях ConfigMap без явной обработки inotify.

Лабораторные доказательства (01-configmap/ в сопутствующем репозитории)

ConfigMap updated: APP_COLOR blue → red

Pod A (env var):      APP_COLOR=blue  ← frozen, restart count: 0
Pod B (volume mount): APP_COLOR=red   ← auto-synced, restart count: 0

Правильный паттерн inotify — отслеживайте директорию, а не файл

watcher.Add(filepath.Dir(configPath))  // watches /etc/config/ — catches IN_CREATE
// watcher.Add(configPath)             // misses symlink swap entirely

for event := range watcher.Events {
    if event.Op&fsnotify.Create == fsnotify.Create {
        reloadConfig()
    }
}
Получите бесплатную консультацию по вашему проекту
Настроим ваш Kubernetes под новые реалии

Сценарий 2: Обновление образов — Пересоздание vs Перезапуск контейнера vs CrashLoop

Эти три сценария выглядят похоже, но фундаментально различаются:

Успешное обновление образа — под пересоздается

BEFORE: Pod UID aaa-bbb, IP 10.244.1.5, nginx:1.25, restarts: 0
AFTER:  Pod UID xxx-yyy, IP 10.244.1.6, nginx:1.27, restarts: 0
                ↑ UID changed — RECREATION, not container restart
Процесс Rolling update, показывающий создание нового ReplicaSet, пересоздание подов и сохранение старого RS для отката.

ImagePullBackOff — старый под остается защищенным

Old pod: Running           ← Kubernetes keeps it alive
New pod: ImagePullBackOff  ← stuck, old pod never killed until new one is healthy

CrashLoopBackOff — тот же под, счетчик перезапусков растет

Pod UID:       aaa-bbb  ← UNCHANGED
Restart count: 0 → 1 → 2 → 3  ← same pod object, container crashing
Диагностическое правило: растущий счетчик перезапусков при неизменном UID = crash loop. Нулевой счетчик перезапусков с новым UID = rolling update.

Заметка о StatefulSet: поды StatefulSet также пересоздаются при изменении образа, но их порядковый идентификатор (pod-0, pod-1) и привязки к PVC сохраняются. Семантика перезапуска контейнеров идентична Deployments — сохранение идентичности пода не означает перезапуск контейнера «in-place».

Сценарий 3: Изменение ресурсов in-place (K8s 1.35 GA)

Kubernetes 1.35 сделал изменение размера подов «на лету» (in-place pod resize) общедоступным (GA). Как CPU, так и память могут быть изменены без пересоздания пода — UID и IP никогда не меняются. Доступность in-place изменения зависит от поддержки со стороны CRI и ОС ноды; проверено на containerd 1.7+ с Linux cgroups v2.

Что происходит с контейнером — зависит от политики resizePolicy, которую вы определяете явно:
resizePolicy:
- resourceName: cpu
  restartPolicy: NotRequired      # cgroup quota updated, process untouched
- resourceName: memory
  restartPolicy: RestartContainer # container restarts inside same pod

Лабораторные доказательства (05-resource-resize/ — требуется K8s 1.35+)

CPU resize 200m → 500m (NotRequired):
  UID: d7c99204  IP: 10.244.0.7  Restarts: 0  ← all unchanged

Memory resize 256Mi → 512Mi (RestartContainer):
  UID: d7c99204  IP: 10.244.0.7  Restarts: 1
       ↑ same pod      ↑ same IP    ↑ our policy choice, not K8s forcing it
Важно: дефолтная политика resizePolicy для памяти — NotRequired. Если вы её опустите, изменение лимита памяти тихо обновит cgroup без перезапуска контейнера — и размер кучи вашей JVM (heap size) останется прежним. Всегда явно определяйте resizePolicy для памяти.

Как применить

kubectl patch pod my-pod -n my-namespace \
  --subresource resize \
  -p '{"spec":{"containers":[{"name":"app","resources":{
    "requests":{"cpu":"250m","memory":"128Mi"},
    "limits":{"cpu":"500m","memory":"256Mi"}
  }}]}}'
# Note: omit --type=merge — causes a validation error with --subresource resize

Сценарий 4: Маршрутизация Istio — ноль перезапусков через xDS

Изменения Istio VirtualService и DestinationRule никогда не вызывают перезапуск контейнеров. istiod поддерживает постоянный двунаправленный gRPC-поток к каждому sidecar-контейнеру Envoy — обновления маршрутов доставляются за миллисекунды, перезаписываются в памяти, не затрагивая под и не записывая файлы.

Лабораторные доказательства (04-istio-routing/ в сопутствующем репозитории)

Four pods. Three routing changes:
  100% v1  →  80/20 canary  →  100% v2

Restart counts: BEFORE 0 0 0 0 / AFTER 0 0 0 0
Pod ages: unchanged throughout all three changes.

Сценарий 5: Stakater Reloader — автоматизация ручного шага

Когда приложения потребляют ConfigMap через переменные окружения, кто-то должен выполнять kubectl rollout restart после каждого обновления. Reloader автоматизирует это с помощью событий отслеживания (watch events) Kubernetes — обнаружение происходит практически мгновенно, без постоянного опроса (polling).
metadata:
  annotations:
    reloader.stakater.com/auto: "true"
Подводный камень в продакшене: При установке через Helm по умолчанию используется значение watchGlobally=false — Reloader отслеживает только собственный namespace. Аннотированные Deployments в других пространствах имен будут тихо проигнорированы без вывода каких-либо ошибок. Всегда устанавливайте чарт с параметром watchGlobally=true.
helm install reloader stakater/reloader \
  --namespace reloader \
  --set reloader.watchGlobally=true

Лабораторные доказательства (07-stakater-reloader/ в сопутствующем репозитории)

ConfigMap updated. No kubectl rollout restart run.

New pod APP_MESSAGE: "Hello from OpsCart v2 — auto reloaded!"
Rolling container restart triggered automatically.

Когда Hot-Reload идет не так

Горячая перезагрузка (hot-reload) не всегда безопаснее, чем перезапуск контейнера. Стоит знать о двух режимах отказа:

Семантически невалидный конфиг принимается молча

Файл обновляется, триггер inotify срабатывает, ошибок не возникает — но в новом конфиге содержится логическая ошибка. Под проходит проверки работоспособности (health checks) и часами работает в сломанном состоянии. Перезапуск контейнера с плохим конфигом упадет сразу и громко. Hot-reload со сломанным конфигом ломается тихо и поздно.

Смягчение последствий: валидируйте конфиг перед тем, как атомарно его подменить.

Envoy молча отклоняет push xDS

istiod отправляет конфигурацию маршрута (RouteConfiguration), ссылающуюся на кластер, который еще не успел распространиться. Envoy отклоняет её и продолжает работу со старыми правилами маршрутизации. Никаких событий в поде не происходит.

Смягчение последствий: мониторьте метрику pilot_xds_push_errors и используйте команду istioctl proxy-status.

Наблюдаемость: 3 команды, которые должен знать каждый оператор

# 1. Container restart or pod recreation? Check UID change
kubectl get pod <pod> -o custom-columns=\
  "NAME:.metadata.name,UID:.metadata.uid,IP:.status.podIP,RESTARTS:.status.containerStatuses[0].restartCount"


# 2. Events on the pod
kubectl describe pod <pod> | grep -A 20 "Events:"


# 3. In-place resize status
kubectl get pod <pod> -o jsonpath='{.status.resize}'

Заключение

Перезапуск контейнера — процесс болезненный, но честный: сценарии отказа проявляются сразу и заметно. Горячая перезагрузка оптимизирует доступность, но сдвигает отказы в сторону скрытых и отложенных во времени. Обе стратегии жизнеспособны. Выбор должен быть осознанным.

Цель заключается не в том, чтобы быстрее автоматизировать перезапуски. Она в том, чтобы понимать процессы достаточно глубоко, чтобы вызывать перезапуск контейнера только тогда, когда процессу действительно необходимо умереть — и использовать другие механизмы во всех остальных случаях.

Сопутствующий репозиторий
Получите бесплатную консультацию по вашему проекту
Настроим ваш Kubernetes под новые реалии