上一章我们学会了 Deployment。但 Deployment 是为无状态应用设计的:Pod 之间可以互相替换,名字随机,存储也随删随弃。这一章讲 StatefulSet,它专门管理有状态应用。
4.1 为什么 StatefulSet 存在
Deployment 管理的是无状态应用。无状态的意思是:任意一个 Pod 被删掉重建,业务都不会受影响。比如 Nginx、Tomcat、Spring Boot 这类 Web 服务,请求处理完就结束了,不依赖本地数据。
但有状态应用不一样:
- MySQL 主从:每个实例身份固定,谁是主、谁是从必须稳定
- ZooKeeper:节点有固定编号,启动顺序和身份绑定
- Kafka:每个 Broker 有唯一 ID,和分区副本一一对应
- etcd:成员之间需要固定名称和持久化数据
如果用 Deployment 跑这些应用,会遇到几个致命问题:
- Pod 名字随机:MySQL 主库叫
mysql-7c4d9f8b5c-2jxmt,重启后变成mysql-7c4d9f8b5c-abc12,域名跟着变,从库找不到主库 - 启动顺序乱:多个副本同时启动,ZooKeeper 还没形成集群就互相报错
- 存储不固定:Pod 删掉后,PVC 可能被删掉,新 Pod 拿不到原来的数据
StatefulSet 就是为解决这些问题而生的。
4.2 StatefulSet 的三条核心保证
StatefulSet 给每个 Pod 提供了三个稳定的东西:稳定网络身份、稳定存储、有序启动和伸缩。
4.2.1 稳定网络身份
StatefulSet 创建的 Pod 名字是有规律的,格式为:
<StatefulSet 名称>-<序号>
比如 StatefulSet 名为 mysql,副本数为 3,那么三个 Pod 的名字分别是:
mysql-0
mysql-1
mysql-2
这个序号叫序数索引(ordinal index)。它不会因为 Pod 重启而改变。mysql-0 被删除重建后,仍然叫 mysql-0。
每个 Pod 还会有一个稳定的 DNS 域名。配合 Headless Service 使用时,格式为:
<pod 名>.<Service 名>.<namespace>.svc.cluster.local
在我们的集群中,如果在 default 命名空间创建了 Headless Service mysql-svc,那么 mysql-0 的完整域名是:
mysql-0.mysql-svc.default.svc.cluster.local
其他节点可以直接通过这个域名访问它,不用担心 IP 变化。
4.2.2 稳定存储
StatefulSet 通过 volumeClaimTemplates 为每个 Pod 单独申请一块 PVC。PVC 的名字也有规律:
<卷模板名>-<StatefulSet 名>-<序号>
比如 volumeClaimTemplates 里定义了一个卷模板叫 data,那么三个 Pod 对应的 PVC 分别是:
data-mysql-0
data-mysql-1
data-mysql-2
每个 PVC 都绑定到一块独立的 PV。Pod 被删除重建后,StatefulSet 控制器会把原来的 PVC 重新挂回给对应序号的 Pod。这意味着 MySQL 的数据不会丢,Kafka 的日志不会乱。
4.2.3 有序部署和伸缩
StatefulSet 创建 Pod 时是有顺序的:
先创建 mysql-0,等它 Ready 了,再创建 mysql-1
等 mysql-1 Ready 了,再创建 mysql-2
缩容的时候也是反向的:
先删除 mysql-2,等它完全删除后,再删除 mysql-1
这个顺序很重要。ZooKeeper 需要先启动第一个节点,它才知道自己是集群的第一个成员。MySQL 主从要先起主库,再起从库。如果同时起多个,集群初始化会失败。
StatefulSet 默认启用 podManagementPolicy: OrderedReady,即按顺序管理。如果改成 Parallel,则像 Deployment 一样同时创建,适用于无状态但又有序号的特殊场景,但不常见。
4.3 典型使用场景
StatefulSet 最适合跑下面几类服务:
| 应用 | 为什么需要 StatefulSet |
|---|---|
| MySQL / PostgreSQL | 主从身份固定,数据持久化 |
| ZooKeeper | 节点编号固定,启动顺序敏感 |
| Kafka | Broker ID 固定,分区副本落点依赖 ID |
| etcd | 成员名称固定,WAL 数据持久化 |
| MongoDB / Redis Cluster | 分片或主从关系需要稳定身份 |
有些数据库虽然有 Operator 可以更方便地部署,但底层通常也是基于 StatefulSet。理解 StatefulSet 是理解这些 Operator 的前提。
4.4 实战:部署一个单节点 MySQL StatefulSet
下面是一个最简版 StatefulSet 例子,基于我们 3 节点集群的实际 IP 环境(192.168.0.201/202/203)编写。
# statefulset-mysql.yaml
apiVersion: v1
kind: Service
metadata:
name: mysql-svc
labels:
app: mysql
spec:
ports:
- port: 3306
name: mysql
clusterIP: None # 关键:Headless Service,不分配虚拟 IP
selector:
app: mysql
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql-svc # 必须关联 Headless Service
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
value: "root1234"
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: data
mountPath: /var/lib/mysql
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: local-path
resources:
requests:
storage: 5Gi
关键字段解读:
| 字段 | 含义 |
|---|---|
clusterIP: None | 把 Service 变成 Headless,DNS 直接解析到 Pod IP |
serviceName | StatefulSet 必须关联一个 Headless Service |
volumeClaimTemplates | 为每个 Pod 按模板创建独立 PVC |
accessModes: ReadWriteOnce | 单个节点读写,适合单节点数据库 |
创建并验证:
$ kubectl apply -f statefulset-mysql.yaml
service/mysql-svc created
statefulset.apps/mysql created
$ kubectl get statefulset,pod,pvc
NAME READY AGE
statefulset.apps/mysql 1/1 2m
NAME READY STATUS AGE
pod/mysql-0 1/1 Running 2m
NAME STATUS VOLUME
persistentvolumeclaim/data-mysql-0 Bound pvc-6f3e2d8b...
进入 Pod 写入数据,然后删除 Pod 验证数据持久化:
$ kubectl exec -it mysql-0 -- mysql -uroot -proot1234 -e "CREATE DATABASE testdb;"
$ kubectl delete pod mysql-0
pod "mysql-0" deleted
$ kubectl get pod -w
NAME READY STATUS AGE
mysql-0 1/1 Running 30s
$ kubectl exec -it mysql-0 -- mysql -uroot -proot1234 -e "SHOW DATABASES;"
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
| testdb |
+--------------------+
testdb 还在。这就是 StatefulSet 加 PVC 的威力。
4.5 多节点 ZooKeeper 示例
ZooKeeper 是一个典型依赖 StatefulSet 的应用。三个节点需要固定编号、固定启动顺序和固定访问地址。
# zk.yaml
apiVersion: v1
kind: Service
metadata:
name: zk-svc
labels:
app: zk
spec:
ports:
- port: 2181
name: client
- port: 2888
name: server
- port: 3888
name: leader-election
clusterIP: None
selector:
app: zk
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zk
spec:
serviceName: zk-svc
replicas: 3
selector:
matchLabels:
app: zk
template:
metadata:
labels:
app: zk
spec:
containers:
- name: zk
image: zookeeper:3.8
ports:
- containerPort: 2181
name: client
- containerPort: 2888
name: server
- containerPort: 3888
name: leader-election
env:
- name: ZOO_MY_ID
valueFrom:
fieldRef:
fieldPath: metadata.name # 利用 Pod 名 my-id-0 设置 ZooKeeper ID
- name: ZOO_SERVERS
value: "server.0=zk-0.zk-svc:2888:3888;2181 server.1=zk-1.zk-svc:2888:3888;2181 server.2=zk-2.zk-svc:2888:3888;2181"
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: local-path
resources:
requests:
storage: 5Gi
注意:这个例子用了
fieldRef把 Pod 名字注入环境变量。真实生产环境推荐用 ZooKeeper Operator 或zk-my-id等辅助方案生成正确的 ID,因为 ZooKeeper 的myid必须是数字。
部署后会按顺序创建 zk-0、zk-1、zk-2,每个 Pod 都有稳定的域名 zk-0.zk-svc、zk-1.zk-svc、zk-2.zk-svc。
4.6 有状态应用扩容与缩容
扩容时:
$ kubectl scale statefulset mysql --replicas=3
执行后,K8s 会先创建 mysql-1,等它 Ready 后,再创建 mysql-2。每个新 Pod 都会对应创建一个新的 PVC,名字分别是 data-mysql-1、data-mysql-2。
缩容时:
$ kubectl scale statefulset mysql --replicas=1
K8s 会先删除 mysql-2,再删除 mysql-1。注意,PVC 不会被自动删除。这是为了防止误删导致数据丢失。如果你确认要清理,需要手动删除 PVC 和 PV:
$ kubectl delete pvc data-mysql-2 data-mysql-1
很多初学者缩容后发现磁盘还占着空间,就是这个原因。StatefulSet 对数据保护非常保守。
4.7 常见问题与排错
| 现象 | 原因 | 排查 |
|---|---|---|
Pod 卡在 Pending | PVC 没绑定 | kubectl get pvc 看是否 Bound |
| 域名解析失败 | 没配 Headless Service 或 serviceName 写错 | kubectl get svc 检查 clusterIP |
| 多节点启动互相连不上 | 启动顺序或地址配置错 | 看日志里 ordinal 和 DNS 是否对应 |
| 扩容只到一半 | 前面 Pod 没 Ready | kubectl describe pod 看 Readiness |
| 数据没保留 | PVC 被误删,或 Pod 序号变了 | StatefulSet 删了重建序号才保留 |
4.8 本章小结
- StatefulSet 解决 Deployment 无法管理的有状态应用问题
- 三个核心保证:稳定网络身份、稳定存储、有序启动和伸缩
- 必须配合 Headless Service 使用
serviceName volumeClaimTemplates为每个 Pod 创建独立 PVC,且持久化- 扩容/缩容按顺序进行,PVC 不会自动删除,保护数据安全
4.9 课后练习
- 用本章 YAML 创建 MySQL StatefulSet,写入数据后删除 Pod,验证数据是否保留。
- 把 MySQL 从 1 副本扩到 3 副本,观察 Pod 创建顺序和 PVC 名称。
- 用
nslookup或dig在集群内测试mysql-0.mysql-svc.default.svc.cluster.local能否解析。 - 尝试部署 ZooKeeper 示例,理解固定编号和启动顺序在集群类应用中的作用。