Kubernetes技术入门与实践(五):DaemonSet与StatefulSet

前言

在掌握了Kubernetes中Pod与Deployment的基础知识之后,我们本章将深入讲解另外两种不可或缺的工作负载控制器:DaemonSet (守护进程集)与StatefulSet(有状态副本集)。如果说Deployment是管理无状态微服务的"瑞士军刀",那么DaemonSet就是守护每个节点的"忠诚哨兵",而StatefulSet则是呵护有状态应用的"贴心管家"。这三种控制器构成了Kubernetes工作负载管理的核心三角,掌握它们就掌握了Kuberentes应用部署的精髓。

本文将基于OpenEuler 24.03 LTS SP3系统环境,从基础概念、实现原理讲起,逐步深入到YAML配置详解、完整部署实战,并结合大量动手实践示例,帮助读者真正理解并学会使用这两种控制器。OpenEuler作为华为主导的国产开源Linux发行版,其24.03 LTS SP3版本在云原生支持方面做了大量优化(支持Docker、Containerd和iSulad等容器运行时),非常适合作为学习和生产环境的基础平台。本文将在全过程中注意标注OpenEuler系统特有的配置要点。

环境说明:本文所有命令和配置已基于openEuler 24.03 LTS SP3系统与Kubernetes v1.28至v1.33版本验证,操作步骤均可直接执行。

第一章 Kubernetes环境准备(基于OpenEuler 24.03 LTS SP3)

在深入DaemonSet和StatefulSet之前,我们首先确保Kubernetes集群已经正确部署在了OpenEuler系统之上。本章将简要回顾OpenEuler下部署Kubernetes的核心步骤,详细步骤可参考系列教程的第二篇文章。

1.1 OpenEuler系统的初始环境配置

OpenEuler 24.03 LTS SP3是华为主导的开源Linux发行版,"24.03"代表2024年3月发布的稳定版,"LTS SP3"代表长期支持版本的第3次更新,它适配x86、鲲鹏等多种架构,稳定性强,特别适合企业级和学习级部署。

在开始之前,所有节点都需要完成以下系统初始化配置:

(1)关闭防火墙

OpenEuler默认开启防火墙,这会阻止Kubernetes各组件之间的网络通信。

bash 复制代码
# 临时关闭防火墙
systemctl stop firewalld
# 永久关闭防火墙
systemctl disable firewalld
# 验证防火墙状态(确保显示 inactive)
systemctl status firewalld

初学者在测试环境中可以直接关闭防火墙;生产环境中则需精细配置防火墙规则。

(2)关闭SELinux

SELinux是Linux的安全增强组件,它会限制容器的权限,干扰Kubernetes的正常运行。

bash 复制代码
# 临时关闭 SELinux(立即生效)
setenforce 0
# 永久关闭 SELinux
sed -i 's/^SELINUX=enforcing$/SELINUX=disabled/' /etc/selinux/config
# 验证 SELinux 状态(确保显示 Permissive 或 Disabled)
getenforce

修改配置文件后需重启系统才能完全生效。

(3)关闭Swap交换分区

Kubernetes官方要求关闭Swap,因为Swap会严重影响节点性能和Pod调度稳定性。

bash 复制代码
# 临时关闭 Swap
swapoff -a
# 永久关闭 Swap(注释掉 /etc/fstab 中的 swap 行)
sed -i 's/.*swap.*/#&/' /etc/fstab

(4)设置内核参数

Kubernetes网络需要启用网桥过滤和IP转发功能。

bash 复制代码
cat <<EOF >> /etc/sysctl.conf
net.ipv4.ip_forward=1
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF

modprobe br_netfilter
sysctl -p

(5)设置主机名与Hosts文件

bash 复制代码
# Master 节点
hostnamectl set-hostname k8s-master
# Node 节点
hostnamectl set-hostname k8s-node1

# 编辑 /etc/hosts,添加集群节点IP映射
cat <<EOF >> /etc/hosts
192.168.100.110 k8s-master
192.168.100.111 k8s-node1
192.168.100.112 k8s-node2
EOF

1.2 容器运行时的安装

Kubernetes本身并不直接运行容器,它需要通过容器运行时接口(Container Runtime Interface, CRI) 来管理容器的生命周期。在OpenEuler系统上,常见的容器运行时选项包括:

容器运行时 简介
Docker 最经典的容器引擎,OpenEuler官方仓库已集成支持
containerd CNCF毕业项目,Kubernetes社区推荐的默认运行时
iSulad 华为自研的统一容器引擎,在OpenEuler上深度优化

Kubernetes作为一种声明式的容器编排平台,其核心功能之一就是自动管理容器的生命周期,包括启动、停止、调度和故障恢复,从而避免手动管理的大量繁琐操作。

下面以使用最广泛的containerd为例(推荐初学者选择此选项):

bash 复制代码
# 1. 安装 containerd
yum install -y containerd

# 2. 生成默认配置文件
containerd config default > /etc/containerd/config.toml

# 3. 修改配置文件(关键步骤)
# 找到 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
# 设置 SystemdCgroup = true(OpenEuler 必须设置此项)
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml

# 4. 启动 containerd 并设置开机自启
systemctl enable containerd --now
systemctl status containerd

OpenEuler特别提示 :如果你使用的是iSulad(yum install -y iSulad),请注意其CRI套接字连接文件为 unix:///var/run/isulad.sock,与containerd的路径不同,在后续kubeadm初始化时需要相应调整。iSulad还支持配置镜像仓库加速、pause容器版本等参数,详细配置可参考OpenEuler官方文档。

1.3 安装Kubernetes核心组件

bash 复制代码
# 配置 Kubernetes YUM 源(使用阿里云镜像源)
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg
       https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF

