詳解Kubernetes Pod優雅退出

人艰不拆_zmc發表於2024-06-20

1、概述

  Pod優雅關閉是指在Kubernetes中,當Pod因為某種原因(如版本更新、資源不足、故障等)需要被終止時,Kubernetes不會立即強制關閉Pod,而是首先嚐試以一種“優雅”的方式關閉Pod。這個過程允許Pod中的容器有足夠的時間來響應終止訊號(預設為SIGTERM),並在終止前完成必要的清理工作,如儲存資料、關閉連線等。

注意 :在《Docker容器優雅退出》這篇博文中,我們詳細講解了Docker優雅退出機制,在本文我們將詳細詳解Kubernetes Pod優雅退出機制。

1.1 Pod優雅退出流程

  具體來說,Pod優雅關閉的流程如下:

  (1)PreStop Hook:

    • 在Pod的定義中,可以配置一個PreStop Hook。這是一個在容器接收到SIGTERM訊號之前執行的命令或HTTP請求。
    • PreStop Hook允許容器在接收到SIGTERM訊號前,有一段緩衝時間來執行清理工作,如關閉資料庫連線、儲存檔案、通知其他系統等。

  (2)SIGTERM訊號:

    • 在PreStop Hook執行完畢後或未定義PreStop Hook的情況下,kubelet 會遍歷 Pod 中 container, 然後呼叫 cri 介面中 StopContainer 方法對 Pod 中的所有 container 進行優雅關停,向 dockerd 傳送 stop -t 指令,用 SIGTERM 訊號以通知容器內應用程序開始優雅停止。
    • 等待容器內應用程序完全停止,如果容器在 gracePeriod 執行時間內還未完全停止,就傳送 SIGKILL 訊號強制殺死應用程序(容器執行時處理)。

  (3)SIGKILL訊號與資源清理:如果容器在寬限期後仍在執行,容器執行時會傳送 SIGKILL 訊號強制終止容器,並隨後清理 Pod 的資源(容器執行時處理)。

注意:Kubelet 呼叫 cri 介面中 StopContainer 方法時,向 dockerd 傳送 stop -t 指令時會帶著優雅關停容器的寬限時間 gracePeriod,gracePeriod 取值分多個情況,預設是 terminationGracePeriodSeconds[30秒] - 容器執行 preStop 時間,具體詳情見下文原始碼分析部分。

1.2 為什麼要進行Pod優雅關閉

  進行Pod優雅關閉的重要性主要體現在以下幾個方面:

  1. 避免服務中斷:透過優雅關閉,Pod可以在終止前完成當前正在處理的請求,確保服務不會因為Pod的突然終止而中斷。

  2. 確保資料一致性:優雅關閉允許Pod在終止前透過PreStop Hook完成必要的資料持久化或事務處理,從而確保資料的一致性。

  3. 最小化使用者體驗影響:透過優雅關閉,可以避免將流量路由到已經被刪除的Pod,減少使用者請求處理失敗的可能性。在滾動更新或擴充套件Pod時,優雅關閉能夠確保服務的平滑過渡,對使用者來說幾乎是無感知的。

  4. 合理利用資源:優雅關閉允許Pod在終止前釋放佔用的資源,避免資源浪費和洩露,提高資源的利用率。

  總的來說,Pod優雅關閉是Kubernetes中一個重要的功能,它結合了PreStop Hook和寬限期等機制,確保Pod在終止前能夠優雅地完成必要的清理工作,從而保持服務的穩定性和可用性、確保資料一致性、提升使用者體驗和合理利用資源。在進行Pod管理時,應該充分了解和利用Pod優雅關閉的功能。

