在 Kubernetes 的世界里,Deployment 解决了无状态应用的弹性伸缩问题,但当你需要在集群上运行 MySQL 主从集群、Redis Sentinel、ZooKeeper Ensemble 或 Kafka Broker 这类有状态应用时,Deployment 的随机 Pod 名称和共享存储模型就捉襟见肘了。StatefulSet 正是为此而生——它为每个 Pod 提供稳定的网络身份、持久化标识和有序的编排能力。本文将从底层机制到生产实战,全面拆解 StatefulSet 的设计哲学与使用方法。
一、StatefulSet 与 Deployment 的本质区别
很多初学者会把 StatefulSet 理解为”带持久卷的 Deployment”,这个认知只触及了表面。两者的核心差异体现在三个维度:Pod 身份稳定性、部署顺序确定性、以及存储绑定的独占性。
| 特性 | Deployment | StatefulSet |
|---|---|---|
| Pod 名称 | 随机后缀(如 app-7b8f9c-x2k4) | 有序编号(如 app-0, app-1, app-2) |
| 网络标识 | 每次重建后变化 | 永久稳定,可通过 DNS 解析 |
| 启动顺序 | 并行,无保证 | 严格顺序(0→1→2…) |
| 存储绑定 | 多 Pod 共享一个 PVC | 每个 Pod 独享一个 PVC |
| 删除策略 | 随机终止 | 逆序终止(2→1→0) |
| 滚动更新 | 按 maxSurge/maxUnavailable | 逆序逐个更新,支持分区 |
这种”确定性”是有状态应用所必需的。以 MySQL 主从复制为例,主节点(app-0)必须最先启动并初始化数据,从节点(app-1、app-2)才能正确连接并同步。如果 Pod 顺序不确定,从节点可能在主节点之前启动,导致复制链路断裂。
二、StatefulSet 的核心架构与工作机制
2.1 稳定的网络身份
StatefulSet 通过 Headless Service 为每个 Pod 分配一个可预测的 DNS 名称。格式为:
1
2
3
4
5 <pod-name>.<service-name>.<namespace>.svc.cluster.local
# 例如:
mysql-0.mysql-headless.default.svc.cluster.local
mysql-1.mysql-headless.default.svc.cluster.local
这意味着应用代码可以通过固定域名连接到特定 Pod,无需依赖环境变量或服务发现。MySQL 从节点可以这样配置主节点地址:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 [mysqld]
# 从节点 my.cnf 配置
server-id = 2
relay-log = mysql-relay-bin
# 直接通过 StatefulSet Pod DNS 连接主节点
report-host = mysql-1.mysql-headless.default.svc.cluster.local
# 主节点的 CHANGE MASTER TO 语句
CHANGE MASTER TO
MASTER_HOST='mysql-0.mysql-headless.default.svc.cluster.local',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='repl_password',
MASTER_AUTO_POSITION=1;
2.2 有序的部署与扩缩容
StatefulSet 默认采用
1 | OrderedReady |
策略,严格保证:
- 部署/扩容:Pod-0 必须达到 Running & Ready 状态后,Pod-1 才开始创建
- 缩容/删除:必须从最高编号开始逆序删除,Pod-N 删除完成后才轮到 Pod-(N-1)
- 滚动更新:从最高编号开始,逐个更新,每个 Pod 更新完成并就绪后才更新下一个
如果对顺序没有严格要求,可以设置
1 | podManagementPolicy: Parallel |
,让 Pod 并行创建和删除,大幅缩短启动时间:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46 apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cluster
spec:
serviceName: redis-headless
replicas: 6
podManagementPolicy: Parallel # 并行管理
podManagementPolicy: OrderedReady # 默认:顺序管理
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 0 # 分区更新,可用于灰度
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: redis
topologyKey: kubernetes.io/hostname
containers:
- name: redis
image: redis:7.2-alpine
ports:
- containerPort: 6379
name: redis
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 10Gi
2.3 独占的持久化存储
这是 StatefulSet 最关键的设计。
1 | volumeClaimTemplates |
为每个 Pod 自动创建独立的 PVC,且 PVC 名称遵循固定格式:
1 | <pvc-template-name>-<statefulset-name>-<pod-index> |
。
当 Pod-1 被删除后重新调度到其他节点,Kubernetes 会自动重新挂载同一个 PVC,确保数据连续性。这种设计对数据库类应用至关重要——MySQL 从节点的 binlog 和数据文件不会因为 Pod 重建而丢失。
1 | persistentVolumeReclaimPolicy: Retain |
意味着 PV 数据会保留,直到管理员手动清理。这是安全设计——防止误删 StatefulSet 导致数据丢失。
三、生产实战:部署 MySQL 一主两从集群
下面通过一个完整的 MySQL 主从集群部署,展示 StatefulSet 在真实场景中的用法。
3.1 创建 Headless Service 和 ConfigMap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 # Headless Service —— 为每个 Pod 提供 DNS 解析
apiVersion: v1
kind: Service
metadata:
name: mysql-headless
labels:
app: mysql
spec:
ports:
- port: 3306
name: mysql
clusterIP: None # 关键:None 使其成为 Headless Service
selector:
app: mysql
---
# ConfigMap —— 包含 MySQL 主从初始化脚本
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql-config
data:
master.cnf: |
[mysqld]
log-bin=mysql-bin
server-id=1
binlog-format=ROW
gtid-mode=ON
enforce-gtid-consistency=ON
slave.cnf: |
[mysqld]
server-id=2
relay-log=mysql-relay-bin
gtid-mode=ON
enforce-gtid-consistency=ON
log-slave-updates=ON
3.2 StatefulSet 主资源定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89 apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql-headless
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
initContainers:
# 初始化容器:根据 Pod 序号判断是主还是从
- name: init-mysql
image: mysql:8.0
command:
- bash
- "-c"
- |
set -ex
# 从 Pod 名称提取序号
ordinal=$(echo $HOSTNAME | awk -F '-' '{print $NF}')
# 复制对应配置文件
if [[ $ordinal -eq 0 ]]; then
cp /mnt/config/master.cnf /etc/mysql/conf.d/server-id.cnf
else
cp /mnt/config/slave.cnf /etc/mysql/conf.d/server-id.cnf
# 设置唯一的 server-id
echo "server-id=$((100 + $ordinal))" >> /etc/mysql/conf.d/server-id.cnf
fi
volumeMounts:
- name: conf
mountPath: /mnt/config
- name: config-files
mountPath: /etc/mysql/conf.d
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: root-password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: data
mountPath: /var/lib/mysql
- name: config-files
mountPath: /etc/mysql/conf.d
readinessProbe:
exec:
command: ["mysqladmin", "ping", "-h", "127.0.0.1"]
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
exec:
command: ["mysqladmin", "ping", "-h", "127.0.0.1"]
initialDelaySeconds: 30
periodSeconds: 10
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 2000m
memory: 4Gi
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 50Gi
- metadata:
name: conf
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
3.3 从节点自动连接主节点的启动脚本
上述配置中,初始化容器根据 Pod 序号判断角色(0 为主,其余为从)。从节点 MySQL 启动后,需要执行 GTID 复制配置。可以在主容器的启动命令中加入自动复制逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 command:
- bash
- "-c"
- |
set -ex
ordinal=$(echo $HOSTNAME | awk -F '-' '{print $NF}')
# 启动 MySQL
docker-entrypoint.sh mysqld &
# 等待 MySQL 就绪
until mysqladmin ping -h 127.0.0.1 --silent; do sleep 2; done
# 从节点配置复制
if [[ $ordinal -ne 0 ]]; then
mysql -uroot -p"$MYSQL_ROOT_PASSWORD" <<EOF
STOP SLAVE;
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='mysql-0.mysql-headless.default.svc.cluster.local',
SOURCE_PORT=3306,
SOURCE_USER='repl',
SOURCE_PASSWORD='repl_pass',
SOURCE_AUTO_POSITION=1;
START SLAVE;
EOF
fi
wait
四、滚动更新与分区灰度策略
StatefulSet 的滚动更新策略比 Deployment 更加精细。通过
1 | partition |
参数,可以控制只更新编号大于等于该值的 Pod,实现灰度发布。
1
2
3
4
5
6
7
8
9
10 # 只更新 Pod-2,保持 Pod-0 和 Pod-1 不变
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 2 # 只有序号 >= 2 的 Pod 会更新
灰度发布的工作流程如下:
- 设置
1partition: N
,只有编号 ≥ N 的 Pod 被更新
- 观察 N 号 Pod 的运行状态和业务表现
- 逐步降低 partition 值(N→N-1→N-2…),逐个 Pod 滚动更新
- 最终
1partition: 0
,所有 Pod 完成更新
对于数据库这类敏感应用,建议配合
1 | OnDelete |
策略手动控制更新节奏:
1
2
3 spec:
updateStrategy:
type: OnDelete # 只在 Pod 被手动删除后才更新
五、常见生产故障与排查指南
5.1 Pod 卡在 Pending 状态
最常见的原因是 PVC 无法绑定。检查步骤:
1
2
3
4
5
6
7
8
9
10
11 # 查看 PVC 状态
kubectl get pvc -l app=mysql
# 查看 PVC 事件
kubectl describe pvc data-mysql-0
# 检查 StorageClass 是否存在
kubectl get storageclass
# 检查是否有可用的 PV
kubectl get pv | grep fast-ssd
常见原因及解决方案:
| 原因 | 症状 | 解决方案 |
|---|---|---|
| StorageClass 不存在 | PVC 一直 Pending | 创建对应的 StorageClass 或修改配置 |
| 存储后端配额用尽 | PV 创建失败 | 扩容云盘配额或清理无用 PV |
| 节点亲和性冲突 | Pod 调度失败 | 检查 nodeSelector 和 affinity 规则 |
| 资源不足 | Pod Pending | 扩容节点或调整 resource requests |
5.2 Pod 卡在 CrashLoopBackOff
通常因为数据损坏或配置错误。StatefulSet 的 PVC 是持久的,如果上次运行导致数据损坏,重新调度后依然会崩溃。
1
2
3
4
5
6
7
8
9
10
11 # 查看容器日志
kubectl logs mysql-1 --tail=100
# 进入容器排查
kubectl exec -it mysql-1 -- bash
# 查看数据目录状态
kubectl exec mysql-1 -- ls -la /var/lib/mysql
# 如果数据损坏,可临时挂载 PVC 到修复容器
kubectl run mysql-repair --rm -it --image=mysql:8.0 --overrides='{"spec":{"volumes":[{"name":"data","persistentVolumeClaim":{"claimName":"data-mysql-1"}}],"containers":[{"name":"mysql-repair","volumeMounts":[{"name":"data","mountPath":"/var/lib/mysql"}]}]}}' -- bash
5.3 扩缩容卡住
当 Pod-1 一直未就绪时,扩容到 Pod-2 会被阻塞(OrderedReady 策略下)。排查路径:
1
2
3
4
5
6
7
8 # 检查 Pod 就绪状态
kubectl get pods -l app=mysql -o wide
# 查看 StatefulSet 状态
kubectl get statefulset mysql -o yaml | grep -A5 "currentReplicas\|readyReplicas"
# 检查 readinessProbe 是否过于严格
kubectl describe pod mysql-1 | grep -A10 "Readiness"
解决方案:放宽 readinessProbe 的失败阈值,或切换为
1 | podManagementPolicy: Parallel |
,或使用
1 | OnDelete |
手动控制节奏。
六、StatefulSet 使用最佳实践清单
- 始终配合 Headless Service:没有 Headless Service,Pod 的稳定 DNS 就无法生效,这是 StatefulSet 的基石
- 设置 Pod 反亲和性:使用
1podAntiAffinity
将同一 StatefulSet 的 Pod 分散到不同节点,避免单节点故障导致多个 Pod 同时不可用
- 合理配置资源限制:有状态应用通常对内存敏感,设置合理的 requests/limits 防止 OOM 导致数据损坏
- 使用合适的 StorageClass:数据库类应用选择低延迟存储(SSD/NVMe),避免使用网络存储导致 IO 瓶颈
- 配置优雅终止:设置合理的
1terminationGracePeriodSeconds
(默认 30s,数据库建议 60-120s),确保数据刷盘完成
- 监控 PVC 使用率:StatefulSet 的 PVC 不会自动扩容,需要配合 Prometheus 监控磁盘使用率并提前预警
- 灰度更新用 partition:避免一次性更新所有 Pod,先更新一个副本观察后再推进
- 备份策略不可省略:StatefulSet 的持久化不等于备份,定期使用 Velero 或数据库原生工具做异地备份
- 初始化逻辑放在 initContainers:利用 Pod 序号判断角色,在 initContainers 中完成差异化配置
- 使用 PodDisruptionBudget:配合 PDB 确保在节点维护时不会同时驱逐过多副本
七、总结
StatefulSet 是 Kubernetes 中管理有状态应用的核心控制器。它的设计哲学可以概括为一句话:确定性优于灵活性——通过稳定的网络身份、有序的编排策略和独占的存储绑定,为数据库、消息队列、分布式协调服务等有状态应用提供可靠的运行环境。
在实际生产中,理解 StatefulSet 的有序语义、PVC 生命周期管理以及灰度更新策略,是构建高可用有状态服务的基础。同时也要注意,StatefulSet 并非万能药——对于复杂的有状态应用(如需要跨集群复制的 MongoDB 分片集群),建议优先考虑成熟的 Operator 方案(如 MongoDB Community Operator),它们在 StatefulSet 之上封装了更高级的集群管理逻辑,能大幅降低运维复杂度。
汤不热吧