业务背景
公司有很多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
|
配置
新增执行器
这里的AppName 就是上面环境变量的XXL_JOB_NAME
如果没啥问题的话,执行器那里就能看到机器地址了
增加任务
这里运行模式选择BEAN
JobHandler
就是上面main.go
注册的 k8s.exec
任务参数:需要运行的命令,例如上面的php -v
实际上应该是你需要在业务容器内运行的命令
手动执行一次,能看到输出就说明没啥问题了
这样只用给需要定时任务的deploy
加上agent
,然后再去xxl-job里加上执行器,任务就可以了。这样不会再有多余的pod,每次定时任务都是在业务容器内去执行了。
github 代码
xxl-job/xxl-job-executor-go: xxl-job 执行器(golang 客户端) (github.com)
分布式任务调度平台XXL-JOB (xuxueli.com)