2、Kubernetes Pod刪除原理

  Kubernetes (k8s) 中的 Pod 可能因多種原因被刪除。以下是一些常見原因:

  1. 手動刪除:使用者使用 kubectl delete pod 命令手動刪除 Pod。

  2. 控制器策略:Deployment、ReplicaSet 或 DaemonSet 等控制器根據其策略調整副本數,例如縮減副本數時會刪除多餘的 Pod;Job 和 CronJob 完成後刪除其建立的 Pod。

  3. 節點故障:如果節點失效,節點上的 Pod 會被 Kubernetes 控制平面標記為失效並在其他節點上重新排程。

  4. 資源限制:當節點資源不足時,Kubernetes 可能會根據優先順序和資源限制(如資源配額和排程策略)來刪除一些 Pod。

  5. 健康檢查失敗:Pod 的 liveness 或 readiness 探針連續失敗,Kubernetes 會認為 Pod 不健康並刪除或重啟它。

  6. 優先順序搶佔:如果有更高優先順序的 Pod 需要資源,Kubernetes 可能會刪除較低優先順序的 Pod 以釋放資源。

  7. 排程器策略:Kubernetes 排程器可能會根據排程策略(如 NodeAffinity、PodAffinity 等)重新分配 Pod,從而刪除舊的 Pod。

  8. 更新策略:Deployment 或 StatefulSet 進行滾動更新時,舊的 Pod 會被刪除並替換為新的 Pod。

  9. 節點自動縮放:當使用叢集自動縮放器時,如果叢集縮小(移除節點),部分 Pod 會被刪除。

  但是不管是何種原因刪除Pod(使用者手動刪除或控制器自動刪除),在Pod的刪除過程中,都會同時會存在兩條並行的時間線,如下圖所示:

  1. 一條時間線是網路規則的更新過程。
  2. 另一條時間線是 Pod 的刪除過程。

  由上面流程圖可知,在 Pod 刪除過程中,存在兩條並行的時間線,這兩條時間線誰先執行完畢是不確定的。如果 Pod 內的容器已經刪除,但網路層面的 Endpoint 資源仍包含該 Pod 的 IP,客戶端請求可能會被路由到已刪除的 Pod,導致請求處理失敗;或者請求未處理完時,Pod 內的容器已經被刪除,這樣也會導致請求處理失敗。以下是一個工作負載滾動升級的示例,說明如果不為 Pod 配置合理的優雅退出機制,會出現什麼問題。

工作負載滾動升級問題示例

  1. 請求路由錯誤:舊 Pod 刪除但仍在 Endpoint 資源中,導致請求被路由到已刪除的 Pod,返回以下錯誤:

    • 502 Bad Gateway:負載均衡器或反向代理無法正確路由請求。
  2. 資料丟失或不一致:舊 Pod 未將正在處理的請求處理完成的情況下被刪除,如果該請求不是冪等性的,則可能導致以下錯誤:

    • 500 Internal Server Error:伺服器內部錯誤,無法完成請求。
    • 404 Not Found:如果資料未正確儲存或更新,可能找不到預期的資源。

注意 1:本文假設刪除Pod都有關聯的svc資源,客戶端都是透過svc訪問Pod。

注意 2:HTTP 404錯誤通常表示伺服器無法找到請求的資源。這可能是因為資源已被刪除、移動或從未存在過。在資料丟失或不一致的場景中,404錯誤可能是一個間接的結果。例如,如果Pod在刪除之前正在處理一個應該建立新資源的請求(如資料庫記錄或檔案),但由於Pod的刪除,該資源可能沒有被正確建立。稍後的請求試圖訪問該資源時,可能會收到404錯誤,因為資源不存在。

2.1 原理分析

一切都從 TerminationGracePeriodSeconds 開始說起,我們回顧下 k8s 關閉 Pod 的流程過程。

網路層面:

  1. Pod 被刪除,狀態置為 Terminating。
  2. Endpoint Controller 將該 Pod 的 ip 從 Endpoint 物件中刪除。
  3. Kube-proxy 根據 Endpoint 物件的改變更新 iptables/ipvs 規則,不再將流量路由到被刪除的 Pod。
  4. 如果還有其他 Gateway 依賴 Endpoint 資源變化的,也會改變自己的配置(比如 Nginx Ingress Controller)。

注意: 預設 Ingres nginx.ingress.kubernetes.io/service-upstream 註解值為false,Nginx Ingress Controller 代理服務時,藉助Endpoint代理代理上游服務到 PodIp。

