背景: 作为企业里唯一熟悉各种云产品的工种,通常需要和各种云产品打交道。当前,我们大部分的云基础设施和云服务都运行在阿里云上,而每个云产品都有独立的管理系统,这使得我们在运维过程中经常无法将相关产品和关联信息有效的组织在一起,来进行快速的问题诊断和信息查询,这对于运维和开发同学来说,在多个系统之间来回跳转查找关联信息是一个低效且极易出错的事务,因此通常来讲,不论是作为运维和开发,我们都希望将企业关联的云资源和服务进行整合关联,以实现效率的最大化。而在这过程中,我们采用Kubernetes集群的CronJob来定期获取阿里云的一些资源,在这过程中,遇到一些问题,根据问题重新细读CronJob官方文档,特记录于此。

CronJob简单介绍

一个CronJob对象就像是一个Linux环境的crontab文件一样,它会在给定的调度周期(crontab格式)内定期的创建一些job.

注意:所有的定时任务的调度周期都依赖于k8s的master节点的时区

通常情况下,CronJob对于创建定期和重复的任务非常有用,比如定期的备份和邮件发送之类的任务场景。

当然了,在Kubernetes集群中,Cronjob也有一些局限性和特性,需要详细了解下才能用的比较好。

注意: Cronjob控制器当前官方仍然是beta版本,也就意味着还是有一些问题存在的。

Cronjob的局限性

一个Cronjob会在它每执行一次调度就大概会创建一个Jobs对象。大概是因为有时候可能会有两个job被创建,或者没有任务创建。 官方实现中尝试去解决这种问题,但是目前仍然无法避免。因此在设计过程中,所有的Job都应该是幂等性的(idempotent)

如果startingDeadlineSeconds参数被设置为一个比较大的值,或者没有设置(默认),并且concurrencyPolicy设置为Allow,那么Job总是会运行至少一次。

对于每一个Cronjob来说,CronJob控制器会检查从上一次调度时间到现在的持续时间内它错过了多少个调度,如果错过调度100次,它将不再执行调度,并且会有如下相关异常.

1
Cannot determine if job needs to be started. Too many missed start time (> 100). Set or decrease .spec.startingDeadlineSeconds or check clock skew.

值得关注的是,如果设置了startingDeadlineSeconds参数(不为空),控制器统计错过的调度次数将不再是从最后一次调度时间,而是 从startingDeadlineSeconds的值到现在进行统计。比如,如果设置startingDeadlineSeconds:200,控制器会统计在最后200秒内错 过了的调度次数。

如果CronJob未能在预定时间创建,则该任务将被视为错过调度。比如,当设置concurrencyPolicy: Forbid时,当前一个任务还在运 行时CronJob尝试再次被调度,此时会被forbid掉,因此也会被记录为错过一次调度。

再比如,我们假设一个定时任务被设置在08:30:00后每一分钟执行一次,并且startingDeadlineSeconds参数没有被设置。如果CronJob控制器在08:29:0010:21:00之间故障了,Job将不会运行,因此错过调度的任务数量将远超过100。

为了更深层次说明这个问题,假设一个定时任务被设置在08:30:00开始每一分钟执行一次,并且startingDeadlineSeconds:200。如果CronJob控制器依然在相同时间段故障了,Job将会在10:22:00开始继续执行。 因为控制器仅会计算在过去的200秒内,错过调度的 次数有多少,因此仅会错过调度3次,远远小于100次,所有定时任务会在控制器恢复后继续调度,而不会影响正常的任务。

另外需要注意的是,CronJob仅负责调度和创建匹配的Jobs,而由Jobs真正去管理真正执行任务的Pods。

