6. StatefulSet与DaemonSet

StatefulSet 与 DaemonSet 控制器

服务有/无状态

​ Deployment 并不能编排所有类型的应用,对无状态服务编排是非常容易的,但是对于有状态服务就无能为力了。我们需要先明白一个概念:什么是有
状态服务,什么是无状态服务。

  • 无状态服务(Stateless Service):该服务运行的实例不会在本地存储需要持久化的数据,并且多个实例对于同一个请求响应的结果是完全一致的,比如前面我们讲解的 WordPress 实例,我们是不是可以同时启动多个实例,但是我们访问任意一个实例得到的结果都是一样的吧?因为他唯一需要持久化的数据是存储在 MySQL 数据库中的,所以我们可以说 WordPress 这个应用是无状态服务,但是 MySQL 数据库就不是了,因为他需要把数据持久化到本地。
  • 有状态服务(Stateful Service):就和上面的概念是对立的了,该服务运行的实例需要在本地存储持久化数据,比如上面的 MySQL 数据库,你现在运行在节点 A,那么他的数据就存储在节点 A 上面的,如果这个时候你把该服务迁移到节点 B 去的话,那么就没有之前的数据了,因为他需要去对应的数据目录里面恢复数据,而此时没有任何数据。

​ 我们常见的 WEB 应用,是通过 Session 来保持用户的登录状态的,如果我们将 Session 持久化到节点上,那么该应用就是一个有状态的服务了,因为我现在登录进来你把我的 Session 持久化到节点 A 上了,下次我登录的时候可能会将请求路由到节点 B 上去了,但是节点 B 上根本就没有我当前的 Session数据,就会被认为是未登录状态了,这样就导致我前后两次请求得到的结果不一致了。所以一般为了横向扩展,我们都会把这类 WEB 应用改成无状态的服务,怎么改?将 Session 数据存入一个公共的地方,比如 Redis 里面,是不是就可以了,对于一些客户端请求 API 的情况,我们就不使用 Session 来保持用户状态,改成用 Token 也是可以的。

​ 无状态服务利用我们前面的 Deployment 可以很好的进行编排,对应有状态服务,需要考虑的细节就要多很多了,容器化应用程序最困难的任务之一,就是设计有状态分布式组件的部署体系结构。由于无状态组件没有预定义的启动顺序、集群要求、点对点 TCP 连接、唯一的网络标识符、正常的启动和终止要求等,因此可以很容易地进行容器化。诸如数据库,大数据分析系统,分布式 key/value 存储、消息中间件需要有复杂的分布式体系结构,都可能会用到上述功能。为此,Kubernetes 引入了 StatefulSet 这种资源对象来支持这种复杂的需求。StatefulSet 类似于 ReplicaSet,但是它可以处理 Pod 的启动顺序,为保留每个 Pod 的状态设置唯一标识,具有以下几个功能特性:

  • 稳定的、唯一的网络标识符
  • 稳定的、持久化的存储
  • 有序的、优雅的部署和缩放
  • 有序的、优雅的删除和终止
  • 有序的、自动滚动更新

概念

StatefulSet(有状态应用)

  • 用来管理 有状态应用(如数据库、Kafka、Zookeeper 等)。

  • 每个 Pod 有 固定的身份(网络标识)和存储卷

    • Pod 名字是有序号的,比如:mysql-0, mysql-1, mysql-2
    • 即使 Pod 重建,序号和绑定的存储卷(PVC)不会变。
  • 典型场景:

    • 数据库(MySQL、PostgreSQL)
    • 分布式协调系统(ZooKeeper、Etcd)
    • 消息队列(Kafka、RabbitMQ)
  • 特点:有顺序性、稳定性、持久性

DaemonSet(守护进程)

  • 保证 每个节点上都运行一个 Pod(或者符合选择条件的节点上)。

  • 常用于运行集群级别的 系统服务

  • 典型场景:

    • 日志收集(Fluentd, Logstash, Filebeat)
    • 节点监控(Node Exporter, Prometheus Agent)
    • 网络插件(Calico, Cilium)
    • 存储代理(Ceph agent, Glusterd)
  • 特点:每个节点都部署,不是可伸缩副本,而是守护进程

StatefulSet

Headless Service

