📢 注意,该文本非最终版本,正在更新中,版权所有,请勿转载!!

前言

在前面我们已经看过了 deployment 和 replicaset 的实现,其实对于 k8s 中的对象已经有了一个基本的认识,其他的对象也都是在这个的基础之上有了不同的能力。而这一节我们来看看另一个常用的对象 statefulset。相对与 deployment 来说 statefulset 用的会更少,因为大部分应用都是无状态的,而有状态的数据类型的应用可能上 k8s 又少,要不就是接云厂商,要不就是独立部署。但是对于一些需要持久化配置或者数据的应用来说,配合 StorageClass 能让 StatefulSet 很好的帮助我们来部署这样类型的应用。

前置知识

  • statefulset 的基本使用
  • statefulset 的更新过程
  • statefulset 的 partition 的作用

心路历程

我们知道滚动更新的时候 statefulset 是一个一个的这里的实现与 deployment 有什么不一样的地方呢?这部分是今天的主角我们需要弄明白。而另一部分是有关于 persistentVolumeClaimRetentionPolicy 这个是 v1.27 beta 的特性,用于控制是否删除以及如何删除 PVC,除了看原本的源码,这次我希望给你看一些不一样的,比如在 k8s 里面,对于新特性是如何引入和做判断的。

码前提问

  1. statefulset 滚动更新的实现与 deployment 有什么区别?
  2. statefulset persistentVolumeClaimRetentionPolicy 是如何实现的

源码分析

寻码过程

这次我就不多说了,有了前面的经验,找到它易如反掌

1
2
3
kubernetes/pkg/controller/deployment
kubernetes/pkg/controller/replicaset
kubernetes/pkg/controller/statefulset

而这一次提供另一种看源码的思路,类比。由于我们已经比较了解 deployment 的整体实现了,所以大部分相同的地方我们可以直接跳过,我们主要去寻找不一样的地方。

结构 和 创建

1
2
3
4
5
6
7
8
9
// pkg/controller/statefulset/stateful_set.go:83
func NewStatefulSetController(
ctx context.Context,
podInformer coreinformers.PodInformer,
setInformer appsinformers.StatefulSetInformer,
pvcInformer coreinformers.PersistentVolumeClaimInformer,
revInformer appsinformers.ControllerRevisionInformer,
kubeClient clientset.Interface,
) *StatefulSetController {

具体结构和创建方法其实都不用贴,这里的入参就很能说明问题了。类比一下

  1. 没有 ReplicaSetInformer 证明 pod 已经不是通过 RS 去控制了,而是直接给到了 podInformer 然后交给 StatefulSetControllerupdatePod 相关方法
  2. 多了 PersistentVolumeClaimInformerControllerRevisionInformer 由于我们知道 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// pkg/controller/statefulset/stateful_set_control.go:658
// 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
}

// ...

// 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)
}
// 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]) {
logger.V(2).Info("Pod of StatefulSet is terminating for update",
"statefulSet", klog.KObj(set), "pod", klog.KObj(replicas[target]))
if err := ssc.podControl.DeleteStatefulPod(set, replicas[target]); err != nil {
if !errors.IsNotFound(err) {
return &status, err
}
}
status.CurrentReplicas--
return &status, err
}

// wait for unhealthy Pods on update
if !isHealthy(replicas[target]) {
logger.V(4).Info("StatefulSet is waiting for Pod to update",
"statefulSet", klog.KObj(set), "pod", klog.KObj(replicas[target]))
return &status, nil
}

}
return &status, nil

