LoginSignup
50
30

More than 5 years have passed since last update.

CronJob(ScheduledJob)

Last updated at Posted at 2016-12-03

このエントリーは Kubernetes Advent Calendar 2016の4日目の記事です。
CronJobs について、キャッチアップしたことを書いて行きたいと思います。

from. http://kubernetes.io/docs/user-guide/cron-jobs/

CronJob(ScheduledJob) とは?

時間ベースのジョブを管理するもの。
所謂Cronみたいに特定の一回だったり、繰り返し実行することができるものです。

ユースケース

  • メール送信
  • バッチ処理
  • 定期的に解析
  • 夜間にバックアップ処理

Kubernetes のバージョンによって名前が違う

  • 1.4 Scheduledjob
  • 1.5 CronJob

利用するための前提条件

You need a working Kubernetes cluster at version >= 1.4 (for ScheduledJob), >= 1.5 (for CronJobs), with batch/v2alpha1 API turned on by passing --runtime-config=batch/v2alpha1 while bringing up the API server (see Turn on or off an API version for your cluster for more). You cannot use Cron Jobs on a hosted Kubernetes provider that has disabled alpha resources.

バージョンで名前が違うのは前述とおりです。
ここでは更にkubernetes の API Server を立ち上げる際に --runtime-config=batch/v2alpha1 を付けることが必要です。

  • 例: launch-k8s-on-aws.sh
#!/bin/bash

# Turned on v2aplpha1

export KUBE_RUNTIME_CONFIG="batch/v2alpha1=true"

# Target aws config

export KUBERNETES_PROVIDER=aws
export AWS_DEFAULT_PROFILE=default

export KUBE_AWS_ZONE=ap-northeast-1c
export AWS_S3_REGION=ap-northeast-1

export NODE_SIZE=c4.large
export MASTER_SIZE=c4.large
export AWS_S3_BUCKET=kubernetes-145

# Kubernetes config

export NUM_NODES=5
export KUBE_AWS_INSTANCE_PREFIX=k8s-145

# Disk size

export MASTER_DISK_SIZE=50
export MASTER_ROOT_DISK_SIZE=50
export NODE_ROOT_DISK_SIZE=50

# Download Kubernetes and automatically set up a default cluster

wget -q -O - https://get.k8s.io | bash

実際に manifest file を書いて実行してみよう

  • hello world 的な物
apiVersion: batch/v2alpha1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "*/1 * * * *"
  jobTemplate:
    spec:
      activeDeadlineSeconds: 10
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            args:
            - /bin/sh
            - -c
            - date; echo Hello from the Kubernetes cluster
          restartPolicy: OnFailure
  • cronjob 作成
$ kubectl create -f examples/cronjob.yaml 
cronjob "hello" created
  • cronjob 確認
$ kubectl get cronjob hello
NAME      SCHEDULE      SUSPEND   ACTIVE    LAST-SCHEDULE
hello     */1 * * * *   False     0         Mon, 29 Aug 2016 14:34:00 -0700
  • job 確認
$ kubectl get job
NAME                         DESIRED   SUCCESSFUL   AGE
hello-1705740100             1         1            32d
  • kubectl describe job するとさらに情報がわかる。
$ kubectl describe job hello-1705740100 
Name:       hello-1705740100
Namespace:  default
Image(s):   busybox
Selector:   controller-uid=93a273e1-9fdf-11e6-b40b-06daaf3c2469
Parallelism:    1
Completions:    1
Start Time: Tue, 01 Nov 2016 12:02:08 +0900
Labels:     <none>
Pods Statuses:  0 Running / 1 Succeeded / 0 Failed
No volumes.
No events.
  • Selector がわかるので、 Pod がわかる
$ kubectl get po -l controller-uid=93a273e1-9fdf-11e6-b40b-06daaf3c2469 --show-all
NAME                               READY     STATUS    RESTARTS   AGE
job-task-latest-1553958718-mg5r8   0/1       Error     0          45s
job-task-latest-1553958718-r65iv   0/1       Error     0          41s
  • cronjob 削除
$ kubectl delete cronjob hello
cronjob "hello" deleted
  • job 削除
$ kubectl get jobs
NAME               DESIRED   SUCCESSFUL   AGE
hello-1201907962   1         1            11m
hello-1202039034   1         1            8m
...

$ kubectl delete jobs hello-1201907962 hello-1202039034 ...
job "hello-1201907962" deleted
job "hello-1202039034" deleted
...

ref. https://github.com/kubernetes/kubernetes/blob/master/docs/proposals/scheduledjob.md

オプション

