StatefulSet Controller

K8s对无状态的情况支持的很好,但是对有状态的也提供了支持,但是支持的并没有那么好。对有状态的支持逻辑都在stateful controller 里。在继续之前,我们先看一下什么叫有状态,什么叫无状态。

  • Stateless Service
    是每个节点都是对等的。 任何一个节点都是可以被替换的。服务运行的实例不会在本地存储需要持久化的数据,并且多个实例对于同一个请求响应的结果是完全一致的。
  • Stateful Service
    是每个节点不是对等,一个节点不可以被其他节点随意替换。该服务的实例可以将一部分数据随时进行备份,并且在创建一个新的有状态服务时,可以通过备份恢复这些数据,以达到数据持久化的目的。通常由状态服务比如msql,zookeeper,redis集群等等。


    image.png

那么stateful 和 stateless 对k8s 的需求也是不一样的

  • 无状态:
    认为Pod都是一样的
    部署没有顺序要求
    不用考虑在哪个node运行
    随意进行伸缩和扩展
  • (StatefulSet)有状态:
    无状态的因素有需要考虑到
    让每个Pod独立,保持Pod启动顺序和唯一性
    唯一的网络标识符,持久存续
    有序部署,比如Mysql主从

StatefulSet

StatefulSet 作为 Controller 为 Pod 提供唯⼀的标识。 它可以保证部署和 scale 的顺序。
StatefulSet是为了解决有状态服务的问题(对应Deployments和ReplicaSets是为⽆状态服务⽽设计) , 其应⽤场景包括:

  • 稳定的持久化存储, 即Pod重新调度后还是能访问到相同的持久化数据, 基于PVC来实现
  • 稳定的⽹络标志, 即Pod重新调度后其PodName和HostName不变, 基于Headless Service(即没有Cluster IP的Service) 来实现
  • 有序部署, 有序扩展, 即Pod是有顺序的, 在部署或者扩展的时候要依据定义的顺序依次依次进⾏(即从0到N-1,在下⼀个Pod运⾏之前所有之前的Pod必须都是Running和Ready状态) , 基于init containers来实现有序收缩, 有序删除(即从N-1到0)

从上⾯的应⽤场景可以发现, StatefulSet由以下⼏个部分组成:

  • ⽤于定义⽹络标志(DNS domain) 的Headless Service
  • ⽤于创建PersistentVolumes的volumeClaimTemplates
  • 定义具体应⽤的StatefulSet

StatefulSet中每个Pod的DNS格式为 statefulSetName-{0..N-1}.serviceName.namespace.svc.cluster.local , 其中

  • serviceName 为Headless Service的名字0..N-1 为Pod所在的序号, 从0开始到N-1
  • statefulSetName 为StatefulSet的名字
  • namespace 为服务所在的namespace, Headless Servic和StatefulSet必须在相同的namespace
  • .cluster.local 为Cluster Domain

部署和Scale保证:

  • 对于有N个副本的StatefulSet,Pod将按照{0..N-1}的顺序被创建和部署。
  • 当删除Pod的时候,将按照逆序来终结,从{N-1..0}
  • 对Pod执⾏scale操作之前,它所有的前任必须处于Running和Ready状态。
  • 在终⽌Pod前,它所有的继任者必须处于完全关闭状态。

Headless Service:
在deployment中,每一个pod是没有名称,是随机字符串,是无序的。而statefulset中是要求有序的,每一个pod的名称必须是固定的。当节点挂了,重建之后的标识符是不变的,每一个节点的节点名称是不能改变的。pod名称是作为pod识别的唯一标识符,必须保证其标识符的稳定并且唯一。
为了实现标识符的稳定,这时候就需要一个headless service 解析直达到pod,还需要给pod配置一个唯一的名称。

volumeClainTemplate:
大部分有状态副本集都会用到持久存储,比如分布式系统来说,由于数据是不一样的,每个节点都需要自己专用的存储节点。而在deployment中pod模板中创建的存储卷是一个共享的存储卷,多个pod使用同一个存储卷,而statefulset定义中的每一个pod都不能使用同一个存储卷,由此基于pod模板创建pod是不适应的,这就需要引入volumeClainTemplate,当在使用statefulset创建pod时,会自动生成一个PVC,从而请求绑定一个PV,从而有自己专用的存储卷。Pod名称、PVC和PV关系图如下:

image.png

