【重识云原生】第六章容器基础6.4.10.5节——Statefulset原理剖析

2023-03-23 22:37:39 浏览数 (1)

1 StatefulSet工作原理剖析

1.1 工作原理简述

        首先,StatefulSet 的控制器直接管理的是 Pod。这是因为,StatefulSet 里的不同 Pod 实例,不再像 ReplicaSet 中那样都是完全一样的,而是有了细微区别的。比如,每个 Pod 的 hostname、名字等都是不同的、携带了编号的。而 StatefulSet 区分这些实例的方式,就是通过在 Pod 的名字里加上事先约定好的编号。

        其次,Kubernetes 通过 Headless Service,为这些有编号的 Pod,在 DNS 服务器中生成带有同样编号的 DNS 记录。只要 StatefulSet 能够保证这些 Pod 名字里的编号不变,那么 Service 里类似于 web-0.nginx.default.svc.cluster.local 这样的 DNS 记录也就不会变,而这条记录解析出来的 Pod 的 IP 地址,则会随着后端 Pod 的删除和再创建而自动更新。这当然是 Service 机制本身的能力,不需要 StatefulSet 操心。

        最后,StatefulSet 还为每一个 Pod 分配并创建一个同样编号的 PVC。这样,Kubernetes 就可以通过 Persistent Volume 机制为这个 PVC 绑定上对应的 PV,从而保证了每一个 Pod 都拥有一个独立的 Volume。在这种情况下,即使 Pod 被删除,它所对应的 PVC 和 PV 依然会保留下来。所以当这个 Pod 被重新创建出来之后,Kubernetes 会为它找到同样编号的 PVC,挂载这个 PVC 对应的 Volume,从而获取到以前保存在 Volume 里的数据。

        从上面内容可以看出,管理有状态应用 Pod 的关键是提供稳定不变的 Pod 标识和稳定不变的存储。

1.2 拓扑状态实现

1.2.1 稳定不变的拓扑状态

        StatefulSet 中Pod标识和 Pod是绑定的,不管 Pod 被调度到哪个节点上,每个 Pod 将被分配一个整数序号,从 0 到 N-1,该序号在 StatefulSet 上是唯一的。

        每个 Pod 根据 StatefulSet 的名称和 Pod 的序号派生出它的主机名。组合主机名的格式为 (StatefulSet名称)(序号);例如StatefulSet 名称为 web,replicas 为 3,那么将会创建三个名称分别为 web-0、web-1、web-2 的 Pod。一旦每个 Pod 创建成功,就会得到一个匹配的 DNS 名称,格式为:. ..svc.cluster.local,其中 service-name 由 StatefulSet 的 serviceName 域来设定;并且这些 Pod 的创建,是严格按照编号顺序进行的。比如,在 web-0 进入到 Running 状态、并且 Conditions 成为 Ready 之前,web-1 会一直处于 Pending 状态。

        当我们把这几个 Pod 删除之后,Kubernetes 会按照原先编号的顺序,创建出新的 Pod。并且,Kubernetes 依然为它们分配了与原来相同的“网络身份”;

        通过这种严格的对应规则,StatefulSet 就保证了 Pod 网络标识的稳定性;

1.2.2 运用Headless Service实现Pod网络标识稳定不变

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

1.2.2.1 Service简介

        Service 类型一般有如下的三种:

  • VIP(Virtual IP,即:虚拟 IP)

        Service提供一个 VIP,我们访问这个IP地址时,它会把请求转发到该 Service 所代理的某一个 Pod 上。

  • DNS (Normal Service)

        这种情况下,访问“my-svc.my-namespace.svc.cluster.local”解析到的,正是 my-svc 这个 Service 的 VIP,后面的流程就跟 VIP 方式一致。

  • DNS (Headless Service)

        这种情况下,访问“my-svc.my-namespace.svc.cluster.local”解析到的,直接就是 my-svc 代理的某一个 Pod 的 IP 地址。所以,Headless Service 不需要分配一个 VIP,而是可以直接以 DNS 记录的方式解析出被代理 Pod 的 IP 地址。

1.2.2.2 Headless Service

1.2.2.2.1 案例

        参考如下的demo-service.yaml案例:

代码语言:javascript复制
apiVersion: v1 
kind: Service 
metadata: 
  name: demo-service 
  labels: 
    app: nginx 
