OpenKruise: CloneSet 代码详解

733 阅读7分钟

阅读本节前请了解 clonset 的使用和功能

OpenKruise 系列专栏: juejin.cn/column/7262…

目录一览: juejin.cn/post/726298…

openkruise doc :openkruise.io/zh/docs/use…

code: github.com/openkruise/…

api: apis/apps/v1alpha1/cloneset_types.go

controller: pkg/controller/cloneset/

Reconcile

预处理

// 预处理
selector, err := metav1.LabelSelectorAsSelector(instance.Spec.Selector)
filteredPods, filteredPVCs, err := r.getOwnedResource(instance)
filteredPods, err = r.claimPods(instance, filteredPods)
revisions, err := r.controllerHistory.ListControllerRevisions(instance, selector)
// Refresh update expectations
for _, pod := range filteredPods {
  clonesetutils.UpdateExpectations.ObserveUpdated(request.String(), updateRevision.Name, pod)
}

为什么我们需要 claim pod?

在OpenKruise的CloneSet中,claimPods方法用于声明属于CloneSet实例的Pod对象。尽管Pod对象在创建时可能已经属于该CloneSet,但在某些情况下,可能需要通过引用管理器(refmanager)来确认并标记这些Pod对象的所有权。

在给定的代码中,claimPods方法被调用来处理筛选后的Pod对象。这个调用的目的是确保这些Pod对象确实属于该CloneSet实例,并且在引用管理器中进行声明。这样做的原因可能有以下几个方面:

  1. 确保一致性和完整性:通过声明Pod对象的所有权,可以确保它们与CloneSet实例之间的关系是一致的。这有助于维护整个系统的一致性和完整性。

  2. 处理外部干预:在某些情况下,Pod对象可能会被手动修改或转移到其他控制器的管理下,从而导致与CloneSet实例的关联丢失。通过声明Pod对象的所有权,可以检测并处理这种外部干预,以确保Pod对象仍然属于正确的CloneSet实例。

  3. 引用管理和资源清理:引用管理器可以跟踪CloneSet实例与其管理的Pod对象之间的引用关系。通过声明Pod对象的所有权,可以更好地管理和清理与CloneSet实例相关联的资源,例如在删除CloneSet实例时自动清理相关的Pod对象。

总之,通过调用claimPods方法并在引用管理器中声明Pod对象的所有权,可以确保CloneSet实例与其管理的Pod对象之间的关系是一致的,并提供更好的资源管理和系统的完整性。

原地升级自动预热