PV和PVC是一一对应关系,当有PV被某个PVC所占用时,会显示banding,其它PVC不能再使用绑定过的PV。
PVC一旦绑定PV,就相当于是一个存储卷,此时PVC可以被多个Pod所使用。(PVC支不支持被多个Pod访问,取决于访问模型accessMode的定义)。
PVC若没有找到合适的PV时,则会处于pending状态。
PV是属于集群级别的,不能定义在名称空间中。
PVC时属于名称空间级别的。
PV的reclaim policy选项:
默认是Retain保留,保留生成的数据。
可以改为recycle回收,删除生成的数据,回收pv
delete,删除,pvc解除绑定后,pv也就自动删除。

例:在nfs服务器上,使用nfs创建多个数据卷,在k8s集群中定义PV/PVC,让pod关联PVC
1.在/data/volumes/创建多个目录:v1,v2,v3,v4,v5,修改/etc/exports文件,使数据卷单独输出。

1 /data/volumes/v1  192.168.1.0/24(rw,no_root_squash)
2 /data/volumes/v2  192.168.1.0/24(rw,no_root_squash)
3 /data/volumes/v3  192.168.1.0/24(rw,no_root_squash)
4 /data/volumes/v4  192.168.1.0/24(rw,no_root_squash)
5 /data/volumes/v5  192.168.1.0/24(rw,no_root_squash)

exportfs -arv:显示nfs当前输出的可以挂载的数据卷。
showmount -e :显示nfs导出的数据卷
2.在k8s集群中定义pv:
vim pv-demo.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv001
  labels:
    name: pv001
spec:
  nfs:
    path: /data/volumes/v1
    server: k8s-node2
  accessModes: ["ReadWriteMany","ReadWriteOnce","ReadOnlyMany"]
  capacity:
    storage: 2Gi
---
...pv002
---
...pv003
    ...创建5个以上内容,修改name和path等参数

应用:kubectl apply -f pv-demo.yaml
查看:kubectl get pv

NAME    CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM           STORAGECLASS   REASON   AGE
pv001   2Gi        RWO,ROX,RWX    Retain           Available                                           56m
pv002   5Gi        RWO,ROX        Retain           Available                                           56m
pv003   3Gi        ROX,RWX        Retain           Available                                           56m
pv004   10Gi       RWO            Retain           Available                                           56m
pv005   15Gi       ROX            Retain           Available                                           56m

创建PVC和pod

vim pod-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mypvc
  namespace: default
spec:
  accessModes: ["ReadWriteOnce"]  ##注:这个访问模型必须是PV中存在的访问模型的子集,否则会找不到合适的PV,会一直Pending。
  resources:
    requests:
      storage: 6Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: myapp-pv
  namespace: default
spec:
  containers:
  - name: myapp
    image: ikubernetes/myapp:v1
    imagePullPolicy: IfNotPresent
    ports:
    - name: html
      containerPort: 80
    volumeMounts:
    - name: nfs
      mountPath: "/usr/share/nginx/html"
  volumes:
  - name: nfs
    persistentVolumeClaim:
      claimName: mypvc

应用:kubectl apply -f pod-pvc.yaml

查看pvc:kubectl get pvc
NAME    STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
mypvc   Bound    pv004    10Gi       RWO                           4s
查看pv:kubectl get pv
NAME    CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM           STORAGECLASS   REASON   AGE
pv001   2Gi        RWO,ROX,RWX    Retain           Available                                           56m
pv002   5Gi        RWO,ROX        Retain           Available                                           56m
pv003   3Gi        ROX,RWX        Retain           Available                                           56m
pv004   10Gi       RWO            Retain           Bound       default/mypvc                           56m
pv005   15Gi       ROX            Retain           Available                                           56m

如此pvc就绑定在合适的pv上了。绑定的pv是pv004,说明pod的/usr/share/nginx/html挂载在nfs服务器/data/volumes/v4目录下。
4.查看pod:kubectl get pods -owide

1 NAME       READY   STATUS    RESTARTS   AGE   IP             NODE        NOMINATED NODE   READINESS GATES
2 myapp-pv   1/1     Running   0          24s   10.244.1.141   k8s-node2   <none>           <none>

statefulset controller

tatefulset controller是kube-controller-manager组件中众多控制器中的一个,是 statefulset 资源对象的控制器,其通过对statefulset、pod资源的监听,当资源发生变化时会触发 statefulset controller 对相应的statefulset资源对象进行调谐操作,从而完成statefulset对于pod的创建、删除、更新、扩缩容、statefulset的滚动更新、statefulset状态status更新、旧版本statefulset清理等操作。

statefulset controller架构图