spec: 
  ports: 
    - port: 80 
      name: web 
  clusterIP: None 
  selector: 
    app: demo-nginx

        创建成功后可以查看到如下的内容:

 demo-service

1.2.2.2.2 clusterIP

        关于于上面的这行配置clusterIP: None。

        clusterIP 是服务的IP地址,通常由主服务器随机分配还具有如下特点:

  • 如果地址是手动指定的,并且未被其他人使用,则该地址将分配给服务;否则,服务创建将失败
  • 无法通过更新更改此字段。
  • 有效值为“ None”,空字符串(“”)或有效IP地址。
  • 当不需要代理时,可以为无头服务指定“无”。
  • 仅适用于ClusterIP,NodePort和LoadBalancer类型。
  • 如果type为ExternalName,则忽略。

        Headless Service 是一个标准 Service 的 YAML 文件。只不过,它的 clusterIP 字段的值是:None。这个 Service 被创建后并不会被分配一个 VIP,而是会以 DNS 记录的方式暴露出它所代理的 Pod。

1.2.2.2.3 DNS 记录

        当按照这样的方式创建了一个 Headless Service 之后,它所代理的所有 Pod 的 IP 地址,都会被绑定一个这样格式的 DNS 记录,如下所示:

代码语言:javascript复制
<pod-name>.<svc-name>.<namespace>.svc.cluster.local

        这个 DNS 记录,正是 Kubernetes 项目为 Pod 分配的唯一的“可解析身份”(Resolvable Identity)。有了这个“可解析身份”,只需要知道了一个 Pod 的名字,以及它对应的 Service 的名字,就可以非常确定地通过这条 DNS 记录访问到 Pod 的 IP 地址。

1.2.2.3 StatefulSet实现Pod的拓扑状态

       Stateful状态集在创建和扩展的时候有特殊的限制,如果一个有状态集期望的Pod数量是N,那么有StatefulSet会从0开始依次创建这些Pod在第一个Pod正常运行之前是不会创建第二个Pod。在删除Pod的时候,则是从第N个Pod开始反向依次删除。

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

       只要知道一个Pod的名字以及它对应的Service的名字,就可以通过这条DNS记录访问到Pod的IP地址(pod的名称.service名称) -> Pod的IP。

       定义Stateful的对象中有一个serviceName字段来告诉Stateful控制器使用具体的service来解析它所管理的Pod的IP地址。

       Pod的名称由StatfulSet对象的名称 Pod创建时所在的索引组成 StatefulSet使用这个DNS记录解析规则来维持Pod的拓扑状态。

       StatefulSet给它所管理的所有Pod的名字进行了编号,编号规则是: - .而且这些编号都是从0开始累加与StatefulSet的每个Pod实例一一对应、绝不重复。

       这些Pod的创建也是严格按照编号顺序进行的,比如,在web-0进入到Running 状态、并且细分状态(Conditions)成为Ready之前,web-1会一直处于Pending状态。

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

       通过永远不改变Pod名称的方式StatefulSet保证了Pod网络标识的稳定性。不管Pod是被删除重启还是被调度到其他节点上都不会改变Pod的名称。

       StatefulSet管理的pod使用的镜像是一样的,但是每个pod的命令和初始化流程可以完全不一样来实现主从Pod的拓扑部署。

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

1.3 “稳定不变的存储”的实现

1.3.1 稳定不变的存储概述

  • 每个 Pod 都会绑定一个固定编号的 PVC,这些 PVC 的名字都是 -;
  • 名叫 web-0 的 Pod,会声明使用名叫 www-web-0 的 PVC;
  • PV/PVC 通过动态卷的方式存储于远程存储服务器;
  • 当一个 Pod 被删除后,对应的 PVC 和 PV 并不会被删除,数据依然存在于远程服务器,Pod 被重建后,StatefulSet 会重新查找对应名称的 PVC 来进行绑定;

