目录

Client-go简单教程(六)-DynamicClient 实战

利用 DynamicClient 来做一个演示.

需求背景

kubernetes 集群里面很多服务处于不健康状态,如镜像拉取失败,pod 一直重复拉取,业务侧也无反馈服务不可用问题,说明这些服务是可以干掉的,因此开发本应用来自动消除这些不健康的服务,以 cronjob 的形式跑在集群内,定时运行,减少人力维护成本。

需求简述

  1. 并不是所有命名空间和 pod 都需要,pod 状态也不止镜像拉取错误,因此配置文件提供了:命名空间,持续时间,pod 失败状态的选择。

  2. 持续时间是 pod 创建开始到当前,目前没有去获取异常开始到当前的时间(后面研究有没有方法)。

  3. cronjob 的形式运行,后期需求如有变动可以用其他形式。

需求实现

1. 项目目录结构

目录结构在 ClientSet 实战 那里有说过,基本就是复制粘贴过来的,这里不做其他赘述。

1
2
3
4
5
6
auto-setzero     
├─conf
├─pkg
│  ├─clientset
│  └─utils
└─service

2. 初始化 ClientSet

详情翻阅 ClientSet 实战

信息
初始化 clientset 实例的时候,首先使用 inCluster 模式(需要去配置对应的 RBAC 权限,默认的 sa 是 default-> 是没有获取 nodes 的 List 权限)

3. utils 小工具

pkg/utils 目录下新建 utils.go , 里面存放的函数主要用来处理时间对比和对 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
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package utils

import (
	"time"
	"fmt"
)

// 时间比较
func CompareTime(t1, t2 string, duration string) bool {
	time1, err := time.Parse("2006-01-02 15:04:05", t1)
	if err != nil {
		fmt.Println("时间转化出错了.")
	}
	time2, err := time.Parse("2006-01-02 15:04:05", t2)
	if err != nil {
		fmt.Println("时间转化出错了.")
	}
	HourAfter, _ := time.ParseDuration(duration)
	time1 = time1.Add(HourAfter)
	// true 表示在 HourAfter 时间内,false 表示过了 HourAfter 小时
	return time2.Before(time1)
}

// 解析pod状态
func podStatusPhase(po *v1.Pod) string {
	status := string(po.Status.Phase)
	if po.Status.Reason != "" {
		if po.DeletionTimestamp != nil && po.Status.Reason == "NodeLost" {
			return "Unknown"
		}
		status = po.Status.Reason
	}
	status, ok := initContainerPhase(po.Status, len(po.Spec.InitContainers), status)
	if ok {
		return status
	}
	status, ok = containerPhase(po.Status, status)
	if ok && status == Completed {
		status = Running
	}
	if po.DeletionTimestamp == nil {
		return status
	}
	return Terminating
}

// 解析容器状态
func containerPhase(st v1.PodStatus, status string) (string, bool) {
	var running bool
	for i := len(st.ContainerStatuses) - 1; i >= 0; i-- {
		cs := st.ContainerStatuses[i]
		switch {
		case State.Waiting != nil && State.Waiting.Reason != "":
			status = State.Waiting.Reason
		case State.Terminated != nil && State.Terminated.Reason != "":
			status = State.Terminated.Reason
		case State.Terminated != nil:
			if State.Terminated.Signal != 0 {
				status = "Signal:" + strconv.Itoa(int(State.Terminated.Signal))
			} else {
				status = "ExitCode:" + strconv.Itoa(int(State.Terminated.ExitCode))
			}
		case Ready && State.Running != nil:
			running = true
		}
	}

	return status, running
}

// 解析初始化容器状态
func initContainerPhase(st v1.PodStatus, initCount int, status string) (string, bool) {
	for i, cs := range st.InitContainerStatuses {
		s := checkContainerStatus(cs, i, initCount)
		if s == "" {
			continue
		}
		return s, true
	}
	return status, false
}

// 检查初始化容器有没有错误
func checkContainerStatus(cs v1.ContainerStatus, i, initCount int) string {
	switch {
	case State.Terminated != nil:
		if State.Terminated.ExitCode == 0 {
			return ""
		}
		if State.Terminated.Reason != "" {
			return "Init:" + State.Terminated.Reason
		}
		if State.Terminated.Signal != 0 {
			return "Init:Signal:" + strconv.Itoa(int(State.Terminated.Signal))
		}
		return "Init:ExitCode:" + strconv.Itoa(int(State.Terminated.ExitCode))
	case State.Waiting != nil && State.Waiting.Reason != "" && State.Waiting.Reason != "PodInitializing":
		return "Init:" + State.Waiting.Reason
	default:
		return "Init:" + strconv.Itoa(i) + "/" + strconv.Itoa(initCount)
	}
}

// 序列化pod的ready, terminating, restartcount次数
func Statuses(ss []v1.ContainerStatus) (cr, ct, rc int) {
	for _, c := range ss {
		if c.State.Terminated != nil {
			ct++
		}
		if c.Ready {
			cr = cr + 1
		}
		rc += int(c.RestartCount)
	}

	return
}

4. 配置主程序

