资料总结 资料总结
首页
go
java
云原生
  • mysql
  • redis
  • MongoDB
  • 设计模式详解
  • 数据结构与算法
  • 前端
  • 项目
  • 理论基础
  • 运营
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

linghui Wu

一只努力学飞的鱼
首页
go
java
云原生
  • mysql
  • redis
  • MongoDB
  • 设计模式详解
  • 数据结构与算法
  • 前端
  • 项目
  • 理论基础
  • 运营
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 01-云原生概念
  • docker

  • k8s

    • 基础介绍
    • 安装
    • 命令详解
    • Pod配置以及生命周期
    • Pod调度
    • Pod控制器
    • Service详解
    • 数据存储
    • 安全
    • 网络
    • 调度框架
    • Helm包管理

    • k8s实战部署

      • 部署k8s1.24版本
      • Harbor部署
      • StatefulSet讲解
        • 1. 拓扑状态
        • 2. 存储状态
        • 3. 更新策略
      • wordpress实战部署
      • k8s部署IM项目-springCloud
    • 总结-实战与基本原理
  • 监控

  • 日志

  • DevOps

  • ServiceMesh

  • Knative
  • spring-cloud-kubernetes
  • ms总结
  • 架构升级踩坑之路
  • Kubernetes
  • k8s
  • k8s实战部署
wulinghui
2023-01-26
目录

StatefulSet讲解

# 通过StatefulSet支持有状态应用

Deployment认为一个应用中所有的Pod是完全一样的,所以他们之间没有顺序,也无所谓运行在哪台宿主机上。需要的时候,Deployment就可以通过Pod模板创建新的Pod,不需要的时候,Deployment就可以"杀掉"任意一个Pod。

在实际的场景中,并不是所有的应用都可以满足这样的要求。

比如数据存储类应用,它的多个实例,往往都会在本地磁盘上保存一份数据。而这些实例一旦被杀掉,即便重建出来,实例与数据之间的对应关系也已经丢失,从而导致应用失败。

比如分布式应用,它的多个实例之间,往往有依赖关系,比如:主从关系、主备关系

这种实例之间有不对等关系,以及实例对外部数据有依赖关系的应用,就被称为“有状态 应用”(Stateful Application)。

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

比如我们常见的 WEB 应用,是通过 Session 来保持用户的登录状态的,如果我们将 Session 持久化到节点上,那么该应用就是一个有状态的服务了。

得益于“控制器模式”的设计思想,Kubernetes 项目很早就在 Deployment 的基础上,扩展 出了对“有状态应用”的初步支持。这个编排功能,就是:StatefulSet。

StatefulSet 的设计其实非常容易理解。它把真实世界里的应用状态,抽象为了两种情况:

  1. 拓扑状态。这种情况意味着,应用的多个实例之间不是完全对等的关系。这些应用实例,必须按照某些顺序启动,比如应用的主节点 A 要先于从节点 B 启动。而如果你把 A 和 B 两个 Pod 删除掉,它们再次被创建出来时也必须严格按照这个顺序才行。并且,新创建出来的 Pod,必须和原来 Pod 的网络标识一样,这样原先的访问者才能使用同样的方法,访问到这个新 Pod。
  2. 存储状态。这种情况意味着,应用的多个实例分别绑定了不同的存储数据。对于这些应用实例来说,Pod A 第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间 Pod A 被重新创建过。这种情况最典型的例子,就是一个数据库应用的多个存储实例。

所以,StatefulSet 的核心功能,就是通过某种方式记录这些状态,然后在 Pod 被重新创建时, 能够为新 Pod 恢复这些状态。

# 1. 拓扑状态

编写一个web.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.17.1
          ports:
            - containerPort: 80
              name: web
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这个yaml文件,和deployment的唯一区别就是多了个serviceName=nginx字段,这个字段的作用,就是告诉StatefulSet控制器,在执行控制循环的时候,请使用nginx这个Headless Service来保证Pod的"可解析身份"。

回顾:

service.yaml

apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: default
  labels:
    app: nginx
spec:
  ports:
  - name: http
    port: 80
  clusterIP: None
  selector:
    app: nginx
1
2
3
4
5
6
7
8
9
10
11
12
13
14

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

<pod-name>.<svc-name>.<namespace>.svc.cluster.local
1

这个 DNS 记录正是 Kubernetes 集群为 Pod 分配的一个唯一标识,只要我们知道 Pod 的名字,以及它对应的 Service 名字,就可以组装出这样一条 DNS 记录访问到 Pod 的 IP 地址。

