Раскат сломанной версии: откат и разбор полётов
Почему 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 не остановил раскат
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 не оживут,
старые останутся на месте.
Как не наступать снова
maxUnavailable: 0для критичных сервисов.- readinessProbe в health-эндпоинт с проверкой зависимостей, а не в
/. - annotation
kubernetes.io/change-causeна каждый релиз, чтобы история ревизий читалась во время инцидента.
Полезные команды
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 # проверить текущий шаг
Коротко
- Сначала откат, потом расследование:
kubectl rollout undoвозвращает прошлый ReplicaSet. kubectl rollout history --revision=Nпоказывает образ и probes конкретной ревизии.- Успешный
rollout statusне означает работающий сервис, если readinessProbe нет или она бьёт в/. maxUnavailable: 0не даёт битому релизу отобрать ёмкость у живых Pod.- Медленный старт закрывают startupProbe, а не раздутым
initialDelaySecondsв liveness.
Прошли материал до конца? Отметьте урок пройденным.
Прогресс сохранён в этом браузере
Почистишь историю или откроешь с телефона, и отметки пропадут. Оставь почту, они переедут в аккаунт. Пароль придумывать не нужно.
Готово. теперь в аккаунте: прогресс виден с любого устройства. Мой прогресс