在 Kubernetes 的世界里,Deployment 无疑是最常用的工作负载之一。它创建的 Pod 实例彼此完全相同:启动没有顺序要求,通常也挂载着相同的存储卷。客户端访问其中任意一个 Pod,得到的结果都是一致的。我们称这类服务为 无状态服务。
然而,当我们试图部署如 MySQL、Redis 这样的数据库服务时,Deployment 就捉襟见肘了,它通常只能满足单节点部署的简单需求。以 MySQL 一主多从集群为例,主节点和从节点各自拥有独立的数据,显然不能共用同一个存储卷。同时,客户端在访问时,需要将写操作精准路由到主节点,而读操作则可以分发到任意从节点。这就要求客户端能够识别并连接多个具体的、身份明确的 MySQL 实例。
Deployment 无法提供固定的网络标识、稳定的存储绑定以及有序的 Pod 启动机制,难以满足上述复杂需求。因此,这类具备状态、节点间有明确职责区分的服务,被称为 有状态服务。
Kubernetes 提供的 StatefulSet 资源,正是为这类场景量身定制的。它能为每个 Pod 分配唯一的名称和存储卷,并确保 Pod 的创建、扩缩容和终止过程都遵循可预测的顺序,是部署数据库、消息队列、分布式缓存等有状态应用的理想选择。
StatefulSet 的核心特征
StatefulSet 资源具备以下几个关键特征,正是这些特征使其能够优雅地管理有状态应用:
- 有序创建 Pod
- 稳定的、唯一的网络标识符
- 稳定的、持久化的存储
- 有序的、自动滚动更新
- 有序的、优雅的删除和终止
1. 顺序创建 Pod
StatefulSet 默认以严格的顺序创建其 Pod,这对于主从复制、领导者选举等有依赖关系的服务至关重要。例如,MySQL 的从节点需要在主节点启动并完成初始化后才能启动。我们可以通过配置 podManagementPolicy 字段来管理 Pod 的启动策略,它可以是 OrderedReady 或 Parallel。
- OrderedReady:按顺序(从序号0开始)启动和终止 Pod,保证有序性和稳定性,这是默认策略。
- Parallel:并行启动和终止 Pod,适用于 Pod 间无依赖关系的应用场景,可以加快部署速度。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
podManagementPolicy: "Parallel" # 设置为并行策略
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: registry.k8s.io/nginx-slim:0.21
ports:
- containerPort: 80
name: web
对于一个拥有 n 个副本的 StatefulSet,Pod 的名称是固定的,遵循 <statefulset name>-<ordinal index> 的命名规则。在 OrderedReady 策略下,Pod 会严格按照 {0..n-1} 的序号顺序创建。
# watch pod 从创建到 running 的过程
kubectl get pods --watch -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 0/1 Pending 0 0s
web-0 0/1 Pending 0 0s
web-0 0/1 ContainerCreating 0 0s
web-0 1/1 Running 0 19s
web-1 0/1 Pending 0 0s
web-1 0/1 Pending 0 0s
web-1 0/1 ContainerCreating 0 0s
web-1 1/1 Running 0 18s
2. 稳定的网络标识与 Headless Service
部署 MySQL 一主多从集群时,如何让客户端精准连接特定节点?直接使用易变的 Pod IP 显然不行。用普通的 Service?它会通过 LabelSelector 将一组相同标签的 Pod 聚合,无法区分主从。
一个直观但笨拙的方案是为主、从节点分别部署独立的 StatefulSet 和 Service,但这导致架构冗余。更优雅的方式是利用 Kubernetes 的 DNS 系统,为每个 Pod 提供独立的、稳定的域名。这正是 StatefulSet 配合 Headless Service(无头服务)的设计。
Headless Service 是一种特殊的 Service,通过设置 clusterIP: None 来声明。它不会分配 ClusterIP 也不做负载均衡,其核心作用是通过 DNS 为每个 Pod 提供一条固定的 SRV 记录。每个 Pod 都将获得一个唯一且稳定的 DNS 域名,格式为:
<pod-name>.<svc-name>.<namespace>.svc.cluster.local
例如,名为 mysql-0 的 Pod,通过名为 mysql 的 Headless Service 暴露,在 default 命名空间下,其域名为 mysql-0.mysql.default.svc.cluster.local。即使 Pod 被重新调度,只要其名称不变,这个域名就始终指向它,DNS 记录会自动更新到新的 IP。这完美满足了有状态服务对节点身份精确识别的需求,也是在 Kubernetes 中部署和管理复杂 有状态服务 的基石。
下面看一个具体的配置示例,了解 StatefulSet 如何与 Headless Service 协同工作:
apiVersion: v1
kind: Service
metadata:
name: mysql
labels:
app: mysql
spec:
# 声明为 Headless Service
clusterIP: None
selector:
app: mysql
ports:
- port: 3306
name: mysql
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
# 此字段必须与上方 Service 的 name 一致
serviceName: mysql
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
name: mysql
......
注意:StatefulSet 中的 serviceName 字段必须与 Headless Service 的 name 字段一致,StatefulSet 控制器正是通过此字段来查找并关联对应的 Service。
部署后,三个 MySQL Pod 实例将拥有以下稳定的 DNS 域名:
mysql-0.mysql.default.svc.cluster.local
mysql-1.mysql.default.svc.cluster.local
mysql-2.mysql.default.svc.cluster.local
在集群内的客户端 Pod 中,使用 nslookup 或 dig 命令解析这些域名,会直接得到对应 Pod 的 IP 地址。
$ / # nslookup mysql-0.mysql
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: mysql-0.mysql
Address 1: 10.244.4.83 mysql-0.mysql.default.svc.cluster.local
$/ # nslookup mysql-1.mysql
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: mysql-1.mysql
Address 1: 10.244.1.175 mysql-1.mysql.default.svc.cluster.local
$/ # nslookup mysql-2.mysql
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: mysql-2.mysql
Address 1: 10.244.2.75 mysql-2.mysql.default.svc.cluster.local

