убрать 502 при деплое за счёт корректного завершения подов
Корректное завершение подов в Kubernetes: PodDisruptionBudget, PreStop и отведение соединений
13 минут
Ошибки 502 при деплое и drain нод часто связаны не со стратегией обновления, а с тем, как под принимает SIGTERM. Сочетайте отведение соединений в приложении, паузу PreStop, PDB для добровольных нарушений и учёт сайдкаров — тогда пользователи не замечают выкат.
Почему rolling update всё ещё даёт 502
Пользователи видят 502 и обрывы соединений при выкате, даже когда стратегия обновления выглядит верной. Kubernetes может остановить под в любой момент: rolling update, drain ноды, сокращение Cluster Autoscaler, вытеснение (preemption). По умолчанию kubelet шлёт SIGTERM, ждёт terminationGracePeriodSeconds (30 с), затем SIGKILL. Большинство приложений не успевают завершить текущую работу. Важна последовательность: под переходит в Terminating и асинхронно убирается из Endpoints и EndpointSlices, параллельно выполняется PreStop, затем контейнер получает SIGTERM. Если процесс сразу выходит, kube-proxy и облачный балансировщик ещё могут слать трафик на умирающий IP. Сайдкары service mesh иногда гаснут раньше приложения и режут исходящие вызовы. Без PodDisruptionBudget один drain может снять все реплики сразу. По отдельности сбои выглядят мелкими; во время выката они складываются в заметные пользователям ошибки.
Отведение нагрузки в приложении по SIGTERM
Настройки платформы не спасут приложение, которое игнорирует SIGTERM. По этому сигналу нужно перестать принимать новую работу, дожать уже открытую и выйти с кодом ноль. HTTP-сервер закрывает listener, ждёт активные обработчики и завершается. gRPC вызывает GracefulStop: новые RPC отклоняются, текущие доводятся до конца. Воркеры очередей заканчивают текущее сообщение и не берут следующие. Пул к базе закрывает свободные соединения и ждёт занятые, а не рвёт сокеты. Добавьте журналы: получен SIGTERM, сколько активных соединений, когда завершение окончено. SIGKILL перехватить нельзя — всё полезное должно случиться до него.
PreStop закрывает гонку с удалением эндпоинтов
PreStop выполняется до SIGTERM и входит в terminationGracePeriodSeconds. Короткая пауза даёт kube-proxy, потребителям EndpointSlice и облачному балансировщику время перестать слать новый трафик, пока процесс ещё не начал отведение соединений. Для ClusterIP часто хватает пяти секунд; для mesh и внешних балансировщиков — десяти–пятнадцати. Держите hook простым: зависание съест всё окно завершения. Сочетайте PreStop с readiness: в Terminating пробы должны падать, чтобы под быстрее ушёл из Ready. Формула для grace period: задержка PreStop плюс самая длинная ожидаемая обработка запроса плюс запас. Тридцать секунд по умолчанию редко хватает платёжным API и long-poll.
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- sleep 10PodDisruptionBudget ограничивает добровольные нарушения
PodDisruptionBudget ограничивает добровольные нарушения — drain, плановые обновления нод, добровольный eviction — но не падения нод и не OOM. minAvailable задаёт нижнюю границу здоровых подов. maxUnavailable удобен в процентах, когда число реплик меняется. Для критичных API с фиксированным полом реплик чаще берут абсолютный minAvailable. На нагрузках с несколькими репликами и пользовательским трафиком PDB обязателен: без него один drain может убрать все поды. PDB не мешает Deployment снизить replicas до нуля по вашей команде, а принудительный drain может обойти бюджет. С Kubernetes 1.26+ unhealthyPodEvictionPolicy определяет, блокирует ли PDB выселение подов, которые так и не стали Ready.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-server-pdb
namespace: production
spec:
minAvailable: 2
selector:
matchLabels:
app: api-serverapiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-server-pdb
namespace: production
spec:
maxUnavailable: "25%"
selector:
matchLabels:
app: api-serverПродакшен-схема: rolling update, PreStop и PDB вместе
Сведите осторожную стратегию обновления, паузу PreStop, реалистичный grace period, readiness и PDB. maxUnavailable ноль при небольшом maxSurge сохраняет ёмкость, пока новые поды не станут Ready. Sleep в PreStop закрывает задержку распространения эндпоинтов. terminationGracePeriodSeconds должен превышать PreStop плюс отведение в приложении. PDB не даёт добровольным drain опуститься ниже порога доступности. Этот каркас не заменяет обработку SIGTERM — он лишь даёт приложению нужное окно времени.
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
namespace: production
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: payment-service
template:
metadata:
labels:
app: payment-service
spec:
terminationGracePeriodSeconds: 90
containers:
- name: payment-service
image: registry.example.com/payment-service:v2.4.1
ports:
- containerPort: 8080
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
readinessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 5
failureThreshold: 2
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: payment-service-pdb
namespace: production
spec:
minAvailable: 2
selector:
matchLabels:
app: payment-serviceЗавершение с учётом сайдкаров и native sidecars
Классические внедрённые прокси mesh часто получали SIGTERM одновременно с приложением и гасли первыми, обрывая исходящие вызовы посреди отведения. Старые обходы в Istio — длинный PreStop и annotation proxy.istio.io/config с terminationDrainDuration, чтобы Envoy жил, пока приложение заканчивает работу. holdApplicationUntilProxyStarts решает порядок запуска, а не остановки — не путайте эти настройки. Предпочтительнее native sidecars Kubernetes (в свежих релизах стабильны и часто включены по умолчанию): прокси объявляют в initContainers с restartPolicy Always. Такой сайдкар стартует до приложения и остаётся до выхода обычных контейнеров — это убирает большинство гонок «прокси умер первым». Linkerd и актуальный Istio опираются на эту модель; перед снятием drain-аннотаций проверьте версии кластера и mesh. shareProcessNamespace и обёртки оставляйте только как мост на старых кластерах.
Проверка, метрики и согласование со SLO
Проверьте путь на стенде: сделайте kubectl drain на ноде с репликой или пошлите SIGTERM внутрь пода и убедитесь, что клиенты не теряют запросы. Согласуйте kubectl drain --grace-period с terminationGracePeriodSeconds пода, иначе drain пришлёт SIGKILL раньше. Смотрите долю 5xx и обрывов соединений во время Deployment; сопоставляйте с событиями Terminating и журналами отведения в приложении. Если p99 — две секунды, а grace 30 с при PreStop 10 с, длинные обработчики всё равно могут попасть под SIGKILL — считайте окно по худшей длительности запроса плюс PreStop. У headless Service и StatefulSet другая семантика эндпоинтов; клиенты должны повторять запрос и заново резолвить DNS при сбое. Корректное завершение незаметно, когда работает: пользователи не видят выкат. Когда нет — каждый деплой играет против вашего error budget.
Когда поды корректно завершаются, проверяйте выкат через автоматический анализ canary и откат по SLO.
Доступность при нарушениях входит в error budget из гайда по SLO и SLI для платформенных команд.
