Stateful-приложения в Kubernetes: StatefulSets, Persistent Volumes и PostgreSQL в кластере
Введение: 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, он обеспечивает три гарантии:
- Стабильные сетевые идентификаторы. Каждый под получает предсказуемое DNS-имя вида
<pod-name>.<service-name>.<namespace>.svc.cluster.local. Для PostgreSQL это означает, чтоpostgres-0.postgres-headless.default.svc.cluster.localвсегда указывает на primary-узел. - Стабильное персистентное хранилище. Каждый под получает собственный
PersistentVolumeClaim, который не удаляется при рестарте пода — данные сохраняются. - Упорядоченное развёртывание и масштабирование. Поды создаются по порядку (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. Подробнее обо мне →