创建

对于一个拥有 N 个副本的 statefulset,pod 是按照 {0..N-1}的序号顺序创建的,并且会等待前一个 pod 变为 后才会启动下一个 pod。

扩容

statefulset 扩容时 pod 也是顺序创建的,编号与前面的 pod 相接。

  1. $ kubectl scale sts web --replicas=4
  2. statefulset.apps/web scaled
  3. $ kubectl get pod -o wide -w
  4. ......
  5. web-2 0/1 Pending 0 0s <none> <none>
  6. web-2 0/1 ContainerCreating 0 1s <none> minikube
  7. web-2 1/1 Running 0 4s 10.1.0.10 minikube
  8. web-3 0/1 Pending 0 0s <none> <none>
  9. web-3 0/1 ContainerCreating 0 1s <none> minikube
  10. web-3 1/1 Running 0 4s 10.1.0.11 minikube

缩容

缩容时控制器会按照与 pod 序号索引相反的顺序每次删除一个 pod,在删除下一个 pod 前会等待上一个被完全删除。

  1. $ kubectl scale sts web --replicas=2
  2. $ kubectl get pod -o wide -w
  3. ......
  4. web-3 1/1 Terminating 0 8m25s 10.1.0.11 minikube
  5. web-3 0/1 Terminating 0 8m27s <none> minikube
  6. web-2 1/1 Terminating 0 8m31s 10.1.0.10 minikube
  7. web-2 0/1 Terminating 0 8m33s 10.1.0.10 minikube

更新

更新策略由 statefulset 中的 spec.updateStrategy.type 字段决定,可以指定为 OnDelete 或者 RollingUpdate , 默认的更新策略为 RollingUpdate。当使用RollingUpdate 更新策略更新所有 pod 时采用与序号索引相反的顺序进行更新,即最先删除序号最大的 pod 并根据更新策略中的 partition 参数来进行分段更新,控制器会更新所有序号大于或等于 partition 的 pod,等该区间内的 pod 更新完成后需要再次设定 partition 的值以此来更新剩余的 pod,最终 partition 被设置为 0 时代表更新完成了所有的 pod。在更新过程中,如果一个序号小于 partition 的 pod 被删除或者终止,controller 依然会使用更新前的配置重新创建。

  1. // 使用 RollingUpdate 策略更新
  2. $ kubectl patch statefulset web --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"nginx:1.16"}]'
  3. statefulset.apps/web patched
  4. $ kubectl rollout status sts/web
  5. Waiting for 1 pods to be ready...
  6. Waiting for partitioned roll out to finish: 1 out of 2 new pods have been updated...
  7. Waiting for 1 pods to be ready...
  8. partitioned roll out complete: 2 new pods have been updated...

如果 statefulset 的 .spec.updateStrategy.type 字段被设置为 OnDelete,在更新 statefulset 时,statefulset controller 将不会自动更新其 pod。你必须手动删除 pod,此时 statefulset controller 在重新创建 pod 时,使用修改过的 .spec.template 的内容创建新 pod。

使用滚动更新策略时你必须以某种策略不段更新 partition 值来进行升级,类似于金丝雀部署方式,升级对于 pod 名称来说是逆序。使用非滚动更新方式式,需要手动删除对应的 pod,升级可以是无序的。

回滚

statefulset 和 deployment 一样也支持回滚操作,statefulset 也保存了历史版本,和 deployment 一样利用.spec.revisionHistoryLimit 字段设置保存多少个历史版本,但 statefulset 的回滚并不是自动进行的,回滚操作也仅仅是进行了一次发布更新,和发布更新的策略一样,更新 statefulset 后需要按照对应的策略手动删除 pod 或者修改 partition 字段以达到回滚 pod 的目的。

  1. // 查看 sts 的历史版本
  2. $ kubectl rollout history statefulset web
  3. statefulset.apps/web
  4. REVISION
  5. 0
  6. 0
  7. 5
  8. 6
  9. $ kubectl get controllerrevision
  10. NAME CONTROLLER REVISION AGE
  11. web-6c4c79564f statefulset.apps/web 6 11m
  12. web-c47b9997f statefulset.apps/web 5 4h13m
  13. $ kubectl rollout undo statefulset web --to-revision=5

因为 statefulset 的使用对象是有状态服务,大部分有状态副本集都会用到持久存储,statefulset 下的每个 pod 正常情况下都会关联一个 pv 对象,对 statefulset 对象回滚非常容易,但其使用的 pv 中保存的数据无法回滚,所以在生产环境中进行回滚时需要谨慎操作,statefulset、pod、pvc 和 pv 关系图如下所示:

删除

  1. // 1、非级联删除
  2. $ kubectl delete statefulset web --cascade=false
  3. // 删除 sts 后 pod 依然处于运行中
  4. $ kubectl get pod
  5. NAME READY STATUS RESTARTS AGE
  6. web-0 1/1 Running 0 4m38s
  7. // 重新创建 sts 后,会再次关联所有的 pod
  8. $ kubectl create -f sts.yaml
  9. $ kubectl get sts
  10. NAME READY AGE
  11. web 2/2 28s