1.3.2 StatefulSet实现Pod的存储状态

      通过PVC机制来实现存储状态管理。

      在StatefulSet对象中除了定义PodTemplate还会定义一个volumeClaimTemplates凡是被这个StatefulSet管理的Pod都会声明一个对应的PVC,这个PVC的定义就来自于 volumeClaimTemplates这个模板字段。

        这个PVC的名字,会被分配一个与这个Pod完全一致的编号。

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

        StatefulSet在重新创建web-0这个pod的时候.它声明使用的PVC的名字还是叫作:www-web-0 这个PVC的定义,还是来自于PVC模板(volumeClaimTemplates)这是StatefulSet创建 Pod的标准流程。

        Kubernetes为它查找名叫www-web-0的PVC时,就会直接找到旧Pod遗留下来的同名的 PVC进而找到跟这个PVC绑定在一起的PV.这样新的Pod就可以挂载到旧Pod对应的那个Volume并且获取到保存在Volume里的数据.通过这种方式Kubernetes的StatefulSet就实现了对应用存储状态的管理。

        StatefulSet其实就是一种特殊的Deployment,而其独特之处在于,它的每个Pod都被编号了.而且,这个编号会体现在Pod的名字和hostname等标识信息上,这不仅代表了Pod的创建顺序,也是Pod的重要网络标识(即:在整个集群里唯一的、可被的访问身份).有了这个编号后StatefulSet就使用Kubernetes里的两个标准功能:Headless Service 和 PV/PVC,实现了对 Pod 的拓扑状态和存储状态的维护。

1.4 核心逻辑实现

        StatefulSet的实现机制整体流程相对简明,接下来按照Pod管理、状态计算、状态管理、更新策略这几部分来依次讲解。

1.4.1 Pod的release与adopt

        statefulSet中的pod的名字都是按照一定规律来进行设置的, 名字本身也有含义, k8s在进行statefulset更新的时候,首先会过滤属于当前statefulset的pod,并做如下操作:

  • K8s中控制器与Pod的关联主要通过两个部分:controllerRef和label, statefulset在进行Pod过滤的时候,如果发现对应的pod的controllerRef都是当前的statefulset但是其label或者名字并不匹配,则就会尝试release对应的Pod
  • 反之如果发现对应Pod的label和名字都匹配,但是controllerRef并不是当前的statefulSet就会更新对应的controllerRef为当前的statefulset, 这个操作被称为adopt。

         通过该流程可以确保当前statefulset关联的Pod要么与当前的对象关联,要么我就释放你,这样可以维护Pod的一致性,即时有人修改了对应的Pod则也会调整成最终一致性。

1.4.2 副本分类

         在经过第一步的Pod状态的修正之后,statefulset会遍历所有属于自己的Pod,同时将Pod分为两个大类:有效副本和无效副本(condemned),前面提到过Pod的名字也是有序的即有N个副本的Pod则名字依次是{0...N-1}, 这里区分有效和无效也是依据对应的索引顺序,如果超过当前的副本即为无效副本

1.4.3 单调更新

        单调更新主要是指的当对应的Pod管理策略不是并行管理的时候,只要当前Replicas(有效副本)中任一一个Pod发生创建、终止、未就绪的时候,都会等待对应的Pod就绪,即你要想更新一个statefulset的Pod的时候,对应的Pod必须已经RunningAndReady

代码语言:javascript复制
func allowsBurst(set *apps.StatefulSet) bool {

    return set.Spec.PodManagementPolicy == apps.ParallelPodManagement

}

1.4.4 基于计数器的滚动更新

         滚动更新的实现相对隐晦一点,其主要是通过控制副本计数来实现,首先倒序检查对应的Pod的版本是否是最新版本,如果发现不是,则直接删除对应的Pod,同时将currentReplica计数减一,这样在检查对应的Pod的时候,就会发现对应的Pod的不存在,就需要为对应的Pod生成新的Pod信息,此时就会使用最新的副本去更新。

代码语言:javascript复制
func newVersionedStatefulSetPod(currentSet, updateSet *apps.StatefulSet, currentRevision, updateRevision string, ordinal int) *v1.Pod {

    // 如果发现当前的Pod的索引小于当的副本计数,则表明当前Pod还没更新到,但实际上可能因为别的原因

    // 需要重新生成Pod模板,此时仍然使用旧的副本配置

    if currentSet.Spec.UpdateStrategy.Type == apps.RollingUpdateStatefulSetStrategyType &&

(currentSet.Spec.UpdateStrategy.RollingUpdate == nil && ordinal < int(currentSet.Status.CurrentReplicas)) ||

(currentSet.Spec.UpdateStrategy.RollingUpdate != nil && ordinal < int(*currentSet.Spec.UpdateStrategy.RollingUpdate.Partition)) {

        pod := newStatefulSetPod(currentSet, ordinal)

        setPodRevision(pod, currentRevision)

        return pod

    }

    // 使用新的配置生成新的Pod配置

    pod := newStatefulSetPod(updateSet, ordinal)

    setPodRevision(pod, updateRevision)

    return pod

}