statefulset controller的大致组成和处理流程如下图,statefulset controller对statefulset、pod对象注册了event handler,当有事件时,会watch到然后将对应的statefulset对象放入到queue中,然后syncStatefulSet方法为statefulset controller调谐statefulset对象的核心处理逻辑所在,从queue中取出statefulset对象,做调谐处理。


image.png

statefulset controller初始化与启动分析

startStatefulSetController函数,作为statefulset controller初始化与启动分析的入口。

startStatefulSetController

startStatefulSetController主要逻辑:

  • 调用statefulset.NewStatefulSetController新建并初始化StatefulSetController;
  • 拉起一个goroutine,跑StatefulSetController的Run方法。
// cmd/kube-controller-manager/app/apps.go
func startStatefulSetController(ctx ControllerContext) (http.Handler, bool, error) {
    if !ctx.AvailableResources[schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "statefulsets"}] {
        return nil, false, nil
    }
    go statefulset.NewStatefulSetController(
        ctx.InformerFactory.Core().V1().Pods(),
        ctx.InformerFactory.Apps().V1().StatefulSets(),
        ctx.InformerFactory.Core().V1().PersistentVolumeClaims(),
        ctx.InformerFactory.Apps().V1().ControllerRevisions(),
        ctx.ClientBuilder.ClientOrDie("statefulset-controller"),
    ).Run(int(ctx.ComponentConfig.StatefulSetController.ConcurrentStatefulSetSyncs), ctx.Stop)
    return nil, true, nil
}

statefulset.NewStatefulSetController

从statefulset.NewStatefulSetController函数代码中可以看到,statefulset controller注册了statefulset、pod对象的EventHandler,也即对这几个对象的event进行监听,把event放入事件队列并做处理,对statefulset controller做了初始化

// pkg/controller/statefulset/stateful_set.go
func NewStatefulSetController(
    podInformer coreinformers.PodInformer,
    setInformer appsinformers.StatefulSetInformer,
    pvcInformer coreinformers.PersistentVolumeClaimInformer,
    revInformer appsinformers.ControllerRevisionInformer,
    kubeClient clientset.Interface,
) *StatefulSetController {
    eventBroadcaster := record.NewBroadcaster()
    eventBroadcaster.StartLogging(klog.Infof)
    eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: kubeClient.CoreV1().Events("")})
    recorder := eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "statefulset-controller"})

    ssc := &StatefulSetController{
        kubeClient: kubeClient,
        control: NewDefaultStatefulSetControl(
            NewRealStatefulPodControl(
                kubeClient,
                setInformer.Lister(),
                podInformer.Lister(),
                pvcInformer.Lister(),
                recorder),
            NewRealStatefulSetStatusUpdater(kubeClient, setInformer.Lister()),
            history.NewHistory(kubeClient, revInformer.Lister()),
            recorder,
        ),
        pvcListerSynced: pvcInformer.Informer().HasSynced,
        queue:           workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "statefulset"),
        podControl:      controller.RealPodControl{KubeClient: kubeClient, Recorder: recorder},

        revListerSynced: revInformer.Informer().HasSynced,
    }

    podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
        // lookup the statefulset and enqueue
        AddFunc: ssc.addPod,
        // lookup current and old statefulset if labels changed
        UpdateFunc: ssc.updatePod,
        // lookup statefulset accounting for deletion tombstones
        DeleteFunc: ssc.deletePod,
    })
    ssc.podLister = podInformer.Lister()
    ssc.podListerSynced = podInformer.Informer().HasSynced

    setInformer.Informer().AddEventHandler(
        cache.ResourceEventHandlerFuncs{
            AddFunc: ssc.enqueueStatefulSet,
            UpdateFunc: func(old, cur interface{}) {
                oldPS := old.(*apps.StatefulSet)
                curPS := cur.(*apps.StatefulSet)
                if oldPS.Status.Replicas != curPS.Status.Replicas {
                    klog.V(4).Infof("Observed updated replica count for StatefulSet: %v, %d->%d", curPS.Name, oldPS.Status.Replicas, curPS.Status.Replicas)
                }
                ssc.enqueueStatefulSet(cur)
            },
            DeleteFunc: ssc.enqueueStatefulSet,
        },
    )
    ssc.setLister = setInformer.Lister()
    ssc.setListerSynced = setInformer.Informer().HasSynced

    // TODO: Watch volumes
    return ssc
}

Run

主要看到for循环处,根据workers的值(可通过kube-controller-manager组件启动参数concurrent-statefulset-syncs来设置,默认值为5),启动相应数量的goroutine,跑ssc.worker方法,调用daemonset controller核心处理方法ssc.sync来调谐statefulset对象。

