Job, CronJob và TTL

K
Kai··7 min read

Bốn controller ta gặp tới giờ có một điểm chung ngầm: chúng chạy mãi. Deployment giữ N pod sống bất tận, StatefulSet và DaemonSet cũng vậy, pod nào chết thì dựng lại, mục tiêu là luôn có pod chạy. Nhưng nhiều việc thật lại có điểm kết thúc: chạy một migration database, backup một volume, xử lý một mẻ dữ liệu. Cho những việc đó, "chạy mãi" là sai, ta cần thứ chạy xong thì dừng. Đó là Job. Bài này khép Part IV với Job, người anh em chạy-theo-lịch của nó là CronJob, và cơ chế TTL tự dọn Job đã xong.

Job: chạy tới hoàn tất rồi dừng

Tài liệu phân biệt rạch ròi: "Jobs represent one-off tasks that run to completion and then stop." và cơ chế: "A Job creates one or more Pods and will continue to retry execution of the Pods until a specified number of them successfully terminate ... When a specified number of successful completions is reached, the task (ie, Job) is complete." Chữ "successfully terminate" là điểm cần để ý: Job quan tâm pod thoát mã 0, không phải pod đang chạy. Một Job tối giản:

apiVersion: batch/v1
kind: Job
metadata: {name: job-once}
spec:
  template:
    spec:
      restartPolicy: Never        # Job chỉ chấp nhận Never hoặc OnFailure
      containers:
      - name: w
        image: busybox:1.36
        command: ["sh","-c","echo lam viec; sleep 3; echo xong; exit 0"]

Lưu ý restartPolicy: Never. Tài liệu bắt buộc: "Only a RestartPolicy equal to Never or OnFailure is allowed." Không được Always (mặc định của Pod, Bài 18), vì Always nghĩa là "luôn chạy lại" thì pod không bao giờ "hoàn tất" được, mâu thuẫn với bản chất Job.

kubectl get job job-once
kubectl get pods -l job-name=job-once
kubectl get job job-once -o jsonpath='succeeded={.status.succeeded} complete={.status.conditions[?(@.type=="Complete")].status}{"\n"}'
NAME       STATUS     COMPLETIONS   DURATION   AGE
job-once   Complete   1/1           7s         30s

NAME             READY   STATUS      ...
job-once-7rdhj   0/1     Completed   ...

succeeded=1 complete=True

STATUS: Complete, COMPLETIONS 1/1, pod ở trạng thái Completed (không phải Running), succeeded=1, condition Complete=True. Job đã làm xong việc và dừng, pod không bị dựng lại. Đó là Succeeded của Bài 18 ở quy mô controller.

completions và parallelism

Một Job có thể cần chạy nhiều lần, có thể song song. Hai trường điều khiển: completions (cần bao nhiêu lần chạy thành công) và parallelism (tối đa bao nhiêu pod chạy cùng lúc).

apiVersion: batch/v1
kind: Job
metadata: {name: job-parallel}
spec:
  completions: 4          # cần 4 lần hoàn tất
  parallelism: 2          # nhưng tối đa 2 pod chạy song song
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: w
        image: busybox:1.36
        command: ["sh","-c","sleep 3"]
kubectl get job job-parallel
kubectl get pods -l job-name=job-parallel --no-headers | wc -l
NAME           STATUS     COMPLETIONS   DURATION   AGE
job-parallel   Complete   4/4           11s        29s

4

COMPLETIONS 4/4 đạt qua 4 pod, nhưng vì parallelism: 2, Job chỉ chạy 2 pod cùng lúc rồi mới tới 2 cái sau — DURATION 11s (xấp xỉ 2 đợt × ~5s) thay vì ~5s nếu chạy cả 4 song song. Đây là khuôn xử lý mẻ: chia việc thành N phần, giới hạn tải đồng thời.

Khi Job lỗi: backoffLimit

Job thử lại khi pod lỗi — nhưng không vô hạn. backoffLimit đặt số lần thử trước khi Job bỏ cuộc. Một Job luôn lỗi với backoffLimit: 2:

apiVersion: batch/v1
kind: Job
metadata: {name: job-fail}
spec:
  backoffLimit: 2
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: w
        image: busybox:1.36
        command: ["sh","-c","echo se loi; exit 1"]
kubectl get job job-fail
kubectl get pods -l job-name=job-fail --no-headers | awk '{print $1,$3}'
kubectl get job job-fail -o jsonpath='failed={.status.failed} reason={.status.conditions[?(@.type=="Failed")].reason} msg={.status.conditions[?(@.type=="Failed")].message}{"\n"}'
NAME       STATUS   COMPLETIONS   DURATION   AGE
job-fail   Failed   0/1           60s        60s

job-fail-69ldt Error
job-fail-pdz4b Error
job-fail-wdlkd Error

failed=3 reason=BackoffLimitExceeded msg=Job has reached the specified backoff limit

Để ý: backoffLimit: 2 nhưng failed=3 — có ba pod lỗi. backoffLimit đếm số lần thử lại, nên tổng số lần chạy là backoffLimit + 1 (lần đầu + 2 lần thử lại). Chạm trần, Job chuyển Failed với reason BackoffLimitExceeded. (Giữa các lần thử, Job chờ theo backoff lũy thừa — giống tinh thần CrashLoopBackOff của Bài 18.) backoffLimit mặc định là 6. Đây là cách Job phân biệt "lỗi tạm thời, thử lại" với "hỏng thật, dừng và báo".