Cronjob

  • .spec.schedule: cron format でスケジュールを指定する
  • .spec.jobTemplate: Job を定義する
  • .spec.startingDeadlineSeconds: CronJob で何かしら実行されなかった時にいつまで実行可能か期限を決める。
  • .spec.concurrencyPolicy: 並列して実行するポリシーを決めるもの。前の job と被って実行されてもいいのか気を付ける。
    • Allow(default): 並列して実行を許可
    • Forbid: 前回がまだ終了していない場合、次回の実行をスキップする
    • Replace: 現在実行中のジョブをキャンセルし、新しいジョブに置き換えます
  • .spec.suspend: trueに設定すると、次回以降に中断され実行されない。既に開始されているものはそのまま実行される。

job

  • .spec.jobTemplate.spec.activeDeadlineSeconds: 失敗する際にデッドラインを決める(デフォルトは無期限だから失敗するコンテナを書くと、ずっとrestartされる)

job の考え方

ref. Handling Pod and Container Failures

A Container in a Pod may fail for a number of reasons, such as because the process in it exited with a non-zero exit code, or the Container was killed for exceeding a memory limit, etc. If this happens, and the .spec.template.containers[].restartPolicy = "OnFailure", then the Pod stays on the node, but the Container is re-run. Therefore, your program needs to handle the case when it is restarted locally, or else specify .spec.template.containers[].restartPolicy = "Never". See pods-states for more information on restartPolicy.

An entire Pod can also fail, for a number of reasons, such as when the pod is kicked off the node (node is upgraded, rebooted, deleted, etc.), or if a container of the Pod fails and the .spec.template.containers[].restartPolicy = "Never". When a Pod fails, then the Job controller starts a new Pod. Therefore, your program needs to handle the case when it is restarted in a new pod. In particular, it needs to handle temporary files, locks, incomplete output and the like caused by previous runs.

Note that even if you specify .spec.parallelism = 1 and .spec.completions = 1 and .spec.template.containers[].restartPolicy = "Never", the same program may sometimes be started twice.

If you do specify .spec.parallelism and .spec.completions both greater than 1, then there may be multiple pods running at once. Therefore, your pods must also be tolerant of concurrency.

  • 基本的に、Jobは何回実行しても結果が同じになることを期待している設計になっている。
  • .spec.template.containers[].restartPolicy = "Never" はあくまでも Pod 内の話であり、Job 事態が完了していないのであれば、新しく Pod を作ってプログラムを実行する
  • .spec.parallelism = 1.spec.completions = 1.spec.template.containers[].restartPolicy = "Never" を指定しても、同じプログラムが2回起動される可能性がある

こんな使い方が出来る

kubectl をクラスタ内部で実行

例えば kubectl set コマンドを使ってコンテナのイメージを替える

apiVersion: batch/v2alpha1
kind: CronJob
metadata:
  name: replace-task-latest
  labels:
    name: app-server-scheduledjob
    role: kubectl
  namespace: app-server
spec:
  schedule: "0 2,6,10,14,18,22 * * *" # every 4 hours (shifted by 2 hours)
  startingDeadlineSeconds: 30
  concurrencyPolicy: "Forbid"
  suspend: false
  jobTemplate:
    metadata:
      name: app-server-scheduledjob
      labels:
        name: app-server-scheduledjob
        role: kubectl
    spec:
      template:
        spec:
          containers:
          - name: app-server-scheduledjob
            image: koudaiii/kubectl
            command:
              - "sh"
              - "-c"
              - >
                echo "kubectl set image deployment/app-server app-server=app-server:latest --namespace=app-server --record";
                kubectl set image deployment/app-server app-server=app-server:latest --namespace=app-server --record
          restartPolicy: Never

manifest file を更新する

  • kubernetes/app-server.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  template:
    metadata:
      name: pi
    spec:
      containers:
      - name: pi
        image: perl
        command: ["perl",  "-Mbignum=bpi", "-wle", "print bpi(2000)"]
      restartPolicy: Never
  • 書いた manifest file を configmap に置く
kubectl create configmap app-server --from-file=kubernetes/app-server.yaml
  • 定期的に実行させる
apiVersion: batch/v2alpha1
kind: CronJob
metadata:
  name: job-task-latest
  labels:
    name: app-server-scheduledjob
    role: kubectl
spec:
  schedule: "*/5 * * * *"
  startingDeadlineSeconds: 30
  concurrencyPolicy: "Forbid"
  suspend: false
  jobTemplate:
    metadata:
      name: app-server-scheduledjob
      labels:
        name: app-server-scheduledjob
        role: kubectl
    spec:
      template:
        spec:
          containers:
          - name: app-server-scheduledjob
            image: koudaiii/kubectl
            command: ["kubectl","replace","-f","tmp/app-server.yaml"]
            volumeMounts:
              - name: deployment
                mountPath: /tmp
                readOnly: true
          restartPolicy: Never
          volumes:
            - name: deployment
              configMap:
                name: app-server

