抖音技术能力大揭密!钜惠大礼、深度体验,尽在火山引擎增长沙龙,就等你来! 立即报名>> 了解详情
写点什么

详解 Kubernetes StatefulSet 实现原理

2019 年 12 月 03 日

详解 Kubernetes StatefulSet 实现原理

在 Kubernetes 的世界中,ReplicaSetDeployment 主要用于处理无状态的服务,无状态服务的需求往往非常简单并且轻量,每一个无状态节点存储的数据在重启之后就会被删除,虽然这种服务虽然常见,但是我们仍然需要有状态的服务来实现一些特殊的需求,StatefulSet 就是 Kubernetes 为了运行有状态服务引入的资源,例如 Zookeeper、Kafka 等。


这篇文章会介绍 Kubernetes 如何在集群中运行有状态服务,同时会分析这些有状态服务 StatefulSet 的同步过程以及实现原理。


概述

StatefulSet 是用于管理有状态应用的工作负载对象,与 ReplicaSetDeployment 这两个对象不同,StatefulSet 不仅能管理 Pod 的对象,还它能够保证这些 Pod 的顺序性和唯一性。


与 Deployment 一样,StatefulSet 也使用规格中声明的 template 模板来创建 Pod 资源,但是这些 Pod 相互之间是不能替换的;除此之外 StatefulSet 会为每个 Pod 设置一个单独的持久标识符,这些用于标识序列的标识符在发生调度时也不会丢失。


YAML


apiVersion: apps/v1kind: StatefulSetmetadata:  name: webspec:  serviceName: "nginx"  replicas: 2  selector:    matchLabels:      app: nginx  template:    metadata:      labels:        app: nginx    spec:      containers:      - name: nginx        image: k8s.gcr.io/nginx-slim:0.8        volumeMounts:        - name: www          mountPath: /usr/share/nginx/html  volumeClaimTemplates:  - metadata:      name: www    spec:      accessModes: [ "ReadWriteOnce" ]      resources:        requests:          storage: 1Gi
复制代码


如果我们在 Kubernetes 集群中创建如上所示的 StatefulSet 对象,会得到以下结果,Kubernetes 不仅会创建 StatefulSet 对象,还会自动创建两个 Pod 副本:


Go


$ kubectl get statefulsets.appskNAME   READY   AGEweb    2/2     2m27s
$ kubectl get podsNAME READY STATUS RESTARTS AGEweb-0 1/1 Running 0 2m31sweb-1 1/1 Running 0 105s
$ kubectl get persistentvolumesNAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGEpvc-19ef374f-39d1-11e9-b870-9efb418608da 1Gi RWO Delete Bound default/www-web-1 do-block-storage 21mpvc-fe53d5f7-39d0-11e9-b870-9efb418608da 1Gi RWO Delete Bound default/www-web-0 do-block-storage 21m
$ kubectl get persistentvolumeclaimsNAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGEwww-web-0 Bound pvc-fe53d5f7-39d0-11e9-b870-9efb418608da 1Gi RWO do-block-storage 21mwww-web-1 Bound pvc-19ef374f-39d1-11e9-b870-9efb418608da 1Gi RWO do-block-storage 21m
复制代码


除此之外,上述 YAML 文件中的 volumeClaimTemplates 配置还会创建持久卷PersistentVolume 和用于绑定持久卷和 Pod 的 PersistentVolumeClaim 资源;两个 Pod 对象名中包含了它们的序列号,该序列号会在 StatefulSet 存在的时间内保持不变,哪怕 Pod 被重启或者重新调度,也不会出现任何的改变。


