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优雅退出的更多相关文章

  1. 详解Kubernetes存储体系

    Volume.PV.PVC.StorageClass由来 先思考一个问题,为什么会引入Volume这样一个概念? " 答案很简单,为了实现数据持久化,数据的生命周期不随着容器的消亡而消亡. ...

  2. 详解kubernetes五种暴露服务的方式

    部署完服务终将是为了访问,那么kubernetes中service和ingress都可以将集群内部的服务能够支持外部访问.service可以让一组 Pod(称为"后端")为集群内的 ...

  3. 详解Kubernetes微服务自动化发布系统

    实施微服务架构后,原先单一的系统结构统变成了数量众多的微服务应用,开发.测试.运维部署等都会面临不少挑战.在微服务架构下如何提高工程研发效率,确保开发.测试.运维部署等流程上的顺畅,是微服务技术体系能 ...

  4. 服务在kubernetes上优雅退出

    在dashboard上,直接操作一下就可以了:将spec.replicas = 0,对应的容器组会自动销毁

  5. Kubernetes 实践指南之Kubernetes 的命令行工具详解

    kubectl作为客户端CLI工具,可以让用户通过命令行的方式对Kubernetes集群进行管理.本节内容将对kubectl的子命令和用法进行详细描述. 一.kubectl 用法概述 kubectl语 ...

  6. Kubernetes-yaml详解

    目录: Yaml语法格式 查看api资源版本标签 deployment模板 service模板 查询帮助和格式指令 Pod模板 写 yaml太累怎么办 yaml文件的学习 方法 deployment. ...

  7. oracle监听(lsnrctl)详解解读

    (总结)Oracle监听服务lsnrctl参数及查询状态详解   lsnrctl命令常用参数详解: lsnrctlstart启动指定的监听器 stop关闭指定的监听器 status显示监听器的状态.s ...

  8. [转帖]技术扫盲:新一代基于UDP的低延时网络传输层协议——QUIC详解

    技术扫盲:新一代基于UDP的低延时网络传输层协议——QUIC详解    http://www.52im.net/thread-1309-1-1.html   本文来自腾讯资深研发工程师罗成的技术分享, ...

  9. kubernetes系列07—Pod控制器详解

    本文收录在容器技术学习系列文章总目录 1.Pod控制器 1.1 介绍 Pod控制器是用于实现管理pod的中间层,确保pod资源符合预期的状态,pod的资源出现故障时,会尝试 进行重启,当根据重启策略无 ...

  10. Kubernetes K8S之通过yaml文件创建Pod与Pod常用字段详解

    YAML语法规范:在kubernetes k8s中如何通过yaml文件创建pod,以及pod常用字段详解 YAML 语法规范 K8S 里所有的资源或者配置都可以用 yaml 或 Json 定义.YAM ...

随机推荐

  1. [FAQ] uni-app 导航路由切换时如何强制刷新页面?

    使用 this.$forceUpdate() 强制刷新页面. Refer:uni-app自定义导航 Link:https://www.cnblogs.com/farwish/p/13870801.ht ...

  2. (数据科学学习手札160)使用miniforge代替miniconda

    本文已收录至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 大家好我是费老师,conda作为Python数据科学领域 ...

  3. netcore依赖注入通过反射简化

    aspnetcore里面用到许多的service,好多业务代码都要通过Service.AddScoped.Singleton.Transient等注入进去,类太多了写起来和管理起来都很麻烦,所以借鉴了 ...

  4. Linux上OcenBase单机版部署及基本信息查询

    OceanBase单机版部署可以通过在线和离线两种方式部署.在线部署可以通过yum源或者apt源部署,直接拉取官方源码即可.实际使用中,大部分环境连不了外网,本文介绍离线方式安装. 下载"O ...

  5. 【爬虫数据集】李子柒YouTube频道TOP10热门视频的TOP2000热门评论,共计2W条

    目录 一.背景 二.爬取目标 三.结果展示 四.演示视频 五.附完整数据 一.背景 这段时间,有超多小伙伴找我要YouTube数据,做数据分析.情感分析之类的研究工作,但很多人并不是计算机软件相关专业 ...

  6. 仅1.0MB,开源压缩软件7-Zip

    首发微信公众号:SQL数据库运维 原文链接:https://mp.weixin.qq.com/s?__biz=MzI1NTQyNzg3MQ==&mid=2247485212&idx=1 ...

  7. 用Python画一个冰墩墩!

    北京2022年冬奥会的召开,吉祥物冰墩墩着实火了,真的是一墩难求,为了实现冰墩墩的自由,经过资料搜集及参考冰墩墩网上的开源代码(https://github.com/HelloCoder-HaC/bi ...

  8. Java简单实现MQ架构和思路02

    Java MQ的100个功能清单 有重复的 一个消息队列(MQ)可以有以下功能: 批量发送消息:允许将多个消息打包成一个批次发送,可以减少网络传输开销和提高系统吞吐量. 消息过期时间:消息可以设置一个 ...

  9. 用pageOffice控件实现 office word文档在线编辑 表格中写数据的方法

    PageOffice对Word文档中Table的操作,包括给单元格赋值和动态添加行的效果. 1 应用场景 OA办公中,经常要在文档的指定位置表格,填充后端指定数据. 如word文档中,表格数据 如下表 ...

  10. 保姆教程系列:Git 实用命令详解

    !!!是的没错,胖友们,保姆教程系列又更新了!!! @ 目录 前言 1.将本地项目推送到远程仓库 2. Idea Git回退到某个历史版本 3. 修改项目关联远程地址方法 4. Git 修改分支的名称 ...