Pod 層面:

  1. Pod 被刪除,狀態置為 Terminating。
  2. Kubelet 捕獲到 ApiServer 中 Pod 狀態變化,執行 syncPod 動作。
  3. 如果 Pod 配置了 preStop Hook ,將會執行。
  4. kubelet 對 Pod 中各個 container 傳送呼叫 cri 介面中 StopContainer 方法,向 dockerd 傳送 stop -t 指令,用 SIGTERM 訊號以通知容器內應用程序開始優雅停止。
  5. 等待容器內應用程序完全停止,如果容器在 gracePeriod 執行時間內還未完全停止,就傳送 SIGKILL 訊號強制殺死應用程序(容器執行時處理)。
  6. 所有容器程序終止,清理 Pod 資源。

注意: 預設 Ingres nginx.ingress.kubernetes.io/service-upstream 註解值為false,Nginx Ingress Controller 代理服務時,藉助Endpoint代理代理上游服務到 PodIp。

我們重點關注下幾個訊號:K8S_EVENT, SIGTERM, SIGKILL

  • K8S_EVENT: SyncPodKill,kubelet 監聽到了 apiServer 關閉 Pod 事件,經過一些處理動作後,向內部發出了一個 syncPod 動作,完成當前真實 Pod 狀態的改變。
  • SIGTERM: 用於終止程式,也稱為軟終止,因為接收 SIGTERM 訊號的程序可以選擇忽略它(容器執行時處理)。
  • SIGKILL: 用於立即終止,也稱為硬終止,這個訊號不能被忽略或阻止。這是殺死程序的野蠻方式,只能作為最後的手段(容器執行時處理)。

注意:訊號詳解可以參加《Docker容器優雅退出》這篇博文。

瞭解訊號的解釋以後,再透過程式碼講解下 Kubelet 關閉 Pod 流程(包含 preStop 和 GracefulStop):

Kubernetes 原始碼(1.21.5版本):

pkg/kubelet/types/pod_update.go:

// SyncPodType classifies pod updates, eg: create, update.
type SyncPodType int

const (
	// SyncPodSync is when the pod is synced to ensure desired state
	SyncPodSync SyncPodType = iota
	// SyncPodUpdate is when the pod is updated from source
	SyncPodUpdate
	// SyncPodCreate is when the pod is created from source
	SyncPodCreate
	// SyncPodKill is when the pod is killed based on a trigger internal to the kubelet for eviction.
	// If a SyncPodKill request is made to pod workers, the request is never dropped, and will always be processed.
	SyncPodKill
)

pkg/kubelet/kubelet.go:

  如果是刪除Pod事件(SyncPodKill)將執行刪除Pod邏輯(killPod)。注意看Kubelet呼叫刪除Pod邏輯方法傳了一個引數PodTerminationGracePeriodSecondsOverride,它是 Kubelet 的一個配置引數,用於覆蓋所有 Pod 的終止寬限時間(grace period)。具體來說,這個引數會設定一個全域性的寬限時間值,該值會覆蓋所有 Pod 自定義的 terminationGracePeriodSeconds 值。

  • 預設情況下,PodTerminationGracePeriodSecondsOverride 是未設定的(即值為 nil 或未定義)。在這種情況下,Kubelet 會使用每個 Pod 自己定義的 terminationGracePeriodSeconds 值,預設值為 30 秒。
  • 如果設定了這個引數,Kubelet 會使用此值作為所有 Pod 的終止寬限時間,而不再使用各個 Pod 自定義的 terminationGracePeriodSeconds。這意味著所有 Pod 都會在這個指定的時間內嘗試完成終止操作,在時間結束後,Kubelet 會強制終止 Pod。