# 安装 kubeadm、kubelet、kubectl
yum install -y kubeadm kubelet kubectl

# 设置 kubelet 开机自启
systemctl enable kubelet

1.4 初始化Kubernetes集群(以containerd为例)

bash 复制代码
# 在 Master 节点执行(注意 pause 镜像版本)
kubeadm init \
  --apiserver-advertise-address=192.168.100.110 \
  --image-repository registry.aliyuncs.com/google_containers \
  --kubernetes-version v1.28.0 \
  --service-cidr=10.96.0.0/16 \
  --pod-network-cidr=10.244.0.0/16

# 配置 kubectl
mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config

# 安装 Calico 网络插件(OpenEuler 推荐使用 Calico 的 IPIP 隧道或 BGP 路由模式)
# Flannel 在 OpenEuler 24.03 上可能存在兼容性问题
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.2/manifests/calico.yaml

OpenEuler特别提示:有实践表明,在OpenEuler 24.03 LTS-SP1及后续版本中推荐使用Calico的IPIP隧道或BGP路由模式作为容器网络方案,Flannel存在一定的兼容性问题。

Worker节点加入集群

bash 复制代码
# 在 Worker 节点执行(使用 kubeadm init 输出的 join 命令)
kubeadm join 192.168.100.110:6443 --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash>

1.5 OpenEuler上的两种便捷部署方式

除了上述标准的手动部署流程外,OpenEuler还提供了两种更便捷的集群部署方式:

方式一:oeDeploy一键部署

oeDeploy是OpenEuler官方集成的部署工具(2025年及以后版本默认集成),只需简单几步即可完成集群部署:

bash 复制代码
# 安装 oeDeploy CLI 工具
yum install -y oedp

# 获取 Kubernetes 部署插件
oedp repo update
oedp init kubernetes-1.31.1

# 修改配置文件 config.yaml 填写节点信息
vim kubernetes-1.31.1/config.yaml

# 一键执行部署
oedp run install -p kubernetes-1.31.1

oeDeploy支持配置容器运行时(docker/containerd)、节点架构(amd64/arm64)和openEuler版本等参数,最大程度简化部署流程。

方式二:Breeze项目快速部署

wise2c-devops/breeze是一个专注于简化Kubernetes部署的开源项目,对OpenEuler环境做了深度适配,通过容器化的方式提供完整的集群部署解决方案。

1.6 验证集群状态

bash 复制代码
# 查看节点状态(所有节点应为 Ready)
kubectl get nodes

# 查看所有系统 Pod 运行状态
kubectl get pods -A

# 预期输出:所有系统组件 Pod 均为 Running 状态

第二章 DaemonSet(守护进程集)深入讲解

2.1 DaemonSet的核心概念

DaemonSet (守护进程集)是Kubernetes中一种特殊的工作负载控制器,它的核心设计目标是确保集群中每个符合条件的节点上都运行且仅运行一个Pod副本。这种"每节点一个"的部署模式,使得DaemonSet成为部署节点级系统守护进程的理想选择。

DaemonSet与节点的关系极为密切。当一个新的节点加入集群时,DaemonSet控制器会自动在该节点上创建一个新的Pod;当节点被从集群中移除时,对应的DaemonSet Pod也会被垃圾回收机制自动清理。需要特别注意的是,如果某个节点出现故障,DaemonSet并不会自动在其他健康节点上创建额外的Pod来"弥补",因为其设计逻辑是"一个节点一个Pod",而非"维持固定的副本总数"。

DaemonSet与Deployment有着本质区别:

特性维度 DaemonSet Deployment
Pod分布策略 每个符合条件的节点各运行一个 由Kubernetes调度器在集群中自由分布
副本数量 自动等于符合条件的节点数,不设replicas参数 通过spec.replicas显式指定
伸缩行为 随节点的加入/移除自动变化 通过kubectl scale手动调整
典型场景 日志采集、监控代理、网络插件 无状态Web服务、API网关、微服务

2.2 DaemonSet的典型应用场景

DaemonSet的设计理念使其特别适合以下场景:

(1)集群日志采集

在微服务架构中,每个节点上都运行着许多容器,产出的日志分散在各处。通过在每台节点上部署一个日志采集Agent(如Fluentd或Fluent Bit),可以统一收集所有容器的日志并发送到集中式存储(如Elasticsearch、Kafka等)。这是DaemonSet最经典的应用案例。

(2)节点监控与指标采集

Prometheus生态中的Node Exporter需要在每台节点上运行,用来采集CPU、内存、磁盘、网络等系统指标。使用DaemonSet部署Node Exporter,可以确保所有节点都被监控覆盖。

(3)集群网络组件

Kubernetes集群的核心网络组件(如Calico的calico-node、Flannel的kube-flannel、kube-proxy等)也都以DaemonSet的形式部署,因为每个节点都需要运行网络代理来处理Pod间的网络通信。

(4)存储插件

分布式存储系统(如Ceph、GlusterFS)的客户端守护进程需要在每台节点上运行,为Pod提供存储卷的挂载能力。

2.3 DaemonSet的工作原理

DaemonSet控制器的工作流程大致如下:

  1. 控制器持续监听集群中的Node资源和DaemonSet资源的变化

  2. 当检测到新节点加入集群时,控制器为该节点创建一个符合Pod模板定义的Pod

  3. 当检测到节点被删除或不再符合调度条件时,控制器清理该节点上的对应Pod

  4. 当DaemonSet自身的Pod模板更新时,根据更新策略逐步替换旧的Pod

DaemonSet的调度与普通Pod不同。普通Pod由kube-scheduler负责调度到合适的节点上;而DaemonSet Pod的调度绕过了调度器------DaemonSet控制器直接为每个符合条件的节点创建Pod,并设置nodeName字段将Pod"绑定"到指定节点上,从而确保每个节点只有一个Pod。

