DevOps

Stateful-приложения в Kubernetes: StatefulSets, Persistent Volumes и PostgreSQL в кластере

Ruslan Ismailov Опубликовано 18 мин чтения
S

Введение: stateful vs stateless — в чём разница и почему это важно

Kubernetes изначально проектировался как платформа для stateless-сервисов: контейнер упал — поднялся новый, всё работает. Но реальный мир устроен иначе. Базы данных, очереди сообщений, кэши с персистентностью — всё это stateful-приложения, которые хранят данные и требуют стабильной идентичности между перезапусками.

Ключевые отличия stateful от stateless в контексте Kubernetes:

  • Идентичность пода. Stateless-поды взаимозаменяемы: pod-abc и pod-xyz делают одно и то же. Stateful-поды имеют стабильные имена (например, postgres-0, postgres-1) и DNS-записи, которые сохраняются при перезапуске.
  • Персистентное хранилище. Stateless-контейнер не нуждается в сохранении данных на диске. PostgreSQL без персистентного тома потеряет все данные при рестарте.
  • Порядок запуска и завершения. В кластере PostgreSQL важно, чтобы primary запускался раньше replica. StatefulSet гарантирует упорядоченное создание и удаление подов.

Именно для решения этих задач в Kubernetes появился объект StatefulSet. Разберём его детально.

StatefulSets: принцип работы и отличия от Deployment

StatefulSet — это контроллер Kubernetes, специально разработанный для управления stateful-приложениями. В отличие от Deployment, он обеспечивает три гарантии:

  1. Стабильные сетевые идентификаторы. Каждый под получает предсказуемое DNS-имя вида <pod-name>.<service-name>.<namespace>.svc.cluster.local. Для PostgreSQL это означает, что postgres-0.postgres-headless.default.svc.cluster.local всегда указывает на primary-узел.
  2. Стабильное персистентное хранилище. Каждый под получает собственный PersistentVolumeClaim, который не удаляется при рестарте пода — данные сохраняются.
  3. Упорядоченное развёртывание и масштабирование. Поды создаются по порядку (0, 1, 2...) и удаляются в обратном порядке.

StatefulSet vs Deployment: практическое сравнение

Deployment подходит для API-серверов, фронтенда, worker-процессов без состояния. StatefulSet необходим для PostgreSQL, Redis Cluster, Elasticsearch, Kafka — везде, где каждый экземпляр уникален и хранит данные. При обновлении Deployment все поды могут заменяться одновременно (при RollingUpdate с нулевыми задержками). StatefulSet обновляет поды последовательно, что критично для кластера БД.

Пример минимального StatefulSet для PostgreSQL

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: default
spec:
  serviceName: postgres-headless
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_DB
              value: mydb
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: username
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          volumeMounts:
            - name: postgres-data
              mountPath: /var/lib/postgresql/data
          resources:
            requests:
              memory: "512Mi"
              cpu: "500m"
            limits:
              memory: "2Gi"
              cpu: "2"
          readinessProbe:
            exec:
              command: ["pg_isready", "-U", "$(POSTGRES_USER)", "-d", "$(POSTGRES_DB)"]
            initialDelaySeconds: 10
            periodSeconds: 5
          livenessProbe:
            exec:
              command: ["pg_isready", "-U", "$(POSTGRES_USER)"]
            initialDelaySeconds: 30
            periodSeconds: 10
  volumeClaimTemplates:
    - metadata:
        name: postgres-data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 50Gi

Обратите внимание на раздел volumeClaimTemplates — это ключевая особенность StatefulSet. Для каждого пода автоматически создаётся отдельный PVC с именем postgres-data-postgres-0, postgres-data-postgres-1 и так далее.

Headless Service для StatefulSet

apiVersion: v1
kind: Service
metadata:
  name: postgres-headless
  namespace: default
spec:
  clusterIP: None
  selector:
    app: postgres
  ports:
    - port: 5432
      targetPort: 5432

