cron là bộ lập lịch kinh điển của Unix: chạy backup lúc 2h sáng, dọn log mỗi tuần, gọi một script định kỳ. Cú pháp năm trường của nó không khó. Nhưng có một câu than quen thuộc với mọi kỹ sư: "script chạy tay thì hoàn hảo, mà để cron chạy thì im lặng không làm gì cả". Nguyên nhân gần như luôn giống nhau, và nó không nằm ở code của bạn. Bài này (phần 11 loạt Unix) chạy một crond thật trong container để bắt tận tay cái bẫy đó và cách sửa.

Cú pháp: năm trường thời gian

Mỗi dòng crontab là năm trường thời gian rồi tới lệnh:

┌─ phút (0-59)
│ ┌─ giờ (0-23)
│ │ ┌─ ngày trong tháng (1-31)
│ │ │ ┌─ tháng (1-12)
│ │ │ │ ┌─ thứ trong tuần (0-6, 0=Chủ nhật)
* * * * *  /duong/dan/job.sh     # mỗi phút
0 2 * * *  backup.sh             # 2h sáng hằng ngày
*/15 * * * * check.sh            # mỗi 15 phút
0 9 * * 1  bao-cao.sh            # 9h sáng thứ Hai

* là "mọi giá trị", */N là "mỗi N", danh sách 1,15 và khoảng 1-5 cũng được. Quản lý bằng crontab -e (sửa), crontab -l (xem), crontab -r (xoá).

Ảnh chụp đoạn mã nền tối minh hoạ cron chạy tác vụ theo lịch và vì sao job im lặng thất bại, cú pháp năm trường thời gian cộng lệnh phút 0-59 giờ 0-23 ngày trong tháng 1-31 tháng 1-12 thứ trong tuần 0-6 0 là chủ nhật sao sao sao sao sao job.sh mỗi phút 0 2 sao sao sao backup.sh 2h sáng mỗi ngày sao chia 15 sao sao sao sao check.sh mỗi 15 phút, bẫy 1 cron chạy với môi trường tối giản cron không nạp bashrc profile PATH rất ngắn thường chỉ usr bin bin lệnh ngoài PATH đó not found chạy tay thì được cron thì không gần như luôn do PATH, cách sửa set PATH hoặc dùng đường dẫn tuyệt đối PATH usr local go bin usr local bin usr bin bin hoặc trong job.sh dùng usr local go bin go thay vì go và source môi trường cần thiết ở đầu script, bẫy 2 không có output không biết lỗi luôn ghi log sao sao sao sao sao job.sh append var log job.log 2 lớn hơn 1 cron gửi output qua mail nội bộ thường không ai đọc chuyển hướng ra file log mới thấy được lỗi thật

Hình 1: Crontab gồm năm trường thời gian (phút/giờ/ngày/tháng/thứ) + lệnh; bẫy #1 là môi trường tối giản (PATH ngắn); cách sửa là set PATH/đường dẫn tuyệt đối; bẫy #2 là không ghi log nên không thấy lỗi.

Đo thật: bẫy PATH, bắt tận tay

Điều khiến job cron thất bại thầm lặng: cron chạy với môi trường tối giản. Nó không nạp ~/.bashrc, ~/.profile hay các biến bạn quen có trong terminal. Đặc biệt, PATH của cron rất ngắn — thường chỉ /usr/bin:/bin. Bất kỳ lệnh nào cài ở nơi khác (như go ở /usr/local/go/bin) sẽ không tìm thấy. Mình chạy crond thật để chứng minh:

Ảnh chụp bảng kết quả chạy thật crond thi hành job mỗi phút output thật, một PATH khác nhau shell thường vs môi trường cron shell thường go bin usr local go bin usr local bin bin môi trường cron usr bin bin thiếu usr local go bin, hai cron thật chạy job lúc 10 24 01 không set PATH 10 24 01 chay job PATH usr bin bin job.sh line 4 go command not found job thất bại nhưng nếu không ghi log thì không ai biết, ba sau khi set PATH trong crontab chạy lúc 10 25 01 PATH go bin usr local go bin usr local bin usr bin bin sao sao sao sao sao job.sh 10 25 01 chay job PATH go bin usr local go bin go version go1.23.12 linux arm64 giờ chạy đúng, kết crond thật thi hành đúng đầu mỗi phút 01 khác biệt duy nhất giữa thất bại và thành công là dòng PATH không phải code