调度控制机制:DaemonSet默认会在所有节点上启动Pod,但可以通过以下API机制进行精确控制:

  • nodeSelector :基于节点标签进行简单的键值对匹配。例如仅在具有disktype=ssd标签的节点上运行。

  • nodeAffinity:提供更复杂的节点亲和性规则,支持"软亲和"(preferredDuringSchedulingIgnoredDuringExecution)和"硬要求"(requiredDuringSchedulingIgnoredDuringExecution)两种语义。

  • Taints与Tolerations:节点通过Taint排斥Pod,DaemonSet通过在Pod模板中声明对应的Toleration来允许Pod调度到这些节点。这使得DaemonSet可以运行在Master节点或被特殊标记的节点上。

2.4 DaemonSet YAML详细解析

下面是一份完整的DaemonSet YAML配置示例及逐段讲解。本示例部署一个Nginx守护进程(实际场景中可替换为日志采集或监控Agent)。

bash 复制代码
apiVersion: apps/v1          # API 版本,DaemonSet 属于 apps/v1
kind: DaemonSet              # 资源类型声明为 DaemonSet
metadata:
  name: nginx-daemonset      # DaemonSet 的名称
  labels:                    # 标签,用于标识和管理该资源
    app: nginx-daemonset
spec:
  # ===== 1. 选择器:必须与 Pod 模板的标签匹配 =====
  selector:
    matchLabels:
      app: nginx-daemonset

  # ===== 2. Pod 模板:定义 DaemonSet 创建的 Pod 规格 =====
  template:
    metadata:
      labels:
        app: nginx-daemonset  # Pod 的标签,必须与 selector 匹配
    spec:
      # ---- 2.1 节点选择器:仅在带有特定标签的节点上创建 Pod ----
      nodeSelector:
        daemon: need           # 只有标签 daemon=need 的节点才运行此 Pod
                               # 如果希望所有节点都运行,删除此字段即可

      # ---- 2.2 容忍度:允许 Pod 调度到有 Taint 的节点 ----
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule     # 允许 Pod 在 Master 节点上运行

      # ---- 2.3 容器定义 ----
      containers:
      - name: nginx-daemonset
        image: nginx:alpine    # 使用轻量级的 Alpine 版 Nginx
        resources:             # 资源请求和限制
          limits:
            cpu: 250m
            memory: 512Mi
          requests:
            cpu: 250m
            memory: 512Mi
        ports:
        - containerPort: 80
          name: http

  # ===== 3. 更新策略 =====
  updateStrategy:
    type: RollingUpdate       # 滚动更新模式(默认为 RollingUpdate)
    rollingUpdate:
      maxUnavailable: 1       # 更新时最多允许 1 个节点上的 Pod 不可用

  # ===== 4. 就绪等待时间 =====
  minReadySeconds: 10         # Pod 就绪后等待 10 秒才视为可用

关键字段详解

  1. kind: DaemonSet:声明资源类型,这是区别于Deployment和StatefulSet的根本标识。

  2. spec.selectortemplate.metadata.labels :这两个标签必须匹配,Kubernetes API Server会在创建时进行校验。这与Deployment的要求相同,但DaemonSet中不存在spec.replicas字段,因为副本数等于符合条件的节点数。

  3. spec.template.spec.nodeSelector:节点选择器,用于控制DaemonSet仅在特定节点上运行Pod。如果希望在所有节点上都运行Pod(如日志采集和监控Agent),可以删除此字段。

  4. spec.template.spec.tolerations :容忍度配置,允许DaemonSet Pod调度到带污点(Taint)的节点上。例如上例中配置了对Master节点NoSchedule污点的容忍。

  5. spec.updateStrategy.type:支持两种更新策略:

    • RollingUpdate(默认) :以滚动方式逐个替换每个节点上的Pod,配合maxUnavailablemaxSurge参数控制更新节奏。

    • OnDelete:仅当手动删除旧Pod后,控制器才会创建新Pod。适合需要精细控制更新顺序的场景。

  6. spec.minReadySeconds:新建的DaemonSet Pod应该在没有任何容器崩溃的情况下处于就绪状态的最小秒数,达到此时长后Pod才被视为可用。默认值为0(Pod就绪后立即视为可用)。

2.5 DaemonSet实战:部署日志采集Agent(Node Exporter风格示例)

下面我们以部署一个模拟的"节点信息采集器"为例(使用busybox容器模拟),完整演示DaemonSet的创建、查看和管理流程。

步骤1:查看集群节点信息

bash 复制代码
# 查看集群中的所有节点及其标签
kubectl get nodes --show-labels

# 输出示例:
# NAME          STATUS   ROLES           AGE   VERSION   LABELS
# k8s-master    Ready    control-plane   30m   v1.28.0   beta.kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-master,...
# k8s-node1     Ready    <none>          28m   v1.28.0   beta.kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-node1,...
# k8s-node2     Ready    <none>          27m   v1.28.0   beta.kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-node2,...

步骤2:创建DaemonSet

创建以下YAML文件(保存为daemonset-demo.yaml):

bash 复制代码
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-collector
  labels:
    app: node-collector
spec:
  selector:
    matchLabels:
      app: node-collector
  template:
    metadata:
      labels:
        app: node-collector
    spec:
      containers:
      - name: collector
        image: busybox:latest
        command:
        - sh
        - -c
        - |
          echo "Hello! I am running on \$(hostname)"
          echo "My pod name is \$HOSTNAME"
          echo "Node IP: \$NODE_IP"
          sleep 3600
        env:
        - name: NODE_IP
          valueFrom:
            fieldRef:
              fieldPath: status.hostIP
      terminationGracePeriodSeconds: 30
bash 复制代码
# 部署 DaemonSet
kubectl apply -f daemonset-demo.yaml