#mermaid-1575352666178 .label{font-family:trebuchet ms,verdana,arial;color:#333}#mermaid-1575352666178 .node circle,#mermaid-1575352666178 .node ellipse,#mermaid-1575352666178 .node polygon,#mermaid-1575352666178 .node rect{fill:#ececff;stroke:#9370db;stroke-width:1px}#mermaid-1575352666178 .node.clickable{cursor:pointer}#mermaid-1575352666178 .arrowheadPath{fill:#333}#mermaid-1575352666178 .edgePath .path{stroke:#333;stroke-width:1.5px}#mermaid-1575352666178 .edgeLabel{background-color:#e8e8e8}#mermaid-1575352666178 .cluster rect{fill:#ffffde!important;stroke:#aa3!important;stroke-width:1px!important}#mermaid-1575352666178 .cluster text{fill:#333}#mermaid-1575352666178 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:trebuchet ms,verdana,arial;font-size:12px;background:#ffffde;border:1px solid #aa3;border-radius:2px;pointer-events:none;z-index:100}#mermaid-1575352666178 .actor{stroke:#ccf;fill:#ececff}#mermaid-1575352666178 text.actor{fill:#000;stroke:none}#mermaid-1575352666178 .actor-line{stroke:grey}#mermaid-1575352666178 .messageLine0{marker-end:“url(#arrowhead)”}#mermaid-1575352666178 .messageLine0,#mermaid-1575352666178 .messageLine1{stroke-width:1.5;stroke-dasharray:“2 2”;stroke:#333}#mermaid-1575352666178 #arrowhead{fill:#333}#mermaid-1575352666178 #crosshead path{fill:#333!important;stroke:#333!important}#mermaid-1575352666178 .messageText{fill:#333;stroke:none}#mermaid-1575352666178 .labelBox{stroke:#ccf;fill:#ececff}#mermaid-1575352666178 .labelText,#mermaid-1575352666178 .loopText{fill:#000;stroke:none}#mermaid-1575352666178 .loopLine{stroke-width:2;stroke-dasharray:“2 2”;marker-end:“url(#arrowhead)”;stroke:#ccf}#mermaid-1575352666178 .note{stroke:#aa3;fill:#fff5ad}#mermaid-1575352666178 .noteText{fill:#000;stroke:none;font-family:trebuchet ms,verdana,arial;font-size:14px}#mermaid-1575352666178 .section{stroke:none;opacity:.2}#mermaid-1575352666178 .section0{fill:rgba(102,102,255,.49)}#mermaid-1575352666178 .section2{fill:#fff400}#mermaid-1575352666178 .section1,#mermaid-1575352666178 .section3{fill:#fff;opacity:.2}#mermaid-1575352666178 .sectionTitle0,#mermaid-1575352666178 .sectionTitle1,#mermaid-1575352666178 .sectionTitle2,#mermaid-1575352666178 .sectionTitle3{fill:#333}#mermaid-1575352666178 .sectionTitle{text-anchor:start;font-size:11px;text-height:14px}#mermaid-1575352666178 .grid .tick{stroke:#d3d3d3;opacity:.3;shape-rendering:crispEdges}#mermaid-1575352666178 .grid path{stroke-width:0}#mermaid-1575352666178 .today{fill:none;stroke:red;stroke-width:2px}#mermaid-1575352666178 .task{stroke-width:2}#mermaid-1575352666178 .taskText{text-anchor:middle;font-size:11px}#mermaid-1575352666178 .taskTextOutsideRight{fill:#000;text-anchor:start;font-size:11px}#mermaid-1575352666178 .taskTextOutsideLeft{fill:#000;text-anchor:end;font-size:11px}#mermaid-1575352666178 .taskText0,#mermaid-1575352666178 .taskText1,#mermaid-1575352666178 .taskText2,#mermaid-1575352666178 .taskText3{fill:#fff}#mermaid-1575352666178 .task0,#mermaid-1575352666178 .task1,#mermaid-1575352666178 .task2,#mermaid-1575352666178 .task3{fill:#8a90dd;stroke:#534fbc}#mermaid-1575352666178 .taskTextOutside0,#mermaid-1575352666178 .taskTextOutside1,#mermaid-1575352666178 .taskTextOutside2,#mermaid-1575352666178 .taskTextOutside3{fill:#000}#mermaid-1575352666178 .active0,#mermaid-1575352666178 .active1,#mermaid-1575352666178 .active2,#mermaid-1575352666178 .active3{fill:#bfc7ff;stroke:#534fbc}#mermaid-1575352666178 .activeText0,#mermaid-1575352666178 .activeText1,#mermaid-1575352666178 .activeText2,#mermaid-1575352666178 .activeText3{fill:#000!important}#mermaid-1575352666178 .done0,#mermaid-1575352666178 .done1,#mermaid-1575352666178 .done2,#mermaid-1575352666178 .done3{stroke:grey;fill:#d3d3d3;stroke-width:2}#mermaid-1575352666178 .doneText0,#mermaid-1575352666178 .doneText1,#mermaid-1575352666178 .doneText2,#mermaid-1575352666178 .doneText3{fill:#000!important}#mermaid-1575352666178 .crit0,#mermaid-1575352666178 .crit1,#mermaid-1575352666178 .crit2,#mermaid-1575352666178 .crit3{stroke:#f88;fill:red;stroke-width:2}#mermaid-1575352666178 .activeCrit0,#mermaid-1575352666178 .activeCrit1,#mermaid-1575352666178 .activeCrit2,#mermaid-1575352666178 .activeCrit3{stroke:#f88;fill:#bfc7ff;stroke-width:2}#mermaid-1575352666178 .doneCrit0,#mermaid-1575352666178 .doneCrit1,#mermaid-1575352666178 .doneCrit2,#mermaid-1575352666178 .doneCrit3{stroke:#f88;fill:#d3d3d3;stroke-width:2;cursor:pointer;shape-rendering:crispEdges}#mermaid-1575352666178 .activeCritText0,#mermaid-1575352666178 .activeCritText1,#mermaid-1575352666178 .activeCritText2,#mermaid-1575352666178 .activeCritText3,#mermaid-1575352666178 .doneCritText0,#mermaid-1575352666178 .doneCritText1,#mermaid-1575352666178 .doneCritText2,#mermaid-1575352666178 .doneCritText3{fill:#000!important}#mermaid-1575352666178 .titleText{text-anchor:middle;font-size:18px;fill:#000}#mermaid-1575352666178 g.classGroup text{fill:#9370db;stroke:none;font-family:trebuchet ms,verdana,arial;font-size:10px}#mermaid-1575352666178 g.classGroup rect{fill:#ececff;stroke:#9370db}#mermaid-1575352666178 g.classGroup line{stroke:#9370db;stroke-width:1}#mermaid-1575352666178 .classLabel .box{stroke:none;stroke-width:0;fill:#ececff;opacity:.5}#mermaid-1575352666178 .classLabel .label{fill:#9370db;font-size:10px}#mermaid-1575352666178 .relation{stroke:#9370db;stroke-width:1;fill:none}#mermaid-1575352666178 #compositionEnd,#mermaid-1575352666178 #compositionStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-1575352666178 #aggregationEnd,#mermaid-1575352666178 #aggregationStart{fill:#ececff;stroke:#9370db;stroke-width:1}#mermaid-1575352666178 #dependencyEnd,#mermaid-1575352666178 #dependencyStart,#mermaid-1575352666178 #extensionEnd,#mermaid-1575352666178 #extensionStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-1575352666178 .branch-label,#mermaid-1575352666178 .commit-id,#mermaid-1575352666178 .commit-msg{fill:#d3d3d3;color:#d3d3d3}#mermaid-1575352666178 {


