VK Cloud

Отказоустойчивый кластер Kubernetes: мультизона, реплики и бэкап

13 июля 2026 г.
шпрингер.png
Елена Шпрингер
Автор статьи
_blog_head_72.png

Отказоустойчивый кластер Kubernetes часто падает не там, где ждали. Архитектор поднимает три мастер-ноды, распределяет реплики по нодам, ставит галочку HA в чек-листе, а через месяц авария в одной зоне доступности кладёт сервис целиком, потому что поды приложения оказались в одном ЦОД, а данные из отвалившегося тома никто не бэкапил.

Дело в том, что отказоустойчивость редко работает как один переключатель. На деле это цепочка независимых уровней: диск и нода, зона доступности, control plane, само приложение, данные приложения. Каждый уровень ломается по-своему и лечится своим набором настроек, и если провисает хотя бы одно звено, рушится вся цепь, даже если остальные уровни продуманы идеально.

С ростом критичных нагрузок — платёжных шлюзов, real-time аналитики, внутренних API с SLA — закрывать глаза на это допущение уже нельзя. Дальше разберём, что и почему ломается на каждом уровне, как реально распределить поды по зонам, а не только на бумаге, зачем нужен PodDisruptionBudget и что бэкапить отдельно от etcd, чтобы к концу статьи можно было спроектировать production-кластер и понимать, что настраивается руками, а что можно получить из managed-сервиса.

Новиков2.jpg

Статья подготовлена вместе с экспертом

Сергей Новиков, менеджер продукта Managed Kubernetes

Что ломается в кластере

Первый уровень — диск и нода. Диск может деградировать без явного отказа: iops падает, задержки растут, под продолжает работать, но медленнее. Нода отваливается целиком по сети, из-за kubelet или физического сбоя хоста. Kubernetes переносит поды на здоровые ноды через некоторое время после того, как перестаёт получать от узла heartbeat, но это время не нулевое, и под с локальным диском без реплики теряет данные необратимо.

Второй уровень — зона доступности. Зона обычно означает отдельный ЦОД со своим питанием, охлаждением и сетевой связностью. Авария в одной зоне не должна касаться остальных, но только если ничего критичного физически не сосредоточено именно в ней: ни worker-ноды с единственной репликой приложения, ни единственный узел etcd.

Третий уровень — control plane, то есть etcd и API-сервер. Etcd — распределённое хранилище состояния кластера, и оно требует кворума: большинство узлов должно быть доступно и согласовано, иначе кластер перестаёт принимать изменения. Кворум etcd держится минимум на 3 узлах, при таком размере кластер переживает потерю одного узла без потери кворума. Второй порог касается производительности: p99 задержки fsync записи в WAL должна укладываться в 10 мс, иначе etcd начинает терять лидера под нагрузкой, а метрика etcd_server_has_leader=0 означает уже аварию, а не деградацию.

Четвёртый уровень — само приложение. Сколько реплик, как они размещены, переживают ли одновременный отказ ноды и вывод другой ноды на обслуживание. Пятый — данные приложения: PVC, тома, состояние, которое не восстановится репланированием пода, если снапшотов нет.

Проверить, что кластер действительно готов к нагрузке, помогает регулярный мониторинг метрик control plane и рабочих нагрузок, например через связку Мониторинг Kubernetes, где видно и etcd_server_has_leader, и латентность API-сервера, и распределение подов по нодам.

Мультизональное размещение

Мультизона нужна не только для эстетики, но и для живучести при отказе целого ЦОД. Стандартная рекомендация — минимум 3 зоны отказа для control plane, по одному узлу etcd на зону, чтобы кворум сохранялся при потере любой одной зоны, и не менее 3 worker-нод, распределённых как минимум по 2 зонам, для рабочей нагрузки.

Схема размещения для production-кластера из трёх зон выглядит так: text

