StatefulSet - 有状态应用
StatefulSet - 有状态应用
为什么需要 StatefulSet
Deployment 适合无状态应用,但对于有状态应用(如数据库、消息队列)存在问题:
无状态 vs 有状态
无状态应用(Stateless)
- Pod 可以随意替换
- Pod 之间完全等价
- 适合使用 Deployment
有状态应用(Stateful)
- 每个 Pod 有唯一身份
- Pod 之间不完全等价
- 需要稳定的网络标识
- 需要持久化存储
- 需要有序部署和扩展
StatefulSet 的特点
- 稳定的网络标识:Pod 名称固定
- 稳定的持久化存储:每个 Pod 独立的 PVC
- 有序部署和扩展:按顺序创建和删除
- 有序滚动更新:可控的更新过程
基础示例
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None # Headless Service
selector:
app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx" # 关联的 Headless Service
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.21
ports:
- containerPort: 80
name: web
kubectl apply -f statefulset.yaml
# 查看 Pod
kubectl get pods -l app=nginx
# 输出
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 10s
web-1 1/1 Running 0 8s
web-2 1/1 Running 0 6s
注意 Pod 名称的规律:<statefulset-name>-<ordinal>
StatefulSet 特性
1. 稳定的网络标识
每个 Pod 有固定的 DNS 名称:
<pod-name>.<service-name>.<namespace>.svc.cluster.local
示例:
# Pod web-0 的 DNS
web-0.nginx.default.svc.cluster.local
# 从集群内访问
ping web-0.nginx
curl http://web-0.nginx
2. 有序部署
Pod 按序号顺序创建:
1. 创建 web-0,等待 Running 和 Ready
2. 创建 web-1,等待 Running 和 Ready
3. 创建 web-2,等待 Running 和 Ready
3. 有序删除
缩容时按逆序删除:
kubectl scale statefulset web --replicas=1
# 删除顺序
1. 删除 web-2
2. 删除 web-1
3. 保留 web-0
4. 持久化存储
每个 Pod 有独立的 PVC:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
# 查看创建的 PVC
kubectl get pvc
# 输出
NAME STATUS VOLUME CAPACITY ACCESS MODES
www-web-0 Bound pv-001 1Gi RWO
www-web-1 Bound pv-002 1Gi RWO
www-web-2 Bound pv-003 1Gi RWO
部署 MySQL 集群
完整示例
# Headless Service
apiVersion: v1
kind: Service
metadata:
name: mysql
labels:
app: mysql
spec:
ports:
- port: 3306
name: mysql
clusterIP: None
selector:
app: mysql
---
# StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
initContainers:
# 初始化容器:生成服务器 ID
- name: init-mysql
image: mysql:8.0
command:
- bash
- "-c"
- |
set -ex
# 从 Pod 序号生成 server-id
[[ $(hostname) =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
echo [mysqld] > /mnt/conf.d/server-id.cnf
echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
volumeMounts:
- name: conf
mountPath: /mnt/conf.d
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
value: "password"
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: data
mountPath: /var/lib/mysql
- name: conf
mountPath: /etc/mysql/conf.d
volumes:
- name: conf
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi
访问 MySQL
# 访问 master(通常是 mysql-0)
mysql -h mysql-0.mysql -uroot -ppassword
# 访问特定实例
mysql -h mysql-1.mysql -uroot -ppassword
mysql -h mysql-2.mysql -uroot -ppassword
更新策略
RollingUpdate(默认)
逐个更新 Pod,从最大序号开始:
spec:
updateStrategy:
type: RollingUpdate
# 更新镜像
kubectl set image statefulset/web nginx=nginx:1.22
# 更新顺序
1. 更新 web-2
2. 更新 web-1
3. 更新 web-0
OnDelete
手动删除 Pod 才会更新:
spec:
updateStrategy:
type: OnDelete
# 删除 Pod 触发更新
kubectl delete pod web-0
kubectl delete pod web-1
kubectl delete pod web-2
分区更新
只更新序号 >= partition 的 Pod:
spec:
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 2 # 只更新 web-2
# 设置 partition
kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":2}}}}'
# 此时只有 web-2 会更新
灰度发布流程:
# 1. 只更新 web-2(金丝雀)
kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":2}}}}'
kubectl set image statefulset/web nginx=nginx:1.22
# 2. 观察 web-2,确认无问题
# 3. 更新 web-1
kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":1}}}}'
# 4. 更新全部
kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":0}}}}'
部署 Redis 集群
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-config
data:
redis.conf: |
cluster-enabled yes
cluster-config-file /data/nodes.conf
cluster-node-timeout 5000
appendonly yes
---
apiVersion: v1
kind: Service
metadata:
name: redis-cluster
spec:
clusterIP: None
ports:
- port: 6379
name: client
- port: 16379
name: gossip
selector:
app: redis-cluster
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cluster
spec:
serviceName: redis-cluster
replicas: 6
selector:
matchLabels:
app: redis-cluster
template:
metadata:
labels:
app: redis-cluster
spec:
containers:
- name: redis
image: redis:7.0-alpine
ports:
- containerPort: 6379
name: client
- containerPort: 16379
name: gossip
command:
- redis-server
- /conf/redis.conf
- --cluster-announce-ip
- $(POD_IP)
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
volumeMounts:
- name: conf
mountPath: /conf
- name: data
mountPath: /data
volumes:
- name: conf
configMap:
name: redis-config
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
初始化 Redis 集群:
# 等待所有 Pod 就绪
kubectl wait --for=condition=ready pod -l app=redis-cluster --timeout=300s
# 创建集群
kubectl exec -it redis-cluster-0 -- redis-cli --cluster create \
redis-cluster-0.redis-cluster:6379 \
redis-cluster-1.redis-cluster:6379 \
redis-cluster-2.redis-cluster:6379 \
redis-cluster-3.redis-cluster:6379 \
redis-cluster-4.redis-cluster:6379 \
redis-cluster-5.redis-cluster:6379 \
--cluster-replicas 1
管理 StatefulSet
扩缩容
# 扩容
kubectl scale statefulset web --replicas=5
# 缩容
kubectl scale statefulset web --replicas=2
注意:缩容不会删除 PVC,需要手动清理。
删除 Pod
# 删除 Pod(会自动重建)
kubectl delete pod web-0
# 级联删除(删除 StatefulSet 和 Pod)
kubectl delete statefulset web
# 仅删除 StatefulSet(保留 Pod)
kubectl delete statefulset web --cascade=orphan
清理资源
# 删除 StatefulSet
kubectl delete statefulset web
# 删除 Service
kubectl delete service nginx
# 删除 PVC
kubectl delete pvc www-web-0 www-web-1 www-web-2
常用命令
# 创建
kubectl apply -f statefulset.yaml
# 查看
kubectl get statefulset
kubectl describe statefulset <name>
kubectl get pods -l app=<label>
# 扩缩容
kubectl scale statefulset <name> --replicas=<count>
# 更新
kubectl set image statefulset/<name> <container>=<image>
kubectl rollout status statefulset/<name>
# 回滚
kubectl rollout undo statefulset/<name>
kubectl rollout history statefulset/<name>
# 删除
kubectl delete statefulset <name>
# 查看 PVC
kubectl get pvc -l app=<label>
最佳实践
1. 使用 Headless Service
StatefulSet 需要配合 Headless Service:
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
clusterIP: None # 必须设置为 None
selector:
app: nginx
2. 配置 Pod Management Policy
spec:
podManagementPolicy: Parallel # 并行创建 Pod
# 或
podManagementPolicy: OrderedReady # 顺序创建(默认)
3. 设置合适的更新策略
spec:
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 0
4. 配置 PVC 保留策略
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain # StatefulSet 删除时保留 PVC
whenScaled: Delete # 缩容时删除 PVC
5. 健康检查
spec:
template:
spec:
containers:
- name: app
livenessProbe:
tcpSocket:
port: 3306
initialDelaySeconds: 30
readinessProbe:
exec:
command:
- /bin/sh
- -c
- mysqladmin ping -h 127.0.0.1
6. 资源限制
spec:
template:
spec:
containers:
- name: mysql
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 1000m
memory: 2Gi
StatefulSet vs Deployment
| 特性 | StatefulSet | Deployment |
|---|---|---|
| Pod 名称 | 固定有序 | 随机 |
| 网络标识 | 稳定 | 不稳定 |
| 存储 | 独立 PVC | 共享或无 |
| 部署顺序 | 有序 | 并行 |
| 扩缩容 | 有序 | 并行 |
| 适用场景 | 有状态应用 | 无状态应用 |
小结
StatefulSet 是管理有状态应用的核心资源:
核心特性:
- 稳定标识:Pod 名称和网络标识固定
- 持久化存储:每个 Pod 独立的 PVC
- 有序操作:按顺序部署、扩展、更新
- 可控更新:支持分区更新和灰度发布
适用场景:
- 数据库(MySQL、PostgreSQL、MongoDB)
- 消息队列(Kafka、RabbitMQ)
- 分布式存储(Ceph、GlusterFS)
- 缓存集群(Redis、Memcached)
关键配置:
- Headless Service 提供网络标识
- volumeClaimTemplates 提供持久化存储
- updateStrategy 控制更新策略
下一章我们将学习 DaemonSet 和 Job,处理特殊工作负载。