​ 在我们学习 StatefulSet 对象之前,我们还必须了解一个新的概念:Headless Service。Service 其实在之前我们和大家提到过,Service 是应用服务的抽象,通过 Labels 为应用提供负载均衡和服务发现,每个 Service 都会自动分配一个 cluster IP 和 DNS 名,在集群内部我们可以通过该地址或者通过 FDQN(域名) 的形式来访问服务。

​ 在集群中我们是通过部署 CoreDNS 组件来为 Kubernetes 集群提供 DNS 服务的,CoreDNS 同样通过 watch 到APIServer 中的新服务来为每个服务创建一组 DNS 记录。如果在整个集群中都启用了 DNS,则所有 Pod 都应该能够通过其 DNS 名称自动解析服务。

​ 例如,如果你在 Kubernetes 命名空间 my-ns 中有一个名为 my-service 的 Service 服务,则控制平面和CoreDNS 会其创建一个 my-service.my-ns 的 DNS 记录(全路径为 my-service.myns.svc.cluster.local),my-ns 命名空间中的 Pod 应该能够通过名称 my-service 来找到服务(当然 myservice.my-ns 也可以工作)。而其他命名空间中的 Pod 必须将名称限定为 my-service.my-ns,这些名称将解析为为 Service 服务分配的 cluster IP,然后该 Service 就会将请求代理到其关联的 Endpoints 列表中的某个Pod 上去了,所以 Service 这里的作用有点类似于 Nginx 代理。

验证:

# 一键列出所有 Service 的解析
ubuntu@ubuntu:~$ kubectl get svc -A -o jsonpath='{range .items[*]}{.metadata.name}.{.metadata.namespace}.svc.cluster.local{"\t"}{.spec.clusterIP}{"\n"}{end}'
kubernetes.default.svc.cluster.local	10.96.0.1
kube-dns.kube-system.svc.cluster.local	10.96.0.10
# 注意这个是 ClusterIP 是虚拟 IP, 真实 Pod/节点 IP 是 Endpoint
# ClusterIP 它只在 集群内部生效,用于 Pod 之间访问 Service。
# ClusterIP 是由 kube-proxy 维护的 iptables/ipvs 规则来实现流量转发。
# Endpoint 是实际运行 API Server 的节点 IP + 端口。
# Pod/Service 发请求到 ClusterIP → kube-proxy → 真实 IP。

sudo conntrack -L | grep 10.96.0.1
KUBE-SVC-NPX46M4PTMTKRN6Y  0.0.0.0/0 → 10.96.0.1:443
KUBE-MARK-MASQ             !10.244.0.0/16 → 10.96.0.1:443
# 可以看到 KUBE-SVC-* → 捕获 ClusterIP 流量
# KUBE-MARK-MASQ → 标记需要做源地址伪装(SNAT)的流量
# 查看连接跟踪(conntrack)
sudo apt install conntrack
ubuntu@ubuntu:~$ sudo conntrack -L | grep 10.96.0.1
tcp      6 86392 ESTABLISHED src=192.168.236.101 dst=10.96.0.1 sport=54690 dport=443 src=192.168.236.101 dst=192.168.236.101 sport=6443 dport=19777 [ASSURED] mark=0 use=1
tcp      6 86393 ESTABLISHED src=10.244.0.19 dst=10.96.0.1 sport=60870 dport=443 src=192.168.236.101 dst=10.244.0.19 sport=6443 dport=60870 [ASSURED] mark=0 use=1
tcp      6 86399 ESTABLISHED src=10.244.0.20 dst=10.96.0.1 sport=50188 dport=443 src=192.168.236.101 dst=10.244.0.20 sport=6443 dport=50188 [ASSURED] mark=0 use=1
# 可以看到发往了 192.168.236.101 443 接口


# 一键列出所有 Pod 的解析
kubectl get pods -A -o jsonpath='{range .items[*]}{.status.podIP}{":\t"}{.metadata.name}.{.metadata.namespace}.pod.cluster.local{"\n"}{end}' \
| sed 's/\./-/g; s/:/\t/'


# 启动临时测试工具
kubectl run dnsutils \
  --image=tutum/dnsutils \
  --restart=Never \
  -it --rm -- sh