func (kl *Kubelet) syncPod(o syncPodOptions) error {
	......

	// if we want to kill a pod, do it now!
	if updateType == kubetypes.SyncPodKill {
		killPodOptions := o.killPodOptions
		if killPodOptions == nil || killPodOptions.PodStatusFunc == nil {
			return fmt.Errorf("kill pod options are required if update type is kill")
		}
		apiPodStatus := killPodOptions.PodStatusFunc(pod, podStatus)
		// 修改 Pod 的狀態
		kl.statusManager.SetPodStatus(pod, apiPodStatus)
		// 這裡事件型別是關閉 Pod,這裡開始執行 Pod 的關閉過程,至此 SyncPodKill 訊號的作用結束
		if err := kl.killPod(pod, nil, podStatus, killPodOptions.PodTerminationGracePeriodSecondsOverride); err != nil {
			kl.recorder.Eventf(pod, v1.EventTypeWarning, events.FailedToKillPod, "error killing pod: %v", err)
			// there was an error killing the pod, so we return that error directly
			utilruntime.HandleError(err)
			return err
		}
		return nil
	}
         ........
}

pkg/kubelet/kubelet_pods.go:

// One of the following arguments must be non-nil: runningPod, status.
func (kl *Kubelet) killPod(pod *v1.Pod, runningPod *kubecontainer.Pod, status *kubecontainer.PodStatus, gracePeriodOverride *int64) error {
	......
	// Call the container runtime KillPod method which stops all running containers of the pod
	if err := kl.containerRuntime.KillPod(pod, p, gracePeriodOverride); err != nil {
		return err
	}
	.......
}

pkg/kubelet/kuberuntime/kuberuntime_manager.go:

func (m *kubeGenericRuntimeManager) KillPod(pod *v1.Pod, runningPod kubecontainer.Pod, gracePeriodOverride *int64) error {
	err := m.killPodWithSyncResult(pod, runningPod, gracePeriodOverride)
	return err.Error()
}

func (m *kubeGenericRuntimeManager) killPodWithSyncResult(pod *v1.Pod, runningPod kubecontainer.Pod, gracePeriodOverride *int64) (result kubecontainer.PodSyncResult) {
	killContainerResults := m.killContainersWithSyncResult(pod, runningPod, gracePeriodOverride)
	for _, containerResult := range killContainerResults {
		result.AddSyncResult(containerResult)
	}

	......
}

pkg/kubelet/kuberuntime/kuberuntime_container.go:

使用協程清理Pod裡面所有的容器。

