Learning Platform
Глоссарий Troubleshooting
Урок 20.03 · 22 мин
Продвинутый
zero-downtimePodDisruptionBudgetPDBminReadySecondsconnection drainingkeep-aliveload balancerrolling update

Zero-downtime deployments

“Rolling update без downtime” — стандартное требование production. На бумаге Kubernetes это делает out-of-the-box: DeploymentRollingUpdate → новые Pods постепенно заменяют старые. На практике 5xx в момент rollout — самая частая жалоба от пользователей.

Причина — zero-downtime требует семи правильно настроенных вещей одновременно, и достаточно нарушить одну, чтобы клиенты получали ошибки. В этом уроке — полный чек-лист и каждый его пункт, плюс особенности для long-lived connections (gRPC, WebSocket) и для координации с внешним load balancer.

NOTE

Race-условие между SIGTERM и kube-proxy/EndpointSlice update разбирается детально в уроке «Graceful shutdown» (19/02). Здесь оно используется как один из пунктов чек-листа; за внутренней механикой EndpointSlice propagation идём в предыдущий урок.


Зачем нужен load balancer: scalability, redundancy, health checks

Чек-лист zero-downtime

1. readinessProbe                  — Pod не получает трафик, пока не готов
2. preStop hook (sleep 5-10s)      — даёт kube-proxy обновить правила
3. terminationGracePeriodSeconds   — бюджет на graceful shutdown
4. SIGTERM handler в приложении    — действительно делает graceful shutdown
5. minReadySeconds                 — Pod проверяется стабильным до scaling старого
6. RollingUpdate maxUnavailable=0  — никогда не теряем Pod до того как новый Ready
7. PodDisruptionBudget             — защита от parallel evictions

Без любого из пунктов rollout может уронить трафик. Разберём каждый.


1. readinessProbe — пропуск к Service

Без readinessProbe Pod считается Ready как только контейнер запустился (Phase=Running). Но Phase=Running не значит “готов принимать трафик” — приложение может ещё инициализироваться (warmup кешей, миграции, JIT). Деплоймент controller убьёт старый Pod, новый ещё не отвечает — клиенты получают 5xx.

containers:
  - name: web
    readinessProbe:
      httpGet:
        path: /healthz
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 5
      failureThreshold: 3

Pod добавляется в EndpointSlice (= получает трафик) только после первой успешной readiness probe.

WARNING

Хороший /healthz для readiness — это не просто return 200. Endpoint должен проверять реальную готовность: DB connection, Redis connection, прогретость кешей. Это не liveness probe (которая просто “процесс жив”). Грубая readiness = ложно-готовые Pods в трафике.

Подробно — см. модуль “Probes & Health Checks”.


2. preStop sleep — race с kube-proxy

Когда Pod marked for deletion, EndpointSlice обновляется и kube-proxy на нодах должен убрать iptables/IPVS правило. Это distributed update, занимает 1-5 секунд. За это время kubelet уже шлёт SIGTERM и приложение закрывает listener.

lifecycle:
  preStop:
    exec:
      command: ["sh", "-c", "sleep 10"]

preStop sleep 5-10s блокирует SIGTERM, давая kube-proxy distribute update. После sleep трафик уже не приходит — SIGTERM безопасен. Подробно — урок “Graceful shutdown”.


3. terminationGracePeriodSeconds — бюджет

spec:
  terminationGracePeriodSeconds: 60     # включает preStop + grace после SIGTERM

Должно покрывать: preStop sleep + время на завершение in-flight requests. Для HTTP с быстрыми requests хватает 30-60s. Для long-lived (file uploads, gRPC streams) — может потребоваться больше.


4. SIGTERM handler — приложение готово

App должен handle SIGTERM:

  1. Stop accepting new connections.
  2. Wait for in-flight requests.
  3. Close DB pool, file handles.
  4. Exit с 0.

Если игнорирует SIGTERM — graceful shutdown превращается в SIGKILL через terminationGracePeriodSeconds → in-flight requests умирают.


5. minReadySeconds — wait для стабильного Pod

spec:
  minReadySeconds: 15

