Kubernetes 中的 StatefulSets 与持久化存储
使用有序扩展和持久数据部署有状态应用
Kubernetes StatefulSets 是管理需要稳定身份、持久存储和有序部署模式的有状态应用程序的最佳解决方案,对于数据库、分布式系统和缓存层至关重要。
如果您是 Kubernetes 新手或正在设置集群,请考虑探索 Kubernetes 发行版,如 k3s 或 MicroK8s 用于开发,或 使用 Kubespray 安装 Kubernetes 用于生产级集群。
这张漂亮的图片是由 AI 模型 Flux 1 dev 生成的。
什么是 StatefulSets?
StatefulSets 是 Kubernetes 的工作负载 API 对象,专门用于管理有状态应用程序。与将所有 Pod 视为可互换的 Deployments 不同,StatefulSets 为每个 Pod 保持唯一的身份,并保证顺序和唯一性。
关键特性:
- 稳定的网络标识符:每个 Pod 都会获得一个在重启后仍能保持的可预测的主机名
- 持久存储:专用的 PersistentVolumeClaims 随着 Pod 的重新调度而跟随
- 有序部署:Pod 按顺序创建(0, 1, 2…)并按相反顺序终止
- 有序更新:滚动更新按顺序进行,确保应用程序的稳定性
StatefulSets 对于 PostgreSQL、MySQL、MongoDB、Cassandra、Elasticsearch、Kafka、ZooKeeper、Redis 和 etcd 等应用程序至关重要,这些应用程序中 Pod 的身份和数据持久性非常重要。
理解 Kubernetes 中的持久存储
Kubernetes 提供了一种复杂的存储抽象层,将存储管理与 Pod 生命周期解耦:
存储组件
PersistentVolume (PV):由管理员预置或通过 StorageClass 动态创建的集群中的一块存储。PV 独立于 Pod 存在。
PersistentVolumeClaim (PVC):Pod 对存储的请求。PVC 绑定到满足其要求(大小、访问模式、存储类)的可用 PV。
StorageClass:定义不同“类”的存储,具有各种提供者(AWS EBS、GCE PD、Azure Disk、NFS 等)和参数,如复制、性能层级和备份策略。
访问模式
- ReadWriteOnce (RWO):卷由单个节点以读写方式挂载
- ReadOnlyMany (ROX):卷由多个节点以只读方式挂载
- ReadWriteMany (RWX):卷由多个节点以读写方式挂载(需要特殊的存储后端)
StatefulSet 存储架构
StatefulSets 使用 volumeClaimTemplates 自动为每个 Pod 副本创建 PersistentVolumeClaims。这与 Deployments 有根本的不同:
volumeClaimTemplates 的工作方式
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql-service
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: data
mountPath: /var/lib/mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: password
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "fast-ssd"
resources:
requests:
storage: 10Gi
当您创建这个 StatefulSet 时:
- Kubernetes 创建 Pod
mysql-0和 PVCdata-mysql-0 - 然后创建 Pod
mysql-1和 PVCdata-mysql-1 - 最后创建 Pod
mysql-2和 PVCdata-mysql-2
每个 Pod 都会获得自己的专用 10GB 持久卷。如果 mysql-1 被删除或重新调度,Kubernetes 会重新创建它并重新附加相同的 data-mysql-1 PVC,保留所有数据。
为 StatefulSets 创建无头服务
StatefulSets 需要一个无头服务来提供稳定的网络身份:
apiVersion: v1
kind: Service
metadata:
name: mysql-service
spec:
clusterIP: None # 这使得它成为一个无头服务
selector:
app: mysql
ports:
- port: 3306
name: mysql
这会为每个 Pod 创建 DNS 条目:
mysql-0.mysql-service.default.svc.cluster.localmysql-1.mysql-service.default.svc.cluster.localmysql-2.mysql-service.default.svc.cluster.local
应用程序可以使用这些稳定的 DNS 名称直接连接到特定的 Pod 实例。
StatefulSet 部署模式
对于管理复杂 Kubernetes 部署的团队,Helm Charts 提供了一种强大的方式,通过模板化、版本管理和依赖管理来打包和部署 StatefulSets。Helm 简化了在不同环境中管理 StatefulSet 配置。
有序扩展
从 3 个副本扩展到 5 个副本时:
kubectl scale statefulset mysql --replicas=5
Kubernetes 按顺序创建 Pod:mysql-3 → 等待 Ready → mysql-4
从 5 个副本扩展到 3 个副本时:
kubectl scale statefulset mysql --replicas=3
Kubernetes 按相反顺序终止:mysql-4 → 等待终止 → mysql-3
滚动更新
StatefulSets 支持两种更新策略:
OnDelete:手动更新——只有在删除 Pod 时才会更新 RollingUpdate:按逆序自动顺序更新
spec:
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 2 # 仅更新序号 >= 2 的 Pod
partition 参数启用金丝雀部署——您可以先更新高序号的 Pod,测试后再推广到所有副本。
存储最佳实践
动态供应
始终使用 StorageClasses 进行动态卷供应:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp3
iops: "3000"
throughput: "125"
allowVolumeExpansion: true
reclaimPolicy: Retain
allowVolumeExpansion:启用在不重新创建 PVC 的情况下调整 PVC 大小
reclaimPolicy:Retain 在 PVC 删除后保留 PV 数据,Delete 自动删除数据
PVC 保留策略
Kubernetes 1.23+ 支持 persistentVolumeClaimRetentionPolicy:
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain # StatefulSet 删除时保留 PVC
whenScaled: Delete # 缩放时删除 PVC
选项:
- Retain:保留 PVC(默认行为,最安全)
- Delete:自动删除 PVC(适用于开发环境)
备份策略
卷快照:使用 VolumeSnapshot 资源创建时间点备份
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: mysql-snapshot-20251113
spec:
volumeSnapshotClassName: csi-snapclass
source:
persistentVolumeClaimName: data-mysql-0
应用级备份:使用 mysqldump、pg_dump 或 Velero 进行数据库特定备份
分布式复制:配置应用级复制(MySQL 复制、PostgreSQL 流式复制)作为第一道防线
实际应用案例
数据库集群(PostgreSQL)
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "standard"
resources:
requests:
storage: 20Gi
分布式缓存(Redis)
对于 Redis 集群,您需要 StatefulSet 和仔细的配置:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
serviceName: redis-service
replicas: 6
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
command:
- redis-server
- "--appendonly"
- "yes"
- "--appendfsync"
- "everysec"
ports:
- containerPort: 6379
name: redis
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
消息队列(Kafka)
Kafka 需要持久存储用于日志和稳定的网络标识用于经纪人协调:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: kafka
spec:
serviceName: kafka-service
replicas: 3
selector:
matchLabels:
app: kafka
template:
metadata:
labels:
app: kafka
spec:
containers:
- name: kafka
image: confluentinc/cp-kafka:7.5.0
ports:
- containerPort: 9092
name: kafka
env:
- name: KAFKA_BROKER_ID
valueFrom:
fieldRef:
fieldPath: metadata.name
volumeMounts:
- name: data
mountPath: /var/lib/kafka/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Gi
监控和故障排除
有关本节中使用的 Kubernetes 命令的全面参考,请参阅 Kubernetes 快速参考。
查看 StatefulSet 状态
# 查看 StatefulSet 详细信息
kubectl get statefulset mysql
kubectl describe statefulset mysql
# 检查 Pod 创建顺序和状态
kubectl get pods -l app=mysql -w
# 查看 PVC 状态
kubectl get pvc
kubectl describe pvc data-mysql-0
常见问题
Pod 卡在 Pending 状态:检查 PVC 状态和存储可用性
kubectl describe pod mysql-0
kubectl get events --sort-by='.lastTimestamp'
存储已满:如果 StorageClass 允许,扩展 PVC
kubectl patch pvc data-mysql-0 -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
Pod 不会终止:检查应用级锁或卷卸载问题
kubectl delete pod mysql-0 --grace-period=0 --force
需要监控的指标
- 存储使用情况:监控 PVC 容量和使用百分比
- I/O 性能:跟踪 IOPS、吞吐量和延迟
- Pod 重启:频繁重启可能表明存储问题
- PVC 绑定时间:绑定缓慢表明供应问题
迁移策略
在迁移到 StatefulSets 时,请确保 Kubernetes 集群已正确配置。对于家庭实验室或小型集群设置,请查看我们的 Kubernetes 发行版全面比较,以选择适合您工作负载需求的平台。
从 Deployment 迁移到 StatefulSet
- 创建 StatefulSet 并包含 volumeClaimTemplates
- 优雅地缩放 Deployment
- 从备份中恢复数据 到 StatefulSet Pod
- 更新 DNS/Service 引用
- 删除旧的 Deployment 和 PVC
迁移前的备份
# 快照现有 PVC
kubectl get pvc -o yaml > pvc-backup.yaml
# 创建卷快照
kubectl apply -f volume-snapshot.yaml
安全考虑
存储加密
使用 StorageClass 参数启用静态加密:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: encrypted-storage
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp3
encrypted: "true"
kmsKeyId: arn:aws:kms:us-east-1:123456789012:key/abcd-1234
访问控制
使用 RBAC 限制谁可以创建/修改 StatefulSets 和 PVC:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: statefulset-manager
rules:
- apiGroups: ["apps"]
resources: ["statefulsets"]
verbs: ["get", "list", "create", "update", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "create", "delete"]
网络策略
限制 Pod 之间的通信:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: mysql-network-policy
spec:
podSelector:
matchLabels:
app: mysql
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: backend
ports:
- protocol: TCP
port: 3306
性能优化
存储性能层级
根据工作负载选择适当的 StorageClasses:
- 高 IOPS:具有大量随机读写的数据库(gp3, io2)
- 高吞吐量:日志聚合、分析(st1, sc1)
- 平衡:通用应用程序(gp3)
Pod 分布
使用 Pod 反亲和性将 StatefulSet Pod 分布在可用区域中:
spec:
template:
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: mysql
topologyKey: topology.kubernetes.io/zone
资源请求和限制
设置适当的资源以实现一致的性能:
resources:
requests:
cpu: "2"
memory: "4Gi"
ephemeral-storage: "10Gi"
limits:
cpu: "4"
memory: "8Gi"
ephemeral-storage: "20Gi"
高级模式
使用初始化容器的有状态应用程序
使用初始化容器进行数据库初始化:
spec:
template:
spec:
initContainers:
- name: init-mysql
image: mysql:8.0
command:
- bash
- "-c"
- |
if [[ ! -d /var/lib/mysql/mysql ]]; then
mysqld --initialize-insecure --datadir=/var/lib/mysql
fi
volumeMounts:
- name: data
mountPath: /var/lib/mysql
用于 Sidecar 的多容器 Pod
添加备份 Sidecar 或监控代理:
spec:
template:
spec:
containers:
- name: mysql
image: mysql:8.0
# ... mysql 配置 ...
- name: backup-sidecar
image: mysql-backup:latest
volumeMounts:
- name: data
mountPath: /var/lib/mysql
readOnly: true
用于动态配置的 ConfigMaps
将配置与 StatefulSet 定义分离:
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql-config
data:
my.cnf: |
[mysqld]
max_connections=200
innodb_buffer_pool_size=2G
---
spec:
template:
spec:
containers:
- name: mysql
volumeMounts:
- name: config
mountPath: /etc/mysql/conf.d
volumes:
- name: config
configMap:
name: mysql-config