Зона A Зона B Зона C ├── etcd-1 ├── etcd-2 ├── etcd-3 ├── control-plane-1 ├── control-plane-2 ├── control-plane-3 ├── worker-1 ├── worker-3 ├── worker-5 ├── worker-2 ├── worker-4 ├── worker-6 └── PV (app-a, реплика 1) └── PV (app-a, реплика 2) └── PV (app-a, реплика 3)

При отказе зоны B кластер теряет один узел etcd (кворум держат A и C), два worker-узла и одну реплику приложения. Сервис продолжает отвечать, если приложение спроектировано на этот сценарий.

Здесь есть жёсткое ограничение уровня хранения. PersistentVolume физически привязан к зоне, где он создан, и получает соответствующую метку зоны. Планировщик учитывает это правило (NoVolumeZoneConflict) и не разместит под с таким PVC в другой зоне. Если под с зональным томом перепланируется в зону, где тома нет, он зависает в Pending. Отсюда следствие: для стабильности при отказе зоны реплики приложения нужно размещать в разных зонах заранее, потому что том уже держит под в конкретной зоне, и рассчитывать, что планировщик сам всё сбалансирует, не стоит.

Мультизона не обходится бесплатно. Межзональный трафик обычно тарифицируется отдельно и добавляет сетевую задержку в единицы миллисекунд между зонами одного региона — для большинства HTTP-сервисов это некритично, но заметно для синхронных БД-транзакций, чувствительных к латентности. Stateful-приложения переносят синхронную репликацию между зонами по-разному: для баз данных с синхронным кворумным подтверждением записи это штатный режим работы, а для других она превращается в источник деградации производительности. Прежде чем размазывать stateful-нагрузку по зонам, стоит проверить, как конкретная СУБД или очередь ведёт себя при межзональной репликации.

Реплики и AntiAffinity

Самая частая ошибка выглядит обманчиво безопасной. Три реплики приложения стоят на разных нодах под контролем podAntiAffinity, и формально это похоже на отказоустойчивость. Но распределение по нодам не гарантирует распределения по зонам: если в кластере 6 нод и 3 из них находятся в одной зоне, планировщик может расположить там все три реплики, не нарушив анти-аффинити по нодам. При отказе этой зоны все реплики падают одновременно.

Классический podAntiAffinity с preferredDuringSchedulingIgnoredDuringExecution вообще не гарантирует ничего — это мягкое предпочтение, которое планировщик может проигнорировать при нехватке ресурсов. Даже requiredDuringSchedulingIgnoredDuringExecution по ключу зоны решает задачу грубо: разносит поды по разным зонам, но не балансирует их количество.

Точнее эту задачу решает topologySpreadConstraints с полем minDomains. Поле прошло путь от alpha в Kubernetes 1.24 до стабильного (GA) в 1.30, а на версиях 1.28–1.29 доступно по умолчанию через feature gate MinDomainsInPodTopologySpread. На кластере версии 1.30 и новее minDomains работает без оговорок, а на 1.28–1.29 требует включённого feature gate — это стоит учитывать, если версия кластера попадает в диапазон 1.25–1.32. Поле задаёт минимальное число доменов отказа (зон), по которым должны быть распределены поды, причём с нуля: даже если часть зон ещё не существует в кластере на момент планирования, ограничение всё равно учитывает их как обязательные домены распределения. Этим minDomains отличается от анти-аффинити, которая реагирует на уже существующие ноды, а не гарантирует целевое количество зон заранее.

Манифест для приложения с тремя репликами, обязательно распределёнными по трём зонам: text

apiVersion: apps/v1 kind: Deployment metadata: name: payment-api spec: replicas: 3 selector: matchLabels: app: payment-api template: metadata: labels: app: payment-api spec: topologySpreadConstraints: - maxSkew: 1 minDomains: 3 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: payment-api containers: - name: payment-api image: registry.example.com/payment-api:1.4.2 resources: requests: cpu: "500m" memory: "512Mi"