Pod считается Available только если он Ready=True непрерывно дольше minReadySeconds. Если Ready перешёл в False — таймер сбрасывается.

Зачем: иногда Pod становится Ready, проходит несколько успешных probes, и потом падает (cold caches, lazy init issues, JIT failures). Без minReadySeconds — controller сразу после первого Ready считает Pod Available и убивает следующий старый. С minReadySeconds — controller ждёт стабильности.

spec:
  minReadySeconds: 30
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 0

Production-grade: 15-60 секунд. Зависит от warmup time приложения.


6. RollingUpdate maxUnavailable=0

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 0       # никогда не теряем Available Pod

maxUnavailable=0 — controller никогда не убивает старый Pod, пока не дождётся нового Ready+Available. Это медленнее (нет parallel scale-down), но гарантирует full capacity всегда.

Подробно про инварианты — модуль “Deployment Strategies”.


7. PodDisruptionBudget — защита от parallel disruptions

PDB отдельная проблема от rolling update. Защищает от voluntary disruptions:

  • kubectl drain node (cluster autoscaler removes node, admin maintenance).
  • Eviction по Pod priority, dischargeerror.
  • Не от: node crash, OOMKill, Pod delete без drain.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web-pdb
spec:
  minAvailable: 80%               # или maxUnavailable: 1
  selector:
    matchLabels:
      app: web

Сценарий без PDB:

  1. Cluster autoscaler решает удалить node A → drain.
  2. Drain пытается evict все Pods с node A.
  3. Pod web-1 и web-2 на node A → evict оба одновременно.
  4. Если они были part of replicas=4, ныне Available count = 2. Если ещё параллельно идёт rolling update → может уйти ниже.

С PDB minAvailable=80%:

  1. Drain пытается evict web-1, web-2 на node A.
  2. API проверяет PDB: после evict web-1 минимально доступных всё ещё ≥ 80%? Если да — evict OK. Если нет (например, параллельно идёт rolling) — eviction блокирован, drain ждёт.
# Альтернативно
spec:
  maxUnavailable: 1               # не более одного Pod недоступно
NOTE

PDB только для voluntary disruptions. Если node физически крашится — все Pod-ы исчезают, PDB не помогает (это involuntary disruption). PDB это про координацию во время плановых операций.

PDB защищает от parallel evictions во время drain
без PDBNode A удаляется, drain evict-ит все Pods на ней. Если web-1 и web-2 оба на node A — evicted ОДНОВРЕМЕННО. Available count резко падает.
результатAvailable count может опуститься ниже допустимого. Сервис деградирует. Latency растёт.
vs
с PDB maxUnavailable=1Drain пытается evict web-1 — OK (1 unavailable). Пытается evict web-2 — БЛОКИРОВАН (это уже 2 unavailable, превышает PDB). Ждёт пока новый Pod на другой ноде запустится и станет Ready.
результатSequential eviction. Между evictions новые Pods создаются на других нодах. Available count никогда не падает ниже PDB threshold.

Connection draining: long-lived connections

HTTP короткие requests — стандартный rolling update работает без особых усилий. Но long-lived connections требуют отдельной обработки:

HTTP/1.1 keep-alive

Если LB переиспользует connection, traffic продолжает идти на старый Pod даже после endpoint removal. Решение — в SIGTERM handler:

// pseudocode
srv.SetKeepAlivesEnabled(false)
// заставляет добавлять "Connection: close" header в next response
// клиент после этого закроет connection

После SIGTERM сервер должен respond Connection: close — LB не reuse этот connection.

gRPC streaming

gRPC streams могут жить минутами/часами. После SIGTERM:

  1. App шлёт GOAWAY frame клиенту (HTTP/2 graceful disconnect signal).
  2. Клиент должен reconnect на другой Pod через service mesh / DNS resolver.
  3. App ждёт active streams завершения (или timeout) до exit.
grpcServer.GracefulStop()   // ждёт all RPCs done, потом stop

В production-grade gRPC: GRPC_GO_LOG_VERBOSITY_LEVEL=2, проверять GOAWAY обработку клиентами.

WebSocket