Headless Service (с clusterIP: None) не создаёт единый виртуальный IP, а регистрирует DNS-записи для каждого пода отдельно. Это фундамент для стабильной адресации узлов кластера PostgreSQL.

Persistent Volumes и Persistent Volume Claims: настройка хранилища

Хранилище в Kubernetes организовано через три уровня абстракции:

  • PersistentVolume (PV) — реальный ресурс хранилища: диск в облаке, NFS-шара, локальный SSD. Создаётся администратором кластера или динамически через StorageClass.
  • PersistentVolumeClaim (PVC) — запрос на хранилище от пода. Описывает требуемый размер, режим доступа и StorageClass.
  • StorageClass — шаблон для динамического создания PV. Определяет тип диска (SSD, HDD), политику утилизации и provisioner.

StorageClass для production PostgreSQL

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: "3000"
  throughput: "125"
  encrypted: "true"
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

Ключевые параметры для production:

  • reclaimPolicy: Retain — при удалении PVC данные на диске сохраняются. Никогда не используйте Delete для production БД.
  • volumeBindingMode: WaitForFirstConsumer — диск создаётся в той же зоне доступности, что и под. Критично для multi-AZ кластеров.
  • allowVolumeExpansion: true — позволяет увеличить размер PVC без пересоздания.
  • encrypted: "true" — шифрование данных на уровне диска.

Увеличение размера PVC

Для увеличения объёма хранилища достаточно изменить поле storage в существующем PVC:

kubectl patch pvc postgres-data-postgres-0 \
  -p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}'

Kubernetes автоматически расширит том, если это поддерживается provisioner и StorageClass имеет allowVolumeExpansion: true.

Запуск PostgreSQL в Kubernetes: шаг за шагом

Рассмотрим полный процесс развёртывания PostgreSQL в Kubernetes с нуля. Мы создадим Secret, ConfigMap, Headless Service, Service для внешнего доступа и StatefulSet.

Шаг 1: Secret с учётными данными

apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
  namespace: default
type: Opaque
stringData:
  username: pgadmin
  password: "S3cur3P@ssw0rd!"
  replication-password: "R3pl1c@P@ss!"

В production рекомендуется использовать внешние менеджеры секретов: HashiCorp Vault с оператором vault-secrets-operator, AWS Secrets Manager или Sealed Secrets.

Шаг 2: ConfigMap с postgresql.conf

apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-config
  namespace: default
data:
  postgresql.conf: |
    max_connections = 200
    shared_buffers = 512MB
    effective_cache_size = 1536MB
    maintenance_work_mem = 128MB
    checkpoint_completion_target = 0.9
    wal_buffers = 16MB
    default_statistics_target = 100
    random_page_cost = 1.1
    effective_io_concurrency = 200
    work_mem = 2621kB
    min_wal_size = 1GB
    max_wal_size = 4GB
    max_worker_processes = 4
    max_parallel_workers_per_gather = 2
    max_parallel_workers = 4
    wal_level = replica
    archive_mode = on
    archive_command = 'test ! -f /archive/%f && cp %p /archive/%f'
    hot_standby = on
    log_timezone = 'UTC'
    datestyle = 'iso, mdy'
    timezone = 'UTC'
    log_statement = 'ddl'
    log_min_duration_statement = 1000

Шаг 3: Service для внешнего доступа

apiVersion: v1
kind: Service
metadata:
  name: postgres-primary
  namespace: default
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-internal: "true"
spec:
  selector:
    app: postgres
    role: primary
  ports:
    - port: 5432
      targetPort: 5432
  type: ClusterIP

Шаг 4: Применение манифестов и проверка

# Применяем все манифесты
kubectl apply -f postgres-secret.yaml
kubectl apply -f postgres-config.yaml
kubectl apply -f postgres-headless-svc.yaml
kubectl apply -f postgres-statefulset.yaml

