🐳 Раздел 14 · Вопрос #23

Что такое StatefulSet и когда его использовать

В отличие от стандартного Deployment, где поды абсолютно взаимозаменяемы, безлики и имеют случайные суффиксы в именах (например, web-78df6c-x9k2), в StatefulSet:


🟢 Junior Level

30-секундный ответ

StatefulSet — это специализированный контроллер рабочей нагрузки Kubernetes, предназначенный для управления приложениями, которым требуется сохранение состояния (State), стабильная сетевая идентичность и персональные постоянные диски.

В отличие от стандартного Deployment, где поды абсолютно взаимозаменяемы, безлики и имеют случайные суффиксы в именах (например, web-78df6c-x9k2), в StatefulSet:

  1. Каждый под получает строго предсказуемое имя с порядковым номером (Ordinal): db-0, db-1, db-2.
  2. Каждый под получает собственный выделенный диск: через шаблон volumeClaimTemplates создаются персональные PVC (data-db-0, data-db-1). При перезапуске под на любой ноде гарантированно подключает обратно свой же диск.
  3. Порядок запуска и остановки строго детерминирован: поды запускаются по очереди (0 $\to$ 1 $\to$ 2) и выключаются в обратном порядке (2 $\to$ 1 $\to$ 0).

Наглядная аналогия из жизни

  • Deployment — это городские такси: вам неважен номер машины и водитель. Если одно такси сломалось, вы садитесь в любое другое — все машины одинаковые и взаимозаменяемые.
  • StatefulSet — это пассажирский поезд: вагоны соединены в строгом порядке: Вагон №1 (Локомотив/Leader), Вагон №2, Вагон №3. Вы не можете выкинуть вагон №2 и поставить случайную машину такси. Если вагон №2 отцепили на техобслуживание, на его место должен вернуться именно отремонтированный вагон №2 со всем багажом пассажиров.

Базовый манифест StatefulSet с Headless Service

apiVersion: v1
kind: Service
metadata:
  name: postgres-cluster
spec:
  clusterIP: None # 1. ОБЯЗАТЕЛЬНЫЙ Headless Service!
  selector:
    app: postgres
  ports:
  - port: 5432
    name: postgres
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres-cluster # Связка с Headless Service
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:16
        ports:
        - containerPort: 5432
        volumeMounts:
        - name: pgdata
          mountPath: /var/lib/postgresql/data
  # 2. Шаблон генерации персональных дисков для каждого пода
  volumeClaimTemplates:
  - metadata:
      name: pgdata
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 20Gi

🟡 Middle Level

4 столпа архитектуры StatefulSet

                       [ StatefulSet: postgres (replicas: 3) ]
                                          │
         ┌────────────────────────────────┼────────────────────────────────┐
         ▼                                ▼                                ▼
    [ Под: postgres-0 ]              [ Под: postgres-1 ]              [ Под: postgres-2 ]
    DNS: postgres-0.cluster...       DNS: postgres-1.cluster...       DNS: postgres-2.cluster...
         │                                │                                │
         ▼                                ▼                                ▼
    [ PVC: pgdata-postgres-0 ]       [ PVC: pgdata-postgres-1 ]       [ PVC: pgdata-postgres-2 ]
    (Диск тома PV №0)                (Диск тома PV №1)                (Диск тома PV №2)
  1. Предсказуемый сетевой DNS (Headless Service): Благодаря clusterIP: None CoreDNS создает персональную A-запись для каждой реплики: <pod-name>.<service-name>.<namespace>.svc.cluster.local
    Например: postgres-0.postgres-cluster.default.svc.cluster.local. Поды кластера всегда знают, где искать лидера (postgres-0).
  2. Персональное дисковое хранилище (volumeClaimTemplates): При масштабировании контроллер автоматически запрашивает новый PersistentVolumeClaim. При удалении пода диск НЕ удаляется, сохраняя базу данных в целостности.
  3. Строгий порядок инициализации: Под postgres-1 не начнёт создаваться, пока под postgres-0 не перейдёт в статус Running и Ready.
  4. Обратный порядок удаления: При уменьшении масштаба (scale --replicas=1) сначала корректно завершается postgres-2, и лишь затем postgres-1.

Сравнительная таблица: StatefulSet vs Deployment

