RollingUpdate: механика maxSurge и maxUnavailable
RollingUpdate — стратегия по умолчанию для Deployment. Она звучит просто: “постепенно заменим старые Pods на новые”. Но под капотом это сложный танец двух ReplicaSet-ов: один скейлится вверх, второй — вниз, причём так, чтобы в любой момент времени сумма Available-Pods не упала ниже инварианта.
Понять механику — значит понять, почему maxSurge=0 и maxUnavailable=0 нельзя поставить вместе, почему без readinessProbe rolling update превращается в roulette, и почему CKAD-задание “обновить без downtime” — это всегда про конкретные значения этих двух параметров.
Compose vs Kubernetes: когда переходить к оркестратору
Что такое RollingUpdate
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.27
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 2
periodSeconds: 5
Поля стратегии:
spec.strategy.type: RollingUpdate— постепенная замена (default)spec.strategy.rollingUpdate.maxSurge— сколько Pod-ов СВЕРХspec.replicasможно создать (default25%)spec.strategy.rollingUpdate.maxUnavailable— сколько Pod-ов могут быть UNAVAILABLE одновременно (default25%)
Оба параметра — либо процент (округляется вверх для surge, вниз для unavailable), либо абсолютное число. Для replicas=4 и default 25%:
maxSurge = ceil(4 * 0.25) = 1maxUnavailable = floor(4 * 0.25) = 1
Это значит: в пике может быть до 5 Pods, минимум 3 Available.
Mechanics: что делает DeploymentController
DeploymentController — это reconcile loop внутри kube-controller-manager. Он наблюдает за Deployment-ом и при изменении spec.template запускает rollout. Алгоритм по шагам.
Важно: controller не “знает план” из 4 шагов заранее. Он просто реагирует на каждое изменение статуса:
- Видит desired=4, текущий new RS replicas < desired → пытается скейлить вверх, ограничен
maxSurge - Видит Available >
replicas - maxUnavailable→ может убить старый Pod - Повторяет, пока new RS не достигнет desired, а old RS — 0
Это reconciliation pattern, не imperative script.
maxSurge и maxUnavailable: инварианты
Два математических инварианта, которые controller всегда сохраняет:
- Pod count ≤
replicas + maxSurge - Available Pod count ≥
replicas - maxUnavailable
Из этих двух инвариантов следует поведение rollout-а при разных конфигурациях.
maxSurge=1, maxUnavailable=0 — safe, no downtime
spec:
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
- Pod count ≤ replicas + 1
- Available count ≥ replicas (никогда не теряем Pod до того как новый Ready)
Сначала создаётся 1 новый Pod, ждёт Ready, потом убивается 1 старый. Цикл повторяется. Самый безопасный вариант, никогда нет downtime. Расход: +1 Pod ресурса на время rollout.
maxSurge=0, maxUnavailable=1 — fast, with planned downtime
spec:
strategy:
rollingUpdate:
maxSurge: 0
maxUnavailable: 1
- Pod count ≤ replicas (никаких лишних)
- Available count ≥ replicas - 1
Сначала убивается 1 старый, потом на его место создаётся 1 новый. На время — пока новый не Ready — Available count меньше replicas. Это planned mini-downtime. Использовать, когда нет ресурсов для surge.
maxSurge=25%, maxUnavailable=25% — default
Default. Для replicas=4: surge=1, unavailable=1. В пике 5 Pods, минимум 3 Available. Компромисс между скоростью и расходом ресурсов.
maxSurge=100%, maxUnavailable=0 — blue-green-like
spec:
strategy:
rollingUpdate:
maxSurge: 100%
maxUnavailable: 0
Сразу создаётся вторая копия (2× ресурсов), ждёт пока все новые Ready, потом убиваются все старые. Без downtime, но временно нужен двукратный объём ресурсов.
maxSurge=0 и maxUnavailable=0 ОБА вместе — невалидная конфигурация. API-сервер отклонит её: новый Pod создать нельзя (surge=0), старый удалить нельзя (unavailable=0) → controller заблокирован, rollout deadlock. Хотя бы один из двух должен быть > 0.
Readiness probe — критична для rolling update
Без readinessProbe Pod считается Available как только его main-process запустился (Phase=Running). Это до того как приложение реально готово принимать трафик. Rolling update в таком случае работает в blind-mode: controller думает “новый Pod ready”, убивает старый, а приложение ещё инициализируется.
containers:
- name: app
image: my-app:v2
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
С readinessProbe:
- Pod становится Ready только когда
/healthzотвечает 200 OK - Service не отправляет трафик на Pod, у которого Ready=False
- Deployment controller не считает такой Pod в
availableReplicas, не идёт дальше rollout
Rolling update БЕЗ readiness probe — частая ошибка. Phase=Running != “готов к трафику”. В первые секунды приложения часто отвечают ошибкой или висят. Если controller думает что Pod готов и убивает старый — клиенты получают 5xx. Всегда ставить readinessProbe для любого Pod, который принимает трафик.
minReadySeconds: ждать после Ready
spec:
minReadySeconds: 10
Pod считается Available только когда он Ready дольше minReadySeconds подряд. Это страховка против Pod-ов, которые становятся Ready, а потом сразу падают.
minReadySeconds=0(default) — Pod Available мгновенно после Ready=TrueminReadySeconds=10— controller ждёт 10 секунд после Ready, прежде чем считать Pod Available и двигаться дальше
Use case: приложение запускается за 2 секунды, но первые 5 секунд работает на холодных кешах и иногда падает. minReadySeconds=10 гарантирует, что мы не убьём старый Pod в этот неуверенный период.
Полный rollout: трассировка
CKAD-сценарий — обновить image и наблюдать поведение.
kubectl set image deploy/web nginx=nginx:1.28
kubectl rollout status deploy/web
# Waiting for deployment "web" rollout to finish: 1 out of 4 new replicas have been updated...
# Waiting for deployment "web" rollout to finish: 2 of 4 updated replicas are available...
# Waiting for deployment "web" rollout to finish: 3 out of 4 new replicas have been updated...
# deployment "web" successfully rolled out
# Параллельно watch
kubectl get pods -l app=web -w
# web-old-abc12 1/1 Running 0 10m
# web-old-def34 1/1 Running 0 10m
# web-old-ghi56 1/1 Running 0 10m
# web-old-jkl78 1/1 Running 0 10m
# web-new-mno90 0/1 ContainerCreating
# web-new-mno90 0/1 Running
# web-new-mno90 1/1 Running
# web-old-abc12 1/1 Terminating
# web-new-pqr12 0/1 ContainerCreating
# ...
Что видно:
- В каждый момент времени count Pods ≤ 5 (= 4 + maxSurge=1)
- Available count ≥ 3 (= 4 - maxUnavailable=1)
- Pod становится Ready после успешной readiness probe
- Старый Pod не убивается, пока новый не Ready
# Смотрим, какие RS существуют
kubectl get rs -l app=web
# NAME DESIRED CURRENT READY
# web-7d4b9f4f7 0 0 0 # старый (сохранён для rollback)
# web-9c2e8a55b 4 4 4 # новый
Killer-моменты
maxSurge=0+maxUnavailable=0= deadlock, API server отклонит. Хотя бы один должен быть > 0.- Без readinessProbe rolling update не safe: Phase=Running != ready, controller думает что Pod готов, а он ещё инициализируется.
minReadySeconds— страховка против Pod-ов, которые Ready, но сразу падают. Production-grade.- Pod-template-hash в RS selector —
DeploymentControllerдобавляет deterministic hash в label каждого Pod-а, чтобы RS управлял только Pod-ами своей ревизии и не путал их со старыми. progressDeadlineSeconds(default 600) — за сколько секунд rollout должен завершиться. Если нет — Deployment.status.conditions.Progressing = False, Reason=ProgressDeadlineExceeded. Это сигнал для CI/CD что rollout сломан.