《一起读 kubernetes 源码》statefulset 的更新有何不同
📢 注意,该文本非最终版本,正在更新中,版权所有,请勿转载!!
前言
在前面我们已经看过了 deployment 和 replicaset 的实现,其实对于 k8s 中的对象已经有了一个基本的认识,其他的对象也都是在这个的基础之上有了不同的能力。而这一节我们来看看另一个常用的对象 statefulset。相对与 deployment 来说 statefulset 用的会更少,因为大部分应用都是无状态的,而有状态的数据类型的应用可能上 k8s 又少,要不就是接云厂商,要不就是独立部署。但是对于一些需要持久化配置或者数据的应用来说,配合 StorageClass 能让 StatefulSet 很好的帮助我们来部署这样类型的应用。
前置知识
- statefulset 的基本使用
- statefulset 的更新过程
- statefulset 的 partition 的作用
心路历程
我们知道滚动更新的时候 statefulset 是一个一个的这里的实现与 deployment 有什么不一样的地方呢?这部分是今天的主角我们需要弄明白。而另一部分是有关于 persistentVolumeClaimRetentionPolicy
这个是 v1.27
beta 的特性,用于控制是否删除以及如何删除 PVC,除了看原本的源码,这次我希望给你看一些不一样的,比如在 k8s 里面,对于新特性是如何引入和做判断的。
码前提问
- statefulset 滚动更新的实现与 deployment 有什么区别?
- statefulset
persistentVolumeClaimRetentionPolicy
是如何实现的
源码分析
寻码过程
这次我就不多说了,有了前面的经验,找到它易如反掌
1 | kubernetes/pkg/controller/deployment |
而这一次提供另一种看源码的思路,类比。由于我们已经比较了解 deployment 的整体实现了,所以大部分相同的地方我们可以直接跳过,我们主要去寻找不一样的地方。
结构 和 创建
1 | // pkg/controller/statefulset/stateful_set.go:83 |
具体结构和创建方法其实都不用贴,这里的入参就很能说明问题了。类比一下
- 没有
ReplicaSetInformer
证明 pod 已经不是通过RS
去控制了,而是直接给到了podInformer
然后交给StatefulSetController
的updatePod
相关方法 - 多了
PersistentVolumeClaimInformer
和ControllerRevisionInformer
由于我们知道 PVC 是什么东西,由于有状态,大多数情况下会用到 PV 和 PVC 所以启动的时候势必需要等待他们完成
那接下来我们的思路就很明确了,我们需要去看 pod 更新的时候具体是如何操作的
更新
之前我们的路径还有印象对吧:Run
-> worker
-> processNextWorkItem
-> syncHandler
。
类比着我们很快能找到在 statefulset 里面也是类似的:Run
-> worker
-> processNextWorkItem
-> sync
-> syncStatefulSet
-> UpdateStatefulSet
-> performUpdate
-> updateStatefulSet
。
而 updateStatefulSet
方法就是更新的关键了。
策略 UpdateStrategy
RollingUpdateStatefulSetStrategyType 是 statefulset 的更新策略,就两种,非常简单
RollingUpdate
,默认就是这个,滚动更新,一个好了接一个OnDelete
,很简单,就是需要用户手动去删除才会更新
由于 OnDelete
很少用到,所以可能被忽略。不过为什么要先知道策略呢?这里就要可以利用另外一个源码的阅读技巧了,合理利用枚举参数。
利用固定的枚举参数,可以快速缩小源码的阅读内容,也可以快速定位目标
滚动更新
我们知道 OnDelete
是用户手动操作才会更新 pod
,那么源码里面必定需要判断这个状态,如果不是这个状态才会去操作 pod 主动去删除。 所以我们直接定位到 updateStatefulSet
的最后:
1 | // pkg/controller/statefulset/stateful_set_control.go:658 |
可以明确看到,如果是 OnDelete
状态那就直接返回了,那也就是说下面就是操作 pod 了。果然,下面的逻辑其实非常简单,其中有三个注意点:
- 将最大的那个 pod 控制去
DeleteStatefulPod
然后直接返回了 if !isHealthy(replicas[target]) {
也就是:当一个 pod 正在更新的时候,也会直接返回。也就是 statefulset 必然是一个好了再下一个更新的Partition
是用来做 金丝雀 发布的,你应该有所了解,也是在这里处理的,只有序号 ≥ partition 的才会更新
其实更新部分我觉得最重要的部分这样就被解决了,我们通过枚举的技巧可以快速将一个 200+ 行的函数快速定位到了自己所需要的部分,当然如果你对前面对于 pod 排序创建等操作,有想要了解的回过头去看另一半就可以了,此时你就可以完全不管删除的逻辑了,上半部分肯定是在处理删除之前的逻辑,那么你的方向会更清晰。
persistentVolumeClaimRetentionPolicy
在以前,statefulset 被删除之后 PVC 通常是不受影响的,也就是 Retain
,而还可以配置 Delete
也就是删除。并且 persistentVolumeClaimRetentionPolicy
可以支持 whenDeleted
和 whenScaled
就是在不同场景下支持不同的控制策略。比如当 pod 被删除时是 PVC 是保留的,但 缩减(scaled) 的时候删除。
这里特性本身不是特别重要,重要的是,我想让你看下对于新特性的引入,在 k8s 中是如何做判断的。
1 | // pkg/controller/statefulset/stateful_set_control.go:387 |
上面这一部分是在 updateStatefulSet
的 processReplica
根据 pod 不同状态执行不同操作,其中我们可以看到,其实并不复杂,就是通过了 utilfeature.DefaultFeatureGate
的 Enabled
方法来得到当前所需要的这个 feature 是否被开启来,如果开启了,就可以执行下方的判断。而 utilfeature.DefaultFeatureGate
本质也就是一个 map ,存储了所有的 feat,而 features 枚举了所以的特性,其中有非常详细的版本注释。
其实和一般处理的方式没啥区别,如果让我们来写也是一样的,就是通过全局变量来注册所有特性的状态而已。而判断的时候也就是判断一下里面开没开。当然这可能确实有点 ”散弹修改“ 的味道,但是由于特性的目标不会多,所以面积不会广,全局也随时能用,无耦合,可以学习。
1 | // pkg/controller/statefulset/stateful_pod_control.go:265 |
当然后面其实就是在 PodClaimIsStale
中判断 是 Retain
还是 Delete
了。
码后解答
- statefulset 滚动更新的实现与 deployment 有什么区别?
- 关键在于顺序(有序)和个数(一次一个)
- statefulset
persistentVolumeClaimRetentionPolicy
是如何实现的?- 很简单,通过
utilfeature.DefaultFeatureGate
一个全局变量来进行判断
- 很简单,通过
总结提升
可以看到,由于我们之前有了其他类似的源码经验,其实对于整体过程已经有了把握,很多地方就没必要再去仔仔细细一步步推敲了,因为实现都是类似的,我们只需要抓住不同,类比即可。找到不同的地方,看自己关心的地方,就能快速知道源码里面做的事情是什么。只要从大方向有了把握,之后有问题你就可以迅速定位到这个问题可能出现的原因,以及有寻找的思路了。
编码上
对于项目内新特性的引入完全可以参考 utilfeature.DefaultFeatureGate
的设计,在引入使用 beta 一段时间,在后续的正式版本中上线。一个 map + 一个 if 的事。