通过kubectl create创建上面这个Service和StatefulSet

[root@master state]# kubectl create -f web.yaml 
statefulset.apps/web created
[root@master state]# kubectl create -f service.yaml 
service/nginx created
[root@master state]# kubectl get service nginx      
NAME    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx   ClusterIP   None         <none>        80/TCP    4s
[root@master state]# kubectl get statefulset web    
NAME   READY   AGE
web    2/2     78s
1
2
3
4
5
6
7
8
9
10

查看StatefulSet的Events来查看Pod的创建过程:

[root@master state]# kubectl describe statefulset web
Name:               web
Namespace:          default
CreationTimestamp:  Tue, 27 Sep 2022 01:20:47 -0400
Selector:           app=nginx
Labels:             <none>
Annotations:        <none>
Replicas:           2 desired | 2 total
Update Strategy:    RollingUpdate
  Partition:        0
Pods Status:        2 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
  Labels:  app=nginx
  Containers:
   nginx:
    Image:        nginx:1.17.1
    Port:         80/TCP
    Host Port:    0/TCP
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Volume Claims:    <none>
Events:
  Type    Reason            Age   From                    Message
  ----    ------            ----  ----                    -------
  Normal  SuccessfulCreate  113s  statefulset-controller  create Pod web-0 in StatefulSet web successful
  Normal  SuccessfulCreate  91s   statefulset-controller  create Pod web-1 in StatefulSet web successful
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

StatefulSet 给它所管理的所有 Pod 的名字, 进行了编号,编号规则是:-。 而且这些编号都是从 0 开始累加,与 StatefulSet 的每个 Pod 实例一一对应,绝不重复。 更重要的是,这些 Pod 的创建,也是严格按照编号顺序进行的。

当这两个 Pod 都进入了 Running 状态之后,我们就可以查看到它们各自唯一的“网络身份”了。我们使用 kubectl exec命令进入到容器中查看它们的 hostname:

[root@master wordpress]kubectl get pods -o wide
NAME    READY   STATUS    RESTARTS        AGE   IP               NODE    NOMINATED NODE   READINESS GATES
web-0   1/1     Running   1 (2m40s ago)   26m   10.244.104.43    node2   <none>           <none>
web-1   1/1     Running   1 (6m25s ago)   25m   10.244.166.168   node1   <none>           <none>
[root@master state]# 
[root@master state]# kubectl exec web-0 -- sh -c 'hostname'
web-0
[root@master state]# kubectl exec web-1 -- sh -c 'hostname' 
web-1
1
2
3
4
5
6
7
8
9

这两个 Pod 的 hostname 与 Pod 名字是一致的,都被分配了对应的编号。

接下来,我们再试着以 DNS 的方式,访问一下这个 Headless Service:

[root@master wordpress]# 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 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.104.43 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.166.168 web-1.nginx.default.svc.cluster.local
1
2
3
4
5
6
7
8
9
10
11
12
13
14

从 nslookup 命令的输出结果中,我们可以看到,在访问 web-0.nginx 的时候,最后解析到的,正是 web-0 这个 Pod 的 IP 地址;而当访问 web-1.nginx 的时候,解析到的则是 web-1 的 IP 地址。

这时候,我们在另外一个 Terminal 里把这两个“有状态应用”的 Pod 删掉:

[root@master wordpress]# kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted
1
2
3

Watch 一下这两个 Pod 的状态变化:

[root@master ~]# kubectl get pod -w -l app=nginx
NAME    READY   STATUS    RESTARTS        AGE
web-0   1/1     Running   1 (5m45s ago)   29m
web-1   1/1     Running   1 (9m30s ago)   28m
web-0   1/1     Terminating   1 (5m58s ago)   29m
web-1   1/1     Terminating   1 (9m43s ago)   29m
web-0   1/1     Terminating   1 (5m58s ago)   29m
web-1   1/1     Terminating   1 (9m43s ago)   29m
web-1   0/1     Terminating   1 (9m43s ago)   29m
web-0   0/1     Terminating   1 (5m58s ago)   29m
web-0   0/1     Terminating   1 (5m58s ago)   29m
web-0   0/1     Terminating   1 (5m58s ago)   29m
web-0   0/1     Pending       0               0s
web-0   0/1     Pending       0               0s
web-0   0/1     ContainerCreating   0               0s
web-1   0/1     Terminating         1 (9m43s ago)   29m
web-1   0/1     Terminating         1 (9m43s ago)   29m
web-0   0/1     ContainerCreating   0               0s
web-0   0/1     ContainerCreating   0               1s
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   0/1     ContainerCreating   0               1s
web-1   1/1     Running             0               1s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

