Job และ CronJob
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”Job รัน Pod หนึ่งตัวหรือมากกว่าจนกว่างานจะสำเร็จ ไม่ใช่ปล่อยให้รันตลอดไป ส่วน CronJob create Job ให้อัตโนมัติตาม schedule แบบ cron
Job: รันจนจบงาน ไม่ใช่รันตลอดไป
หัวข้อที่มีชื่อว่า “Job: รันจนจบงาน ไม่ใช่รันตลอดไป”Pod ของ Deployment มีไว้ให้รันไม่มีกำหนด ส่วน Pod ของ Job มีไว้ให้ จบงาน Job จะนับจำนวนที่สำเร็จและ retry เมื่อพังจนถึง limit ที่กำหนด แทนที่จะ restart Pod ไปเรื่อย ๆ แบบ controller ที่รันระยะยาว
apiVersion: batch/v1kind: Jobmetadata: name: report-backfillspec: completions: 5 parallelism: 2 backoffLimit: 3 activeDeadlineSeconds: 600 template: spec: restartPolicy: OnFailure containers: - name: backfill image: myregistry/report-backfill:1.0.0 args: ['--month=2026-06']completions: 5 หมายความว่า Job จะยังไม่จบจนกว่า Pod 5 ตัวจะ exit สำเร็จ parallelism: 2 จำกัดว่า Pod พวกนั้นรันพร้อมกันได้กี่ตัว backoffLimit: 3 คือจำนวนครั้งที่ยอมให้พังได้ก่อนที่ Job จะยอมแพ้แล้วตีตัวเองเป็น Failed activeDeadlineSeconds: 600 คือ timeout รวมของทั้ง Job นับเป็นเวลาจริง พอเกินก็ terminate ทันทีไม่สนว่าสำเร็จไปกี่ตัวแล้ว สังเกต restartPolicy: OnFailure ด้วย Pod template ของ Job ต้องใช้ OnFailure หรือ Never เท่านั้น ห้ามใช้ Always เพราะจุดประสงค์ทั้งหมดของ Job คือหยุดเมื่องานเสร็จ พิสูจน์แล้วบน API server จริง:
# k8s/jobs-and-cronjobs/job-bad-restart.yaml — @expect-error restartPolicy: Always is not a valid Job Pod template valueapiVersion: batch/v1kind: Jobmetadata: name: bad-jobspec: template: spec: restartPolicy: Always containers: - name: work image: busybox:1.38 command: ['sh', '-c', 'echo hi']$ kubectl apply --dry-run=server -f job-bad-restart.yamlThe Job "bad-job" is invalid: spec.template.spec.restartPolicy: Required value: valid values: "OnFailure", "Never"# Watch completions accumulate, then check the Job's final statuskubectl get job report-backfill --watchkubectl describe job report-backfill
# Logs from a finished Job's Podskubectl logs -l job-name=report-backfill --tail=100backoffLimit เทียบกับ activeDeadlineSeconds: สองวิธีที่ Job ยอมแพ้ต่างกัน
หัวข้อที่มีชื่อว่า “backoffLimit เทียบกับ activeDeadlineSeconds: สองวิธีที่ Job ยอมแพ้ต่างกัน”ทั้งคู่หยุด Job ที่พังได้ แต่คนละแกน backoffLimit นับ จำนวนครั้งที่พัง activeDeadlineSeconds คือ เพดานเวลาจริง ของทั้ง Job ไม่ว่าจะใช้ attempt ไปกี่ครั้งแล้วก็ตาม พิสูจน์ทั้งคู่ ด้วย condition จริงจาก cluster จริง:
# backoffLimit: 2, container always exits 1, no activeDeadlineSeconds set$ kubectl get pods -l job-name=backoff-demo --no-headers | wc -l3 # ordinals 0, 1, 2 -- backoffLimit + 1 attempts, exactly
$ kubectl get job backoff-demo -o jsonpath='{range .status.conditions[*]}{.type}{"="}{.status}{" reason="}{.reason}{" message="}{.message}{"\n"}{end}'FailureTarget=True reason=BackoffLimitExceeded message=Job has reached the specified backoff limitFailed=True reason=BackoffLimitExceeded message=Job has reached the specified backoff limit# backoffLimit: 6 (would normally allow 7 attempts), activeDeadlineSeconds: 8, container always exits 1$ kubectl get pods -l job-name=deadline-demo --no-headers | wc -l1 # cut off by the deadline before a 2nd attempt's backoff delay even elapsed
$ kubectl get job deadline-demo -o jsonpath='{range .status.conditions[*]}{.type}{"="}{.status}{" reason="}{.reason}{" message="}{.message}{"\n"}{end}'FailureTarget=True reason=DeadlineExceeded message=Job was active longer than specified deadlineFailed=True reason=DeadlineExceeded message=Job was active longer than specified deadlinebackoffLimit: 2 หมายถึง Pod attempt 3 ครั้งเป๊ะ ๆ ก่อนจะ BackoffLimitExceeded จริง activeDeadlineSeconds: 8 ตัด Job ก่อนที่ backoffLimit: 6 ที่สูงกว่ามากจะถูกแตะด้วยซ้ำ ด้วย reason ที่ต่างกันชัดเจน (DeadlineExceeded) ใครมาถึงก่อนคนนั้นชนะ ใช้ activeDeadlineSeconds เมื่อต้องการเพดานเวลารันรวมแบบเด็ดขาดไม่ว่าจะ retry กี่ครั้ง (batch job ตอนกลางคืนที่ห้ามยังรันอยู่ตอน 6 โมงเช้า) และใช้ backoffLimit เมื่อสิ่งที่สำคัญคือจำกัด จำนวนครั้ง ที่ retry ไม่ว่าจะใช้เวลานานแค่ไหน
podFailurePolicy: action ตาม exit code จริง ไม่ใช่แค่ “มันพัง”
หัวข้อที่มีชื่อว่า “podFailurePolicy: action ตาม exit code จริง ไม่ใช่แค่ “มันพัง””GA ตั้งแต่ Kubernetes v1.28 (feature gate JobPodFailurePolicy ถูกเอาออก/lock เป็น true ใน 1.32 — feature gate history) podFailurePolicy ให้ Job react ต่าง exit code ต่างกันได้ แทนที่จะปฏิบัติกับทุกความพังเหมือนกันหมดเทียบกับ backoffLimit สอง action ของมัน พิสูจน์แล้ว:
FailJob หยุดทั้ง Job ทันทีเมื่อเจอ exit code ที่ระบุไว้ ไม่สนใจ backoffLimit เลย (ไม่ retry error ที่ fatal ไม่ transient ให้เสียเวลา):
apiVersion: batch/v1kind: Jobmetadata: name: pfp-failjobspec: backoffLimit: 6 podFailurePolicy: rules: - action: FailJob onExitCodes: containerName: work operator: In values: [42] template: spec: restartPolicy: Never containers: - name: work image: busybox:1.38 command: ['sh', '-c', 'echo simulating a fatal, non-retryable config error; exit 42']$ kubectl get pods -l job-name=pfp-failjobNAME READY STATUS RESTARTS AGEpfp-failjob-48t28 0/1 Error 0 4s
$ kubectl get job pfp-failjob -o jsonpath='{range .status.conditions[*]}{.type}{"="}{.status}{" reason="}{.reason}{" message="}{.message}{"\n"}{end}'FailureTarget=True reason=PodFailurePolicy message=Container work for pod default/pfp-failjob-48t28 failed with exit code 42 matching FailJob rule at index 0Failed=True reason=PodFailurePolicy message=Container work for pod default/pfp-failjob-48t28 failed with exit code 42 matching FailJob rule at index 0มี Pod รันแค่ หนึ่ง ตัวเท่านั้น ทั้งที่ backoffLimit: 6 FailJob ตัดสั้นข้ามงบ retry ไปเลย
Ignore ไม่นับ exit code นี้เข้า backoffLimit เลย retry ต่อไปเรื่อย ๆ ไม่สนใจ:
apiVersion: batch/v1kind: Jobmetadata: name: pfp-ignorespec: backoffLimit: 0 podFailurePolicy: rules: - action: Ignore onExitCodes: containerName: work operator: In values: [1] template: spec: restartPolicy: Never containers: - name: work image: busybox:1.38 command: ['sh', '-c', 'echo transient failure, will keep getting ignored; exit 1']$ kubectl get pods -l job-name=pfp-ignoreNAME READY STATUS RESTARTS AGEpfp-ignore-q52jr 0/1 Error 0 15spfp-ignore-xlnn4 0/1 Error 0 5s
$ kubectl get job pfp-ignore -o jsonpath='{.status.failed}' # empty/0 -- despite backoffLimit: 0, two Pods have already failed and a new one keeps getting createdด้วย backoffLimit: 0 Job ปกติจะยอมแพ้หลังพังครั้งแรกเลย ตรงนี้ Pod พังไปแล้วสองตัว และ Job ก็ยังสร้างตัวใหม่ต่อไปเรื่อย ๆ status.failed ไม่เพิ่มเลย เพราะ Ignore แปลว่า exit code นี้ไม่เคยถูกนับเข้างบ backoff เลย
Indexed Job: แต่ละ Pod รู้ index ของตัวเอง
หัวข้อที่มีชื่อว่า “Indexed Job: แต่ละ Pod รู้ index ของตัวเอง”completionMode: Indexed ให้แต่ละ Pod มี completion index คงที่ ไม่ซ้ำกัน (0..completions-1) ที่ inject มาเป็น env var JOB_COMPLETION_INDEX และ label batch.kubernetes.io/job-completion-index ของ Pod มีประโยชน์สำหรับ shard ข้อมูลข้าม Pod โดยไม่ต้องมีการประสานงานระหว่าง Pod เลย:
apiVersion: batch/v1kind: Jobmetadata: name: indexed-reportspec: completions: 4 parallelism: 4 completionMode: Indexed ttlSecondsAfterFinished: 60 template: spec: restartPolicy: Never containers: - name: work image: busybox:1.38 command: ['sh', '-c', 'echo "this is completion index $JOB_COMPLETION_INDEX"']$ kubectl get pods -l job-name=indexed-reportNAME READY STATUS AGEindexed-report-0-9j8ff 0/1 Completed 3sindexed-report-1-ncff4 0/1 Completed 3sindexed-report-2-lw5cv 0/1 Completed 3sindexed-report-3-clz2x 0/1 Completed 3s
$ kubectl logs indexed-report-0-9j8ffthis is completion index 0$ kubectl logs indexed-report-2-lw5cvthis is completion index 2ชื่อ Pod เองก็ฝัง index ไว้ (indexed-report-<index>-<suffix>) แต่ละ container อ่าน $JOB_COMPLETION_INDEX ที่ต่างกันจริง ๆ และ ttlSecondsAfterFinished: 60 หมายความว่าทั้ง Job (พร้อม Pod ของมัน) จะถูก garbage-collect อัตโนมัติ ~60 วินาทีหลัง complete พิสูจน์ตรง ๆ:
$ kubectl get job ttl-demoNAME STATUS COMPLETIONS DURATION AGEttl-demo Complete 1/1 3s 4s
# ...15s later (ttlSecondsAfterFinished: 15 on this one)...$ kubectl get job ttl-demoError from server (NotFound): jobs.batch "ttl-demo" not foundถ้าไม่มี ttlSecondsAfterFinished Job ที่ complete แล้ว (พร้อม Pod ของมัน) จะอยู่ตลอดไปจนกว่าจะลบด้วยมือ สำหรับ Job แบบ batch ที่รันบ่อย ๆ นี่สะสมเหมือนกับ CronJob history ที่ไม่มีขอบเขต ซึ่งพูดถึงต่อไป
CronJob: ตั้งเวลารัน Job
หัวข้อที่มีชื่อว่า “CronJob: ตั้งเวลารัน Job”CronJob ไม่ได้รัน Pod เองตรง ๆ แต่จะ create Job ตาม schedule แล้ว Job นั้นก็ create Pod เหมือนกับที่คุณ submit Job ด้วยมือตอนนั้นเป๊ะ ๆ
apiVersion: batch/v1kind: CronJobmetadata: name: nightly-reportspec: schedule: '0 2 * * *' concurrencyPolicy: Forbid startingDeadlineSeconds: 300 successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 1 jobTemplate: spec: backoffLimit: 2 template: spec: restartPolicy: OnFailure containers: - name: report image: myregistry/nightly-report:1.0.0schedule: '0 2 * * *' รันตอน 02:00 ทุกวัน ตาม cron syntax มาตรฐาน concurrencyPolicy ตัดสินว่าจะทำยังไงถ้า run ก่อนหน้ายังไม่จบตอนที่ run ถัดไปถึงกำหนด Allow (ค่า default) ปล่อยให้รันคู่กันไปเลย Forbid ข้าม run ใหม่ไปทั้งหมด และ Replace ยกเลิก Job ที่ยังรันอยู่แล้วเริ่ม Job ใหม่แทนที่ startingDeadlineSeconds กำหนดว่า run ที่พลาดเวลาไปแล้วยังเริ่มได้ช้าสุดแค่ไหน successfulJobsHistoryLimit กับ failedJobsHistoryLimit คุมว่า CronJob จะเก็บ Job object เก่าไว้ให้ตรวจสอบกี่ตัวก่อนจะ garbage-collect ทิ้ง timeZone (ชื่อ IANA แบบ 'Asia/Bangkok') ปักหมุด schedule ไว้กับ time zone จริง แทนที่จะใช้ time zone ของ process kube-controller-manager เอง ถูก validate กับ system time zone database จริงทั้งตอน admission และตอนรัน
concurrencyPolicy พิสูจน์ด้วย schedule ที่ทับซ้อนกันจริง
หัวข้อที่มีชื่อว่า “concurrencyPolicy พิสูจน์ด้วย schedule ที่ทับซ้อนกันจริง”สอง CronJob ทั้งคู่ schedule: '* * * * *' (ทุกนาที) รัน Job ที่ sleep 90 วินาที ตัวหนึ่ง concurrencyPolicy: Allow อีกตัว Forbid:
apiVersion: batch/v1kind: CronJobmetadata: name: slow-allowspec: schedule: '* * * * *' concurrencyPolicy: Allow successfulJobsHistoryLimit: 2 failedJobsHistoryLimit: 1 jobTemplate: spec: backoffLimit: 0 template: spec: restartPolicy: Never containers: - name: work image: busybox:1.38 command: ['sh', '-c', 'echo start $(date +%T); sleep 90; echo done $(date +%T)']apiVersion: batch/v1kind: CronJobmetadata: name: slow-forbidspec: schedule: '* * * * *' concurrencyPolicy: Forbid jobTemplate: spec: backoffLimit: 0 template: spec: restartPolicy: Never containers: - name: work image: busybox:1.38 command: ['sh', '-c', 'echo start $(date +%T); sleep 90; echo done $(date +%T)']สังเกตบน cluster จริงนานกว่า 3 นาที:
# concurrencyPolicy: Allow -- events, in order08:03:00 SuccessfulCreate Created job slow-allow-2984016308:04:00 SuccessfulCreate Created job slow-allow-29840164 # <-- created while -163 (90s job) is STILL RUNNING08:04:32 SawCompletedJob Saw completed job: slow-allow-29840163, condition: Complete08:05:00 SuccessfulCreate Created job slow-allow-29840165 # <-- created while -164 is STILL RUNNING08:05:32 SawCompletedJob Saw completed job: slow-allow-29840164, condition: Complete08:06:00 SuccessfulCreate Created job slow-allow-2984016608:06:32 SuccessfulDelete Deleted job slow-allow-29840163 # successfulJobsHistoryLimit: 2 pruning the oldest# concurrencyPolicy: Forbid -- events, same 3+ minutes, same cluster08:03:00 SuccessfulCreate Created job slow-forbid-2984016308:04:32 SawCompletedJob Saw completed job: slow-forbid-29840163, condition: Complete08:04:32 SuccessfulCreate Created job slow-forbid-29840164 # the 08:04:00 tick, held until -163 finished08:06:05 JobAlreadyActive Not starting job because prior execution is running and concurrency policy is Forbid08:06:05 SawCompletedJob Saw completed job: slow-forbid-29840164, condition: Complete08:06:05 SuccessfulCreate Created job slow-forbid-29840166 # note: -29840165 (the 08:05:00 tick) never exists at allชื่อ Job ของแต่ละนาทีฝังไว้ว่ามันเป็นของ schedule เวลาไหน (ส่วนต่อท้าย ...163/...164/…) ด้วย Allow slow-allow-29840164 (กำหนด 08:04:00) ถูก create ตรงเวลาเป๊ะ ๆ ในขณะที่ slow-allow-29840163 (Job 90 วินาทีของนาทีก่อนหน้า) ยังเหลืออีก 08:04:00–08:04:32 กว่าจะ complete overlap จริงที่พิสูจน์ได้ สอง Job มี Pod รันพร้อมกันจริง ๆ ด้วย Forbid tick ของ 08:04:00 ไม่ได้ถูกข้ามไปเฉย ๆ แต่ถูก กัก ไว้ controller create slow-forbid-29840164 ทันทีที่ -163 complete (08:04:32 ไม่ใช่ 08:04:00) และ tick ถัดไปที่ถึงกำหนด (08:05:00 ซึ่งจะเป็น -29840165) ถูกข้ามไป ถาวร ทันทีที่รู้ว่า -164 ยังทำงานอยู่ (JobAlreadyActive log ตอน controller reconcile ครั้งถัดไปที่ 08:06:05) -29840165 ไม่เคยปรากฏใน kubectl get jobs เลย controller กระโดดไปที่ tick ที่ถึงกำหนดถัดไปเลย (-29840166) แทนที่จะ queue ทุก tick ที่พลาดไป บรรทัด SuccessfulDelete บน CronJob Allow คือ successfulJobsHistoryLimit: 2 ทำงานจริง พอถึง 08:06:32 มี Job ที่ complete แล้ว 3 ตัว ตัวเก่าสุด (-163) เลยถูก garbage-collect ทิ้ง
flowchart LR cj["CronJob nightly-report (schedule: 0 2 * * *)"] -->|creates| j["Job nightly-report-29102026"] j -->|creates| p1["Pod (runs to completion)"] j -->|creates| p2["Pod (runs to completion)"] p1 --> done["Succeeded, Pods stop"] p2 --> done
startingDeadlineSeconds, timeZone และ history limits
หัวข้อที่มีชื่อว่า “startingDeadlineSeconds, timeZone และ history limits”$ kubectl explain cronjob.spec.startingDeadlineSecondsFIELD: startingDeadlineSeconds <integer>
DESCRIPTION: Optional deadline in seconds for starting the job if it misses scheduled time for any reason. Missed jobs executions will be counted as failed ones.การพิสูจน์ Forbid ข้างบนแสดงพฤติกรรม missed-schedule default (ไม่ตั้ง startingDeadlineSeconds) ให้เห็นตรง ๆ อยู่แล้ว controller ไม่ queue ทุก tick ที่พลาดไป มันพยายามแค่ tick ที่ถึงกำหนดล่าสุดตัวเดียวเท่านั้น แล้วยอมแพ้กับตัวเก่ากว่านั้นทันทีที่ concurrency เคลียร์ -29840165 ถูกข้ามไปตลอดกาล ไม่ใช่แค่เลื่อนออกไป การตั้ง startingDeadlineSeconds ทำให้การข้ามนั้นมีขอบเขตชัดเจนและตั้งใจ (แทนที่จะเป็นผลข้างเคียงที่โผล่ขึ้นมาเองจาก controller ตามหลังไปไกลแค่ไหน) และตามคำอธิบายของ field เองข้างบน มันนับ miss ที่ช้าเกินไปว่าเป็น run ที่ failed แทนที่จะหายไปเงียบ ๆ
timeZone ถูกยอมรับและ validate ตอน admission กับ system time zone database จริง:
apiVersion: batch/v1kind: CronJobmetadata: name: tz-demospec: schedule: '30 2 * * *' timeZone: 'Asia/Bangkok' jobTemplate: spec: template: spec: restartPolicy: Never containers: - name: work image: busybox:1.38 command: ['echo', 'hi']$ kubectl apply --dry-run=server -f cj-timezone.yamlcronjob.batch/tz-demo created (server dry run)จัดการ Job และ CronJob
หัวข้อที่มีชื่อว่า “จัดการ Job และ CronJob”# List CronJobs and the Jobs they have createdkubectl get cronjob nightly-reportkubectl get jobs --selector=job-name
# Trigger one ad-hoc run right now, without waiting for the schedulekubectl create job nightly-report-manual --from=cronjob/nightly-reportเพราะ schedule ของ CronJob ยิงซ้ำได้หลายรอบ successfulJobsHistoryLimit/failedJobsHistoryLimit เลยสำคัญในทางปฏิบัติ ถึงแม้ทั้งคู่จะ default เป็นตัวเลขเล็ก ๆ ที่มีขอบเขต (3/1 พิสูจน์ด้านล่าง) ไม่ใช่ “ไม่มีขอบเขต” ก็ตาม พิสูจน์แล้วข้างบน successfulJobsHistoryLimit: 2 ทำให้เกิด SuccessfulDelete บน Job ที่ complete เก่าสุดจริง ๆ ทันทีที่ตัวที่ 3 เสร็จ
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอน review Job หรือ CronJob ที่ AI generate มา เช็คพวกนี้
restartPolicy: Alwaysบน Pod template ของ Job พิสูจน์แล้วข้างบนว่านี่คือ API server reject จริงทันที (Required value: valid values: "OnFailure", "Never") ไม่ใช่อะไรที่จะค่อย ๆ เสื่อมลงเงียบ ๆ- มีทั้ง
activeDeadlineSecondsและbackoffLimitแต่ผู้เขียนคิดว่ามีแค่ตัวเดียวที่ “ใช้จริง” พิสูจน์แล้วข้างบนว่าใครมาถึงเงื่อนไขก่อนคนนั้นชนะ พร้อมreasonที่ต่างกันชัดเจน (DeadlineExceededเทียบกับBackoffLimitExceeded) alert ที่ผูกกับ reason string ผิดตัวจะพลาดอีกเส้นทางไปเลย - rule
podFailurePolicyที่ใช้Ignoreบน exit code ที่เกิดขึ้นได้ตลอดไปจริง ๆ (เช่น input file พังที่ไม่มีวันถูกต้อง) พิสูจน์แล้วข้างบนว่าIgnoreไม่นับเข้าbackoffLimitเลยจริง ๆ ดังนั้น Job ที่ความพังจริง ๆ ถาวร ไม่ใช่ transient จะ retry ไม่มีสิ้นสุดโดยไม่มีอะไรมาหยุดอัตโนมัติ - CronJob ที่
schedule: '* * * * *'(หรือถี่พอ ๆ กัน) กับ Job ที่รันนานกว่าช่วง schedule ได้ โดยไม่ตั้งconcurrencyPolicyเลย default คือAllowพิสูจน์แล้วข้างบนว่ามันรัน Job ที่ทับซ้อนกันจริง ๆ คู่กันไปเลย ซึ่งแทบไม่ใช่สิ่งที่ผู้เขียน “รันทุกนาที” ตั้งใจไว้ สำหรับ job ที่ runtime ไม่ถูกจำกัดแน่นอน - ไม่มี
ttlSecondsAfterFinishedบน Job แบบ batch ที่รันซ้ำ ๆ ที่ไม่ได้อยู่ภายใต้ CronJob เลย พิสูจน์แล้วข้างบนว่า Job (ต่างจาก history limit ของ CronJob เองที่ default เป็นตัวเลขมีขอบเขต3/1) ไม่มี TTL default เลย สะสมไม่มีขอบเขตจนกว่าจะลบด้วยมือ
🐛 หา bug ในโค้ดนี้
apiVersion: batch/v1kind: CronJobmetadata:name: nightly-cleanupspec:schedule: '*/1 * * * *'jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: cleanup image: myregistry/cleanup-job:2.0.0เฉลย
บั๊กจริงคือ ไม่ได้ตั้ง concurrencyPolicy เลย มันเลย default เป็น Allow พิสูจน์แล้วข้างบนด้วย schedule จริงว่า Allow รัน Job ใหม่ทุก tick ของนาทีจริง ๆ แม้ว่า run ก่อนหน้ายังทำงานอยู่ ซึ่งแทบไม่ใช่สิ่งที่ตั้งใจสำหรับ cleanup job ที่ scan/mutate shared state (สอง cleanup run ที่ทับซ้อนแข่งกันเองคือ correctness bug ไม่ใช่แค่เปลืองทรัพยากร) ทางแก้: เพิ่ม concurrencyPolicy: Forbid (พิสูจน์แล้วข้างบนว่ามันกัก tick ที่ยังถึงกำหนดไว้จนกว่า run ก่อนหน้าจะจบ หรือทิ้งไปเลยถ้าตามหลังไปไกลเกิน ไม่มีวันรันสองตัวพร้อมกันเด็ดขาด) successfulJobsHistoryLimit/failedJobsHistoryLimit ไม่ได้ตั้งไว้ตรงนี้ ซึ่งจริง ๆ ไม่เป็นไร พิสูจน์แล้วข้างบนว่ามัน default เป็น 3/1 ที่มีขอบเขต ไม่ใช่ไม่มีขอบเขต การไม่ตั้งอันนี้เพียงอย่างเดียวเลยไม่ใช่บั๊ก
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Job ไม่ใช่แค่ “Pod ที่มี retry” มันคือ ตัวนับ completion ที่มีงบ retry completions/parallelism บอกเป้าหมายกับ concurrency backoffLimit/activeDeadlineSeconds/podFailurePolicy คือสามวิธีอิสระจากกันที่ตัวนับนี้จะตัดสินใจว่ามันไม่มีวันถึงเป้าหมายแล้วยอมแพ้ (พิสูจน์แล้วข้างบน: แต่ละอันมี reason ของตัวเองชัดเจน) CronJob คือโรงงานผลิต Job ที่ผูกกับนาฬิกา ความซับซ้อนทั้งหมดของมัน concurrencyPolicy, startingDeadlineSeconds, history limit จริง ๆ แล้วคือคำถามเดียวที่ถูกถามซ้ำ ๆ “tick ใหม่ถึงกำหนดแล้ว มี Job ที่ฉัน create ไว้ก่อนหน้ายังมีอยู่ไหม ถ้ามีจะทำยังไงกับ tick นี้”
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
podFailurePolicy กับ FailJob บน exit code ที่ fatal | หยุดทันทีเมื่อเจอ error ที่ retry ไม่ได้ พิสูจน์แล้วข้างบน (1 Pod ไม่ใช่ backoffLimit+1) | ต้องคอยดูแลให้ตรงกับ exit-code contract จริงของแอปตลอด |
podFailurePolicy กับ Ignore บน exit code ที่ transient | ความพังที่ transient จริง ๆ ไม่เสียงบ retry เลย พิสูจน์แล้วข้างบน | ความพังที่คิดว่าtransient แต่จริง ๆ ไม่ใช่ จะ retry ไม่มีสิ้นสุดโดยไม่มีอะไรหยุด |
completionMode: Indexed | shard งานได้โดยไม่ต้องประสานงานเลยผ่าน JOB_COMPLETION_INDEX พิสูจน์แล้วข้างบน | งานต้อง partition ได้ด้วย integer index ล้วน ๆ ตั้งแต่ต้น |
concurrencyPolicy: Allow (default) | ง่าย ไม่มีวันบล็อก run ตาม schedule เลย | พิสูจน์แล้วข้างบนว่า run ที่ช้ากับ run ของ tick ถัดไปรันคู่กันจริง ๆ |
concurrencyPolicy: Forbid | ไม่มี run ที่ทับซ้อนกันเลย พิสูจน์แล้วข้างบน | run ที่ยังทำงานอยู่ตอนหลาย tick ผ่านไปทำให้ tick พวกนั้นถูกข้ามไปเงียบ ๆ ถาวร (พิสูจน์แล้วข้างบน: -29840165 ไม่เคยมีอยู่จริง) |
ttlSecondsAfterFinished / history limit | Job/Pod สะสมมีขอบเขต พิสูจน์แล้วข้างบน | history เก่าหายไปเมื่อเกิน TTL/limit ไม่มีบันทึกไว้ตรวจสอบย้อนหลังด้วยมือ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “backoffLimit กับ activeDeadlineSeconds เป็นปุ่มเดียวกันแค่วัดคนละหน่วย” พิสูจน์แล้วข้างบน มันเป็นเงื่อนไขอิสระจากกันที่มี reason แยกแยะได้ชัดเจน (
BackoffLimitExceededเทียบกับDeadlineExceeded) ใครมาถึงก่อนคนนั้นจบ Job - “podFailurePolicy: Ignore แค่ทำให้ Job retry นานขึ้นอีกนิด” พิสูจน์แล้วข้างบน มันหมายความว่า exit code นั้น ไม่ถูกนับเลย ด้วย
backoffLimit: 0Job ยังคง retry ต่อหลังพังจริง 2 ครั้งstatus.failedไม่ขยับเลย - “concurrencyPolicy: Forbid จะ queue run ที่ถูกข้ามไว้แล้วค่อยตามทีหลัง” พิสูจน์แล้วข้างบนด้วยชื่อ Job จริง tick ที่ถูกข้ามเพราะ
Forbid(-29840165) ไม่เคยถูก create เลย controller กระโดดไป tick ที่ถึงกำหนดถัดไปเลย ไม่ backfill ทุก tick ที่พลาดไป - “CronJob ที่ไม่ตั้ง history limit ไว้เก็บ history ไม่มีขอบเขต” พิสูจน์แล้วข้างบนด้วย
kubectl explainจริงsuccessfulJobsHistoryLimitdefault3,failedJobsHistoryLimitdefault1ไม่ตั้ง field ไม่ได้แปลว่าไม่มีขอบเขต มันแปลว่าใช้ตัวเลขเล็ก ๆ พวกนี้แทน