// killContainersWithSyncResult kills all pod's containers with sync results.
func (m *kubeGenericRuntimeManager) killContainersWithSyncResult(pod *v1.Pod, runningPod kubecontainer.Pod, gracePeriodOverride *int64) (syncResults []*kubecontainer.SyncResult) {
	containerResults := make(chan *kubecontainer.SyncResult, len(runningPod.Containers))
	wg := sync.WaitGroup{}

	wg.Add(len(runningPod.Containers))
	for _, container := range runningPod.Containers {
		go func(container *kubecontainer.Container) {
			......
			if err := m.killContainer(pod, container.ID, container.Name, "", reasonUnknown, gracePeriodOverride); 
                        ......
	}
	wg.Wait()
	close(containerResults)

	for containerResult := range containerResults {
		syncResults = append(syncResults, containerResult)
	}
	return
}            

pkg/kubelet/kuberuntime/kuberuntime_container.go:

Kubelet 進行 Pod 中容器的關停,這個方法比較關鍵,這裡重點講解下:

(1)計算容器優雅關閉寬限時間

  1. 預設設定容器最小優雅關停寬限時間等於2秒。
  2. 如果 podDeletionGracePeriodSeconds 不是 nil,即 Pod 是被 Apiserver 刪除的,那麼 gracePeriod 直接取值,優先使用呼叫 Apiserver 刪除Pod時指定的值作為優雅關閉Pod寬限時間,比如kubectl delete pod my-pod --grace-period=60。
  3. 如果 pod Spec.TerminationGracePeriodSeconds 不是 nil,gracePeriod 取值分為以下三種情況:
  • 使用Pod規格配置檔案中的定義的terminationGracePeriodSeconds的值,gracePeriod 預設值30秒;
  • 如果刪除的原因是執行失敗 startupProbe,gracePeriod 取啟動探針TerminationGracePeriodSeconds值(啟用探針寬限時間特性);
  • 如果刪除的原因是執行失敗 livenessProbe,gracePeriod 取存活探針TerminationGracePeriodSeconds值(啟用探針寬限時間特性);

(2)如果容器配置了 lifecycle preStop ,執行 container 中 lifecycle preStop 設定的動作或命令,並計算容器執行 lifecycle preStop 的時間。

(3)容器寬限時間 gracePeriod = gracePeriod - 容器執行 lifecycle preStop 的時間。

(4)如果容器執行完 lifecycle preStop 後的寬限時間 < minimumGracePeriodInSeconds(2秒)的話,gracePeriod = minimumGracePeriodInSeconds。

(5)如果kubelet全域性配置不為空,所有容器退出寬限時間使用kubelet PodTerminationGracePeriodSecondsOverride配置引數值。

(6)呼叫 CRI 介面,呼叫容器雲執行時 /container/{containerID}/stop 介面用於關停容器,容器優雅停止的 gracePeriod 值,為上面計算的 gracePeriod。

// killContainer kills a container through the following steps:
// * Run the pre-stop lifecycle hooks (if applicable).
// * Stop the container.
func (m *kubeGenericRuntimeManager) killContainer(pod *v1.Pod, containerID kubecontainer.ContainerID, containerName string, message string, reason containerKillReason, gracePeriodOverride *int64) error {
	var containerSpec *v1.Container
	if pod != nil {
		if containerSpec = kubecontainer.GetContainerSpec(pod, containerName); containerSpec == nil {
			return fmt.Errorf("failed to get containerSpec %q (id=%q) in pod %q when killing container for reason %q",
				containerName, containerID.String(), format.Pod(pod), message)
		}
	} else {
		// Restore necessary information if one of the specs is nil.
		restoredPod, restoredContainer, err := m.restoreSpecsFromContainerLabels(containerID)
		if err != nil {
			return err
		}
		pod, containerSpec = restoredPod, restoredContainer
	}

	// 最小優雅關閉Pod週期是2秒
	gracePeriod := int64(minimumGracePeriodInSeconds)
	switch {
	case pod.DeletionGracePeriodSeconds != nil:
		// 優先使用刪除Pod時指定的值作為優雅關閉Pod寬限時間,比如kubectl delete pod my-pod --grace-period=60
		gracePeriod = *pod.DeletionGracePeriodSeconds
	case pod.Spec.TerminationGracePeriodSeconds != nil:
		// 使用Pod規格配置檔案中的定義的terminationGracePeriodSeconds的值,預設30秒
		gracePeriod = *pod.Spec.TerminationGracePeriodSeconds

		// 如果啟用探針寬限時間特性的話,寬限時間使用探針寬限時間
		if utilfeature.DefaultFeatureGate.Enabled(features.ProbeTerminationGracePeriod) {
			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
				}
			}
		}
	}

	if len(message) == 0 {
		message = fmt.Sprintf("Stopping container %s", containerSpec.Name)
	}
	m.recordContainerEvent(pod, containerSpec, containerID.ID, v1.EventTypeNormal, events.KillingContainer, message)

	// 	空殼函式,沒有實際作用,估計是為了以後的擴充套件用的
	// Run internal pre-stop lifecycle hook
	if err := m.internalLifecycle.PreStopContainer(containerID.ID); err != nil {
		return err
	}

	// 這裡真正執行 container 中 lifecycle preStop 設定的動作或命令
	// 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)
	}
	// 寬限時間不夠的話再多給2秒
	// always give containers a minimal shutdown window to avoid unnecessary SIGKILLs
	if gracePeriod < minimumGracePeriodInSeconds {
		gracePeriod = minimumGracePeriodInSeconds
	}
	// 如果kubelet全域性配置不為空,所有容器退出寬限時間使用kubelet PodTerminationGracePeriodSecondsOverride配置引數值
	if gracePeriodOverride != nil {
		gracePeriod = *gracePeriodOverride
		klog.V(3).InfoS("Killing container with a grace period override", "pod", klog.KObj(pod), "podUID", pod.UID,
			"containerName", containerName, "containerID", containerID.String(), "gracePeriod", gracePeriod)
	}

	klog.V(2).InfoS("Killing container with a grace period override", "pod", klog.KObj(pod), "podUID", pod.UID,
		"containerName", containerName, "containerID", containerID.String(), "gracePeriod", gracePeriod)

	// 呼叫 CRI 介面,呼叫容器雲執行時 /container/{containerID}/stop 介面用於關停容器,容器優雅停止的 gracePeriod 值,為上面計算的 gracePeriod
	err := m.runtimeService.StopContainer(containerID.ID, gracePeriod)
	if err != nil {
		klog.ErrorS(err, "Container termination failed with gracePeriod", "pod", klog.KObj(pod), "podUID", pod.UID,
			"containerName", containerName, "containerID", containerID.String(), "gracePeriod", gracePeriod)
	} else {
		klog.V(3).InfoS("Container exited normally", "pod", klog.KObj(pod), "podUID", pod.UID,
			"containerName", containerName, "containerID", containerID.String())
	}

	return err
}