在 service 目录下新建 auto_setzero_service.go ,这里主要讲下代码的思路:

  • 先收集 podList 的信息,遍历出每个 pod

  • 判断 pod 当前状态和创建时间距离现在有多久,时间过长肯定是没人理的

  • 判断服务是 deployment 类型还是 rollouts 类型

  • 根据不通服务类型执行不同的消除 pod 函数

注意

这里设置 merge patch 类型 types.MergePatchType

如果要 SSA patch 类型,需要改为 types.ApplyPatchType

这里主要展示 rollouts 类型的处理函数,因为它需要用到动态客户端去执行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 设置 rollouts pod 数量设置为0
func (c *clients) setRolloutZero(namespace, rolloutsName string) error {
	gvr := schema.GroupVersionResource{Group: "argoproj.io", Version: "v1alpha1", Resource: "rollouts"}
	patchData := []byte(`{"spec": {"replicas": 0}}`)
	_, err := c.dynamicClient.
	Resource(gvr).
	Namespace(namespace).
	Patch(context.TODO(), rolloutsName, types.MergePatchType, patchData, metav1.PatchOptions{})
	if err != nil {
		return err
	}
	return nil
}

5. main 函数启动

写好逻辑代码后,我们在 main 入口函数出引入,具体步骤如下:

  1. 先初始化配置文件

  2. 再执行 AutoSetZero 程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
	// 初始化配置文件
	cf.InitConfig()

	InitKubeConfig()
	clientset := GetClientSet()
	dynamicClient := GetDynamicClient()
	
	// 执行程序
	client := service.NewClients(clientset, dynamicClient)
	client.AutoSetZero()
}

代码编译

假设你本地已经装好了golang ,然后设置为 go module 模式

1. 环境配置

如果你是 windows 系统,需要交叉编译得先设置 go env -w GOOS=linux

1
2
3
4
5
6
7
go env -w CGO_ENABLED=0
go env -w GO111MODULE=on
go env -w GOARCH=amd64
go env -w GOOS=linux
go mod init auto-setzero
go mod tidy
go build -o auto_setzero --tags netgo -ldflags="-w -s"
注意
编译完后打成 Docker 镜像,推送到你的镜像仓,以供后面步骤使用(cicd 自行设计)。

2. 配置文件格式

根据 node 节点 pod 数量自动对节点设置污点,使其不再调度 pod 进入,pod 数量 maxmin 可以在 config.yml 里面设置,当 pod 大于 max 时,打污点,小于 min 时解除污点,例如:

1
2
3
4
5
errImagePull:
  namespaces: default
  hours: 168h
  status: ErrImagePull,ImagePullBackOff
  enable: true

部署到集群

在具有 kubernetes config 配置文件的机器上部署,默认读取当前用户 .kube/config 文件,如文件不在该路径,可以通过 --kubeconfig filepath 指定,如不想这么调度,可以创建具有权限的 sa 账号给程序使用

新建 yaml 文件 auto-setzero.ymlcronjob 时间和 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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: auto-setzero
  namespace: kube-system
spec:
  concurrencyPolicy: Allow
  failedJobsHistoryLimit: 1
  jobTemplate:
    metadata:
      creationTimestamp: null
      labels:
        app: auto-setzero
    spec:
      template:
        metadata:
          creationTimestamp: null
          labels:
            app: auto-setzero
        spec:
          containers:
          - command:
            - /opt/auto_setzero
            image: your-docker-image-repositry/auto_setzero:v1.0
            workingDir: /opt
            imagePullPolicy: Always
            name: auto-setzero
            resources:
              requests:
                cpu: 1
                memory: 500Mi                 
              limits:
                cpu: 1
                memory: 500Mi
            terminationMessagePath: /dev/termination-log
            terminationMessagePolicy: File
            volumeMounts:
            - mountPath: /root/.kube
              name: kubeconfig
              readOnly: true
            - mountPath: /etc/localtime
              name: time-name
            - mountPath: /opt/config.yml
              name: config
              subPath: config.yml
          dnsPolicy: ClusterFirst
          nodeSelector:
            node-role.kubernetes.io/master: ""
          tolerations:
          - key: "node-role.kubernetes.io/master"
            operator: "Exists"
            effect: "NoSchedule"
          restartPolicy: OnFailure
          schedulerName: default-scheduler
          securityContext: {}
          terminationGracePeriodSeconds: 30
          volumes:
          - hostPath:
              path: /root/.kube
              type: DirectoryOrCreate
            name: kubeconfig
          - hostPath:
              path: /usr/share/zoneinfo/Asia/Shanghai
              type: ""
            name: time-name
          - name: config
            configMap:
              name: auto-setzero-cm
              items:
              - key: config.yml
                path: config.yml
  schedule: "*/5 * * * *"
  successfulJobsHistoryLimit: 3
  suspend: false

配置 configmap:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apiVersion: v1
data:
  config.yml: |
    errImagePull:
      namespaces: default
      hours: 168h
      status: ErrImagePull,ImagePullBackOff
      enable: true    
kind: ConfigMap
metadata:
  name: auto-setzero-cm
  namespace: kube-system

最后执行:kubectl apply -f 配置文件 应用到集群。

至此,DynamicClient 实战到此分享结束,有兴趣的朋友可以自行尝试。