可以明确看到,如果是 OnDelete 状态那就直接返回了,那也就是说下面就是操作 pod 了。果然,下面的逻辑其实非常简单,其中有三个注意点:

  1. 将最大的那个 pod 控制去 DeleteStatefulPod 然后直接返回了
  2. if !isHealthy(replicas[target]) { 也就是:当一个 pod 正在更新的时候,也会直接返回。也就是 statefulset 必然是一个好了再下一个更新的
  3. Partition 是用来做 金丝雀 发布的,你应该有所了解,也是在这里处理的,只有序号 ≥ partition 的才会更新

其实更新部分我觉得最重要的部分这样就被解决了,我们通过枚举的技巧可以快速将一个 200+ 行的函数快速定位到了自己所需要的部分,当然如果你对前面对于 pod 排序创建等操作,有想要了解的回过头去看另一半就可以了,此时你就可以完全不管删除的逻辑了,上半部分肯定是在处理删除之前的逻辑,那么你的方向会更清晰。

persistentVolumeClaimRetentionPolicy

在以前,statefulset 被删除之后 PVC 通常是不受影响的,也就是 Retain,而还可以配置 Delete 也就是删除。并且 persistentVolumeClaimRetentionPolicy 可以支持 whenDeletedwhenScaled 就是在不同场景下支持不同的控制策略。比如当 pod 被删除时是 PVC 是保留的,但 缩减(scaled) 的时候删除。

这里特性本身不是特别重要,重要的是,我想让你看下对于新特性的引入,在 k8s 中是如何做判断的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// pkg/controller/statefulset/stateful_set_control.go:387
// If we find a Pod that has not been created we create the Pod
if !isCreated(replicas[i]) {
if utilfeature.DefaultFeatureGate.Enabled(features.StatefulSetAutoDeletePVC) {
if isStale, err := ssc.podControl.PodClaimIsStale(set, replicas[i]); err != nil {
return true, err
} else if isStale {
// If a pod has a stale PVC, no more work can be done this round.
return true, err
}
}
if err := ssc.podControl.CreateStatefulPod(ctx, set, replicas[i]); err != nil {
return true, err
}
if monotonic {
// if the set does not allow bursting, return immediately
return true, nil
}
}

上面这一部分是在 updateStatefulSetprocessReplica 根据 pod 不同状态执行不同操作,其中我们可以看到,其实并不复杂,就是通过了 utilfeature.DefaultFeatureGateEnabled 方法来得到当前所需要的这个 feature 是否被开启来,如果开启了,就可以执行下方的判断。而 utilfeature.DefaultFeatureGate 本质也就是一个 map ,存储了所有的 feat,而 features 枚举了所以的特性,其中有非常详细的版本注释。

其实和一般处理的方式没啥区别,如果让我们来写也是一样的,就是通过全局变量来注册所有特性的状态而已。而判断的时候也就是判断一下里面开没开。当然这可能确实有点 ”散弹修改“ 的味道,但是由于特性的目标不会多,所以面积不会广,全局也随时能用,无耦合,可以学习。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// pkg/controller/statefulset/stateful_pod_control.go:265
func (spc *StatefulPodControl) PodClaimIsStale(set *apps.StatefulSet, pod *v1.Pod) (bool, error) {
policy := getPersistentVolumeClaimRetentionPolicy(set)
if policy.WhenScaled == apps.RetainPersistentVolumeClaimRetentionPolicyType {
// PVCs are meant to be reused and so can't be stale.
return false, nil
}
for _, claim := range getPersistentVolumeClaims(set, pod) {
pvc, err := spc.objectMgr.GetClaim(claim.Namespace, claim.Name)
switch {
case apierrors.IsNotFound(err):
// If the claim doesn't exist yet, it can't be stale.
continue
case err != nil:
return false, err
case err == nil:
if hasStaleOwnerRef(pvc, pod, podKind) {
return true, nil
}
}
}
return false, nil
}

当然后面其实就是在 PodClaimIsStale 中判断 是 Retain 还是 Delete 了。

码后解答

  1. statefulset 滚动更新的实现与 deployment 有什么区别?
    1. 关键在于顺序(有序)和个数(一次一个)
  2. statefulset persistentVolumeClaimRetentionPolicy 是如何实现的?
    1. 很简单,通过 utilfeature.DefaultFeatureGate 一个全局变量来进行判断

总结提升

可以看到,由于我们之前有了其他类似的源码经验,其实对于整体过程已经有了把握,很多地方就没必要再去仔仔细细一步步推敲了,因为实现都是类似的,我们只需要抓住不同,类比即可。找到不同的地方,看自己关心的地方,就能快速知道源码里面做的事情是什么。只要从大方向有了把握,之后有问题你就可以迅速定位到这个问题可能出现的原因,以及有寻找的思路了。

编码上

对于项目内新特性的引入完全可以参考 utilfeature.DefaultFeatureGate 的设计,在引入使用 beta 一段时间,在后续的正式版本中上线。一个 map + 一个 if 的事。