// 計算容器執行preStopHook時間
// executePreStopHook runs the pre-stop lifecycle hooks if applicable and returns the duration it takes.
func (m *kubeGenericRuntimeManager) executePreStopHook(pod *v1.Pod, containerID kubecontainer.ContainerID, containerSpec *v1.Container, gracePeriod int64) int64 {
	klog.V(3).InfoS("Running preStop hook", "pod", klog.KObj(pod), "podUID", pod.UID, "containerName", containerSpec.Name, "containerID", containerID.String())

	start := metav1.Now()
	done := make(chan struct{})
	go func() {
		defer close(done)
		defer utilruntime.HandleCrash()
		if msg, err := m.runner.Run(containerID, pod, containerSpec, containerSpec.Lifecycle.PreStop); err != nil {
			klog.ErrorS(err, "PreStop hook failed", "pod", klog.KObj(pod), "podUID", pod.UID,
				"containerName", containerSpec.Name, "containerID", containerID.String())
			m.recordContainerEvent(pod, containerSpec, containerID.ID, v1.EventTypeWarning, events.FailedPreStopHook, msg)
		}
	}()

	select {
	case <-time.After(time.Duration(gracePeriod) * time.Second):
		klog.V(2).InfoS("PreStop hook not completed in grace period", "pod", klog.KObj(pod), "podUID", pod.UID,
			"containerName", containerSpec.Name, "containerID", containerID.String(), "gracePeriod", gracePeriod)
	case <-done:
		klog.V(3).InfoS("PreStop hook completed", "pod", klog.KObj(pod), "podUID", pod.UID,
			"containerName", containerSpec.Name, "containerID", containerID.String())
	}

	return int64(metav1.Now().Sub(start.Time).Seconds())
}

注意:這裡只貼上和Pod優雅退出相關程式碼,其他程式碼直接忽視了。

容器執行時Docker原始碼:

moby/daemon/stop.go:

// containerStop sends a stop signal, waits, sends a kill signal.
func (daemon *Daemon) containerStop(ctx context.Context, ctr *container.Container, options containertypes.StopOptions) (retErr error) {
	...

	var (
		// 獲得配置的 StopSignal 值,一般我們不會做配置,所以這裡預設就是 SIGTERM
		stopSignal  = ctr.StopSignal()
		...
	)

	...

	// 1. 傳送關閉訊號 SIGTERM
	err := daemon.killPossiblyDeadProcess(ctr, stopSignal)
	if err != nil {
		wait = 2 * time.Second
	}

	...

	// 2. 啟動一個超時等待器,等待容器關停優雅寬限時間結束(kubelet呼叫傳過來的gracePeriod,一般是terminationGracePeriodSeconds[30秒] - 容器執行 preStop 時間)
	if status := <-ctr.Wait(subCtx, container.WaitConditionNotRunning); status.Err() == nil {
		// container did exit, so ignore any previous errors and return
		return nil
	}

	...

	// 3. 如果在容器優雅退出時間內(如果是kubelet呼叫CRI介面的話,容器優雅退出時間預設情況下等於terminationGracePeriodSeconds - preStop 執行時間)還未完全停止,就傳送 SIGKILL 訊號強制殺死應用程序
	if err := daemon.Kill(ctr); err != nil {
		// got a kill error, but give container 2 more seconds to exit just in case
		subCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
		defer cancel()
		status := <-ctr.Wait(subCtx, container.WaitConditionNotRunning)
		if status.Err() != nil {
			logrus.WithError(err).WithField("container", ctr.ID).Errorf("error killing container: %v", status.Err())
			return err
		}
		// container did exit, so ignore previous errors and continue
	}

	return nil
}

  透過上面的程式碼,驗證了之前架構圖中流程。我們這邊可以簡單的終結下一些內容:

  1. kubelet 作為觀察者監控著 ApiServer 中Pod的變化,呼叫 syncPod 方法去完成當前 node 內的 Pod 狀態更新。(刪除 Pod 也算是一種 Pod 的狀態更新)
  2. kubelet 不對 Pod 內的 container 應用程式傳送任何訊號,這個是由 CRI 介面實現體來操作,但是容器的優雅關停的寬限時間是kubelet計算傳輸的