Здесь maxSkew: 1 ограничивает разницу в количестве подов между зонами одним подом, а whenUnsatisfiable: DoNotSchedule запрещает планировщику размещать под, если ограничение нарушается: под остаётся в Pending, а не тихо садится в переполненную зону. Это жёстче, чем ScheduleAnyway, но для критичных сервисов явный Pending лучше скрытой потери отказоустойчивости.

PodDisruptionBudget и пробы

PodDisruptionBudget нужен для ситуации, о которой редко думают заранее: что происходит с приложением при плановых действиях — drain ноды перед обновлением, upgrade кластера, автоскейлинг вниз. Это voluntary disruptions, добровольные нарушения, и именно от них защищает PDB. От внезапного падения ноды он не защищает — это involuntary disruption, и здесь работают уже мультизона и репликация, разобранные выше.

Здесь легко ошибиться в обе стороны. Если задать minAvailable равным текущему числу реплик или maxUnavailable: 0, PDB запрещает любые добровольные нарушения, и kubectl drain для ноды с такими подами зависает бесконечно: планировщик просто не может эвакуировать под без нарушения бюджета. Правило простое — PDB должен допускать хотя бы одно нарушение одновременно, иначе плановое обслуживание кластера блокируется на тех же подах, которые оно и должно пережить.

Для приложений с кворумом, например для etcd-подобных сервисов или распределённых баз с нечётным числом реплик, лучше привязывать minAvailable к размеру кворума, а не к общему числу реплик. Для типовых stateless-сервисов проще и надёжнее maxUnavailable: 1: он не требует пересчёта при масштабировании реплик, тогда как абсолютный minAvailable нужно поднимать вручную каждый раз при увеличении их числа.

text

apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: payment-api-pdb spec: maxUnavailable: 1 selector: matchLabels: app: payment-api

Для подов без контроллера (голые Pod) или при DaemonSet-специфике имеет смысл явно выставлять unhealthyPodEvictionPolicy: AlwaysAllow — эта политика разрешает эвакуацию уже нездоровых подов независимо от бюджета, чтобы PDB не блокировал обслуживание из-за подов, которые и так не отвечают на трафик. PDB стоит заводить и для формально stateless-сервисов: без него drain может одновременно снять все реплики одного приложения, если они volunteer-disruption-совместимы по расписанию узла.

Отдельная линия защиты — пробы (probes), и она тоже строится на разделении ответственности. readinessProbe решает, готов ли под принимать трафик прямо сейчас: провал убирает под из Service, не перезапуская его. livenessProbe решает, жив ли процесс: провал перезапускает контейнер. Смешивать их опасно: если livenessProbe проверяет доступность внешней базы данных, временная недоступность базы вызывает каскадный рестарт всех подов приложения, хотя приложение само по себе живо. Liveness должен быть лёгким и локальным — проверка, что процесс отвечает, без похода во внешние зависимости.

Третья проба, startupProbe, нужна для медленно стартующих приложений: она отключает liveness и readiness до тех пор, пока приложение не завершит инициализацию, и делает это точнее, чем статичный initialDelaySeconds, который либо слишком долгий для быстрого старта, либо слишком короткий для медленного. StartupProbe адаптируется к реальному времени старта конкретного экземпляра.

Бэкап etcd и приложений

Мультизона и PDB защищают доступность, но не защищают от потери данных, ни состояния кластера, ни данных внутри приложений. Здесь работают два независимых контура резервного копирования, и подменять один другим нельзя.

Первый контур — снапшоты etcd на уровне кластера. Рекомендуемая практика: регулярный снапшот, например раз в 6 часов в зависимости от требований к RPO, с выгрузкой в объектное хранилище S3-совместимого типа, отдельно от самого кластера. Снапшот без регулярной проверки restore — это иллюзия бэкапа. Еженедельная верификация восстановления на тестовом стенде через etcdutl snapshot restore — единственный способ убедиться, что снапшот действительно разворачивается, а не просто существует в хранилище.

