OpenKruise: Advanced Statefulset 代码详解

570 阅读15分钟

openkruise 系列教程:juejin.cn/column/7262…

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

openkruise doc :openkruise.io/zh/docs/

code: github.com/openkruise/…

重要概念

本章主要介绍了 openkruise workload Advanced Statefulset 的代码实现,同时也描述了 kruise-daemon imagepuller 的原理

Revision

可以理解为模版的版本, 用于版本控制

在Kubernetes中,Revision(修订版本)是一种资源对象,用于跟踪控制器的变更历史。不同类型的控制器,如Deployment、StatefulSet和DaemonSet,都可以使用Revision来记录其变更历史。

Revision是控制器的一个抽象概念,它代表了控制器在某个时间点上的配置状态。每当控制器进行重大更改或滚动更新时,都会创建一个新的Revision。每个Revision都包含了一个与之关联的副本配置,例如ReplicaSet或StatefulSet的配置。

在Kubernetes层面,Revision并不是一种直接可见的资源类型,而是由不同类型的控制器所使用和管理的。例如,对于Deployment控制器,可以通过kubectl命令或Kubernetes API获取Deployment对象的修订版本历史。对于StatefulSet控制器,可以通过kubectl命令或API获取StatefulSet对象的修订版本历史。每个修订版本都具有唯一的标识符和与之关联的资源配置。

通过查看控制器对象的修订版本历史,可以了解控制器的变更情况、回滚操作以及版本控制等相关信息。这对于管理和监视控制器的状态和变更非常有用。

为啥会有孤儿 revision ?

孤儿(orphan)Revision是指在Kubernetes中没有与之关联的控制器的Revision。这种情况可能发生在以下情形下:

  1. 控制器被删除:当一个控制器被删除时,与之关联的Revision可能成为孤儿Revision。这可能是因为删除控制器时没有正确处理与之关联的Revision,或者在删除控制器之前,Revision已经被解除与控制器的关联。
  2. 控制器更改:当控制器的配置发生更改时,旧的Revision可能会成为孤儿Revision。这是因为新的Revision已经被创建,并与控制器关联,但旧的Revision仍然存在,但没有与新的控制器关联。

孤儿Revision的存在可能会导致一些问题,例如资源泄漏和不一致性。因此,在管理控制器和Revision时,需要确保没有孤儿Revision存在。这可以通过采纳(adopt)孤儿Revision并将其与适当的控制器关联起来来解决。这样做可以确保Revision的稳定性和持久性,并避免不必要的资源浪费。

在下述的代码片段中有adoptOrphanRevisions函数,就是用来采纳与StatefulSet的选择器匹配的孤儿ControllerRevisions,并将它们与当前的StatefulSet关联起来,以确保这些Revision不会被删除。这样可以保持Revision的完整性,并确保它们能够被正确地管理和使用。

初始化

初始化流程在整个 kruise-manager 运行时进行

|- controller.init()
|- main()
  |- controller.SetupWithManager(mgr)
    |- statefulset.Add(mgr)
      |- utildiscovery.DiscoverGVK(controllerKind)
      |- r := newReconciler(mgr)
        |- statefulSetInformer, err := cacher.GetInformerForKind(context.TODO(), controllerKind)
        |- podInformer, err := cacher.GetInformerForKind(context.TODO(), v1.SchemeGroupVersion.WithKind("Pod"))
        |- xxxInformer ......
      |- add(mgr, r) // add adds a new Controller to mgr with r as the reconcile.Reconciler
        |- Watch(asts)
        |- Watch(Pod)

Reconcile

逻辑概览

func (ssc *ReconcileStatefulSet) Reconcile(_ context.Context, request reconcile.Request) (res reconcile.Result, retErr error)
    set, err := ssc.setLister.StatefulSets(namespace).Get(name){
    
    // 通过 spec.selector 生成一个 selector 对象,方便后续的匹配
    selector, err := metav1.LabelSelectorAsSelector(set.Spec.Selector)
    
    // 通过 ssc.control.ListRevisions 方法找出所有的 revision,并把与当前
    // 控制器匹配的孤儿 revision 纳入管理
    ssc.adoptOrphanRevisions(set)
    
    // 拿到 sts 应该管理的 pods
    pods, err := ssc.getPodsForStatefulSet(set, selector)
    
    // 会走到 ssc.control.UpdateStatefulSet(set.DeepCopy(), pods)
    ssc.syncStatefulSet(set, pods)
    
    // 参数填一个时间间隔表示多久后再 reconcile 一把, 这个时间从一个 set 里取
    return reconcile.Result{RequeueAfter: durationStore.Pop(getStatefulSetKey(set))}, err
}

核心逻辑: UpdateStatefulSet

File: stateful_set_control.go

