このエントリーは 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は今後も追っていきたいと思います。