OpenKruise 是基于 CRD 的拓展,蕴含了很多利用工作负载和运维加强能力,本系列文章会从源码和底层原理上解读各个组件,以帮忙大家更好地应用和了解 OpenKruise。让咱们开始 OpenKruise 的源码之旅吧!
前言
在上一篇文章中咱们解读了 OpenKruise 原地降级的原理和相干代码,在此基础上咱们来钻研一个基于原地降级能力的组件 - ContainerRecreateRequest。ContainerRecreateRequest(下文简称 CRR)
可能重建 Pod 中一个或多个容器。该性能和 Kruise 提供的原地降级相似,当一个容器重建的时候,Pod 中的其余容器还放弃失常运行。重建实现后,Pod 中除了该容器的 restartCount 减少以外不会有什么其余变动。如果挂载了 volume mount 挂载卷,卷中的数据不会失落也不须要从新挂载。这个性能实现了运维容器与业务容器的治理拆散,比方一个 Pod 中会有主容器中运行外围业务,sidecar 中运行运维容器,比方日志收集等.当业务容器须要重启的时候,传统的更新形式会让整个 Pod 重启从而导致运维容器无端被重启从而中断服务,而应用 ContainerRecreateRequest 能够实现只让特定的容器重启,高效的同时更加平安。
明天就让咱们从源码的角度来看一下 ContainerRecreateRequest
的实现原理。
源码解读
咱们先来看一下整个 CRR
的代码流程概览,能够看到整个过程次要有三个组件参加,包含 CRR 的 admission webhook, controller manager,以及咱们上一篇就提到过的原地降级中的重要组件 - kruise-daemon
中的 crr daemon controller
。
而后咱们再逐渐拆开解说每一步的内容。
1. create CRR
先看一下 CRR 这个自定义资源的 schema 定义:
apiVersion: apps.kruise.io/v1alpha1kind: ContainerRecreateRequestmetadata: namespace: pod-namespace name: xxxspec: podName: pod-name containers: # 要重建的容器名字列表,至多要有 1 个 - name: app - name: sidecar strategy: failurePolicy: Fail # 'Fail' 或 'Ignore',示意一旦有某个容器进行或重建失败, CRR 立刻完结 orderedRecreate: false # 'true' 示意要等前一个容器重建实现了,再开始重建下一个 terminationGracePeriodSeconds: 30 # 期待容器优雅退出的工夫,不填默认用 Pod 中定义的 unreadyGracePeriodSeconds: 3 # 在重建之前先把 Pod 设为 not ready,并期待这段时间后再开始执行重建 minStartedSeconds: 10 # 重建后新容器至多放弃运行这段时间,才认为该容器重建胜利 activeDeadlineSeconds: 300 # 如果 CRR 执行超过这个工夫,则间接标记为完结(未完结的容器标记为失败) ttlSecondsAfterFinished: 1800 # CRR 完结后,过了这段时间主动被删除掉
而后开始走读代码流程。
1.1 查看 feature-gate
当咱们创立一个 CRR 的时候,会最先通过 adminssion webhook,webhook 中会最先查看以后 feature gates 中是否开启了 kruise-daemon
,因为这个性能依赖于 kruise-daemon 组件来进行 Pod 容器,如果 KruiseDaemon feature-gate 被敞开了,ContainerRecreateRequest 也将无奈应用。
func (h *ContainerRecreateRequestHandler) Handle(ctx context.Context, req admission.Request) admission.Response { if !utilfeature.DefaultFeatureGate.Enabled(features.KruiseDaemon) { return admission.Errored(http.StatusForbidden, fmt.Errorf("feature-gate %s is not enabled", features.KruiseDaemon)) } ...}
1.2 注入默认值并查看 Pod
创立 CRR 的时候要为其注入一些特定的标签,为前面管制启动容器的流程做筹备,比方打上 ContainerRecreateRequestPodNameKey
,ContainerRecreateRequestActiveKey
的标签:
obj.Labels[appsv1alpha1.ContainerRecreateRequestPodNameKey] = obj.Spec.PodNameobj.Labels[appsv1alpha1.ContainerRecreateRequestActiveKey] = "true"
查看以后解决的 Pod 是否合乎更新条件,比方 Pod 是否是 active 的:
func IsPodActive(p *v1.Pod) bool { return v1.PodSucceeded != p.Status.Phase && v1.PodFailed != p.Status.Phase && p.DeletionTimestamp == nil}
以及 Pod 是否曾经实现调度,如果未实现调度的话就无奈实现原地重启(无奈应用部署到节点上的 kruise-daemon):
if !kubecontroller.IsPodActive(pod) { return admission.Errored(http.StatusBadRequest, fmt.Errorf("not allowed to recreate containers in an inactive Pod")) } else if pod.Spec.NodeName == "" { return admission.Errored(http.StatusBadRequest, fmt.Errorf("not allowed to recreate containers in a pending Pod")) }
1.3 将 Pod 中的信息注入到 CRR
CRR 的运行须要获取 Pod 的信息,比方获取 Pod 中的 Lifecycle.PreStop
让 kruise-daemon 执行 preStop hook 后把容器停掉,获取指定容器的 containerID
来判断重启后 containerID 的变动等。
err = injectPodIntoContainerRecreateRequest(obj, pod) if err != nil { return admission.Errored(http.StatusBadRequest, err) }...if podContainer.Lifecycle != nil && podContainer.Lifecycle.PreStop != nil { c.PreStop = &appsv1alpha1.ProbeHandler{ Exec: podContainer.Lifecycle.PreStop.Exec, HTTPGet: podContainer.Lifecycle.PreStop.HTTPGet, TCPSocket: podContainer.Lifecycle.PreStop.TCPSocket, } } ......
2. CRR controller
创立 CRR 并为其注入相干信息后,CRR 的 controller manager 接管 CRR 的更新。
2.1 同步 container status
CRR 的 status 中蕴含所要重启的 container 的相干状态信息:
type ContainerRecreateRequestStatus struct { // Phase of this ContainerRecreateRequest, e.g. Pending, Recreating, Completed Phase ContainerRecreateRequestPhase `json:"phase"` // Represents time when the ContainerRecreateRequest was completed. It is not guaranteed to // be set in happens-before order across separate operations. // It is represented in RFC3339 form and is in UTC. CompletionTime *metav1.Time `json:"completionTime,omitempty"` // A human readable message indicating details about this ContainerRecreateRequest. Message string `json:"message,omitempty"` // ContainerRecreateStates contains the recreation states of the containers. ContainerRecreateStates []ContainerRecreateRequestContainerRecreateState `json:"containerRecreateStates,omitempty"`}type ContainerRecreateRequestContainerRecreateState struct { // Name of the container. Name string `json:"name"` // Phase indicates the recreation phase of the container. Phase ContainerRecreateRequestPhase `json:"phase"` // A human readable message indicating details about this state. Message string `json:"message,omitempty"`}
CRR controller 不断更新 container 的重启信息到 status 中。
func (r *ReconcileContainerRecreateRequest) syncContainerStatuses(crr *appsv1alpha1.ContainerRecreateRequest, pod *v1.Pod) error { ...}
controller 同步 container status 的逻辑十分重要,在这里笔者已经遇到一个诡异的问题,就是创立了好几个 CRR 后,其中几个 CRR 始终卡在 Recreating
的状态,即便 container 曾经重启实现或者 TTL 到期也不会发生变化,详情能够见这个 issue。起因就是同步 container status 的逻辑跟时钟同步无关:
containerStatus := util.GetContainerStatus(c.Name, pod) if containerStatus == nil { klog.Warningf("Not found %s container in Pod Status for CRR %s/%s", c.Name, crr.Namespace, crr.Name) continue } else if containerStatus.State.Running == nil || containerStatus.State.Running.StartedAt.Before(&crr.CreationTimestamp) { // 只有 container 的创立工夫晚于 crr 的创立工夫,才认为 crr 重启了 container,如果此时 CRR 所处节点或者 Pod 所在节点的时钟产生漂移,那有可能呈现 container 创立的工夫早于 crr 创立工夫,即便该 container 是由 crr 管制重启。 continue } ...
通过排查后发现的确是好多 k8s Node 的 NTP server 呈现问题导致时钟漂移,再加上上述的逻辑,就不难解释为何 CRR 会卡住不动了。
2.2 make pod not ready
CRR 在重启 container 之前会给 Pod 注入一个 v1.PodConditionType
- KruisePodReadyConditionType
并置为 false, 使 Pod 进入 not ready 状态,从 service 的 Endpoint 上摘掉流量。
condition := GetReadinessCondition(newPod) // 获取 KruisePodReadyConditionType condition if condition == nil { // 如果没有设置,就新建一个 _, messages := addMessage("", msg) newPod.Status.Conditions = append(newPod.Status.Conditions, v1.PodCondition{ Type: appspub.KruisePodReadyConditionType, Message: messages.dump(), LastTransitionTime: metav1.Now(), }) } else {// 如果存在该 condition,就置为 false changed, messages := addMessage(condition.Message, msg) if !changed { return nil } condition.Status = v1.ConditionFalse condition.Message = messages.dump() condition.LastTransitionTime = metav1.Now() }
3. kruise daemon controller
CRR kruise daemon controller 会监听 CRR 资源的 create, update, delete 事件,而后在 manage
函数中更新 CRR。
3.1 watch CRR
CRR controller 将 update 和 create 事件都退出到 process 队列中,期待解决。
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { crr, ok := obj.(*appsv1alpha1.ContainerRecreateRequest) if ok { enqueue(queue, crr) } }, UpdateFunc: func(oldObj, newObj interface{}) { crr, ok := newObj.(*appsv1alpha1.ContainerRecreateRequest) if ok { enqueue(queue, crr) } }, DeleteFunc: func(obj interface{}) { crr, ok := obj.(*appsv1alpha1.ContainerRecreateRequest) if ok { resourceVersionExpectation.Delete(crr) } }, })
3.2 CRR phase to recreating
daemon controller 的代码入口处先把 CRR 的 phase 设置为 ContainerRecreateRequestRecreating
// once first update its phase to recreating if crr.Status.Phase != appsv1alpha1.ContainerRecreateRequestRecreating { return c.updateCRRPhase(crr, appsv1alpha1.ContainerRecreateRequestRecreating) }
3.3 wait for unready grace period
CRR 中的 unreadyGracePeriodSeconds
示意在 2.2 步骤中将 Pod 设置为 not ready 后期待多久再执行 restart container。
// crr_daemon_controller.goleftTime := time.Duration(*crr.Spec.Strategy.UnreadyGracePeriodSeconds)*time.Second - time.Since(unreadyTime) if leftTime > 0 { klog.Infof("CRR %s/%s is waiting for unready grace period %v left time.", crr.Namespace, crr.Name, leftTime) c.queue.AddAfter(crr.Namespace+"/"+crr.Spec.PodName, leftTime+100*time.Millisecond) return nil }
3.4 KillContainer
kruise-daemon 会执行 preStop hook 后把容器停掉,而后 kubelet 感知到容器退出,则会新建一个容器并启动。 最初 kruise-daemon 看到新容器曾经启动胜利超过 minStartedSeconds 工夫后,会上报这个容器的 phase 状态为 Succeeded。
// crr_daemon_controller.go err := runtimeManager.KillContainer(pod, kubeContainerStatus.ID, state.Name, msg, nil)
3.5 更新 CRRContainerRecreateStates
不断更新 CRR status 中对于 container 的状态信息 - containerRecreateStates
。
c.patchCRRContainerRecreateStates(crr, newCRRContainerRecreateStates)
4. 实现 CRR
4.1 CRR 置为 completed
这部分逻辑在 controller manager
和 kruise daemon
都有,而且断定 CRR completed 的形式比拟多,这里举几个典型的例子:
4.1.1
当实现重启 container 的数量跟 CRR 中 ContainerRecreateStates 的数组长度统一的时候认为曾经实现所有容器的重启工作,能够标记 CRR 为实现。
if completedCount == len(newCRRContainerRecreateStates) { return c.completeCRRStatus(crr, "") }
4.1.2
当发现有容器重启失败了,并且策略是 ignore
就间接标记本次 CRR 为 completed。
case appsv1alpha1.ContainerRecreateRequestFailed: completedCount++ if crr.Spec.Strategy.FailurePolicy == appsv1alpha1.ContainerRecreateRequestFailurePolicyIgnore { continue } return c.completeCRRStatus(crr, "")
4.1.3
下面两个例子都是在 crr_daemon_controller.go
中的,这里列一个 crr_controller
断定实现的例子:
if crr.Spec.ActiveDeadlineSeconds != nil { leftTime := time.Duration(*crr.Spec.ActiveDeadlineSeconds)*time.Second - time.Since(crr.CreationTimestamp.Time) if leftTime <= 0 { klog.Warningf("Complete CRR %s/%s as failure for recreating has exceeded the activeDeadlineSeconds", crr.Namespace, crr.Name) return reconcile.Result{}, r.completeCRR(crr, "recreating has exceeded the activeDeadlineSeconds") } duration.Update(leftTime) }
CRR 在规定的 TTL 工夫里没有实现工作,会被在这里标记为实现,然而会标记一个含有失败信息的 message。
4.2 到期删除 CRR
如果 CRR 设置了 TTLSecondsAfterFinished
字段,达到该工夫后,零碎就会将 CRR 删除,这对定期清理曾经实现的 CRR 很有帮忙。
if crr.Spec.TTLSecondsAfterFinished != nil { leftTime = time.Duration(*crr.Spec.TTLSecondsAfterFinished)*time.Second - time.Since(crr.Status.CompletionTime.Time) if leftTime <= 0 { klog.Infof("Deleting CRR %s/%s for ttlSecondsAfterFinished", crr.Namespace, crr.Name) if err = r.Delete(context.TODO(), crr); err != nil { return reconcile.Result{}, fmt.Errorf("delete CRR error: %v", err) } return reconcile.Result{}, nil } }
结语
文章的结尾再来回顾一下 CRR 是如何在几个组件合作之下工作的:
传统的 Pod 重启就是将原有的 Pod 删除,期待重建新的 Pod,而 CRR 的呈现为咱们提供了一种全新的重启服务的形式。
本文由mdnice多平台公布