找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4040

积分

0

好友

529

主题
发表于 10 小时前 | 查看: 4| 回复: 0

在 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 的启动策略,它可以是 OrderedReadyParallel

  • 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 中,使用 nslookupdig 命令解析这些域名,会直接得到对应 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

Kubernetes StatefulSet 与 Headless Service 网络架构示意图

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 上,从而实现了数据的持久化。这种机制是保障 数据库 等有状态应用数据安全的关键。

Kubernetes StatefulSet 持久化卷绑定关系示意图

4. 更新策略

有状态服务的多个实例间通常存在数据同步或复制关系,更新时需要格外小心,以维持集群的一致性与可用性。StatefulSet 通过 spec.updateStrategy 字段定义更新策略,支持两种模式:

OnDelete
此策略下,当您修改了 StatefulSet 的 Pod 模板(如更新镜像版本)后,控制器不会自动更新任何 Pod。只有在用户手动删除某个旧 Pod 时,它才会根据新模板被重新创建。这提供了最大的控制权,适合对更新流程有严格管控的场景。

RollingUpdate(默认策略)
这是 StatefulSet 的默认更新策略。它会以 逆序(从序号最大的 Pod 开始到序号最小的 Pod)的方式,逐个对 Pod 进行滚动更新。
更新过程是:终止当前 Pod -> 等待新 Pod 进入 RunningReady 状态 -> 再继续更新下一个 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 中驾驭有状态服务。如果你想了解更多云原生实践或与其他开发者交流,欢迎访问 云栈社区 探讨。




上一篇:从kubeadm部署到双向TLS认证:深度解析Kubernetes集群证书体系与运维实践
下一篇:K8S Service实现原理详解:从Pod访问到负载均衡的三种类型
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-7 17:25 , Processed in 0.582748 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表