使用 client-go 实现 Kubernetes 节点 Drain:详解与实战教程

在 Kubernetes 中使用 client-go 实现 drain 功能涉及多个步骤,需要模仿 kubectl drain 的行为。这包括将节点标记为不可调度(cordon)、驱逐 Pod,并处理 DaemonSet 和不可驱逐 Pod 的逻辑。以下是实现 drain 的主要步骤:


1. 基本思路

drain 的核心步骤包括:

  1. 标记节点为不可调度(cordon)
  2. 列出节点上的所有 Pod
  3. 逐一驱逐可驱逐的 Pod
    • 忽略 DaemonSet 创建的 Pod。
    • 检查 PodDisruptionBudget(PDB)约束。
  4. 等待 Pod 驱逐完成

2. 代码实现

以下是一个基于 client-go 的示例代码,演示如何实现 drain 功能。

go 复制代码
package main

import (
	"context"
	"fmt"
	"time"

	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
)

func main() {
	// 加载 kubeconfig
	config, err := clientcmd.BuildConfigFromFlags("", "/path/to/kubeconfig")
	if err != nil {
		panic(err)
	}

	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		panic(err)
	}

	nodeName := "your-node-name"
	err = drainNode(clientset, nodeName)
	if err != nil {
		fmt.Printf("Failed to drain node: %v\n", err)
	} else {
		fmt.Printf("Node %s drained successfully\n", nodeName)
	}
}

// DrainNode 实现 drain 功能
func drainNode(clientset *kubernetes.Clientset, nodeName string) error {
	ctx := context.TODO()

	// Step 1: 将节点标记为不可调度
	if err := cordonNode(clientset, nodeName); err != nil {
		return fmt.Errorf("failed to cordon node: %w", err)
	}

	// Step 2: 获取节点上的所有 Pod
	podList, err := listPodsOnNode(clientset, nodeName)
	if err != nil {
		return fmt.Errorf("failed to list pods on node: %w", err)
	}

	// Step 3: 驱逐所有可驱逐的 Pod
	for _, pod := range podList.Items {
		if isDaemonSetPod(&pod) || isMirrorPod(&pod) {
			// 忽略 DaemonSet 和 Mirror Pod
			continue
		}

		// 驱逐 Pod
		err := evictPod(clientset, &pod)
		if err != nil {
			return fmt.Errorf("failed to evict pod %s: %w", pod.Name, err)
		}
	}

	// Step 4: 等待所有 Pod 被驱逐
	err = wait.PollImmediate(2*time.Second, 5*time.Minute, func() (bool, error) {
		remainingPods, err := listPodsOnNode(clientset, nodeName)
		if err != nil {
			return false, err
		}

		// 检查是否还有非 DaemonSet/Mirror 的 Pod
		for _, pod := range remainingPods.Items {
			if !isDaemonSetPod(&pod) && !isMirrorPod(&pod) {
				return false, nil
			}
		}

		return true, nil
	})
	if err != nil {
		return fmt.Errorf("timeout waiting for pods to be evicted: %w", err)
	}

	return nil
}

// 将节点标记为不可调度
func cordonNode(clientset *kubernetes.Clientset, nodeName string) error {
	ctx := context.TODO()

	node, err := clientset.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{})
	if err != nil {
		return err
	}

	if node.Spec.Unschedulable {
		// 已经是不可调度状态
		return nil
	}

	node.Spec.Unschedulable = true
	_, err = clientset.CoreV1().Nodes().Update(ctx, node, metav1.UpdateOptions{})
	return err
}

// 获取节点上的所有 Pod
func listPodsOnNode(clientset *kubernetes.Clientset, nodeName string) (*v1.PodList, error) {
	ctx := context.TODO()
	podList, err := clientset.CoreV1().Pods("").List(ctx, metav1.ListOptions{
		FieldSelector: fmt.Sprintf("spec.nodeName=%s", nodeName),
	})
	if err != nil {
		return nil, err
	}
	return podList, nil
}

// 检查是否为 DaemonSet 创建的 Pod
func isDaemonSetPod(pod *v1.Pod) bool {
	for _, owner := range pod.OwnerReferences {
		if owner.Kind == "DaemonSet" {
			return true
		}
	}
	return false
}

// 检查是否为 Mirror Pod
func isMirrorPod(pod *v1.Pod) bool {
	_, exists := pod.Annotations["kubernetes.io/config.mirror"]
	return exists
}

// 驱逐 Pod
func evictPod(clientset *kubernetes.Clientset, pod *v1.Pod) error {
	ctx := context.TODO()

	eviction := &v1.Eviction{
		ObjectMeta: metav1.ObjectMeta{
			Name:      pod.Name,
			Namespace: pod.Namespace,
		},
	}
	err := clientset.CoreV1().Pods(pod.Namespace).Evict(ctx, eviction)
	if err != nil {
		return err
	}
	return nil
}

3. 实现细节说明

  1. 节点 Cordon

    • 使用 clientset.CoreV1().Nodes().Update 更新节点的 Unschedulable 字段。
  2. 列出节点上的 Pod

    • 使用 FieldSelector 过滤出运行在目标节点上的 Pod。
  3. DaemonSet 和 Mirror Pod

    • 通过 Pod 的 OwnerReferences 判断是否属于 DaemonSet。
    • 通过检查 kubernetes.io/config.mirror 注解判断是否为 Mirror Pod。
  4. Pod 驱逐

    • 使用 Eviction 对象发起驱逐请求。
    • 需要确保 Pod 不违反 PodDisruptionBudget
  5. 等待驱逐完成

    • 使用 wait.PollImmediate 轮询检查节点上的 Pod 是否已被全部驱逐。

4. 注意事项

  1. 权限要求

    • 执行 cordonevict 操作需要相应的集群权限(如 nodes/updatepods/evict)。
  2. DaemonSet Pod 的处理

    • 默认忽略 DaemonSet Pod,如需强制移除需特殊处理。
  3. 驱逐策略

    • 驱逐操作可能受到 PodDisruptionBudget 的限制,需要确保驱逐不会违反 PDB。
  4. 资源规划

    • 确保其他节点有足够的资源接收被驱逐的 Pod。

以上代码和说明提供了一个完整的 client-go 实现 drain 的方法,适合在自定义工具或自动化运维场景中使用。

相关推荐
可观测性用观测云2 小时前
云原生网关 Ingress-Nginx 链路追踪实战:OpenTelemetry 采集与观测云集成方案
nginx·kubernetes
蝎子莱莱爱打怪2 天前
GitLab CI/CD + Docker Registry + K8s 部署完整实战指南
后端·docker·kubernetes
蝎子莱莱爱打怪5 天前
Centos7中一键安装K8s集群以及Rancher安装记录
运维·后端·kubernetes
花酒锄作田6 天前
Gin 框架中的规范响应格式设计与实现
golang·gin
阿里云云原生6 天前
Kubernetes 官方再出公告,强调立即迁移 Ingress NGINX
kubernetes
至此流年莫相忘6 天前
Kubernetes实战篇之配置与存储
云原生·容器·kubernetes
qwfys2006 天前
How to install golang 1.26.0 to Ubuntu 24.04
ubuntu·golang·install
至此流年莫相忘6 天前
Kubernetes实战篇之服务发现
容器·kubernetes·服务发现
only_Klein6 天前
Kubernetes 版本升级
容器·kubernetes·upgrade
codeejun6 天前
每日一Go-25、Go语言进阶:深入并发模式1
开发语言·后端·golang