Hình 2: Chạy thật — PATH shell thường có /usr/local/go/bin, môi trường cron chỉ /usr/bin:/bin; crond fire lúc 10:24:01 báo go: command not found; sau khi thêm PATH=... vào crontab, job chạy lúc 10:25:01 cho go version go1.23.12 linux/arm64.

Đây là bằng chứng end-to-end, không phải mô phỏng: crond thật thi hành job đúng đầu mỗi phút (:01). Lần đầu (không set PATH), job chạy với PATH=/usr/bin:/bin và go — dù cài sẵn — báo command not found. Sau khi thêm một dòng PATH=... vào crontab, job phút sau chạy trơn tru và in đúng phiên bản Go. Khác biệt duy nhất giữa thất bại và thành công là dòng PATH — code không đổi một ký tự.

Hai cách sửa

  • Set PATH ở đầu crontab: thêm PATH=/usr/local/go/bin:/usr/local/bin:/usr/bin:/bin trước các dòng job. Đơn giản, áp cho mọi job trong crontab đó.
  • Dùng đường dẫn tuyệt đối trong script: gọi /usr/local/go/bin/go thay vì go, và source môi trường cần thiết ở đầu script. Bền hơn khi script được gọi từ nhiều nơi.

Bẫy thứ hai: không có log = mù

Cron gửi output (stdout/stderr) của job qua mail nội bộ của hệ thống — mà gần như không ai đọc. Nên khi job lỗi, bạn không thấy gì. Luôn chuyển hướng ra file log:

* * * * * /work/job.sh >> /var/log/job.log 2>&1

2>&1 gộp cả stderr vào file (đúng bài học thứ tự redirect ở phần pipe/fd). Không có dòng này, một job hỏng sẽ hỏng âm thầm hàng tháng trời — chính là kịch bản "sao backup không chạy?" kinh điển.

Đánh đổi cần cân nhắc

cron không "bù" lần chạy bị lỡ. Nếu máy tắt đúng lúc job đến hạn, cron bỏ qua lần đó — không chạy bù khi máy bật lại. Với máy tính cá nhân/laptop hay tắt, dùng anacron (chạy bù các job ngày/tuần/tháng bị lỡ) hoặc systemd timer với Persistent=true. cron hợp nhất cho server luôn bật.

Múi giờ và định dạng % là bẫy nhỏ. cron chạy theo múi giờ của hệ thống (hoặc CRON_TZ) — đặt sai là job chạy lệch giờ, đúng như bẫy TZ khi triển khai. Và ký tự % trong dòng crontab có nghĩa đặc biệt (xuống dòng cho stdin), phải thoát \% — hay gặp khi dùng date +%Y%m%d trực tiếp trong crontab; bọc trong script để tránh.

systemd timer là lựa chọn hiện đại cho nhiều tình huống. Trên hệ thống dùng systemd, systemd timer cho log tích hợp (journalctl), phụ thuộc dịch vụ, chạy bù, và quản lý tập trung hơn cron. cron vẫn đơn giản và có mặt khắp nơi; chọn theo môi trường và nhu cầu giám sát.

Ba ý mang về

  1. cron chạy với môi trường tối giản: đo thật crond fire lúc 10:24:01 với PATH=/usr/bin:/bin khiến go: command not found dù go đã cài — đây là lý do #1 của "chạy tay được, cron thì không". Sửa bằng set PATH trong crontab hoặc dùng đường dẫn tuyệt đối (đo thật job chạy đúng ở phút kế).
  2. Luôn ghi log job cron: cron gửi output qua mail nội bộ mà không ai đọc — thêm >> /var/log/job.log 2>&1 để lỗi không hỏng âm thầm.
  3. Biết giới hạn của cron: cú pháp năm trường (phút/giờ/ngày/tháng/thứ); cron không chạy bù lần lỡ (dùng anacron/systemd timer cho máy hay tắt), chạy theo múi giờ hệ thống, và % cần thoát trong crontab.

Nguồn

Phần sau khép lại loạt với bài tổng hợp: vì sao script shell hỏng, và dùng shellcheck để bắt trước hàng loạt lỗi (quoting, biến chưa gán, exit code) mà chúng ta đã gặp suốt loạt bài.