color: rgb(58, 65, 69);


font: normal normal 400 normal 18px / 33.3px “Hiragino Sans GB”, “Heiti SC”, “Microsoft YaHei”, sans-serif, Merriweather, serif;


}


StatefulSet


Pod1


Pod2


PersistentVolumeClaim1


PersistentVolumeClaim2


PersistentVolume1


PersistentVolume2


StatefulSet 的拓扑结构和其他用于部署的资源其实比较类似,比较大的区别在于 StatefulSet 引入了 PV 和 PVC 对象来持久存储服务产生的状态,这样所有的服务虽然可以被杀掉或者重启,但是其中的数据由于 PV 的原因不会丢失。


这里不会展开介绍 PV 和 PVC,感兴趣的读者可以阅读 详解 Kubernetes Volume 的实现原理 了解 Kubernetes 存储系统的实现原理。


实现原理

ReplicaSetDeployment 资源一样,StatefulSet 也使用控制器的方式实现,它主要由 StatefulSetControllerStatefulSetControlStatefulPodControl 三个组件协作来完成 StatefulSet 的管理,StatefulSetController 会同时从 PodInformerReplicaSetInformer 中接受增删改事件并将事件推送到队列中:


#mermaid-1575352666525 .label{font-family:trebuchet ms,verdana,arial;color:#333}#mermaid-1575352666525 .node circle,#mermaid-1575352666525 .node ellipse,#mermaid-1575352666525 .node polygon,#mermaid-1575352666525 .node rect{fill:#ececff;stroke:#9370db;stroke-width:1px}#mermaid-1575352666525 .node.clickable{cursor:pointer}#mermaid-1575352666525 .arrowheadPath{fill:#333}#mermaid-1575352666525 .edgePath .path{stroke:#333;stroke-width:1.5px}#mermaid-1575352666525 .edgeLabel{background-color:#e8e8e8}#mermaid-1575352666525 .cluster rect{fill:#ffffde!important;stroke:#aa3!important;stroke-width:1px!important}#mermaid-1575352666525 .cluster text{fill:#333}#mermaid-1575352666525 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:trebuchet ms,verdana,arial;font-size:12px;background:#ffffde;border:1px solid #aa3;border-radius:2px;pointer-events:none;z-index:100}#mermaid-1575352666525 .actor{stroke:#ccf;fill:#ececff}#mermaid-1575352666525 text.actor{fill:#000;stroke:none}#mermaid-1575352666525 .actor-line{stroke:grey}#mermaid-1575352666525 .messageLine0{marker-end:“url(#arrowhead)”}#mermaid-1575352666525 .messageLine0,#mermaid-1575352666525 .messageLine1{stroke-width:1.5;stroke-dasharray:“2 2”;stroke:#333}#mermaid-1575352666525 #arrowhead{fill:#333}#mermaid-1575352666525 #crosshead path{fill:#333!important;stroke:#333!important}#mermaid-1575352666525 .messageText{fill:#333;stroke:none}#mermaid-1575352666525 .labelBox{stroke:#ccf;fill:#ececff}#mermaid-1575352666525 .labelText,#mermaid-1575352666525 .loopText{fill:#000;stroke:none}#mermaid-1575352666525 .loopLine{stroke-width:2;stroke-dasharray:“2 2”;marker-end:“url(#arrowhead)”;stroke:#ccf}#mermaid-1575352666525 .note{stroke:#aa3;fill:#fff5ad}#mermaid-1575352666525 .noteText{fill:#000;stroke:none;font-family:trebuchet ms,verdana,arial;font-size:14px}#mermaid-1575352666525 .section{stroke:none;opacity:.2}#mermaid-1575352666525 .section0{fill:rgba(102,102,255,.49)}#mermaid-1575352666525 .section2{fill:#fff400}#mermaid-1575352666525 .section1,#mermaid-1575352666525 .section3{fill:#fff;opacity:.2}#mermaid-1575352666525 .sectionTitle0,#mermaid-1575352666525 .sectionTitle1,#mermaid-1575352666525 .sectionTitle2,#mermaid-1575352666525 .sectionTitle3{fill:#333}#mermaid-1575352666525 .sectionTitle{text-anchor:start;font-size:11px;text-height:14px}#mermaid-1575352666525 .grid .tick{stroke:#d3d3d3;opacity:.3;shape-rendering:crispEdges}#mermaid-1575352666525 .grid path{stroke-width:0}#mermaid-1575352666525 .today{fill:none;stroke:red;stroke-width:2px}#mermaid-1575352666525 .task{stroke-width:2}#mermaid-1575352666525 .taskText{text-anchor:middle;font-size:11px}#mermaid-1575352666525 .taskTextOutsideRight{fill:#000;text-anchor:start;font-size:11px}#mermaid-1575352666525 .taskTextOutsideLeft{fill:#000;text-anchor:end;font-size:11px}#mermaid-1575352666525 .taskText0,#mermaid-1575352666525 .taskText1,#mermaid-1575352666525 .taskText2,#mermaid-1575352666525 .taskText3{fill:#fff}#mermaid-1575352666525 .task0,#mermaid-1575352666525 .task1,#mermaid-1575352666525 .task2,#mermaid-1575352666525 .task3{fill:#8a90dd;stroke:#534fbc}#mermaid-1575352666525 .taskTextOutside0,#mermaid-1575352666525 .taskTextOutside1,#mermaid-1575352666525 .taskTextOutside2,#mermaid-1575352666525 .taskTextOutside3{fill:#000}#mermaid-1575352666525 .active0,#mermaid-1575352666525 .active1,#mermaid-1575352666525 .active2,#mermaid-1575352666525 .active3{fill:#bfc7ff;stroke:#534fbc}#mermaid-1575352666525 .activeText0,#mermaid-1575352666525 .activeText1,#mermaid-1575352666525 .activeText2,#mermaid-1575352666525 .activeText3{fill:#000!important}#mermaid-1575352666525 .done0,#mermaid-1575352666525 .done1,#mermaid-1575352666525 .done2,#mermaid-1575352666525 .done3{stroke:grey;fill:#d3d3d3;stroke-width:2}#mermaid-1575352666525 .doneText0,#mermaid-1575352666525 .doneText1,#mermaid-1575352666525 .doneText2,#mermaid-1575352666525 .doneText3{fill:#000!important}#mermaid-1575352666525 .crit0,#mermaid-1575352666525 .crit1,#mermaid-1575352666525 .crit2,#mermaid-1575352666525 .crit3{stroke:#f88;fill:red;stroke-width:2}#mermaid-1575352666525 .activeCrit0,#mermaid-1575352666525 .activeCrit1,#mermaid-1575352666525 .activeCrit2,#mermaid-1575352666525 .activeCrit3{stroke:#f88;fill:#bfc7ff;stroke-width:2}#mermaid-1575352666525 .doneCrit0,#mermaid-1575352666525 .doneCrit1,#mermaid-1575352666525 .doneCrit2,#mermaid-1575352666525 .doneCrit3{stroke:#f88;fill:#d3d3d3;stroke-width:2;cursor:pointer;shape-rendering:crispEdges}#mermaid-1575352666525 .activeCritText0,#mermaid-1575352666525 .activeCritText1,#mermaid-1575352666525 .activeCritText2,#mermaid-1575352666525 .activeCritText3,#mermaid-1575352666525 .doneCritText0,#mermaid-1575352666525 .doneCritText1,#mermaid-1575352666525 .doneCritText2,#mermaid-1575352666525 .doneCritText3{fill:#000!important}#mermaid-1575352666525 .titleText{text-anchor:middle;font-size:18px;fill:#000}#mermaid-1575352666525 g.classGroup text{fill:#9370db;stroke:none;font-family:trebuchet ms,verdana,arial;font-size:10px}#mermaid-1575352666525 g.classGroup rect{fill:#ececff;stroke:#9370db}#mermaid-1575352666525 g.classGroup line{stroke:#9370db;stroke-width:1}#mermaid-1575352666525 .classLabel .box{stroke:none;stroke-width:0;fill:#ececff;opacity:.5}#mermaid-1575352666525 .classLabel .label{fill:#9370db;font-size:10px}#mermaid-1575352666525 .relation{stroke:#9370db;stroke-width:1;fill:none}#mermaid-1575352666525 #compositionEnd,#mermaid-1575352666525 #compositionStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-1575352666525 #aggregationEnd,#mermaid-1575352666525 #aggregationStart{fill:#ececff;stroke:#9370db;stroke-width:1}#mermaid-1575352666525 #dependencyEnd,#mermaid-1575352666525 #dependencyStart,#mermaid-1575352666525 #extensionEnd,#mermaid-1575352666525 #extensionStart{fill:#9370db;stroke:#9370db;stroke-width:1}#mermaid-1575352666525 .branch-label,#mermaid-1575352666525 .commit-id,#mermaid-1575352666525 .commit-msg{fill:#d3d3d3;color:#d3d3d3}#mermaid-1575352666525 {


