StatefulSets: Chạy Stateful Applications trên Kubernetes
Deployment là workload controller mặc định cho stateless applications — bạn có thể scale up/down thoải mái vì mỗi Pod là identical và interchangeable. Nhưng với databases, message brokers, và distributed systems, mỗi instance cần có identity riêng, storage riêng, và thứ tự khởi động/tắt có ý nghĩa. Đây chính là lý do StatefulSet tồn tại.
StatefulSets vs Deployments
Khi Nào Dùng Deployment?
- Ứng dụng stateless: web servers, API services, microservices
- Tất cả Pods có thể xử lý bất kỳ request nào
- Không cần persistent storage riêng cho từng Pod
- Pods có thể bị replace ngẫu nhiên mà không ảnh hưởng hoạt động
Khi Nào Dùng StatefulSet?
- Ứng dụng cần stable network identity (Pod name không thay đổi)
- Mỗi Pod cần persistent storage riêng (primary, replica-1, replica-2)
- Thứ tự deployment, scaling, và deletion quan trọng
- Peer discovery dựa trên predictable DNS names
- Use cases: PostgreSQL, MySQL clusters, Redis Cluster, Kafka, Zookeeper, Elasticsearch
So Sánh Trực Quan
# Deployment: Pods có random names
kubectl get pods -l app=web-app
# NAME READY STATUS RESTARTS
# web-app-6d7b9c8f5-xk2p9 1/1 Running 0
# web-app-6d7b9c8f5-m3qr7 1/1 Running 0
# web-app-6d7b9c8f5-p8w4n 1/1 Running 0
# StatefulSet: Pods có stable, predictable names
kubectl get pods -l app=postgres
# NAME READY STATUS RESTARTS
# postgres-0 1/1 Running 0 ← primary
# postgres-1 1/1 Running 0 ← replica
# postgres-2 1/1 Running 0 ← replica
StatefulSet Guarantees
Stable Pod Identity
Mỗi Pod trong StatefulSet được đặt tên theo pattern {statefulset-name}-{ordinal}. Ordinal bắt đầu từ 0 và tăng dần. Kể cả khi Pod bị xóa và recreate, nó vẫn nhận lại đúng identity đó (postgres-1 luôn là postgres-1).
Stable Network Identity với Headless Service
StatefulSet yêu cầu một Headless Service (ClusterIP: None) để tạo DNS entries cho từng Pod. Với Headless Service, DNS không trỏ đến cluster IP mà trỏ trực tiếp đến Pod IPs.
# Headless Service cho StatefulSet
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: production
labels:
app: postgres
spec:
clusterIP: None # Đây là "headless" - không có ClusterIP
selector:
app: postgres
ports:
- port: 5432
name: postgres
Với headless service, DNS records được tạo theo pattern:
postgres-0.postgres.production.svc.cluster.localpostgres-1.postgres.production.svc.cluster.localpostgres-2.postgres.production.svc.cluster.local
Điều này cho phép các Pods tìm thấy nhau một cách deterministic — không cần service discovery phức tạp.
StatefulSet: PostgreSQL Example
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: production
spec:
serviceName: postgres # Phải match với Headless Service name
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
terminationGracePeriodSeconds: 60
initContainers:
# Init container để setup replication
- name: init-postgres
image: postgres:16
command:
- bash
- "-c"
- |
set -ex
# Xác định ordinal từ hostname
[[ $HOSTNAME =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
# Pod 0 là primary, các Pod còn lại là replica
if [[ $ordinal -eq 0 ]]; then
echo "primary" > /etc/postgres/role
else
echo "replica" > /etc/postgres/role
fi
volumeMounts:
- name: postgres-config
mountPath: /etc/postgres
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: POSTGRES_REPLICATION_USER
value: replicator
- name: POSTGRES_REPLICATION_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: replication-password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 2000m
memory: 4Gi
readinessProbe:
exec:
command: ["pg_isready", "-U", "postgres"]
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3
livenessProbe:
exec:
command: ["pg_isready", "-U", "postgres"]
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
- name: postgres-config
mountPath: /etc/postgres
volumes:
- name: postgres-config
emptyDir: {}
# volumeClaimTemplates: tự động tạo PVC riêng cho mỗi Pod
volumeClaimTemplates:
- metadata:
name: data
labels:
app: postgres
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: longhorn-replicated
resources:
requests:
storage: 100Gi
# Verify StatefulSet deployment
kubectl get statefulset postgres -n production
kubectl get pods -l app=postgres -n production -w
# Quan sát ordered deployment
# postgres-0 0/1 Pending 0 0s
# postgres-0 0/1 Init:0/1 0 1s
# postgres-0 1/1 Running 0 15s
# postgres-1 0/1 Pending 0 0s ← Chỉ bắt đầu sau khi postgres-0 Running
# postgres-1 1/1 Running 0 20s
# postgres-2 0/1 Pending 0 0s ← Chỉ bắt đầu sau khi postgres-1 Running
# Kiểm tra PVC được tạo tự động
kubectl get pvc -n production
# NAME STATUS VOLUME CAPACITY ACCESS MODES
# data-postgres-0 Bound pvc-abc123 100Gi RWO
# data-postgres-1 Bound pvc-def456 100Gi RWO
# data-postgres-2 Bound pvc-ghi789 100Gi RWO
# Kết nối vào primary
kubectl exec -it postgres-0 -n production -- psql -U postgres
StatefulSet: Redis Cluster Example
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cluster
namespace: production
spec:
serviceName: redis-cluster
replicas: 6 # 3 masters + 3 replicas
selector:
matchLabels:
app: redis-cluster
template:
metadata:
labels:
app: redis-cluster
spec:
containers:
- name: redis
image: redis:7.2
ports:
- containerPort: 6379
name: client
- containerPort: 16379
name: gossip
command: ["/conf/update-node.sh", "redis-server", "/conf/redis.conf"]
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: 1000m
memory: 2Gi
volumeMounts:
- name: conf
mountPath: /conf
readOnly: false
- name: data
mountPath: /data
volumes:
- name: conf
configMap:
name: redis-cluster-config
defaultMode: 0755
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: longhorn-replicated
resources:
requests:
storage: 10Gi
# Khởi tạo Redis Cluster sau khi tất cả Pods Running
kubectl exec -it redis-cluster-0 -n production -- redis-cli \
--cluster create \
$(kubectl get pods -n production -l app=redis-cluster -o jsonpath='{range .items[*]}{.status.podIP}:6379 {end}') \
--cluster-replicas 1 \
--cluster-yes
StatefulSet: Kafka với Strimzi Operator
Giới Thiệu Strimzi
Chạy Kafka thuần với StatefulSet rất phức tạp do Kafka phụ thuộc vào Zookeeper (đến version 3.x) và có nhiều cấu hình phức tạp. Strimzi Operator là operator chuyên biệt giúp deploy và manage Kafka clusters trên Kubernetes, abstract away complexity.
# Cài Strimzi Operator
kubectl create namespace kafka
kubectl apply -f 'https://strimzi.io/install/latest?namespace=kafka' -n kafka
# Verify operator
kubectl get pods -n kafka
# Kafka cluster với KRaft mode (không cần Zookeeper từ K8s 3.3+)
apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
metadata:
name: production-kafka
namespace: kafka
spec:
kafka:
version: 3.8.0
replicas: 3
listeners:
- name: plain
port: 9092
type: internal
tls: false
- name: tls
port: 9093
type: internal
tls: true
- name: external
port: 9094
type: loadbalancer
tls: true
config:
offsets.topic.replication.factor: 3
transaction.state.log.replication.factor: 3
transaction.state.log.min.isr: 2
default.replication.factor: 3
min.insync.replicas: 2
inter.broker.protocol.version: "3.8"
storage:
type: jbod
volumes:
- id: 0
type: persistent-claim
size: 100Gi
class: longhorn-replicated
deleteClaim: false
resources:
requests:
cpu: 500m
memory: 2Gi
limits:
cpu: 2000m
memory: 8Gi
# KRaft mode: không cần Zookeeper
metadataVersion: 3.8-IV0
entityOperator:
topicOperator: {}
userOperator: {}
# Bật KRaft mode
clusterCa:
renewalDays: 30
validityDays: 365
# Verify Kafka cluster
kubectl get kafka -n kafka
kubectl get pods -n kafka
# Tạo Kafka topic qua CRD
kubectl apply -f - << 'EOF'
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaTopic
metadata:
name: user-events
namespace: kafka
labels:
strimzi.io/cluster: production-kafka
spec:
partitions: 12
replicas: 3
config:
retention.ms: 604800000 # 7 days
compression.type: lz4
EOF
Update Strategies
RollingUpdate (Mặc Định)
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
updateStrategy:
type: RollingUpdate
rollingUpdate:
# Cập nhật từ Pod có ordinal cao nhất xuống thấp nhất
# Dừng lại ở partition - chỉ update Pods có ordinal >= partition
partition: 0 # Update tất cả Pods
# partition: 2 # Chỉ update pod-2, pod-3, ... (canary deploy)
# Canary update: chỉ update Pod cuối cùng trước
kubectl patch statefulset postgres -n production \
--patch '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":2}}}}'
# Verify Pod 2 được update
kubectl get pods -l app=postgres -n production -o jsonpath='{range .items[*]}{.metadata.name}: {.spec.containers[0].image}{"\n"}{end}'
# Nếu OK, update tất cả
kubectl patch statefulset postgres -n production \
--patch '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":0}}}}'
OnDelete Strategy
spec:
updateStrategy:
type: OnDelete # Chỉ update khi Pod bị delete thủ công
# Với OnDelete, sau khi update spec, phải manually delete Pods
kubectl delete pod postgres-2 -n production # Update pod-2
kubectl delete pod postgres-1 -n production # Update pod-1
kubectl delete pod postgres-0 -n production # Update pod-0 (primary) - cuối cùng
CloudNativePG: Chuẩn Mới Cho PostgreSQL
Tại Sao CloudNativePG?
StatefulSet thuần cho PostgreSQL vẫn đòi hỏi nhiều manual work: streaming replication setup, failover, backup management, monitoring. CloudNativePG (CNPG) là CNCF project (Sandbox 2022, Incubating 2024) được thiết kế specifically cho PostgreSQL trên Kubernetes.
# Cài CloudNativePG Operator
kubectl apply -f \
https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.23/releases/cnpg-1.23.0.yaml
# Verify
kubectl get pods -n cnpg-system
# PostgreSQL cluster với CloudNativePG
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgres-production
namespace: production
spec:
instances: 3 # 1 primary + 2 replicas
imageName: ghcr.io/cloudnative-pg/postgresql:16.3
# PostgreSQL configuration
postgresql:
parameters:
max_connections: "200"
shared_buffers: "512MB"
effective_cache_size: "2GB"
maintenance_work_mem: "128MB"
wal_level: "replica"
max_wal_senders: "10"
# Bootstrap từ scratch
bootstrap:
initdb:
database: myapp
owner: myapp_user
secret:
name: postgres-credentials
# Storage
storage:
size: 100Gi
storageClass: longhorn-replicated
# Backup sang S3
backup:
barmanObjectStore:
destinationPath: s3://my-backups/postgres
s3Credentials:
accessKeyId:
name: aws-credentials
key: ACCESS_KEY_ID
secretAccessKey:
name: aws-credentials
key: SECRET_ACCESS_KEY
wal:
compression: gzip
retentionPolicy: "30d"
# Monitoring
monitoring:
enablePodMonitor: true # Tích hợp với Prometheus Operator
# Resources
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 2000m
memory: 4Gi
# Verify cluster
kubectl get cluster postgres-production -n production
kubectl get pods -n production -l cnpg.io/cluster=postgres-production
# Xem trạng thái primary/replica
kubectl describe cluster postgres-production -n production
# ...
# Status:
# Current Primary: postgres-production-1
# Ready Instances: 3
# Phase: Cluster in healthy state
# Kết nối qua service
# Primary (read-write): postgres-production-rw
# Replica (read-only): postgres-production-ro
# Bất kỳ instance nào: postgres-production-r
kubectl exec -it postgres-production-1 -n production -- psql -U myapp_user myapp
# Trigger manual failover (nếu cần)
kubectl cnpg promote postgres-production postgres-production-2 -n production
# On-demand backup
kubectl cnpg backup postgres-production -n production
Khi Nào Dùng Operator Thay Vì StatefulSet Thuần?
StatefulSet Thuần Phù Hợp Khi:
- Application đơn giản, không cần phức tạp như databases lớn
- Bạn có đủ expertise để tự quản lý replication, failover
- Cần kiểm soát tối đa mọi aspect của deployment
- Application chưa có mature operator
Operator Phù Hợp Khi:
- Database/stateful system phức tạp: PostgreSQL, MySQL, Kafka, Elasticsearch
- Cần automated failover, backup, restore
- Team không có deep expertise về specific system
- Muốn Day-2 operations được automate (upgrades, scaling, certificates)
Recommended Operators (2026)
- PostgreSQL: CloudNativePG (CNCF Incubating) — production-ready, active development
- MySQL: MySQL Operator by Oracle hoặc Percona Operator for MySQL
- Kafka: Strimzi (CNCF Incubating) — mature, feature-rich
- Redis: Redis Operator by OpsTree hoặc Spotahome Redis Operator
- Elasticsearch/OpenSearch: ECK (Elastic Cloud on Kubernetes) hoặc OpenSearch Operator
- MongoDB: MongoDB Community Operator
Tổng Kết
StatefulSets là công cụ thiết yếu cho stateful workloads trong Kubernetes, nhưng hiểu rõ khi nào nên dùng StatefulSet thuần và khi nào nên dùng Operator là quan trọng:
- StatefulSet đảm bảo stable identity, ordered ops, và persistent storage per Pod — những gì stateful apps cần
- Headless Service là bắt buộc để tạo DNS records cho peer discovery
- volumeClaimTemplates tự động tạo PVC riêng cho mỗi Pod — không chia sẻ storage
- Ordered deployment/deletion: 0→N khi deploy, N→0 khi scale down — đảm bảo an toàn cho distributed systems
- Partition rolling updates cho phép canary deploy với StatefulSet
- CloudNativePG là chuẩn tốt nhất cho PostgreSQL production trên K8s năm 2026
- Với databases phức tạp, Operators tiết kiệm đáng kể thời gian và giảm rủi ro vận hành