# Проверяем статус
kubectl get statefulsets
kubectl get pods -l app=postgres
kubectl get pvc

# Подключаемся к PostgreSQL
kubectl exec -it postgres-0 -- psql -U pgadmin -d mydb

# Проверяем статус репликации
kubectl exec -it postgres-0 -- psql -U pgadmin -c "SELECT * FROM pg_stat_replication;"

PostgreSQL Operator (CloudNativePG) в 2026 году: автоматизация управления кластером

Ручное управление StatefulSet подходит для изучения, но в production настоятельно рекомендуется использовать оператор. CloudNativePG — де-факто стандарт для запуска PostgreSQL в Kubernetes по состоянию на 2026 год. Это CNCF-проект, активно развиваемый командой EDB (EnterpriseDB).

Что даёт CloudNativePG

  • Автоматическое управление primary/replica топологией с failover.
  • Встроенная поддержка потоковой репликации и слотов репликации.
  • Нативная интеграция с pg_basebackup, Barman и WAL-G для бэкапов.
  • Управление пользователями и базами данных через CRD.
  • Автоматическое применение изменений конфигурации PostgreSQL без downtime.
  • Поддержка scheduled backups, point-in-time recovery (PITR).
  • Интеграция с Prometheus и Grafana из коробки.

Установка CloudNativePG

# Установка через kubectl
kubectl apply -f \
  https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.25/releases/cnpg-1.25.0.yaml

# Или через Helm
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo update
helm upgrade --install cnpg \
  --namespace cnpg-system \
  --create-namespace \
  cnpg/cloudnative-pg

# Проверяем установку
kubectl get pods -n cnpg-system

Создание кластера PostgreSQL через CloudNativePG

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgres-cluster
  namespace: production
spec:
  instances: 3

  imageName: ghcr.io/cloudnative-pg/postgresql:16.3

  postgresql:
    parameters:
      max_connections: "200"
      shared_buffers: "512MB"
      effective_cache_size: "1536MB"
      log_statement: "ddl"
      log_min_duration_statement: "1000"
    pg_hba:
      - host all all 10.0.0.0/8 scram-sha-256

  bootstrap:
    initdb:
      database: myapp
      owner: myapp_user
      secret:
        name: myapp-db-credentials

  storage:
    size: 100Gi
    storageClass: fast-ssd

  walStorage:
    size: 20Gi
    storageClass: fast-ssd

  backup:
    barmanObjectStore:
      destinationPath: s3://my-postgres-backups/cnpg
      s3Credentials:
        accessKeyId:
          name: s3-credentials
          key: ACCESS_KEY_ID
        secretAccessKey:
          name: s3-credentials
          key: ACCESS_SECRET_KEY
      wal:
        compression: gzip
        maxParallel: 8
    retentionPolicy: "30d"

  resources:
    requests:
      memory: "1Gi"
      cpu: "500m"
    limits:
      memory: "4Gi"
      cpu: "4"

  affinity:
    enablePodAntiAffinity: true
    topologyKey: kubernetes.io/hostname

  monitoring:
    enablePodMonitor: true

Этот манифест создаёт трёхузловой кластер PostgreSQL 16 с автоматической репликацией, бэкапами в S3 и встроенным мониторингом. CloudNativePG автоматически назначает роли primary и replica, управляет failover и обновлением узлов.

Проверка статуса кластера через kubectl plugin

# Установка плагина cnpg
kubectl krew install cnpg

# Статус кластера
kubectl cnpg status postgres-cluster -n production

# Переключение primary вручную (switchover)
kubectl cnpg promote postgres-cluster postgres-cluster-2 -n production

# Просмотр логов
kubectl cnpg logs cluster postgres-cluster -n production

Резервное копирование и восстановление PostgreSQL в Kubernetes

