kubectl drain <node> 到底干了啥?

基于 v1.18.1

kubectl drain 是一个比较常用的 k8s 集群管理命令,用于将某一个节点设置成维护状态,目的是在不中断业务的情况下维护 k8s 集群中的节点

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

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

详细探究一下整个驱逐过程的细节,可以窥见 API Server 实现的冰山一角

Pod 是怎么驱逐的?

kubectl 的代码中得知,Pod 主要有两种驱逐方式,判断的方式基于 api-server 是否支持 Pod 的子资源 pods/eviction

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

如果不支持,将直接删除 Pod ,由 PodOwner 来重新创建被删除的 Pod(因为被排水节点已经设置成了 Unschedulable,所以不会有 Pod 被调度上去)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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 也就是驱逐的过程

Pod 驱逐的详细过程

在确认了当前的 API Server 支持并且 kubectl 没有明确禁用驱逐的情况下,进入了 evictPods 方法

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

首先这个方法会接受一个待驱逐的 Pod 列表,很好理解,就是被排水的节点上所有的 Pod

其次是 API Server 支持的 policy 组的版本 policyGroupVersion,是在检查 API Server 是否支持 pods/eviction 资源时返回的 policy 组的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// https://kubernetes.default.svc/apis
{
"kind": "APIGroupList",
"apiVersion": "v1",
"groups": [
{
"name": "policy",
"versions": [
{
"groupVersion": "policy/v1beta1",
"version": "v1beta1"
}
],
"preferredVersion": {
"groupVersion": "policy/v1beta1",
"version": "v1beta1"
}
}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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 的闭包函数,主要用途是在驱逐时能获取到最新的 Pod 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 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 驱逐完必,返回过程中产生的错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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 路由

到这其实在 kubectl 的流程就完成了,剩下的步骤将由 API Server 处理

来到 k8s.io/kubernetes/pkg/registry 这个包,这个包主要是对存储的实现以及 API Server 的核心逻辑

1
2
3
4
// 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() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 会触发对应的 Owner 重新创建 Pod,至此驱逐动作就结束了

总结

其实驱逐 Pod 过程无非是从存储中删除掉 Pod 对象,让对应的 Owner 重新在符合调度容忍的节点上重新创建 Pod,只是在支持了 policy/v1beta1 的集群中,API Server 会更优雅的先判断是否符合 Pod 的摧毁预算,某种程度上保证了 Pod 的高可用