color: rgb(58, 65, 69);


font: normal normal 400 normal 18px / 33.3px “Hiragino Sans GB”, “Heiti SC”, “Microsoft YaHei”, sans-serif, Merriweather, serif;


}


Add/Update/Delete


Add/Update/Delete


Add StatefulSet


Loop


Get StatefulSet


Loop


Get StatefulSet


PodInformer


StatefulSetController


ReplicaSetInformer


worker1


queue


worker2


控制器 StatefulSetController 会在 Run 方法中启动多个 Goroutine 协程,这些协程会从队列中获取待处理的 StatefulSet 资源进行同步,接下来我们会先介绍 Kubernetes 同步 StatefulSet 的过程。


同步

StatefulSetController 使用 sync 方法同步 StatefulSet 资源,这是同步该资源的唯一入口,下面是这个方法的具体实现:


Go


func (ssc *StatefulSetController) sync(key string) error {  namespace, name, _ := cache.SplitMetaNamespaceKey(key)  set, _ := ssc.setLister.StatefulSets(namespace).Get(name)
ssc.adoptOrphanRevisions(set)
selector, _ := metav1.LabelSelectorAsSelector(set.Spec.Selector) pods, _ := ssc.getPodsForStatefulSet(set, selector)
return ssc.syncStatefulSet(set, pods)}
func (ssc *StatefulSetController) syncStatefulSet(set *apps.StatefulSet, pods []*v1.Pod) error { ssc.control.UpdateStatefulSet(set.DeepCopy(), pods); err != nil return nil}
复制代码


  1. 先重新获取 StatefulSet 对象;

  2. 收养集群中与 StatefulSet 有关的孤立控制器版本;

  3. 获取当前 StatefulSet 对应的全部 Pod 副本;

  4. 调用 syncStatefulSet 方法同步资源;