// pkg/controller/statefulset/stateful_set.go
func (ssc *StatefulSetController) Run(workers int, stopCh <-chan struct{}) {
    defer utilruntime.HandleCrash()
    defer ssc.queue.ShutDown()

    klog.Infof("Starting stateful set controller")
    defer klog.Infof("Shutting down statefulset controller")

    if !cache.WaitForNamedCacheSync("stateful set", stopCh, ssc.podListerSynced, ssc.setListerSynced, ssc.pvcListerSynced, ssc.revListerSynced) {
        return
    }

    for i := 0; i < workers; i++ {
        go wait.Until(ssc.worker, time.Second, stopCh)
    }

    <-stopCh
}

statefulset controller核心处理逻辑分析

直接看到statefulset controller核心处理入口sync方法。
主要逻辑:

  • 获取执行方法时的当前时间,并定义defer函数,用于计算该方法总执行时间,也即统计对一个 statefulset 进行同步调谐操作的耗时;
  • 根据 statefulset 对象的命名空间与名称,获取 statefulset 对象;
  • 调用ssc.adoptOrphanRevisions,检查是否有孤儿 controllerrevisions 对象(即.spec.ownerReferences中无controller属性定义或其属性值为false),若有且其与 statefulset 对象的selector匹配 的则添加 ownerReferences 进行关联;
  • 调用ssc.getPodsForStatefulSet,根据 statefulset 对象的selector去查找pod列表,且若有孤儿pod的label与 statefulset 的selector能匹配的则进行关联,若已关联的pod的label不再与statefulset的selector匹配,则更新解除它们的关联关系;
  • 调用ssc.syncStatefulSet,对 statefulset 对象做调谐处理。
// pkg/controller/statefulset/stateful_set.go
func (ssc *StatefulSetController) sync(key string) error {
    startTime := time.Now()
    defer func() {
        klog.V(4).Infof("Finished syncing statefulset %q (%v)", key, time.Since(startTime))
    }()

    namespace, name, err := cache.SplitMetaNamespaceKey(key)
    if err != nil {
        return err
    }
    set, err := ssc.setLister.StatefulSets(namespace).Get(name)
    if errors.IsNotFound(err) {
        klog.Infof("StatefulSet has been deleted %v", key)
        return nil
    }
    if err != nil {
        utilruntime.HandleError(fmt.Errorf("unable to retrieve StatefulSet %v from store: %v", key, err))
        return err
    }

    selector, err := metav1.LabelSelectorAsSelector(set.Spec.Selector)
    if err != nil {
        utilruntime.HandleError(fmt.Errorf("error converting StatefulSet %v selector: %v", key, err))
        // This is a non-transient error, so don't retry.
        return nil
    }

    if err := ssc.adoptOrphanRevisions(set); err != nil {
        return err
    }

    pods, err := ssc.getPodsForStatefulSet(set, selector)
    if err != nil {
        return err
    }

    return ssc.syncStatefulSet(set, pods)
}

ssc.getPodsForStatefulSet

ssc.getPodsForStatefulSet方法主要作用是获取属于 statefulset 对象的pod列表并返回,并检查孤儿pod与已匹配的pod,看是否需要更新statefulset与pod的匹配。

主要逻辑:

  • 获取 statefulset 所在命名空间下的所有pod;
  • 定义过滤出属于 statefulset 对象的pod的函数,即isMemberOf函数(根据pod的名称与statefulset名称匹配来过滤属于statefulset的pod);
  • 调用cm.ClaimPods,过滤出属于该statefulset对象的pod,且若有孤儿pod的label与 statefulset 的selector能匹配的则进行关联,若已关联的pod的label不再与statefulset的selector匹配,则更新解除它们的关联关系。