# 查看 DaemonSet 状态
kubectl get daemonset node-collector

# 输出示例:
# NAME             DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
# node-collector   2         2         2       2            2           <none>          30s

上面输出中,DESIRED为2表示集群中有2个符合条件的节点(Worker节点),控制器已经在每个节点上都运行了一个Pod。

步骤3:观察Pod分布

bash 复制代码
# 查看Pod的详细信息,注意它们分布在哪些节点上
kubectl get pods -o wide -l app=node-collector

# 输出示例:
# NAME                   READY   STATUS    RESTARTS   AGE   IP              NODE
# node-collector-6x8k2   1/1     Running   0          45s   10.244.166.130  k8s-node1
# node-collector-7j9ql   1/1     Running   0          45s   10.244.104.6    k8s-node2

可以看到每个节点上只运行了一个Pod,且Pod名称后面带有一个随机后缀(与Deployment类似,但Deployment会有两个随机后缀)。

步骤4:验证"新节点自动部署"特性

bash 复制代码
# 向某个节点添加标签(模拟新节点加入的场景)
kubectl label node k8s-master daemon=need --overwrite

# 检查 DaemonSet 的 Pod 数量是否增加
kubectl get pods -l app=node-collector -o wide

你会发现DESIRED和CURRENT数量都增加了1(但注意这会因nodeSelector的配置而异)。

步骤5:测试更新策略(滚动更新)

bash 复制代码
# 更新 DaemonSet 的镜像版本
kubectl set image daemonset/node-collector collector=busybox:1.36

# 观察滚动更新过程
kubectl rollout status daemonset/node-collector

# 查看更新历史
kubectl rollout history daemonset/node-collector

2.6 DaemonSet与Deployment的深度对比

虽然DaemonSet和Deployment都是Pod控制器,但它们在设计目标、调度机制和使用场景上有着根本性的不同:

调度机制的差异

  • Deployment依赖kube-scheduler进行调度决策。调度器会综合考虑节点的资源可用性、亲和性规则、污点容忍度等因素,为每个Pod选择最优的节点。Pod之间没有"必须分布在不同节点"的约束。

  • DaemonSet 直接跳过调度器。DaemonSet控制器遍历所有符合条件的节点,直接设置Pod的nodeName字段来将Pod绑定到特定节点。这保证了"每节点一个Pod"的严格分布。

生命周期管理的差异

  • 扩容Deployment时,新的Pod可以被调度到集群中任意有足够资源的节点上。缩容时,控制器可以删除任意Pod。

  • DaemonSet的"副本数"始终等于符合条件的节点数。当新节点加入时自动增加Pod;当节点被移除时自动删除对应的Pod。

更新行为的差异

  • Deployment的滚动更新基于maxSurgemaxUnavailable参数,允许多个Pod同时更新。

  • DaemonSet的滚动更新按节点逐个进行。在一个节点上的新Pod变为Ready后,才会开始更新下一个节点上的Pod。

2.7 OpenEuler系统下DaemonSet部署的特殊注意事项

在OpenEuler系统上部署DaemonSet时,有几点需要特别注意:

  1. 容器镜像兼容性:OpenEuler基于RPM包管理体系,默认使用glibc。部分Busybox或Alpine镜像基于musl libc构建,在x86环境下一般没问题,但在鲲鹏(ARM64)架构上要选择对应的ARM镜像。

  2. 资源限制合理配置 :OpenEuler系统资源管理较为严格,建议在DaemonSet的Pod模板中明确配置resources.requestsresources.limits,避免资源争抢。

  3. 主机路径挂载:在OpenEuler上使用hostPath挂载主机目录时,需要注意目录权限和SELinux(即使已禁用)的潜在影响。建议在测试环境充分验证路径的可读写性。

第三章 StatefulSet(有状态副本集)深入讲解

3.1 StatefulSet的核心概念

在前面的Deployment中,所有的Pod都是"平等"的------它们共享同一个Pod模板,名称随机生成(如nginx-7f9b8f4d9f-2xq8k),可以被任意删除和重建,它们之间的网络标识和存储也是不固定的。这种"无状态"的特性使得Deployment非常适合Web服务、API网关等场景,但在面对数据库、消息队列、分布式存储等"有状态"应用时,就捉襟见肘了。

什么是"有状态应用"? 有状态应用的核心特点是需要稳定的身份标识和持久化存储。举个例子:

  • MySQL主从集群:主库和从库身份不能混淆,数据不能丢失。主节点必须先于从节点启动。

  • ZooKeeper集群:每个节点有固定的编号(server-1、server-2、server-3......),重启后身份必须保持不变。

  • Kafka集群:每个Broker有唯一的ID,数据分区与特定Broker绑定。

StatefulSet正是专门为这些有状态应用设计的控制器。它提供三大核心保障:

  1. 稳定的Pod名称和网络标识 :Pod名称为固定的<StatefulSet名称>-<序号>格式,如web-0web-1,重启或重新调度后名称和HostName保持不变。

  2. 稳定的持久化存储 :通过volumeClaimTemplates为每个Pod创建独立的PersistentVolumeClaim(PVC),PVC名称固定为<volumeName>-<StatefulSetName>-<序号>格式,Pod无论被调度到哪里,始终绑定同一块存储卷。

  3. 有序部署和滚动更新:Pod按照从0到N-1的顺序依次创建和启动;更新时则按照从N-1到0的逆序滚动更新。

3.2 StatefulSet适用于哪些场景