Второй контур — данные приложений: PVC, состояние баз данных, файловые тома. Здесь снапшот etcd бесполезен, потому что хранит только объекты Kubernetes — манифесты, конфигурацию, — а не содержимое томов. Эту задачу закрывает Velero: CSI-снапшоты в нём стабильны начиная с версии 1.9, а начиная с версии 1.14 поддержка CSI встроена в ядро Velero без отдельного плагина. Для переноса данных в объектное хранилище, а не только локального снапшота на уровне провайдера, Velero использует data movement через Kopia — дедуплицирующий движок передачи данных в S3-совместимое хранилище, независимое от кластера.

Оба контура нужны одновременно: снапшот etcd восстановит конфигурацию и объекты Kubernetes после потери control plane, но не восстановит содержимое базы данных внутри пода, а Velero-бэкап восстановит данные приложения даже в новый кластер, но не заменяет консистентный снапшот etcd для быстрого disaster recovery всего control plane.

Резервная ёмкость — смежный с бэкапом вопрос: если авария требует поднять нагрузку в оставшихся зонах, кластеру нужен запас по ресурсам и работающий автоскейлинг, иначе восстановленные из бэкапа поды просто не найдут места для планирования. Логику автоскейлинга на разных уровнях разбирает статья Автомасштабирование Kubernetes, она пригодится при проектировании резервной мощности под сценарий отказа зоны.

Отказоустойчивость в VK Cloud Containers

Часть уровней отказоустойчивости, разобранных выше, можно взять как managed-функциональность, не собирая её вручную. В VK Cloud Containers production-кластер Kubernetes разворачивается автоматически с изолированным control plane по модели Kubernetes-in-Kubernetes: control plane каждого кластера клиента работает в отдельном изолированном окружении, а не разделяется между клиентами. Кластер поддерживает версии Kubernetes 1.25–1.34 (скоро будет 135), что даёт возможность управлять апгрейдами по собственному графику, не отставая от актуальных релизов.

Мультизона в VK Cloud Containers работает на уровне инфраструктуры: worker-ноды и control plane размещаются в 3 ЦОД, а сетевая связность между зонами обеспечивается собственной программно-определяемой сетью SDN Sprut. Self-Healing автоматически заменяет отказавшие ноды без ручного вмешательства инженера. Cluster Autoscaler масштабирует пул нод по нагрузке, а кластер выдерживает рост до 55 000 подов, что покрывает сценарии крупных production-нагрузок. SLA сервиса составляет 99,95%.

Модель биллинга снижает стоимость экспериментов с топологией: посекундная тарификация и позволяет держать тестовый мультизональный кластер без постоянных затрат на control plane, а функция stop/start останавливает биллинг worker-нод для некритичных сред без удаления кластера. Маркетплейс аддонов (с аддоном Agro CD), Istio и Prometheus сокращают время на подключение CI/CD, service mesh и мониторинга тех же метрик etcd и API-сервера, о которых шла речь в начале статьи.

Важно честно разделить ответственность. Managed-кластер снимает операционную нагрузку по эксплуатации control plane, замене нод и мультизональному размещению инфраструктуры, но PodDisruptionBudget, readiness/liveness/startup-пробы и бэкап данных приложений через Velero остаются настройками уровня приложения, и managed-провайдер их не создаёт автоматически. Self-Healing автоматически восстановит инфраструктурный уровень, однако отказоустойчивость приложения по-прежнему зависит от корректной настройки PDB и распределения реплик, отказ этой же ноды всё равно даст простой на время замены. Managed снимает инфраструктурный слой цепочки, а слой приложения — topologySpreadConstraints, PDB, probes, Velero — остаётся зоной ответственности команды, которая эксплуатирует кластер.

Тем, кто переносит существующую нагрузку в managed-кластер, полезно заранее свериться с порядком переноса. Этому посвящена статья Миграция в managed Kubernetes.

FAQ

