路线图

StatefulSet:有状态应用的有序管理

星辉 2026-07-02 阅读 3 min 572 字 路线图
StatefulSet:有状态应用的有序管理 封面

Deployment 能搞定大多数无状态服务,但数据库、消息队列、分布式存储这类应用有严格的状态依赖:每个实例需要稳定的身份、持久的存储、确定的启动顺序。StatefulSet 就是为此设计的控制器。它给每个 Pod 一个"名字"而非随机编号,给每个 Pod 一块"专属磁盘"而非临时存储,并保证它们按顺序出生和死亡。


有状态 vs 无状态:核心区别#

先明确概念,再看 K8s 如何应对这两类应用的需求差异。

维度无状态应用 (Deployment)有状态应用 (StatefulSet)
网络标识Pod 名随机(如 nginx-app-a1b2c),IP 不固定Pod 名有序(如 mysql-0),DNS 名稳定
存储持久性Pod 删除后存储丢失,重新调度到不同节点每个 Pod 绑定专属 PVC,Pod 重建后挂回同一 PVC
部署顺序所有 Pod 同时启动,无先后依赖从序号 0 开始逐个启动(0→1→2),前一个就绪后才启动下一个
删除顺序所有 Pod 同时终止从最大序号开始逆序终止(2→1→0)
更新策略滚动更新,新旧交替,无顺序要求逆序更新(2→1→0),或按需自定义
典型应用Web 服务、API 网关、微服务MySQL 主从、ZooKeeper、Elasticsearch、Kafka

为什么这些区别重要?以 MySQL 主从为例:slave 必须在 master 就绪后才能连接并同步数据;master 的数据目录必须持久化——Pod 重建后数据不能丢。Deployment 既不能保证启动顺序,也不能给每个 Pod 绑定独立存储,所以不适合这类场景。


稳定网络标识与持久存储#

Headless Service:生成稳定 DNS 名#

普通 Service 为 Pod 提供一个 ClusterIP,访问时随机分配到后端 Pod。但 StatefulSet 需要每个 Pod 有独立的 DNS 名,因为客户端往往要定向访问某个 Pod(比如只连接 master,不连 slave)。

Headless Service 就是把 clusterIP 设为 None

yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql-headless
spec:
  clusterIP: None          # 关键:不分配 ClusterIP
  selector:
    app: mysql
  ports:
    - port: 3306

当 Headless Service 与 StatefulSet 配合时,K8s 为每个 Pod 生成一条 DNS 记录:

text
<pod-name>.<headless-service-name>.<namespace>.svc.cluster.local

比如 mysql-0 的完整域名是:

text
mysql-0.mysql-headless.default.svc.cluster.local

不管 Pod 被重新调度到哪个节点,只要 StatefulSet 还存在,这个 DNS 名始终指向同一个序号的 Pod(虽然 IP 可能变了,但 DNS 名不变)。

volumeClaimTemplates:每个 Pod 一块专属存储#

StatefulSet 通过 volumeClaimTemplates 自动为每个 Pod 创建一个独立的 PVC:

yaml
volumeClaimTemplates:
  - metadata:
      name: mysql-data      # PVC 名称前缀,实际名称为 mysql-data-mysql-0
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 10Gi

这意味着:

  • mysql-0 获得 PVC mysql-data-mysql-0
  • mysql-1 获得 PVC mysql-data-mysql-1
  • mysql-2 获得 PVC mysql-data-mysql-2

每个 PVC 在首次创建时就绑定了一个 PV。Pod 重建(哪怕是调度到不同节点)后,StatefulSet 会让同序号的 Pod 挂回同名的 PVC——数据不会丢失。PVC 与 Pod 生命周期独立:Pod 删了,PVC 保留。默认情况下 PVC 永远不会被自动删除——即便删除整个 StatefulSet,volumeClaimTemplates 生成的 PVC 仍会保留(对有状态数据正是期望行为)。只有显式配置 persistentVolumeClaimRetentionPolicy(1.32 GA,可设 whenDeleted/whenScaledDelete)才会让 PVC 随 StatefulSet 删除或缩容而回收。

ReadWriteOnce(RWO)是大多数块存储的默认访问模式,同一 PVC 只能被一个节点挂载。这对数据库是合理的——数据文件不允许多节点同时写入。


有序部署与删除#

StatefulSet 默认使用 OrderedReady 策略,严格控制 Pod 的启停顺序。

部署过程(OrderedReady)#

replicas: 3 为例:

  1. 创建 mysql-0,等待它 Running + Ready
  2. 创建 mysql-1,等待它 Running + Ready
  3. 创建 mysql-2,等待它 Running + Ready

前一个 Pod 未就绪时,后一个 Pod 不会被创建。这对 MySQL 主从部署至关重要:master(mysql-0)必须先启动并初始化数据,slave 才能连接它进行同步。

删除过程#