2.2 隱形的時間軸

原則公式:T1 = T2 + T3

  • TerminationGracePeriodSeconds(T1): 總體容器關閉容忍時間。這個值並不是一個固定參考值,每一個應用對著值的要求也不一樣,所以這個值有明確的業務屬性。這個值來源優先順序如下:
    • 如果kubelet全域性配置不為空,所有容器退出寬限時間使用 kubelet PodTerminationGracePeriodSecondsOverride 配置引數值;
    • 如果 podDeletionGracePeriodSeconds 不是 nil,即 Pod 是被 Apiserver 刪除的,那麼容器關閉容忍時間等於 podDeletionGracePeriodSeconds 值;
    • 如果 K8s 叢集開啟了探針寬限時間特性的話,那麼容器關閉容忍時間優先等於啟動探針TerminationGracePeriodSeconds值,其次等於存活探針 TerminationGracePeriodSeconds值;
    • 使用Pod規格配置檔案中的定義的terminationGracePeriodSeconds的值,gracePeriod 預設值30秒。
  • Lifecycle PreStop Hook 執行時間(T2): 等待應用程序關閉前需要執行動作的執行時間,這個主要是影響 “新建請求” 到業務 Pod,因為在執行 preStop 的時候 k8s 網路層的變更也在執行。
  • Container Graceful Stop 執行時間(T3): 等待應用自主關閉已有請求的連線,同時結束到資料庫之類後端資料寫入工作,保證資料都落庫或者落盤。這個值來源優先順序如下:
    • 如果kubelet全域性配置不為空,所有容器退出寬限時間使用 kubelet PodTerminationGracePeriodSecondsOverride 配置引數值(T2時間不算入到容器優雅關停時間裡面);
    • T1 - T2(T3 = T1 - T2 );
    • 如果 T1 - T2時間 < minimumGracePeriodInSeconds(2秒)的話,T3 = minimumGracePeriodInSeconds。
  • Kubernetes 網路層變更時間(T4)

複雜的邏輯:

  • T4 <= T2,正常,響應碼200;
  • T2 < T4 <= T1,如果服務程式碼裡面正確編寫了優雅關停邏輯的話,那麼正常,響應碼200;如果未優雅關停,可能存在未將正在處理的請求處理完成的情況下被刪除,響應碼可能是404、500;
  • T1 < T4,Bad Gateway,Pod 已經刪除了,但是網路層還沒有完成變更,導致流量還在往不存在的Pod轉發,響應碼 502。

2.3 處理方法

心思新密的小夥伴可能逐漸發現,要解決問題,實際就是做一個巧妙的動作調整時間差,滿足業務 pod 能夠真正的正確的關閉。

知道了原因,知道了邏輯,那順理成章的就有了解決方案:

  1. 容器應用程序中要有優雅退出程式碼,能夠執行優雅退出;
  2. 增加 preStopHook,能夠執行一定時間的 sleep;
  3. 修改 TerminationGracePeriodSeconds,每一個業務根據實際需要修改;

3、示例(透過Lifecycle PreStop Hook來優雅的停掉服務)

有時候我們也想在服務停止前,透過執行一條命令或者傳送一個HTTP請求來優雅的停掉服務。