获取期望状态

  1. currentRevision, updateRevision, collisionCount, err := ssc.getStatefulSetRevisions(set, revisions) 该函数返回 asts 的 【当前版本 预期版本 冲突数量】,冲突数量直接从当前的 status.CollisionCount 拿,代表了如多个 pod 想用同一编号的情况
  2. 根据新的修订版本( updateRevision ), 更新 expectations.objsUpdated 集合:
for _, pod := range pods {
   updateExpectations.ObserveUpdated(getStatefulSetKey(set), updateRevision.Name, pod)
}

func (r *realUpdateExpectations) ObserveUpdated(controllerKey, revision string, obj metav1.Object) {
    r.Lock()
    defer r.Unlock()

    expectations := r.controllerCache[controllerKey]
    if expectations == nil {
        return
    }
    
    // expectations.objUpdated 集合存放预期已经更新的 pod 信息
    // 因此如果期待更新的 pod 已经是最新版本,意味着预期与实际相符
    // 则从该集合中删除
    if expectations.revision == revision && expectations.objsUpdated.Has(getKey(obj)) && r.revisionAdapter.EqualToRevisionHash(controllerKey, obj, revision) {
        expectations.objsUpdated.Delete(getKey(obj))
    }
    
    // 如果预期与实际不符,那意味着控制器状态落后了,需要从缓存清理
    if expectations.revision != revision || expectations.objsUpdated.Len() == 0 {
        delete(r.controllerCache, controllerKey)
    }
}

更新 asts