// pkg/controller/statefulset/stateful_set.go
func (ssc *StatefulSetController) getPodsForStatefulSet(set *apps.StatefulSet, selector labels.Selector) ([]*v1.Pod, error) {
    // List all pods to include the pods that don't match the selector anymore but
    // has a ControllerRef pointing to this StatefulSet.
    pods, err := ssc.podLister.Pods(set.Namespace).List(labels.Everything())
    if err != nil {
        return nil, err
    }

    filter := func(pod *v1.Pod) bool {
        // Only claim if it matches our StatefulSet name. Otherwise release/ignore.
        return isMemberOf(set, pod)
    }

    // If any adoptions are attempted, we should first recheck for deletion with
    // an uncached quorum read sometime after listing Pods (see #42639).
    canAdoptFunc := controller.RecheckDeletionTimestamp(func() (metav1.Object, error) {
        fresh, err := ssc.kubeClient.AppsV1().StatefulSets(set.Namespace).Get(set.Name, metav1.GetOptions{})
        if err != nil {
            return nil, err
        }
        if fresh.UID != set.UID {
            return nil, fmt.Errorf("original StatefulSet %v/%v is gone: got uid %v, wanted %v", set.Namespace, set.Name, fresh.UID, set.UID)
        }
        return fresh, nil
    })

ssc.syncStatefulSet

ssc.syncStatefulSet方法可以说是statefulset controller的核心处理逻辑所在了,主要看到ssc.control.UpdateStatefulSet方法。

// pkg/controller/statefulset/stateful_set.go
func (ssc *StatefulSetController) syncStatefulSet(set *apps.StatefulSet, pods []*v1.Pod) error {
    klog.V(4).Infof("Syncing StatefulSet %v/%v with %d pods", set.Namespace, set.Name, len(pods))
    // TODO: investigate where we mutate the set during the update as it is not obvious.
    if err := ssc.control.UpdateStatefulSet(set.DeepCopy(), pods); err != nil {
        return err
    }
    klog.V(4).Infof("Successfully synced StatefulSet %s/%s successful", set.Namespace, set.Name)
    return nil
}

ssc.control.UpdateStatefulSet方法主要逻辑:

  • 获取statefulset的所有ControllerRevision并根据版本新老顺序排序;
  • 调用ssc.getStatefulSetRevisions,获取现存最新的statefulset版本以及计算出一个新的版本;
  • 调用ssc.updateStatefulSet,完成statefulset对象对于pod的创建、删除、更新、扩缩容等操作;
  • 调用ssc.updateStatefulSetStatus,更新statefulset对象的status状态;
  • 调用ssc.truncateHistory,根据statefulset对象配置的历史版本数量限制,按之前的排序顺序清理掉没有pod的statefulset历史版本。
// pkg/controller/statefulset/stateful_set_control.go
func (ssc *defaultStatefulSetControl) UpdateStatefulSet(set *apps.StatefulSet, pods []*v1.Pod) error {

    // list all revisions and sort them
    revisions, err := ssc.ListRevisions(set)
    if err != nil {
        return err
    }
    history.SortControllerRevisions(revisions)

    // get the current, and update revisions
    currentRevision, updateRevision, collisionCount, err := ssc.getStatefulSetRevisions(set, revisions)
    if err != nil {
        return err
    }

    // perform the main update function and get the status
    status, err := ssc.updateStatefulSet(set, currentRevision, updateRevision, collisionCount, pods)
    if err != nil {
        return err
    }

    // update the set's status
    err = ssc.updateStatefulSetStatus(set, status)
    if err != nil {
        return err
    }

    klog.V(4).Infof("StatefulSet %s/%s pod status replicas=%d ready=%d current=%d updated=%d",
        set.Namespace,
        set.Name,
        status.Replicas,
        status.ReadyReplicas,
        status.CurrentReplicas,
        status.UpdatedReplicas)

    klog.V(4).Infof("StatefulSet %s/%s revisions current=%s update=%s",
        set.Namespace,
        set.Name,
        status.CurrentRevision,
        status.UpdateRevision)

    // maintain the set's revision history limit
    return ssc.truncateHistory(set, pods, revisions, currentRevision, updateRevision)
}
    cm := controller.NewPodControllerRefManager(ssc.podControl, set, selector, controllerKind, canAdoptFunc)
    return cm.ClaimPods(pods, filter)
}

ssc.updateStatefulSet

updateStatefulSet方法是statefulset对象调谐操作中的核心方法,完成statefulset对象对于pod的创建、删除、更新、扩缩容等操作。此方法代码比较长,跟随我的节奏慢慢分析。