if !isPreDownloadDisabled {
  if currentRevision.Name != updateRevision.Name {
      createImagePullJobsForInPlaceUpdate(instance, currentRevision, updateRevision)          
}

update/scale pod

入口:delayDuration, syncErr := r.syncCloneSet(instance, &newStatus, currentRevision, updateRevision, revisions, filteredPods, filteredPVCs)

// 获取当前和更新后的版本信息.
currentSet, err := r.revisionControl.ApplyRevision(instance, currentRevision)
updateSet, err := r.revisionControl.ApplyRevision(instance, updateRevision)

// Scale
scaling, podsScaleErr = r.syncControl.Scale(currentSet, updateSet, currentRevision.Name, updateRevision.Name, filteredPods, filteredPVCs)

clonset_scale.go: Scale()

处理 toDelete 和 preDelete 状态的 pods

删除标记是一个 annotation: SpecifiedDeleteKey = "apps.kruise.io/specified-delete", 有这种标记的 pod 被称为 podsSpecifiedToDelete (toDelete)

另外还有一个 annotation 叫做 "lifecycle.apps.kruise.io/state" 来表示 pod 的生命周期,它有以下值

    LifecycleStateNormal          LifecycleStateType = "Normal"
    LifecycleStatePreparingUpdate LifecycleStateType = "PreparingUpdate"
    LifecycleStateUpdating        LifecycleStateType = "Updating"
    LifecycleStateUpdated         LifecycleStateType = "Updated"
    LifecycleStatePreparingDelete LifecycleStateType = "PreparingDelete" 

我们通过podsSpecifiedToDelete, podsInPreDelete, numToDelete := getPlannedDeletedPods(updateCS, pods) 拿到处于 toDelete 和 preDelete 的 pods, 通过

r.managePreparingDelete(updateCS, pods, podsInPreDelete, numToDelete)

将所有不处于 toDelete 但处于 preDelete 状态的 pod(在 podsInPreDelete 数组里) 的生命期周期状态修正为 "Normal", 目的是在删除阶段能够使得 pod 被正确清理

Scale

  1. 调用 calculateDiffsWithExpectation(updateCS, pods, currentRevision, updateRevision) 返回一个 expectationDiffs 对象(diffRes)

    1.   该函数根据当前 CloneSet 的配置(比如 MaxSurge 最大弹性数量等)和 pods 的状态计算出扩缩容和更新所需的Pod数量并返回
    2. type expectationDiffs struct {
          // 代表需要扩缩容 pod 的数量差值,是一个有符号数
          // > 0 代表扩容, < 0 代表缩容
          scaleNum int
          
          // scaleNum中属于旧版本Pod的数量
          scaleNumOldRevision int
          
          // 在扩容时创建Pod的上限数量。受scaleStrategy.maxUnavailable的限制
          scaleUpLimit int
          
          // 可以删除的就绪Pod的上限数量。受UpdateStrategy.maxUnavailable的限制
          deleteReadyLimit int
          
          // 临时超过所需副本数的Pod数量。用于表示临时的扩容数量。
          useSurge int
          
          // useSurge中属于旧版本Pod的数量。用于指示临时扩容的旧版本Pod数量
          useSurgeOldRevision int
      
          // 需要进行更新的 Pod 数量
          // 正数表示需要更新指定数量的 Pod 到 updateRevision;
          // 负数表示需要回滚指定数量的 Pod 到 currentRevision。
          updateNum int
          
          // 可以进行更新的就绪Pod的最大数量。用于限制更新操作的可用Pod数量。
          updateMaxUnavailable int
      }
      

Attachment:

这个算法用于计算当前CloneSet的扩缩容和更新所需的Pod数量。下面是算法的具体逻辑:

  1. 获取CloneSet的相关配置和参数。

  2. 初始化变量用于统计各种Pod数量。

  3. 遍历所有的Pod,根据其所属的版本进行分类统计。

  4. 对于属于新版本的Pod:

  • 如果Pod处于预删除状态,增加预删除计数。

  • 否则,如果Pod被指定为删除,则增加要删除的新版本Pod计数。

  • 否则,如果Pod不可用(未准备好),增加不可用的新版本Pod计数。

  • 否则,增加活跃的新版本Pod计数。

  1. 对于属于旧版本的Pod:

  • 如果Pod处于预删除状态,增加预删除计数。

  • 否则,如果Pod被指定为删除,则增加要删除的旧版本Pod计数。

  • 否则,如果Pod不可用(未准备好),增加不可用的旧版本Pod计数。

  • 否则,增加活跃的旧版本Pod计数。

  1. 根据新旧版本Pod的数量差异和相关配置,计算扩缩容和更新所需的Pod数量。

  2. 如果允许使用扩容(maxSurge大于0):

  • 如果存在要删除的Pod,则计算可用于扩容的Pod数量,并确保不超过maxSurge。

  • 否则,根据新旧版本Pod数量差异计算可用于扩容的Pod数量,并确保不超过maxSurge。

  1. 计算需要缩容的Pod数量,确保不超过当前Replicas数。

  2. 如果需要扩容:

  • 计算可用于扩容的Pod数量上限,确保不超过scaleMaxUnavailable和总不可用Pod数量的差值。

  • 计算可用于删除的Pod数量上限,确保不超过maxUnavailable和总不可用Pod数量的差值。

  1. 如果存在要删除的Pod或需要缩容:

  • 计算可用于删除的Pod数量上限,确保不超过maxUnavailable和总不可用Pod数量的差值。

  1. 根据更新的新旧版本Pod数量差异,计算更新的Pod数量上限。
  2. 返回计算结果。
  1. 将 pod 分组 updatedPods, notUpdatedPods := clonesetutils.SplitPodsByRevision(pods, updateRevision)

  2. 扩容 (diffRes.scaleNum > 0 && diffRes.scaleUpLimit > 0): 创建 pod 和 pvc

  3. 删除 podsInPreDelete 数组里的 pods

  4. 对于不在 podsInPreDelete 但在 podsSpecifiedToDelete中的pods (toDelete), 采用一下逻辑进行删除

    1. 首先把要删除的 pod 根据 revision 分为两组 oldPodsToDelete 和 newPodsToDelete, 分别代表更新后和更新前的不同版本
    2. 优先从 newPodsToDelete 中选取diffRes.deleteReadyLimit个pod标记删除,如果数量不够,再从 oldPodsToDelete 里面按序拿,以此规则来对删除的顺序和并发度进行限制
  5. 缩容: 缩容的关键是选取哪些 pod 应当被删除,具体可以参考函数 choosePodsToDelete,规则简述如下:

    1. 优先删除 notUpdatedPods, 如果不够再从 UpdatedPods 里面选。 其中从 notUpdatedPods 里面可选的 pod 数量受 diffRes.scaleNumOldRevision 限制。

    2. 从集合里选取 pod 的规则:如果配置了 Pod 拓扑分布约束(Topology Spread Constraints), 则按照该规则排序 pods, 没有则按照 NewSameNodeRanker 规则进行排序,该规则可以理解为 pod 的默认权重,对位于同一个 node 上的 pos 按照 ActivePodsWithRanks 排序。

生命周期钩子

先阅读官方文档,重复内容不赘述

生命周期钩子是代码切面的一种实现,在 pod 状态流转时通过 lifecycle 暴露切面,使得用户可以注入自定义逻辑。

每个 cloneset 所管理的 pod 会有明确的状态,在 pod label 中被 lifecycle.apps.kruise.io/state 所反应,总共五个状态

    LifecycleStateNormal          LifecycleStateType = "Normal"
    LifecycleStatePreparingUpdate LifecycleStateType = "PreparingUpdate"
    LifecycleStateUpdating        LifecycleStateType = "Updating"
    LifecycleStateUpdated         LifecycleStateType = "Updated"
    LifecycleStatePreparingDelete LifecycleStateType = "PreparingDelete" 

Pod 会继承用户在 asts 中定义的 PreNormal / PreparingDelete / PreparingUpdate 生命周期钩子,那么在 cs pod 创建/删除/更新的时候会通过函数 IsPodHooked 判断并调整 pod 生命周期 state,如果没有相关的钩子则会直接进入 normal / updating / terminating。

如果有生命周期钩子, pod 上会看到 lable/finalizer 信息,需要用户的 controller 去处理这些消息,当所有 finalizer 处理完毕后,框架才会进行状态流转

使用

  lifecycle:
    preDelete:
      markPodNotReady: true
      finalizersHandler:
      - example.io/unready-blocker

代码示例

生命周期钩子相关的代码仍然注册在 reconcile 流程中

例如删除函数在发现有生命周期钩子后就会把实例状态变更为 “PreparingDelete”, 只有用户的controller去掉了对应的 lable/finalizer 时, 状态才会流转到 Delete

func (r *realControl) deletePods(cs *appsv1alpha1.CloneSet, podsToDelete []*v1.Pod, pvcs []*v1.PersistentVolumeClaim) (bool, error) {
    var modified bool
    for _, pod := range podsToDelete {
        if cs.Spec.Lifecycle != nil && lifecycle.IsPodHooked(cs.Spec.Lifecycle.PreDelete, pod) {
            if updated, err := r.lifecycleControl.UpdatePodLifecycle(pod, appspub.LifecycleStatePreparingDelete); err != nil {
                return false, err
            } else if updated {
                klog.V(3).Infof("CloneSet %s scaling update pod %s lifecycle to PreparingDelete",
                    clonesetutils.GetControllerKey(cs), pod.Name)
                modified = true
                clonesetutils.ResourceVersionExpectations.Expect(pod)
            }
            continue
        }
        ......
    }

    return modified, nil
}

type Lifecycle struct {
    // PreDelete is the hook before Pod to be deleted.
    PreDelete *LifecycleHook `json:"preDelete,omitempty"`
    // InPlaceUpdate is the hook before Pod to update and after Pod has been updated.
    InPlaceUpdate *LifecycleHook `json:"inPlaceUpdate,omitempty"`
}
func IsPodHooked(hook *appspub.LifecycleHook, pod *v1.Pod) bool {
    if hook == nil || pod == nil {
        return false
    }
    for _, f := range hook.FinalizersHandler {
        if controllerutil.ContainsFinalizer(pod, f) {
            return true
        }
    }
    
    for k, v := range hook.LabelsHandler {
        if pod.Labels[k] == v {
            return true
        }
    }
    return false
}