/*
这段代码的作用是根据 StatefulSet 的目标状态,创建、更新和删除 Pods,以使其符
合目标状态,并根据不同的 UpdateStrategy.Type 进行不同的处理。如果操作成功,
返回的 StatefulSetStatus 是有效的,并且更新操作必须被记录。如果操作失败,
应该重试该方法直到成功为止

该更新操作一共被分为三步
1. 创建 pod 
2. 删除 pod
3. 更新 pod
*/
func (ssc *defaultStatefulSetControl) updateStatefulSet (...){
  // 根据当前的 revision 和 updateRevision 拿到 
  // 两个对应的 appsv1beta1.StatefulSet 结构体
  currentSet, err := ApplyRevision(set, currentRevision)
  updateSet, err := ApplyRevision(set, updateRevision)
  
  // 找到一些特殊的 pod 放进 specialPods, 特殊的 pods 指的是在 templates
  // 中 resourceRequirements 被更新时候,不修改其 pod 的 resourceRequirements
  // 的 pod 
  specialPods := []*v1.Pod{}
  for _, podsSelector := range set.Spec.SpecialPodsSelectors {
    ......
  }
  
  if !isPreDownloadDisabled && sigsruntimeClient != nil {
    if currentRevision.Name != updateRevision.Name {
        // 需要更新且开启预热 [特性:原地升级自动预热]
        /*
          1. 根据 annotation 拿到 minUpdatedReadyPods(最小就绪数)
          2. 如果 最小就绪数 <= UpdatedReadyReplicas, 那么代表
             已经至少有 minUpdatedReadyPods 更新但还没有达到最小就就绪要求
             ,那么可以开始后续镜像的预热
        */
        minUpdatedReadyPods, ok := set.Annotations[appsv1alpha1.ImagePreDownloadMinUpdatedReadyPods]
        if int32(minUpdatedReadyPodsCount) <= updatedReadyReplicas {
          ssc.createImagePullJobsForInPlaceUpdate(set, currentRevision, updateRevision)
        }
    }else {
      //  版本相等则没有更新操作,那么删掉 pull image job
    }
  }
  
  // 生成一个新的 status 变量并设置一些参数
  status := appsv1beta1.StatefulSetStatus{}
  ....
  // 这里重点说一下 ReserveOrdinals []int `json:"reserveOrdinals,omitempty"`
  // 它的作用是与 replicas 一起管理 pod 的序号,管理的规则如下:
  // 假设 replica = 3, 那么 pod 有编号 [0, 1 ,2]
  // 当 reserveOrdinals = [1], 那么 pod 会变成 [0, 2, 3], sts 会删除 pod-1 
  // 并创建 pod-3
  // 如果只想删除 pod-1, 那么应该同时把 replicas set 为 2, 那么 pods 为 [0, 2]
  reserveOrdinals := sets.NewInt(set.Spec.ReserveOrdinals...)
  
  if set.Spec.ScaleStrategy != nil && set.Spec.ScaleStrategy.MaxUnavailable != nil {
    // [特性: 最大不可用]
    // 使得 scaleMaxUnavailable = &maxUnavailable, 
    // 能在接下来上下文中被使用
  }
  
  // 根据传进来的 pods 信息更新 status
  // 包括更新 replicas, readyReplicas, 
  // updatedReadyReplicas, availableReplicas 等
  for i := range pods {
        status.Replicas++
        // ...... 更新各种 replicas
        
        // 把 pod 分为两类,replicas[] 存要保留的
        // condemned[] 存需要删除的
        if ord := getOrdinal(pods[i]); 0 <= ord && ord < replicaCount && !reserveOrdinals.Has(ord) {
            replicas[ord] = pods[i]

        } else if ord >= replicaCount || reserveOrdinals.Has(ord) {
            condemned = append(condemned, pods[i])
        }
   }
   
   // 创建需要的 pod info,上面已经描述了整体逻辑, 从此之后 replicas[] 存的包含全部
   // 需要的 pod 信息
   for ord := 0; ord < replicaCount; ord++ {
        if reserveOrdinals.Has(ord) {
            continue
        }
        if replicas[ord] == nil {
            replicas[ord] = newVersionedStatefulSetPod(
                currentSet,
                updateSet,
                currentRevision.Name,
                updateRevision.Name, ord, replicas)
        }
    }
    
    /*
      健康检查, 从要删除的 pods 中寻找第一个不健康的 pod 并保存到
      firstUnhealthyOrdinal 和 firstUnhealthyPod
    */
    
    /*
      对正常的 pos 进行检查
    */
    for i := range replicas {
      /*
        1. delete and recreate failed pods
        2. 如果 pod 还没创建,那么创建它
           如果策略上不允许 paraller,则不连续创建,直接返回(一个一个创建)
      */
      if isFailed(replicas[i]) {
        .......
      }
      if !isCreated(replicas[i]) {
        .......
      }
      
      /*
        monotonic := !allowsBurst(set) 这句话的含义是
        monotonic := !(podManagementPolicy == paraller)
        变量 monotonic 代表是否为串行操作
        如果为串行操作(monotonic == true), 那我们得等到最后一个 pod 结束
        如果为兵法操作(monotonic == false), 我们通过 maxUnavailable 控制并发度 
      */
      if isTerminating(replicas[i]) && (monotonic || decreaseAndCheckMaxUnavailable(scaleMaxUnavailable)){......}
      
      /*
         1. 等待到该 replica Running and availabe
         2. 执行 sts 不变性条件: identityMatchs(身份匹配) &  
            storageMatchs(存储匹配), 如果都 ok 直接 continue
      */
      ......
      
       
      /*
        这里展开说一下 UpdateStatefulPod
        1. 检查 identityMatches, 如果不满足则会更新 pod 相应的信息
           pod.Name, pod.Namespace, pod.Labels
        2. 检查 storageMatch, 如果不满足也会更新 pod 相应的信息并创建 pvc 资源
        3. 如果上述两个检查都满足了,则直接返回 nil
        4. 调用 k8s client 去真正地将更新内容 apply 到 pod 上
      */
      replica := replicas[i].DeepCopy()
      ssc.podControl.UpdateStatefulPod(updateSet, replica)
    }
    
    // 此时我们所有 replicas 里的 pod 都应该是 running 状态,接下来要处理
    // condemned 里的 pod,需要把他们都删除掉
    for target := len(condemned) - 1; target >= 0; target-- {
      if isTerminating(condemned[target]) {
        // 串行策略则按序等待,并行策略则 continue 继续处理
      }
      
      /*
        这里如果是串行策略且当前处理的不是第一个不健康 pod,那么会直接 return
        这样的策略在多次 reconcile 的意义是会优先删除不健康的 pod
      */
      if avail, waitTime := isRunningAndAvailable(condemned[target], minReadySeconds); !avail && monotonic && condemned[target] != firstUnhealthyPod {
        return
      }
      
      ssc.deletePod(set, condemned[target])
      更新 status 等信息
    }
    
    // 前面完成了 pod 的创建和删除,现在来进行 pod 的更新
    // 如果有滚动升级策略,那么解析出对应的 maxUnavailable, 如果没有,该值为 1
    maxUnavailable := 1
    if set.Spec.UpdateStrategy.RollingUpdate != nil {......}
    
    for _, pod := range pods {
      // 刷新一把所有 pod 的状态
      ssc.refreshPodState(set, pod)
    }
    
    /* 
      根据预先定义的优先级策略来排序 [特性: 优先级策略]
      返回需要更新的 indexes
    */
    updateIndexes := sortPodsToUpdate(set.Spec.UpdateStrategy.RollingUpdate, updateRevision.Name, *set.Spec.Replicas, replicas)
    
    for _, target := range updateIndexes {
      /*
       对于 special pods 会先判断一下是否有必要更新,
       如果没必要则直接 continue
       没必要更新意味着只需要更新  resourceRequirements
      */
      isUpdateNeeded(replicas[target], set, revisions, currentSet, updateSet)
      
      // 处理 revision 不对且没有处于删除状态的 pod
      if getPodRevision(replicas[target]) != updateRevision.Name && !isTerminating(replicas[target]) {
        /*
          尝试原地更新一下 pod
          如果 inplacing == false,则代表无法原地更新,则删除 pod
        */
        inplacing, inplaceUpdateErr := ssc.inPlaceUpdatePod(set, replicas[target], updateRevision, revisions)
        if !inplacing { 
          if err := ssc.podControl.DeleteStatefulPod(set, replicas[target]); err != nil {
                    return &status, err
          }
        } 
        /*
          这里说一下条件为什么是 currentRevision.Name
          通过这个条件的 pod 为更新失败但 DeleteStatefulPod 调用成功的,
          意味着成功被删除。 所以需要调整一下 status.CurrentReplicas
        */
        if getPodRevision(replicas[target]) == currentRevision.Name {
           status.CurrentReplicas--
        }
      }
      
      /* 
        更新后的 pod 状态检查,把还没有 runningAndAvailable 的 pod
        都放入 unavailablesPods 中,当 unavailablesPods 的数量 >=
        maxUnavailable 时,阻塞更新
      */
      ......
    }
    
}