syncStatefulSet 方法只是将方法的调用转发到了一个 StatefulSetControlInterface 的实现 defaultStatefulSetControl 上,StatefulSetControlInterface 定义了用与控制 StatefulSet 和 Pod 副本的接口,这里调用的 UpdateStatefulSet 函数执行了一个 StatefulSet 的核心逻辑,它会负责获取 StatefulSet 版本、更新 StatefulSet 以及它的状态和历史:


Go


func (ssc *defaultStatefulSetControl) UpdateStatefulSet(set *apps.StatefulSet, pods []*v1.Pod) error {  revisions, err := ssc.ListRevisions(set)  history.SortControllerRevisions(revisions)
currentRevision, updateRevision, collisionCount, err := ssc.getStatefulSetRevisions(set, revisions)
status, err := ssc.updateStatefulSet(set, currentRevision, updateRevision, collisionCount, pods)
ssc.updateStatefulSetStatus(set, status)
return ssc.truncateHistory(set, pods, revisions, currentRevision, updateRevision)}
复制代码


它会使用默认的单调递增策略,按照升序依次创建副本并按照降序删除副本,当出现 Pod 处于不健康的状态时,那么新的 Pod 就不会被创建,StatefulSetController 会等待 Pod 恢复后继续执行下面的逻辑。


