OpenKruise 源碼分析之 ContainerRecreateRequest
OpenKruise 是基于 CRD 的拓展,包含了很多應用工作負載和運維增強能力,本系列文章會從源碼和底層原理上解讀各個組件,以幫助大家更好地使用和理解 OpenKruise。讓我們開始 OpenKruise 的源碼之旅吧!
前言
在上一篇文章[1]中我們解讀了 OpenKruise 原地升級的原理和相關代碼,在此基礎上我們來研究一個基于原地升級能力的組件 - ContainerRecreateRequest[2]。ContainerRecreateRequest(下文簡稱 CRR) 能夠重建 Pod 中一個或多個容器。該功能和 Kruise 提供的原地升級類似,當一個容器重建的時候,Pod 中的其他容器還保持正常運行。重建完成后,Pod 中除了該容器的 restartCount 增加以外不會有什么其他變化。如果掛載了 volume mount 掛載卷,卷中的數(shù)據(jù)不會丟失也不需要重新掛載。這個功能實現(xiàn)了運維容器與業(yè)務容器的管理分離,比如一個 Pod 中會有主容器中運行核心業(yè)務,sidecar 中運行運維容器,比如日志收集等.當業(yè)務容器需要重啟的時候,傳統(tǒng)的更新方式會讓整個 Pod 重啟從而導致運維容器無故被重啟從而中斷服務,而使用 ContainerRecreateRequest 可以實現(xiàn)只讓特定的容器重啟,高效的同時更加安全。
今天就讓我們從源碼的角度來看一下 ContainerRecreateRequest 的實現(xiàn)原理。
源碼解讀
我們先來看一下整個 CRR 的代碼流程概覽,可以看到整個過程主要有三個組件參與,包括 CRR 的 admission webhook, controller manager,以及我們上一篇就提到過的原地升級中的重要組件 - kruise-daemon 中的 crr daemon controller。