舉例:

  • 比如對於Spring boot應用,Spring Boot Actuator提供了服務優雅停止辦法,當要停止服務時,可以向服務傳送一個post方法的shutdown HTTP請求。
  • 比如對於Nginx服務,當要停止服務時,可以執行命令kill -QUIT Nginx主程序號來停止服務。

下面我們將以Nginx服務來講解如何使用一條命令來停止服務:

先回顧一下有關Nginx的基礎知識:

Nginx是一個多程序服務,master程序和一堆worker程序,master程序只負責校驗配置檔案語法,建立worker程序,真正的執行、接收客戶請求、處理配置檔案中指令都是由worker程序來完成的。master程序與worker程序之間主要是透過Linux Signal來實現互動。Nginx提供了大量的命令和處理訊號來實現對配置檔案的語法檢查,服務優雅停止,程序平滑重啟、升級等功能,我們這裡僅簡單介紹與nginx優雅停止相關命令觸發的Linux Signal執行過程和執行原理。

nginx 的停止方法有很多,一般透過傳送系統訊號給 nginx 的master程序的方式來停止 nginx。

3.1 優雅停止 nginx

[root@localhost ~]# nginx -s quit
[root@localhost ~]# kill -QUIT 【Nginx主程序號】
[root@localhost ~]# kill -QUIT /usr/local/nginx/logs/nginx.pid

master程序接到SIGQUIT訊號時,將此訊號轉發給所有工作程序。工作程序隨後關閉監聽埠以便不再接收新的連線請求,並閉空閒連線,等待活躍連線全部正常結速後,呼叫 ngx_worker_process_exit 退出。而 master 程序在所有工作程序都退出後,呼叫 ngx_master_process_exit 函式退出。

注意:以上三個命令中,任選一個執行就可以達到停止 Nginx 服務的目的。推薦使用 nginx -s quit 或 kill -QUIT /usr/local/nginx/logs/nginx.pid,因為它們不需要你手動查詢 Nginx 主程序號。

3.2 快速停止 nginx

[root@localhost ~]# nginx -s stop
[root@localhost ~]# kill -TERM 【Nginx主程序號】
[root@localhost ~]# kill -INT 【Nginx主程序號】

TERM訊號在Linux系統可以稱為優雅的退出訊號,INT訊號是系統SIGINT訊號,Nginx對這兩個訊號的處理方式有所不同。Nginx用SIGQUIT(3)訊號來優雅停止服務。

master程序接收到SIGTERM或者SIGINT訊號時,將訊號轉發給工作程序,工作程序直接呼叫ngx_worker_process_exit 函式退出。master程序在所有工作程序都退出後,呼叫 ngx_master_process_exit 函式退出。另外,如果工作程序未能正常退出,master程序會等待1秒後,傳送SIGKILL訊號強制終止工作程序。

3.3 強制停止所有 nginx 程序

[root@localhost ~]# nginx -s stop
[root@localhost ~]# pkill -9 nginx

直接給所有的nginx程序傳送SIGKILL訊號。

3.4 使用Lifecycle PreStop Hook來優雅關停 nginx

1) 執行Docker hub官方提供的Nginx映象。

官方提供的Nginx Dockerfile中提供的預設的啟動Nginx命令如下

Dockerfile

...
CMD ["nginx", "-g", "daemon off;"]

上面CMD指定直接在前端啟動nginx。

2) Nginx優雅關停Deployment Yaml配置。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
        lifecycle:
          preStop:
            exec:
              command: ["/usr/sbin/nginx", "-s", "quit"]
      terminationGracePeriodSeconds: 120  # 設定優雅終止的超時時間為 120 秒(2 分鐘)

4、總結

透過合理的優雅退出配置 T4 <= T2,即在確保網路層面已經刪除了Pod IP的前提下,容器再進行優雅退出,在優雅退出過程中繼續處理尚未完成的請求,並完成必要的清理工作,如資料儲存、連線關閉等。確保Pod在退出時對使用者客戶端請求是無感知的,同時保證服務的一致性和可靠性。
參考:《詳細解讀 Kubernetes 中 Pod 優雅退出,幫你解決大問題...
參考:《K8s Pod優雅關閉,沒你想象的那麼簡單!

相關文章