| Критерий | Deployment | StatefulSet | | :— | :— | :— | | Именование подов | Случайный хэш (app-79d8f-kx9q) | Строгий порядковый номер (app-0, app-1) | | Сетевая адресация | Единый виртуальный ClusterIP | Персональный DNS для каждого пода (Headless) | | Дисковые тома | Общие или эфемерные | Персональный постоянный PV на каждый под | | Порядок запуска | Параллельный (быстрый) | Строго поочерёдный (0 $\to$ 1 $\to$ 2) | | Типичные нагрузки | Stateless REST API, Spring Boot воркеры | PostgreSQL, Kafka, Cassandra, ZooKeeper, Redis |


🔴 Senior Level

Главная иллюзия: почему «голый» StatefulSet не спасает базы данных

Senior-инженер должен понимать ключевое ограничение:

StatefulSet управляет подами и дисками, но он АБСОЛЮТНО НИЧЕГО не знает о внутренней репликации баз данных!

  • Если запустить PostgreSQL в StatefulSet на 3 реплики, Kubernetes поднимет три изолированных экземпляра PostgreSQL, которые ничего не знают друг о друге.
  • StatefulSet не умеет автоматически выбирать нового мастера при падении postgres-0, не умеет настраивать streaming replication, не управляет WAL-архивацией и не защищает от Split-Brain.
  • Продакшен-стандарт: базы данных в Kubernetes разворачивают не через сырой StatefulSet, а с помощью специализированных Операторов (Operators):
    • PostgreSQL: CloudNativePG, Zalando Postgres Operator (на базе Patroni).
    • Apache Kafka: Strimzi Operator.
    • Redis: Redis Operator.

Проблема зональной блокировки дисков и WaitForFirstConsumer

В публичных облаках (AWS, GCP, Azure) дисковые тома (например, AWS EBS) жестко привязаны к конкретной зоне доступности (Availability Zone, например us-east-1a).

Катастрофический сценарий с volumeBindingMode: Immediate:

  1. Создаётся PVC. При значении Immediate облако моментально выделяет EBS-диск в случайной зоне us-east-1a.
  2. Планировщик kube-scheduler запускает под, но назначает его на ноду в зоне us-east-1b (где больше свободной памяти).
  3. Kubelet пытается примонтировать диск из соседней зоны: возникает фатальная ошибка VolumeZoneMismatch / Multi-Attach error. Под навсегда зависает в статусе ContainerCreating!

Архитектурное решение:

В StorageClass обязательно указывается режим volumeBindingMode: WaitForFirstConsumer:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ebs-gp3-sc
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer # Создавать диск строго в той зоне, куда шедулер посадил Под!

Канареечные обновления через partition

В StatefulSet доступен уникальный механизм канареечного обновления stateful-сервисов с помощью параметра partition:

spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      partition: 2 # Обновить только поды с номером >= 2

Если у вас 3 реплики (0, 1, 2), то при изменении образа обновится только под postgres-2. Поды 0 и 1 останутся на старой версии! Вы можете провести нагрузочные тесты на канареечном поде, после чего выставить partition: 0 для завершения раскатки кластера.


4 Tricky Questions

1. Почему для работы StatefulSet строго обязателен Headless Service (clusterIP: None), и что сломается, если использовать обычный ClusterIP Service?

Ответ:

  • В стандартном сервисе ClusterIP запросы балансируются через iptables/IPVS случайным образом между всеми подами за единым виртуальным IP.
  • Однако кластерным stateful-приложениям (таким как Kafka, Cassandra или PostgreSQL) категорически требуется обращаться к конкретному узлу напрямую (например, запись должна идти строго на мастера db-0, а реплики должны синхронизироваться со строго определенным брокером).
  • Headless Service отключает виртуальный IP (clusterIP: None). Вместо этого DNS-сервер кластера (CoreDNS) при запросе к db-headless возвращает список персональных A-записей для каждого отдельного пода (db-0.db-headless, db-1.db-headless). Без Headless Service узлы распределенной системы не смогут обнаружить топологию кластера и установить междоузельные P2P-соединения.

2. Почему при масштабировании StatefulSet вниз (kubectl scale --replicas=0) или удалении объекта (kubectl delete statefulset) Kubernetes намеренно НЕ удаляет созданные PersistentVolumeClaims (PVC)?

