حالت تاریک
فصل ۱۲ — Jobها
تا اینجا روی فرایندهای long-running مثل databaseها و web applicationها تمرکز کردیم. این نوع workloadها تا زمانی که upgrade شوند یا دیگر به سرویس نیاز نباشد اجرا میمانند. اگرچه فرایندهای long-running بخش عمده workloadهایی هستند که روی یک cluster Kubernetes اجرا میشوند، اغلب نیاز به اجرای taskهای کوتاهمدت و one-off هم هست. شیء Job برای مدیریت این نوع taskها ساخته شده است.
یک Job، Podهایی میسازد که تا termination موفق (یعنی exit با کد ۰) اجرا میشوند. در مقابل، یک Pod معمولی بدون توجه به exit code مداوم restart میشود. Jobها برای کارهایی که فقط یکبار میخواهید انجام دهید مفیدند، مثل database migration یا batch job. اگر بهصورت Pod معمولی اجرا شود، task migration پایگاه داده در یک loop اجرا میشود و بعد از هر exit مداوم پایگاه داده را دوباره populate میکند.
در این فصل رایجترین الگوهای Job که Kubernetes فراهم میکند را بررسی میکنیم. این الگوها را در سناریوهای واقعی هم به کار میگیریم.
شیء Job
شیء Job مسئول ساخت و مدیریت Podهایی است که در template داخل specification Job تعریف شدهاند. این Podها عموماً تا completion موفق اجرا میشوند. شیء Job هماهنگی اجرای تعدادی Pod بهصورت موازی را مدیریت میکند.
اگر Pod قبل از termination موفق fail کند، job controller بر اساس Pod template در specification Job یک Pod جدید میسازد. از آنجا که Podها باید schedule شوند، احتمال دارد Job اجرا نشود اگر scheduler منابع لازم را پیدا نکند. همچنین بهخاطر ماهیت سیستمهای توزیعشده، در برخی سناریوهای failure احتمال کمی وجود دارد که Podهای duplicate برای یک task ساخته شوند.
الگوهای Job
Jobها برای مدیریت workloadهای batch-like طراحی شدهاند که work itemها توسط یک یا چند Pod پردازش میشوند. بهطور پیشفرض هر Job یک Pod را یکبار تا termination موفق اجرا میکند. این الگوی Job با دو attribute اصلی Job تعریف میشود: تعداد completionهای Job و تعداد Podهایی که بهصورت موازی اجرا میشوند. در الگوی «یکبار تا completion»، پارامترهای completions و parallelism روی ۱ تنظیم میشوند.
جدول ۱۲-۱ الگوهای Job را بر اساس ترکیب completions و parallelism در پیکربندی Job نشان میدهد.
| نوع | Use case | رفتار | completions | parallelism |
|---|---|---|---|---|
| One shot | Database migrationها | یک Pod که یکبار تا termination موفق اجرا میشود | 1 | 1 |
| Parallel fixed completions | چند Pod که مجموعهای از کار را بهصورت موازی پردازش میکنند | یک یا چند Pod که یک یا چند بار اجرا میشوند تا به تعداد completion ثابت برسند | 1+ | 1+ |
| Work queue: parallel jobs | چند Pod که از یک work queue متمرکز پردازش میکنند | یک یا چند Pod که یکبار تا termination موفق اجرا میشوند | 1 | 2+ |
One Shot
Jobهای one-shot راهی برای اجرای یک Pod یکبار تا termination موفق فراهم میکنند. اگرچه ممکن است ساده به نظر برسد، کارهایی برای انجام درست آن لازم است. اول باید Pod ساخته و به Kubernetes API submit شود. این با Pod template تعریفشده در پیکربندی Job انجام میشود. وقتی Job up and running است، Pod پشتیبان Job برای termination موفق monitor میشود. Job به دلایل مختلفی میتواند fail کند، از جمله خطای application، exception uncaught در runtime، یا failure node قبل از اینکه Job فرصت complete شدن داشته باشد. در همه موارد، job controller مسئول recreate کردن Pod تا occurrence termination موفق است.
راههای متعددی برای ساخت one-shot Job در Kubernetes وجود دارد. سادهترین استفاده از ابزار خط فرمان kubectl است:
$ kubectl run -i oneshot \
--image=gcr.io/kuar-demo/kuard-amd64:blue \
--restart=OnFailure \
-- --keygen-enable \
--keygen-exit-on-complete \
--keygen-num-to-gen 10
...
(ID 0) Workload starting
(ID 0 1/10) Item done: SHA256:nAsUsG54XoKRkJwyN+OShkUPKew3mwq7OCc
(ID 0 2/10) Item done: SHA256:HVKX1ANns6SgF/er1lyo+ZCdnB8geFGt0/8
(ID 0 3/10) Item done: SHA256:irjCLRov3mTT0P0JfsvUyhKRQ1TdGR8H1jg
(ID 0 4/10) Item done: SHA256:nbQAIVY/yrhmEGk3Ui2sAHuxb/o6mYO0qRk
(ID 0 5/10) Item done: SHA256:CCpBoXNlXOMQvR2v38yqimXGAa/w2Tym+aI
(ID 0 6/10) Item done: SHA256:wEY2TTIDz4ATjcr1iimxavCzZzNjRmbOQp8
(ID 0 7/10) Item done: SHA256:t3JSrCt7sQweBgqG5CrbMoBulwk4lfDWiTI
(ID 0 8/10) Item done: SHA256:E84/Vze7KKyjCh9OZh02MkXJGoty9PhaCec
(ID 0 9/10) Item done: SHA256:UOmYex79qqbI1MhcIfG4hDnGKonlsij2k3s
(ID 0 10/10) Item done: SHA256:WCR8wIGOFag84Bsa8f/9QHuKqF+0mEnCADY
(ID 0) Workload exitingنکاتی که باید توجه کنید:
- گزینه
-iدر kubectl نشان میدهد این دستور interactive است. kubectl تا وقتی Job running باشد منتظر میماند و سپس log output از اولین (و در این مورد تنها) Pod در Job را نشان میدهد. --restart=OnFailureگزینهای است که به kubectl میگوید یک شیء Job بسازد.- همه گزینهها بعد از
--آرگومانهای خط فرمان به container image هستند. اینها به test server ما (kuard) میگویند ۱۰ کلید SSH ۴٬۰۹۶-bit تولید کند و سپس exit کند. - خروجی شما ممکن است دقیقاً مطابق این نباشد. kubectl اغلب با گزینه
-iچند خط اول output را از دست میدهد.
بعد از completion Job، شیء Job و Pod مرتبط هنوز باقی میمانند تا بتوانید log output را inspect کنید. توجه کنید این Job در kubectl get jobs دیده نمیشود مگر flag -a بدهید. بدون این flag، kubectl Jobهای completed را مخفی میکند. قبل از ادامه Job را حذف کنید:
$ kubectl delete jobs oneshotگزینه دیگر برای ساخت one-shot Job استفاده از فایل پیکربندی است، همانطور که در مثال ۱۲-۱ نشان داده شده.
مثال ۱۲-۱. job-oneshot.yaml
yaml
apiVersion: batch/v1
kind: Job
metadata:
name: oneshot
spec:
template:
spec:
containers:
- name: kuard
image: gcr.io/kuar-demo/kuard-amd64:blue
imagePullPolicy: Always
args:
- "--keygen-enable"
- "--keygen-exit-on-complete"
- "--keygen-num-to-gen=10"
restartPolicy: OnFailureJob را با دستور kubectl apply submit کنید:
$ kubectl apply -f job-oneshot.yaml
job "oneshot" createdسپس Job oneshot را describe کنید:
$ kubectl describe jobs oneshot
Name: oneshot
Namespace: default
Image(s): gcr.io/kuar-demo/kuard-amd64:blue
Selector: controller-uid=cf87484b-e664-11e6-8222-42010a8a007b
Parallelism: 1
Completions: 1
Start Time: Sun, 29 Jan 2017 12:52:13 -0800
Labels: Job=oneshot
Pods Statuses: 0 Running / 1 Succeeded / 0 Failed
No volumes.
Events:
... Reason Message
... ------ -------
... SuccessfulCreate Created pod: oneshot-4kfdtنتایج Job را با نگاه به logهای Pod ساختهشده میبینید:
$ kubectl logs oneshot-4kfdt
...
Serving on :8080
(ID 0) Workload starting
(ID 0 1/10) Item done: SHA256:+r6b4W81DbEjxMcD3LHjU+EIGnLEzbpxITKn8IqhkPI
(ID 0 2/10) Item done: SHA256:mzHewajaY1KA8VluSLOnNMk9fDE5zdn7vvBS5Ne8AxM
(ID 0 3/10) Item done: SHA256:TRtEQHfflJmwkqnNyGgQm/IvXNykSBIg8c03h0g3onE
(ID 0 4/10) Item done: SHA256:tSwPYH/J347il/mgqTxRRdeZcOazEtgZlA8A3/HWbro
(ID 0 5/10) Item done: SHA256:IP8XtguJ6GbWwLHqjKecVfdS96B17nnO21I/TNc1j9k
(ID 0 6/10) Item done: SHA256:ZfNxdQvuST/6ZzEVkyxdRG98p73c/5TM99SEbPeRWfc
(ID 0 7/10) Item done: SHA256:tH+CNl/IUl/HUuKdMsq2XEmDQ8oAvmhMO6Iwj8ZEOj0
(ID 0 8/10) Item done: SHA256:3GfsUaALVEHQcGNLBOu4Qd1zqqqJ8j738i5r+I5XwVI
(ID 0 9/10) Item done: SHA256:5wV4L/xEiHSJXwLUT2fHf0SCKM2g3XH3sVtNbgskCXw
(ID 0 10/10) Item done: SHA256:bPqqOonwSbjzLqe9ZuVRmZkz+DBjaNTZ9HwmQhbdWLI
(ID 0) Workload exitingتبریک، Job شما با موفقیت اجرا شد!
ممکن است متوجه شده باشید هنگام ساخت شیء Job هیچ labelی مشخص نکردیم. مثل controllerهای دیگر (DaemonSetها، ReplicaSetها، deploymentها و غیره) که از label برای شناسایی مجموعهای از Podها استفاده میکنند، اگر Pod در چند object reuse شود رفتارهای غیرمنتظره رخ میدهد. چون Jobها شروع و پایان محدود دارند، رایج است کاربران زیادی از آنها بسازند. این انتخاب labelهای یکتا را سختتر و حیاتیتر میکند. به همین دلیل، شیء Job بهطور خودکار یک label یکتا انتخاب میکند و از آن برای شناسایی Podهایی که میسازد استفاده میکند. در سناریوهای پیشرفته (مثل جایگزینی Job در حال اجرا بدون kill کردن Podهای تحت مدیریتش)، کاربران میتوانند این رفتار خودکار را خاموش کنند و labelها و selectorها را دستی مشخص کنند.
Pod failure
تازه دیدیم Job چطور با موفقیت complete میشود. اما اگر چیزی fail شود چه میشود؟ بیایید امتحان کنیم.
آرگومانهای kuard را در فایل پیکربندی تغییر دهید تا بعد از تولید سه کلید با exit code غیرصفر fail کند، همانطور که در مثال ۱۲-۲ نشان داده شده.
مثال ۱۲-۲. job-oneshot-failure1.yaml
yaml
...
spec:
template:
spec:
containers:
...
args:
- "--keygen-enable"
- "--keygen-exit-on-complete"
- "--keygen-exit-code=1"
- "--keygen-num-to-gen=3"
...حالا با kubectl apply -f job-oneshot-failure1.yaml اجرا کنید. کمی بگذارید run شود و سپس وضعیت Pod را ببینید:
$ kubectl get pod -a -l job-name=oneshot
NAME READY STATUS RESTARTS AGE
oneshot-3ddk0 0/1 CrashLoopBackOff 4 3mاینجا میبینیم همان Pod چهار بار restart شده. Kubernetes برای این Pod در CrashLoopBackOff است. غیرمعمول نیست باگی جایی باشد که برنامه به محض start crash کند. در آن صورت Kubernetes کمی صبر میکند قبل از restart Pod تا crash loop منابع node را نخورد. همه اینها local به node توسط kubelet بدون دخالت Job مدیریت میشود.
Job را kill کنید (kubectl delete jobs oneshot) و چیز دیگری امتحان کنیم. دوباره فایل config را تغییر دهید و restartPolicy را از OnFailure به Never. با kubectl apply -f jobs-oneshot-failure2.yaml اجرا کنید.
اگر کمی بگذارید run شود و Podهای مرتبط را ببینید، چیز جالبی پیدا میکنید:
$ kubectl get pod -l job-name=oneshot -a
NAME READY STATUS RESTARTS AGE
oneshot-0wm49 0/1 Error 0 1m
oneshot-6h9s2 0/1 Error 0 39s
oneshot-hkzw0 1/1 Running 0 6s
oneshot-k5swz 0/1 Error 0 28s
oneshot-m1rdw 0/1 Error 0 19s
oneshot-x157b 0/1 Error 0 57sمیبینیم چند Pod اینجا error دادهاند. با restartPolicy: Never به kubelet میگوییم Pod را در failure restart نکند، بلکه Pod را failed اعلام کند. سپس شیء Job متوجه میشود و Pod جایگزین میسازد. اگر دقت نکنید، «junk» زیادی در cluster ایجاد میشود. به همین دلیل پیشنهاد میکنیم restartPolicy: OnFailure استفاده کنید تا Podهای failed در همان جا دوباره run شوند.
با kubectl delete jobs oneshot پاک کنید.
تا اینجا دیدیم برنامه با exit با exit code غیرصفر fail میکند. اما workerها راههای دیگر هم fail میکنند؛ مثلاً stuck میشوند و پیشرفت نمیکنند. برای پوشش این حالت میتوانید liveness probe با Jobها استفاده کنید. اگر policy liveness probe تشخیص دهد Pod مرده است، برایتان restart/replace میشود.
Parallelism
تولید کلید میتواند کند باشد. بیایید تعداد زیادی worker با هم start کنیم تا تولید کلید سریعتر شود. از ترکیب پارامترهای completions و parallelism استفاده میکنیم. هدف ما تولید ۱۰۰ کلید با ۱۰ run از kuard است که هر run ۱۰ کلید تولید کند. اما نمیخواهیم cluster را غرق کنیم، پس خود را به پنج Pod همزمان محدود میکنیم.
این یعنی completions روی ۱۰ و parallelism روی ۵. پیکربندی در مثال ۱۲-۳ نشان داده شده.
مثال ۱۲-۳. job-parallel.yaml
yaml
apiVersion: batch/v1
kind: Job
metadata:
name: parallel
labels:
chapter: jobs
spec:
parallelism: 5
completions: 10
template:
metadata:
labels:
chapter: jobs
spec:
containers:
- name: kuard
image: gcr.io/kuar-demo/kuard-amd64:blue
imagePullPolicy: Always
args:
- "--keygen-enable"
- "--keygen-exit-on-complete"
- "--keygen-num-to-gen=10"
restartPolicy: OnFailurestart کنید:
$ kubectl apply -f job-parallel.yaml
job "parallel" createdحالا ببینید Podها up میشوند، کارشان را انجام میدهند و exit میکنند. Podهای جدید تا وقتی ۱۰ تا altogether complete شوند ساخته میشوند. اینجا flag --watch را استفاده میکنیم تا kubectl بماند و تغییرات را list کند:
$ kubectl get pods -w
NAME READY STATUS RESTARTS AGE
parallel-55tlv 1/1 Running 0 5s
parallel-5s7s9 1/1 Running 0 5s
parallel-jp7bj 1/1 Running 0 5s
parallel-lssmn 1/1 Running 0 5s
parallel-qxcxp 1/1 Running 0 5s
NAME READY STATUS RESTARTS AGE
parallel-jp7bj 0/1 Completed 0 26s
parallel-tzp9n 0/1 Pending 0 0s
parallel-tzp9n 0/1 Pending 0 0s
parallel-tzp9n 0/1 ContainerCreating 0 1s
parallel-tzp9n 1/1 Running 0 1s
parallel-tzp9n 0/1 Completed 0 48s
parallel-x1kmr 0/1 Pending 0 0s
parallel-x1kmr 0/1 Pending 0 0s
parallel-x1kmr 0/1 ContainerCreating 0 0s
parallel-x1kmr 1/1 Running 0 1s
parallel-5s7s9 0/1 Completed 0 1m
parallel-tprfj 0/1 Pending 0 0s
parallel-tprfj 0/1 Pending 0 0s
parallel-tprfj 0/1 ContainerCreating 0 0s
parallel-tprfj 1/1 Running 0 2s
parallel-x1kmr 0/1 Completed 0 52s
parallel-bgvz5 0/1 Pending 0 0s
parallel-bgvz5 0/1 Pending 0 0s
parallel-bgvz5 0/1 ContainerCreating 0 0s
parallel-bgvz5 1/1 Running 0 2s
parallel-qxcxp 0/1 Completed 0 2m
parallel-xplw2 0/1 Pending 0 1s
parallel-xplw2 0/1 Pending 0 1s
parallel-xplw2 0/1 ContainerCreating 0 1s
parallel-xplw2 1/1 Running 0 3s
parallel-bgvz5 0/1 Completed 0 40s
parallel-55tlv 0/1 Completed 0 2m
parallel-lssmn 0/1 Completed 0 2mJobهای completed را study کنید و logهایشان را برای fingerprint کلیدهای تولیدشده ببینید. با kubectl delete job parallel شیء Job تمامشده را پاک کنید.
Work Queueها
use case رایج Jobها پردازش کار از یک work queue است. در این سناریو، taskی تعدادی work item میسازد و در work queue publish میکند. یک worker Job میتواند اجرا شود تا هر work item را پردازش کند تا work queue خالی شود (شکل ۱۲-۱).
شکل ۱۲-۱. Jobهای موازی
راهاندازی work queue
اول یک سرویس work queue متمرکز launch میکنیم. kuard یک سیستم work queue ساده مبتنی بر memory دارد. یک instance از kuard start میکنیم تا coordinator همه کارها باشد.
بعد یک ReplicaSet ساده برای مدیریت singleton work queue daemon میسازیم. از ReplicaSet استفاده میکنیم تا در صورت failure machine، Pod جدید ساخته شود، همانطور که در مثال ۱۲-۴ نشان داده شده.
مثال ۱۲-۴. rs-queue.yaml
yaml
apiVersion: extensions/v1beta1
kind: ReplicaSet
metadata:
labels:
app: work-queue
component: queue
chapter: jobs
name: queue
spec:
replicas: 1
template:
metadata:
labels:
app: work-queue
component: queue
chapter: jobs
spec:
containers:
- name: queue
image: "gcr.io/kuar-demo/kuard-amd64:blue"
imagePullPolicy: Alwayswork queue را با دستور زیر اجرا کنید:
$ kubectl apply -f rs-queue.yamlدر این نقطه work queue daemon باید up and running باشد. از port forwarding برای اتصال استفاده کنید. این دستور را در یک terminal window running نگه دارید:
$ QUEUE_POD=$(kubectl get pods -l app=work-queue,component=queue \
-o jsonpath='{.items[0].metadata.name}')
$ kubectl port-forward $QUEUE_POD 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080مرورگر را به http://localhost:8080 باز کنید و interface kuard را ببینید. به تب «MemQ Server» بروید تا اتفاقات را ببینید.
با work queue server آماده، باید آن را با service expose کنیم. این برای producerها و consumerها پیدا کردن work queue از طریق DNS آسان میشود، همانطور که مثال ۱۲-۵ نشان میدهد.
مثال ۱۲-۵. service-queue.yaml
yaml
apiVersion: v1
kind: Service
metadata:
labels:
app: work-queue
component: queue
chapter: jobs
name: queue
spec:
ports:
- port: 8080
protocol: TCP
targetPort: 8080
selector:
app: work-queue
component: queuequeue service را با kubectl بسازید:
$ kubectl apply -f service-queue.yaml
service "queue" createdپر کردن queue
حالا آمادهایم work itemهای زیادی در queue بگذاریم. برای سادگی فقط از curl برای drive کردن API سرور work queue استفاده میکنیم و work itemهای زیادی insert میکنیم. curl از طریق kubectl port-forward که قبلاً setup کردیم با work queue ارتباط برقرار میکند، همانطور که در مثال ۱۲-۶ نشان داده شده.
مثال ۱۲-۶. load-queue.sh
bash
# Create a work queue called 'keygen'
curl -X PUT localhost:8080/memq/server/queues/keygen
# Create 100 work items and load up the queue.
for i in work-item-{0..99}; do
curl -X POST localhost:8080/memq/server/queues/keygen/enqueue \
-d "$i"
doneاین دستورات را اجرا کنید؛ باید ۱۰۰ شیء JSON با message identifier یکتا برای هر work item در terminal ببینید. وضعیت queue را با تب «MemQ Server» در UI یا مستقیماً از API work queue تأیید کنید:
$ curl 127.0.0.1:8080/memq/server/stats
{
"kind": "stats",
"queues": [
{
"depth": 100,
"dequeued": 0,
"drained": 0,
"enqueued": 100,
"name": "keygen"
}
]
}حالا آمادهایم Jobی kick off کنیم که work queue را تا خالی شدن consume کند.
ساخت consumer Job
اینجا جالب میشود! kuard میتواند در حالت consumer هم عمل کند. میتوانیم آن را setup کنیم work itemها را از work queue بکشد، کلید بسازد و وقتی queue خالی شد exit کند، همانطور که در مثال ۱۲-۷ نشان داده شده.
مثال ۱۲-۷. job-consumers.yaml
yaml
apiVersion: batch/v1
kind: Job
metadata:
labels:
app: message-queue
component: consumer
chapter: jobs
name: consumers
spec:
parallelism: 5
template:
metadata:
labels:
app: message-queue
component: consumer
chapter: jobs
spec:
containers:
- name: worker
image: "gcr.io/kuar-demo/kuard-amd64:blue"
imagePullPolicy: Always
args:
- "--keygen-enable"
- "--keygen-exit-on-complete"
- "--keygen-memq-server=http://queue:8080/memq/server"
- "--keygen-memq-queue=keygen"
restartPolicy: OnFailureاینجا به Job میگوییم پنج Pod بهصورت موازی start کند. چون پارامتر completions تنظیم نشده، Job را در حالت worker pool قرار میدهیم. وقتی اولین Pod با exit code صفر exit کند، Job شروع به winding down میکند و Pod جدید start نمیکند. یعنی هیچ worker نباید exit کند تا کار تمام شود و همه در حال finish up باشند.
consumers Job را بسازید:
$ kubectl apply -f job-consumers.yaml
job "consumers" createdبعد از ساخت Job، Podهای پشتیبان Job را ببینید:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
queue-43s87 1/1 Running 0 5m
consumers-6wjxc 1/1 Running 0 2m
consumers-7l5mh 1/1 Running 0 2m
consumers-hvz42 1/1 Running 0 2m
consumers-pc8hr 1/1 Running 0 2m
consumers-w20cc 1/1 Running 0 2mپنج Pod بهصورت موازی running هستند. این Podها تا خالی شدن work queue running میمانند. در UI روی work queue server میتوانید ببینید. با خالی شدن queue، consumer Podها cleanly exit میکنند و consumers Job complete در نظر گرفته میشود.
پاکسازی
با labelها همه چیزهایی که در این بخش ساختیم را پاک میکنیم:
$ kubectl delete rs,svc,job -l chapter=jobsCronJobها
گاهی میخواهید Job را در interval مشخصی schedule کنید. برای این کار میتوانید CronJob در Kubernetes declare کنید که مسئول ساخت شیء Job جدید در interval مشخص است. declaration CronJob شبیه این است:
yaml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: example-cron
spec:
# Run every fifth hour
schedule: "0 */5 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: batch-job
image: my-batch-image
restartPolicy: OnFailureبه فیلد spec.schedule توجه کنید که interval CronJob را در فرمت cron استاندارد دارد.
میتوانید این فایل را cron-job.yaml ذخیره کنید و CronJob را با kubectl create -f cron-job.yaml بسازید. اگر به وضعیت فعلی CronJob علاقه دارید، kubectl describe <cron-job> جزئیات را میدهد.
خلاصه
روی یک cluster، Kubernetes هم workloadهای long-running مثل web applicationها و هم workloadهای کوتاهمدت مثل batch jobها را handle میکند. abstraction Job به شما اجازه میدهد الگوهای batch job از taskهای one-time ساده تا Jobهای موازی که تا اتمام کار many item پردازش میکنند را model کنید.
Jobها primitive سطح پایین هستند و برای workloadهای ساده مستقیماً قابل استفادهاند. اما Kubernetes از پایه extensible با objectهای سطح بالاتر ساخته شده. Jobها هم exception نیستند؛ بهراحتی توسط سیستمهای orchestration سطح بالاتر برای taskهای پیچیدهتر استفاده میشوند.