概述

在 Kubernetes 集群中,当节点由于某些原因(网络、宕机等)不能正常工作时会被认定为不可用状态(Unknown 或者 False 状态),当时间超过了 pod-eviction-timeout 值时,那么节点上的所有 Pod 都会被节点控制器计划删除。

详细分析

Kubernetes 集群中有一个节点生命周期控制器:node_lifecycle_controller.go。它会与每一个节点上的 kubelet 进行通信,以收集各个节点已经节点上容器的相关状态信息。当超出一定时间后不能与 kubelet 通信,那么就会标记该节点为 Unknown 状态。并且节点生命周期控制器会自动创建代表状况的污点,用于防止调度器调度 pod 到该节点。

那么 Unknown 状态的节点上已经运行的 pod 会怎么处理呢?节点上的所有 Pod 都会被污点管理器(taint_manager.go)计划删除。而在节点被认定为不可用状态到删除节点上的 Pod 之间是有一段时间的,这段时间被称为容忍度。你可以通过下面的方式来配置容忍度的时间长短:

      tolerations:
      - key: node.kubernetes.io/not-ready
        operator: Exists
        effect: NoExecute
        tolerationSeconds: 180
      - key: node.kubernetes.io/unreachable
        operator: Exists
        effect: NoExecute
        tolerationSeconds: 180

如果在不配置的情况下,Kubernetes 会自动给 Pod 添加一个 key 为 node.kubernetes.io/not-ready 的容忍度 并配置 tolerationSeconds=300,同样,Kubernetes 会给 Pod 添加一个 key 为 node.kubernetes.io/unreachable 的容忍度 并配置 tolerationSeconds=300。从 Kubernetes 源码中也可找到相应的配置:
在这里插入图片描述
当到了删除 Pod 时,污点管理器会创建污点标记事件,然后驱逐 pod 。这里需要注意的是由于已经不能与 kubelet 通信,所以该节点上的 Pod 在管理后台看到的是处于灰色标记,但是此时如果去获取 pod 的状态其实还是处于 Running 状态。每种类型的资源都有相应的资源控制器(Controller),例如:deployment_controller.go、stateful_set_control.go。每种控制器都在监听资源变化,从而做出相应的动作执行。deployment 控制器在监听到 Pod 被驱逐后会创建一个新的 Pod 出来,但是 Statefulset 控制器并不会创建出新的 Pod,原因是因为它可能会违反 StatefulSet 固有的至多一个的语义,可能出现具有相同身份的多个成员,这将可能是灾难性的,并且可能导致数据丢失。具体的原因请看官方文档: 强制删除 StatefulSet 类型的 Pods

源码分析

下面将从代码的角度去分析 Pod 驱逐过程中的一些关键步骤。

节点容忍度时间的选择

在污点管理器 taint_manager.go 文件中有一个 processPodOnNode 方法,该方法就是当节点状态变化时处理节点上 Pod 的过程。代码如下:

func (tc *NoExecuteTaintManager) processPodOnNode(
	podNamespacedName types.NamespacedName,
	nodeName string,
	tolerations []v1.Toleration,
	taints []v1.Taint,
	now time.Time,
) {
	if len(taints) == 0 {
		tc.cancelWorkWithEvent(podNamespacedName)
	}
	allTolerated, usedTolerations := v1helper.GetMatchingTolerations(taints, tolerations)
	if !allTolerated {
		klog.V(2).Infof("Not all taints are tolerated after update for Pod %v on %v", podNamespacedName.String(), nodeName)
		// We're canceling scheduled work (if any), as we're going to delete the Pod right away.
		tc.cancelWorkWithEvent(podNamespacedName)
		tc.taintEvictionQueue.AddWork(NewWorkArgs(podNamespacedName.Name, podNamespacedName.Namespace), time.Now(), time.Now())
		return
	}
	minTolerationTime := getMinTolerationTime(usedTolerations)
	// getMinTolerationTime returns negative value to denote infinite toleration.
	if minTolerationTime < 0 {
		klog.V(4).Infof("New tolerations for %v tolerate forever. Scheduled deletion won't be cancelled if already scheduled.", podNamespacedName.String())
		return
	}

	startTime := now
	triggerTime := startTime.Add(minTolerationTime)
	scheduledEviction := tc.taintEvictionQueue.GetWorkerUnsafe(podNamespacedName.String())
	if scheduledEviction != nil {
		startTime = scheduledEviction.CreatedAt
		if startTime.Add(minTolerationTime).Before(triggerTime) {
			return
		}
		tc.cancelWorkWithEvent(podNamespacedName)
	}
	tc.taintEvictionQueue.AddWork(NewWorkArgs(podNamespacedName.Name, podNamespacedName.Namespace), startTime, triggerTime)
}

其中 minTolerationTime := getMinTolerationTime(usedTolerations) 语句是用于获取驱逐时间的,方法的内容主要是获取之前编排文件中配置的容忍度(tolerations)相关的属性,找出最小值返回。其内容如下:

// getMinTolerationTime returns minimal toleration time from the given slice, or -1 if it's infinite.
func getMinTolerationTime(tolerations []v1.Toleration) time.Duration {
	minTolerationTime := int64(math.MaxInt64)
	if len(tolerations) == 0 {
		return 0
	}

	for i := range tolerations {
		if tolerations[i].TolerationSeconds != nil {
			tolerationSeconds := *(tolerations[i].TolerationSeconds)
			if tolerationSeconds <= 0 {
				return 0
			} else if tolerationSeconds < minTolerationTime {
				minTolerationTime = tolerationSeconds
			}
		}
	}

	if minTolerationTime == int64(math.MaxInt64) {
		return -1
	}
	return time.Duration(minTolerationTime) * time.Second
}
Pod 驱逐处理

在 processPodOnNode 方法的最后一行会把要驱逐的 Pod 信息添加到污点驱逐队列(taintEvictionQueue)中,指定创建时间(now)和触发时间(now + minTolerationTime)。

tc.taintEvictionQueue.AddWork(NewWorkArgs(podNamespacedName.Name, podNamespacedName.Namespace), startTime, triggerTime)

污点驱逐队列是一个 TimedWorkerQueue 类型的对象,该类型有一个定时器和处理函数,当到达触发时间时就会执行处理函数。那么 Pod 的驱逐处理也将在 minTolerationTime 秒后被执行。

Pod 的删除逻辑主要在 deletePodHandler 函数中,改函数主要是产生一个 Pod 被污点标记删除事件,然后请求删除 Pod。其代码如下:

func deletePodHandler(c clientset.Interface, emitEventFunc func(types.NamespacedName)) func(args *WorkArgs) error {
	return func(args *WorkArgs) error {
		ns := args.NamespacedName.Namespace
		name := args.NamespacedName.Name
		klog.V(0).Infof("NoExecuteTaintManager is deleting Pod: %v", args.NamespacedName.String())
		if emitEventFunc != nil {
			emitEventFunc(args.NamespacedName)
		}
		var err error
		for i := 0; i < retries; i++ {
			err = c.CoreV1().Pods(ns).Delete(name, &metav1.DeleteOptions{})
			if err == nil {
				break
			}
			time.Sleep(10 * time.Millisecond)
		}
		return err
	}
}

污点标记删除事件的语句是 emitEventFunc(args.NamespacedName) ,它会产生一个 Marking for deletion Pod %s 事件,在 Kubernetes 的管理后台也能看到。其代码如下:

func (tc *NoExecuteTaintManager) emitPodDeletionEvent(nsName types.NamespacedName) {
	if tc.recorder == nil {
		return
	}
	ref := &v1.ObjectReference{
		Kind:      "Pod",
		Name:      nsName.Name,
		Namespace: nsName.Namespace,
	}
	tc.recorder.Eventf(ref, v1.EventTypeNormal, "TaintManagerEviction", "Marking for deletion Pod %s", nsName.String())
}

总结

本文主要介绍了在节点异常时 Pod 被驱逐的过程,先总体介绍了驱逐过程,主要是节点生命周期控制器与节点上 kubelet 通信收集相关状态信息,在一定时间无法通信后认为该节点不可用,然后污点管理器就会执行相关驱逐Pod 的逻辑。后面又结合源码进行的具体详细的分析,希望对你有用。

参考

Logo

开源、云原生的融合云平台

更多推荐