Отказоустойчивость в Kubernetes держится на трёх независимых уровнях: kubelet перезапускает контейнеры внутри пода, контроллеры следят за числом подов, scheduler перепланирует поды с упавших узлов. Чтобы сервис переживал сбои без ручного вмешательства, настраивают четыре вещи: probes, количество реплик, PodDisruptionBudget и стратегию обновления. Отдельная зона риска - etcd, хранилище состояния всего кластера.
Коротко о логике работы. Liveness probe перезапускает зависший контейнер, readiness probe убирает неготовый под из балансировки, startup probe даёт медленно стартующему приложению время без ложных перезапусков. Две-три реплики плюс PDB с minAvailable: 2 позволяют пережить отказ узла и плановый drain. Rolling update с maxUnavailable: 0 обновляет версию без простоя, blue-green и canary добавляют контроль риска.
Ниже разбираем каждый механизм до уровня манифеста: параметры probes, YAML для Deployment и PDB, примеры blue-green и canary, команды для etcd и набор проверок, которые стоит прогнать перед выходом в прод.
Как Kubernetes обеспечивает самовосстановление и отказоустойчивость
Самовосстановление работает без внешних скриптов. Достаточно, чтобы объект в API описывал желаемое состояние: контроллеры сравнивают его с текущим и устраняют расхождение. Упал контейнер - kubelet поднимет его снова. Удалили под - ReplicaSet создаст замену. Узел ушёл в NotReady - поды переедут на живые узлы.
Устройство control plane и объектов API, включая Pods, Deployments и Services, разбираем в материале про архитектуру кластера. Здесь важен практический вывод: самовосстановление включается самой моделью декларативных объектов, а не дополнительной настройкой.
Роль kubelet и контроллеров в перезапуске подов
kubelet работает на каждом узле и следит за контейнерами пода. Поведение при завершении контейнера задаёт restartPolicy: Always перезапускает всегда, OnFailure только при ненулевом коде выхода, Never оставляет контейнер остановленным. При серии падений под уходит в CrashLoopBackOff, а задержка между попытками растёт экспоненциально: 10 с, 20 с, 40 с и дальше до пяти минут.
Контроллеры отвечают за количество подов, а не за их внутреннее состояние. ReplicaSet сравнивает фактическое число подов с spec.replicas и создаёт либо удаляет поды. Deployment управляет ReplicaSet и историей версий. Ручное удаление пода проверяется за секунды:
kubectl delete pod nginx-7d8f9c6b5-xk2lp kubectl get pods -l app=nginx -w
Через несколько секунд в выводе появится новый под с другим суффиксом, а счётчик AGE сбросится. Если под не создаётся, смотрите Events в описании ReplicaSet: чаще всего причина в квотах, ресурсах или невыполнимых требованиях affinity.
Одна деталь окружения, которую легко упустить: kubelet и container runtime должны использовать один и тот же cgroup driver и одинаковую конфигурацию. Иначе поведение контейнеров становится непредсказуемым. Требование зафиксировано в документации по container runtime, там же описаны поддерживаемые CRI-рантаймы.
Реакция на отказ узла: перепланирование подов
Когда узел перестаёт отвечать, контроллер узла помечает его как NotReady. По истечении таймаута (по умолчанию около пяти минут) поды на нём переводятся в Terminating и удаляются из API, а контроллеры создают замены на доступных узлах. Время недоступности складывается из таймаута обнаружения, выбора нового узла, скачивания образа и прохождения readiness probe.
Два условия обязательны. Первое: replicas больше единицы, иначе сервис недоступен всё время перепланирования. Второе: корректные probes, по которым Service понимает, что под готов принимать трафик.
Чтобы реплики не собирались на одном узле или в одной зоне, задайте распределение через topologySpreadConstraints:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api
Альтернатива - podAntiAffinity с topologyKey: kubernetes.io/hostname. Правило гарантирует, что два пода одной роли не окажутся на одном узле, и отказ машины не уносит весь сервис.
Настройка probes: liveness, readiness и startup
Probes - самый частый источник ложных перезапусков. Контейнер, которому не хватает времени на старт, циклически убивается kubelet, хотя приложение исправно. Три типа проверок решают разные задачи, и их нельзя заменять друг другом.
| Probe | Что проверяет | Реакция при провале | Типичный сценарий |
|---|---|---|---|
| startupProbe | Приложение завершило инициализацию | Перезапуск контейнера | Долгий прогрев кэша, миграции |
| livenessProbe | Процесс жив и не завис | Перезапуск контейнера | Дедлок, зависший воркер |
| readinessProbe | Под готов принимать трафик | Исключение из Endpoints | Потеря соединения с БД, перегрузка |
Liveness probe: когда перезапускать контейнер
Liveness probe проверяет, что процесс отвечает. Если проверка не проходит failureThreshold раз подряд, kubelet перезапускает контейнер. Проверка должна быть дешёвой и не зависеть от внешних сервисов: недоступная база не повод убивать под, это задача readiness.
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
successThreshold: 1
С таким набором контейнер перезапускается после трёх провалов по 5 секунд, то есть примерно через 15 секунд реального зависания. Агрессивные значения опасны: timeoutSeconds: 1 при пиковой нагрузке даст ложные срабатывания и шторм перезапусков. Держите отдельный эндпоинт /healthz, который проверяет только внутреннее состояние процесса, и отдельный /ready для зависимости от БД и кэша.
Для не-HTTP сервисов используйте TCP-сокет или команду:
livenessProbe:
tcpSocket:
port: 5432
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
exec:
command: ["pg_isready", "-U", "postgres"]
periodSeconds: 10
failureThreshold: 6
Readiness probe: как не отправлять трафик на неготовый под
Readiness probe определяет, попадёт ли под в список Endpoints сервиса. Провал проверки не перезапускает контейнер, под просто выводится из балансировки. Это ключевой механизм rolling update: новый под не получит трафик, пока не станет готов, а старый не будет удалён раньше времени.
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 2
successThreshold: 1
Эндпоинт /ready должен проверять зависимости, без которых запросы не обрабатываются: соединение с базой, доступность Redis, загруженные конфиги. Если проверка ходит в БД на каждый вызов, поставьте periodSeconds не меньше 5-10 секунд, чтобы не создавать лишнюю нагрузку. successThreshold больше единицы нужен для приложений, которые прогреваются постепенно и отвечают неустойчиво на первых запросах.
Startup probe: защита медленно стартующих приложений
Startup probe закрывает классическую проблему: приложение стартует две минуты, а liveness probe убивает его через 30 секунд. Пока startup probe не завершилась успешно, liveness и readiness не запускаются вообще.
startupProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
failureThreshold: 30
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
failureThreshold: 3
Расчёт простой: 30 попыток по 10 секунд дают до пяти минут на старт. Как только startup probe прошла, в дело вступает liveness с обычными интервалами. Startup probe появилась в Kubernetes 1.16 и стабильна с 1.20, поэтому на актуальных версиях feature gate не нужен.
Реплики и PodDisruptionBudget: сколько подов нужно для отказоустойчивости
Одна реплика в проде означает простой при любом обновлении, drain узла или падении пода. Минимум для отказоустойчивого сервиса - две реплики, практический стандарт для критичных сервисов - три. Три реплики позволяют потерять один под и сохранить запас на время восстановления.
ReplicaSet и Deployment: управление количеством подов
ReplicaSet следит за числом подов, Deployment управляет ReplicaSet и версиями. На практике вы описываете Deployment, а ReplicaSet создаётся автоматически.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: registry.example.com/api:1.8.4
ports:
- containerPort: 8080
Проверка состояния выполняется двумя командами:
kubectl get deploy api kubectl get rs -l app=api
В колонке READY у Deployment должно быть 3/3, у ReplicaSet - ровно три желаемых пода. Готовые манифесты с лимитами ресурсов и probes собраны в руководстве по Deployment, включая откат версии без простоя.
PodDisruptionBudget: защита от одновременного удаления подов
PodDisruptionBudget ограничивает количество подов, которые одновременно недоступны при добровольных disruptions: drain узла, обновление кластера, удаление через Eviction API. Без PDB команда kubectl drain может выселить все поды сервиса сразу.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: api
С тремя репликами и minAvailable: 2 выселение третьего пода заблокируется до тех пор, пока один из оставшихся не станет готов. Альтернатива - maxUnavailable: 1, что даёт тот же эффект при трёх репликах, но по-другому масштабируется при изменении replicas.
kubectl get pdb kubectl describe pdb api-pdb kubectl drain node-2 --ignore-daemonsets --delete-emptydir-data
В describe смотрите поля ALLOWED DISRUPTIONS и CURRENT HEALTHY: если разрешённых выселений ноль, drain зависнет в ожидании. И помните про границу применимости: PDB защищает только от плановых работ. Аппаратный сбой узла, отключение питания или падение зоны PDB не остановит. Подробные примеры связки PDB, StatefulSet и PersistentVolume приведены в гайде по отказоустойчивому кластеру.
Стратегии обновления: rolling update, blue-green и canary
Стратегия определяет, как новая версия появляется в проде и насколько быстро можно откатиться. Для большинства сервисов подходит rolling update, для критичных релизов с миграциями схемы - blue-green, для проверки гипотез на живом трафике - canary.
Rolling update: постепенное обновление без простоя
Rolling update заменяет поды партиями. Управляют процессом два параметра: maxSurge задаёт, сколько подов можно создать сверх желаемого числа, maxUnavailable - сколько подов допустимо вывести из строя одновременно.
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
minReadySeconds: 10
progressDeadlineSeconds: 600
template:
spec:
containers:
- name: api
image: registry.example.com/api:1.9.0
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
failureThreshold: 2
Схема с maxUnavailable: 0 поднимает новый под, ждёт readiness, только затем удаляет старый. Простой нулевой, но требуется запас ресурсов на один дополнительный под. minReadySeconds добавляет паузу перед признанием пода готовым: полезно, если приложение падает через несколько секунд после старта.
kubectl rollout status deploy/api kubectl rollout history deploy/api kubectl rollout undo deploy/api --to-revision=3
Прогресс удобно отслеживать по колонкам READY и UP-TO-DATE, а причину застревания искать в ReplicaSet через kubectl describe. Развёрнутое сравнение параметров и способов отката есть в обзоре стратегий обновления.
Blue-green deployment: переключение трафика между версиями
Blue-green держит два окружения одновременно. Оба Deployment несут общий label app: web и различаются меткой версии, а Service выбирает активную версию через selector.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-blue
labels:
version: blue
spec:
replicas: 3
selector:
matchLabels:
app: web
version: blue
template:
metadata:
labels:
app: web
version: blue
spec:
containers:
- name: web
image: registry.example.com/web:1.8.4
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-green
labels:
version: green
spec:
replicas: 3
selector:
matchLabels:
app: web
version: green
template:
metadata:
labels:
app: web
version: green
spec:
containers:
- name: web
image: registry.example.com/web:1.9.0
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web
version: blue
ports:
- port: 80
targetPort: 8080
Переключение выполняется правкой одного поля:
kubectl patch svc web -p '{"spec":{"selector":{"app":"web","version":"green"}}}'
kubectl get endpoints web
Откат занимает столько же времени, сколько переключение: меняете selector обратно на blue. Цена подхода - двойной расход ресурсов и необходимость совместимости схемы данных между версиями. Для инкрементальных релизов blue-green избыточен.
Canary deployment: постепенный выпуск на часть трафика
Canary отправляет малую долю запросов на новую версию, пока основная масса идёт на стабильную. В базовом варианте достаточно двух Deployment с общим label и Service, который выбирает поды по нему, а точные веса задаёт Ingress-контроллер.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-canary
port:
number: 80
Вес поднимают постепенно: 5%, 10%, 25%, 50%, затем полный перевод. Ключевое условие - метрики. Без сравнения latency, доли ошибок и насыщения ресурсов между stable и canary эксперимент превращается в лотерею: вес можно увеличивать только пока различия остаются в пределах допустимого. Снимать canary нужно мгновенно, поэтому Deployment canary держат отдельно от stable.
| Стратегия | Сложность | Риск для пользователей | Скорость отката | Расход ресурсов |
|---|---|---|---|---|
| Rolling update | Низкая | Средний: новая версия сразу на всех | Откат через rollout undo | Плюс maxSurge подов |
| Blue-green | Средняя | Низкий: трафик переключается целиком | Секунды: правка selector | Двойной на время релиза |
| Canary | Высокая | Низкий: доля трафика ограничена | Зависит от веса, обычно минуты | Плюс несколько подов canary |
Отказоустойчивость etcd: кворум и резервное копирование
etcd хранит всё состояние кластера: объекты API, конфигурации, секреты. Потеря кворума останавливает запись и переводит control plane в режим только для чтения, поэтому etcd защищают отдельно от рабочих нагрузок.
Кворум etcd: сколько узлов нужно для отказоустойчивости
etcd использует алгоритм Raft и требует кворума, то есть большинства голосов, для фиксации записи. Чётное число узлов смысла не имеет: четыре узла дают кворум три и переносят отказ только одного, как и три узла.
| Узлов etcd | Кворум | Переносимый отказ |
|---|---|---|
| 1 | 1 | 0 |
| 3 | 2 | 1 узел |
| 5 | 3 | 2 узла |
| 7 | 4 | 3 узла |
Для продакшена минимально допустимы три узла etcd, оптимально пять для крупных кластеров. Семь и больше увеличивают задержку записи из-за большего числа подтверждений. Проверить состояние можно так:
ETCDCTL_API=3 etcdctl endpoint health \ --endpoints=https://10.0.0.11:2379,https://10.0.0.12:2379,https://10.0.0.13:2379 ETCDCTL_API=3 etcdctl member list \ --endpoints=https://10.0.0.11:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key
Команда member list показывает идентификаторы, адреса и роли участников. Если число здоровых узлов падает до кворума минус один, запись прекращается, а в логах kube-apiserver появляются ошибки вида etcdserver: request timed out. Узлы etcd размещайте на отдельных дисках с низкой задержкой и на разных физических хостах.
Резервное копирование и восстановление etcd
Снимок состояния снимается одной командой. Ключи и сертификаты указывайте явно: без них etcdctl не подключится к защищённому эндпоинту.
ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-$(date +%F-%H%M).db \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key ETCDCTL_API=3 etcdctl snapshot status /backup/etcd-2026-09-26-0300.db --write-out=table
snapshot status выводит хеш, ревизию, общее число ключей и размер файла. Эти значения стоит сохранять в лог бэкапа: они подтверждают, что снимок не пустой, и позволяют сравнить его с состоянием кластера. Бэкап по расписанию удобно вынести в cron на управляющем узле, а копии складывать вне кластера, в объектное хранилище или на отдельный сервер.
Восстановление требует остановки etcd на всех узлах и запуска со снимком. Порядок такой: сохраните текущий каталог данных etcd, восстановите снимок в новый каталог, укажите его в конфигурации, верните остальные узлы в кластер и проверьте, что member list показывает все участники, а kube-apiserver отвечает на запросы.
ETCDCTL_API=3 etcdctl snapshot restore /backup/etcd-2026-09-26-0300.db \ --name=etcd-1 \ --initial-cluster=etcd-1=https://10.0.0.11:2380,etcd-2=https://10.0.0.12:2380,etcd-3=https://10.0.0.13:2380 \ --initial-advertise-peer-urls=https://10.0.0.11:2380 \ --data-dir=/var/lib/etcd-restored
Восстановление etcd - операция, которую нельзя проводить впервые во время аварии. Прогоните её на стенде минимум раз в квартал и зафиксируйте фактическое время простоя: обычно от 10 до 30 минут на трёх узлах. Снимки храните не меньше 30 дней, а на кластерах с частыми релизами - до 90 дней.
Проверка и отладка отказоустойчивости: команды и чек-лист
Настроенные probes и реплики нужно проверить до инцидента. Набор команд ниже закрывает большинство вопросов: почему под перезапускается, почему трафик не идёт, почему обновление зависло.
Команды kubectl для диагностики probes и реплик
kubectl describe pod api-7d8f9c6b5-xk2lp kubectl get events --sort-by=.metadata.creationTimestamp kubectl logs api-7d8f9c6b5-xk2lp --previous kubectl exec -it api-7d8f9c6b5-xk2lp -- curl -s localhost:8080/ready kubectl get endpoints web
В describe pod смотрите секцию Events: строки вида Liveness probe failed: HTTP probe failed with statuscode: 500 показывают, какая именно проверка падает и с какой периодичностью. Запись Unhealthy с последующим Killing означает, что kubelet перезапустил контейнер по liveness. Если под долго в Ready 0/1, проверяйте readiness через kubectl exec: часто эндпоинт отвечает 500 из-за недоступной зависимости.
Симуляция отказа занимает минуту и показывает реальное поведение:
kubectl delete pod api-7d8f9c6b5-xk2lp kubectl get pods -l app=api -w kubectl drain node-2 --ignore-daemonsets --delete-emptydir-data kubectl get pdb api-pdb
Ожидаемая картина: удалённый под пересоздаётся, drain не проходит дальше лимита PDB, ALLOWED DISRUPTIONS остаётся равным единице при трёх репликах и minAvailable: 2. Если drain выселяет все поды сразу, значит PDB не совпадает по selector с метками пода.
Чек-лист отказоустойчивости Kubernetes
- Readiness probe настроена и проверяет реальные зависимости, а не только ответ процесса.
- Liveness probe отделена от readiness и не зависит от внешних сервисов.
- Для приложений со стартом дольше 30 секунд задана startup probe.
- replicas не меньше 2 для критичных сервисов, практически достаточно 3.
- Реплики распределены по узлам через topologySpreadConstraints или podAntiAffinity.
- PodDisruptionBudget задан для всех сервисов, которые должны переживать drain.
- Стратегия обновления выбрана осознанно, maxUnavailable и maxSurge проверены на стенде.
- Откат версии протестирован командой rollout undo.
- Кластер etcd содержит нечётное число узлов, минимум 3.
- Снимки etcd снимаются по расписанию, хранятся вне кластера и проверяются через snapshot status.
- Восстановление из снимка отработано на стенде с замером времени.
- Мониторинг и алерты настроены на перезапуски контейнеров, недоступность подов и потерю кворума etcd.
Типичные ошибки, которые ломают отказоустойчивость: одна реплика в проде, отсутствие PDB, aggressive liveness probe с timeoutSeconds: 1 и общий эндпоинт /healthz для всех проверок. Логику самовосстановления и причины, по которым поды иногда не поднимаются, разбираем отдельно в материале про self-healing в Kubernetes.
Актуальные версии и совместимость: что учесть в 2026 году
Отказоустойчивость зависит не только от манифестов, но и от версии кластера и рантайма. Несколько фактов, которые влияют на настройку.
Kubernetes 1.37 требует container runtime с поддержкой CRI, а dockershim удалён из проекта начиная с версии 1.24. Если манифесты или скрипты деплоя всё ещё ссылаются на docker как на рантайм узла, их нужно переводить на containerd или CRI-O. Список поддерживаемых вариантов и требования к конфигурации перечислены в документации Kubernetes по container runtime. Помните, что kubelet и рантайм обязаны использовать один cgroup driver.
На создание подов влияют admission controllers. Это код, который перехватывает запросы к API-серверу после аутентификации и авторизации, но до сохранения объекта. Контроллеры применяются к созданию, удалению и изменению объектов и не могут блокировать чтение. Процесс идёт в две фазы: сначала mutating, затем validating; отклонение на любой фазе немедленно отклоняет весь запрос. Практический вывод: если под не создаётся, а Events пусты, проверьте validating-политики и квоты. Механика описана в справочнике по admission control.
Наблюдаемость самовосстановления строится на метриках и трейсах системных компонентов: перезапуски контейнеров, статусы проб, доступность узлов. Способы сбора системных метрик Kubernetes описаны в разделе документации по мониторингу.
Ограничение: точные номера версий и статус стабильности отдельных полей меняются от релиза к релизу, а часть переводов документации отстаёт от англоязычного оригинала. Перед изменением продакшена сверяйтесь с release notes и англоязычной версией документации для вашей версии Kubernetes.
Практический порядок внедрения: сначала probes и три реплики, затем PodDisruptionBudget, потом отработка стратегии обновления на стенде и только после этого регулярные бэкапы etcd с проверенным восстановлением. Такой порядок закрывает самые частые причины простоя и оставляет читаемый набор манифестов, который можно передать коллегам.