在 Kubernetes 中,Deployment、ReplicaSet 等控制器主要用于管理无状态服务(如 Web 应用),这类服务的 Pod 实例可随意替换,IP、名称不影响服务可用性。但对于 MySQL 主从、Redis 集群、ZooKeeper 等有状态服务,Pod 实例需保持固定标识、独立存储和有序启停,StatefulSet 正是为解决这类需求而设计的核心控制器。

一、StatefulSet 核心概念与原理

1.1 有状态服务 vs 无状态服务

StatefulSet 的本质是为 “有状态服务” 提供稳定的运行环境,需先明确两类服务的核心差异:

维度无状态服务(如 Web、Nginx)有状态服务(如 MySQL 主从、Redis 集群)
Pod 标识名称、IP 随机,可随意替换名称、IP 需固定(如 mysql-0mysql-1
存储需求所有 Pod 共享同一存储卷(如 NFS 共享目录)每个 Pod 需独立存储卷(数据不互通)
启停顺序无要求,可并行创建 / 删除需有序(如先启动主库 mysql-0,再启动从库 mysql-1
服务发现通过 Service ClusterIP 访问,无需区分实例需通过固定标识访问特定实例(如主库只能是 mysql-0

1.2 StatefulSet 的核心组成

StatefulSet并非独立工作,需依赖两个关键组件实现“状态稳定”,三者协同构成有状态服务的管理体系:

(1)Headless Service(无头服务)

Headless Service是StatefulSet的“网络标识核心”,与普通Service的最大区别区别是不分配ClusterIP。其核心作用是:

  • 为 StatefulSet 管理的每个 Pod 生成固定且可解析的 DNS 记录,确保 Pod 重建后仍能通过原标识被访问;
  • 提供 Pod 实例的 “服务发现” 能力:通过解析 Service DNS,可直接获取所有 Pod 的 IP 列表(普通 Service 仅返回自身 ClusterIP)。

DNS 记录格式(K8s 集群内全局唯一):

  • Pod 级 DNS:$(Pod 名称).$(Headless Service 名称).$(命名空间).svc.cluster.local例:StatefulSet 名称为 mysql,副本数 2,Headless Service 名为 mysql-svc,则 Pod DNS 为 mysql-0.mysql-svc.default.svc.cluster.localmysql-1.mysql-svc.default.svc.cluster.local
  • Service 级 DNS:$(Headless Service 名称).$(命名空间).svc.cluster.local解析此 DNS 会返回所有关联 Pod 的 IP 列表,而非 ClusterIP。

(2)VolumeClaimTemplates(存储卷申请模板)

有状态服务的每个 Pod 需独立存储(如 MySQL 主从不能共用数据目录),VolumeClaimTemplates 是 StatefulSet 的 “存储保障核心”,其作用是:

  • 自动为每个 Pod 生成对应的 PersistentVolumeClaim(PVC),无需手动创建;
  • 每个 PVC 会自动绑定符合条件的 PersistentVolume(PV),最终实现 “一个 Pod 对应一个独立存储卷”,数据永久独立。

绑定关系:StatefulSet 副本数为 N 时,会生成 N 个 PVC,命名格式为 $(模板名)-$(StatefulSet 名)-$(Pod 序号)(如 data-mysql-0data-mysql-1),每个 PVC 绑定独立 PV。

(3)StatefulSet 控制器本身

StatefulSet 控制器负责协调 Pod 的全生命周期管理,核心能力包括:

  • 有序创建 / 删除:创建时按 0→1→2 顺序启动 Pod,删除时按 2→1→0 顺序停止(确保依赖关系,如从库需等主库就绪);
  • 固定 Pod 名称:Pod 名称格式固定为 $(StatefulSet 名)-$(Pod 序号)(如 web-0web-1),重建后名称不变(区别于 Deployment 的随机名称);
  • 状态恢复:Pod 因故障重建时,会自动挂载原 PVC(数据不丢失),并复用原 DNS 记录(网络标识不变)。

二、StatefulSet 资源清单编写技巧

StatefulSet 的资源清单需包含 “Headless Service 定义” 和 “StatefulSet 定义” 两部分,核心字段需严格匹配(如 Service 选择器与 Pod 标签一致)。

2.1 核心字段解析

通过 kubectl explain statefulset.spec 可查看关键字段,重点关注以下必填 / 核心配置:

字段作用说明
spec.serviceName(必填)关联的 Headless Service 名称(必须与 Headless Service 的 metadata.name 一致)
spec.selector(必填)标签选择器,需匹配 spec.template.metadata.labels(否则创建失败)
spec.template(必填)Pod 模板,与 Deployment 的 Pod 模板格式一致(定义容器、端口、资源等)
spec.replicas(可选)副本数,默认 1(需有序时建议不超过 10)
spec.volumeClaimTemplates(可选)存储卷申请模板,为每个 Pod 生成独立 PVC(有状态服务必配)
spec.updateStrategy(可选)更新策略,默认 RollingUpdate(有序更新,先更新高序号 Pod),也可设为 OnDelete(手动删除后重建)

2.2 资源清单结构示例

StatefulSet 清单通常采用 “YAML 多文档” 格式,同时定义 Headless Service 和 StatefulSet:

# 1. 定义 Headless Service(为 Pod 提供 DNS 标识)
apiVersion: v1
kind: Service
metadata:
  name: nginx-svc  # Headless Service 名称,后续 StatefulSet 需引用
  labels:
    app: nginx
spec:
  ports:
  - port: 80        # 服务端口
    name: web
  clusterIP: None   # 关键:设为 None 表示 Headless Service,不分配 ClusterIP
  selector:
    app: nginx      # 匹配 StatefulSet 管理的 Pod 标签

---
# 2. 定义 StatefulSet(管理有状态 Pod)
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx-sts  # StatefulSet 名称
spec:
  serviceName: "nginx-svc"  # 关联 Headless Service(必须与上面的 name 一致)
  replicas: 2               # 副本数:2 个 Pod(nginx-sts-0、nginx-sts-1)
  selector:
    matchLabels:
      app: nginx            # 匹配 Pod 标签(与 Service selector 一致)
  template:
    metadata:
      labels:
        app: nginx          # Pod 标签(与 selector 匹配)
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www         # 挂载卷名称,需与 volumeClaimTemplates 中的 name 一致
          mountPath: /usr/share/nginx/html  # 容器内挂载路径
  # 3. 存储卷申请模板:为每个 Pod 生成独立 PVC
  volumeClaimTemplates:
  - metadata:
      name: www  # 卷名称,需与容器 volumeMounts.name 一致
    spec:
      accessModes: ["ReadWriteOnce"]  # 访问模式:单节点读写
      storageClassName: "nfs-sc"      # 关联存储类(需提前创建,用于动态生成 PV)
      resources:
        requests:
          storage: 1Gi  # 每个 PVC 请求 1Gi 存储

三、StatefulSet 实战:部署 Web 站点

以 “多实例 Web 站点” 为例,演示 StatefulSet 的创建、验证与核心特性,需提前准备:

  • 已部署 NFS 服务(或其他分布式存储);
  • 已创建存储类 nfs-sc(参考前文 StorageClass 章节,用于动态生成 PV)。

3.1 步骤 1:创建 StatefulSet 与 Headless Service

  1. 编写资源清单(statefulset-web.yaml):

# Headless Service
apiVersion: v1
kind: Service
metadata:
  name: web-svc
  labels:
    app: web
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: web

---
# StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "web-svc"
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: "nfs-sc"  # 引用提前创建的存储类
      resources:
        requests:
          storage: 1Gi

2.应用清单并验证基础资源:

# 创建资源
kubectl apply -f statefulset-web.yaml

# 1. 查看 StatefulSet 状态(READY 为 2/2 表示正常)
kubectl get statefulset
# 输出:
# NAME   READY   AGE
# web    2/2     2m

# 2. 查看 Pod(名称有序:web-0、web-1,调度节点可能不同)
kubectl get pods -l app=web -o wide
# 输出:
# NAME    READY   STATUS    RESTARTS   AGE   IP            NODE   NOMINATED NODE
# web-0   1/1     Running   0          2m    10.244.2.10   hd2    <none>
# web-1   1/1     Running   0          1m    10.244.3.15   hd3    <none>

# 3. 查看 Headless Service(CLUSTER-IP 为 None)
kubectl get svc web-svc
# 输出:
# NAME      TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
# web-svc   ClusterIP   None         <none>        80/TCP    2m

# 4. 查看自动生成的 PVC(每个 Pod 对应一个 PVC,命名格式:www-web-0、www-web-1)
kubectl get pvc
# 输出:
# NAME        STATUS   VOLUME                                     CAPACITY   ACCESS MODES
# www-web-0   Bound    pvc-xxx-xxx-xxx-xxx-xxx                   1Gi        RWO
# www-web-1   Bound    pvc-yyy-yyy-yyy-yyy-yyy                   1Gi        RWO

3.2 步骤 2:验证 StatefulSet 核心特性

StatefulSet 的核心价值在于 “标识固定”“存储独立”“有序管理”,通过以下验证确认特性生效:

(1)验证固定 DNS 与服务发现

Headless Service 为每个 Pod 生成固定 DNS,可在集群内通过 nslookup 解析:

# 1. 启动一个带 dnsutils 工具的临时 Pod(用于解析 DNS)
kubectl run -it --rm dns-test --image=busybox:1.35 -- sh

# 2. 在临时 Pod 内解析 Pod 级 DNS(web-0 的 DNS)
nslookup web-0.web-svc.default.svc.cluster.local
# 输出(IP 与 web-0 的实际 IP 一致):
# Name:      web-0.web-svc.default.svc.cluster.local
# Address 1: 10.244.2.10 web-0

# 3. 解析 Service 级 DNS(返回所有 Pod 的 IP)
nslookup web-svc.default.svc.cluster.local
# 输出(包含 web-0 和 web-1 的 IP):
# Name:      web-svc.default.svc.cluster.local
# Address 1: 10.244.2.10 web-0
# Address 2: 10.244.3.15 web-1
(2)验证独立存储

每个 Pod 挂载独立 PVC,数据不互通,可通过写入测试文件验证:

# 1. 向 web-0 的存储卷写入文件(容器内路径 /usr/share/nginx/html)
kubectl exec -it web-0 -- sh -c 'echo "web-0: StatefulSet Test" > /usr/share/nginx/html/index.html'

# 2. 向 web-1 的存储卷写入不同内容
kubectl exec -it web-1 -- sh -c 'echo "web-1: StatefulSet Test" > /usr/share/nginx/html/index.html'

# 3. 访问两个 Pod 的 IP,验证内容不同(独立存储生效)
curl 10.244.2.10  # 输出:web-0: StatefulSet Test
curl 10.244.3.15  # 输出:web-1: StatefulSet Test
(3)验证 Pod 重建后状态不变

删除 web-0,观察重建后的名称、存储和 DNS 是否保持一致:

# 1. 删除 web-0
kubectl delete pod web-0

# 2. 查看重建后的 Pod(名称仍为 web-0,IP 可能变化)
kubectl get pods -l app=web
# 输出:
# NAME    READY   STATUS    RESTARTS   AGE
# web-0   1/1     Running   0          30s  # 重建完成
# web-1   1/1     Running   0          5m

# 3. 访问重建后的 web-0(数据仍存在,独立存储生效)
curl $(kubectl get pod web-0 -o jsonpath='{.status.podIP}')
# 输出:web-0: StatefulSet Test(原数据未丢失)

# 4. 解析 web-0 的 DNS(仍指向新 IP,DNS 标识不变)
kubectl exec -it dns-test -- nslookup web-0.web-svc.default.svc.cluster.local
# 输出(新 IP 已更新到 DNS):
# Name:      web-0.web-svc.default.svc.cluster.local
# Address 1: 10.244.2.11 web-0  # 新 IP,但 DNS 名称不变

四、StatefulSet 日常运维:扩容、缩容与更新

StatefulSet 的运维操作需遵循 “有序” 原则,确保有状态服务的稳定性(如从库不先于主库更新)。

4.1 扩容(增加副本数)

扩容时 StatefulSet 按 0→1→2... 顺序创建新 Pod,每个新 Pod 会自动生成对应的 PVC:

# 方式 1:编辑 StatefulSet 配置,修改 replicas 为 3
kubectl edit statefulset web
# 将 spec.replicas: 2 改为 spec.replicas: 3,保存退出

# 方式 2:使用 kubectl scale 命令(更便捷)
kubectl scale statefulset web --replicas=3

# 验证扩容结果(新 Pod 名称为 web-2,有序创建)
kubectl get pods -l app=web
# 输出:
# NAME    READY   STATUS    RESTARTS   AGE
# web-0   1/1     Running   0          10m
# web-1   1/1     Running   0          15m
# web-2   1/1     Running   0          1m  # 新扩容的 Pod

# 验证新 PVC(自动生成 www-web-2)
kubectl get pvc | grep web-2
# 输出:www-web-2   Bound    pvc-zzz-zzz-zzz-zzz-zzz   1Gi        RWO

4.2 缩容(减少副本数)

缩容与扩容逻辑相反,StatefulSet 会按 高序号→低序号(如 2→1→0)的顺序删除 Pod,确保不影响核心实例(如主库 web-0 最后删除)。需注意:默认情况下,缩容仅删除 Pod,不会自动删除对应的 PVC 和 PV(避免误删数据),需手动清理无用存储资源。

操作步骤
  1. 执行缩容(以从 3 个副本缩容到 2 个为例):

# 方式 1:编辑 StatefulSet,修改 replicas 为 2
kubectl edit statefulset web
# 将 spec.replicas: 3 改为 spec.replicas: 2,保存退出

# 方式 2:使用 kubectl scale 命令(更便捷)
kubectl scale statefulset web --replicas=2

2.验证缩容结果

# 查看 Pod 状态:高序号 Pod(web-2)被删除,保留 web-0、web-1
kubectl get pods -l app=web
# 输出:
# NAME    READY   STATUS    RESTARTS   AGE
# web-0   1/1     Running   0          20m
# web-1   1/1     Running   0          18m

# 查看 PVC 状态:web-2 对应的 PVC(www-web-2)仍存在(状态为 Bound)
kubectl get pvc | grep web
# 输出:
# www-web-0   Bound    pvc-xxx-xxx-xxx-xxx-xxx   1Gi        RWO   nfs-sc   20m
# www-web-1   Bound    pvc-yyy-yyy-yyy-yyy-yyy   1Gi        RWO   nfs-sc   18m
# www-web-2   Bound    pvc-zzz-zzz-zzz-zzz-zzz   1Gi        RWO   nfs-sc   5m  # 缩容后仍保留

3.清理无用存储资源(可选,确认数据无需保留后执行):

# 删除缩容后无用的 PVC(www-web-2)
kubectl delete pvc www-web-2
# 若 PV 的回收策略为 Delete,PVC 删除后 PV 会自动删除;若为 Retain,需手动删除 PV
kubectl delete pv pvc-zzz-zzz-zzz-zzz-zzz  # 替换为实际 PV 名称

4.3 镜像更新

StatefulSet 的镜像更新需遵循 “有序更新” 原则,默认采用 RollingUpdate(滚动更新)策略,按 高序号→低序号 顺序重建 Pod(如先更新 web-1,再更新 web-0),避免核心实例(如主库)先下线导致服务中断。也可配置为 OnDelete 策略(需手动删除 Pod 才触发更新)。

4.3.1 查看默认更新策略
# 查看 StatefulSet 的更新策略(默认 type: RollingUpdate)
kubectl get statefulset web -o jsonpath='{.spec.updateStrategy}' | jq
# 输出:
# {
#   "rollingUpdate": {
#     "partition": 0  # 分区更新参数,默认 0 表示全量更新
#   },
#   "type": "RollingUpdate"
# }
4.3.2 执行镜像更新(以更新 Nginx 镜像为例)
  1. 方式 1:使用 kubectl set image(推荐,便捷)

# 格式:kubectl set image statefulset/<StatefulSet 名称> <容器名>=<新镜像>
kubectl set image statefulset/web nginx=nginx:1.23  # 将 nginx 镜像从 latest 改为 1.23

2.方式 2:编辑 StatefulSet 配置

kubectl edit statefulset web
# 将 spec.template.spec.containers[0].image 改为 nginx:1.23,保存退出

3.观察更新过程

# 实时查看 Pod 状态:高序号 Pod 先重建,完成后再更新低序号 Pod
kubectl get pods -l app=web -w
# 输出(过程示例):
# NAME    READY   STATUS              RESTARTS   AGE
# web-0   1/1     Running             0          25m
# web-1   1/1     Running             0          23m
# web-1   1/1     Terminating         0          23m  # 先终止 web-1
# web-1   0/1     Pending             0          0s   # 重建 web-1
# web-1   0/1     ContainerCreating   0          0s
# web-1   1/1     Running             0          3s   # web-1 重建完成
# web-0   1/1     Terminating         0          25m  # 再终止 web-0
# web-0   0/1     Pending             0          0s
# web-0   0/1     ContainerCreating   0          0s
# web-0   1/1     Running             0          2s   # web-0 重建完成

4.验证更新结果

# 查看 Pod 使用的镜像是否为新镜像(nginx:1.23)
kubectl get pods -l app=web -o jsonpath='{range .items[*]}{.metadata.name}{": "}{.spec.containers[0].image}{"\n"}{end}'
# 输出:
# web-0: nginx:1.23
# web-1: nginx:1.23
4.3.3 配置 OnDelete 更新策略(手动触发更新)

若需更严格的更新控制(如数据库主从更新需先手动同步数据),可将更新策略改为 OnDelete

# 编辑 StatefulSet,修改 updateStrategy.type 为 OnDelete
kubectl edit statefulset web
# 修改内容:
# spec:
#   updateStrategy:
#     type: OnDelete  # 替换默认的 RollingUpdate

# 之后更新镜像后,需手动删除 Pod 才会触发重建(更新)
kubectl set image statefulset/web nginx=nginx:1.24  # 更新镜像配置
kubectl delete pod web-1  # 手动删除 web-1,触发重建(使用新镜像)
kubectl delete pod web-0  # 再删除 web-0,完成更新

4.4 版本回滚

若更新后出现问题(如镜像兼容问题),可通过 StatefulSet 的 “历史版本” 回滚到之前的稳定状态。StatefulSet 会保留 revisionHistoryLimit(默认 10)个历史版本,可通过 kubectl rollout 命令管理。

操作步骤
  1. 查看历史版本

# 查看 StatefulSet 的历史版本(每个版本对应一个 ReplicaSet)
kubectl rollout history statefulset web
# 输出:
# deployments "web"
# REVISION  CHANGE-CAUSE
# 1         kubectl apply --filename=statefulset-web.yaml --record=true
# 2         kubectl set image statefulset/web nginx=nginx:1.23

2.查看指定版本的详细变更(可选):

# 方式 1:回滚到上一个版本(快捷命令)
kubectl rollout undo statefulset web

# 方式 2:回滚到指定版本(如版本 1)
kubectl rollout undo statefulset web --to-revision=1

3.执行回滚(以回滚到版本 1 为例):

# 方式 1:回滚到上一个版本(快捷命令)
kubectl rollout undo statefulset web

# 方式 2:回滚到指定版本(如版本 1)
kubectl rollout undo statefulset web --to-revision=1

4.验证回滚结果

# 查看 Pod 镜像是否回滚到旧版本(如 nginx:latest)
kubectl get pods -l app=web -o jsonpath='{range .items[*]}{.metadata.name}{": "}{.spec.containers[0].image}{"\n"}{end}'
# 输出:
# web-0: nginx:latest
# web-1: nginx:latest

五、StatefulSet 进阶:关键配置与注意事项

5.1 关键配置优化

(1)调整历史版本保留数

默认保留 10 个历史版本,若集群资源有限,可减少保留数:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  revisionHistoryLimit: 5  # 保留 5 个历史版本(建议至少保留 2-3 个)
  # 其他配置...
(2)配置分区更新(金丝雀发布)

通过 spec.updateStrategy.rollingUpdate.partition 实现 “分区更新”,仅更新序号大于等于 partition 的 Pod(适用于金丝雀发布或灰度测试):

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  updateStrategy:
    rollingUpdate:
      partition: 1  # 仅更新序号 >=1 的 Pod(即 web-1,不更新 web-0)
    type: RollingUpdate
  # 其他配置...

使用场景:先更新从库(web-1)验证稳定性,确认无误后将 partition 改为 0,再更新主库(web-0)。

5.2 注意事项

  1. 存储必须独立:S tatefulSet 依赖 volumeClaimTemplates 为每个 Pod 分配独立 PVC,不可使用共享存储(如 NFS 的 ReadWriteMany 模式),否则会导致数据冲突。
  2. 避免频繁扩缩容:扩缩容会创建 / 删除 PVC,若存储类使用动态 PV(如云厂商存储),可能产生额外费用;缩容后需手动清理无用 PVC/PV,避免资源浪费。
  3. 主从集群需额外配置:StatefulSet 仅保证 “标识固定” 和 “存储独立”,主从同步(如 MySQL 主从复制、Redis 主从)需通过初始化脚本(如 initContainers)或配置中心(如 ConfigMap)实现。
  4. 节点亲和性与污点容忍:若需将 StatefulSet Pod 调度到指定节点,可配置 nodeAffinity;若节点有污点(如控制节点的 node-role.kubernetes.io/control-plane),需添加对应的容忍(tolerations)。

StatefulSet 是 Kubernetes 管理有状态服务的核心控制器,通过 Headless Service 实现固定网络标识、VolumeClaimTemplates 实现独立存储、有序生命周期管理保障服务稳定性,适用于 MySQL 主从、Redis 集群、ZooKeeper 等场景。

点赞+收藏+关注,下期我们讲讲DaemonSet控制器!不见不散

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