原地更新

[特性: 原地升级]: 实际上它适用于 cloneSet, asts, advanced DaemonSet, SidecarSet,本小节以 asts 入口为例来说明该特性

Asts 都是通过调用到函数 defaultCalculateInPlaceUpdateSpec 来判断是否可进行原地更新的。 如果可以更新则返回一个 UpdateSpec 结构体,否则返回 nil (不能原地更新)

对于当前的 asts 实现,只能更新 image, 当然我们可以自定义下面的函数来扩展原地更新的内容

函数注册

更新相关的一些函数我们都注册在这里,也可以由此定义我们自己的更新规则

func SetOptionsDefaults(opts *UpdateOptions) *UpdateOptions {
    if opts == nil {
        opts = &UpdateOptions{}
    }

    if opts.CalculateSpec == nil {
        opts.CalculateSpec = defaultCalculateInPlaceUpdateSpec
    }

    if opts.PatchSpecToPod == nil {
        opts.PatchSpecToPod = defaultPatchUpdateSpecToPod
    }

    if opts.CheckUpdateCompleted == nil {
        opts.CheckUpdateCompleted = DefaultCheckInPlaceUpdateCompleted
    }

    return opts
}

入口 inPlaceUpdatePod

/*
这段代码是 `inPlaceUpdatePod` 方法的实现。该方法用于执行就地更新
(in-place update)操作,并返回更新是否成功以及相关的错误。

首先,它通过循环遍历 `revisions` 切片,找到与目标 Pod 的修订名称
匹配的旧修订版本。这个旧修订版本用于后续的就地更新操作。

然后,它创建了一个 `UpdateOptions` 对象 `opts`,并根据 
StatefulSet 的配置设置 `GracePeriodSeconds`。

接下来,它调用 `ssc.inplaceControl.CanUpdateInPlace()` 
方法来检查是否可以进行就地更新。如果可以进行就地更新,并且
更新涉及到容器镜像的更改,则进一步检查 StatefulSet 的配置是否允许就地更新。

在进行实际的就地更新之前,它检查目标 Pod 的生命周期状态,
并根据状态执行相应的操作。根据状态的不同,可能会更新 Pod 的生命周期状态。

然后,它调用 `ssc.inplaceControl.Update()` 方法来执行实际的就地
更新操作。如果更新成功,则记录相关日志和事件,并返回更新结果。

最后,如果就地更新不满足条件或者更新失败,则根据 StatefulSet 的
配置返回相应的错误。

总的来说,这段代码的作用是执行就地更新操作,并根据更新结果返回相应的
状态和错误信息。
*/
func (ssc *defaultStatefulSetControl) inPlaceUpdatePod(
    set *appsv1beta1.StatefulSet, pod *v1.Pod,
    updateRevision *apps.ControllerRevision, revisions []*apps.ControllerRevision,
) (bool, error) {

    var oldRevision *apps.ControllerRevision
    for _, r := range revisions {
        if r.Name == getPodRevision(pod) {
            oldRevision = r
            break
        }
    }

    opts := &inplaceupdate.UpdateOptions{}
    if set.Spec.UpdateStrategy.RollingUpdate != nil && set.Spec.UpdateStrategy.RollingUpdate.InPlaceUpdateStrategy != nil {
        opts.GracePeriodSeconds = set.Spec.UpdateStrategy.RollingUpdate.InPlaceUpdateStrategy.GracePeriodSeconds
    }
    
    // 判断一把
    if ok, updateOpt := ssc.inplaceControl.CanUpdateInPlace(oldRevision, updateRevision, opts); ok {
        if len(updateOpt.ContainerImages) != 0 {
            if set.Spec.UpdateStrategy.RollingUpdate == nil {
                return false, nil
            }
            if set.Spec.UpdateStrategy.RollingUpdate.PodUpdatePolicy != appsv1beta1.InPlaceIfPossiblePodUpdateStrategyType &&
                set.Spec.UpdateStrategy.RollingUpdate.PodUpdatePolicy != appsv1beta1.InPlaceOnlyPodUpdateStrategyType {
                return false, nil
            }
        }
        klog.V(2).Infof("ServerlessDB: inplace update resources")
        state := lifecycle.GetPodLifecycleState(pod)
        switch state {
        case "", appspub.LifecycleStateNormal:
            var err error
            var updated bool
            if set.Spec.Lifecycle != nil && lifecycle.IsPodHooked(set.Spec.Lifecycle.InPlaceUpdate, pod) {
                if updated, err = ssc.lifecycleControl.UpdatePodLifecycle(pod, appspub.LifecycleStatePreparingUpdate); err == nil && updated {
                    klog.V(3).Infof("StatefulSet %s updated pod %s lifecycle to PreparingUpdate",
                        getStatefulSetKey(set), pod.Name)
                }
                return true, err
            }
        case appspub.LifecycleStateUpdated:
            var err error
            var updated bool
            var inPlaceUpdateHandler *appspub.LifecycleHook
            if set.Spec.Lifecycle != nil {
                inPlaceUpdateHandler = set.Spec.Lifecycle.InPlaceUpdate
            }
            if updated, err = ssc.lifecycleControl.UpdatePodLifecycleWithHandler(pod, appspub.LifecycleStatePreparingUpdate, inPlaceUpdateHandler); err == nil && updated {
                klog.V(3).Infof("StatefulSet %s updated pod %s lifecycle to PreparingUpdate",
                    getStatefulSetKey(set), pod.Name)
            }
            return true, err
        case appspub.LifecycleStatePreparingUpdate:
            if set.Spec.Lifecycle != nil && lifecycle.IsPodHooked(set.Spec.Lifecycle.InPlaceUpdate, pod) {
                return true, nil
            }
        case appspub.LifecycleStateUpdating:
        default:
            return true, fmt.Errorf("not allowed to in-place update pod %s in state %s", pod.Name, state)
        }

        if state != "" {
            opts.AdditionalFuncs = append(opts.AdditionalFuncs, lifecycle.SetPodLifecycle(appspub.LifecycleStateUpdating))
        }
        res := ssc.inplaceControl.Update(pod, oldRevision, updateRevision, opts)
        if res.InPlaceUpdate {
            if res.DelayDuration > 0 {
                durationStore.Push(getStatefulSetKey(set), res.DelayDuration)
            }

            if res.UpdateErr == nil {
                updateExpectations.ExpectUpdated(getStatefulSetKey(set), updateRevision.Name, pod)
                ssc.recorder.Eventf(set, v1.EventTypeNormal, "SuccessfulUpdatePodInPlace", "successfully update pod %s in-place(revision %v)", pod.Name, updateRevision.Name)
                return res.InPlaceUpdate, nil
            }
            ssc.recorder.Eventf(set, v1.EventTypeWarning, "FailedUpdatePodInPlace", "failed to update pod %s in-place(revision %v): %v", pod.Name, updateRevision.Name, res.UpdateErr)
            return res.InPlaceUpdate, res.UpdateErr
        }
    }

    if set.Spec.UpdateStrategy.RollingUpdate != nil && set.Spec.UpdateStrategy.RollingUpdate.PodUpdatePolicy == appsv1beta1.InPlaceOnlyPodUpdateStrategyType {
        return false, fmt.Errorf("find strategy is InPlaceOnly but Pod %s can not update in-place", pod.Name)
    }

    return false, nil
}

