StatefulSet 有状态应用深度解析
为什么需要 StatefulSet
Deployment 管理的 Pod 是无状态的(Pod 名称随机、重建后 IP 变化、存储不持久)。StatefulSet 为有状态应用提供:
- 稳定的网络标识:Pod 名称固定(
pod-0、pod-1、pod-2) - 稳定的存储:每个 Pod 绑定独立的 PVC,重建后自动重新挂载
- 有序部署/扩缩容:按序号顺序操作(0→1→2)
- 有序滚动更新:从最大序号开始(2→1→0)
典型使用场景
- 数据库集群(MySQL、PostgreSQL、MongoDB)
- 消息队列(Kafka、RabbitMQ)
- 分布式存储(Elasticsearch、Cassandra)
- 分布式协调(ZooKeeper、etcd)
完整配置示例
yaml
# Headless Service(StatefulSet 必须配套)
apiVersion: v1
kind: Service
metadata:
name: mysql
labels:
app: mysql
spec:
clusterIP: None # Headless:不分配 ClusterIP
selector:
app: mysql
ports:
- port: 3306
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql # 关联 Headless Service
replicas: 3
selector:
matchLabels:
app: mysql
# 更新策略
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 0 # 只更新序号 >= partition 的 Pod(金丝雀发布)
# Pod 管理策略
podManagementPolicy: OrderedReady # OrderedReady | Parallel
template:
metadata:
labels:
app: mysql
spec:
initContainers:
- name: init-mysql
image: mysql:8.0
command:
- bash
- "-c"
- |
# 根据 Pod 序号决定角色
[[ $(hostname) =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
if [[ $ordinal -eq 0 ]]; then
echo "I am the primary"
else
echo "I am a replica"
fi
volumeMounts:
- name: conf
mountPath: /mnt/conf.d
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: password
ports:
- containerPort: 3306
volumeMounts:
- name: data
mountPath: /var/lib/mysql
- name: conf
mountPath: /etc/mysql/conf.d
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
volumes:
- name: conf
emptyDir: {}
# 每个 Pod 独立的 PVC 模板
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 50Gi稳定的网络标识
StatefulSet 配合 Headless Service,每个 Pod 获得固定 DNS 名称:
Pod 名称:mysql-0, mysql-1, mysql-2
DNS 格式:<pod-name>.<service-name>.<namespace>.svc.cluster.local
mysql-0.mysql.default.svc.cluster.local → mysql-0 的 IP
mysql-1.mysql.default.svc.cluster.local → mysql-1 的 IP
mysql-2.mysql.default.svc.cluster.local → mysql-2 的 IP应用可以通过固定 DNS 名称访问特定 Pod(如主节点):
go
// 连接 MySQL 主节点(始终是 mysql-0)
db, err := sql.Open("mysql", "root:password@tcp(mysql-0.mysql:3306)/mydb")有序操作
扩容(0→1→2):
创建 mysql-0 → 等待 Ready → 创建 mysql-1 → 等待 Ready → 创建 mysql-2
缩容(2→1→0):
删除 mysql-2 → 等待终止 → 删除 mysql-1 → 等待终止
滚动更新(2→1→0):
更新 mysql-2 → 等待 Ready → 更新 mysql-1 → 等待 Ready → 更新 mysql-0金丝雀发布(partition)
bash
# 只更新序号 >= 2 的 Pod(先验证 mysql-2)
kubectl patch statefulset mysql -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":2}}}}'
kubectl set image statefulset/mysql mysql=mysql:8.1
# 验证 mysql-2 正常后,继续更新剩余 Pod
kubectl patch statefulset mysql -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":0}}}}'常用操作
bash
# 查看 StatefulSet 状态
kubectl get statefulset mysql
kubectl rollout status statefulset/mysql
# 查看各 Pod 的 PVC
kubectl get pvc -l app=mysql
# 扩容(PVC 自动创建)
kubectl scale statefulset mysql --replicas=5
# 缩容(PVC 不会自动删除,需手动清理)
kubectl scale statefulset mysql --replicas=3
kubectl delete pvc data-mysql-3 data-mysql-4
# 删除 StatefulSet(保留 PVC)
kubectl delete statefulset mysql --cascade=orphan与 Deployment 对比
| 特性 | Deployment | StatefulSet |
|---|---|---|
| Pod 名称 | 随机后缀 | 固定序号 |
| 网络标识 | 随机 IP | 固定 DNS |
| 存储 | 共享或无 | 每 Pod 独立 PVC |
| 部署顺序 | 并行 | 有序 |
| 更新顺序 | 随机 | 逆序 |
| 适用场景 | 无状态服务 | 有状态服务 |