基于 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 方法中,核心做了两件事情
- 遍历待驱逐 Pod 列表为每个 Pod 创建了一个 goroutine 来调用
d.EvictPod
处理和等待 Pod 的驱逐 - 等待所有的 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
,所以我们就可以来到 EvictionREST
的 Create()
方法
// 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 的摧毁预算,保证了业务的高可用