# 执行测试
nslookup kubernetes.default.svc.cluster.local
Server:		10.96.0.10
Address:	10.96.0.10#53

Name:	kubernetes.default.svc.cluster.local
Address: 10.96.0.1

对于 DNS 的使用实际上也有两种情况:

  • 第一种就是普通的 Service,我们访问 my-service.my-ns.svc.cluster.local 的时候是通过集群中的 DNS服务解析到的 my-service 这个 Service 的 cluster IP
  • 第二种情况就是 Headless Service,对于这种情况,我们访问 my-service.my-ns.svc.cluster.local 的时候是直接解析到的 my-service 代理的某一个具体的 Pod 的 IP 地址,中间少了 cluster IP 的转发,这就是二者的最大区别,Headless Service 不需要分配一个 IP,kube-proxy 不会处理它们,而且平台也不会为它们进行负载均衡和路由,而是可以直接以 DNS 的记录方式解析到后面的 Pod 的 IP 地址

比如我们定义一个如下的 Headless Service:

# headless-svc.yaml
apiVersion: v1
# 类型
kind: Service
metadata:
  name: nginx
  namespace: default
  labels:
    app: nginx
spec:
  ports:
    - name: http
      port: 80
  # 没有clusterIP
  clusterIP: None
  selector:
    app: nginx

​ 实际上 Headless Service 在定义上和普通的 Service 几乎一致, 只是他配置的 clusterIP=None,所以,这个Service 被创建后并不会被分配一个 cluster IP,而是会以 DNS 记录的方式暴露出它所代理的 Pod,而且还有一个非常重要的特性,对于 Headless Service 所代理的所有 Pod 的 IP 地址都会绑定一个如下所示的 DNS 记录:

<service-name>.<namespace>.svc.cluster.local

​ 这个 DNS 记录正是 Kubernetes 集群为 Pod 分配的一个唯一标识,只要我们知道 Pod 的名字,以及它对应的Service 名字,就可以组装出这样一条 DNS 记录访问到 Pod 的 IP 地址,这个能力是非常重要的,接下来我们就来看下 StatefulSet 资源对象是如何结合 Headless Service 提供服务的。

验证:

ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl get svc -A
NAMESPACE     NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
default       kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP                  7d23h
default       nginx        ClusterIP   None         <none>        80/TCP                   39s
kube-system   kube-dns     ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   7d23h

# 启动临时测试工具
kubectl run dnsutils \
  --image=tutum/dnsutils \
  --restart=Never \
  -it --rm -- sh
# 执行测试
# nslookup nginx.default.svc.cluster.local
Server:		10.96.0.10
Address:	10.96.0.10#53

** server can't find nginx.default.svc.cluster.local: NXDOMAIN
# 因为暂时没有Pod在运行这个service下

# 先去执行一下下面的pod.yaml
ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl get pods -A -o wide
NAMESPACE      NAME                             READY   STATUS    RESTARTS       AGE     IP                NODE     NOMINATED NODE   READINESS GATES
default        nginx-deploy-f576985cc-l6gkt     1/1     Running   0              104s    10.244.1.36       node2    <none>           <none>
default        nginx-deploy-f576985cc-l9lwb     1/1     Running   0              104s    10.244.2.59       node1    <none>           <none>
default        nginx-deploy-f576985cc-tw844     1/1     Running   0              104s    10.244.1.35       node2    <none>           <none>

# 在执行 dns 解析
# nslookup nginx.default.svc.cluster.local
Server:		10.96.0.10
Address:	10.96.0.10#53

Name:	nginx.default.svc.cluster.local
Address: 10.244.1.36
Name:	nginx.default.svc.cluster.local
Address: 10.244.2.59
Name:	nginx.default.svc.cluster.local
Address: 10.244.1.35
# 成功解析了
# nginx-headless.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deploy
  namespace: default
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:alpine
          ports:
            - containerPort: 80

StatefulSet

​ 在开始之前,我们先准备两个 1G 的存储卷(PV),如果你使用的是 Kind 搭建的集群,则可以忽略该步骤,因为Kind 搭建的集群会自动提供一个 local-path-provisioner 组件,该组件会自动生成 PV

ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl get pods -n local-path-storage
No resources found in local-path-storage namespace.