3. 稳定的持久化存储
对于 MySQL 这类有状态服务,每个实例的数据存储必须是独立且持久的,即使 Pod 重启,也必须能重新挂载到原来的数据卷上。StatefulSet 通过 volumeClaimTemplates(卷声明模板)字段来实现这一功能。
volumeClaimTemplates 定义了创建 PVC(PersistentVolumeClaim)的模板。StatefulSet 控制器会为每个 Pod 实例(如 mysql-0, mysql-1)自动创建一份独立的 PVC,其名称遵循 <volumeClaimTemplatesName>-<podName> 的规则。
apiVersion: v1
kind: Service
metadata:
name: mysql
labels:
app: mysql
spec:
clusterIP: None
selector:
app: mysql
ports:
- port: 3306
name: mysql
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: data
mountPath: /var/lib/mysql
subPath: mysql
# PVC 模板
volumeClaimTemplates:
- metadata:
name: mysql-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
应用上述配置后,会创建三个独立的 PVC:
$ kubectl get pvc -l app=mysql
NAME STATUS VOLUME CAPACITY ACCESSMODES AGE
mysql-data-mysql-0 Bound pvc-15c268c7-b507-11e6-932f-42010a800002 1Gi RWO 48s
mysql-data-mysql-1 Bound pvc-15c79307-b507-11e6-932f-42010a800002 1Gi RWO 48s
由于 PVC 名称与 Pod 名称严格绑定,即使 mysql-0 这个 Pod 被删除重建,新的 mysql-0 Pod 依然会绑定到名为 mysql-data-mysql-0 的原有 PVC 上,从而实现了数据的持久化。这种机制是保障 数据库 等有状态应用数据安全的关键。