Похоже на gRPC. App должен:

  1. Шлёт frame “server going down” клиенту.
  2. Клиент reconnect.
  3. App закрывает socket после ack или timeout.

terminationGracePeriodSeconds для WebSocket-heavy apps часто 120-300s — чтобы дать всем клиентам reconnect.


Database connection pool

При SIGTERM:

# pseudocode
async def shutdown():
    server.stop_accepting()
    await server.wait_for_in_flight()  # active requests завершатся
    await db_pool.close()              # ROLLBACK uncommitted, close sockets
    await metrics_flush()
    sys.exit(0)

Закрыть connection pool до exit. Иначе:

  • Активные queries прерваны RST → some DB-серверы (PostgreSQL) logging это как error.
  • Connections в pool остаются open на DB-стороне до timeout (60-120s) → лишний резерв БД.

Coordination с внешним Load Balancer

В managed K8s Service типа LoadBalancer создаёт cloud-resource (AWS ELB, GCP LB, Azure LB). Эти LB имеют свой health check, свой TTL, свою propagation latency.

Ловушка: K8s Pod removed instantly из EndpointSlice → kube-proxy убирает iptables → но внешний LB ещё ~30s держит target в healthy pool (он же независимо проверяет каждые 10-30s). Если ваш сервис принимает трафик напрямую от LB (NodePort или target Pod IP) — может быть drop.

External LB имеет свой TTL независимо от K8s
AWS ELBELB health check каждые 30s. Когда target становится unhealthy — deregister после 2-3 failed checks. То есть до 60-90s latency между 'Pod removed в K8s' и 'ELB перестал слать трафик'.
K8s EndpointSliceОбновляется мгновенно при Pod removal. kube-proxy на нодах синхронизируется за 1-5s. Но это ВНУТРИ кластера — ELB снаружи.
разрывK8s: трафик от kube-proxy остановлен. ELB: трафик ещё идёт на NodePort/Pod IP, потому что ELB думает что target healthy. Если LB пробрасывает на NodePort или Pod IP напрямую — клиенты получают connection refused в этот gap.

Решения:

  1. AWS ALB ingress controller + IP-mode: target в ELB — Pod IP напрямую, ALB наблюдает за health через K8s readiness probe. Sync быстрее.
  2. Long terminationGracePeriodSeconds + preStop sleep, чтобы Pod продолжал отвечать пока ELB не deregister.
  3. Service mesh (Istio, Linkerd) — sidecar обрабатывает graceful shutdown, координирует с control plane.

Полный production-grade шаблон

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 4
  minReadySeconds: 20
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 0
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: web
          image: my-web:v1.2.3
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
            failureThreshold: 3
          livenessProbe:
            httpGet:
              path: /healthz/live
              port: 8080
            periodSeconds: 10
            failureThreshold: 5
          lifecycle:
            preStop:
              exec:
                command: ["sh", "-c", "sleep 10"]
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web-pdb
spec:
  minAvailable: 75%
  selector:
    matchLabels:
      app: web

И в коде:

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM)
go srv.ListenAndServe()
<-sigChan
ctx, cancel := context.WithTimeout(ctx, 45*time.Second)
defer cancel()
srv.Shutdown(ctx)
db.Close()

Killer-моменты

  • Zero-downtime = 7 пунктов одновременно. Один пропущенный — клиенты получают 5xx. Это не “one trick”, а system property.
  • maxUnavailable=0 + maxSurge>0 — гарантия full capacity всегда. Самый безопасный rolling update. Чуть медленнее.
  • PDB только для voluntary disruptions (drain). От crash/OOMKill не защищает.
  • minReadySeconds — страховка от Pod-ов, которые становятся Ready, но сразу падают. 15-30s типично.
  • External LB имеет свой TTL независимо от K8s. На AWS — targetType: ip (ALB) обходит часть проблем.
  • HTTP/1.1 keep-alive требует Connection: close после SIGTERM, иначе LB reuse dead connection.
  • gRPC graceful: GracefulStop() ждёт active RPCs, шлёт GOAWAY клиентам. Без него — abrupt termination.