缩容或删除 StatefulSet 时,逆序执行:

  1. 删除 mysql-2,等待终止完成
  2. 删除 mysql-1,等待终止完成
  3. 删除 mysql-0,等待终止完成

逆序删除保证 master 是最后一个被终止的——避免 slave 还在同步时 master 就消失了。

parallel 策略#

如果应用没有顺序依赖(比如每个 Pod 是独立的缓存节点),可以设置 podManagementPolicy: Parallel,让所有 Pod 同时启动和终止,加速部署。但大多数有状态应用还是用默认的 OrderedReady。

OrderedReady 的代价是慢——3 个副本的部署可能要几分钟。如果你的应用可以在启动后快速自愈(如 Elasticsearch),可以考虑 parallel 策略缩短部署时间。


实战:MySQL 主从部署示例#

以下是一个最小可运行的 MySQL 主从 StatefulSet 配置,包含 Headless Service + PVC + 主从复制初始化脚本。

Headless Service#

yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql-headless
  labels:
    app: mysql
spec:
  clusterIP: None
  selector:
    app: mysql
  ports:
    - name: mysql
      port: 3306

StatefulSet + volumeClaimTemplates#

yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql-headless    # 必须指定 Headless Service 名称
  replicas: 2                    # mysql-0 = master, mysql-1 = slave
  selector:
    matchLabels:
      app: mysql
  podManagementPolicy: OrderedReady
  template:
    metadata:
      labels:
        app: mysql
    spec:
      initContainers:
        # 初始化容器:根据 Pod 序号决定是 master 还是 slave
        - name: mysql-init
          image: mysql:8.0
          command:
            - bash
            - "-c"
            - |
              if [ "$(hostname | sed 's/mysql-//')" = "0" ]; then
                # mysql-0 是 master,无需特殊初始化
                echo "This is master (mysql-0), no slave setup needed."
              else
                # mysql-1 是 slave,从 master 克隆数据并配置复制
                echo "This is slave, cloning data from master..."
                # 实际生产中应使用 xtrabackup 或 mysqldump 克隆
                # 这里简化演示:仅配置复制指向 master
              fi
          env:
            - name: MYSQL_ROOT_PASSWORD
              value: "changeme"
      containers:
        - name: mysql
          image: mysql:8.0
          ports:
            - containerPort: 3306
          env:
            - name: MYSQL_ROOT_PASSWORD
              value: "changeme"
          volumeMounts:
            - name: mysql-data
              mountPath: /var/lib/mysql
  volumeClaimTemplates:
    - metadata:
        name: mysql-data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 10Gi

要点解读

  1. serviceName: mysql-headless——这是 StatefulSet 的关键字段,它告诉 K8s 用哪个 Headless Service 为 Pod 生成 DNS 记录。不设置的话,Pod 不会有稳定域名
  2. initContainers 中的序号判断——hostname 在 StatefulSet 中返回有序 Pod 名(mysql-0mysql-1),据此区分 master/slave 角色
  3. volumeClaimTemplates——自动为 mysql-0 和 mysql-1 各创建一个 PVC,数据持久化到各自独立的 PV 上
  4. OrderedReady 策略——mysql-0 先启动完成,mysql-1 才开始初始化并连接 master 同步数据

完整生产级 MySQL 主从还需要:配置主从复制的 SQL 语句、xtrabackup 数据克隆脚本、健康检查探针、备份策略等。以上示例展示了 StatefulSet 的核心机制,可作为生产配置的骨架。


实战要点#

  1. 必须配 Headless Service——没有 Headless Service,Pod 就没有稳定域名,主从复制等定向连接无法工作
  2. 不要用 Deployment 替代 StatefulSet——对数据库等有状态应用,Deployment 的随机 Pod 名和共享存储会带来数据丢失风险
  3. 注意 PVC 不会自动删除——StatefulSet 删除后 PVC 仍然保留。如果确认要清除数据,需手动删除 PVC
  4. 缩容要谨慎——从 3 缩到 2 时,mysql-2 的 PVC 依然保留。再次扩回 3 时,mysql-2 会挂回原来的 PVC(数据恢复)。但如果你已经删除了 mysql-2 的 PVC,数据就真的丢了

小结#

StatefulSet 是 Kubernetes 管理有状态应用的专用控制器。与 Deployment 相比,它提供了三个关键能力:稳定的网络标识(通过 Headless Service)、持久化的专属存储(通过 volumeClaimTemplates)、有序的部署和删除(通过 OrderedReady 策略)。这三者共同满足了数据库、分布式系统等有状态应用对身份、数据和顺序的严格要求。MySQL 主从部署示例展示了这些机制的协同工作——master 先启动初始化数据,slave 后启动连接同步,每个实例的数据独立持久化。理解 StatefulSet,你就理解了 K8s 如何在不可靠的容器环境中支撑有状态的业务逻辑。