
Проблема терминологии
Инженеры говорят «под перезапустился», когда имеют в виду четыре совершенно разные вещи. Ошибки в этом понимании ведут к дефектным ранбукам и неверным решениям во время дежурств (on-call).
| Термин | Меняется ли UID? | Меняется ли IP-адрес? | Счетчик перезапусков (Restart Count) |
|---|---|---|---|
| Перезапуск контейнера (перезапуск процесса внутри того же пода) | Нет | Нет | +1 |
| Пересоздание пода (rolling update, drain) | Да | Да | Сбрасывается в 0 |
| In-place изменение размера (1.35 GA) — CPU | Нет | Нет | 0 |
| In-place изменение размера (1.35 GA) — memory (политика RestartContainer) | Нет | Нет | +1 |
Практический тест: изменился ли UID пода? Если да — это пересоздание (recreation), а не перезапуск контейнера. Счетчик перезапусков сбрасывается в ноль. Если нет — объект пода тот же самый, внутри него перезапустился процесс контейнера.
Главный инсайт: за чем на самом деле следит kubelet
kubelet следит за спецификацией пода (pod spec) — не за ConfigMap, не за Secrets, не за CRD-ресурсами Istio. Если pod spec не изменился, kubelet никогда не сработает. Этот единственный факт объясняет большинство расследований в продакшене на тему «почему мой конфиг не обновился?».
Mutating admission webhooks могут изменять pod spec в момент создания, но никогда после допуска (admission) — они не могут инициировать перезапуски контейнеров после создания.
Матрица решений
| Изменение | Перезапуск контейнера? | Под пересоздается? | Автоматически? |
|---|---|---|---|
| Образ контейнера | Да | Да | Да — Deployment controller |
| Переменная окружения | Да | Нет | Ручной rollout |
| ConfigMap — volume mount | Решает приложение | Нет | Частично — приложение должно отслеживать inotify |
| ConfigMap — envFrom | Да | Нет | Ручной rollout |
| Secret — volume mount | Решает приложение | Нет | Частично — приложение должно отслеживать inotify |
| Secret — envFrom | Да | Нет | Ручной rollout |
| Проецируемый токен ServiceAccount | Никогда | Нет | Да — kubelet ротирует автоматически |
| Изменение CPU (K8s 1.35+) | Никогда | Нет | Ручной патч |
| Изменение Memory (K8s 1.35+) | Согласно resizePolicy | Нет | Ручной патч |
| Istio VirtualService / DestinationRule | Никогда | Нет | Да — push через xDS |
| NetworkPolicy | Никогда | Нет | Да — CNI-агент |
| Порты сервиса | Никогда | Нет | Да — kube-proxy |
| RBAC | Никогда | Нет | Да — API-сервер |
| Drain / эвикция ноды | Да | Да | Да — автоматически |
Диаграмма ниже переводит эту же матрицу в путь принятия решений, по которому вы можете пройти даже в 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()
}
}
Сценарий 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}'
Заключение
Перезапуск контейнера — процесс болезненный, но честный: сценарии отказа проявляются сразу и заметно. Горячая перезагрузка оптимизирует доступность, но сдвигает отказы в сторону скрытых и отложенных во времени. Обе стратегии жизнеспособны. Выбор должен быть осознанным.
Цель заключается не в том, чтобы быстрее автоматизировать перезапуски. Она в том, чтобы понимать процессы достаточно глубоко, чтобы вызывать перезапуск контейнера только тогда, когда процессу действительно необходимо умереть — и использовать другие механизмы во всех остальных случаях.