创建PV:

# pv.yaml
# PersistentVolume 示例 1
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv001   # PV 名称
spec:
  capacity:
    storage: 1Gi  # PV 容量
  accessModes:
    - ReadWriteOnce  # 访问模式:单个节点可读写
  hostPath:
    path: /tmp/pv001  # 节点上的存储路径

---

# PersistentVolume 示例 2
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv002
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /tmp/pv002

创建:

ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl apply -f ./pv.yaml 
persistentvolume/pv001 created
persistentvolume/pv002 created
ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl get pv
NAME    CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pv001   1Gi        RWO            Retain           Available                          <unset>                          21s
pv002   1Gi        RWO            Retain           Available                          <unset>                          21s
ubuntu@ubuntu:~/example/stateful_set_daemon_set$ 

然后接下来声明一个如下所示的 StatefulSet 资源清单:

# nginx-sts.yaml
# Headless Service,供 StatefulSet 使用
apiVersion: v1
kind: Service
metadata:
  name: nginx          # Service 名称
  namespace: default   # 所在命名空间
  labels:
    app: nginx         # 标签,可用于选择器
spec:
  clusterIP: None      # Headless Service,不分配 ClusterIP
  selector:
    app: nginx         # 选择标签匹配的 Pod
  ports:
    - name: http       # 端口名称
      port: 80         # Service 对外端口

---

# StatefulSet,管理有状态的 nginx Pod
apiVersion: apps/v1
# StatefulSet 管理的是 有状态的 Pod ,命名规则为:<statefulset-name>-<ordinal> 
kind: StatefulSet
metadata:
  name: web             # StatefulSet 名称
  namespace: default
spec:
  serviceName: nginx    # 关联的 Headless Service 名称
  replicas: 2           # Pod 副本数
  selector:
    matchLabels:
      app: nginx        # StatefulSet 选择器,匹配 Pod 标签
  template:
    metadata:
      labels:
        app: nginx      # Pod 标签,必须和 selector 匹配
    spec:
      containers:
        - name: nginx
          image: nginx:1.7.9
          ports:
            - name: web
              containerPort: 80
          volumeMounts:
            - name: www        # 挂载卷名称
              mountPath: /usr/share/nginx/html  # Pod 内挂载路径
  volumeClaimTemplates:         # 为每个 Pod 创建独立 PVC
    - metadata:
        name: www              # PVC 名称
      spec:
        accessModes: ["ReadWriteOnce"]  # PVC 访问模式
        resources:
          requests:
            storage: 1Gi       # PVC 请求存储大小

​ 从上面的资源清单中可以看出和我们前面的 Deployment 基本上也是一致的,也是通过声明的 Pod 模板来创建 Pod
的,另外上面资源清单中和 volumeMounts 进行关联的不是 volumes 而是一个新的属性:volumeClaimTemplates,该属性会自动创建一个 PVC 对象,其实这里就是一个 PVC 的模板,和 Pod 模板类似,PVC 被创建后会自动去关联当前系统中和他合适的 PV 进行绑定。除此之外,还多了一个 serviceName: "nginx"的字段,serviceName 就是管理当前 StatefulSet 的服务名称,该服务必须在 StatefulSet 之前存在(实际测试并不是必须存在,但是如果要做 DNS 解析则必须存在),Pod 会遵循以下格式获取 DNS/主机名:pod-specificstring.serviceName..svc.cluster.local,其中 pod-specific-string 由 StatefulSet 控制器管理。

验证:

ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl apply -f ./nginx-sts.yaml 
service/nginx created
statefulset.apps/web created
^Cubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl get pods -l app=nginx
NAME    READY   STATUS    RESTARTS   AGE
web-0   1/1     Running   0          5m31s
web-1   1/1     Running   0          5m31s
# 直接查看host
ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl exec web-0 -- hostname
web-0
ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl exec web-1 -- hostname
web-1
ubuntu@ubuntu:~/example/stateful_set_daemon_set$

StatefulSet 中 Pod 副本的创建会按照序列号升序处理,副本的更新和删除会按照序列号降序处理。