上述代码会在获取 StatefulSet 的历史版本之后调用 updateStatefulSet 方法开始更新 StatefulSet,这个将近 300 行的代码会按照执行以下的执行:


  1. 将当前 StatefulSet 持有的 Pod 副本按照序列号进行分组,超出数量的副本将被分入 condemned 中等待后续的删除操作,这次同步中需要保留的副本将进入 replicas 分组;

  2. 对当前的 StatefulSet 进行扩容,让集群达到目标的副本数;

  3. 获取副本数组中第一个不健康的 Pod;

  4. 根据副本的序列号检查各个副本的状态;

  5. 如果发现了失败的副本就会进行重启;

  6. 如果当前副本没有正常运行就会退出循环,直到当前副本达到正常运行的状态;

  7. 按照降序依次删除 condemned 数组中的副本;

  8. 按照降序依次更新 replicas 数组中的副本;


Go


func (ssc *defaultStatefulSetControl) updateStatefulSet(set *apps.StatefulSet, currentRevision *apps.ControllerRevision, updateRevision *apps.ControllerRevision, collisionCount int32, pods []*v1.Pod) (*apps.StatefulSetStatus, error) {  currentSet, _ := ApplyRevision(set, currentRevision)  updateSet, _ := ApplyRevision(set, updateRevision)
status := apps.StatefulSetStatus{} // ...
replicaCount := int(*set.Spec.Replicas) replicas := make([]*v1.Pod, replicaCount) condemned := make([]*v1.Pod, 0, len(pods)) unhealthy := 0 var firstUnhealthyPod *v1.Pod
for i := range pods { if ord := getOrdinal(pods[i]); 0 <= ord && ord < replicaCount { replicas[ord] = pods[i] } else if ord >= replicaCount { condemned = append(condemned, pods[i]) } }
sort.Sort(ascendingOrdinal(condemned))
for ord := 0; ord < replicaCount; ord++ { if replicas[ord] == nil { replicas[ord] = newVersionedStatefulSetPod(currentSet, updateSet, currentRevision.Name, updateRevision.Name, ord) } }
复制代码


这里通过 StatefulSet 应该持有的副本数对当前的副本进行分组,一部分是需要保证存活的 replicas,另一部分是需要被终止的副本 condemned,如果分组后的 replicas 数量不足,就会通过 newVersionedStatefulSetPod 函数创建新的 Pod,不过这里的 Pod 也只是待创建的模板。



拿到线上应该存在的 replicas 数组时,我们就可以进行通过 CreateStatefulPod 进行扩容了,每个 Pod 的更新和创建都会等待前面所有 Pod 正常运行,它会调用 isFailedisCreatedisTerminating 等方法保证每一个 Pod 都正常运行时才会继续处理下一个 Pod,如果使用滚动更新策略,那么会在完成扩容之后才会对当前的 Pod 进行更新:


Go


for i := range replicas {    if isFailed(replicas[i]) {      ssc.podControl.DeleteStatefulPod(set, replicas[i])      replicas[i] = newVersionedStatefulSetPod(i)    }    if !isCreated(replicas[i]) {      ssc.podControl.CreateStatefulPod(set, replicas[i])      return &status, nil    }    if isTerminating(replicas[i]) || !isRunningAndReady(replicas[i]) {      return &status, nil    }    if identityMatches(set, replicas[i]) && storageMatches(set, replicas[i]) {      continue    }    replica := replicas[i].DeepCopy()    ssc.podControl.UpdateStatefulPod(updateSet, replica)  }
复制代码


StatefulSetController 处理完副本的创建和更新任务之后,就开始删除需要抛弃的节点了,节点的删除也需要确定按照降序依次进行:


Go


for target := len(condemned) - 1; target >= 0; target-- {    if isTerminating(condemned[target]) {      return &status, nil    }    if !isRunningAndReady(condemned[target]) && condemned[target] != firstUnhealthyPod {      return &status, nil    }
ssc.podControl.DeleteStatefulPod(set, condemned[target]) return &status, nil }
updateMin := 0 if set.Spec.UpdateStrategy.RollingUpdate != nil { updateMin = int(*set.Spec.UpdateStrategy.RollingUpdate.Partition) } for target := len(replicas) - 1; target >= updateMin; target-- { if getPodRevision(replicas[target]) != updateRevision.Name && !isTerminating(replicas[target]) { ssc.podControl.DeleteStatefulPod(set, replicas[target]) return &status, err }
if !isHealthy(replicas[target]) { return &status, nil } } return &status, nil}
复制代码


