Kubernetes 中 Pod 的優雅退出機制

語言: CN / TW / HK

文章《Kubernetes 中 Pod 的優雅退出機制》最早釋出在:https://blog.hdls.me/16538814058331.html

Kubernetes 提供了一種 Pod 優雅退出機制,使 Pod 在退出前可以完成一些清理工作。但若執行清理工作時出錯了,Pod 能正常退出嗎?多久能退出?退出時間可以指定嗎?系統有預設引數嗎?這其中有若干細節值得我們去注意,本文就從這些細節出發,梳理清楚每種情況下 Kubernetes 的元件的各項行為及其引數設定。

本文基於對 Kubernetes v1.23.1 的原始碼閱讀

Pod 正常退出

Pod 正常退出是指非被驅逐時退出,包括人為刪除、執行出錯被刪除等。在 Pod 退出時,kubelet 刪除容器之前,會先執行 pod 的 preStop,允許 pod 在退出前執行一段指令碼用以清除必要的資源等。然而 preStop 也有執行失敗或者直接 hang 住的情況,這個時候 preStop 並不會阻止 pod 的退出,kubelet 也不會重複執行,而是會等一段時間,超過這個時間會直接刪除容器,保證整個系統的穩定。

整個過程在函式 killContainer 中,我們在 pod 優雅退出時,需要明確的是,kubelet 的等待時間由那幾個因素決定,使用者可以設定的欄位和系統元件的引數是如何共同作用的。

gracePeriod

kubelet 計算 gracePeriod 的過程為:

  1. 如果 pod 的 DeletionGracePeriodSeconds 不為 nil,表示是 ApiServer 刪除的,gracePeriod 直接取值;
  2. 如果 pod 的 Spec.TerminationGracePeriodSeconds 不為 nil,再看 pod 刪除的原因是什麼;
    1. 若刪除原因為執行 startupProbe 失敗,gracePeriod 取值為 startupProbe 中設定的 TerminationGracePeriodSeconds
    2. 若刪除原因為執行 livenessProbe 失敗,gracePeriod 取值為 livenessProbe 中設定的 TerminationGracePeriodSeconds

獲得到 gracePeriod 之後,kubelet 執行 pod 的 preStop,函式 executePreStopHook 中會起一個 goroutine ,並計算其執行的時間,gracePeriod 再減去該時間,就是最終傳給 runtime 的刪除容器的 timeout 時間。所以,若我們設定了 pod preStop,需要同時考慮到 preStop 的執行時間以及容器退出的時間,可以給 TerminationGracePeriodSeconds 設定一個大於 preStop + 容器退出的時間。

```go func (m kubeGenericRuntimeManager) killContainer(pod v1.Pod, containerID kubecontainer.ContainerID, containerName string, message string, reason containerKillReason, gracePeriodOverride int64) error { ... // From this point, pod and container must be non-nil. gracePeriod := int64(minimumGracePeriodInSeconds) switch { case pod.DeletionGracePeriodSeconds != nil: gracePeriod = pod.DeletionGracePeriodSeconds case pod.Spec.TerminationGracePeriodSeconds != nil: gracePeriod = *pod.Spec.TerminationGracePeriodSeconds

    switch reason {
    case reasonStartupProbe:
        if containerSpec.StartupProbe != nil && containerSpec.StartupProbe.TerminationGracePeriodSeconds != nil {
            gracePeriod = *containerSpec.StartupProbe.TerminationGracePeriodSeconds
        }
    case reasonLivenessProbe:
        if containerSpec.LivenessProbe != nil && containerSpec.LivenessProbe.TerminationGracePeriodSeconds != nil {
            gracePeriod = *containerSpec.LivenessProbe.TerminationGracePeriodSeconds
        }
    }
}

// Run internal pre-stop lifecycle hook
if err := m.internalLifecycle.PreStopContainer(containerID.ID); err != nil {
    return err
}

// Run the pre-stop lifecycle hooks if applicable and if there is enough time to run it
if containerSpec.Lifecycle != nil && containerSpec.Lifecycle.PreStop != nil && gracePeriod > 0 {
    gracePeriod = gracePeriod - m.executePreStopHook(pod, containerID, containerSpec, gracePeriod)
}
// always give containers a minimal shutdown window to avoid unnecessary SIGKILLs
if gracePeriod < minimumGracePeriodInSeconds {
    gracePeriod = minimumGracePeriodInSeconds
}
if gracePeriodOverride != nil {
    gracePeriod = *gracePeriodOverride
}

err := m.runtimeService.StopContainer(containerID.ID, gracePeriod)

... return nil } ```

