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優雅關閉的重要性主要體現在以下幾個方面:
-
避免服務中斷:透過優雅關閉,Pod可以在終止前完成當前正在處理的請求,確保服務不會因為Pod的突然終止而中斷。
-
確保資料一致性:優雅關閉允許Pod在終止前透過PreStop Hook完成必要的資料持久化或事務處理,從而確保資料的一致性。
-
最小化使用者體驗影響:透過優雅關閉,可以避免將流量路由到已經被刪除的Pod,減少使用者請求處理失敗的可能性。在滾動更新或擴充套件Pod時,優雅關閉能夠確保服務的平滑過渡,對使用者來說幾乎是無感知的。
-
合理利用資源:優雅關閉允許Pod在終止前釋放佔用的資源,避免資源浪費和洩露,提高資源的利用率。
總的來說,Pod優雅關閉是Kubernetes中一個重要的功能,它結合了PreStop Hook和寬限期等機制,確保Pod在終止前能夠優雅地完成必要的清理工作,從而保持服務的穩定性和可用性、確保資料一致性、提升使用者體驗和合理利用資源。在進行Pod管理時,應該充分了解和利用Pod優雅關閉的功能。
2、Kubernetes Pod刪除原理
Kubernetes (k8s) 中的 Pod 可能因多種原因被刪除。以下是一些常見原因:
-
手動刪除:使用者使用 kubectl delete pod 命令手動刪除 Pod。
-
控制器策略:Deployment、ReplicaSet 或 DaemonSet 等控制器根據其策略調整副本數,例如縮減副本數時會刪除多餘的 Pod;Job 和 CronJob 完成後刪除其建立的 Pod。
-
節點故障:如果節點失效,節點上的 Pod 會被 Kubernetes 控制平面標記為失效並在其他節點上重新排程。
-
資源限制:當節點資源不足時,Kubernetes 可能會根據優先順序和資源限制(如資源配額和排程策略)來刪除一些 Pod。
-
健康檢查失敗:Pod 的 liveness 或 readiness 探針連續失敗,Kubernetes 會認為 Pod 不健康並刪除或重啟它。
-
優先順序搶佔:如果有更高優先順序的 Pod 需要資源,Kubernetes 可能會刪除較低優先順序的 Pod 以釋放資源。
-
排程器策略:Kubernetes 排程器可能會根據排程策略(如 NodeAffinity、PodAffinity 等)重新分配 Pod,從而刪除舊的 Pod。
-
更新策略:Deployment 或 StatefulSet 進行滾動更新時,舊的 Pod 會被刪除並替換為新的 Pod。
-
節點自動縮放:當使用叢集自動縮放器時,如果叢集縮小(移除節點),部分 Pod 會被刪除。
但是不管是何種原因刪除Pod(使用者手動刪除或控制器自動刪除),在Pod的刪除過程中,都會同時會存在兩條並行的時間線,如下圖所示:
- 一條時間線是網路規則的更新過程。
- 另一條時間線是 Pod 的刪除過程。
由上面流程圖可知,在 Pod 刪除過程中,存在兩條並行的時間線,這兩條時間線誰先執行完畢是不確定的。如果 Pod 內的容器已經刪除,但網路層面的 Endpoint 資源仍包含該 Pod 的 IP,客戶端請求可能會被路由到已刪除的 Pod,導致請求處理失敗;或者請求未處理完時,Pod 內的容器已經被刪除,這樣也會導致請求處理失敗。以下是一個工作負載滾動升級的示例,說明如果不為 Pod 配置合理的優雅退出機制,會出現什麼問題。
工作負載滾動升級問題示例
-
請求路由錯誤:舊 Pod 刪除但仍在 Endpoint 資源中,導致請求被路由到已刪除的 Pod,返回以下錯誤:
- 502 Bad Gateway:負載均衡器或反向代理無法正確路由請求。
-
資料丟失或不一致:舊 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 的流程過程。
網路層面:
- Pod 被刪除,狀態置為 Terminating。
- Endpoint Controller 將該 Pod 的 ip 從 Endpoint 物件中刪除。
- Kube-proxy 根據 Endpoint 物件的改變更新 iptables/ipvs 規則,不再將流量路由到被刪除的 Pod。
- 如果還有其他 Gateway 依賴 Endpoint 資源變化的,也會改變自己的配置(比如 Nginx Ingress Controller)。
注意: 預設 Ingres nginx.ingress.kubernetes.io/service-upstream 註解值為false,Nginx Ingress Controller 代理服務時,藉助Endpoint代理代理上游服務到 PodIp。
Pod 層面:
- Pod 被刪除,狀態置為 Terminating。
- Kubelet 捕獲到 ApiServer 中 Pod 狀態變化,執行 syncPod 動作。
- 如果 Pod 配置了 preStop Hook ,將會執行。
- kubelet 對 Pod 中各個 container 傳送呼叫 cri 介面中 StopContainer 方法,向 dockerd 傳送 stop -t 指令,用 SIGTERM 訊號以通知容器內應用程序開始優雅停止。
- 等待容器內應用程序完全停止,如果容器在 gracePeriod 執行時間內還未完全停止,就傳送 SIGKILL 訊號強制殺死應用程序(容器執行時處理)。
- 所有容器程序終止,清理 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)計算容器優雅關閉寬限時間
- 預設設定容器最小優雅關停寬限時間等於2秒。
- 如果 podDeletionGracePeriodSeconds 不是 nil,即 Pod 是被 Apiserver 刪除的,那麼 gracePeriod 直接取值,優先使用呼叫 Apiserver 刪除Pod時指定的值作為優雅關閉Pod寬限時間,比如kubectl delete pod my-pod --grace-period=60。
- 如果 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 }
透過上面的程式碼,驗證了之前架構圖中流程。我們這邊可以簡單的終結下一些內容:
- kubelet 作為觀察者監控著 ApiServer 中Pod的變化,呼叫 syncPod 方法去完成當前 node 內的 Pod 狀態更新。(刪除 Pod 也算是一種 Pod 的狀態更新)
- 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 能夠真正的正確的關閉。
知道了原因,知道了邏輯,那順理成章的就有了解決方案:
- 容器應用程序中要有優雅退出程式碼,能夠執行優雅退出;
- 增加 preStopHook,能夠執行一定時間的 sleep;
- 修改 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 分鐘)