我们首先会删除待抛弃列表中的副本,其次根据滚动更新 RollingUpdate 的配置从高到低依次删除所有 Pod 版本已经过时的节点,所有删除节点的方式都会通过 DeleteStatefulPod 方法进行,该方法会通过客户端的接口直接根据 Pod 名称删除对应的资源。


序列号

Pod 的序列号(Ordinal)是其唯一性和顺序性的保证,在创建和删除 StatefulSet 的副本时,我们都需要按照 Pod 的序列号对它们按照顺序操作,副本的创建会按照序列号升序处理,副本的更新和删除会按照序列号降序处理。



创建 StatefulSet 中的副本时,就会在 newStatefulSetPod 函数中传入当前 Pod 的 ordinal 信息,该方法会调用 GetPodFromTemplate 获取 StatefulSet 中的 Pod 模板并且初始化 Pod 的 metadata 和引用等配置:


Go


func newStatefulSetPod(set *apps.StatefulSet, ordinal int) *v1.Pod {  pod, _ := controller.GetPodFromTemplate(&set.Spec.Template, set, metav1.NewControllerRef(set, controllerKind))  pod.Name = getPodName(set, ordinal)  initIdentity(set, pod)  updateStorage(set, pod)  return pod}
func getPodName(set *apps.StatefulSet, ordinal int) string { return fmt.Sprintf("%s-%d", set.Name, ordinal)}
复制代码


getPodName 函数的实现非常简单,它将 StatefulSet 的名字和传入的序列号通过破折号连接起来组成我们经常见到的 web-0web-1 等形式的副本名;initIdentity 会更新 Pod 的主机名、资源名、命名空间标签,而 updateStorage 会为待创建的副本设置卷:


Go


func updateStorage(set *apps.StatefulSet, pod *v1.Pod) {  currentVolumes := pod.Spec.Volumes  claims := getPersistentVolumeClaims(set, pod)  newVolumes := make([]v1.Volume, 0, len(claims))  for name, claim := range claims {    newVolumes = append(newVolumes, v1.Volume{      Name: name,      VolumeSource: v1.VolumeSource{        PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{          ClaimName: claim.Name,          ReadOnly: false,        },      },    })  }  for i := range currentVolumes {    if _, ok := claims[currentVolumes[i].Name]; !ok {      newVolumes = append(newVolumes, currentVolumes[i])    }  }  pod.Spec.Volumes = newVolumes}
复制代码


设置卷的配置主要来自于 StatefulSet 规格中的 volumeClaimTemplates 模板,所有卷相关的配置信息都会通过该方法传递过来。


Pod 通过当前名字存储自己对应的序列号,在 StatefulSetController 同步时就会从 Pod 的名字中取出序列号并进行排序,随后的各种循环就可以选择使用正序或者倒序的方式依次处理各个节点了。


删除