功能实现 Update

func (c *realControl) Update(pod *v1.Pod, oldRevision, newRevision *apps.ControllerRevision, opts *UpdateOptions) UpdateResult {
    opts = SetOptionsDefaults(opts)

    /*
      计算是否可以原地更新,如果满足条件会返回 UpdateSpec
      asts 默认的 CalculateSpec 函数为  defaultCalculateInPlaceUpdateSpec
    */
    spec := opts.CalculateSpec(oldRevision, newRevision, opts)
    if spec == nil {
        return UpdateResult{}
    }

    // TODO(FillZpp): maybe we should check if the previous in-place update has completed

    // 2. update condition for pod with readiness-gate
    if containsReadinessGate(pod) {
        newCondition := v1.PodCondition{
            Type:               appspub.InPlaceUpdateReady,
            LastTransitionTime: c.now(),
            Status:             v1.ConditionFalse,
            Reason:             "StartInPlaceUpdate",
        }
        if err := c.updateCondition(pod, newCondition); err != nil {
            return UpdateResult{InPlaceUpdate: true, UpdateErr: err}
        }
    }

    // 3. update container images
    if err := c.updatePodInPlace(pod, spec, opts); err != nil {
        return UpdateResult{InPlaceUpdate: true, UpdateErr: err}
    }

    var delayDuration time.Duration
    if opts.GracePeriodSeconds > 0 {
        delayDuration = time.Second * time.Duration(opts.GracePeriodSeconds)
    }
    return UpdateResult{InPlaceUpdate: true, DelayDuration: delayDuration}
}