主要逻辑:
(1)看到第一个for循环,将statefulset的所有pod按ord(ord即为pod name中的序号)的值分到replicas和condemned两个数组中,序号小于statefulset对象期望副本数值的放到replicas数组(因为序号从0开始,所以是小于期望副本数值),大于等于期望副本数值的放到condemned数组,replicas数组代表正常的可用的pod列表,condemned数组中的是需要被删除的pod列表;在遍历pod时,同时根据pod的状态计算statefulset对象的status值;
(2)第二个for循环,当序号小于statefulset期望副本数值的pod未创建出来时,则根据statefulset对象中的pod模板,构建出相应序号值的pod对象(此时还并没有向apiserver发起创建pod的请求,只是构建好pod结构体);
(3)第三个和第四个for循环,遍历replicas和condemned两个数组,找到非healthy状态的最小序号的pod记录下来,并记录序号;
(4)当statefulset对象的DeletionTimestamp不为nil时,直接返回前面计算出来的statefulset的新status值,不再进行方法后续的逻辑处理;
(5)获取monotonic的值,当statefulset.Spec.PodManagementPolicy的值为Parallel时,monotonic的值为false,否则为true(Parallel代表statefulset controller可以并行的处理同一statefulset的pod,串行则代表在启动和终止下一个pod之前需要等待前一个pod变成ready状态或pod对象被删除掉);
(6)第五个for循环,遍历replicas数组,处理statefulset的pod,主要是做pod的创建(包括根据statefulset.Spec.VolumeClaimTemplates中定义的pod所需的pvc的创建):
(6.1)当pod处于fail状态(pod.Status.Phase的值为Failed)时,调用apiserver删除该pod(pod对应的pvc在这里不会做删除操作)并给replicas数组构建相应序号的新的pod结构体(用于下一步中重新创建该序号的pod);
(6.2)如果相应序号的pod未创建时,调用apiserver创建该序号的pod(包括创建pvc),且当monotonic为true时(statefulset没有配置Parallel),直接return,结束updateStatefulSet方法的执行;
(6.3)剩下的逻辑就是当没有配置Parallel时,将串行处理pod,在启动和终止下一个pod之前需要等待前一个pod变成ready状态或pod对象被删除掉,不再展开分析;
(7)第六个for循环,逆序(pod序号从大到小)遍历condemned数组,处理statefulset的pod,主要是做多余pod的删除,删除逻辑也受Parallel影响,不展开分析。
(8)判断statefulset的更新策略,若为OnDelete,则直接return(使用了该更新策略,则需要人工删除pod后才会重建相应序号的pod);
(9)获取滚动更新配置中的Partition值,当statefulset进行滚动更新时,小于等于该序号的pod将不会被更新;
(10)第七个for循环,主要是处理更新策略为RollingUpdate的statefulset对象的更新。statefulset的滚动更新,从序号大到小的顺序对pod进行更新,先删除后创建,且需等待当前序号的pod再次创建完成且状态为Ready时才能进行下一个pod的更新处理。

