Qua hai điểm luôn kẻ được một đường thẳng, nhưng đường đó có thể sai hoàn toàn với điểm thứ ba. Đây là bài mà tôi suýt công bố một quy luật sai vì đúng cái bẫy đó — chỉ cứu được nhờ đo thêm. 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 mà một job chạy hơi lâu là đủ kích hoạ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. activeDeadlineSeconds là mứ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).
Mẫu số chung: một điểm không phân biệt được cộng với nhân, và việc định kỳ chậm hơn chu kỳ sẽ tự chồng
Bài này gói hai bài học vượt khỏi Kubernetes. Thứ nhất, một kỷ luật đo lường mà tôi suýt vi phạm: một điểm dữ liệu vẽ vừa mọi đường cong bạn giả định trước — muốn phân biệt một hằng số cộng với một tỉ lệ nhân, phải đo ở ít nhất ba mức khác nhau. "+31 cố định" trông y hệt "×3,1" khi chỉ có một điểm. Đây đúng là chuyện đo độ phức tạp thuật toán ở một kích thước đầu vào thì không tài nào tách O(n) khỏi O(n²); là một tỉ lệ tính trên hai lần thử (câu "sai 1/1 lần = 100%"); là ngoại suy một xu hướng từ hai điểm. Trước khi tuyên bố một quy luật co giãn, hãy đo ở nhiều thang — Spring @Scheduled, benchmark, hay bất cứ đâu bạn định nói "cái này tỉ lệ với cái kia".
Thứ hai: một tác vụ lặp theo lịch mà chạy lâu hơn chu kỳ của nó sẽ chồng lên chính mình và tự nhân bản, trừ khi có một chốt chặn. CronJob mặc định Allow là cái bẫy self-multiplying đó. Cùng hình dạng khắp nơi: một dòng cron không có flock/lockfile chạy đè lên lượt trước; một setInterval trong JavaScript bắn lượt mới trước khi callback bất đồng bộ của lượt cũ xong; và rõ nhất trong Java — @Scheduled(fixedRate=...) có thể chồng nếu tác vụ dài hơn rate, trong khi fixedDelay chờ lượt trước xong rồi mới đếm tiếp. Nguyên tắc: một việc lặp lại phải quyết định trước — bỏ qua hay chờ — khi lượt trước chưa xong; đừng để mặc định là "cứ chạy chồng lên".
Mẫu số chung: đừng suy một quy luật từ một điểm đo — lấy ít nhất ba mức để biết là cộng hay nhân; và với mọi việc chạy định kỳ, hãy khai rõ điều gì xảy ra khi lượt trước chưa xong, vì im lặng thường có nghĩa là chồng lượt cho tới khi vỡ.
Muốn tự soi, 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.