gracePeriodOverride

在上面分析的過程中,kubelet 呼叫 runtime 介面之前,會再判斷一步 gracePeriodOverride,若傳進來的值不為空,直接用該值覆蓋前面的 gracePeriod

kubelet 計算 gracePeriodOverride 的主要過程如下:

  1. 取值 pod 的 DeletionGracePeriodSeconds
  2. 若 kubelet 是在驅逐 pod,則用驅逐的設定 pod 退出時間覆蓋;

go func calculateEffectiveGracePeriod(status *podSyncStatus, pod *v1.Pod, options *KillPodOptions) (int64, bool) { gracePeriod := status.gracePeriod // this value is bedrock truth - the apiserver owns telling us this value calculated by apiserver if override := pod.DeletionGracePeriodSeconds; override != nil { if gracePeriod == 0 || *override < gracePeriod { gracePeriod = *override } } // we allow other parts of the kubelet (namely eviction) to request this pod be terminated faster if options != nil { if override := options.PodTerminationGracePeriodSecondsOverride; override != nil { if gracePeriod == 0 || *override < gracePeriod { gracePeriod = *override } } } // make a best effort to default this value to the pod's desired intent, in the event // the kubelet provided no requested value (graceful termination?) if gracePeriod == 0 && pod.Spec.TerminationGracePeriodSeconds != nil { gracePeriod = *pod.Spec.TerminationGracePeriodSeconds } // no matter what, we always supply a grace period of 1 if gracePeriod < 1 { gracePeriod = 1 } return gracePeriod, status.gracePeriod != 0 && status.gracePeriod != gracePeriod }

ApiServer 的行為

在上面分析 kubelet 處理 pod 的退出時間時,我們會發現 kubelet 會首先用 pod 的 DeletionGracePeriodSeconds,而該值正是 ApiServer 刪除 pod 時寫入的。本節我們來分析 ApiServer 刪除 pod 時的行為。

ApiServer 中計算 pod 的 GracePeriodSeconds 過程為:

  1. options.GracePeriodSeconds 不為空,則設定為該值;否則設定為 spec 中使用者指定的 Spec.TerminationGracePeriodSeconds(預設為 30s);
  2. 若 pod 未被排程或已經退出,則設定為 0,即立即刪除;

其中,options.GracePeriodSeconds 為 kubectl 刪除 pod 時,可以指定的引數 --grace-period;或者程式裡呼叫 ApiServer 介面時指定的引數,如 client-go 中的 DeleteOptions.GracePeriodSeconds

```go func (podStrategy) CheckGracefulDelete(ctx context.Context, obj runtime.Object, options metav1.DeleteOptions) bool { if options == nil { return false } pod := obj.(api.Pod) period := int64(0) // user has specified a value if options.GracePeriodSeconds != nil { period = options.GracePeriodSeconds } else { // use the default value if set, or deletes the pod immediately (0) if pod.Spec.TerminationGracePeriodSeconds != nil { period = pod.Spec.TerminationGracePeriodSeconds } } // if the pod is not scheduled, delete immediately if len(pod.Spec.NodeName) == 0 { period = 0 } // if the pod is already terminated, delete immediately if pod.Status.Phase == api.PodFailed || pod.Status.Phase == api.PodSucceeded { period = 0 }

if period < 0 {
    period = 1
}

// ensure the options and the pod are in sync
options.GracePeriodSeconds = &period
return true

} ```

kubelet 驅逐 pod

另外,在 kubelet 驅逐 pod 時,pod 的優雅退出時間是被覆蓋的。

go func (m *managerImpl) synchronize(diskInfoProvider DiskInfoProvider, podFunc ActivePodsFunc) []*v1.Pod { ... // we kill at most a single pod during each eviction interval for i := range activePods { pod := activePods[i] gracePeriodOverride := int64(0) if !isHardEvictionThreshold(thresholdToReclaim) { gracePeriodOverride = m.config.MaxPodGracePeriodSeconds } message, annotations := evictionMessage(resourceToReclaim, pod, statsFunc) if m.evictPod(pod, gracePeriodOverride, message, annotations) { metrics.Evictions.WithLabelValues(string(thresholdToReclaim.Signal)).Inc() return []*v1.Pod{pod} } } ... }

其 override 值為 EvictionMaxPodGracePeriod,且只有軟碟機逐時有效,該值為 kubelet 的驅逐相關的配置引數:

go // Map of signal names to quantities that defines hard eviction thresholds. For example: {"memory.available": "300Mi"}. EvictionHard map[string]string // Map of signal names to quantities that defines soft eviction thresholds. For example: {"memory.available": "300Mi"}. EvictionSoft map[string]string // Map of signal names to quantities that defines grace periods for each soft eviction signal. For example: {"memory.available": "30s"}. EvictionSoftGracePeriod map[string]string // Duration for which the kubelet has to wait before transitioning out of an eviction pressure condition. EvictionPressureTransitionPeriod metav1.Duration // Maximum allowed grace period (in seconds) to use when terminating pods in response to a soft eviction threshold being met. EvictionMaxPodGracePeriod int32

kubelet 驅逐 pod 的函式是啟動時注入的,函式如下:

```go func killPodNow(podWorkers PodWorkers, recorder record.EventRecorder) eviction.KillPodFunc { return func(pod v1.Pod, isEvicted bool, gracePeriodOverride int64, statusFn func(v1.PodStatus)) error { // determine the grace period to use when killing the pod gracePeriod := int64(0) if gracePeriodOverride != nil { gracePeriod = gracePeriodOverride } else if pod.Spec.TerminationGracePeriodSeconds != nil { gracePeriod = *pod.Spec.TerminationGracePeriodSeconds }

    // we timeout and return an error if we don't get a callback within a reasonable time.
    // the default timeout is relative to the grace period (we settle on 10s to wait for kubelet->runtime traffic to complete in sigkill)
    timeout := int64(gracePeriod + (gracePeriod / 2))
    minTimeout := int64(10)
    if timeout < minTimeout {
        timeout = minTimeout
    }
    timeoutDuration := time.Duration(timeout) * time.Second

    // open a channel we block against until we get a result
    ch := make(chan struct{}, 1)
    podWorkers.UpdatePod(UpdatePodOptions{
        Pod:        pod,
        UpdateType: kubetypes.SyncPodKill,
        KillPodOptions: &KillPodOptions{
            CompletedCh:                              ch,
            Evict:                                    isEvicted,
            PodStatusFunc:                            statusFn,
            PodTerminationGracePeriodSecondsOverride: gracePeriodOverride,
        },
    })

    // wait for either a response, or a timeout
    select {
    case <-ch:
        return nil
    case <-time.After(timeoutDuration):
        recorder.Eventf(pod, v1.EventTypeWarning, events.ExceededGracePeriod, "Container runtime did not kill the pod within specified grace period.")
        return fmt.Errorf("timeout waiting to kill pod")
    }
}

} ```

killPodNow 函式是 kubelet 在驅逐 pod 時所呼叫的函式,gracePeriodOverride 為軟碟機逐時設定的引數,當其沒有設定時,gracePeriod 依然取值 pod.Spec.TerminationGracePeriodSeconds。然後該函式會呼叫 podWorkers.UpdatePod,傳入相應引數,並且設定一個跟 gracePeriod 相關的超時時間,等待其返回。

總結

Pod 的優雅退出是由 preStop 實現的,本文就 Pod 正常退出和被驅逐時,Pod 的退出時間受哪些因素影響,各引數之間是如何相互作用的做了簡要的分析。瞭解了這些細節後,我們對 Pod 的退出流程就有了一個更加全面的認知。