// pkg/controller/statefulset/stateful_set_control.go
func (ssc *defaultStatefulSetControl) updateStatefulSet(
    set *apps.StatefulSet,
    currentRevision *apps.ControllerRevision,
    updateRevision *apps.ControllerRevision,
    collisionCount int32,
    pods []*v1.Pod) (*apps.StatefulSetStatus, error) {
    // get the current and update revisions of the set.
    currentSet, err := ApplyRevision(set, currentRevision)
    if err != nil {
        return nil, err
    }
    updateSet, err := ApplyRevision(set, updateRevision)
    if err != nil {
        return nil, err
    }

    // set the generation, and revisions in the returned status
    status := apps.StatefulSetStatus{}
    status.ObservedGeneration = set.Generation
    status.CurrentRevision = currentRevision.Name
    status.UpdateRevision = updateRevision.Name
    status.CollisionCount = new(int32)
    *status.CollisionCount = collisionCount

    replicaCount := int(*set.Spec.Replicas)
    // slice that will contain all Pods such that 0 <= getOrdinal(pod) < set.Spec.Replicas
    replicas := make([]*v1.Pod, replicaCount)
    // slice that will contain all Pods such that set.Spec.Replicas <= getOrdinal(pod)
    condemned := make([]*v1.Pod, 0, len(pods))
    unhealthy := 0
    firstUnhealthyOrdinal := math.MaxInt32
    var firstUnhealthyPod *v1.Pod
    
    // 第一个for循环,将statefulset的pod分到replicas和condemned两个数组中,其中condemned数组中的pod代表需要被删除的
    // First we partition pods into two lists valid replicas and condemned Pods
    for i := range pods {
        status.Replicas++

        // count the number of running and ready replicas
        if isRunningAndReady(pods[i]) {
            status.ReadyReplicas++
        }

        // count the number of current and update replicas
        if isCreated(pods[i]) && !isTerminating(pods[i]) {
            if getPodRevision(pods[i]) == currentRevision.Name {
                status.CurrentReplicas++
            }
            if getPodRevision(pods[i]) == updateRevision.Name {
                status.UpdatedReplicas++
            }
        }

        if ord := getOrdinal(pods[i]); 0 <= ord && ord < replicaCount {
            // if the ordinal of the pod is within the range of the current number of replicas,
            // insert it at the indirection of its ordinal
            replicas[ord] = pods[i]

        } else if ord >= replicaCount {
            // if the ordinal is greater than the number of replicas add it to the condemned list
            condemned = append(condemned, pods[i])
        }
        // If the ordinal could not be parsed (ord < 0), ignore the Pod.
    }
    
    // 第二个for循环,当序号小于statefulset期望副本数值的pod未创建出来时,则根据statefulset对象中的pod模板,构建出相应序号值的pod对象(此时还并没有向apiserver发起创建pod的请求,只是构建好pod结构体)
    // for any empty indices in the sequence [0,set.Spec.Replicas) create a new Pod at the correct revision
    for ord := 0; ord < replicaCount; ord++ {
        if replicas[ord] == nil {
            replicas[ord] = newVersionedStatefulSetPod(
                currentSet,
                updateSet,
                currentRevision.Name,
                updateRevision.Name, ord)
        }
    }

    // sort the condemned Pods by their ordinals
    sort.Sort(ascendingOrdinal(condemned))
    
    // 第三个和第四个for循环,遍历replicas和condemned两个数组,找到非healthy状态的最小序号的pod记录下来,并记录序号
    // find the first unhealthy Pod
    for i := range replicas {
        if !isHealthy(replicas[i]) {
            unhealthy++
            if ord := getOrdinal(replicas[i]); ord < firstUnhealthyOrdinal {
                firstUnhealthyOrdinal = ord
                firstUnhealthyPod = replicas[i]
            }
        }
    }
    for i := range condemned {
        if !isHealthy(condemned[i]) {
            unhealthy++
            if ord := getOrdinal(condemned[i]); ord < firstUnhealthyOrdinal {
                firstUnhealthyOrdinal = ord
                firstUnhealthyPod = condemned[i]
            }
        }
    }

    if unhealthy > 0 {
        klog.V(4).Infof("StatefulSet %s/%s has %d unhealthy Pods starting with %s",
            set.Namespace,
            set.Name,
            unhealthy,
            firstUnhealthyPod.Name)
    }
    
    // 当statefulset对象的DeletionTimestamp不为nil时,直接返回前面计算出来的statefulset的新status值,不再进行方法后续的逻辑处理
    // If the StatefulSet is being deleted, don't do anything other than updating
    // status.
    if set.DeletionTimestamp != nil {
        return &status, nil
    }
    
    // 获取monotonic的值,当statefulset.Spec.PodManagementPolicy的值为Parallel时,monotonic的值为false,否则为true
    monotonic := !allowsBurst(set)
    
    // 第五个for循环,遍历replicas数组,处理statefulset的pod,主要是做pod的创建
    // Examine each replica with respect to its ordinal
    for i := range replicas {
        // delete and recreate failed pods
        if isFailed(replicas[i]) {
            ssc.recorder.Eventf(set, v1.EventTypeWarning, "RecreatingFailedPod",
                "StatefulSet %s/%s is recreating failed Pod %s",
                set.Namespace,
                set.Name,
                replicas[i].Name)
            if err := ssc.podControl.DeleteStatefulPod(set, replicas[i]); err != nil {
                return &status, err
            }
            if getPodRevision(replicas[i]) == currentRevision.Name {
                status.CurrentReplicas--
            }
            if getPodRevision(replicas[i]) == updateRevision.Name {
                status.UpdatedReplicas--
            }
            status.Replicas--
            replicas[i] = newVersionedStatefulSetPod(
                currentSet,
                updateSet,
                currentRevision.Name,
                updateRevision.Name,
                i)
        }
        // If we find a Pod that has not been created we create the Pod
        if !isCreated(replicas[i]) {
            if err := ssc.podControl.CreateStatefulPod(set, replicas[i]); err != nil {
                return &status, err
            }
            status.Replicas++
            if getPodRevision(replicas[i]) == currentRevision.Name {
                status.CurrentReplicas++
            }
            if getPodRevision(replicas[i]) == updateRevision.Name {
                status.UpdatedReplicas++
            }

            // if the set does not allow bursting, return immediately
            if monotonic {
                return &status, nil
            }
            // pod created, no more work possible for this round
            continue
        }
        // If we find a Pod that is currently terminating, we must wait until graceful deletion
        // completes before we continue to make progress.
        if isTerminating(replicas[i]) && monotonic {
            klog.V(4).Infof(
                "StatefulSet %s/%s is waiting for Pod %s to Terminate",
                set.Namespace,
                set.Name,
                replicas[i].Name)
            return &status, nil
        }
        // If we have a Pod that has been created but is not running and ready we can not make progress.
        // We must ensure that all for each Pod, when we create it, all of its predecessors, with respect to its
        // ordinal, are Running and Ready.
        if !isRunningAndReady(replicas[i]) && monotonic {
            klog.V(4).Infof(
                "StatefulSet %s/%s is waiting for Pod %s to be Running and Ready",
                set.Namespace,
                set.Name,
                replicas[i].Name)
            return &status, nil
        }
        // Enforce the StatefulSet invariants
        if identityMatches(set, replicas[i]) && storageMatches(set, replicas[i]) {
            continue
        }
        // Make a deep copy so we don't mutate the shared cache
        replica := replicas[i].DeepCopy()
        if err := ssc.podControl.UpdateStatefulPod(updateSet, replica); err != nil {
            return &status, err
        }
    }
    
    // 第六个for循环,逆序(pod序号从大到小)遍历condemned数组,处理statefulset的pod,主要是做多余pod的删除
    // At this point, all of the current Replicas are Running and Ready, we can consider termination.
    // We will wait for all predecessors to be Running and Ready prior to attempting a deletion.
    // We will terminate Pods in a monotonically decreasing order over [len(pods),set.Spec.Replicas).
    // Note that we do not resurrect Pods in this interval. Also note that scaling will take precedence over
    // updates.
    for target := len(condemned) - 1; target >= 0; target-- {
        // wait for terminating pods to expire
        if isTerminating(condemned[target]) {
            klog.V(4).Infof(
                "StatefulSet %s/%s is waiting for Pod %s to Terminate prior to scale down",
                set.Namespace,
                set.Name,
                condemned[target].Name)
            // block if we are in monotonic mode
            if monotonic {
                return &status, nil
            }
            continue
        }
        // if we are in monotonic mode and the condemned target is not the first unhealthy Pod block
        if !isRunningAndReady(condemned[target]) && monotonic && condemned[target] != firstUnhealthyPod {
            klog.V(4).Infof(
                "StatefulSet %s/%s is waiting for Pod %s to be Running and Ready prior to scale down",
                set.Namespace,
                set.Name,
                firstUnhealthyPod.Name)
            return &status, nil
        }
        klog.V(2).Infof("StatefulSet %s/%s terminating Pod %s for scale down",
            set.Namespace,
            set.Name,
            condemned[target].Name)

        if err := ssc.podControl.DeleteStatefulPod(set, condemned[target]); err != nil {
            return &status, err
        }
        if getPodRevision(condemned[target]) == currentRevision.Name {
            status.CurrentReplicas--
        }
        if getPodRevision(condemned[target]) == updateRevision.Name {
            status.UpdatedReplicas--
        }
        if monotonic {
            return &status, nil
        }
    }
    
    // 判断statefulset的更新策略,若为OnDelete,则直接return(使用了该更新策略,则需要人工删除pod后才会重建相应序号的pod)
    // for the OnDelete strategy we short circuit. Pods will be updated when they are manually deleted.
    if set.Spec.UpdateStrategy.Type == apps.OnDeleteStatefulSetStrategyType {
        return &status, nil
    }
    
    // 获取滚动更新配置中的Partition值,当statefulset进行滚动更新时,小于等于该序号的pod将不会被更新
    // we compute the minimum ordinal of the target sequence for a destructive update based on the strategy.
    updateMin := 0
    if set.Spec.UpdateStrategy.RollingUpdate != nil {
        updateMin = int(*set.Spec.UpdateStrategy.RollingUpdate.Partition)
    }
    
    // 第七个for循环,主要是处理更新策略为RollingUpdate的statefulset对象的更新
    // we terminate the Pod with the largest ordinal that does not match the update revision.
    for target := len(replicas) - 1; target >= updateMin; target-- {

        // delete the Pod if it is not already terminating and does not match the update revision.
        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
        }

        // wait for unhealthy Pods on update
        if !isHealthy(replicas[target]) {
            klog.V(4).Infof(
                "StatefulSet %s/%s is waiting for Pod %s to update",
                set.Namespace,
                set.Name,
                replicas[target].Name)
            return &status, nil
        }

    }
    return &status, nil
}

结合上面对该方法的分析,来总结下在此方法中都有哪些步骤涉及了statefulset对象对于pod的创建、删除、扩缩容、更新操作:
1.创建:主要是(6)第五个for循环;
2.删除:主要是(7)第六个for循环;
3.扩缩容: (1)~(7);
4.更新:主要是(8)(9)与(10)第七个for循环,其中(8)为OnDelete更新策略的处理,(9)(10)为滚动更新策略的处理。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容