可以看到,当我们把这两个 Pod 删除之后,Kubernetes 会按照原先编号的顺序,创建出了两个新的 Pod。并且,Kubernetes 依然为它们分配了与原来相同的“网络身份”:web-0.nginx 和 web-1.nginx。

通过这种严格的对应规则,StatefulSet 就保证了 Pod 网络标识的稳定性。 比如,如果 web-0 是一个需要先启动的主节点,web-1 是一个后启动的从节点,那么只要这个 StatefulSet 不被删除,你访问 web-0.nginx 时始终都会落在主节点上,访问 web-1.nginx 时,则始终都会落在从节点上,这个关系绝对不会发生任何变化。

如果我们再用 nslookup 命令,查看一下这个新 Pod 对应的 Headless Service :

[root@master ~]# kubectl get pod -o wide
NAME    READY   STATUS    RESTARTS   AGE     IP               NODE    NOMINATED NODE   READINESS GATES
web-0   1/1     Running   0          2m13s   10.244.104.46    node2   <none>           <none>
web-1   1/1     Running   0          2m12s   10.244.166.179   node1   <none>           <none>
[root@master wordpress]# 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 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.104.46 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.166.179 web-1.nginx.default.svc.cluster.local
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

我们可以看到,在这个 StatefulSet 中,这两个新 Pod 的“网络标识”(比如:web-0.nginx 和 web-1.nginx),再次解析到了正确的 IP 地址。

通过这种方法,Kubernetes 就成功地将 Pod 的拓扑状态(比如:哪个节点先启动,哪个节点后启动),按照 Pod 的“名字 + 编号”的方式固定了下来。此外,Kubernetes 还为每一个 Pod 提供了一个固定并且唯一的访问入口,即:这个 Pod 对应的 DNS 记录。 这些状态,在 StatefulSet 的整个生命周期里都会保持不变,绝不会因为对应 Pod 的删除或者 重新创建而失效。

不过,相信你也已经注意到了,尽管 web-0.nginx 这条记录本身不会变,但它解析到的 Pod 的 IP 地址,并不是固定的。这就意味着,对于“有状态应用”实例的访问,你必须使用 DNS 记录 或者 hostname 的方式,而绝不应该直接访问这些 Pod 的 IP 地址。

StatefulSet 这个控制器的主要作用之一,就是使用 Pod 模板创建 Pod 的时候, 对它们进行编号,并且按照编号顺序逐一完成创建工作。而当 StatefulSet 的“控制循环”发现 Pod 的“实际状态”与“期望状态”不一致,需要新建或者删除 Pod 进行“调谐”的时候,它会严格按照这些 Pod 编号的顺序,逐一完成这些操作。

与此同时,通过 Headless Service 的方式,StatefulSet 为每个 Pod 创建了一个固定并且稳定的 DNS 记录,来作为它的访问入口。

# 2. 存储状态

准备两个1G存储卷(PV): pv.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv001
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  hostPath:
    path: /tmp/pv001

---

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

直接创建pv

[root@master state]# kubectl create -f pv.yaml
persistentvolume/pv001 created
persistentvolume/pv002 created
[root@master state]# kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM                                      STORAGECLASS        REASON   AGE
pv001                                      1Gi        RWO            Retain           Available                                                                           5s
pv002                                      1Gi        RWO            Retain           Available   
1
2
3
4
5
6
7

接下来声明一个如下所示的StatefulSet资源清单:(nginx-sts.yaml)

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.17.1
          ports:
            - containerPort: 80
              name: web
          volumeMounts:
            - name: ww
              mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
    - metadata:
        name: ww
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 1Gi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

我们为这个 StatefulSet 额外添加了一个 volumeClaimTemplates 字段。从名字就可以看出来,它跟 Deployment 里 Pod 模板(PodTemplate)的作用类似。也就是说,凡是被这个StatefulSet管理的 Pod,都会声明一个对应的 PVC;而这个 PVC 的定义,就来自于 volumeClaimTemplates 这个模板字段。