在级联删除 statefulset 时,会将所有的 pod 同时删掉,statefulset 控制器会首先进行一个类似缩容的操作,pod 按照和他们序号索引相反的顺序每次终止一个。在终止一个 pod 前,statefulset 控制器会等待 pod 后继者被完全终止。

  1. // 2、级联删除
  2. $ kubectl delete statefulset web
  3. $ kubectl get pod -o wide -w
  4. ......
  5. web-0 1/1 Terminating 0 17m 10.1.0.18 minikube <none> <none>
  6. web-1 1/1 Terminating 0 36m 10.1.0.15 minikube <none> <none>
  7. web-1 0/1 Terminating 0 36m 10.1.0.15 minikube <none> <none>
  8. web-0 0/1 Terminating 0 17m 10.1.0.18 minikube <none> <none>

Pod 管理策略

statefulset 的默认管理策略是 OrderedReady,该策略遵循上文展示的顺序性保证。statefulset 还有另外一种管理策略 ParallelParallel 管理策略告诉 statefulset 控制器并行的终止所有 pod,在启动或终止另一个 pod 前,不必等待这些 pod 变成 Running & Ready 或者完全终止状态,但是 Parallel 仅仅支持在 OnDelete 策略下生效,下文会在源码中具体分析。

startStatefulSetController 是 statefulSetController 的启动方法,其中调用 NewStatefulSetController 进行初始化 controller 对象然后调用 Run 方法启动 controller。其中 ConcurrentStatefulSetSyncs 默认值为 5。

k8s.io/kubernetes/cmd/kube-controller-manager/app/apps.go:55

当 controller 启动后会通过 informer 同步 cache 并监听 pod 和 statefulset 对象的变更事件,informer 的处理流程此处不再详细讲解,最后会执行 sync 方法,sync 方法是每个 controller 的核心方法,下面直接看 statefulset controller 的 sync 方法。

sync

sync 方法的主要逻辑为:

  • 1、根据 ns/name 获取 sts 对象;
  • 2、获取 sts 的 selector;
  • 3、调用 ssc.adoptOrphanRevisions 检查是否有孤儿 controllerrevisions 对象,若有且能匹配 selector 的则添加 ownerReferences 进行关联,已关联但 label 不匹配的则进行释放;
  • 4、调用 ssc.getPodsForStatefulSet 通过 selector 获取 sts 关联的 pod,若有孤儿 pod 的 label 与 sts 的能匹配则进行关联,若已关联的 pod label 有变化则解除与 sts 的关联关系;
  • 5、最后调用 ssc.syncStatefulSet 执行真正的 sync 操作;

