暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Kubernetes系列10 一 容器编排之StatefulSet

栋总侃技术 2021-06-15
975

在上一节我们了解到了Deployment 的水平扩展/收缩和滚动更新,这足以满足大多数的 web 应用了,但是也不能覆盖到所有的应用。例如数据库的主从这类存在着拓扑关系的应用,Deployment 无法满足现实的应用场景的。

Deployment 满足的容器编排场景,我们称之为无状态应用。而对应的有状态应用中,所谓的有状态表现在以下两个方面:

  • 多个实例之间的顺序关系或者拓扑关系,当容器重启后仍然能够保持顺序关系或者拓扑关系;

  • 多个实例在磁盘上的存储依赖,当容器重启之后依赖关系仍然能够保留,例如对于Mysql,当重启后,master节点对应的存储文件一定是重启前master节点对应的存储。

Kuberenetes 中的 StatefulSet 控制对象正是存储了有状态应用的 “状态”,使得 StatefulSet 对有状态应用容器的支持。

我们通过一个 StatefulSet 的yaml文件来看下其定义:

    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
    name: web
    spec:
    replicas: 2
    selector:
    matchLabels:
    app: nginx
    template:
    metadata:
    labels:
    app: nginx
    spec:
    containers:
    - name: nginx
    image: nginx:1.9.1
    ports:
    - containerPort: 80
    name: web

    可以看到与 Deployment 的定义结构完全一样,只是kind为 StatefulSet。同样的 使用apply -f 命令启动该控制对象,并查看 Pod:

      kubectl apply -f nginx-statefulset.yaml
      statefulset.apps/web created
      kubectl get pods
      NAME READY STATUS RESTARTS AGE
      web-0 1/1 Running 0 9s
      web-1 1/1 Running 0 7s

      我们再回过头来对比下创建 Deployment 控制对象时所管理的Pod:

        kubectl get pods
        NAME READY STATUS RESTARTS AGE
        nginx-deployment-54f57cf6bf-djgwn 1/1 Running 0 79s
        nginx-deployment-54f57cf6bf-q4pw2   1/1     Running   0          79s

        可以看到Name的明显差异,Deloyment 所管理的 Pod 命名是产生了一个随机的UID,而 StatefulSet 所管理的Pod命名是有明显的顺序的,从0开始依次累加。

        不仅仅如此,使用 StatefulSet管理的Pod 启动顺序也有严格的保证,当 n 编号的Pod状态为 Running 之后编号为 n+1 的 Pod 一直都是 Pending 状态。

        当Pod A需要被Pod B的访问时,需要定义Pod A的Service,Pod B通过访问 Pod A的Service既可访问到 Pod A。那么Sevice是如何被访问到的呢,有以下两种方式:

        1. Service 拥有自己的虚拟IP(vip);

        2. 通过Service的DNS访问,通过DNS访问,又有以下区分:

          1. DNS解析到 Service的VIP,与第一种方式一致,只是多了DNS解析;

          2. Service没有VIP,通过DNS直接解析到Pod的IP。

        我们首先看下Service的定义文件:

          apiVersion: v1
          kind: Service
          metadata:
          name: nginx
          labels:
          app: nginx
          spec:
          ports:
          - port: 80
          name: web
          clusterIP: None
          selector:
          app: nginx

          我们来GET下这个 Service传达的重要点:

          • 定义的类型 kind = Service

          • 暴露 80 端口

          • 看到了熟悉的selector,表示该Service将会应用到所有label含有app=nginx 的Pod;

          • 这里我们需要注意 clusterIP = None,表示这个Service是没有虚拟IP的,对应的 Service访问类型为上文中介绍 Service 的2.b项。

          接下来到了本节的重点了,我们结合着 Service 和 StatefulSet 来解释 StatefulSet是如何保证Pod的顺序关系的。

          我们在 StatefulSet 的yaml文件的第 6 行插入一段:

            apiVersion: apps/v1
            kind: StatefulSet
            metadata:
            name: web
            spec:
            serviceName: "nginx"
            replicas: 2
              ...

            这里 serviceName = ngixn 与 上面 Service 定义中的 name = nginx 对应,当 StatefulSet 控制循环 (在上节说到 kubernetes 的所有控制对象,都是使用控制循环来控制其他API 对象的)对Pod进行控制时,使用该Service来保持其解析身份。

            解析身份可以理解为当使用固定的DNS访问Pod时,始终保持着 a.demo.com 解析到 web-0 Pod,b.demo.com 解析到 web-1 Pod。

            当然在 kubernetes中的DNS并不是这里的 a.demo.com 或者 b.demo.com,而是一组由 pod、service、namespace等拼接起来的唯一标识,其格式如下:

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

              当我们在yaml文件中插入 serviceName: "nginx" 后,再次 kubectl apply -f 该文件,同时也创建 上文中的 Service。可以看到两个Pod对应的hostname:

                kubectl exec web-0 -- sh -c 'hostname'
                web-0
                kubectl exec web-1 -- sh -c 'hostname'
                web-1

                这说明了 web-0容器所对应的DNS记录始终是以 web-0标识的,web-1容器所对应的DNS记录始终也是以 web-1标识的。

                我们可以试想下如果我们将这里的 StatefulSet 对象改为 Deployment,由于Pod的名称是随机的,那么也就不会有DNS记录与Pod顺序的关系,这也说明了 StatefulSet 在文章一开始说明的一个特点:

                • 多个实例之间的顺序关系或者拓扑关系,当容器重启后仍然能够保持顺序关系或者拓扑关系

                在理解Pod保持存储依赖前我们需要先了解PVC (Persistent Volume Claim)对象和PV(Persistent Volume)对象。Pod的持久化存储离不开 Kubernetes 的 PVC (Persistent Volume Claim)对象和 PV(Persistent Volume)对象。我们先了解下PVC和PV。

                作为开发人员可以这样去理解PVC、PV,把PVC看作是接口,PV看作是接口的实现:

                • PVC是接口,作为使用人员(开发人员),无需关注具体存在哪、怎么存,只需要使用PVC声明自己的Volume就行;

                • 而PV则是接口的实现,具体怎么存、存在哪对于专业的专业的运维人员更熟悉,运维人员“真正”的去实现怎么存、存在哪。

                我们先来看一个 PVC的 yaml 定义文件:

                  kind: PersistentVolumeClaim
                  apiVersion: v1
                  metadata:
                  name: pv-claim
                  spec:
                  accessModes:
                  - ReadWriteOnce
                  resources:
                  requests:
                  storage: 1Gi

                  通过上面的yaml文件我们可以看到,仅仅只是提供了使用者关注的信息,空间大小1GB,权限为可读可写。

                  对于开发人员,在Pod中声明Volume时,只需要关注我在容器的那个目录需要挂载出来,根据需要的空间是多少,以及权限来选择PVC。我们看一个使用PVC声明Volume的Pod定义文件:

                    apiVersion: v1
                    kind: Pod
                    metadata:
                    name: pv-pod
                    spec:
                    containers:
                    - name: pv-container
                    image: nginx
                    ports:
                    - containerPort: 80
                    name: "http-server"
                    volumeMounts:
                    - mountPath: "/usr/share/nginx/html"
                    name: pv-storage
                    volumes:
                    - name: pv-storage
                    persistentVolumeClaim:
                    claimName: pv-claim

                    在spec.volumeMounts 节点,nginx容器需要将 usr/share/nginx/html目录做持久化存储,使用的volume名称为 pv-storage。

                    而在 volumes节点声明使用了一个名称为 pv-claim 的PVC。pv-claim正是我们上面示例中的PVC名称。

                    我们再来看一下PV的yaml文件:

                      apiVersion: v1
                      kind: PersistentVolume
                      metadata:
                      name: pv-claim
                      spec:
                      capacity:
                      storage: 5Gi
                      accessModes:
                      - ReadWriteOnce
                      nfs:
                      path: /data/nfs
                      server: 192.168.56.103

                      可以看到PV中需要的空间是5GB,是通过NFS挂载磁盘实现的存储。当创建PVC时,kubernetes会自动绑定符合条件的PV。所以,示例中的PVC会与PV完成绑定。

                      我们再回过头来看PVC是接口,PV是实现这个比喻。kubernetes 这样把volume的使用和实现通过解耦的方式实现,带来了以下两个好处:

                      • 开发者无需关注复杂的存储相关内容,只需要使用就行;

                      • 运维人员可以根据需要对存储进行替换(nfs、Ceph RBD等)。

                      在初步的了解了PV、PVC后,我们回到正题,StatefulSet如何Pod保持存储依赖

                      我们首先还是看下一个使用了volume的 Statefulset对象的完整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.9.1
                        ports:
                        - containerPort: 80
                        name: web
                        volumeMounts:
                        - name: www
                        mountPath: /usr/share/nginx/html
                        volumeClaimTemplates:
                        - metadata:
                        name: www
                        spec:
                        accessModes:
                        - ReadWriteOnce
                        resources:
                        requests:
                        storage: 1Gi

                        我们可以看到Pod的 user/share/nginx/html 是需要做持久化存储的,而当前StatefulSet管理的 Pod 副本数为2,那么两个Pod是如何保持重启后,Pod读取Volume的顺序是一致的,经过前面对service的讲解,想必大家很容易的就猜想到同样的使用编号顺序。

                        当我们创建完成上面的StatefulSet后,我们可以看到果然是通过编号区分pvc的。

                          $ kubectl get pvc 
                          NAME STATUS VOLUME CAPACITY ACCESSMODES AGE
                           www-web-0 Bound pvc-xxx 1Gi RWO 4s
                           www-web-1 Bound pvc-xxx 1Gi RWO 4s

                          StatefulSet 处理 PVC 与 Pod的逻辑关系同Service 与 Pod 的逻辑一致,这里就不再详细说明了。但是我们可以从Service 到 Pod 到 PVC整个过程来理解StatefulSet如何保证Pod的顺序/拓扑结构的。

                          外部访问Servcie时,通过DNS解析到Pod 0 访问Pod 0容器,Pod 0 读取 PVC 0的数据,Pod 1 访问Pod 1容器,Pod 1 读取 PVC 1的数据。当Pod重启,虽然Pod的启动有先后,但是DNS解析到的Pod 0始终读取的是PVC 0的数据,而Pod 1始终读取的是PVC 1的数据。

                          Deployment通过生成UUID来对管理的对象命名,StatefulSet通过编号来对管理的对象命名,这是Deployment与StatefulSet对管理API对象本质上的不同,正是StatefulSet的编号机制,使得控制对象实现对有状态应用的容器编排管理。



                          文章转载自栋总侃技术,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                          评论