気をつける点

ここからは、実際に使ってみてハマったものをまとめました。

実行はあくまでも UTC?

NAME                                 SCHEDULE     SUSPEND   ACTIVE    LAST-SCHEDULE
people--ios-event-appsflyer-screen   0 16 * * *   False     0         Sat, 03 Dec 2016 01:00:00 +0900

LASTをみると日本時間で表示されるが Scheduler が実行されるのはUTCになってる。

もしかすると、 get.k8s.io で作る場合はUTCなのかもしれない。get.k8s.ioで特に指定するところが GitHub の kubernetes/cluster/* からはなく、ここは自分で Cluster を作ってみないとわからない感じです。

二回目の kubectl apply で失敗する

# scheduledjob.yaml
# From http://kubernetes.io/docs/user-guide/scheduled-jobs/#creating-a-scheduled-job
apiVersion: batch/v2alpha1
kind: ScheduledJob
metadata:
  name: hello
spec:
  schedule: 0/1 * * * ?
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            args:
            - /bin/sh
            - -c
            - date; echo Hello from the Kubernetes cluster
          restartPolicy: OnFailure
  • 1回目は成功
$ kubectl apply -f scheduledjob.yaml 
scheduledjob "hello" created
  • 2回目に失敗する。
$ kubectl apply -f scheduledjob.yaml
error: error when applying patch:

to:
&{0xc8200bd740 0xc8202f8ee0 sandbox hello manifests.yaml XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Travis CI 経由で Job をまとめて kubectl apply -f ./dir/ とかやろうとした際にこの不具合にあたった。

Job が already exists で時々実行されない

Error creating job: jobs.batch "es-cleanup-1629652801" already exists

getTimeHash の実装が原因で hourly とか設定すると時々名前が被ってJobが失敗する
getTimeHash(scheduledTime)

Date Epoch Hash Hits
Nov 14 00:00:00 1479081600 4175643772 1
Nov 14 00:05:00 1479081900 4209853567 1
Nov 14 00:10:00 1479082200 4141499513 1
Nov 14 00:15:00 1479082500 4175709308 1
Nov 14 00:20:00 1479082800 4209919103 1
Nov 14 00:25:00 1479083100 4244128898 1
Nov 14 00:30:00 1479083400 4072621171 1
Nov 14 00:35:00 1479083700 4106830966 1
Nov 14 00:40:00 1479084000 4141040761 1
Nov 14 00:45:00 1479084300 4072686707 1
Nov 14 00:50:00 1479084600 4106896502 1
Nov 14 00:55:00 1479084900 4141106297 1
Nov 14 01:00:00 1479085200 4072752243 1
Nov 14 01:05:00 1479085500 4106962038 1
Nov 14 01:10:00 1479085800 4141171833 1
Nov 14 01:15:00 1479086100 4175381628 1
Nov 14 01:20:00 1479086400 4107027574 1
Nov 14 01:25:00 1479086700 4141237369 1
Nov 14 01:30:00 1479087000 4175447164 1
Nov 14 01:35:00 1479087300 4107093110 1
Nov 14 01:40:00 1479087600 4141302905 1
Nov 14 01:45:00 1479087900 4175512700 1
Nov 14 01:50:00 1479088200 4107158646 1
Nov 14 01:55:00 1479088500 4141368441 1
Nov 14 02:00:00 1479088800 4175578236 1
Nov 14 02:05:00 1479089100 4209788031 1
Nov 14 02:10:00 1479089400 4141433977 1
Nov 14 02:15:00 1479089700 4175643772 2
Nov 14 02:20:00 1479090000 4209853567 2
Nov 14 02:25:00 1479090300 4141499513 2
    // We want job names for a given nominal start time to have a deterministic name to avoid the same job being created twice
    name := fmt.Sprintf("%s-%d", sj.Name, getTimeHash(scheduledTime))

Unix Epoch Time を返すように変更されている。次のリリースで治りそう。

まとめ

実際に使ってみて、クラスタリング上で job が実行されるメリットは大きいです。

スケジュールを実行するため専用のサーバーを作ったりしていると思いますが、その必要がなくなりより効率的に全体のリソースを使えるようになるからです。

アプリケーションエンジニアの技術選定の幅がより広がるので、Cronjobは今後も追っていきたいと思います。

50
30
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50
30