defaultCalculateInPlaceUpdateSpec

/*
这个函数是用于计算就地更新(in-place update)的规范(spec)的默认实现。它接受旧的修订版本(
oldRevision)、新的修订版本(newRevision)和更新选项(opts),并返回一个包含更新规范的结构体(
*UpdateSpec)。

首先,函数会检查旧的修订版本和新的修订版本是否为nil,如果是,则返回nil。然后,它会为更新选项设置
默认值(SetOptionsDefaults(opts))。

接下来,函数会使用旧的修订版本和新的修订版本之间的差异,生成一个JSON补丁(patches)。如果生成补丁
的过程中出现错误,则返回nil。

然后,函数会从旧的修订版本和新的修订版本中获取模板(template)。如果获取模板的过程中出现错误,
则返回nil。

接下来,函数会创建一个UpdateSpec结构体,并设置一些基本属性,如修订版本(Revision)、
容器镜像(ContainerImages)、容器资源(ContainerResources)和优雅终止期限(GraceSeconds)。
如果提供了获取修订版本的函数(opts.GetRevision),则使用该函数获取修订版本。

接下来,函数会遍历补丁中的每个操作,对于PodSpec的所有补丁,它只会更新镜像(ContainerImages)。
如果补丁操作是"replace"并且匹配镜像更新的路径(inPlaceImageUpdatePatchRexp.MatchString
jsonPatchOperation.Path)),则将旧的容器名称与新的镜像值关联起来,并存储在ContainerImages中。

如果补丁操作匹配资源更新的路径(inPlaceResourcesUpdatePatchRexp.MatchString
(jsonPatchOperation.Path)),则将旧的容器名称与新的资源值关联起来,并存储在ContainerResources中。

如果补丁操作不匹配镜像更新或资源更新的路径,则返回nil。

最后,如果元数据发生了变化,函数会生成元数据的补丁(MetaDataPatch),并检查是否启用了特定的功能门限(
utilfeature.DefaultFeatureGate.Enabled(features.InPlaceUpdateEnvFromMetadata))
或者ContainerImages为空。如果满足条件,则会检查每个容器的环境变量是否发生了变化,如果发生了变化,
则将UpdateEnvFromMetadata设置为true。

最后,函数返回生成的更新规范(UpdateSpec)。
*/
func defaultCalculateInPlaceUpdateSpec(oldRevision, newRevision *apps.ControllerRevision, opts *UpdateOptions) *UpdateSpec {
	if oldRevision == nil || newRevision == nil {
		return nil
	}
	opts = SetOptionsDefaults(opts)

	patches, err := jsonpatch.CreatePatch(oldRevision.Data.Raw, newRevision.Data.Raw)
	if err != nil {
		return nil
	}

	oldTemp, err := GetTemplateFromRevision(oldRevision)
	if err != nil {
		return nil
	}
	newTemp, err := GetTemplateFromRevision(newRevision)
	if err != nil {
		return nil
	}

	updateSpec := &UpdateSpec{
		Revision:             newRevision.Name,
		ContainerImages:      make(map[string]string),
		ContainerRefMetadata: make(map[string]metav1.ObjectMeta),
		GraceSeconds:         opts.GracePeriodSeconds,
	}
	if opts.GetRevision != nil {
		updateSpec.Revision = opts.GetRevision(newRevision)
	}

	// all patches for podSpec can just update images in pod spec
	var metadataPatches []jsonpatch.Operation
	for _, op := range patches {
		op.Path = strings.Replace(op.Path, "/spec/template", "", 1)

		if !strings.HasPrefix(op.Path, "/spec/") {
			if strings.HasPrefix(op.Path, "/metadata/") {
				metadataPatches = append(metadataPatches, op)
				continue
			}
			return nil
		}
		if op.Operation != "replace" || !containerImagePatchRexp.MatchString(op.Path) {
			return nil
		}
		// for example: /spec/containers/0/image
		words := strings.Split(op.Path, "/")
		idx, _ := strconv.Atoi(words[3])
		if len(oldTemp.Spec.Containers) <= idx {
			return nil
		}
		updateSpec.ContainerImages[oldTemp.Spec.Containers[idx].Name] = op.Value.(string)
	}

	if len(metadataPatches) > 0 {
		if utilfeature.DefaultFeatureGate.Enabled(features.InPlaceUpdateEnvFromMetadata) {
			// for example: /metadata/labels/my-label-key
			for _, op := range metadataPatches {
				if op.Operation != "replace" && op.Operation != "add" {
					continue
				}
				words := strings.SplitN(op.Path, "/", 4)
				if len(words) != 4 || (words[2] != "labels" && words[2] != "annotations") {
					continue
				}
				key := rfc6901Decoder.Replace(words[3])

				for i := range newTemp.Spec.Containers {
					c := &newTemp.Spec.Containers[i]
					objMeta := updateSpec.ContainerRefMetadata[c.Name]
					switch words[2] {
					case "labels":
						if !utilcontainermeta.IsContainerReferenceToMeta(c, "metadata.labels", key) {
							continue
						}
						if objMeta.Labels == nil {
							objMeta.Labels = make(map[string]string)
						}
						objMeta.Labels[key] = op.Value.(string)
						delete(oldTemp.ObjectMeta.Labels, key)
						delete(newTemp.ObjectMeta.Labels, key)

					case "annotations":
						if !utilcontainermeta.IsContainerReferenceToMeta(c, "metadata.annotations", key) {
							continue
						}
						if objMeta.Annotations == nil {
							objMeta.Annotations = make(map[string]string)
						}
						objMeta.Annotations[key] = op.Value.(string)
						delete(oldTemp.ObjectMeta.Annotations, key)
						delete(newTemp.ObjectMeta.Annotations, key)
					}

					updateSpec.ContainerRefMetadata[c.Name] = objMeta
					updateSpec.UpdateEnvFromMetadata = true
				}
			}
		}

		oldBytes, _ := json.Marshal(v1.Pod{ObjectMeta: oldTemp.ObjectMeta})
		newBytes, _ := json.Marshal(v1.Pod{ObjectMeta: newTemp.ObjectMeta})
		patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldBytes, newBytes, &v1.Pod{})
		if err != nil {
			return nil
		}
		updateSpec.MetaDataPatch = patchBytes
	}
	return updateSpec
}

