Deployment giữ thứ chạy mãi. Job chạy một lần rồi thôi. Bài này đo ba cách nó hỏng, và một cái bẫy mặc định của CronJob.

Độ lùi khi thử lại, độ lệch của deadline, và ba chính sách chồng lượt

Thử lại với độ lùi gấp đôi

backoffLimit: 4, container luôn exit 1:

pod thứ 1 tạo ra ở giây   0,1
pod thứ 2 tạo ra ở giây  10,3
pod thứ 3 tạo ra ở giây  30,7
pod thứ 4 tạo ra ở giây  71,5

Khoảng cách: 10, 20, 40 giây — gấp đôi mỗi lần.

Tổng cộng 5 pod (lần đầu cộng 4 lần thử lại), rồi Job được đánh dấu BackoffLimitExceeded.

Độ lùi bị chặn ở 6 phút. Với backoffLimit mặc định là 6, một job hỏng ngay lập tức vẫn mất khoảng 20 phút để đi hết chuỗi thử lại — điều đáng biết khi bạn ngồi chờ nó kết luận.

activeDeadlineSeconds cộng thêm 31 giây cố định

Đo bằng chính dấu thời gian của Kubernetes (status.startTime tới lastTransitionTime của điều kiện Failed):

activeDeadlineSeconds Bị cắt sau Chênh
15 46 giây +31
30 61 giây +31
60 91 giây +31

Không phải nhân, mà là cộng một hằng số.

Lần đầu tôi đo mỗi giá trị 15 giây và thấy "gấp 3,1 lần", suýt viết thành một quy luật về tỉ lệ. Đo thêm hai giá trị nữa thì lộ ra đó là hằng số — và kết luận về tỉ lệ sẽ hoàn toàn sai với deadline dài.

Đây là bài học lặp lại từ sê-ri Kafka: một điểm dữ liệu không phân biệt được nhân với cộng. Cần ít nhất ba điểm ở các mức khác nhau.

Con số 31 giây là chu kỳ đồng bộ của bộ điều khiển Job. activeDeadlineSecondsmức sàn, không phải hạn chót — cùng tính chất với retention.ms của Kafka (phần 17).

Hệ quả: đừng dùng activeDeadlineSeconds cho ràng buộc thời gian chặt. Với job phải xong trong 10 giây, nó sẽ chạy 41 giây.

CronJob: ba chính sách chồng lượt

CronJob chạy mỗi phút, mỗi lượt mất 150 giây — cố ý để lượt sau bắt đầu khi lượt trước chưa xong:

Thời điểm Allow Forbid Replace
0 s job=1 / chạy=1 job=1 / chạy=1 job=1 / chạy=1
51 s job=2 / chạy=2 job=1 / chạy=1 job=1 / chạy=1
101 s job=3 / chạy=3 job=1 / chạy=1 job=1 / chạy=1
152 s job=4 / chạy=3 job=2 / chạy=1 job=1 / chạy=1

Allow — mặc định. Các lượt chồng lên nhau, đỉnh 3 lượt cùng chạy.

Forbid — bỏ qua lượt mới khi lượt cũ chưa xong. Số job tăng (lượt bị bỏ vẫn được ghi nhận) nhưng số đang chạy luôn là 1.

Replace — giết lượt cũ, chỉ luôn có đúng một job.

Mặc định là Allow, và đó là cái bẫy: một job chạy lâu hơn chu kỳ lịch sẽ tự nhân bản cho tới khi hết tài nguyên — và không có gì cảnh báo.

Kịch bản thật: job dọn dẹp chạy mỗi 5 phút, bình thường mất 2 phút. Một ngày dữ liệu tăng lên và nó mất 7 phút. Từ đó mỗi lượt chồng lên lượt trước, số job tăng dần, và nếu chúng cùng ghi vào một nơi thì kết quả hỏng.

Với gần như mọi công việc định kỳ, Forbid là lựa chọn đúng.

Ba tham số CronJob nên khai

spec:
  schedule: "*/5 * * * *"
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 200
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3

startingDeadlineSeconds — nếu bộ điều khiển bỏ lỡ một lượt (control plane vừa khởi động lại), nó có được chạy bù không, và trong bao lâu. Không khai thì lượt bị lỡ không bao giờ chạy.

Nhưng đừng đặt quá dài: nếu CronJob bỏ lỡ hơn 100 lượt trong khoảng deadline, bộ điều khiển ngừng lên lịch hẳn và ghi lỗi Cannot determine if job needs to be started. Đây là cách một CronJob im lặng chết sau một sự cố dài.

successfulJobsHistoryLimit — mặc định 3. Job cũ và pod của chúng nằm lại trong etcd. Với CronJob chạy mỗi phút, giữ nhiều là tốn thật.

ttlSecondsAfterFinished

apiVersion: batch/v1
kind: Job
spec:
  ttlSecondsAfterFinished: 600

Job xong sẽ tự xoá sau 10 phút, kể cả pod của nó. Không có nó, Job hoàn thành nằm lại vĩnh viễn — và một hệ thống tạo Job theo yêu cầu sẽ tích luỹ hàng nghìn object mà không ai để ý.

Đây là chỗ phần 45 nói tới: etcd phình ra vì object không ai dọn.

Job song song

spec:
  completions: 10      # cần 10 lần chạy thành công
  parallelism: 3       # tối đa 3 pod cùng lúc

Không khai gì thì cả hai bằng 1 — một pod, một lần thành công.

Với completions không khai nhưng parallelism: 5, Job trở thành hàng đợi công việc: 5 pod chạy song song, và Job xong khi một pod thoát với mã 0. Mẫu này hợp với việc lấy việc từ một hàng đợi bên ngoài.

restartPolicy chỉ có hai giá trị

Job không cho restartPolicy: Always — nó mâu thuẫn với ý nghĩa "chạy một lần rồi thôi".

  • Never — container hỏng thì tạo pod mới. backoffLimit đếm số pod.
  • OnFailure — container hỏng thì khởi động lại trong cùng pod. backoffLimit đếm số lần khởi động lại.

Never dễ gỡ lỗi hơn vì mỗi lần thử để lại một pod riêng với log riêng. OnFailure tiết kiệm hơn nhưng log của lần trước bị mất trừ khi bạn dùng --previous (phần 54).

Thử ba mươi giây

Tìm CronJob có nguy cơ chồng lượt:

kubectl get cronjobs -A -o custom-columns=\
NS:.metadata.namespace,TEN:.metadata.name,\
LICH:.spec.schedule,\
CHINH_SACH:.spec.concurrencyPolicy,\
DANG_CHAY:.status.active

Cột CHINH_SACH rỗng nghĩa là Allow. Cột DANG_CHAY có nhiều hơn một phần tử nghĩa là đang chồng ngay lúc này — và đó là thứ nên sửa trước khi nó thành sự cố.

Phần sau đo HorizontalPodAutoscaler: mở rộng mất 30 giây, thu gọn mất 258 giây.