Проверка знанийKnowledge check
Rolling update идёт, но клиенты иногда получают 502. readinessProbe есть, terminationGracePeriodSeconds=30. Что проверить?
ОтветAnswer
Самые частые причины: (1) Нет preStop hook со sleep — SIGTERM приходит до того, как kube-proxy обновил правила на всех нодах. Трафик идёт на Pod, который уже не отвечает. Fix: lifecycle.preStop.exec.command sleep 5-10. (2) maxUnavailable > 0 — может быть момент когда Available < replicas. Fix: maxUnavailable=0. (3) Приложение не handles SIGTERM — после терминации просто прибивается через 30s SIGKILL, in-flight requests умирают. Fix: signal handler с graceful shutdown. (4) readinessProbe слишком наивная (return 200 без проверки реальной готовности) — Pod marked Ready пока DB connection не установлен. Fix: real /healthz с проверкой dependencies. (5) HTTP keep-alive — LB reuse dead connection, нужно Connection: close в SIGTERM handler.
Проверка знанийKnowledge check
PDB minAvailable=80% для Deployment replicas=10. Drain ноды, на которой 3 Pod-а. Что произойдёт?
ОтветAnswer
80% от 10 = 8 Pods минимально доступных. Текущий count 10 — Available=10. Drain пытается evict 3 Pods последовательно: (1) evict Pod-1 → Available=9 (всё ещё >= 8) → OK. (2) evict Pod-2 → Available=8 (= 8, минимально допустимое) → OK. (3) evict Pod-3 → Available=7 (< 8) → API отклоняет eviction с 429 'cannot evict due to PDB'. Drain ЖДЁТ пока не появится новый Pod на другой ноде (через ReplicaSet → scheduling). Только когда Available снова >= 9 — drain может evict Pod-3. То есть drain не падает, а просто становится медленнее, гарантируя что 80% capacity всегда сохранено.
Проверка знанийKnowledge check
gRPC server с long-lived bidirectional streams. Как обеспечить zero-downtime rolling update?
ОтветAnswer
Требования больше чем для HTTP/1.1: (1) terminationGracePeriodSeconds увеличить до 300s+ — стримы могут жить долго. (2) В SIGTERM handler вызвать grpcServer.GracefulStop() (Go) или аналог — он перестаёт accept new RPCs, шлёт GOAWAY всем connected клиентам (HTTP/2 graceful disconnect signal), ждёт пока active RPCs завершатся. (3) preStop sleep 10s (kube-proxy update). (4) Клиенты должны handle GOAWAY: gRPC client автоматически reconnect на новый Pod через DNS/service mesh. Без правильной handling GOAWAY в клиенте — стримы обрываются, retry logic должна compensate. (5) Service mesh (Istio/Linkerd) упрощает: sidecar обрабатывает GOAWAY централизованно. (6) Не использовать maxUnavailable > 0 — нужен maxSurge для стандартного rolling без drops. (7) PDB для drain protection.
Проверка знанийKnowledge check
Какая разница между PodDisruptionBudget и RollingUpdate maxUnavailable, и зачем нужны оба?
ОтветAnswer
Они защищают от РАЗНЫХ типов disruptions. maxUnavailable в Deployment strategy — это про rolling update (controller scaling new RS up + old RS down). Только когда image меняется. PodDisruptionBudget — это про voluntary disruptions от ДРУГИХ источников: node drain (autoscaler, admin maintenance), eviction по priority, kubectl drain. Сценарии где нужны оба: (1) Идёт rolling update Deployment-а, ПАРАЛЛЕЛЬНО cluster autoscaler решил удалить ноду. Drain пытается evict Pod, который и так в rolling update. Без PDB — может уйти ниже минимально допустимого Available. С PDB — eviction блокирован, ждёт окончания rolling. (2) Только PDB, без rolling update strategy — image не обновляется, но cluster autoscaler делает drain. PDB защищает. (3) Только maxUnavailable, без PDB — rolling работает, но первый же drain ноды может уронить трафик. Production требует ОБА.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. PodDisruptionBudget minAvailable=80% для Deployment replicas=10. kubectl drain node-A пытается evict 3 Pods, которые на ней работают. Что произойдёт?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5