升级预热

[特性:ImagePullJob]

File: statefulset_predownload_image.go

顶层入口 createImagePullJobsForInPlaceUpdate

func (dss *defaultStatefulSetControl) createImagePullJobsForInPlaceUpdate(sts *appsv1beta1.StatefulSet, currentRevision, updateRevision *apps.ControllerRevision) error {
  // 一些基本的条件判断
  .......
  // 拿到需要下载的 image
  containerImages := diffImagesBetweenRevisions(currentRevision, updateRevision)
  // 对每一个 image 创建下载任务
  for name, image := range containerImages {
    imagejobutilfunc.CreateJobForWorkload(sigsruntimeClient, sts, controllerKind, jobName, image, labelMap, *selector, pullSecrets)
  }
}

Job 实现 CreateJobForWorkload

通过 OwnerReferences 来将 job 和 workload 进行关联,从而使得它拉取的镜像能够被目标 workload 所使用

func CreateJobForWorkload(c client.Client, owner metav1.Object, gvk schema.GroupVersionKind, name, image string, labels map[string]string, podSelector metav1.LabelSelector, pullSecrets []string) error {
    // 一些参数配置
    var pullTimeoutSeconds int32 = 300
    if str, ok := owner.GetAnnotations()[appsv1alpha1.ImagePreDownloadTimeoutSecondsKey]; ok {
        if i, err := strconv.Atoi(str); err == nil {
            pullTimeoutSeconds = int32(i)
        }
    }

    parallelism := intstr.FromInt(1)
    if str, ok := owner.GetAnnotations()[appsv1alpha1.ImagePreDownloadParallelismKey]; ok {
        if i, err := strconv.Atoi(str); err == nil {
            parallelism = intstr.FromInt(i)
        }
    }
    
    // job 模版渲染
    job := &appsv1alpha1.ImagePullJob{
        ObjectMeta: metav1.ObjectMeta{
            Namespace:       owner.GetNamespace(),
            Name:            name,
            OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(owner, gvk)},
            Labels:          labels,
        },
        Spec: appsv1alpha1.ImagePullJobSpec{
            Image:       image,
            PullSecrets: pullSecrets,
            PodSelector: &appsv1alpha1.ImagePullJobPodSelector{LabelSelector: podSelector},
            Parallelism: &parallelism,
            PullPolicy:  &appsv1alpha1.PullPolicy{BackoffLimit: utilpointer.Int32Ptr(1), TimeoutSeconds: &pullTimeoutSeconds},
            CompletionPolicy: appsv1alpha1.CompletionPolicy{
                Type:                    appsv1alpha1.Always,
                TTLSecondsAfterFinished: utilpointer.Int32Ptr(600),
            },
        },
    }

    return c.Create(context.TODO(), job)
}

Node 级预热( NodeImage )

pkg/daemon/imagepuller

openkruise.io/zh/docs/use…

用于在指定 node 去拉镜像

Controller

Controller 主要用于管理 NodeImage,核心逻辑都在 sync 函数里面

  1. 调用 c.puller.Sync(nodeImage.DeepCopy(), ref) 来调用访问 worker 的 sync 函数以实现 image pull
  2. 刷新 NodeImage 的 status 来反应当前 job 的状态

Worker

调度逻辑