ttlSecondsAfterFinished: tự dọn Job đã xong

Job xong không tự biến mất — object Job và pod Completed của nó nằm lại để bạn xem log/kết quả. Tích lũy lâu ngày thì rác. ttlSecondsAfterFinished cho Job tự hủy sau khi kết thúc (dù Complete hay Failed) một số giây:

apiVersion: batch/v1
kind: Job
metadata: {name: job-ttl}
spec:
  ttlSecondsAfterFinished: 20      # xong 20s sau là tự xóa
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: w
        image: busybox:1.36
        command: ["sh","-c","echo nhanh gon; exit 0"]

Job hoàn tất gần như tức thì. Đợi quá 20 giây rồi tìm lại:

kubectl get job job-ttl
Error from server (NotFound): jobs.batch "job-ttl" not found

Job đã tự xóa và kéo theo cả pod của nó. Không cần cron dọn dẹp hay script ngoài. Với các Job sinh ra liên tục (nhất là từ CronJob dưới đây), ttlSecondsAfterFinished là cách gọn để cluster không ngập Job cũ.

CronJob: Job theo lịch

Cuối cùng là CronJob: "A CronJob creates Jobs on a repeating schedule." Tài liệu ví: "One CronJob object is like one line of a crontab file on a Unix system." Nó dùng cú pháp cron năm trường (phút, giờ, ngày, tháng, thứ) và một jobTemplate chính là khuôn Job ở trên. Lịch * * * * * nghĩa là mỗi phút:

apiVersion: batch/v1
kind: CronJob
metadata: {name: cron-demo}
spec:
  schedule: "* * * * *"
  successfulJobsHistoryLimit: 3      # giữ 3 Job thành công gần nhất
  failedJobsHistoryLimit: 1          # giữ 1 Job lỗi gần nhất
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: w
            image: busybox:1.36
            command: ["sh","-c","date; echo chao tu cronjob"]

Tạo lúc 23:26:32, chờ qua mốc phút kế (23:27:00) rồi xem:

kubectl get cronjob cron-demo
kubectl get jobs
kubectl logs job/cron-demo-29659227
NAME        SCHEDULE    SUSPEND   ACTIVE   LAST SCHEDULE   AGE
cron-demo   * * * * *   False     0        30s             58s

NAME                 STATUS     COMPLETIONS   DURATION   AGE
cron-demo-29659227   Complete   1/1           3s         31s

Sat May 23 16:27:00 UTC 2026
chao tu cronjob

LAST SCHEDULE 30s — CronJob đã nổ đúng mốc 23:27:00, đẻ ra Job cron-demo-29659227 (hậu tố là mốc thời gian theo phút), và log pod in đúng 16:27:00 — chạy khít đầu phút. Job này thuộc sở hữu CronJob:

kubectl get job cron-demo-29659227 -o jsonpath='ownerKind={.metadata.ownerReferences[0].kind} ownerName={.metadata.ownerReferences[0].name}{"\n"}'
# ownerKind=CronJob ownerName=cron-demo

Chuỗi sở hữu là CronJob → Job → Pod, và successfulJobsHistoryLimit: 3 (mặc định 3) giữ lại 3 Job thành công gần nhất rồi tự dọn cái cũ hơn, failedJobsHistoryLimit: 1 (mặc định 1) giữ 1 Job lỗi. Vài trường đáng biết khác: concurrencyPolicy xử lý khi lượt mới tới mà lượt cũ chưa xong, gồm Allow (mặc định, cho chạy chồng), Forbid (bỏ qua lượt mới), Replace (thay lượt cũ); và suspend: true để tạm ngưng lịch mà không xóa CronJob.

🧹 Dọn dẹp

kubectl delete cronjob cron-demo
kubectl delete job --all

Xóa CronJob kéo theo các Job và pod nó đẻ ra; job-ttl thì đã tự xóa rồi. Cụm về lại hai pod CoreDNS. Manifest ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 27-job-cronjob.

Tổng kết

Job là controller chạy-tới-hoàn-tất, ngược với Deployment/StatefulSet/DaemonSet chạy mãi. Nó tạo pod tới khi đủ số thoát mã 0: completions (cần mấy lần), parallelism (mấy pod song song), backoffLimit (mấy lần thử lại trước khi Failed với BackoffLimitExceeded, tổng lần chạy = backoffLimit + 1, mặc định 6); restartPolicy phải là Never hoặc OnFailure. ttlSecondsAfterFinished cho Job tự xóa sau khi kết thúc (ta đã thấy job-ttl biến mất sau 20s). CronJob đẻ Job theo lịch cron qua jobTemplate (ta bắt được nó nổ đúng mốc phút, chuỗi sở hữu CronJob→Job→Pod), với concurrencyPolicy, history limit (mặc định 3 thành công / 1 lỗi), và suspend. Tới đây lời hứa ở Bài 19 cũng rõ nghĩa: sidecar native không chặn Job hoàn tất, còn sidecar kiểu cũ thì treo Job mãi.

Hết Part IV, ta đã đủ năm họ controller. Part V chuyển từ "chạy gì" sang "tổ chức và truy vấn đối tượng": Bài 28 mở đầu với label, selector, namespace, annotation và field selector, bộ công cụ phân loại và lọc mà ta đã dùng rải rác (chính -l job-name=... ở bài này) giờ được đào bài bản.

Related Posts