然后我們再逐步拆開講解每一步的內容。
1. create CRR
先看一下 CRR 這個自定義資源的 schema 定義:
apiVersion: apps.kruise.io/v1alpha1
kind: ContainerRecreateRequest
metadata:
namespace: pod-namespace
name: xxx
spec:
podName: pod-name
containers: # 要重建的容器名字列表,至少要有 1 個
- name: app
- name: sidecar
strategy:
failurePolicy: Fail # 'Fail' 或 'Ignore',表示一旦有某個容器停止或重建失敗, CRR 立即結束
orderedRecreate: false # 'true' 表示要等前一個容器重建完成了,再開始重建下一個
terminationGracePeriodSeconds: 30 # 等待容器優(yōu)雅退出的時間,不填默認用 Pod 中定義的
unreadyGracePeriodSeconds: 3 # 在重建之前先把 Pod 設為 not ready,并等待這段時間后再開始執(zhí)行重建
minStartedSeconds: 10 # 重建后新容器至少保持運行這段時間,才認為該容器重建成功
activeDeadlineSeconds: 300 # 如果 CRR 執(zhí)行超過這個時間,則直接標記為結束(未結束的容器標記為失敗)
ttlSecondsAfterFinished: 1800 # CRR 結束后,過了這段時間自動被刪除掉
然后開始走讀代碼流程。
1.1 檢查 feature-gate
當我們創(chuàng)建一個 CRR 的時候,會最先經(jīng)過 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
創(chuàng)建 CRR 的時候要為其注入一些特定的標簽,為后面控制啟動容器的流程做準備,比如打上 ContainerRecreateRequestPodNameKey,ContainerRecreateRequestActiveKey的標簽:
obj.Labels[appsv1alpha1.ContainerRecreateRequestPodNameKey] = obj.Spec.PodName
obj.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 是否已經(jīng)完成調度,如果未完成調度的話就無法完成原地重啟(無法使用部署到節(jié)點上的 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 執(zhí)行 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
創(chuàng)建 CRR 并為其注入相關信息后,CRR 的 controller manager 接管 CRR 的更新。
2.1 同步 container status
CRR 的 status 中包含所要重啟的 container 的相關狀態(tài)信息:
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 的邏輯非常重要,在這里筆者曾經(jīng)遇到一個詭異的問題,就是創(chuàng)建了好幾個 CRR 后,其中幾個 CRR 一直卡在 Recreating 的狀態(tài),即使 container 已經(jīng)重啟完成或者 TTL 到期也不會發(fā)生變化,詳情可以見這個 issue[3]。原因就是同步 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 的創(chuàng)建時間晚于 crr 的創(chuàng)建時間,才認為 crr 重啟了 container,假如此時 CRR 所處節(jié)點或者 Pod 所在節(jié)點的時鐘發(fā)生漂移,那有可能出現(xiàn) container 創(chuàng)建的時間早于 crr 創(chuàng)建時間,即使該 container 是由 crr 控制重啟。
continue
}
...
經(jīng)過排查后發(fā)現(xiàn)確實是好多 k8s Node 的 NTP server 出現(xiàn)問題導致時鐘漂移,再加上上述的邏輯,就不難解釋為何 CRR 會卡住不動了。
2.2 make pod not ready
CRR 在重啟 container 之前會給 Pod 注入一個 v1.PodConditionType - KruisePodReadyConditionType 并置為 false, 使 Pod 進入 not ready 狀態(tài),從 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 會監(jiān)聽 CRR 資源的 create, update, delete 事件,然后在 manage 函數(shù)中更新 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 后等待多久再執(zhí)行 restart container。
// crr_daemon_controller.go
leftTime := 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 會執(zhí)行 preStop hook 后把容器停掉,然后 kubelet 感知到容器退出,則會新建一個容器并啟動。最后 kruise-daemon 看到新容器已經(jīng)啟動成功超過 minStartedSeconds 時間后,會上報這個容器的 phase 狀態(tài)為 Succeeded。
// crr_daemon_controller.go
err := runtimeManager.KillContainer(pod, kubeContainerStatus.ID, state.Name, msg, nil)
3.5 更新 CRRContainerRecreateStates
不斷更新 CRR status 中關于 container 的狀態(tài)信息 - containerRecreateStates。
c.patchCRRContainerRecreateStates(crr, newCRRContainerRecreateStates)
4. 完成 CRR
4.1 CRR 置為 completed
這部分邏輯在 controller manager 和 kruise daemon 都有,而且判定 CRR completed 的方式比較多,這里舉幾個典型的例子:
4.1.1
當完成重啟 container 的數(shù)量跟 CRR 中 ContainerRecreateStates 的數(shù)組長度一致的時候認為已經(jīng)完成所有容器的重啟工作,可以標記 CRR 為完成。
if completedCount == len(newCRRContainerRecreateStates) {
return c.completeCRRStatus(crr, "")
}
4.1.2
當發(fā)現(xiàn)有容器重啟失敗了,并且策略是 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 在規(guī)定的 TTL 時間里沒有完成任務,會被在這里標記為完成,但是會標記一個含有失敗信息的 message。
4.2 到期刪除 CRR
如果 CRR 設置了 TTLSecondsAfterFinished 字段,達到該時間后,系統(tǒng)就會將 CRR 刪除,這對定期清理已經(jīng)完成的 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 是如何在幾個組件協(xié)作之下工作的:
傳統(tǒng)的 Pod 重啟就是將原有的 Pod 刪除,等待重建新的 Pod,而 CRR 的出現(xiàn)為我們提供了一種全新的重啟服務的方式。
參考資料
OpenKruise 源碼分析之原地升級: https://cloudsjhan.github.io/2022/06/19/OpenKruise-源碼解讀之原地升級/
[2]kruise CRR: https://openkruise.io/zh/docs/user-manuals/containerrecreaterequest
[3]kruise issue 895: https://github.com/openkruise/kruise/issues/895
