StatefulSet 有状态服务
StatefulSet 适合副本不能简单互换的服务,例如数据库、Kafka、ZooKeeper 或需要固定网络身份的集群软件。它提供稳定的 Pod 名称、可配套独立 PVC,并按顺序创建和终止副本。
本篇目标:能判断有状态与无状态应用的根本区别,写出配套的 Headless Service 和 volumeClaimTemplates,理解有序启动/终止与分区更新,并了解 MySQL/Redis 主从的部署思路。

1. 有状态 vs 无状态应用的根本区别
| 维度 | 无状态 | 有状态 |
|---|---|---|
| 副本是否可互换 | ✅ 任意副本可互换 | ❌ 每个副本有身份(如主/从) |
| 数据存放 | 通常外部共享或无状态 | 常为每副本独立持久卷 |
| 网络身份 | 随机名称、无稳定 DNS | 稳定编号、稳定 DNS |
| 扩缩容 | 随意增减 | 需考虑数据与集群成员关系 |
| 典型代表 | API、Web、Worker | 数据库、消息队列、协调服务 |
StatefulSet 不是"更高级的 Deployment"。如果服务不要求稳定身份或独立数据卷,优先选择更简单的 Deployment。
2. StatefulSet 核心特性
2.1 连续编号与稳定身份
StatefulSet 的 Pod 名称固定为 <statefulset-name>-<序号>(mysql-0、mysql-1、mysql-2),删除重建后名称不变:
kubectl get pods -n learning -l app.kubernetes.io/name=mysql -o wide
# 删除 mysql-1,观察重建后名称是否仍是 mysql-1
2.2 Headless Service 提供稳定 DNS
StatefulSet 必须配一个 headless Service(clusterIP: None),为每个 Pod 提供稳定 DNS:
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: learning
spec:
clusterIP: None
selector:
app.kubernetes.io/name: mysql
ports:
- port: 3306
对应 Pod 可以通过 mysql-0.mysql.learning.svc.cluster.local 这类地址互相发现。
2.3 volumeClaimTemplates:每副本独立存储
spec:
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: standard
resources:
requests:
storage: 20Gi
这会为 mysql-0 创建 data-mysql-0 PVC,mysql-1 创建 data-mysql-1 PVC,依此类推。
3. 部署与扩缩容策略
3.1 有序启动与终止
| 策略 | 行为 | 适用场景 |
|---|---|---|
OrderedReady(默认) | mysql-0 启动 → Ready → mysql-1 启动 → Ready → … | 需要 leader 先就绪的集群 |
Parallel | 所有 Pod 同时启动/终止 | 各副本之间无启动顺序依赖 |
删除 StatefulSet 时终止顺序与创建相反:mysql-2 先终止 → mysql-1 → mysql-0。
3.2 分区更新:只更新部分副本
rollingUpdate.partition 让你只更新序号 ≥ N 的 Pod,适合分阶段灰度:
spec:
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 2 # 只更新 mysql-2 及更大序号的 Pod
# 阶段 1:只更新 mysql-2(partition=2)→ 验证 → partition=1 → partition=0
kubectl patch statefulset mysql -n learning \
-p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":1}}}}'
分区更新是 StatefulSet 特有的能力,Deployment 没有等价功能。
3.3 扩缩容注意
- 缩容前先备份数据——Kubernetes 不保证缩掉的 Pod 的数据还能找回;
- 缩容顺序默认从最大序号开始;
- 单副本 StatefulSet 不是高可用方案,它仍是单点。
4. 典型案例:MySQL 主从集群部署思路
下面以 MySQL 为例展示有状态应用在 Kubernetes 上的部署思路,不是可直接上生产的完整配置(真实集群还需要初始化脚本、备份、TLS 与高可用验证)。
4.1 基础清单
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: learning
spec:
clusterIP: None
selector:
app.kubernetes.io/name: mysql
ports:
- port: 3306
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
namespace: learning
spec:
serviceName: mysql
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: mysql
template:
metadata:
labels:
app.kubernetes.io/name: mysql
spec:
initContainers:
- name: init-cluster
image: registry.example.com/mysql-init:1.0
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
# mysql-0 初始化为 leader,mysql-1/2 作为 follower 加入
command:
- sh
- -c
- |
if [ "$POD_NAME" = "mysql-0" ]; then
echo "初始化为主节点"
else
echo "等待 mysql-0 就绪后加入集群"
fi
containers:
- name: mysql
image: mysql:8.4
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: root-password
ports:
- containerPort: 3306
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 20Gi
练习前先创建 Secret(占位密码,仅本地实验使用):
kubectl create secret generic mysql-secret \
--from-literal=root-password='change-me' -n learning
4.2 部署思路要点
- headless Service + 序号:
mysql-0固定是初始化主库的锚点,其他副本通过mysql-0.mysql.learning.svc.cluster.local找到它; - Init 容器判断角色:
POD_NAME(fieldRef 读取 metadata.name)+OrderedReady保证mysql-0先就绪; - 每副本独立 PVC:
volumeClaimTemplates自动为每个副本创建独立数据卷; - 应用侧脚本负责主从复制:Kubernetes 只提供稳定身份与存储,复制关系由数据库自身(或 Operator)建立。
4.3 什么时候用 Operator
MySQL/Redis 有大量运维细节(备份、故障转移、升级、监控)。生产环境优先考虑:
- 云厂商托管数据库(RDS 等),省去自建;
- 成熟的 Operator(如 KubeBlocks、Percona Operator),它们把主从切换、备份封装成自定义资源;
- 自研脚本方案风险高,只适合学习或边缘场景。
5. 生产检查项
- 优先使用云厂商托管数据库;自建前先明确高可用、备份、恢复演练和升级责任;
- 把数据备份恢复流程视为工作负载的一部分,不能只依赖 PVC;
- 升级镜像前阅读应用自身的兼容性矩阵——Kubernetes 能更新 Pod,不会替你完成数据格式迁移;
- 避免把单副本 StatefulSet 误当成高可用方案,它仍是单点;
- 确认
persistentVolumeClaimRetentionPolicy与团队的备份策略一致; - 存储扩容与快照细节见第 12-13 章,备份恢复演练见第 20 章补充。
6. 练习
练习 A
- 创建 Headless Service + StatefulSet(3 副本,用
nginx:alpine代替 MySQL),观察 Pod 命名是否依次为mysql-0、mysql-1、mysql-2 - 用
kubectl run tmp --image=busybox --rm -it --restart=Never -- nslookup mysql-0.mysql.learning测试 DNS - 删除
mysql-1Pod,观察它重新创建后名称是否还是mysql-1(验证稳定身份)
练习 B
- 给 StatefulSet 添加
volumeClaimTemplates,查看是否为每个 Pod 创建了独立 PVC - 将
podManagementPolicy改为Parallel,重建 StatefulSet,观察 Pod 是否同时启动而非依次 - 用
partition: 2触发一次更新,观察只有mysql-2被更新
下一篇:DaemonSet 节点级服务。