Бэкапы — критически важная часть production-эксплуатации. CloudNativePG поддерживает два режима: физические бэкапы через pg_basebackup/Barman и WAL-архивирование для PITR.

Scheduled Backup

apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
  name: postgres-daily-backup
  namespace: production
spec:
  schedule: "0 2 * * *"  # Каждый день в 02:00 UTC
  backupOwnerReference: self
  cluster:
    name: postgres-cluster
  method: barmanObjectStore
  immediate: true

Point-in-Time Recovery (PITR)

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgres-restored
  namespace: production
spec:
  instances: 3
  imageName: ghcr.io/cloudnative-pg/postgresql:16.3

  bootstrap:
    recovery:
      source: postgres-cluster
      recoveryTarget:
        targetTime: "2026-03-15 14:30:00"

  externalClusters:
    - name: postgres-cluster
      barmanObjectStore:
        destinationPath: s3://my-postgres-backups/cnpg
        s3Credentials:
          accessKeyId:
            name: s3-credentials
            key: ACCESS_KEY_ID
          secretAccessKey:
            name: s3-credentials
            key: ACCESS_SECRET_KEY

  storage:
    size: 100Gi
    storageClass: fast-ssd

PITR позволяет восстановить базу данных в любой момент времени, для которого сохранены WAL-сегменты. Это незаменимо при случайном DROP TABLE или логической ошибке приложения.

Ручной бэкап через Velero

Для дополнительного уровня защиты можно использовать Velero для резервного копирования PVC на уровне Kubernetes. Однако для PostgreSQL предпочтительнее application-consistent бэкапы через Barman или pg_dump, так как Velero делает snapshot на уровне файловой системы, что может привести к inconsistent-состоянию при снятии снимка на работающей БД.

Сетевые политики и безопасность для stateful-сервисов

Безопасность stateful-сервисов в Kubernetes требует дополнительного внимания: утечка данных из БД катастрофична. Применяйте принцип минимальных привилегий.

NetworkPolicy для PostgreSQL

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgres-network-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      cnpg.io/cluster: postgres-cluster
  policyTypes:
    - Ingress
    - Egress
  ingress:
    # Разрешаем трафик только от приложения
    - from:
        - namespaceSelector:
            matchLabels:
              name: production
          podSelector:
            matchLabels:
              app: myapp
      ports:
        - protocol: TCP
          port: 5432
    # Разрешаем внутрикластерную репликацию
    - from:
        - podSelector:
            matchLabels:
              cnpg.io/cluster: postgres-cluster
      ports:
        - protocol: TCP
          port: 5432
  egress:
    # Разрешаем репликацию между нодами
    - to:
        - podSelector:
            matchLabels:
              cnpg.io/cluster: postgres-cluster
      ports:
        - protocol: TCP
          port: 5432
    # Разрешаем DNS
    - ports:
        - protocol: UDP
          port: 53
    # Разрешаем бэкапы в S3
    - ports:
        - protocol: TCP
          port: 443

Pod Security и RBAC

Дополнительные меры безопасности для production:

  • Используйте PodSecurityAdmission с политикой restricted для namespace с PostgreSQL.
  • Настройте SecurityContext с runAsNonRoot: true, readOnlyRootFilesystem: true и allowPrivilegeEscalation: false.
  • Ограничьте RBAC: ServiceAccount PostgreSQL не должен иметь доступа к секретам других namespace.
  • Используйте TLS для соединений между репликами (CloudNativePG включает это по умолчанию).
  • Включите шифрование etcd для защиты Secrets на уровне кластера Kubernetes.
  • Ротируйте пароли через внешний vault и используйте short-lived credentials.

PodDisruptionBudget для высокой доступности

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: postgres-pdb
  namespace: production
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      cnpg.io/cluster: postgres-cluster

PDB гарантирует, что Kubernetes не удалит более одного пода одновременно при дрейне ноды или обновлении кластера, обеспечивая кворум для репликации.

