← Kubernetes: разбор поломок
8 мин · жёстко · Урок 9 из 12

Раскат сломанной версии: откат и разбор полётов

Почему rollout прошёл «зелёным», а сервис лёг, и как настроить probe, чтобы следующий раскат встал сам.

Классика вечернего релиза: CI отрапортовал успех, kubectl rollout status вернул successfully rolled out, а через пару минут посыпались 5xx. Первый вопрос инцидента не «почему код сломан», а «как быстро вернуть прошлую версию». Разбор причин идёт после отката.

Deployment это объект Kubernetes, который держит нужное число одинаковых Pod. Pod это наименьшая единица запуска, внутри неё работает контейнер. ReplicaSet следит за числом Pod. kubectl это командная утилита для управления кластером.

История ревизий

Каждое изменение spec.template создаёт новый ReplicaSet и увеличивает номер ревизии. Ревизия это сохранённая версия шаблона Pod на момент изменения. По умолчанию Deployment хранит 10 ревизий, число задаёт поле spec.revisionHistoryLimit.

kubectl rollout history deploy/web
deployment.apps/web
REVISION  CHANGE-CAUSE
1         <none>
2         <none>

Колонка CHANGE-CAUSE берётся из annotation kubernetes.io/change-cause. annotation это произвольная пометка на объекте в формате ключ-значение, её ставят руками. Без неё в колонке стоит <none>, и во время инцидента приходится угадывать, какая ревизия что везла.

Шаблон конкретной ревизии:

kubectl rollout history deploy/web --revision=2

Команда покажет образ, env и probes этой ревизии. Удобно сравнить версии перед откатом.

Откат

На предыдущую ревизию:

kubectl rollout undo deploy/web

На конкретную:

kubectl rollout undo deploy/web --to-revision=1

Откат переиспользует старый ReplicaSet ревизии 1 и повышает его номер. После отката на ревизию 1 в истории появится ревизия 3 с тем же шаблоном. История растёт, ревизии не «отматываются назад».

Откат это плавный переход в обратную сторону, а не мгновенное переключение. Неудачный ReplicaSet сжимается до нуля, рабочий растёт до нужного числа реплик. Скорость зависит от maxSurge и maxUnavailable.

Проверяют результат так:

kubectl rollout status deploy/web --timeout=120s
kubectl get pods -l app=web
kubectl get deploy web -o jsonpath='{.spec.template.spec.containers[0].image}'

Почему readiness не остановил раскат

rollout status зелёный а сервис отдаёт 5xx ImagePullBackOff битый тег, старые Pod живы CrashLoopBackOff образ есть, процесс падает Running и Ready readiness бьёт в /, не в /healthz
Третий исход самый неприятный: Pod здоров по формальным признакам.

readinessProbe это проверка, которая решает, готов ли Pod принимать трафик. Плавный переход поднимает новые Pod небольшими группами, по умолчанию по 25% от числа реплик. Если новый Pod не проходит readinessProbe, он не помечается ready, и переход застревает. Старые Pod при этом живы и обслуживают трафик.

Сервис ломается в двух случаях.

Первый: readinessProbe нет вообще. Тогда kubelet считает Pod ready сразу после старта контейнера. kubelet это агент Kubernetes на ноде, который запускает контейнеры. Процесс упал через 5 секунд, а новые Pod уже вытеснили старые.

Второй: readinessProbe проверяет не то. Например, httpGet: / на корень отвечает 200, пока приложение ещё не подключилось к базе. Переход проходит успешно, а реальные эндпоинты падают. Именно эта проверка чаще всего и пропускает битый релиз в прод.

kubectl rollout status без --timeout в пайплайне отчитывается успехом раньше, чем кластер закончит переход. CI зелёный, сервис уже лёг.

Симптом зависит от того, чем именно сломан раскат. Если новый образ с битым тегом, kubectl rollout status может отчитаться успехом (старые Pod живы при maxUnavailable: 0), а новые Pod висят в ImagePullBackOff, статусе Pod, чей образ не скачался. Если образ скачался, но процесс падает, Pod уходит в CrashLoopBackOff: стартует и тут же падает по кругу.

Правильный readinessProbe

Проверка должна обращаться к health-эндпоинту, который подтверждает готовность приложения, а не просто факт живого веб-сервера.

readinessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5
  failureThreshold: 3

initialDelaySeconds: 5 даёт приложению стартовать перед первой проверкой. Слишком большое значение тормозит переход, слишком малое сбивает kubelet.

periodSeconds: 5 это проверка раз в 5 секунд.

failureThreshold: 3 означает, что три провала подряд снимают пометку ready, и Service убирает Pod из endpoints. endpoints это список IP-адресов Pod, на которые Service шлёт трафик.

Медленный старт (Java, Rails, прогрев кэшей) лечится отдельным startupProbe. Это проверка, которая ждёт первого старта приложения:

startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30  # 30 × periodSeconds = 5 минут на старт
  periodSeconds: 10

startupProbe отключает liveness и readiness до своего успеха. После этого readiness работает как обычно.

maxSurge и maxUnavailable

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%

maxSurge это сколько Pod разрешено создать сверх числа реплик во время перехода. Чем больше значение, тем быстрее переход и выше нагрузка на кластер.

maxUnavailable это сколько Pod разрешено убрать до подъёма новых. При значении 0 кластер не теряет ёмкость: переход идёт медленнее, зато неудачный релиз не уронит даже часть сервиса. Новые Pod не оживут, старые останутся на месте.

Как не наступать снова

Полезные команды

kubectl rollout history deploy/web
kubectl rollout history deploy/web --revision=2
kubectl rollout undo deploy/web
kubectl rollout undo deploy/web --to-revision=1
kubectl rollout status deploy/web --timeout=120s
kubectl get rs -l app=web

kubectl explain deployment.spec.strategy.rollingUpdate
kubectl explain pod.spec.containers.readinessProbe
kubectl explain pod.spec.containers.startupProbe

Домашка

Теорию закрепляет задание на своём кластере: 2 шага с проверкой. Кластер поднимается по треку Kubernetes локально, задания лежат в репозитории k8s-practice.

./run.sh start k8s-broken-rollout     # развернуть стартовое состояние
./run.sh verify k8s-broken-rollout    # проверить текущий шаг

Коротко

Прошли материал до конца? Отметьте урок пройденным.