​ 由于我们这里用 volumeClaimTemplates 声明的模板是挂载点的方式,并不是 volume,所有实际上上当于把 PV 的存储挂载到容器中,所以会覆盖掉容器中的数据,在容器启动完成后我们可以手动在 PV 的存储里面新建 index.html 文件来保证容器的正常访问,当然也可以进入到容器中去创建,这样更加方便:

for i in 0 1; do kubectl exec web-$i -- sh -c 'echo hello $(hostname) > /usr/share/nginx/html/index.html'; done


# 或者去目录修改
ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl get pv -o wide
NAME    CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM               STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE   VOLUMEMODE
pv001   1Gi        RWO            Retain           Bound    default/www-web-0                  <unset>                          18m   Filesystem
pv002   1Gi        RWO            Retain           Bound    default/www-web-1                  <unset>                          18m   Filesystem
# 两个pv 都挂上了 然后对pod describe 一下
ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl describe pv pv001
Name:            pv001
Labels:          <none>
Annotations:     pv.kubernetes.io/bound-by-controller: yes
Finalizers:      [kubernetes.io/pv-protection]
StorageClass:    
Status:          Bound
Claim:           default/www-web-0
Reclaim Policy:  Retain
Access Modes:    RWO
VolumeMode:      Filesystem
Capacity:        1Gi
Node Affinity:   <none>
Message:         
Source:
    Type:          HostPath (bare host directory volume)
    Path:          /tmp/pv001
    HostPathType:  
Events:            <none>
# pv002 一样。
# 然后看pod 的地址
ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl get pods -l app=nginx -o wide
NAME    READY   STATUS    RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATES
web-0   1/1     Running   0          17m   10.244.1.37   node2   <none>           <none>
web-1   1/1     Running   0          17m   10.244.2.60   node1   <none>           <none>

# 去 node2 看 /tmp/pv001 
# 去 node1 看 /tmp/pv002
# 然后可以直接修改

​ 现在我们创建一个 busybox(该镜像中有一系列的测试工具)的容器,在容器中用 DNS 的方式来访问一下这个Headless Service,由于我们这里只是单纯的为了测试,所以没必要写资源清单文件来声明,用 kubectl run 命令启动一个测试的容器即可:

ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl run -it --image busybox:1.28.3 test --restart=Never --rm /bin/sh
If you don't see a command prompt, try pressing enter.
/ # nslookup nginx
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      nginx
Address 1: 10.244.1.37 web-0.nginx.default.svc.cluster.local
Address 2: 10.244.2.60 web-1.nginx.default.svc.cluster.local

/ # ping nginx
PING nginx (10.244.1.37): 56 data bytes
64 bytes from 10.244.1.37: seq=0 ttl=62 time=0.646 ms
64 bytes from 10.244.1.37: seq=1 ttl=62 time=0.320 ms
64 bytes from 10.244.1.37: seq=2 ttl=62 time=0.961 ms
^C
--- nginx ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.320/0.642/0.961 ms
/ # ping nginx
PING nginx (10.244.2.60): 56 data bytes
64 bytes from 10.244.2.60: seq=0 ttl=64 time=0.040 ms
64 bytes from 10.244.2.60: seq=1 ttl=64 time=0.337 ms
64 bytes from 10.244.2.60: seq=2 ttl=64 time=0.104 ms
^C
--- nginx ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.040/0.160/0.337 ms
/ # ping nginx
PING nginx (10.244.1.37): 56 data bytes
64 bytes from 10.244.1.37: seq=0 ttl=62 time=0.355 ms
64 bytes from 10.244.1.37: seq=1 ttl=62 time=1.120 ms
64 bytes from 10.244.1.37: seq=2 ttl=62 time=0.625 ms
^C
--- nginx ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.355/0.700/1.120 ms
/ # nslookup web-0.nginx
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 10.244.1.37 web-0.nginx.default.svc.cluster.local
/ # nslookup web-1.nginx
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 10.244.2.60 web-1.nginx.default.svc.cluster.local
/ # 

# 可以看到解析 web-0.nginx 的时候解析到了 web-0 这个 Pod 的 IP,web-1.nginx 解析到了 web-1 这个 Pod
# 的 IP,而且这个 DNS 地址还是稳定的,因为 Pod 名称就是固定的,比如我们这个时候去删掉 web-0 和 web-1 这两
# 个 Pod:

删除测试

ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted
ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl get pods -l app=nginx
NAME    READY   STATUS    RESTARTS   AGE
web-0   1/1     Running   0          11s
web-1   1/1     Running   0          10s
ubuntu@ubuntu:~/example/stateful_set_daemon_set$ 

# 还是刚才那个 启动的 busybox
/ # nslookup web-1.nginx
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 10.244.2.62 web-1.nginx.default.svc.cluster.local
/ # nslookup web-0.nginx
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 10.244.1.38 web-0.nginx.default.svc.cluster.local
/ # 
# 中间 发现没有curl 
ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl run curlpod --image=curlimages/curl --restart=Never  -it --rm -- sh 
If you don't see a command prompt, try pressing enter.
~ $ curl 10.244.1.38
hello web-0
~ $ curl 10.244.2.62
hello web-1
~ $ 

​ 可以看到 StatefulSet 控制器仍然会安装顺序创建出两个 Pod 副本出来,而且 Pod 的唯一标识依然没变,所以这两个Pod 的网络标识还是固定的,我们依然可以通过 web-0.nginx 去访问到 web-0 这个 Pod,虽然 Pod 已经重建了,对应 Pod IP 已经变化了,但是访问这个 Pod 的地址(DNS)依然没变。

管理策略

​ 对于某些分布式系统来说,StatefulSet 的顺序性保证是不必要和/或者不应该的,这些系统仅仅要求唯一性和身份标志。为了解决这个问题,我们只需要在声明 StatefulSet 的时候重新设置 spec.podManagementPolicy 的策略即可。

​ 默认的管理策略是 OrderedReady,表示让 StatefulSet 控制器遵循上文演示的顺序性保证。除此之外,还可以设置为 Parallel 管理模式,表示让 StatefulSet 控制器并行的终止所有 Pod,在启动或终止另一个 Pod 前,不必等待这些Pod 变成 Running 和 Ready 或者完全终止状态。

更新策略

​ 前面课程中我们学习了 Deployment 的升级策略,在 StatefulSet 中同样也支持两种升级策略:onDeleteRollingUpdate,同样可以通过设置 .spec.updateStrategy.type 进行指定。

  • OnDelete : 该策略表示当更新了 StatefulSet 的模板后,只有手动删除旧的 Pod 才会创建新的 Pod。
  • RollingUpdate:该策略表示当更新 StatefulSet 模板后会自动删除旧的 Pod 并创建新的 Pod,如果更新发生了错误,这次“滚动更新”就会停止。不过需要注意 StatefulSet 的 Pod 在部署时是顺序从 0~n 的,而在滚动更新时,这些 Pod 则是按逆序的方式即 n~0 一次删除并创建。

修改 spec.podManagementPolicy:

# nginx-sts.yaml
# Headless Service,供 StatefulSet 使用
apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: default
  labels:
    app: nginx
spec:
  clusterIP: None
  selector:
    app: nginx
  ports:
    - name: http
      port: 80

---

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
  namespace: default
spec:
  # 修改这里!!!
  updateStrategy:
    type: OnDelete 
    # type: RollingUpdate
  serviceName: nginx
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.7.9
          ports:
            - name: web
              containerPort: 80
          volumeMounts:
            - name: www
              mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
    - metadata:
        name: www
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 1Gi

验证:

# 一个终端执行
kubectl get pod -l app=nginx -w
# 一个终端 修改nginx 版本 1.7.9 和 latest 来回切换 观察


# OnDelete 观察结果
ubuntu@ubuntu:~$ kubectl get pod -l app=nginx -w
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          1s
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          1s
# 并没有发生更新
# 终端执行 
kubectl delete pod web-0
kubectl delete pod web-1
# 观察结果 
ubuntu@ubuntu:~$ kubectl get pod -l app=nginx -w
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          1s
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          1s
web-0   1/1     Terminating         0          5m26s
web-0   0/1     Completed           0          5m26s
web-0   0/1     Completed           0          5m27s
web-0   0/1     Completed           0          5m27s
web-0   0/1     Pending             0          0s
web-0   0/1     Pending             0          0s
web-0   0/1     ContainerCreating   0          0s
web-1   1/1     Terminating         0          5m26s
web-1   0/1     Completed           0          5m26s
web-0   1/1     Running             0          1s
web-1   0/1     Completed           0          5m27s
web-1   0/1     Completed           0          5m27s
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          1s
# 查看版本 发现更新了
ubuntu@ubuntu:~$ kubectl describe pod web-0 
...
Containers:
  nginx:
    Container ID:   containerd://f40db34c192af90647dab835edbd30b21418bc671e2d744dcc9500b340a04cae
    Image:          nginx:latest
    Image ID:       docker.io/library/nginx@sha256:d5f28ef21aabddd098f3dbc21fe5b7a7d7a184720bc07da0b6c9b9820e97f25e
    Port:           80/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Fri, 12 Sep 2025 01:52:04 +0000
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
...


