P5-StatefulSet Controller
前言
在前面的几篇文章中,先对deployment controller进行了初步分析:
Controller-P3-Deployment Controller
严格来讲deployment的管理pod的逻辑是基于replicaSet来实现的,因此接下来结合replicaSet controller进行了深入:
Controller-P3-ReplicaSet Controller
那么在本篇,来看看另一个最常用的承载在pod之上的管理单位的控制器实现: StatefulSet Controller
StatefulSet 的基本特性
在看代码之前,先回顾一下sts的基本运行特性,代入地阅读代码会比较顺畅
创建
sts是有序的,pod副本有序串行地新建,pod名称为{sts_name}-{0..N},从小序号的pod(名称为{sts_name}-0)创建,一直到第n个副本的pod(名称为{sts_name}-n)
更新
sts的更新策略有2种:
RollingUpdateStatefulSetStrategyType
,默认的滚动更新策略,此策略下,更新时pod根据序号反顺序更新,从最大序号的pod开始删除重建,更新至序号最小的pod。更新过程中,始终保持pod数量等于指定副本数,即每删除一个pod,才会再创建一个。同时可以指定一个partition参数,指定这个参数后,只有序号大于等于partition的pod才会被更新,序号小于partition参数的pod不会被更新,例如有5个副本,partition设置为2,那么在更新sts时,0和1号pod不会更新,2 3 4号pod则会更新重建;此时继续将partition缩减为0,则0 1号pod也会更新重建。默认partition为0,即所有的pod都会更新。这个参数一般不会使用,但可用在发布时动态更新递减partition的值,来实现滚动灰度发布。OnDeleteStatefulSetStrategyType
, 此策略下controller不会对pod做任何操作,由手动删除pod来触发新pod的创建
删除
删除sts时,可以指定级联模式的参数--cascade=true
,默认为true,意思是删除sts会同时删除它所管理的pod。设置为false时,删除sts不会影响pod的运行,且sts重建后依然能与此前的pod关联起来(这种方式可能会产生孤儿pod)。
关联关系
先来看看sts和pod的关联方式:
1 | # sts |
再来看看sts和ControllerRevision关联方式:
1 | [root@008019 ~]# kubectl get sts deptest11dev |
记住这几者之间双向地关联方式,下面会提到。
StatefulSet Controller
初始化
cmd/kube-controller-manager/app/apps.go:59
1 | func startStatefulSetController(ctx ControllerContext) (http.Handler, bool, error) { |
先来看看NewStatefulSetController
做了什么:
==> pkg/controller/statefulset/stateful_set.go:81
1 | func NewStatefulSetController( |
先看注释1,可以发现,StatefulSetController关注四种类型的资源: Pod/Sts/PVC/ControllerRevision,其中的ControllerRevision不太熟悉,先找出来看下它的结构,逐级跳转:
cmd/kube-controller-manager/app/apps.go:63
==> vendor/k8s.io/client-go/informers/apps/v1/interface.go:28
==>vendor/k8s.io/client-go/informers/apps/v1/controllerrevision.go:38
==> vendor/k8s.io/client-go/listers/apps/v1/controllerrevision.go:29
==> vendor/k8s.io/api/apps/v1/types.go:800
1 | type ControllerRevision struct { |
阅读这个结构体上方的注释可以得知,ControllerRevision提供给DaemonSet和StatefulSet用作更新和回滚,ControllerRevision存放的是数据的快照,ControllerRevision生成之后内容是不可修改的,由调用端来负责序列化写入和反序列化读取。其中Revision(int64)字段相当于ControllerRevision的版本id号,Data字段则存放序列化后的数据。
画外音:不难猜测,StatefulSet的更新以及回滚(也是一种特殊的更新)操作,是基于对新旧ControllerRevision的对比来进行的
在来看下注释2,NewDefaultStatefulSetControl方法:
pkg/controller/statefulset/stateful_set.go:95
==> pkg/controller/statefulset/stateful_set_control.go:54
1 | func NewDefaultStatefulSetControl( |
NewDefaultStatefulSetControl返回的defaultStatefulSetControl结构体对象是sts管理控制逻辑的语义实现,defaultStatefulSetControl结构体里面包含了sts控制过程中的各种接口:
- 管理sts对应的pod/pvc(podControl)的方法接口,有(CreateStatefulPod/UpdateStatefulPod/DeleteStatefulPod)这几个方法,通过NewRealStatefulPodControl函数返回的realStatefulPodControl结构体对象来实现
- 管理sts status状态的更新(statusUpdater)的方法接口,有UpdateStatefulSetStatus这一个方法,通过NewRealStatefulSetStatusUpdater返回的realStatefulSetStatusUpdater结构体对象来实现。
- 管理ControllerRevision版本(controllerHistory) 的方法接口,有(ListControllerRevisions/CreateControllerRevision/DeleteControllerRevision/UpdateControllerRevision/AdoptControllerRevision/ReleaseControllerRevision)这几个方法,通过history.NewHistory返回的realHistory结构体对象来实现。
现在接着往下,去看看ssc(StatefulSetController) 运行的Run函数。
工作过程
*StatefulSetController.Run()
函数:
1 | func (ssc *StatefulSetController) Run(workers int, stopCh <-chan struct{}) { |
wait.Until定时器前面已经讲过,不再复述,重点在于ssc.worker函数,代码里有多次跳跃:
pkg/controller/statefulset/stateful_set.go:159
==>pkg/controller/statefulset/stateful_set.go:410
==> pkg/controller/statefulset/stateful_set.go:399
==>pkg/controller/statefulset/stateful_set.go:415
1 | // sync syncs the given statefulset. |
来分步看下
孤儿Revisions修正托管
上面指出sts和revision两者之间显示地双向指定字段来关联对方,明白这一点那么这个函数就好理解了。
出现孤儿ControllerRevisions的原因,很有可能是sts在此期间进行了反复的更新,更新时间差之中产生了脏数据.
pkg/controller/statefulset/stateful_set.go:316
1 | // adoptOrphanRevisions adopts any orphaned ControllerRevisions matched by set's Selector. |
获取到sts管理的pod
pkg/controller/statefulset/stateful_set.go:285
1 | func (ssc *StatefulSetController) getPodsForStatefulSet(set *apps.StatefulSet, selector labels.Selector) ([]*v1.Pod, error) { |
ClaimPods
pkg/controller/controller_ref_manager.go:171
1 | func (m *PodControllerRefManager) ClaimPods(pods []*v1.Pod, filters ...func(*v1.Pod) bool) ([]*v1.Pod, error) { |
####
####ClaimObject
pkg/controller/controller_ref_manager.go:66
1 | func (m *BaseControllerRefManager) ClaimObject(obj metav1.Object, match func(metav1.Object) bool, adopt, release func(metav1.Object) error) (bool, error) { |
到这里,所有应当被sts管理的pod(包括孤儿pod)就过滤完毕了,开始执行真正的sts sync。
syncStatefulSet
在找到了所有管理的pod后,就要开始sts 的sync,进行更新sts及更新pod的操作了,回到这里:
pkg/controller/statefulset/stateful_set.go:451
==> pkg/controller/statefulset/stateful_set.go:458
==> pkg/controller/statefulset/stateful_set_control.go:75
1 | func (ssc *defaultStatefulSetControl) UpdateStatefulSet(set *apps.StatefulSet, pods []*v1.Pod) error { |
这里面最核心的函数是updateStatefulSetStatus
,接着往下
updateStatefulSet
这一个函数内容很多,200多行代码,需要说明的地方会在下面代码中注释。
1 | func (ssc *defaultStatefulSetControl) updateStatefulSet( |
updateStatefulSet函数总结
- 每个循环的周期中,最多操作一个pod
- 根据sts.spec.replicas对比现有pod的序号,对pod进行划分,一部分划为合法(保留/重建),一部分划为非法(删除)
- 对pods进行划分,一部分划入current(old) set阵营,另一部分划入update(new) set阵营
- 更新过程中,无论是删减、还是新建,都保持pod数量固定,有序地递增、递减
- 最终保证所有的pod都归属于update revision
总结
statefulset 在设计上与 deployment 有许多不同的地方,例如:
deployment通过rs管理pod,sts通过controllerRevision管理pod;
deployment curd是无序的,sts强保证有序curd
sts需要检查存储的匹配
在了解sts管理操作pod方式的基础上来看代码,会有许多的帮助。