Skip to content

StatefulSet

Deployment 管理无状态副本,Pod 名字是随机后缀,PVC 也是共享一个。StatefulSet 管理有状态副本,给每个 Pod 提供三样东西:稳定名称、稳定网络身份和独立 PVC。

稳定名称意味着 Pod 被重建后名字不变(mysql-0 永远是 mysql-0),稳定网络身份意味着配合 Headless Service 后每个 Pod 有固定 DNS,独立 PVC 意味着缩容后 PVC 不删,下次扩容同序号 Pod 会重新挂上同一块盘。

单独的 StatefulSet 不提供高可用。复制、选主、备份、恢复这些能力要由数据库或 Operator 自己处理。StatefulSet 只负责把"每个副本有固定身份和独立存储"这件事做好。

稳定网络身份和 Headless Service

StatefulSet 的每个 Pod 有两个名字:

  • Pod 名:<statefulset-name>-<序号>,如 mysql-0mysql-1
  • DNS 名:<pod-name>.<headless-service-name>.<namespace>.svc.cluster.local

DNS 名的前提是创建了一个 clusterIP: None 的 Headless Service:

yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: demo
spec:
  clusterIP: None        # Headless——DNS 返回 Pod IP 而非 ClusterIP
  selector:
    app: mysql
  ports:
    - name: mysql
      port: 3306

Headless Service 的 DNS 查询不回一个虚拟 IP,而是返回所有 Ready Pod 的 IP 列表。调用方拿到的是真实 Pod IP,直连后端。这对 Kafka、ZooKeeper、数据库集群非常关键:节点之间通信需要知道彼此的真实地址,而不是经过 ClusterIP 再做一层转发。

Pod 的 DNS 名则是独立解析的:

text
mysql-0.mysql.demo.svc.cluster.local  → mysql-0 的 Pod IP
mysql-1.mysql.demo.svc.cluster.local  → mysql-1 的 Pod IP

volumeClaimTemplates

Deployment 里所有 Pod 共享同一个 PVC 模板(写在 spec.template.spec.volumes 里)。StatefulSet 用 volumeClaimTemplates,会为每个 Pod 单独创建一个 PVC,命名规则是 <template名称>-<statefulset名称>-<序号>

yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
  namespace: demo
spec:
  serviceName: mysql       # 必须指定,对应 Headless Service 名称
  replicas: 2
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
        - name: mysql
          image: mysql:8.4
          env:
            - name: MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-root
                  key: password
          volumeMounts:
            - name: data
              mountPath: /var/lib/mysql
  volumeClaimTemplates:     # 每个 Pod 独立 PVC 的来源
    - metadata:
        name: data
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: nfs-client
        resources:
          requests:
            storage: 10Gi

创建后查看 PVC 命名:

bash
kubectl -n demo get pod -l app=mysql
kubectl -n demo get pvc

会看到 data-mysql-0data-mysql-1 两个 PVC,分别绑定到两个 Pod。mysql-0 Pod 删掉重建后,名字不变,会重新挂上 data-mysql-0 这块 PVC——数据还在。

扩缩容

扩容按序号递增,先建 mysql-2,Ready 后才建 mysql-3

bash
kubectl -n demo scale statefulset mysql --replicas=3

缩容按序号递减,先删 mysql-2,再删 mysql-1

bash
kubectl -n demo scale statefulset mysql --replicas=1

缩容时 StatefulSet 默认不删除 PVC。这是有意为之:Pod 删了数据还在,万一误缩容还能扩回去。但这也意味着 PVC 会一直占用后端存储空间,需要在确认数据不再需要后手动清理 PVC。

扩缩容的默认有序行为可以通过 podManagementPolicy: Parallel 改成并行,适合不需要启动顺序的场景。

更新策略

yaml
updateStrategy:
  type: RollingUpdate
  rollingUpdate:
    partition: 0   # 只更新序号 ≥ partition 的 Pod,可用于金丝雀

StatefulSet 的滚动更新是从最大序号向最小序号逐个进行的。mysql-2 更新完成并 Ready 后,才会更新 mysql-1。这和 Deployment 的滚动更新逻辑不同。

对于数据库类服务,默认的滚动更新机制可能不够。主从复制场景下,先更新从库再更新主库的顺序需求、更新前的数据一致性检查、更新后的复制状态验证,StatefulSet 本身并不处理。这些通常由 Operator 接管。

partition 是一个实用的字段:设为 2 时只更新序号 ≥2 的 Pod,可以先在一两个副本上验证新版本,再逐步调小 partition 推向全部。

适用和不适用

适合 StatefulSet 的场景:

场景StatefulSet 提供的价值
需要固定网络身份的中间件Kafka broker ID、ZooKeeper myid 和配置里硬编码的地址可以一一对应
每个副本需要独立数据盘数据库 data 目录、索引、WAL 各自独立
有启动顺序要求的集群先启动种子节点,再启动普通节点
稳定的 Pod 名用于服务发现配置文件和应用代码里能写出确定的地址

不适合直接套用的情况:

场景原因
无状态 Web 服务Deployment 更简单,没有序号和 PVC 的额外管理成本
MySQL 高可用还要考虑复制、选主、读写分离、备份、故障切换
复杂的分布式数据库通常有专门的 Operator 来处理配置、扩缩、备份和升级
不了解底层存储PVC 和后端存储的故障处理比 Deployment 的排查链路更长

排查

bash
kubectl -n demo get sts mysql
kubectl -n demo get pod -l app=mysql -o wide
kubectl -n demo get pvc
kubectl -n demo describe pod mysql-0

常见问题:

现象判断方向
Pod 卡在 PendingPVC 未绑定、节点资源不够、调度亲和性/污点不满足
Pod 卡在 ContainerCreating存储挂载失败、节点缺存储客户端、CSI 异常
扩容新副本异常volumeClaimTemplates 里 StorageClass 不存在、镜像错误、资源不足
删除 StatefulSet 后 PVC 和数据还在设计行为——PVC 不随 StatefulSet 级联删除
新 Pod 挂上了旧数据同序号 PVC 被复用,这正是 StatefulSet 的设计预期
缩容后 PVC 仍占用空间需手动删除不再需要的 PVC