Ответ: Это фундаментальный защитный механизм безопасности Kubernetes для предотвращения необратимой потери данных:

  • Если бы Kubernetes автоматически удалял PVC при случайном уменьшении масштаба или пересоздании StatefulSet, любая ошибка инженера в CI/CD пайплайне приводила бы к мгновенному уничтожению терабайтов боевых баз данных.
  • Тома PVC объявляются «осиротевшими» (orphaned), но продолжают надежно хранить данные.
  • При последующем увеличении масштаба вновь созданный под db-0 найдет существующий PVC с точно совпадающим именем data-db-0 и автоматически подмонтирует прежний диск со всей историей данных. Если удаление дисков действительно необходимо, администратор обязан удалить их отдельной осознанной командой kubectl delete pvc -l app=db.

3. Как поведёт себя StatefulSet при падении физической ноды с работающим подом, и почему Kubernetes не пересоздаст под на другой ноде автоматически без вмешательства оператора?

Ответ: Если физическая нода перестаёт отвечать (Node NotReady), контроллер StatefulSet НЕ пересоздает под на другой ноде автоматически.

  • Причина: защита от разделения памяти и повреждения данных (Split-Brain / Data Corruption). Kubernetes не может достоверно знать: упал ли сервер физически или просто потерял сетевой линк. Если контроллер запустит дубликат пода db-0 на другой ноде, оба инстанса могут попытаться писать на один и тот же сетевой диск, что гарантированно разрушит файловую систему базы данных.
  • Под останется в статусе Terminating или Unknown.
  • Для переноса пода на другую ноду требуется либо восстановление связи, либо принудительное подтверждение оператора/контроллера отказоустойчивости (STONITH / Node Fencing, либо kubectl delete pod db-0 --force --grace-period=0).

4. В каких случаях имеет смысл изменить политику podManagementPolicy со стандартного OrderedReady на Parallel?

Ответ: По умолчанию OrderedReady заставляет поды подниматься строго по одному. Если запуск тяжелого Java-контейнера занимает 2 минуты, запуск 10 реплик продлится 20 минут. Политика podManagementPolicy: Parallel отключает сериализацию: все поды создаются и масштабируются одновременно и параллельно, сохраняя при этом все остальные преимущества StatefulSet (уникальные стабильные имена node-0, node-1, персональные PVC и персональный DNS). Это применяется для распределённых систем, протоколы которых изначально рассчитаны на параллельный старт узлов (например, data-ноды Elasticsearch, кэширующий кластер Redis Cluster или распределенное хранилище ScyllaDB / Cassandra).


🎯 Шпаргалка для интервью

Главное о StatefulSet

  • Суть: контроллер для stateful-приложений, требующих сохранения состояния, стабильного сетевого имени и уникальных дисков.
  • Предсказуемые имена: pod-0, pod-1, pod-2 (сохраняются при любых перезапусках).
  • Headless Service: clusterIP: None обязателен для создания прямых DNS-записей к каждому узлу.
  • volumeClaimTemplates: автоматически генерирует персональный PVC для каждого пода; PVC никогда не удаляются автоматически при scale down.
  • Порядок: по умолчанию OrderedReady (запуск 0 $\to$ 1 $\to$ 2, остановка 2 $\to$ 1 $\to$ 0).
  • Production Standard: сырой StatefulSet недостаточен для боевых СУБД; для автоматического failover и бэкапов используются Kubernetes Operators (CloudNativePG, Zalando, Strimzi).
  • Зональность хранилищ: в StorageClass обязателен параметр volumeBindingMode: WaitForFirstConsumer.

StatefulSet vs Deployment

| Параметр | Deployment | StatefulSet | | :— | :— | :— | | Идентичность | Взаимозаменяемые поды (Cattle) | Уникальные именованные поды (Pets) | | Хранилище | Общий том или без диска | Персональный PVC на каждый под | | Сетевой доступ| Единый балансировщик Service | Прямой DNS к каждому модулю (Headless) | | Порядок старта| Параллельный | Поочерёдный (OrderedReady) |

Красные флаги на собеседовании (Чего говорить нельзя)

  • ❌ «Мы развернули PostgreSQL в StatefulSet на 3 реплики, и он сам автоматически настроил master-slave репликацию» — StatefulSet не знает логику БД, для этого нужен Operator или Patroni.
  • ❌ «StatefulSet подходит для обычных stateless микросервисов на Spring Boot» — антипаттерн, усложняющий кластер и замедляющий деплой.
  • ❌ «При удалении StatefulSet все базы данных и PVC автоматически стираются» — диски PVC остаются нетронутыми.

Связанные вопросы