1.4.5 无效副本的清理

        无效副本的清理应该主要是发生在对应的statefulset缩容的时候,如果发现对应的副本已经被遗弃,就会直接删除,此处默认也需要遵循单调性原则,即每次都只更新一个副本

1.4.6 基于删除的单调性更新

代码语言:javascript复制
if getPodRevision(replicas[target]) != updateRevision.Name && !isTerminating(replicas[target]) {

    klog.V(2).Infof("StatefulSet %s/%s terminating Pod %s for update", set.Namespace, set.Name, replicas[target].Name)

    err := ssc.podControl.DeleteStatefulPod(set, replicas[target])

    status.CurrentReplicas--

    return &status, err

}

        Pod的版本检测位于对应一致性同步的最后,当代码走到当前位置,则证明当前的statefulSet在满足单调性的情况下,有效副本里面的所有Pod都是RunningAndReady状态了,此时就开始倒序进行版本检查,如果发现版本不一致,就根据当前的partition的数量来决定允许并行更新的数量,在这里删除后,就会触发对应的事件,从而触发下一个调度事件,触发下一轮一致性检查。

1.4.7 OnDelete策略

代码语言:javascript复制
if set.Spec.UpdateStrategy.Type == apps.OnDeleteStatefulSetStrategyType {

    return &status, nil

}

        StatefulSet的更新策略除了RollingUpdate还有一种即OnDelete即必须人工删除对应的 Pod来触发一致性检查,所以针对那些如果想只更新指定索引的statefulset可以尝试该策略,每次只删除对应的索引,这样只有指定的索引会更新为最新的版本。

1.4.8 状态存储

        状态存储其实就是我们常说的PVC,在Pod创建和更新的时候,如果发现对应的PVC的不存在则就会根据statefulset里面的配置创建对应的PVC,并更新对应Pod的配置。

1.5 有状态应用总结

        从核心实现分析中可以看出来,有状态应用的实现,实际上核心是基于一致性状态、单调更新、持久化存储的组合,通过一致性状态、单调性更新,保证期望副本的数量的Pod处于RunningAndReady的状态并且保证有序性,同时通过持久化存储来进行数据的保存。

         有序的重要性,在分布式系统中比较常见的两个设计就是分区和副本,其中副本主要是为了保证可用性,而分区主要是进行数据的平均分布,二者通常都是根据当前集群中的节点来进行分配的,如果我们节点短暂的离线升级,数据保存在对应的PVC中,在恢复后可以很快的进行节点的信息的恢复并重新加入集群,所以后面如果开发这种类似的分布式应用的时候,可以将底层的恢复和管理交给k8s,数据保存在PVC中,则应用更多的只需要关注系统的集群管理和数据分布问题即,这也是云原生带来的改变。

参考链接

Statefulset详细解析 - 不懂123 - 博客园

k8s中statefulset资源类型的深入理解

十,StatefulSet简介及简单使用 - 戴红领巾的少年 - 博客园

k8s之StatefulSet详解_最美dee时光的博客-CSDN博客_statefulset

Kubernetes学习笔记 —— 7. StatefulSet - 知乎

容器化部署实战(五)|控制器 StatefulSet 的原理

K8s StatefulSet

kubernetes——StatefulSet详解

Kubernetes 深入理解StatefulSet(一):拓扑状态_富士康质检员张全蛋的博客-CSDN博客

图解kubernetes控制器StatefulSet核心实现原理_8小时的博客-CSDN博客

详解 Kubernetes StatefulSet 实现原理_cbmljs的博客-CSDN博客_kubectl statefulset

Kubernetes:StatefulSet剖析

Kubernetes Deployments vs StatefulSets - Stack Overflow

StatefulSet: Run and Scale Stateful Applications Easily in Kubernetes | Kubernetes

Running ZooKeeper, A Distributed System Coordinator | Kubernetes

StatefulSets | Kubernetes

StatefulSet · Kubernetes Engine

0 人点赞