调度的主要逻辑都在 sync 里,所以下面详述 sync 的逻辑

  1. reset workerPools(map[string]workerPool): 把不在 spec 里的 work 停止并删除; 把 pools 里没有的worker 创建并加入; 最终使得 workerPools 里每个元素都与 NodeImage.Spec.Images 一一对应

  2. 对每个 worker(wokerPools[imageName]) 调用 pool.Sync(&imageSpec, imageStatus, ref) 函数

    1. 遍历 NodeImage.Spec.Tags 字段(用于管理多版本的 image),并检查 status 中是否存在对应的 tag,并根据 status 的结果来将 tags 分为 allTags 和 activeTags
    2. apiVersion: apps.kruise.io/v1alpha1
      kind: NodeImage
      metadata:
        name: my-app-image
      spec:
        image: my-registry/my-app
        tags:
          - v1.0
          - v1.1
          - latest
      
    3. 清理 workerpool 中 non-active | version 不对的 worker: delete(w.pullWorkers, tag), 这里 w 就是上一层函数的 pool
    4. 对于 activeTags 中存在但 worker pool 里找不到的对象,起新的 worker newPullWorker(w.name, tagSpec, secrets, w.runtime, w, ref, w.eventRecorder)
    5. func newPullWorker(name string, tagSpec appsv1alpha1.ImageTagSpec, secrets []v1.Secret, runtime runtimeimage.ImageService, statusUpdater imageStatusUpdater, ref *v1.ObjectReference, eventRecorder record.EventRecorder) *pullWorker {
          o := &pullWorker{
              name:          name,
              tagSpec:       tagSpec,
              secrets:       secrets,
              runtime:       runtime,
              statusUpdater: statusUpdater,
              ref:           ref,
              eventRecorder: eventRecorder,
              active:        true,
              stopCh:        make(chan struct{}),
          }
          go o.Run()
          return o
      }
      

工作逻辑

这里介绍 pullWorker Run 函数的逻辑

  1. 参数初始化阶段

    1. timeout = tagSpec.PullPolicy.TimeoutSeconds, 默认值 10min
    2. backoffLimit = tagSpec.PullPolicy.BackoffLimit, 默认值 3(最大重试次数)
    3. deadline = now + tagSpec.PullPolicy.ActiveDeadlineSeconds, 默认值为 nil (不设上限)
  2. 会根据上述三个参数来决定是否使任务失败,如果在最大重试次数之内且单次任务没超时且没超过deadline,那么会调用函数 doPullImage(pullContext, newStatus)

    1. 调用 getImageInfo(ctx)拿到 image 信息,image 信息如下所示

    2. type ImageInfo struct {
          // ID of an image.
          ID string `json:"Id,omitempty"`
          // repository with digest.
          RepoDigests []string `json:"RepoDigests"`
          // repository with tag.
          RepoTags []string `json:"RepoTags"`
          // size of image's taking disk space.
          Size int64 `json:"Size,omitempty"`
      }
      
    3. w.runtime.PullImage(ctx, w.name, tag, w.secrets) 拉镜像

      1. 该函数位于 pkg/daemon/criruntime/containerd.go

      2. imageRef = imageName:tag (e.g.: my-registry/my-app:v1.0)

      3. 规范化镜像名 namedRef = daemonutil.NormalizeImageRef(imageRef)

      4. 获取镜像解析器 resolver, isSchema1, err := d.getResolver(ctx, namedRef, pullSecrets),其返回值分别为镜像解析器和是否为 Docker Schema 1 格式的镜像还有错误信息

        1. 尝试用传进来的 pullSecrets 解析镜像,如果解析失败则进入下一步
        2. 尝试用 containerdImageClient 的 accountManager 里记录的默认账户信息或匿名账户信息解析镜像
      5. d.doPullImage(ctx, namedRef, isSchema1, resolver)

        1. 创建一个 newFeatchJobs 实例 ongoing 用于跟踪当前的拉取任务

        2. 创建 pipeRpipeW, 分别代表管道读取器和管道写入器,pipeR 用于返回 ImagePullStatusReader

        3. 创建一个基于 json 的 stream 流,用于将 pullimage 进度信息写入 pipeW

        4. 使用containerd.RemoteOpt选项配置了拉取镜像的参数。其中包括使用Schema1转换、指定解析器、解压缩拉取的镜像、指定快照存储器以及设置镜像处理程序。

        5. 起一个协程调用 fetchProgress函数使得 ongoing 的内容能够写入 stream

        6. 起一个协程拉镜像

          1. 首先调用了 img, err := d.client.Pull(pctx, ref.String(), opts...) 拉镜像,其中ops 就是步骤 4 里的参数

            1. 这里的 pull 函数是 k8s 原生的,用于将提供的内容(通常是镜像)下载到 container 的内容存储中
          2. 调用 createRepoDigestRecord 更新 container 中的摘要信息(Digest)

    4. Select 等待上述函数的信号,根据返回值判断任务执行情况并更新 status

  3. 根据 doPullImage 的结果来处理失败或者正常结束