首页 > 其他分享 >详解Kubernetes Pod优雅退出

详解Kubernetes Pod优雅退出

时间:2024-06-20 15:55:42浏览次数:21  
标签:容器 删除 Kubernetes gracePeriod 优雅 详解 Pod pod

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优雅关闭,没你想象的那么简单!

标签:容器,删除,Kubernetes,gracePeriod,优雅,详解,Pod,pod
From: https://www.cnblogs.com/zhangmingcheng/p/18254613

相关文章

  • Docker配置与使用详解
    一、引言随着云计算和微服务的兴起,Docker作为一种轻量级的容器化技术,越来越受到开发者和运维人员的青睐。Docker通过容器化的方式,将应用程序及其依赖项打包成一个可移植的镜像,从而实现了应用程序的快速部署和扩展。本文将详细介绍Docker的配置与使用,包括Docker的安装、镜像......
  • 【windows|007】DHCP服务详解
    ......
  • openEuler安装Kubernetes
    openEulerkubeadm安装Kubernetes禁用或关闭服务firewalldsystemctlstopfirewalldsystemctldisablefirewalldselinuxsed-i's/SELINUX=enforcing/SELINUX=disabled/'/etc/selinux/configswap分区sed-i'/swap/s/\(.*\)/#\1/g'/etc/fstab设置......
  • 【Kubernetes】集群学习
    常见的Kubernetes集群类型Kubernetes集群可以根据不同的标准进行分类,但通常我们根据其部署环境和用途来区分集群类型。以下是几种常见的Kubernetes集群类型:开发集群(DevelopmentCluster):用于开发和测试环境,通常部署在本地机器或开发者的私有云中。这种集群用于验证应......
  • Python中__delattr__()函数详解
    在Python中,__delattr__是一个特殊方法(也称为魔术方法或双下划线方法),它在尝试删除对象的属性时被调用。当你使用del语句来删除一个对象的属性时,Python会自动调用这个方法来执行删除操作。__delattr__方法接受一个参数,即要删除的属性的名称(通常是一个字符串)。你可以在这个方法中自......
  • 从0开始C++(三):构造函数与析构函数详解
    目录构造函数 构造函数的基本使用构造函数也支持函数重载构造函数也支持函数参数默认值构造初始化列表拷贝构造函数浅拷贝和深拷贝析构函数 总结练习一下ヽ( ̄▽ ̄)ノ 构造函数 构造函数的基本使用构造函数是一种特殊的成员函数,用于创建对象时初始化,写法上有以下......
  • STM32学习笔记(十)--I2C、IIC总线协议详解
    概述:InterIntegratedCircuit,一组多从多组多从有应答是一种同步(具有时钟线需要同步时钟SCL)、串行(一位一位的往一个方向发送)、半双工(发送接收存在一种)通信总线。(1)硬件电路所有I2C设备的SCL连接在一起,SDA连在一起            设备的SCL和SDA均要......
  • UML:用例图的扩展点特性详解
    引言UML(统一建模语言)用例图是一种常用的系统建模工具,它可以帮助我们直观地表示系统的功能和用户的交互。这里将重点讲解UML用例图中的扩展点(ExtensionPoint)特性,通过具体示例深入理解这一概念。什么是扩展点?扩展点是UML用例图中的一个重要概念,它允许我们在用例的基础上添......
  • 12.1.k8s中的pod数据持久化-pv与pvc资源及动态存储StorageClass
    目录一、pc与pvc的概念二、pvc与pv初体验1,准别nfs环境1.1.所有节点安装nfs工具1.2.harbor节点编辑nfs配置文件 2,创建3个pv资源3,harbor节点创建pv对应的nfs存储路径 4,创建pvc关联pv5,创建pod引入pvc6,编辑index访问文件到harbor存储目录下7,浏览器访问测试三、Storag......
  • 【深度学习驱动流体力学】OpenFOAM 编译完成Bin目录命令计算流体力学详解
    OpenFOAM译完成Bin目录下包含了多个关键命令和工具,用于管理、运行和优化仿真过程中的各个环节。这些命令涵盖了从创建新案例、运行仿真到分析结果的全过程,包括处理网格、设置物理条件、运行求解器和后处理数据等多个方面。每个命令和工具都有其特定的功能和操作方法,用户......