Мониторинг PostgreSQL в Kubernetes: pg_exporter и Grafana

Без полноценного мониторинга управление production PostgreSQL невозможно. CloudNativePG включает встроенный Prometheus-совместимый эндпоинт, но для детального мониторинга рекомендуется использовать postgres_exporter.

Развёртывание postgres_exporter

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres-exporter
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres-exporter
  template:
    metadata:
      labels:
        app: postgres-exporter
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9187"
    spec:
      containers:
        - name: postgres-exporter
          image: prometheuscommunity/postgres-exporter:v0.15.0
          ports:
            - containerPort: 9187
          env:
            - name: DATA_SOURCE_NAME
              valueFrom:
                secretKeyRef:
                  name: postgres-exporter-secret
                  key: datasource
          args:
            - --collector.stat_statements
            - --collector.replication
            - --collector.replication_slot
            - --collector.long_running_transactions
          resources:
            requests:
              memory: "64Mi"
              cpu: "50m"
            limits:
              memory: "128Mi"
              cpu: "200m"

PodMonitor для CloudNativePG

apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: postgres-cluster-monitor
  namespace: production
spec:
  selector:
    matchLabels:
      cnpg.io/cluster: postgres-cluster
  podMetricsEndpoints:
    - port: metrics
      interval: 30s
      scrapeTimeout: 25s

Ключевые метрики для дашборда Grafana

Настройте алерты и дашборды для следующих метрик:

  • pg_stat_database_tup_fetched — количество fetch-операций по базам данных.
  • pg_stat_replication_pg_wal_lsn_diff — replication lag в байтах. Алерт при превышении 100MB.
  • pg_locks_count — количество блокировок. Рост указывает на проблемы с запросами.
  • pg_stat_bgwriter_buffers_alloc — давление на буферный кэш.
  • cnpg_collector_pg_postmaster_start_time — время последнего рестарта PostgreSQL.
  • pg_database_size_bytes — размер баз данных. Для планирования расширения хранилища.
  • pg_stat_statements_mean_exec_time_seconds — среднее время выполнения запросов.

Grafana Dashboard

Используйте готовый дашборд CloudNativePG для Grafana (ID: 20417) — он включает все критически важные метрики кластера и отдельные панели для каждого экземпляра. Для postgres_exporter используйте дашборд ID 9628.

Заключение и рекомендации

Запуск PostgreSQL в Kubernetes перестал быть экзотикой — в 2026 году это зрелая практика с богатой экосистемой инструментов. Подведём итоги ключевых рекомендаций для production:

  • Используйте CloudNativePG вместо ручного управления StatefulSet. Оператор берёт на себя failover, управление репликацией, бэкапы и обновления.
  • Всегда настраивайте StorageClass с reclaimPolicy: Retain. Потеря данных из-за случайного удаления PVC недопустима.
  • Размещайте реплики в разных зонах доступности с помощью affinity-правил и topologySpreadConstraints.
  • Настройте WAL-архивирование с самого начала. PITR спасает при логических ошибках, которые не покрываются физическими бэкапами.
  • Используйте NetworkPolicy для ограничения доступа к PostgreSQL только от авторизованных сервисов.
  • Не запускайте PostgreSQL на shared-нодах с resource-intensive приложениями. Используйте dedicated node pool с taint/toleration для изоляции.
  • Регулярно тестируйте восстановление. Бэкап, который не тестировался, — не бэкап.
  • Мониторьте replication lag и размер WAL. Эти метрики первыми сигнализируют о проблемах производительности.

Экосистема Kubernetes продолжает развиваться, и stateful-приложения в Kubernetes становятся всё более надёжными. Правильно настроенный кластер PostgreSQL в Kubernetes с CloudNativePG, grounded мониторингом и автоматическими бэкапами может соответствовать требованиям самых высоконагруженных production-систем.

Технологии

Теги

Руслан Исмаилов

Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →