基于 v1.18.1

kubectl drain 是一个比较常用的 Kubernetes 集群管理命令,用于将某一个节点设置成维护状态,目的是尽可能优雅的下线节点。

这个命令主要执行的两个动作:

  • 设置节点为 Unschedulable 状态(相当于执行了 kubectl cordon <node>
  • 驱逐这个节点上所有的 Pod

设置节点不可调度的逻辑比较简单,本文主要着眼于如何驱逐 Pod。

Pod 是怎么驱逐的?

Pod 主要有两种驱逐方式,而具体采用哪种方式,是通过判断 api-server 是否支持 Pod 的子资源 pods/eviction

如果支持,优先采用为需要驱逐的 Pod 创建 Eviction 类型的子资源的形式,来驱逐对应的 Pod

如果不支持,将直接删除 Pod ,由 Pod 的管理者(可能是 ReplicaSet Controller)来负责重新创建被删除的 Pod(因为 kubectl cordon <node> 已经将节点设置成了 Unschedulable,所以不会新创建的 Pod 被调度上去)

// k8s.io/pkg/kubectl/pkg/drain/drain.go:219

// DeleteOrEvictPods deletes or evicts the pods on the api server
func (d *Helper) DeleteOrEvictPods(pods []corev1.Pod) error {
	if len(pods) == 0 {
		return nil
	}

	getPodFn := func(namespace, name string) (*corev1.Pod, error) {
		return d.Client.CoreV1().Pods(namespace).Get(context.TODO(), name, metav1.GetOptions{})
	}

	if !d.DisableEviction {
		policyGroupVersion, err := CheckEvictionSupport(d.Client) // 检查 API Server 是否支持 eviction
		if err != nil {
			return err
		}

		if len(policyGroupVersion) > 0 {
			return d.evictPods(pods, policyGroupVersion, getPodFn)
		}
	}

	return d.deletePods(pods, getPodFn)
}

直接删除 Pod 的方式比较简单,这里详细探讨一下 evict 也就是驱逐的过程

通过 Eviction 驱逐 Pod 的详细过程

在确认了当前的 API Server 支持并且 kubectl 没有通过选项 --disable-eviction 禁用驱逐的情况下,进入了 evictPods 方法

func (d *Helper) evictPods(pods []corev1.Pod, policyGroupVersion string, getPodFn func(namespace, name string) (*corev1.Pod, error)) error

首先这个方法会接受一个待驱逐的 Pod 列表,其中的元素就是 drain 节点上所有的 Pod。

第二个参数是 API Server 所支持的 policy API 组的版本,是在检查 API Server 是否支持 pods/eviction 资源时获取到的。

// https://kubernetes.default.svc/apis
{
    "kind": "APIGroupList",
    "apiVersion": "v1",
    "groups": [
        {
            "name": "policy",
            "versions": [
                {
                    "groupVersion": "policy/v1beta1",
                    "version": "v1beta1"
                }
            ],
            "preferredVersion": {
                "groupVersion": "policy/v1beta1",
                "version": "v1beta1"
            }
        }
    ]
}
// https://kubernetes.default.svc/api/v1
{
    "kind": "APIResourceList",
    "groupVersion": "v1",
    "resources": [
        {
            "name": "pods/eviction",
            "singularName": "",
            "namespaced": true,
            "group": "policy",
            "version": "v1beta1",
            "kind": "Eviction",
            "verbs": [
                "create"
            ]
        }
    ]
}

从这个接口返回中可以看到,pods/eviction 资源的类型为 policy/v1beta1 组下的 Eviction 类型,只支持 create 动作,对应到 HTTP 的方法为 POST。

第三个参数是一个用于获取 Pod 对象的闭包函数,主要用途是在驱逐时能从 API Server 中获取到最新的 Pod 对象。

// k8s.io/pkg/kubectl/pkg/drain/drain.go:244
func (d *Helper) evictPods(pods []corev1.Pod, policyGroupVersion string, getPodFn func(namespace, name string) (*corev1.Pod, error)) error {
	...

	for _, pod := range pods {
		go func(pod corev1.Pod, returnCh chan error) {
			for {

				...

				err := d.EvictPod(pod, policyGroupVersion)
				
				...
			}

			...

			_, err := waitForDelete(params)
			
			...

		}(pod, returnCh)
	}

	doneCount := 0
	var errors []error

	numPods := len(pods)
	for doneCount < numPods {
		select {
		case err := <-returnCh:
			doneCount++
			if err != nil {
				errors = append(errors, err)
			}
		default:
		}
	}

	return utilerrors.NewAggregate(errors)
}

在 evictPods 方法中,核心做了两件事情

  1. 遍历待驱逐 Pod 列表为每个 Pod 创建了一个 goroutine 来调用 d.EvictPod 处理和等待 Pod 的驱逐
  2. 等待所有的 Pod 驱逐完必,返回过程中产生的错误
// EvictPod will evict the give pod, or return an error if it couldn't
func (d *Helper) EvictPod(pod corev1.Pod, policyGroupVersion string) error {
	
	...

	eviction := &policyv1beta1.Eviction{
		TypeMeta: metav1.TypeMeta{
			APIVersion: policyGroupVersion,
			Kind:       EvictionKind,
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      pod.Name,
			Namespace: pod.Namespace,
		},
		DeleteOptions: &delOpts,
	}

	// Remember to change change the URL manipulation func when Eviction's version change
	return d.Client.PolicyV1beta1().Evictions(eviction.Namespace).Evict(context.TODO(), eviction)
}

d.EvictPod 方法中,可以看到,驱逐 Pod 本质上是为 Pod 对象创建了一个类型为 Eviction 的子资源,对应到 HTTP 请求就是请求 pods/eviction 路由

剩下对于 Pod 资源的操作将由 API Server 处理

k8s.io/kubernetes/pkg/registry 包中主要是对 Kubernetes 集群元数据存储的实现以及 API Server 的核心逻辑

// k8s.io/kubernetes/pkg/registry/core/rest/storage_core.go:318
if legacyscheme.Scheme.IsVersionRegistered(schema.GroupVersion{Group: "policy", Version: "v1beta1"}) {
	restStorageMap["pods/eviction"] = podStorage.Eviction
}

restStorageMap 是一个 RESTful 路由与元数据操作之间的映射,这里也是 API Server 最核心的功能:提供一套 RESTful API 用于操作元数据存储(默认为 etcd

此处我们可以看到当确认 Scheme 中有注册 policy/v1beta1 资源组时,添加了对 pods/eviction 这个路由的支持,这个资源只支持一种动作,就是 create,所以我们就可以来到 EvictionRESTCreate() 方法

// k8s.io/kubernetes/pkg/registry/core/pod/storage/eviction.go:104
func (r *EvictionREST) Create(ctx context.Context, name string, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {

	...

	obj, err = r.store.Get(ctx, eviction.Name, &metav1.GetOptions{}) // 从存储中取出 Pod 对象
	if err != nil {
		return nil, err
	}
	pod := obj.(*api.Pod)

	if pod.Status.Phase == api.PodSucceeded || pod.Status.Phase == api.PodFailed { // 检查 Pod 的状态
		_, _, err = r.store.Delete(ctx, eviction.Name, rest.ValidateAllObjectFunc, deletionOptions) // 直接删除 Pod
		if err != nil {
			return nil, err
		}
		return &metav1.Status{
			Status: metav1.StatusSuccess}, nil
	}

	...

	err = func() error {
		pdbs, err := r.getPodDisruptionBudgets(ctx, pod) // 检查是否有 Pod 摧毁预算
		if err != nil {
			return err
		}

		if len(pdbs) == 0 {
			return nil
		}

		...

		err = retry.RetryOnConflict(EvictionsRetry, func() error {

			...

			if err = r.checkAndDecrement(pod.Namespace, pod.Name, *pdb, dryrun.IsDryRun(deletionOptions.DryRun)); err != nil { // 确认 Pod 可以被摧毁
				refresh = true
				return err
			}
			return nil
		}
	}

	...

	_, _, err = r.store.Delete(ctx, eviction.Name, rest.ValidateAllObjectFunc, deletionOptions) // 确认了 Pod 可以被摧毁之后删除 Pod
	if err != nil {
		return nil, err
	}

	// Success!
	return &metav1.Status{Status: metav1.StatusSuccess}, nil
}

Create() 方法中就是驱逐 Pod 的核心部分了

首先从存储中取出最新的待驱逐 Pod 对象,检查 Pod 对象的状态,如果为 Success 或是 Failed 说明容器已经退出了,可以直接驱逐,此时的操作是直接从存储中删除 Pod 对象

紧接着,检查当前 Pod 是否有对应的 Pod 摧毁预算配置,如果有就先检查当前的条件是否满足摧毁 Pod 之后的预算,如果满足预算,就令允许预算减一,然后再删除 Pod 对象

func (r *EvictionREST) checkAndDecrement(namespace string, podName string, pdb policyv1beta1.PodDisruptionBudget, dryRun bool) error {

	...

	pdb.Status.DisruptionsAllowed--

	...

	if _, err := r.podDisruptionBudgetClient.PodDisruptionBudgets(namespace).UpdateStatus(context.TODO(), &pdb, metav1.UpdateOptions{}); err != nil {
		return err
	}

	return nil
}

至此 Pod 的驱逐动作就结束了

总结

其实驱逐 Pod 过程就是从存储中删除掉 Pod 对象的过程,只是在支持了 policy/v1beta1 的集群中,API Server 会更优雅的先判断是否满足 Pod 的摧毁预算,保证了业务的高可用