# 换成滚动更新观察 自动更新了
ubuntu@ubuntu:~$ kubectl get pod -l app=nginx -w
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          0s
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          1s
web-1   1/1     Terminating         0          21s
web-1   0/1     Completed           0          22s
web-1   0/1     Completed           0          22s
web-1   0/1     Completed           0          22s
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          1s
web-0   1/1     Terminating         0          23s
web-0   0/1     Completed           0          23s
web-0   0/1     Completed           0          24s
web-0   0/1     Completed           0          24s
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          1s

分段更新(没事别用这个,了解即可)

​ 另外 StatefulSet 的滚动升级还支持 Partitions的特性,可以通过.spec.updateStrategy.rollingUpdate.partition 进行设置,在设置 partition 后,StatefulSet 的 Pod中序号大于或等于 partition 的 Pod 会在 StatefulSet 的模板更新后进行滚动升级,而其余的 Pod 保持不变,这个功能是不是可以实现灰度发布?

# nginx-sts-parts.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: default
  labels:
    app: nginx
spec:
  ports:
    - name: http
      port: 80
  clusterIP: None
  selector:
    app: nginx

---

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
  namespace: default
spec:
  serviceName: nginx
  replicas: 4
  updateStrategy:
    type: RollingUpdate
    # updateStrategy.rollingUpdate.partition: 2
    # 表示 从 Pod web-3 开始更新,直到 web-2。
    # web-0 和 web-1 会保持旧版本不动。
    rollingUpdate:
      partition: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.7.9
          ports:
            - name: web
              containerPort: 80
          volumeMounts:
            - name: www
              mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
    - metadata:
        name: www
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 1Gi

验证:

ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl apply -f ./nginx-sts-parts.yaml 
service/nginx created
statefulset.apps/web created
# 修改版本并应用
ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl apply -f ./nginx-sts-parts.yaml 
service/nginx unchanged
statefulset.apps/web configured

# 观察结果
ubuntu@ubuntu:~$ kubectl get pod -l app=nginx -w
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          1s
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          1s
web-2   0/1     Pending             0          0s
web-2   0/1     Pending             0          0s
web-2   0/1     Pending             0          97s
web-2   0/1     ContainerCreating   0          97s
web-2   1/1     Running             0          98s
web-3   0/1     Pending             0          0s
web-3   0/1     Pending             0          0s
web-3   0/1     Pending             0          0s
web-3   0/1     ContainerCreating   0          0s
web-3   1/1     Running             0          1s
# 这里开始滚动更新
web-3   1/1     Terminating         0          92s
web-3   0/1     Completed           0          92s
web-3   0/1     Completed           0          93s
web-3   0/1     Completed           0          93s
web-3   0/1     Pending             0          0s
web-3   0/1     Pending             0          0s
web-3   0/1     ContainerCreating   0          0s
web-3   1/1     Running             0          1s
web-2   1/1     Terminating         0          3m12s
web-2   0/1     Completed           0          3m13s
web-2   0/1     Completed           0          3m13s
web-2   0/1     Completed           0          3m13s
web-2   0/1     Pending             0          0s
web-2   0/1     Pending             0          0s
web-2   0/1     ContainerCreating   0          0s
web-2   1/1     Running             0          1s


​ 在实际的项目中,其实我们还是很少会去直接通过 StatefulSet 来部署我们的有状态服务的,除非你自己能够完全能够hold 住,对于一些特定的服务,我们可能会使用更加高级的 Operator 来部署,比如 etcd-operator、prometheusoperator等等,这些应用都能够很好的来管理有状态的服务,而不是单纯的使用一个 StatefulSet 来部署一个 Pod就行,因为对于有状态的应用最重要的还是数据恢复、故障转移等等。