Что ломается в кластере чаще всего?

Чаще всего отдельные ноды и диски, реже целые зоны доступности. Control plane страдает при потере кворума etcd (менее 3 доступных узлов) или деградации диска, из-за которой растёт задержка fsync записи в WAL.

Зачем мультизональное размещение?

Мультизона переносит устойчивость с уровня отдельной ноды на уровень целого ЦОД: авария в одной зоне не останавливает сервис, если control plane и реплики приложения физически распределены минимум по трём зонам заранее.

Что такое PodDisruptionBudget?

PodDisruptionBudget ограничивает число подов, которые можно одновременно вывести из строя при плановых действиях: drain ноды, обновление кластера, автоскейлинг. От отказа железа он не защищает, это задача мультизоны и репликации.

Как бэкапить кластер?

Двумя независимыми контурами. Регулярный снапшот etcd с выгрузкой в S3 и еженедельной проверкой восстановления защищает состояние кластера, а Velero с CSI-снапшотами и переносом данных через Kopia защищает содержимое томов приложений.

Какой SLA даёт VK Cloud?

VK Cloud Containers даёт SLA 99,95% для managed-кластера Kubernetes с мультизональным размещением в 3 ЦОД и автоматической заменой отказавших нод.

Заключение

Отказоустойчивый кластер Kubernetes — это цепочка из пяти уровней: диск и нода, зона доступности, control plane, приложение, данные приложения. Три мастер-ноды без мультизонального распределения не переживут отказ ЦОД. Реплики, размазанные по нодам без topologySpreadConstraints и minDomains, могут физически оказаться в одной зоне. PodDisruptionBudget без допустимого нарушения блокирует собственное же обслуживание кластера. А без Velero и проверенных снапшотов etcd восстановление после аварии превращается в восстановление пустого кластера k8s без данных.

После разбора каждого уровня чек-лист production-кластера Kubernetes выглядит так:

  • Control plane и etcd развёрнуты минимум в 3 зонах отказа, кворум etcd держится на 3 узлах.
  • Worker-ноды распределены минимум по 2–3 зонам, с запасом мощности на отказ одной из них.
  • Реплики критичных приложений разнесены через topologySpreadConstraints с minDomains, а не только podAntiAffinity.
  • PersistentVolume и под приложения физически совместимы по зоне, это учтено на этапе проектирования, а не постфактум.
  • PodDisruptionBudget задан для каждого критичного сервиса и допускает хотя бы одно добровольное нарушение.
  • Readiness, liveness и startup пробы разделены по ответственности, liveness не зависит от внешних сервисов.
  • Снапшоты etcd снимаются регулярно, восстановление проверяется еженедельно.
  • Данные приложений бэкапятся отдельно через Velero с CSI-снапшотами, независимо от бэкапа etcd.

Managed-платформа вроде VK Cloud Containers закрывает инфраструктурный слой: мультизону, Self-Healing нод, изолированный control plane. Слой приложения — PDB, пробы, Velero — остаётся за архитектором. Кластер k8s становится отказоустойчивым не в момент включения HA-режима, а в момент, когда проверена вся цепочка целиком.

Оставьте заявку, чтобы получить консультацию

Наши специалисты свяжутся с вами в ближайшее время и ответят на все вопросы.

section_subscribe_2x_9ab2d878a6.png

            Узнавайте о выходе новых статей в блоге первыми!

            Будем держать в курсе новостей и облачных трендов

            section-subscribe_2x.png
              section-subscribe_2x.png
              Теги: kubernetes, кластер, бэкапы, VK Cloud
              Ссылка скопирована
              Поделиться

              Почитать по теме

              _blog_head_172.png
              8 июля

              GPU Operator в Managed Kubernetes: запуск GPU-вычислений без ручной настройки драйверов

              _blog_head_102.png
              24 июня

              Как устроен Cilium: eBPF-технологии в сетевой подсистеме Kubernetes

              40+ готовых сервисов