Pod优雅关闭是指在Kubernetes中,当Pod因为某种原因(如版本更新、资源不足、故障等)需要被终止时,Kubernetes不会立即强制关闭Pod,而是首先尝试以一种“优雅”的方式关闭Pod。这个过程允许Pod中的容器有足够的时间来响应终止信号(默认为SIGTERM),并在终止前完成必要的清理工作,如保存数据、关闭连接等。
注意 :在《Docker容器优雅退出》这篇博文中,我们详细讲解了Docker优雅退出机制,在本文我们将详细详解Kubernetes Pod优雅退出机制。
具体来说,Pod优雅关闭的流程如下:
(1)PreStop Hook:
(2)SIGTERM信号:
(3)SIGKILL信号与资源清理:如果容器在宽限期后仍在运行,容器运行时会发送 SIGKILL 信号强制终止容器,并随后清理 Pod 的资源(容器运行时处理)。
注意:Kubelet 调用 cri 接口中 StopContainer 方法时,向 dockerd 发送 stop -t 指令时会带着优雅关停容器的宽限时间 gracePeriod,gracePeriod 取值分多个情况,默认是 terminationGracePeriodSeconds[30秒] - 容器执行 preStop 时间,具体详情见下文源码分析部分。
进行Pod优雅关闭的重要性主要体现在以下几个方面:
避免服务中断:通过优雅关闭,Pod可以在终止前完成当前正在处理的请求,确保服务不会因为Pod的突然终止而中断。
确保数据一致性:优雅关闭允许Pod在终止前通过PreStop Hook完成必要的数据持久化或事务处理,从而确保数据的一致性。
最小化用户体验影响:通过优雅关闭,可以避免将流量路由到已经被删除的Pod,减少用户请求处理失败的可能性。在滚动更新或扩展Pod时,优雅关闭能够确保服务的平滑过渡,对用户来说几乎是无感知的。
合理利用资源:优雅关闭允许Pod在终止前释放占用的资源,避免资源浪费和泄露,提高资源的利用率。
总的来说,Pod优雅关闭是Kubernetes中一个重要的功能,它结合了PreStop Hook和宽限期等机制,确保Pod在终止前能够优雅地完成必要的清理工作,从而保持服务的稳定性和可用性、确保数据一致性、提升用户体验和合理利用资源。在进行Pod管理时,应该充分了解和利用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 内的容器已经删除,但网络层面的 Endpoint 资源仍包含该 Pod 的 IP,客户端请求可能会被路由到已删除的 Pod,导致请求处理失败;或者请求未处理完时,Pod 内的容器已经被删除,这样也会导致请求处理失败。以下是一个工作负载滚动升级的示例,说明如果不为 Pod 配置合理的优雅退出机制,会出现什么问题。
工作负载滚动升级问题示例
请求路由错误:旧 Pod 删除但仍在 Endpoint 资源中,导致请求被路由到已删除的 Pod,返回以下错误:
数据丢失或不一致:旧 Pod 未将正在处理的请求处理完成的情况下被删除,如果该请求不是幂等性的,则可能导致以下错误:
注意 1:本文假设删除Pod都有关联的svc资源,客户端都是通过svc访问Pod。
注意 2:HTTP 404错误通常表示服务器无法找到请求的资源。这可能是因为资源已被删除、移动或从未存在过。在数据丢失或不一致的场景中,404错误可能是一个间接的结果。例如,如果Pod在删除之前正在处理一个应该创建新资源的请求(如数据库记录或文件),但由于Pod的删除,该资源可能没有被正确创建。稍后的请求试图访问该资源时,可能会收到404错误,因为资源不存在。
一切都从 TerminationGracePeriodSeconds 开始说起,我们回顾下 k8s 关闭 Pod 的流程过程。
注意: 默认 Ingres nginx.ingress.kubernetes.io/service-upstream 注解值为false,Nginx Ingress Controller 代理服务时,借助Endpoint代理代理上游服务到 PodIp。
注意: 默认 Ingres nginx.ingress.kubernetes.io/service-upstream 注解值为false,Nginx Ingress Controller 代理服务时,借助Endpoint代理代理上游服务到 PodIp。
我们重点关注下几个信号:K8S_EVENT, 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 值。
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)如果容器配置了 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 }
通过上面的代码,验证了之前架构图中流程。我们这边可以简单的终结下一些内容:
原则公式:T1 = T2 + T3
复杂的逻辑:
心思新密的小伙伴可能逐渐发现,要解决问题,实际就是做一个巧妙的动作调整时间差,满足业务 pod 能够真正的正确的关闭。
知道了原因,知道了逻辑,那顺理成章的就有了解决方案:
有时候我们也想在服务停止前,通过执行一条命令或者发送一个HTTP请求来优雅的停掉服务。
举例:
下面我们将以Nginx服务来讲解如何使用 停止服务:
先回顾一下有关Nginx的基础知识:
Nginx是一个多进程服务,master进程和一堆worker进程,master进程只负责校验配置文件语法,创建worker进程,真正的执行、接收客户请求、处理配置文件中指令都是由worker进程来完成的。master进程与worker进程之间主要是通过Linux Signal来实现交互。Nginx提供了大量的命令和处理信号来实现对配置文件的语法检查,服务优雅停止,进程平滑重启、升级等功能,我们这里仅简单介绍与nginx优雅停止相关命令触发的Linux Signal执行过程和执行原理。
nginx 的停止方法有很多,一般通过发送系统信号给 nginx 的master进程的方式来停止 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 主进程号。
[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信号强制终止工作进程。
[root@localhost ~]# nginx -s stop [root@localhost ~]# pkill -9 nginx
直接给所有的nginx进程发送SIGKILL信号。
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 分钟)