Job, CronJob & workload đặc biệt: việc chạy-rồi-xong
Tới giờ mọi workload ta gặp đều chạy mãi mãi: Deployment giữ web sống 24/7, StatefulSet giữ database luôn bật. Nhưng đời thật đầy việc chạy một lần rồi…
Tới giờ mọi workload ta gặp đều chạy mãi mãi: Deployment giữ web sống 24/7, StatefulSet giữ database luôn bật. Nhưng đời thật đầy việc chạy một lần rồi xong, hoặc chạy theo lịch:
- Chạy một lần: migration schema DB, import dữ liệu, xử lý một batch ảnh, gửi một đợt email.
- Theo lịch: backup mỗi đêm 2 giờ sáng, dọn log mỗi tuần, tổng hợp báo cáo mỗi đầu tháng.
Nếu ép những việc này vào Deployment, bạn dính một nghịch lý chết người (xem ngay dưới). K8s có đúng công cụ: Job (chạy tới khi hoàn tất), CronJob (Job theo lịch), và tiện thể ta đào sâu DaemonSet (một Pod trên mỗi node) — nhóm workload có vòng đời rất khác Deployment.
Vấn đề: Deployment không biết khái niệm "xong"
Thử chạy một script migration bằng Deployment:
Deployment mặc định: restartPolicy = Always
│
▼
Script migration chạy xong → thoát code 0 (thành công)
│
▼
Deployment thấy container "chết" → KHỞI ĐỘNG LẠI
│
▼
Migration chạy lại... lại xong... lại restart...
→ CrashLoopBackOff dù script HOÀN TOÀN ĐÚNG. 💥Đây là nghịch lý: với Deployment, thoát ra = lỗi cần khởi động lại. Nhưng với việc batch, thoát ra code 0 = thành công, đừng chạy lại. Deployment không phân biệt được. Ta cần một workload hiểu rằng "hoàn tất" là trạng thái đích, không phải sự cố — đó là Job.
(Nhớ Bài 02: Pod phase Succeeded hợp với Job, Running mãi hợp với web. Giờ ta dùng tới phân biệt đó.)
Job — chạy tới khi hoàn tất
Job tạo một (hoặc nhiều) Pod, chạy tới khi hoàn tất thành công, rồi dừng — không khởi động lại Pod đã xong.
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
namespace: myapp
spec:
template:
spec:
restartPolicy: OnFailure # KHÁC Deployment: Never hoặc OnFailure
containers:
- name: migrate
image: myapp:1.0
command: ["python", "manage.py", "migrate"]
backoffLimit: 4 # thử lại tối đa 4 lần nếu FAIL rồi mới bỏ cuộckubectl apply -f job.yaml
kubectl get jobs
# NAME COMPLETIONS DURATION AGE
# db-migration 1/1 8s 30s ← 1/1 = xong 1 Pod thành công
kubectl logs job/db-migration # xem output migrationCác núm điều khiển quan trọng
completions: 5 cần 5 Pod chạy XONG THÀNH CÔNG thì Job mới coi là done
parallelism: 2 chạy tối đa 2 Pod SONG SONG cùng lúc (tăng tốc batch)
backoffLimit: 4 fail thì thử lại tối đa 4 lần rồi bỏ (tránh lặp vô hạn)
activeDeadlineSeconds: 600 quá 10 phút chưa xong → giết Job (chống treo)
ttlSecondsAfterFinished: 3600 tự XÓA Job + Pod sau 1h khi xong (dọn rác)Ví dụ batch 100 việc, chạy 5 Pod song song:
spec:
completions: 100 # tổng 100 đơn vị việc
parallelism: 5 # 5 Pod cùng lúc
backoffLimit: 6⚠️ restartPolicy của Job chỉ được Never hoặc OnFailure — không được Always (vì Always là bản chất của "chạy mãi", trái ngược với Job). Đây là lỗi copy-paste hay gặp khi bê template Deployment sang.
💡 Job vs khởi động lại:
backoffLimitđếm số lần cả Job thử lại khi fail, khác với việc container tự restart. Fail quábackoffLimit→ Job chuyểnFailed, không thử nữa. DùngactiveDeadlineSecondsđể chặn Job treo vô hạn.
CronJob — Job theo lịch
CronJob tạo Job theo lịch định kỳ — đúng như crontab bạn đã biết ở chặng Bash, nhưng chạy trên cluster.
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-backup
namespace: myapp
spec:
schedule: "0 2 * * *" # 2 giờ sáng mỗi ngày (cú pháp cron chuẩn)
concurrencyPolicy: Forbid # không cho 2 lần backup chồng nhau
successfulJobsHistoryLimit: 3 # giữ log 3 lần chạy thành công gần nhất
failedJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: myapp-backup:1.0
command: ["/bin/sh", "-c", "pg_dump ... > /backup/db-$(date +%F).sql"] schedule "0 2 * * *"
│ │ │ │ └─ thứ trong tuần (0-6, 0=CN)
│ │ │ └─── tháng (1-12)
│ │ └───── ngày trong tháng (1-31)
│ └─────── giờ (0-23)
└───────── phút (0-59)
Ví dụ: "*/5 * * * *" mỗi 5 phút
"0 */6 * * *" mỗi 6 tiếng
"0 0 * * 0" 0h Chủ Nhật hằng tuầnconcurrencyPolicy — chống chồng chéo
Đây là núm quan trọng nhất, và là nguồn của một sự cố kinh điển (kể ở phần chuyện):
Allow (mặc định) cho phép nhiều lần chạy CHỒNG nhau
→ nguy hiểm nếu 1 lần chạy lâu hơn chu kỳ!
Forbid nếu lần trước CHƯA xong, BỎ QUA lần này
→ an toàn cho backup, ETL (không muốn chồng)
Replace hủy lần đang chạy, thay bằng lần mớiNúm khác: startingDeadlineSeconds (nếu lỡ lịch quá lâu thì bỏ), suspend: true (tạm dừng CronJob mà không xóa).
kubectl get cronjobs
kubectl get jobs # thấy các Job do CronJob sinh ra theo thời gian
kubectl create job --from=cronjob/nightly-backup test-now # chạy thử NGAY, không đợi lịch💡 Mẹo test: đừng ngồi đợi 2 giờ sáng.
kubectl create job --from=cronjob/<tên>chạy ngay một lần để kiểm tra logic.
DaemonSet — một Pod trên mỗi node
Bài 03 mới nhắc tên; giờ mở ra. DaemonSet đảm bảo đúng một bản Pod chạy trên MỖI node (hoặc mỗi node khớp selector).
Thêm node mới vào cluster → DaemonSet TỰ đặt 1 Pod lên node đó
Gỡ node → Pod trên đó biến mất theo
┌────────┐ ┌────────┐ ┌────────┐
│worker-1│ │worker-2│ │worker-3│ ← thêm worker-4 thì
│ [agent]│ │ [agent]│ │ [agent]│ tự có [agent] thứ 4
└────────┘ └────────┘ └────────┘Dùng cho những thứ thuộc về node, không thuộc về app:
- Log agent — Fluent Bit, Promtail (gom log mọi container trên node).
- Monitoring — Node Exporter (metric CPU/RAM/disk từng node).
- Networking — CNI (Bài 01), kube-proxy thực ra chạy kiểu này.
- Storage — driver CSI trên mỗi node.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
namespace: monitoring
spec:
selector:
matchLabels: { app: node-exporter }
template:
metadata:
labels: { app: node-exporter }
spec:
tolerations: # để chạy được cả trên node master
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
containers:
- name: node-exporter
image: prom/node-exporter:latest⚠️ Nhớ taint ở Bài 03: node master bị taint nên DaemonSet thường không đặt Pod lên master. Nhưng agent giám sát/log thì cần cả master → phải thêm tolerations để "được phép" lên master. Đây là ứng dụng thực tế của taint/toleration.
Bức tranh đầy đủ — chọn workload nào
Gom lại toàn bộ họ workload đã học để bạn không bao giờ chọn nhầm:
Workload Vòng đời Dùng cho
──────── ──────── ────────
Deployment chạy mãi, Pod vô danh web, API stateless (B03)
StatefulSet chạy mãi, Pod có danh tính DB, Kafka, app có trạng thái (B06)
DaemonSet chạy mãi, 1 Pod/node log/monitor/CNI agent (B07)
Job chạy tới khi XONG migration, batch một lần (B07)
CronJob Job theo lịch backup, báo cáo định kỳ (B07)→ Câu hỏi vàng khi tạo workload: "Việc này chạy mãi hay chạy-rồi-xong? Thuộc về app hay thuộc về node?" — trả lời được là chọn đúng ngay.
🚀 Lab — Job, CronJob, DaemonSet
Dùng cluster kind.
1. Job chạy một lần
cat <<EOF | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata: { name: pi }
spec:
backoffLimit: 2
template:
spec:
restartPolicy: Never
containers:
- name: pi
image: perl:5.34
command: ["perl","-Mbignum=bpi","-wle","print bpi(200)"]
EOF
kubectl get jobs --watch # COMPLETIONS 0/1 → 1/1
kubectl logs job/pi # in 200 chữ số của pi, rồi Job dừng (không restart)2. Job song song
cat <<EOF | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata: { name: batch }
spec:
completions: 6
parallelism: 2 # 2 Pod cùng lúc, tổng 6 lần
template:
spec:
restartPolicy: Never
containers:
- name: work
image: busybox
command: ["sh","-c","echo xu ly 1 viec; sleep 3"]
EOF
kubectl get pods --watch # thấy 2 Pod chạy song song, lần lượt đủ 63. CronJob mỗi phút
cat <<EOF | kubectl apply -f -
apiVersion: batch/v1
kind: CronJob
metadata: { name: hello }
spec:
schedule: "*/1 * * * *" # mỗi phút
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 2
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: hello
image: busybox
command: ["sh","-c","echo Chao luc \$(date)"]
EOF
# Không đợi 1 phút — chạy thử ngay:
kubectl create job --from=cronjob/hello hello-now
kubectl logs job/hello-now
kubectl get jobs --watch # đợi ~1 phút sẽ thấy CronJob tự đẻ Job mới4. Xem DaemonSet có sẵn của hệ thống
kubectl get daemonset -n kube-system
# kube-proxy chạy kiểu DaemonSet — 1 bản mỗi node. CNI cũng vậy.5. Dọn
kubectl delete job pi batch hello-now 2>/dev/null
kubectl delete cronjob hello 2>/dev/null→ Khoảnh khắc "à há" ở bước 1: Job in xong pi rồi dừng hẳn, không CrashLoop — đúng thứ Deployment không làm được. Và bước 3 cho thấy CronJob tự đẻ Job theo lịch y như crontab, nhưng phân tán trên cluster.
Câu chuyện thực tế
Hai sự cố "chạy-rồi-xong" dạy tôi hai bài khác nhau.
Sự cố một — migration chạy trong Deployment. Dev mới lên K8s, cần chạy một script migration DB. Bạn ấy làm cái quen tay nhất: bọc vào Deployment. Script chạy 5 giây, xong, thoát code 0. Deployment thấy "container chết" → restart → migration chạy lại. Vì migration đó không idempotent (chạy lại là lỗi "column already exists"), lần thứ hai trở đi nó fail → CrashLoopBackOff. Nhìn log thì thấy lỗi SQL, tưởng script sai, sửa mãi không được. Thật ra script đúng — sai ở chỗ dùng Deployment cho việc chạy-một-lần. Đổi sang Job, chạy đúng một lần, xong là dừng. 30 phút hoang mang chỉ vì chọn sai loại workload.
Sự cố hai — CronJob backup chồng nhau. Chúng tôi đặt CronJob backup DB mỗi giờ, để mặc định concurrencyPolicy: Allow. Bình thường backup mất ~10 phút, không sao. Cho tới ngày DB phình to, một lần backup kéo hơn 1 tiếng. Giờ tiếp theo, CronJob thản nhiên khởi động lần backup thứ hai trong khi lần đầu chưa xong. Rồi lần thứ ba. Ba tiến trình pg_dump cùng cày một database → I/O nghẽn → database gần như đứng, app production chậm rề. 2 giờ chiều, khách phàn nàn, chúng tôi mới phát hiện cả một đống Job backup đang chạy chồng lên nhau. Đổi concurrencyPolicy: Forbid — nếu lần trước chưa xong thì bỏ qua lần này — và thêm activeDeadlineSeconds. Từ đó êm.
Bài học:
"Chạy-rồi-xong" thì dùng Job, không dùng Deployment. Deployment coi "thoát ra" là lỗi và restart mãi. Migration/batch bọc Deployment = CrashLoop dù code đúng.
concurrencyPolicy: Forbidcho việc không được chồng. Backup, ETL, dọn dẹp — mặc địnhAllowlà bom hẹn giờ khi một lần chạy lâu hơn chu kỳ. Đây là lỗi tôi mong bạn không phải tự vấp.Migration nên idempotent, và có
activeDeadlineSeconds. Chạy lại không được gây hại, và có trần thời gian để không treo vô hạn.ttlSecondsAfterFinishedđể dọn rác. Job xong không tự biến mất — hàng trăm Job cũ đọng lại làm rối cluster. Đặt TTL cho nó tự dọn.DaemonSet cho thứ thuộc về node. Log/monitor agent phải có mặt trên mọi node kể cả master (nhớ toleration) — đó là lúc DaemonSet đúng, không phải Deployment.
Pitfalls
Chạy batch/migration bằng Deployment → CrashLoopBackOff dù code đúng, vì
restartPolicy: Alwaysrestart mãi khi việc xong. Dùng Job.Đặt
restartPolicy: Alwayscho Job → K8s từ chối. Job chỉ nhậnNeverhoặcOnFailure.CronJob
concurrencyPolicy: Allowcho việc dài → các lần chạy chồng nhau, nghẽn tài nguyên. DùngForbid/Replace.Quên
activeDeadlineSeconds→ một Job treo (vd chờ mạng) chạy mãi, ăn tài nguyên. Đặt trần thời gian.Không dọn Job cũ → hàng trăm Job/Pod
Completedđọng lại. DùngttlSecondsAfterFinishedvàsuccessfulJobsHistoryLimit.Migration không idempotent rồi bị retry (
backoffLimit) → lỗi "already exists". Viết migration chạy lại được, hoặcbackoffLimit: 0.DaemonSet quên toleration → agent giám sát không lên được node master (do taint Bài 03) → mù nửa cluster.
Nhầm
parallelismvớireplicas.parallelismlà số Pod chạy song song của một Job hữu hạn; không phải "giữ N bản mãi" như Deployment.CronJob lịch sai timezone. CronJob chạy theo giờ của control plane (thường UTC) — "2 giờ sáng" có thể lệch múi giờ VN. Kiểm tra kỹ, hoặc dùng field
timeZone(K8s ≥ 1.27).Tưởng xóa CronJob sẽ dừng Job đang chạy. Xóa CronJob không giết Job/Pod nó đã sinh ra — dọn cả các Job con nếu cần.
Tóm tắt
- Không phải workload nào cũng chạy mãi. Việc "chạy-rồi-xong" hoặc "theo lịch" cần Job/CronJob, không phải Deployment.
- Deployment coi "thoát ra" là lỗi → restart mãi. Bọc batch/migration vào Deployment = CrashLoop dù code đúng.
- Job chạy tới khi hoàn tất.
restartPolicychỉNever/OnFailure. Núm:completions,parallelism,backoffLimit,activeDeadlineSeconds,ttlSecondsAfterFinished. - CronJob = Job theo lịch cron.
concurrencyPolicy(Forbid cho việc không được chồng), history limit,suspend. Test ngay bằngkubectl create job --from=cronjob/.... - DaemonSet = 1 Pod/mỗi node, tự theo số node. Cho log/monitor/CNI agent. Cần
tolerationsđể lên master. - Chọn workload: hỏi "chạy mãi hay chạy-rồi-xong? thuộc app hay thuộc node?".
Bài sau (Bài 08 — Bảo mật & giới hạn tài nguyên) là bài "làm cho cluster đủ an toàn để lên production". Mặc định, container chạy bằng root, được ăn vô hạn CPU/RAM, và nói chuyện được với mọi Pod khác — ba quả bom. Ta sẽ dựng hàng rào: SecurityContext & Pod Security Standards (container chạy quyền tối thiểu), QoS/LimitRange (chia tài nguyên công bằng), và NetworkPolicy (tường lửa giữa các Pod).
Nếu hôm nay bạn không còn bọc migration vào Deployment, và biết concurrencyPolicy: Forbid cứu bạn khỏi một đêm mất ngủ — bạn đã hiểu nhóm workload chạy-rồi-xong.
— Minh Hưng