4. 更新策略
有状态服务的多个实例间通常存在数据同步或复制关系,更新时需要格外小心,以维持集群的一致性与可用性。StatefulSet 通过 spec.updateStrategy 字段定义更新策略,支持两种模式:
OnDelete
此策略下,当您修改了 StatefulSet 的 Pod 模板(如更新镜像版本)后,控制器不会自动更新任何 Pod。只有在用户手动删除某个旧 Pod 时,它才会根据新模板被重新创建。这提供了最大的控制权,适合对更新流程有严格管控的场景。
RollingUpdate(默认策略)
这是 StatefulSet 的默认更新策略。它会以 逆序(从序号最大的 Pod 开始到序号最小的 Pod)的方式,逐个对 Pod 进行滚动更新。
更新过程是:终止当前 Pod -> 等待新 Pod 进入 Running 且 Ready 状态 -> 再继续更新下一个 Pod。这种顺序性最大程度保障了集群的稳定性,尤其适合数据库这类应用。
此外,你还可以通过配置 .spec.updateStrategy.rollingUpdate.partition 来实现分区更新。设置分区值后,只有序号大于或等于该分区值的 Pod 才会被更新,序号小于分区值的 Pod 则保持原状。这常用于金丝雀发布或分阶段更新。
5. 删除与 PVC 保留策略
默认情况下,删除 StatefulSet 或其 Pod 时,关联的 PVC 和 PV 会被保留。这确保了数据安全,下次创建同名 StatefulSet 时,Pod 能重新挂载旧数据。
但从 Kubernetes v1.27 开始,你可以通过 spec.persistentVolumeClaimRetentionPolicy 字段更精细地控制 PVC 的生命周期。该功能在 v1.32 中趋于稳定,需确保 API Server 开启了 StatefulSetAutoDeletePVC 特性门控。
该策略包含两个子策略,可分别配置:
- whenDeleted:当整个 StatefulSet 被删除时触发。
- whenScaled:当 StatefulSet 缩容(减少副本数)时触发。
每个子策略都可设置为以下两种行为之一:
- Retain(默认):保留 PVC。
- Delete:删除 PVC。
以下是几种常见的配置组合示例:
# 示例1: 默认行为,任何情况下都保留PVC
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Retain
---
# 示例2: 删除StatefulSet时清理PVC,缩容时保留
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted: Delete
whenScaled: Retain
---
# 示例3: 任何删除Pod的操作都清理对应的PVC
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted: Delete
whenScaled: Delete
---
# 示例4: 缩容时清理多余PVC,但删除整个StatefulSet时保留(可用于数据备份)
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Delete
注意:此策略仅适用于由 StatefulSet 控制器发起的删除操作(如删除整个资源对象或缩容)。如果 Pod 因节点故障、被驱逐等原因被重建,其 PVC 仍会被保留并重新挂载。
背后的机制:
- 当设置
whenDeleted: Delete,控制器会将 StatefulSet 设置为 PVC 的属主(ownerReferences)。删除 StatefulSet 时,Kubernetes 的垃圾回收器会级联删除这些 PVC。
- 当设置
whenScaled: Delete,在缩容前,控制器会将即将被删除的 Pod 设置为对应 PVC 的属主。待 Pod 被删除后,PVC 也随之被垃圾回收。这套机制为 运维 人员提供了灵活且安全的存储资源管理能力。
总结
StatefulSet 相较于 Deployment,在设计上更为复杂,也更具场景针对性。它通过提供稳定的网络标识、唯一的持久化存储以及有序的 Pod 生命周期管理,完美解决了有状态应用在 Kubernetes 中部署的难题。在部署 MySQL、Redis、MongoDB、Kafka 等关键中间件时,StatefulSet 通常是首选方案。
然而,对于生产环境,仅仅完成部署还远远不够。我们往往还需要故障自动转移、备份恢复、监控告警等高级能力。这时,Operator 模式便成为更优解。像 etcd-operator、prometheus-operator 这样的项目,通过扩展 Kubernetes API 和控制器,实现了对特定有状态应用的全生命周期自动化管理。对于复杂的业务场景,开发自定义 Operator 将是构建健壮、自动化 云原生 服务体系的终极武器。
希望这篇关于 StatefulSet 原理的解析,能帮助你更深入地理解如何在 Kubernetes 中驾驭有状态服务。如果你想了解更多云原生实践或与其他开发者交流,欢迎访问 云栈社区 探讨。