StatefulSet: приложения с состоянием

StatefulSet запускает поды с устойчивой идентичностью и собственным хранилищем — это контроллер для баз данных и кластеров, а не для безликих веб-реплик.

StatefulSet — контроллер для приложений с состоянием: он даёт каждому поду стабильное имя, предсказуемый порядок запуска и собственный том, который переживает пересоздание пода.

В разделе про базовые контроллеры вы уже видели Deployment: он отлично гоняет stateless-приложения, где реплики взаимозаменяемы. Поды Deployment получают случайные имена вроде web-7d9f8c-abcde, стартуют и гибнут в любом порядке, и любой из них может ответить на запрос. Но как только приложению нужна идентичность — собственные данные на диске, фиксированный адрес, роль «первичного» узла — модель Deployment ломается. Поднимать так PostgreSQL, MySQL, MongoDB, Kafka, Zookeeper, Elasticsearch нельзя: реплики такого кластера не равноправны.

Для этого и существует StatefulSet. Он сохраняет идею декларативного контроллера (вы описываете желаемое, Kubernetes приводит кластер к нему), но добавляет три гарантии: устойчивые сетевые имена, упорядоченное развёртывание и привязку постоянного тома к конкретному поду.

Зачем это на практике

Представьте кластер PostgreSQL из трёх узлов: один первичный (принимает запись) и две реплики (только чтение). Здесь нельзя «просто перезапустить любой под»: у каждого свои данные, а реплики должны знать постоянный адрес первичного, чтобы тянуть журнал. StatefulSet решает три задачи разом:

  • Стабильная идентичность. Поды называются предсказуемо: postgres-0, postgres-1, postgres-2. Имя сохраняется при пересоздании: упавший postgres-1 вернётся именно как postgres-1, а не получит случайный суффикс.
  • Привязка данных. Каждый под получает собственный PersistentVolumeClaim. Том data-postgres-1 навсегда закреплён за postgres-1 — пересоздание пода не теряет данные и не путает их с чужими.
  • Порядок. Поды поднимаются по очереди: сначала postgres-0 станет первичным, потом подключаются реплики. Удаление идёт в обратном порядке.

Headless service и стабильные DNS-имена

Чтобы у каждого пода был постоянный адрес, StatefulSet работает в паре с headless-сервисом — обычным Service с полем clusterIP: None. Такой сервис не выдаёт единый виртуальный IP и не балансирует нагрузку; вместо этого он публикует DNS-запись на каждый под отдельно.

apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  clusterIP: None        # headless: без единого виртуального IP
  selector:
    app: postgres
  ports:
    - port: 5432

В результате внутри кластера резолвятся стабильные имена вида postgres-0.postgres.default.svc.cluster.local. Реплика всегда найдёт первичный по адресу postgres-0.postgres, даже если под пересоздавался. Имя сервиса указывается в поле serviceName самого StatefulSet — это обязательная связка.

Манифест StatefulSet

Главное отличие от Deployment в манифесте — секция volumeClaimTemplates: это не один общий том, а шаблон, по которому Kubernetes создаёт отдельный PVC для каждого пода.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres        # ссылка на headless-сервис
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16
          ports:
            - containerPort: 5432
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:        # по PVC на каждый под
    - metadata:
        name: data
      spec:
        accessModes: [ "ReadWriteOnce" ]
        resources:
          requests:
            storage: 10Gi

Применяем и наблюдаем за упорядоченным запуском:

kubectl apply -f postgres-statefulset.yaml

# Поды появляются строго по очереди: 0, затем 1, затем 2
kubectl get pods -l app=postgres -w

# Имена PVC привязаны к индексам подов
kubectl get pvc
# data-postgres-0   Bound   ...
# data-postgres-1   Bound   ...
# data-postgres-2   Bound   ...

Как это работает под капотом

Контроллер StatefulSet в kube-controller-manager ведёт поды по индексам, а не как безымянное множество. При создании он идёт от 0 к N-1 и не запускает следующий под, пока предыдущий не станет Ready (по умолчанию политика OrderedReady). При удалении — наоборот, от старшего к младшему. Это и даёт упорядоченность: реплика не стартует раньше, чем первичный готов её обслужить.

Тома живут отдельной жизнью. PVC, созданные из volumeClaimTemplates, не удаляются вместе со StatefulSet — это защита от случайной потери данных. Когда под пересоздаётся, планировщик находит существующий PVC с тем же индексом и монтирует его обратно. Именно поэтому postgres-1, упав, возвращается со своими прежними данными.

Масштабирование тоже упорядочено. kubectl scale statefulset postgres --replicas=5 добавит postgres-3 и postgres-4 по очереди; уменьшение до трёх удалит postgres-4, затем postgres-3, но их PVC останутся, чтобы при обратном росте данные вернулись.

Частые ошибки

  • Нет headless-сервиса или не совпадает serviceName. Без него стабильные DNS-имена подов не появятся, и кластер не соберётся. Сервис должен существовать и его имя — точно совпадать с spec.serviceName.
  • Ожидание, что StatefulSet удалит тома. После kubectl delete statefulset PVC остаются. Это не баг: удаляйте их вручную, когда данные точно не нужны, иначе хранилище будет копиться.
  • StatefulSet там, где хватило бы Deployment. Для stateless-приложений он избыточен: упорядоченный запуск замедляет выкатку, а PVC не нужны. Правило простое: нет собственных данных и роли узла — берите Deployment.
  • Расчёт на общий том для всех реплик. volumeClaimTemplates даёт отдельный диск каждому поду (режим ReadWriteOnce). Это не общая папка — у каждого узла своя копия данных, как и должно быть в кластере БД.

Итоги

  • StatefulSet — контроллер для приложений с состоянием: БД, очередей, кластеров.
  • Даёт стабильные имена подов (name-0, name-1, …), сохраняющиеся при пересоздании.
  • Запуск и удаление идут упорядоченно по индексам; следующий под ждёт готовности предыдущего.
  • volumeClaimTemplates создаёт по постоянному тому на под, и том навсегда привязан к своему индексу.
  • Требует headless-сервиса (clusterIP: None) для адресации отдельных подов через DNS.
Проверьте себя
1. Чем поды StatefulSet отличаются от подов Deployment?
AОни получают случайные имена и стартуют параллельно
BОни получают стабильные имена (name-0, name-1) и запускаются по порядку
CОни вообще не могут иметь подключённых томов
DОни всегда работают только в одном экземпляре
2. Зачем StatefulSet нужен headless-сервис (clusterIP: None)?
AЧтобы балансировать нагрузку между подами одним IP
BЧтобы публиковать стабильную DNS-запись на каждый под отдельно
CЧтобы скрыть поды от остального кластера
DЧтобы автоматически удалять PVC при удалении StatefulSet
3. Что происходит с PVC, созданными через volumeClaimTemplates, при удалении StatefulSet?
AОни удаляются автоматически вместе со StatefulSet
BОни остаются — данные нужно удалять вручную
CОни превращаются в один общий том
DОни переносятся на другой узел кластера