Cronjob的参数详情

  • spec.startingDeadlineSeconds: 表示统计错过调度次数(100次)的开始时间,默认从最后一次调度时间开始统计错过调度次数(超 过100不再调度)
  • spec.concurrencyPolicy: 并发调度策略,可选值:{“Allow”:“允许并发”,“Forbid”:“不允许”,“Replace”:“调度覆盖”}.

    • Allow: 注意:当设置为Allow时,需要考虑到任务执行时间和调度周期,因为可能上个任务没执行成功,下个任务就到执行时间了,如此下来可能会有很多任务都执行积压,造成资源误使用;
    • Replace: 当使用Replace遇到上述情况,后个任务会将前一个任务替换掉,如此以来所有的任务可能都不会完整执行;
    • Forbid: 则不允许并发调度,也即就调度一次,下一次调度周期再调度,但是可能由于任务执行过长,导致大部分的任务在每一 次调度时间都完美的错过了,此时startingDeadlineSeconds参数也并没有设置,就可能会出现该任务不会再调度,对应到k8s里的事 件可能是Cannot determine if job needs to be started: too many missed start time (> 100). Set or decrease .spec.startingDeadlineSeconds or check clock skew
  • spec.schedule: 调度周期,格式为标准的crontab格式[分 时 日 月 周]

  • spec.failedJobsHistoryLimit: 历史失败的任务数限制(通常可以保留1-2个,用于查看失败详情,以调整调度策略)

  • spec.successfulJobsHistoryLimit: 历史成功的任务数限制(可以自己决定保留多少个成功任务)

  • spec.jobTemplate: 标准的pod运行的模板(容器运行时的相关参数)

  • spec.suspend: 可选参数,如果设置为true,所有后续的任务都会被暂停执行,该参数不适用于已经运行的任务,默认为False

CronJob示例

 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
# 配置了一个定期去阿里云云解析获取解析详情的数据
$ cat dnsall-cronjob.yaml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  labels:
    run: dnsall
  name: dnsall
  namespace: myapp
spec:
  # 强烈建议设置并发策略,根据调度周期和任务特性进行设置
  concurrencyPolicy: Forbid
  # 强烈建议设置失败任务数,用于排查任务失败根因,以优化任务
  failedJobsHistoryLimit: 1
  successfulJobsHistoryLimit: 3
  # 强烈建议设置错过调度的计算时间
  startingDeadlineSeconds: 600
  # 调度周期
  schedule: '05,15,25,35,45,55 */1 * * *'
  suspend: false
  jobTemplate:
    metadata:
    spec:
      template:
        metadata:
          labels:
            run: dnsall
        spec:
          imagePullSecrets:
          - name: mydocker
          containers:
          - args:
            - -cmdbtype
            - dns
            image: harbor.bgbiao.top/cron-job:2019-12-04
            imagePullPolicy: Always
            name: dnsall
            resources: {}
            terminationMessagePath: /dev/termination-log
            terminationMessagePolicy: File
          dnsPolicy: ClusterFirst
          # 强烈建议设置任务的重启策略(任务的失败会触及到Jobs控制器中的Backofflimit参数,导致job失败)
          restartPolicy: OnFailure
          schedulerName: default-scheduler
          securityContext: {}
          terminationGracePeriodSeconds: 30

$ kubectl  get cronjob -n myapp
NAME             SCHEDULE                      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
dnsall           05,15,25,35,45,55 */1 * * *   False     0        8m41s           23h

# cronjob其实定期的创建了job,因此具体的任务pod其实是由job控制器来维护的
# 这里可以看到,我们上面的cronjob保存的三个执行成功的任务
$ kubectl  get jobs -n myapp  | grep dns
dnsall-1577597100           1/1           23s        22m
dnsall-1577597700           1/1           24s        12m
dnsall-1577598300           1/1           24s        2m22s

# 再查看一个job真正管理的pod任务的执行
# 任务已经已完成,所以任务的期望值为1,当前值为0
$ kubectl  get pods -n myapp | grep dnsall-1577598300
dnsall-1577598300-hdl4z           0/1     Completed   0          3m29s