更重要的是,这个 PVC 的名字,会被分配一个与这个 Pod 完全一致的编号。 这个自动创建的 PVC,与 PV 绑定成功后,就会进入 Bound 状态,这就意味着这个 Pod 可以挂载并使用这个 PV 了。

[root@master state]# kubectl create -f nginx-sts.yaml 
statefulset.apps/web created
[root@master state]# kubectl get pvc
NAME       STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
ww-web-0   Bound    pv001    1Gi        RWO                           5s
ww-web-1   Bound    pv002    1Gi        RWO                           2s
1
2
3
4
5
6

使用 kubectl create 创建了 StatefulSet 之后,就会看到 Kubernetes 集群里出 现了两个 PVC。

这些 PVC,都"<PVC名字>-<StatefulSet名字>-< 编号 >”的方式命名,并且处于 Bound 状态。

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

[root@master state]# for i in 0 1; do kubectl exec web-$i -- sh -c 'echo hello $(hostname) > /usr/share/nginx/html/index.html'; done
[root@master state]# for i in 0 1; do kubectl exec -it web-$i -- sh -c "cat /usr/share/nginx/html/index.html"; done
hello web-0
hello web-1
1
2
3
4

通过 kubectl exec 指令,我们在每个 Pod 的 Volume 目录里,写入了一个 index.html 文件。这个文件的内容,正是 Pod 的 hostname。

如果使用kubectl delete删除这两个pod,这些volume文件会不会丢失呢?

[root@master state]# kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted
1
2
3

上面删除的两个pod会被按照编号的循环重新创建,那么我们写入的index.html文件是否还在?

[root@master state]# for i in 0 1; do kubectl exec -it web-$i -- sh -c "cat /usr/share/nginx/html/index.html"; done
hello web-0
hello web-1
1
2
3

这个请求依然会返回:hello web-0,hello web-1。也就是说,原先与名叫 web-0 的 Pod 绑定的 PV,在这个 Pod 被重新创建之后,依然同新的名叫 web-0 的 Pod 绑定在了一起。对于 Pod web-1 来说,也是完全一样的情况。

这是怎么做到的呢?

首先,当把一个 Pod,比如 web-0,删除之后,这个 Pod 对应的 PVC 和 PV,并不会被删除,而这个 Volume 里已经写入的数据,也依然会保存在远程存储服务里。

此时,StatefulSet 控制器发现,一个名叫 web-0 的 Pod 消失了。所以,控制器就会重新创建 一个新的、名字还是叫作 web-0 的 Pod 来,“纠正”这个不一致的情况。 需要注意的是,在这个新的 Pod 对象的定义里,它声明使用的 PVC 的名字,还是叫作:ww-web-0。

这个 PVC 的定义,还是来自于 PVC 模板(volumeClaimTemplates),这是 StatefulSet 创建 Pod 的标准流程。

所以,在这个新的 web-0 Pod 被创建出来之后,Kubernetes 为它查找名叫 ww-web-0 的 PVC 时,就会直接找到旧 Pod 遗留下来的同名的 PVC,进而找到跟这个 PVC 绑定在一起的 PV。 这样,新的 Pod 就可以挂载到旧 Pod 对应的那个 Volume,并且获取到保存在 Volume 里的数据。

通过这种方式,Kubernetes 的 StatefulSet 就实现了对应用存储状态的管理。

# 3. 更新策略

在 StatefulSet 中同样也支持两种升级策略:onDelete 和 RollingUpdate,同样可以通过设置 .spec.updateStrategy.type 进行指定。

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

另外SatefulSet 的滚动升级还支持 Partitions特性,通过.spec.updateStrategy.rollingUpdate.partition 进行设置,在设置 partition 后,SatefulSet 的 Pod 中序号大于或等于 partition 的 Pod 会在 StatefulSet 的模板更新后进行滚动升级,而其余的 Pod 保持不变。

......
updateStrategy:
  rollingUpdate: # 如果更新的策略是OnDelete,那么rollingUpdate就失效
    partition: 2 # 表示从第2个分区开始更新,默认是0
  type: RollingUpdate /OnDelete # 滚动更新/删除之后更新
1
2
3
4
5
编辑 (opens new window)
上次更新: 2023/01/26, 22:57:26
Harbor部署
wordpress实战部署

← Harbor部署 wordpress实战部署→

最近更新
01
架构升级踩坑之路
02-27
02
总结
02-27
03
语法学习
02-27
更多文章>
| Copyright © 2021-2025 Wu lingui |
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式