当我们删除一个 Kubernetes 中的 StatefulSet 资源时,它对应的全部 Pod 副本都会被 垃圾收集器 自动删除,该收集器在检查到当前 Pod 的 metadata.ownerReferences 已经不再存在时就会删除 Pod 资源,读者可以阅读 垃圾收集器 了解具体的执行过程和实现原理。


Go


$ kubectl delete statefulsets.apps webstatefulset.apps "web" deleted
$ kubectl get pods --watchNAME READY STATUS RESTARTS AGEweb-2 1/1 Terminating 0 14hweb-1 1/1 Terminating 0 14hweb-0 1/1 Terminating 0 14h
复制代码


我们会发现除了 StatefulSet 和 Pod 之外的任何其他资源都没有被删除,之前创建的 PersistentVolumePersistentVolumeClaim 对象都没有发生任何的变化,这也是 StatefulSet 的行为,它会在服务被删除之后仍然保留其中的状态,也就是数据,这些数据就都存储在 PersistentVolume 中。


如果我们重新创建相同的 StatefulSet,它还会使用之前的 PV 和 PVC 对象,不过也可以选择手动删除所有的 PV 和 PVC 来生成新的存储,这两个对象都属于 Kubernetes 的存储系统,感兴趣的读者可以通过 存储系统 了解 Kubernetes 中 Volume 的设计和实现。


总结

StatefulSet 是 Kubernetes 为了处理有状态服务引入的概念,在有状态服务中,它为无序和短暂的 Pod 引入了顺序性和唯一性,使得 Pod 的创建和删除更容易被掌控和预测,同时加入 PV 和 PVC 对象来存储这些 Pod 的状态,我们可以使用 StatefulSet 实现一些偏存储的有状态系统,例如 Zookeeper、Kafka、MongoDB 等,这些系统大多数都需要持久化的存储数据,防止在服务宕机时发生数据丢失。


相关文章


Reference


**本文转载自 Draveness 技术博客。


原文链接:https://draveness.me/kubernetes-statefulset


2019 年 12 月 03 日 15:12409

评论

发布
暂无评论
发现更多内容

阿里混沌工程平台实践

心远

阿里巴巴 混沌工程

一文搞懂 Flink Stream Join原理

shengjk1

flink源码 flink join

Flink SQL 自定义 Source format

shengjk1

flink sql

产品 0 期 - 第八周作业

曾烧麦

产品训练营

2021金三银四总结面试必备清单:字节/蚂蚁金服/腾讯/百度

比伯

Java 程序员 架构 互联网 技术宅

用户路径地图

王一凡

云原生AI成过去式,AI原生的云才是通向未来的“藏宝图”

脑极体

一文搞懂 Flink 处理水印全过程

shengjk1

this指向

y

this指针

第八章作业-用户路劲图

z

产品经理训练营第八章作业

新盛

产品经理第 0 期训练营第九周作业提交

Krystal

牛掰!阿里新产12万字大厂面试总结已收录Github,涵盖字节、滴滴、美团、腾讯、百度、京东等诸多大厂面试真题

程序员小毕

Java 程序员 架构 面试 分布式

用户路径地图+漏斗模型总结

mas

你有没有领导力?

石云升

领导力 28天写作 职场经验 管理经验 3月日更

继承

y

继承

直播预告|4月1日与你相约腾讯云共探TcaplusDB

TcaplusDB

数据库 nosql 后端 TcaplusDB

systemedctl使用指南

happlyfox

3月日更

携手百度智能云推动实现工业AR空间智能化

百度大脑

AR 百度智能云

maven项目下的重复依赖校验

程序员架构进阶

maven 持续集成 28天写作 3月日更 Jar包扫描

用户路径地图+漏斗模型

mas

架构师:始于理性思考,成于科学实践——郭东白老师分享总结

青春不可负,生活不可欺

成长 架构设计 架构师

结合 Flink 学习装饰者模式

shengjk1

flink源码 flink源码分析

作业 - 第八章 数据分析

hao hao

数据分析

王一凡

“韩剧TV”用户路径地图

李钊悌

「架构师训练营 4 期」 第十二周 - 001&2

凯迪

架构师训练营 4 期

产品经理训练营 Week11 学习心得

Mai

Vue中如何在线预览pdf文件

y

Android 系统开发做什么?

吴小龙同学

产品经理训练营 Week11 作业

Mai

Study Go: From Zero to Hero

Study Go: From Zero to Hero

详解 Kubernetes StatefulSet 实现原理-InfoQ