DaemonSet

​ 通过该控制器的名称我们可以看出它的用法:Daemon,就是用来部署守护进程的,DaemonSet用于在每个 Kubernetes节点中将守护进程的副本作为后台进程运行,说白了就是在每个节点部署一个 Pod 副本,当节点加入到 Kubernetes 集群中,Pod 会被调度到该节点上运行,当节点从集群只能够被移除后,该节点上的这个 Pod 也会被移除,当然,如果我们删除 DaemonSet,所有和这个对象相关的 Pods 都会被删除。那么在哪种情况下我们会需要用到这种业务场景呢?其实这种场景还是比较普通的,比如:

  • 集群存储守护程序,如 glusterd、ceph 要部署在每个节点上以提供持久性存储;
  • 节点监控守护进程,如 Prometheus 监控集群,可以在每个节点上运行一个 **node-exporter ** 进程来收集监控节点的信息;
  • 日志收集守护程序,如 fluentd 或 logstash,在每个节点上运行以收集容器的日志
  • 节点网络插件,比如 flannel、calico,在每个节点上运行为 Pod 提供网络服务。

​ 这里需要特别说明的一个就是关于 DaemonSet 运行的 Pod 的调度问题,正常情况下,Pod 运行在哪个节点上是由Kubernetes 的调度器策略来决定的,然而,由 DaemonSet 控制器创建的 Pod 实际上提前已经确定了在哪个节点上了(Pod 创建时指定了.spec.nodeName),所以:

  • DaemonSet 并不关心一个节点的 **unshedulable ** 字段,这个我们会在后面的调度章节和大家讲解的。

  • DaemonSet 可以创建 Pod,即使调度器还没有启动。

配置:

# nginx-ds.yaml
# 这是一个 DaemonSet 的定义文件,它会在集群中每个节点上运行一个 nginx Pod
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: nginx-ds
  namespace: default
spec:
  selector:
    matchLabels:
      k8s-app: nginx
  template:
    metadata:
      labels:
        k8s-app: nginx
    spec:
      containers:
        - image: nginx:1.7.9
          name: nginx
          ports:
            - name: http
              containerPort: 80

验证:

ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl get nodes
NAME     STATUS   ROLES           AGE   VERSION
master   Ready    control-plane   8d    v1.33.4
node1    Ready    <none>          8d    v1.33.4
node2    Ready    <none>          8d    v1.33.4
ubuntu@ubuntu:~/example/stateful_set_daemon_set$ kubectl get pods -l k8s-app=nginx -o wide
NAME             READY   STATUS    RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATES
nginx-ds-gk57f   1/1     Running   0          33s   10.244.2.73   node1   <none>           <none>
nginx-ds-zclfd   1/1     Running   0          33s   10.244.1.50   node2   <none>           <none>
ubuntu@ubuntu:~/example/stateful_set_daemon_set$ 

​ 集群中的 Pod 和 Node 是一一对应的,而 DaemonSet 会管理全部机器上的 Pod 副本,负责对它们进行更新和删除。

​ 那么,DaemonSet 控制器是如何保证每个 Node 上有且只有一个被管理的 Pod 呢?

  • 首先控制器从 Etcd 获取到所有的 Node 列表,然后遍历所有的 Node。

  • 根据资源对象定义是否有调度相关的配置,然后分别检查 Node 是否符合要求。

  • 在可运行 Pod 的节点上检查是否已有对应的 Pod,如果没有,则在这个 Node 上创建该 Pod;如果有,并且数量大于 1,那就把多余的 Pod 从这个节点上删除;如果有且只有一个 Pod,那就说明是正常情况。

​ 实际上当我们学习了资源调度后,我们也可以自己用 Deployment 来实现 DaemonSet 的效果,这里我们明白DaemonSet 如何使用的即可,当然该资源对象也有对应的更新策略,有 OnDelete 和 RollingUpdate 两种方式,默认是滚动更新。

posted @ 2025-09-12 10:19  beamsoflight  阅读(9)  评论(0)    收藏  举报