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。这种情况可能发生在以下情形下:
- 控制器被删除:当一个控制器被删除时,与之关联的Revision可能成为孤儿Revision。这可能是因为删除控制器时没有正确处理与之关联的Revision,或者在删除控制器之前,Revision已经被解除与控制器的关联。
- 控制器更改:当控制器的配置发生更改时,旧的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
获取期望状态
currentRevision, updateRevision, collisionCount, err := ssc.getStatefulSetRevisions(set, revisions)该函数返回 asts 的 【当前版本 预期版本 冲突数量】,冲突数量直接从当前的 status.CollisionCount 拿,代表了如多个 pod 想用同一编号的情况- 根据新的修订版本( 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: ¶llelism,
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
用于在指定 node 去拉镜像
Controller
Controller 主要用于管理 NodeImage,核心逻辑都在 sync 函数里面
- 调用
c.puller.Sync(nodeImage.DeepCopy(), ref)来调用访问 worker 的 sync 函数以实现 image pull - 刷新 NodeImage 的 status 来反应当前 job 的状态
Worker
调度逻辑
调度的主要逻辑都在 sync 里,所以下面详述 sync 的逻辑
-
reset workerPools(map[string]workerPool): 把不在 spec 里的 work 停止并删除; 把 pools 里没有的worker 创建并加入; 最终使得 workerPools 里每个元素都与 NodeImage.Spec.Images 一一对应
-
对每个 worker(wokerPools[imageName]) 调用
pool.Sync(&imageSpec, imageStatus, ref)函数- 遍历 NodeImage.Spec.Tags 字段(用于管理多版本的 image),并检查 status 中是否存在对应的 tag,并根据 status 的结果来将 tags 分为 allTags 和 activeTags
-
apiVersion: apps.kruise.io/v1alpha1 kind: NodeImage metadata: name: my-app-image spec: image: my-registry/my-app tags: - v1.0 - v1.1 - latest - 清理 workerpool 中 non-active | version 不对的 worker:
delete(w.pullWorkers, tag), 这里 w 就是上一层函数的 pool - 对于 activeTags 中存在但 worker pool 里找不到的对象,起新的 worker
newPullWorker(w.name, tagSpec, secrets, w.runtime, w, ref, w.eventRecorder) -
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 函数的逻辑
-
参数初始化阶段
- timeout = tagSpec.PullPolicy.TimeoutSeconds, 默认值 10min
- backoffLimit = tagSpec.PullPolicy.BackoffLimit, 默认值 3(最大重试次数)
- deadline = now + tagSpec.PullPolicy.ActiveDeadlineSeconds, 默认值为 nil (不设上限)
-
会根据上述三个参数来决定是否使任务失败,如果在最大重试次数之内且单次任务没超时且没超过deadline,那么会调用函数
doPullImage(pullContext, newStatus)-
调用
getImageInfo(ctx)拿到 image 信息,image 信息如下所示 -
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"` } -
w.runtime.PullImage(ctx, w.name, tag, w.secrets)拉镜像-
该函数位于 pkg/daemon/criruntime/containerd.go
-
imageRef = imageName:tag (e.g.: my-registry/my-app:v1.0)
-
规范化镜像名
namedRef = daemonutil.NormalizeImageRef(imageRef) -
获取镜像解析器
resolver, isSchema1, err := d.getResolver(ctx, namedRef, pullSecrets),其返回值分别为镜像解析器和是否为 Docker Schema 1 格式的镜像还有错误信息- 尝试用传进来的 pullSecrets 解析镜像,如果解析失败则进入下一步
- 尝试用 containerdImageClient 的 accountManager 里记录的默认账户信息或匿名账户信息解析镜像
-
d.doPullImage(ctx, namedRef, isSchema1, resolver)-
创建一个
newFeatchJobs实例ongoing用于跟踪当前的拉取任务 -
创建
pipeR和pipeW, 分别代表管道读取器和管道写入器,pipeR 用于返回 ImagePullStatusReader -
创建一个基于 json 的
stream流,用于将 pullimage 进度信息写入pipeW -
使用
containerd.RemoteOpt选项配置了拉取镜像的参数。其中包括使用Schema1转换、指定解析器、解压缩拉取的镜像、指定快照存储器以及设置镜像处理程序。 -
起一个协程调用
fetchProgress函数使得ongoing的内容能够写入stream -
起一个协程拉镜像
-
首先调用了
img, err := d.client.Pull(pctx, ref.String(), opts...)拉镜像,其中ops 就是步骤 4 里的参数- 这里的 pull 函数是 k8s 原生的,用于将提供的内容(通常是镜像)下载到 container 的内容存储中
-
调用
createRepoDigestRecord更新 container 中的摘要信息(Digest)
-
-
-
-
Select 等待上述函数的信号,根据返回值判断任务执行情况并更新 status
-
-
根据 doPullImage 的结果来处理失败或者正常结束