k8s.io/kubernetes/pkg/controller/statefulset/stateful_set.go:408

  1. func (ssc *StatefulSetController) sync(key string) error {
  2. ......
  3. namespace, name, err := cache.SplitMetaNamespaceKey(key)
  4. if err != nil {
  5. return err
  6. }
  7. // 1、获取 sts 对象
  8. set, err := ssc.setLister.StatefulSets(namespace).Get(name)
  9. ......
  10. selector, err := metav1.LabelSelectorAsSelector(set.Spec.Selector)
  11. ......
  12. // 2、关联以及释放 sts 的 controllerrevisions
  13. if err := ssc.adoptOrphanRevisions(set); err != nil {
  14. return err
  15. }
  16. // 3、获取 sts 所关联的 pod
  17. pods, err := ssc.getPodsForStatefulSet(set, selector)
  18. if err != nil {
  19. return err
  20. }
  21. return ssc.syncStatefulSet(set, pods)

syncStatefulSet

syncStatefulSet 中仅仅是调用了 ssc.control.UpdateStatefulSet 方法进行处理。ssc.control.UpdateStatefulSet 会调用 defaultStatefulSetControlUpdateStatefulSet 方法,defaultStatefulSetControl 是 statefulset controller 中另外一个对象,主要负责处理 statefulset 的更新。

  1. func (ssc *StatefulSetController) syncStatefulSet(set *apps.StatefulSet, pods []*v1.Pod) error {
  2. ......
  3. if err := ssc.control.UpdateStatefulSet(set.DeepCopy(), pods); err != nil {
  4. }
  5. ......
  6. return nil
  7. }

UpdateStatefulSet 方法的主要逻辑如下所示:

  • 1、获取历史 revisions;
  • 2、计算 currentRevisionupdateRevision,若 sts 处于更新过程中则 currentRevisionupdateRevision 值不同;
  • 3、调用 ssc.updateStatefulSet 执行实际的 sync 操作;
  • 4、调用 ssc.updateStatefulSetStatus 更新 status subResource;
  • 5、根据 sts 的 spec.revisionHistoryLimit字段清理过期的 controllerrevision

在基本操作的回滚阶段提到了过,sts 通过 controllerrevision 保存历史版本,类似于 deployment 的 replicaset,与 replicaset 不同的是 controllerrevision 仅用于回滚阶段,在 sts 的滚动升级过程中是通过 currentRevisionupdateRevision来进行控制并不会用到 controllerrevision

k8s.io/kubernetes/pkg/controller/statefulset/stateful_set_control.go:75

  1. func (ssc *defaultStatefulSetControl) UpdateStatefulSet(set *apps.StatefulSet, pods []*v1.Pod) error {
  2. // 1、获取历史 revisions
  3. revisions, err := ssc.ListRevisions(set)
  4. if err != nil {
  5. return err
  6. }
  7. history.SortControllerRevisions(revisions)
  8. // 2、计算 currentRevision 和 updateRevision
  9. currentRevision, updateRevision, collisionCount, err := ssc.getStatefulSetRevisions(set, revisions)
  10. if err != nil {
  11. return err
  12. }
  13. // 3、执行实际的 sync 操作
  14. status, err := ssc.updateStatefulSet(set, currentRevision, updateRevision, collisionCount, pods)
  15. if err != nil {
  16. return err
  17. }
  18. // 4、更新 sts 状态
  19. err = ssc.updateStatefulSetStatus(set, status)
  20. if err != nil {
  21. return err
  22. }
  23. ......
  24. // 5、清理过期的历史版本
  25. return ssc.truncateHistory(set, pods, revisions, currentRevision, updateRevision)
  26. }

updateStatefulSet

updateStatefulSet 是 sync 操作中的核心方法,对于 statefulset 的创建、扩缩容、更新、删除等操作都会在这个方法中完成,以下是其主要逻辑:

  • 1、分别获取 currentRevisionupdateRevision 对应的的 statefulset object;
  • 2、构建 status 对象;
  • 3、将 statefulset 的 pods 按 ord(ord 为 pod name 中的序号)的值分到 replicas 和 condemned 两个数组中,0 <= ord < Spec.Replicas 的放到 replicas 组,ord >= Spec.Replicas 的放到 condemned 组,replicas 组代表可用的 pod,condemned 组是需要删除的 pod;
  • 4、找出 replicas 和 condemned 组中的 unhealthy pod,healthy pod 指 running & ready 并且不处于删除状态;
  • 5、判断 sts 是否处于删除状态;
  • 6、遍历 replicas 数组,确保 replicas 数组中的容器处于 running & ready状态,其中处于 failed 状态的容器删除重建,未创建的容器则直接创建,最后检查 pod 的信息是否与 statefulset 的匹配,若不匹配则更新 pod 的状态。在此过程中每一步操作都会检查 monotonic 的值,即 sts 是否设置了 Parallel 参数,若设置了则循环处理 replicas 中的所有 pod,否则每次处理一个 pod,剩余 pod 则在下一个 syncLoop 继续进行处理;
  • 7、按 pod 名称逆序删除 condemned 数组中的 pod,删除前也要确保 pod 处于 running & ready状态,在此过程中也会检查 monotonic 的值,以此来判断是顺序删除还是在下一个 syncLoop 中继续进行处理;
  • 8、判断 sts 的更新策略 .Spec.UpdateStrategy.Type,若为 OnDelete 则直接返回;
  • 9、此时更新策略为 RollingUpdate,更新序号大于等于 .Spec.UpdateStrategy.RollingUpdate.Partition 的 pod,在 RollingUpdate 时,并不会关注 monotonic 的值,都是顺序进行处理且等待当前 pod 删除成功后才继续删除小于上一个 pod 序号的 pod,所以 Parallel 的策略在滚动更新时无法使用。

updateStatefulSet 这个方法中包含了 statefulset 的创建、删除、扩若容、更新等操作,在源码层面对于各个功能无法看出明显的界定,没有 deployment sync 方法中写的那么清晰,下面还是按 statefulset 的功能再分析一下具体的操作:

  • 创建:在创建 sts 后,sts 对象已被保存至 etcd 中,此时 sync 操作仅仅是创建出需要的 pod,即执行到第 6 步就会结束;
  • 扩缩容:对于扩若容操作仅仅是创建或者删除对应的 pod,在操作前也会判断所有 pod 是否处于 running & ready状态,然后进行对应的创建/删除操作,在上面的步骤中也会执行到第 6 步就结束了;
  • 更新:可以看出在第六步之后的所有操作就是与更新相关的了,所以更新操作会执行完整个方法,在更新过程中通过 pod 的 currentRevisionupdateRevision 来计算 currentReplicasupdatedReplicas 的值,最终完成所有 pod 的更新;
  • 删除:删除操作就比较明显了,会止于第五步,但是在此之前检查 pod 状态以及分组的操作确实是多余的;

本文分析了 statefulset controller 的主要功能,statefulset 在设计上有很多功能与 deployment 是类似的,但其主要是用来部署有状态应用的,statefulset 中的 pod 名称存在顺序性和唯一性,同时每个 pod 都使用了 pv 和 pvc 来存储状态,在创建、删除、更新操作中都会按照 pod 的顺序进行。

参考:

https://github.com/kubernetes/kubernetes/issues/78007