xxl-job sidecar 运行在K8s中

业务背景

公司有很多php的定时任务 以cronjob的方式运行在k8s集群内,大概有100多个,每一个任务运行都会创建一个job

使用kubectl get pods 可以看到许多已完成的pod, 这些pod 默认不会删除,是会占用资源的

实际上可以通过设置保留完成 Job 数 successfulJobsHistoryLimit: 0 来删除已完成的pod

但还是会有一批定时任务同时创建的问题,例如某个业务 每两分钟运行一个定时任务,每四分钟再运行一个定时任务。一旦同时在更新部署其他业务,会造成资源抢占,(阿里云集群每个节点只能运行110个pod)也会造成一定的资源浪费。

实际上,这两个定时任务可以在同一个pod内执行,并没有必要启动两个pod来运行。

解决方案

首先想到的是使用xxl-job 或类似的定时任务调度中心来完成,但php接入xxl-job 又比较麻烦,对历史项目进行改造的成本很高。

一番思索加上google了一下,看到了阿里云的解决方案Sidecar方式接入SchedulerX (aliyun.com),好像很适合目前的项目。

很nice,再github上找找,选择了golang版本的xxl-job/xxl-job-executor-go: xxl-job 执行器(golang 客户端) (github.com)

golang更简单,打包出来占用内存

代码

执行器代码比较少,可以参考xxl-job-executor-go/example at master · xxl-job/xxl-job-executor-go (github.com)

这里我只写了一个k8s_exec.go

  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
package task

import (
	"context"
	"fmt"
	"github.com/xxl-job/xxl-job-executor-go"
	v1 "k8s.io/api/core/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/tools/remotecommand"
	"log"
	"os"
	"strconv"
)

var (
	client    *kubernetes.Clientset
	config    *rest.Config
	err       error
	namespace string
	podName   string
)

func init() {

	// 是否在集群内运行?
	inCluster, _ := strconv.ParseBool(os.GetEnv("IN_CLUSTER"))

	if inCluster {
		// 使用集群内配置
		config, err = rest.InClusterConfig()
	} else {
		// 默认使用本机的~/.kube/conf 配置
		config, err = clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
	}

	if err != nil {
		panic(err.Error())
	}

	client, err = kubernetes.NewForConfig(config)

	if err != nil {
		panic(err.Error())
	}

	namespace = os.Getenv("NAMESPACE")
	podName = os.Getenv("POD_NAME")
}

func K8s_exec(cxt context.Context, param *xxl.RunReq) string {
	logger := log.New(os.Stdout, fmt.Sprintf("XXL-AGENT-K8S-EXEC [%d]", param.LogID), 0)

	logger.Println("开始执行任务")

	// 执行命令
	cmd := []string{
		"sh",
		"-c",
		param.ExecutorParams,
	}

	// 构建请求 通过namespace  podName  containerName 找到对应的业务容器
	req := client.CoreV1().RESTClient().Post().Resource("pods").Name(podName).
		Namespace(namespace).SubResource("exec").Param("container", os.Getenv("CONTAINER_NAME"))

	option := &v1.PodExecOptions{
		Command: cmd,
		Stdin:   true,
		Stdout:  true,
		Stderr:  true,
		TTY:     false,
	}

	req.VersionedParams(
		option,
		scheme.ParameterCodec,
	)
	exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())

	if err != nil {
		logger.Panic(err)
	}

	err = exec.Stream(remotecommand.StreamOptions{
		Stdin:  os.Stdin,
		Stdout: os.Stdout,
		Stderr: os.Stderr,
		Tty:    false,
	})

	if err != nil {
		logger.Panic(err)
	}

	logger.Println("任务执行完毕")
	return "executed"
}

其实就是通过namespace podName containerName 找到对应的业务容器(需要执行命令的容器),通过exec的方式执行对应的命令

然后在main.go注册一下

1
2
3
...
	exec.RegTask("task.panic", task.Panic)
...

部署

注意这里,需要给ServiceAccount 添加对应的pods/exec权限

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: xxl-job-agent-exec
rules:
  - apiGroups:
      - ""
    resources:
      - 'pods/exec'
    verbs:
      - create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: xxl-job-agent-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: xxl-job-agent-exec
subjects:
  - kind: ServiceAccount
    name: default
    namespace: default

然后弄个项目测试一下

 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
kind: Deployment
apiVersion: apps/v1
metadata:
  name: xxl-job-test
  namespace: contract
  labels:
    app: xxl-job-test
    app.auth.matrix.io/id: contract
    app.kubernetes.io/name: contract
    app.kubernetes.io/version: v1
    version: v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: xxl-job-test
  template:
    metadata:
      labels:
        app: xxl-job-test
        app.auth.matrix.io/id: contract
        app.kubernetes.io/name: contract
        app.kubernetes.io/version: v1
        version: v1
    spec:
      containers:
        - name: business-api
          image: 'yourDockerImage:lastest'
          ports:
            - name: http-80
              containerPort: 80
              protocol: TCP
          resources: {}
          imagePullPolicy: Always
        - name: xxl-job-agent
          image: 'xxl-job-agent:test'
          ports:
            - name: http
              containerPort: 9999
              protocol: TCP
          env:
            - name: XXL_JOB_ADDR
              value: 'http://xxl-job.company.svc.cluster.local:8080/xxl-job-admin'
            - name: XXL_JOB_NAME
              value: contract-job
            - name: CONTAINER_NAME
              value: business-api
            - name: NAMESPACE
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: metadata.namespace
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: metadata.name

这里的deploy 中有两个container 其中 business-api是业务容器,xxl-job-agent就是我们的执行器

这里在执行器里需要配置一些环境变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
           - name: XXL_JOB_ADDR  # xxl-job 集群内部地址 按需配置
              value: 'http://xxl-job.company.svc.cluster.local:8080/xxl-job-admin'
            - name: XXL_JOB_NAME # 执行器的名称  
              value: busiess-job
            - name: CONTAINER_NAME # 业务容器的名称
              value: business-api
            - name: NAMESPACE # 命名空间
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: metadata.namespace
            - name: POD_NAME # podName
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: metadata.name

配置

新增执行器

image.png

这里的AppName 就是上面环境变量的XXL_JOB_NAME

如果没啥问题的话,执行器那里就能看到机器地址了

增加任务

image.png

这里运行模式选择BEAN

JobHandler 就是上面main.go注册的 k8s.exec

任务参数:需要运行的命令,例如上面的php -v 实际上应该是你需要在业务容器内运行的命令

手动执行一次,能看到输出就说明没啥问题了 image.png

这样只用给需要定时任务的deploy加上agent ,然后再去xxl-job里加上执行器,任务就可以了。这样不会再有多余的pod,每次定时任务都是在业务容器内去执行了。

github 代码

xxl-job/xxl-job-executor-go: xxl-job 执行器(golang 客户端) (github.com)

分布式任务调度平台XXL-JOB (xuxueli.com)

Built with Hugo
主题 StackJimmy 设计