应用类型 具体示例 使用StatefulSet的原因
关系型数据库 MySQL、PostgreSQL 主从复制需要固定的主节点身份,每个节点需要独立持久化存储
NoSQL数据库 MongoDB、Cassandra 每个数据节点有唯一标识,数据分片与节点绑定
消息队列 Kafka、RabbitMQ Broker ID固定,分区数据不能丢失
分布式协调 ZooKeeper、etcd 集群成员有固定ID,Leader选举依赖稳定的节点标识
搜索引擎 Elasticsearch 每个节点身份固定,数据分片位置稳定

3.3 StatefulSet的工作原理

StatefulSet控制器的核心工作流程包括以下步骤:

  1. 控制器根据spec.replicas确定目标Pod数量N

  2. 按照从0到N-1的顺序,依次创建Pod(例如先创建web-0,等其变为Running后再创建web-1

  3. 为每个Pod自动创建对应的PVC(如果有volumeClaimTemplates定义)

  4. 通过Headless Service为每个Pod注册独立的DNS记录

  5. 当进行滚动更新时,按照从N-1到0的逆序逐个更新Pod

  6. 缩容时,也是从最大序号开始逆序删除Pod

有序性保证:StatefulSet对于部署(deploy)、扩缩容(scale)和删除操作都保证严格的顺序性。具体来说:

  • 部署时 :Pod按照 0 → 1 → 2 → ... → N-1 的顺序依次启动,每个Pod必须达到Running和Ready状态后,下一个Pod才会开始创建。

  • 缩容时 :Pod按照 N-1 → N-2 → ... → 0 的逆序依次终止和删除,每个Pod完全终止后,下一个Pod才会开始终止。

  • 滚动更新时 :同样按照逆序(N-1 → 0)逐个更新Pod,保证在任意时刻都维持服务可用性。

这种有序性对于主从架构的数据库尤为重要:主节点(通常是序号最小的Pod)必须最先启动,最后更新,最后关闭。

3.4 Headless Service的深入理解

在正式介绍StatefulSet的YAML配置之前,必须深刻理解Headless Service(无头服务) 的概念和作用。

为什么需要Headless Service? 普通的ClusterIP Service提供负载均衡功能------当客户端访问 Service的ClusterIP时,流量会被随机分发到后端任意一个Pod。这对于无状态应用来说是完美的,但对于有状态应用来说却是问题:客户端可能需要连接到特定的Pod(比如MySQL的主节点mysql-0),而不是被随机分配到任意一个副本。

Headless Service通过设置spec.clusterIP: None来禁用负载均衡。当对Headless Service执行DNS查询时,DNS服务器不会返回单个虚拟IP,而是直接返回所有匹配Pod的IP地址列表;当使用具体Pod名称查询时(如web-0.nginx),DNS会返回该特定Pod的IP地址。

固定DNS名称模式:

bash 复制代码
<pod-name>.<headless-service-name>.<namespace>.svc.<cluster-domain>

例如:web-0.nginx.default.svc.cluster.local

这意味着集群中的任何组件都可以通过固定的DNS名称访问StatefulSet中的特定Pod,而不需要关心该Pod的IP地址是否发生了变化。

Headless Service YAML示例:

bash 复制代码
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None           # 核心配置:将 clusterIP 显式设置为 None
  selector:
    app: nginx               # 与 StatefulSet 中 Pod 的标签匹配

Headless Service虽然不提供负载均衡,但它仍然会创建Endpoints资源,跟踪所有匹配Pod的IP地址。这使得DNS服务器能够为每个Pod创建独立的A记录。

3.5 StatefulSet YAML详细解析

下面是一份完整的StatefulSet YAML配置(已包含Headless Service)及逐段讲解:

yaml

bash 复制代码
# =====================================================================
# 第一部分:Headless Service(无头服务)
# =====================================================================
apiVersion: v1
kind: Service
metadata:
  name: nginx                      # Service 名称,StatefulSet 会通过 serviceName 引用它
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None                  # 必须设置为 None,表示这是一个 Headless Service
  selector:
    app: nginx                     # 选择标签,必须与 StatefulSet 中 Pod 的标签匹配

---
# =====================================================================
# 第二部分:StatefulSet
# =====================================================================
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web                        # StatefulSet 名称,Pod 将命名为 web-0, web-1, ...
spec:
  serviceName: "nginx"             # 指定关联的 Headless Service 的名称(必须字段)
  replicas: 3                      # 副本数量,将创建 web-0, web-1, web-2 三个 Pod
  podManagementPolicy: OrderedReady # Pod 管理策略:OrderedReady(默认)或 Parallel

  # ---- Pod 选择器 ----
  selector:
    matchLabels:
      app: nginx

  # ---- Pod 模板 ----
  template:
    metadata:
      labels:
        app: nginx                 # Pod 标签,必须与 selector 和 Service 的 selector 匹配
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www                 # 引用下面 volumeClaimTemplates 中定义的卷名称
          mountPath: /usr/share/nginx/html   # 挂载到 Nginx 的静态文件目录

  # ---- PVC 模板:StatefulSet 独有的核心特性 ----
  volumeClaimTemplates:
  - metadata:
      name: www                     # PVC 名称前缀,实际 PVC 名为 www-web-0, www-web-1, ...
    spec:
      accessModes: [ "ReadWriteOnce" ]  # 单节点读写模式
      storageClassName: "standard"      # 存储类名称(根据实际集群 StorageClass 调整)
      resources:
        requests:
          storage: 1Gi              # 每个 Pod 申请 1GB 存储空间

  # ---- 更新策略 ----
  updateStrategy:
    type: RollingUpdate             # 滚动更新模式(默认)
    rollingUpdate:
      partition: 0                  # 分区更新参数,0 表示全量更新(详见下文)

关键字段详解

  1. spec.serviceName :StatefulSet必须关联一个Headless Service,此字段的值必须与Service的metadata.name一致。这是StatefulSet实现稳定网络标识的基础。

  2. spec.volumeClaimTemplates:这是StatefulSet与Deployment最大的区别之一。它是一个PVC模板数组,为每个Pod自动创建独立的PVC:

    • 每个被创建的Pod都会获得一个独立的PVC,PVC命名为<volumeName>-<StatefulSetName>-<序号>,如www-web-0

    • 当Pod被删除或重新调度到其他节点时,PVC不会被删除,新的Pod会自动绑定到原有的PVC

    • 删除StatefulSet时,PVC默认不会自动删除(保护数据安全),需要手动清理

  3. spec.podManagementPolicy

    • OrderedReady(默认) :Pod严格按照序号顺序启动和终止

    • Parallel:Pod可以并行启动和终止,不保证顺序

  4. spec.updateStrategy.rollingUpdate.partition:这是一个非常有用的金丝雀发布参数:

    • partition: 3 表示仅更新序号 >= 3 的Pod

    • 可以将部分Pod更新到新版本,验证无误后再降低partition值进行全量更新

    • 这是StatefulSet实现灰度发布的简洁方式

3.6 StatefulSet实战:在OpenEuler上部署MySQL

接下来我们通过部署一个MySQL实例来深入理解StatefulSet的实际用法。完整部署包含Secret(存储密码)、ConfigMap(存储配置)、Headless Service和StatefulSet。

步骤1:创建Secret存储MySQL密码

bash 复制代码
kubectl create namespace stateful-demo

kubectl create secret generic mysql-secret \
  --from-literal=root-password=MySecurePass123 \
  -n stateful-demo

步骤2:创建MySQL配置文件ConfigMap

bash 复制代码
# 保存为 mysql-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-config
  namespace: stateful-demo
data:
  my.cnf: |
    [mysqld]
    character-set-server=utf8mb4
    collation-server=utf8mb4_unicode_ci
    default-authentication-plugin=mysql_native_password
    bind-address=0.0.0.0
    max_connections=1000

步骤3:创建Headless Service

bash 复制代码
# 保存为 mysql-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: stateful-demo
  labels:
    app: mysql
spec:
  ports:
  - port: 3306
    name: mysql
  clusterIP: None
  selector:
    app: mysql

步骤4:创建StatefulSet

yaml

bash 复制代码
# 保存为 mysql-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
  namespace: stateful-demo
spec:
  serviceName: "mysql"
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: root-password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql          # MySQL 数据目录
        - name: mysql-config
          mountPath: /etc/mysql/conf.d       # 自定义配置目录
        resources:
          requests:
            cpu: 500m
            memory: 512Mi
          limits:
            cpu: "2"
            memory: 2Gi
        # 就绪探针:确认 MySQL 服务是否可用
        readinessProbe:
          exec:
            command:
            - bash
            - -c
            - mysqladmin ping -uroot -p$MYSQL_ROOT_PASSWORD
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
      volumes:
      - name: mysql-config
        configMap:
          name: mysql-config
  volumeClaimTemplates:
  - metadata:
      name: mysql-data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 10Gi

步骤5:部署并验证

bash 复制代码
# 按顺序部署
kubectl apply -f mysql-configmap.yaml
kubectl apply -f mysql-service.yaml
kubectl apply -f mysql-statefulset.yaml

# 观察 Pod 创建顺序(先 mysql-0,再 mysql-1,然后 mysql-2)
kubectl get pods -w -n stateful-demo

# 查看 PVC
kubectl get pvc -n stateful-demo
# 输出示例:
# NAME                  STATUS   VOLUME         CAPACITY   ACCESS MODES
# mysql-data-mysql-0    Bound    pvc-xxxxxx     10Gi       RWO
# mysql-data-mysql-1    Bound    pvc-yyyyyy     10Gi       RWO
# mysql-data-mysql-2    Bound    pvc-zzzzzz     10Gi       RWO

每个Pod都有自己独立的PVC和持久化存储卷,这保证了即使Pod被重新调度,MySQL数据也不会丢失。

3.7 验证StatefulSet的网络标识与持久化

(1)验证DNS稳定性

bash 复制代码
# 使用临时Pod进行DNS查询
kubectl run -it --rm debug --image=busybox --restart=Never -n stateful-demo -- nslookup mysql-1.mysql

# 预期输出包含完整的域名:
# Name: mysql-1.mysql.stateful-demo.svc.cluster.local
# Address: 10.244.x.x

(2)验证持久化存储

bash

bash 复制代码
# 在 mysql-0 中创建一个测试数据库
kubectl exec -it mysql-0 -n stateful-demo -- mysql -uroot -pMySecurePass123 -e "CREATE DATABASE test_stateful;"

# 验证数据库创建成功
kubectl exec -it mysql-0 -n stateful-demo -- mysql -uroot -pMySecurePass123 -e "SHOW DATABASES;"

# 删除 mysql-0 Pod(模拟故障)
kubectl delete pod mysql-0 -n stateful-demo

# StatefulSet 会自动重建 mysql-0,等待其 Running
kubectl get pods -w -n stateful-demo

# 再次验证数据库仍然存在(因为数据存储在持久卷上)
kubectl exec -it mysql-0 -n stateful-demo -- mysql -uroot -pMySecurePass123 -e "SHOW DATABASES;"
# 输出中仍然包含 test_stateful 数据库,证明数据已持久化

3.8 StatefulSet的扩缩容

StatefulSet的扩缩容操作严格遵循有序性原则:

bash 复制代码
# 扩容:从 3 增加到 5
kubectl scale statefulset mysql --replicas=5 -n stateful-demo
# 将依次创建 mysql-3 和 mysql-4

# 缩容:从 5 减少到 2
kubectl scale statefulset mysql --replicas=2 -n stateful-demo
# 将先删除 mysql-4,再删除 mysql-3,然后删除 mysql-2

重要提示 :缩容时,Pod会被删除,但PVC不会被自动删除。这是有意为之的设计------防止误操作导致数据永久丢失。如果需要清理不用的PVC,需要手动执行:

bash 复制代码
# 查看所有PVC
kubectl get pvc -n stateful-demo
# 手动删除不再需要的PVC
kubectl delete pvc mysql-data-mysql-4 -n stateful-demo

3.9 StatefulSet的更新策略与金丝雀发布

StatefulSet支持两种更新策略,通过spec.updateStrategy.type字段进行配置:

(1)RollingUpdate(滚动更新,默认)

逆序逐个更新Pod,保证在更新过程中始终有可用的副本:

bash 复制代码
updateStrategy:
  type: RollingUpdate
  rollingUpdate:
    partition: 0    # 0 表示全量更新

(2)OnDelete

仅当用户手动删除Pod后,控制器才会使用新的Pod模板重建:

bash 复制代码
updateStrategy:
  type: OnDelete

(3)金丝雀发布(Canary Rollout)

StatefulSet的分区更新(partition)机制是实现金丝雀发布的优雅方式:

bash 复制代码
# 假设当前总副本数为 5(mysql-0 到 mysql-4)
# 设置 partition=3,则只有 mysql-3 和 mysql-4 会更新
kubectl patch statefulset mysql -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":3}}}}' -n stateful-demo

# 验证 2 个高序号 Pod 确认无误后,逐步降低 partition
kubectl patch statefulset mysql -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":1}}}}' -n stateful-demo
# 此时 mysql-1、mysql-2、mysql-3、mysql-4 都会更新

# 确认全部正常后,最终全量更新
kubectl patch statefulset mysql -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":0}}}}' -n stateful-demo

这种机制对于数据库等有状态应用的风险控制尤为关键------可以先验证少数副本在新版本下的表现,确认稳定后再全面铺开。

第四章 三种控制器的综合对比

4.1 功能对比表

对比维度 Deployment DaemonSet StatefulSet
设计目标 管理无状态应用的水平扩展 在每个节点上运行守护进程 管理有状态应用,保证稳定身份和存储
Pod名称 随机生成(如nginx-7f9b8-2xq8k) 随机生成(如fluentd-abc123) 固定有序(如web-0、web-1、web-2)
副本数量 通过replicas显式指定 自动等于符合条件的节点数 通过replicas显式指定
网络标识 通过ClusterIP Service访问(随机负载均衡) 一般不单独暴露网络服务 通过Headless Service提供固定DNS
持久化存储 所有Pod共享PVC或使用emptyDir临时存储 通常使用hostPath挂载节点目录 每个Pod拥有独立的PVC
部署顺序 并行创建(无序) 并行创建(无序) 顺序创建(0→1→2→...→N-1)
缩容顺序 并行删除(无序) 随节点移除自动删除 逆序删除(N-1→N-2→...→0)
更新策略 RollingUpdate / Recreate RollingUpdate / OnDelete RollingUpdate(支持partition) / OnDelete
典型应用 Web服务、API网关、微服务 日志采集、监控、网络代理 数据库、消息队列、分布式存储

4.2 选型决策指南

面对不同的业务场景,如何选择合适的控制器?以下是一个简洁的决策流程图:

  1. 应用是否需要在每个节点上都运行一个实例?

    • 是 → 选择 DaemonSet(如日志采集Agent、监控Node Exporter、网络插件)
  2. 应用是否需要稳定的网络标识和持久化存储?

    • 是 → 选择 StatefulSet(如MySQL、Kafka、ZooKeeper、Elasticsearch)
  3. 应用是否"无状态",可以随意被部署到任意节点、任意删除重建?

    • 是 → 选择 Deployment(如Web前端、RESTful API、批处理任务)

第五章 故障排查与常见问题

5.1 DaemonSet Pod未在期望的节点上运行

问题现象:DaemonSet的DESIRED与CURRENT数量不匹配,部分节点上没有运行Pod。

排查步骤

bash 复制代码
# 1. 查看 DaemonSet 整体状态
kubectl describe daemonset <daemonset-name>

# 2. 查看节点上的Taint(污点)
kubectl describe node <node-name> | grep Taints

# 3. 查看节点标签是否符合 nodeSelector
kubectl get node <node-name> --show-labels

# 4. 查看DaemonSet Pod事件
kubectl describe pod <daemonset-pod-name>

常见原因

  • 节点存在DaemonSet Pod模板中没有容忍(Tolerations)的Taint

  • nodeSelector标签不匹配

  • 节点资源不足(CPU/内存不满足Pod的requests)

解决方法

  • 为DaemonSet添加对应的Tolerations

  • 调整nodeSelector或为节点添加正确的标签

  • 降低Pod的资源请求或增加节点资源

5.2 StatefulSet Pod一直处于Pending状态

问题现象:StatefulSet的Pod长时间处于Pending状态,无法变为Running。

排查步骤

bash 复制代码
# 1. 查看Pod状态和事件
kubectl describe pod <pod-name> -n <namespace>

# 2. 查看PVC绑定状态
kubectl get pvc -n <namespace>

# 3. 查看StorageClass是否存在
kubectl get storageclass

# 4. 查看PV状态
kubectl get pv

常见原因

  • PVC无法绑定到可用的PV(没有匹配的StorageClass或PV容量不足)

  • 前一个序号的Pod尚未变为Running(OrderedReady策略下)

  • 镜像拉取失败

解决方法

  • 确保集群中存在可用的StorageClass,且满足PVC的存储需求

  • 检查前序Pod的状态,确保其已正常启动

  • 检查镜像地址是否正确,镜像仓库是否可达

5.3 缩容后数据丢失

问题现象 :执行kubectl scale缩容StatefulSet后,PVC中的数据无法恢复。

说明:这不是Bug,而是StatefulSet的有意设计------缩容时不会自动删除PVC,以防误操作导致数据永久丢失。如果数据丢失,说明PVC被手动删除了。

最佳实践

  • 缩容前使用--cascade=orphan参数(需要手动处理Pod)

  • 缩容后手动保留PVC,确认不再需要后再手动删除

  • 为重要数据配置定期备份策略

5.4 OpenEuler系统特有问题的排查

在OpenEuler系统上,还需要额外关注以下几点:

  1. SELinux干扰 :即使已执行setenforce 0,某些情况下SELinux仍可能影响容器对hostPath的读写,需要在Pod的securityContext中增加privileged: true或调整SELinux标签。

  2. 内核模块加载 :OpenEuler默认可能未加载br_netfilter模块,需要手动执行modprobe br_netfilter后再使用lsmod | grep br_netfilter确认。

  3. 阿里云镜像源偶尔不可用:建议同时配置多个国内镜像源作为备份。

第六章 最佳实践总结

6.1 DaemonSet最佳实践

  1. 合理使用nodeSelector:避免在某些专用节点上部署不必要的DaemonSet,减少资源浪费。

  2. 配置合适的资源限制 :DaemonSet在每个节点上都运行,如果资源消耗过大,会挤占业务Pod的资源。建议为DaemonSet Pod配置合理的resources.limits

  3. 使用RollingUpdate策略:生产环境中推荐使用RollingUpdate而非OnDelete,减少人工介入,降低运维复杂度。

  4. 节点关键组件优先保障 :对于kube-proxy、CNI插件等关键DaemonSet,建议使用priorityClassName设置为system-node-critical,避免被驱逐。

  5. 注意hostPath的使用:hostPath与节点强绑定,如果Pod被调度到其他节点,将无法访问原来的hostPath数据。对于需要持久化的数据,应使用PV/PVC体系。

6.2 StatefulSet最佳实践

  1. 使用Headless Service暴露Pod:不要为有状态Pod创建ClusterIP Service来做负载均衡,应该使用Headless Service让客户端能够直接访问特定Pod。

  2. 配置合适的探针 :务必配置readinessProbe和livenessProbe。对于数据库类应用,使用exec探针检查服务是否真正可用,而非仅检查端口。

  3. 缩容前备份数据:在缩容前确认相关Pod的数据是否已备份或不再需要,避免误操作丢失重要数据。

  4. 使用PVC保留策略:了解集群中PVC的生命周期管理策略,必要时手动清理不再使用的PVC。

  5. 利用partition实现灰度发布:StatefulSet的分区更新机制是实现安全变更的利器,尤其适用于数据库等核心应用。

  6. 避免频繁变更:StatefulSet的每次变更都会触发有序的Pod重建。对于数据库等大型应用,每次重建可能代价较高(数据预热、连接重建等),变更前要充分评估影响范围。

  7. 合理设置podManagementPolicy :对于可以并行启动的有状态应用(如某些NoSQL数据库),可以设置podManagementPolicy: Parallel来加速部署和扩缩容过程。

6.3 OpenEuler环境下的补充建议

  1. 镜像选择:优先选择基于CentOS/RHEL系构建的容器镜像,与OpenEuler的兼容性更好。部分Alpine镜像在ARM64架构下可能有兼容问题。

  2. 定期更新系统:OpenEuler定期发布安全补丁,建议保持系统更新。

  3. 监控系统健康:使用DaemonSet部署的监控Agent(如Node Exporter)持续监控OpenEuler节点的系统健康状态,及时发现磁盘空间、内存泄漏等问题。

第七章 总结

本文基于OpenEuler 24.03 LTS SP3系统环境,系统性地讲解了Kubernetes中的DaemonSet和StatefulSet两种核心工作负载控制器。我们从概念原理讲到YAML配置,从实战部署讲到故障排查,力图覆盖从入门到进阶的完整知识体系。

回顾三种核心控制器的定位:

  • Deployment:无状态应用的"瑞士军刀",适合水平扩展的Web服务和微服务。

  • DaemonSet:集群节点的"忠诚哨兵",适合日志采集、监控、网络代理等节点级服务。

  • StatefulSet:有状态应用的"贴心管家",适合数据库、消息队列、分布式存储等需要稳定身份和持久化存储的应用。

掌握这三种控制器,意味着你已经能够应对Kubernetes中绝大多数的工作负载部署场景。理论是基础,实践才是最好的学习方式------建议读者在OpenEuler集群上完成本文中的所有实战练习,亲手创建、管理、排错DaemonSet和StatefulSet,从实践中建立真正的理解。

相关推荐
Dontla1 小时前
kubectl命令介绍(K8s命令行客户端)
云原生·容器·kubernetes
又来敲代码了2 小时前
k8s的部署
linux·运维·云原生·容器·kubernetes
炸裂狸花猫3 小时前
开源身份认证与访问管理平台 - Keycloak(二)
docker·云原生·容器·kubernetes·开源·keycloak·sso
D4c-lovetrain3 小时前
Linux个人心得29(k8s的一些个人理解)
linux·运维·kubernetes
炸裂狸花猫3 小时前
开源身份认证与访问管理平台 - Keycloak(一)
docker·云原生·kubernetes·开源·devops
gwjcloud3 小时前
Kubernetes从入门到精通(基础篇)02
云原生·容器·kubernetes
RkxI7soAM4 小时前
Ledger 硬件钱包在中国市场的竞争优势分析
决策树·贪心算法
布吉岛的石头4 小时前
云原生面试考点:K8s 核心组件 + Deployment 实战
云原生·面试·kubernetes
运维老郭5 小时前
Kubernetes Ingress Controller完全指南:7种选型对比+Istio集成+Gateway API迁移
运维·云原生·kubernetes