SSD Nodes Learn 🎉 VPS từ $5.50/tháng
Hướng dẫn Matt ConnorBởi Matt Connor

5 lý do cron job không chạy và cách kiểm tra

Cron job không chạy thường do PATH tối giản, dấu % chưa escape, sai crontab, mất output qua mail hoặc script cần login shell. Kiểm tra từng lỗi.

Vì sao cron job của bạn không chạy

Một cron job “không bao giờ chạy” gần như luôn đã từng chạy. Nó chạy trong một environment không phải shell của bạn, fail ngay trong giây đầu tiên, và thông báo được gửi đến nơi bạn không đọc. Năm nguyên nhân giải thích gần như mọi trường hợp báo lỗi: search path, dấu phần trăm, file crontab sai, output được gửi qua mail và script cần một login session.

cron là một daemon (dịch vụ chạy nền) đọc các file crontab và khởi chạy command theo lịch. Nó không đọc .bashrc của bạn, không mở terminal, không khởi động login shell và không cho bạn biết khi command fail. Mọi nguyên nhân bên dưới đều xuất phát từ bốn thực tế này.

Hãy kiểm tra lần lượt theo thứ tự và bắt đầu bằng câu hỏi bao quát tất cả các nguyên nhân: cron có thực sự kích hoạt job không? “cron không bao giờ khởi động job” và “job đã khởi động rồi thoát” là hai vấn đề khác nhau, không có điểm chung, vì vậy hãy trả lời câu hỏi đó trước.

Cron có thực thi hay không?

Daemon này có tên unit khác nhau trên từng nhóm bản phân phối. Kiểm tra cả hai tên, sau đó đọc log.

systemctl status cron
systemctl status crond
journalctl -u cron --since "2 hours ago"
journalctl -u crond --since "2 hours ago"

Debian và Ubuntu gọi unit này là cron. Fedora, Rocky và Alma gọi là crond. Trên mỗi máy chỉ tồn tại một trong hai tên này, nên một trong hai lệnh báo unit không tồn tại là điều bình thường, không phải lỗi.

Đọc các entry do chính hệ thống của bạn ghi. Không tìm một dòng được chép từ tài liệu hướng dẫn, vì cách diễn đạt khác nhau giữa các triển khai cron và các cấu hình logging. Bạn chỉ cần kiểm tra hai việc: có entry tại đúng phút mà lịch chạy chỉ định hay không, và entry đó có ghi tên command của bạn hay không. Entry ghi tên command nghĩa là cron đã hoàn thành phần việc của nó, còn lỗi nằm bên trong command. Không có entry nào nghĩa là cron chưa từng nhận schedule của bạn; đây là nguyên nhân 3 bên dưới.

Một số image chuyển message của cron qua rsyslog vào file thay vì journal. Tìm trong /var/log một file có tên liên quan đến cron hoặc syslog, rồi đọc phần cuối file.

ls -l /var/log
sudo tail -n 50 /var/log/syslog

Nếu cả unit và log đều không tồn tại, có thể cron chưa được cài đặt. Các cloud image tối giản và container thường không cài sẵn cron.

dpkg -l cron
rpm -q cronie
sudo apt install cron
sudo dnf install cronie
sudo systemctl enable --now cron

Nguyên nhân 1: cron không có PATH của bạn

Shell tương tác của bạn tạo PATH từ /etc/profile, ~/.profile, ~/.bashrc và mọi file được source từ các file đó. Không có phần nào trong số này chạy cho cron job. cron khởi động command với environment rút gọn riêng, nên không tìm thấy program nằm ngoài các thư mục hệ thống chuẩn. Mọi thứ trong /usr/local/bin, /opt, language version manager, Python virtual environment hoặc Go workspace đều có thể gây ra lỗi này. Job fail ngay ở dòng đầu tiên. Shell ghi một lỗi dạng "not found"; nội dung chính xác phụ thuộc vào shell đã chạy job.

Tìm path thực của mọi command mà job sử dụng.

command -v docker
command -v node
readlink -f "$(command -v node)"

Sau đó, ghi các absolute path đó vào job hoặc đặt PATH một lần ở đầu crontab.

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * /usr/local/bin/mytool run

Lấy danh sách đó trên chính máy của bạn bằng echo "$PATH", rồi xóa mọi mục chỉ tồn tại trong interactive session. Có một quy tắc quan trọng: cron không mở rộng biến trong các dòng assignment này. PATH=$PATH:/usr/local/bin lưu nguyên văn đoạn text $PATH:/usr/local/bin, nên job sẽ có một search path không chứa thư mục nào dùng được. Hãy ghi ra toàn bộ danh sách.

Version manager cần nhiều hơn một path. nvm, pyenv, rbenv và asdf cài một shell function hoặc một thư mục shims từ .bashrc, nhưng cron không bao giờ đọc file đó. Hãy gọi binary theo version bằng absolute path hoặc source init script của manager ở dòng đầu tiên trong script của bạn.

Nguyên nhân 2: dấu phần trăm kết thúc command của bạn

Trong trường command của crontab, % không phải là ký tự thông thường. % đầu tiên không được escape sẽ kết thúc command. Mọi nội dung sau đó được truyền cho command dưới dạng standard input, và mỗi % tiếp theo sẽ trở thành một dòng mới. Đây là tính năng có thật của cron để truyền input ngắn cho một chương trình. Đây cũng là lý do filename có ngày tháng là dạng crontab thường bị lỗi.

Viết 0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +%F).tar.gz /srv/site thì tar không bao giờ nhận được ngày tháng đã format. cron cắt dòng tại % đầu tiên. Vì vậy shell nhận một command substitution chưa hoàn chỉnh, còn phần còn lại của dòng được truyền vào dưới dạng standard input. Hãy escape mọi dấu phần trăm bằng backslash.

0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +\%F).tar.gz /srv/site

Một dòng đó được đọc qua 2 lớp, theo thứ tự. \% là một cron rule, được cron áp dụng trước khi khởi chạy bất kỳ thứ gì. $(date +\%F)command substitution, được shell do cron khởi chạy áp dụng sau đó. Nắm rõ lớp nào xử lý ký tự nào là điểm mấu chốt.

Cách an toàn hơn là không đặt logic trực tiếp trong crontab. Hãy đưa logic vào một script, nơi dấu phần trăm không có ý nghĩa đặc biệt.

#!/bin/bash
set -euo pipefail
stamp="$(date +%F)"
tar -czf "/srv/backups/site-${stamp}.tar.gz" /srv/site

Khi đó, dòng crontab chỉ chứa path và redirect, không có gì khác. Crontab có thể đọc và hiểu ngay là crontab bạn có thể debug.

Nguyên nhân 3: bạn đã sửa crontab nào?

Không chỉ có một crontab. Có nhiều file crontab, với chủ sở hữu và số lượng trường khác nhau. Nếu ghi job vào nhầm file, cron sẽ không nhìn thấy job đó.

  • crontab -e sửa crontab của user chạy lệnh. sudo crontab -e sửa crontab của root. Hai người cùng debug một máy thường đọc hai file khác nhau.
  • sudo crontab -l -u deploy liệt kê crontab của user khác. Đây là cách xác nhận nội dung thực sự đã được cài cho account cần chạy job.
  • /etc/crontab và mọi file trong /etc/cron.d có thêm một trường giữa lịch chạy và command: user cần chạy job. Nếu dán một dòng crontab 5 trường của user vào /etc/cron.d, từ đầu tiên của command sẽ bị đọc thành username.
  • Tên các file trong /etc/cron.d chỉ được chứa chữ cái, chữ số, dấu gạch dưới và dấu gạch ngang. File tên backup.sh hoặc site.conf sẽ bị bỏ qua chỉ vì tên không hợp lệ. Đổi tên thành backup rồi kiểm tra lại log.
  • Các file trong /etc/cron.d phải thuộc sở hữu của root và không được cho group hoặc các user khác quyền ghi. ls -l /etc/cron.d hiển thị cả hai thông tin này cùng lúc.
  • Các script đặt trong /etc/cron.daily và những thư mục tương tự phải tuân theo cùng quy tắc đặt tên, đồng thời phải có execute bit. Thiếu execute bit sẽ khiến script bị bỏ qua mà không có thông báo rõ ràng.
  • /etc/cron.allow/etc/cron.deny quyết định ai được phép cài crontab. Nếu một trong hai file này tồn tại trên máy, hãy đọc nó trước khi giả định user của bạn được phép cài crontab.

Cài user crontab bằng command crontab thay vì tự sửa spool file, vì crontab sẽ phân tích file trước khi cài. Sau khi lưu, hãy đọc nội dung command in ra. Nếu command từ chối file, phiên bản trước vẫn tiếp tục hoạt động và thay đổi của bạn không có hiệu lực. Trường hợp này trông giống hệt cron đang bỏ qua bạn.

Owner cũng quyết định quyền truy cập. Job trong crontab của root tạo ra các file thuộc sở hữu của root, trong khi application đọc các file đó có thể không có quyền ghi. Job trong crontab của user thông thường không thể đọc thư mục chỉ root mới truy cập được. Hãy chọn owner phù hợp với công việc: tác vụ bảo trì application nên chạy bằng account riêng của application. Đây là lý do của việc thay WordPress wp-cron bằng system cron job. Mode của các file do job tạo ra được lấy từ umask mà job kế thừa. Giá trị này không phải umask của shell, vì vậy hãy đọc cách umask thiết lập quyền file nếu output của job bị tạo ở trạng thái không thể đọc.

Nguyên nhân 4: output được gửi vào hộp thư không ai đọc

cron thu thập mọi thứ mà một job ghi vào standard output và standard error. Nếu job ghi ra bất kỳ nội dung nào, cron chuyển nội dung đó cho local mail system, gửi đến chủ sở hữu của crontab hoặc địa chỉ mà MAILTO chỉ định. Trên VPS tối giản thường không cài MTA (mail transfer agent), nên không có thành phần nào gửi thư đi. Lỗi của bạn chỉ tồn tại trong chốc lát rồi biến mất. Đó là toàn bộ lý do một job bị lỗi nhưng trông như không có output.

Thay vào đó, hãy gửi output vào một file mà bạn kiểm soát.

0 3 * * * /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1

>> nối standard output vào file. 2>&1 trỏ standard error đến đích mà standard output đang trỏ tới tại thời điểm đó, nên phải đặt sau redirect. Nếu viết theo thứ tự ngược lại, như 2>&1 >> file, standard error vẫn giữ đích ban đầu, và lỗi bạn đang tìm chính là phần không bao giờ được ghi vào file.

journal là một đích phù hợp khác. logger ghi vào syslog với tag do bạn chọn.

0 3 * * * /usr/local/sbin/backup-site.sh 2>&1 | logger -t backup-site

Đọc lại bằng journalctl -t backup-site. Cách này giữ output của job ngay cạnh các cron entry, nên dễ theo dõi timeline. Nếu bạn cũng cần ghi lại người nào đã chạy lệnh nào trên máy chủ, đó là một hệ thống riêng; kiểm tra audit các lệnh người dùng trên máy chủ trình bày phần này.

MAILTO="" ở đầu crontab sẽ tắt mail cho các job bên dưới. Đặt MAILTO thành một địa chỉ thật chỉ có tác dụng nếu đã có MTA hoạt động, vì vậy hãy xác nhận mail thực sự rời khỏi máy chủ trước khi phụ thuộc vào cách này.

Một quy tắc khi debug: không bao giờ nối thêm > /dev/null 2>&1. Đây là dòng phổ biến nhất trong mọi crontab, nhưng nó loại bỏ bằng chứng duy nhất bạn có. Sau này, khi job đã chạy đúng, bạn có thể thêm lại nếu muốn.

Nguyên nhân 5: script giả định các biến môi trường mà cron không cung cấp

Khi đã tìm thấy command và thu thập output của nó, phần còn lại là mọi thứ khác mà session của bạn tự động cung cấp.

  • Shell có thể không phải là bash. Kiểm tra bằng ls -l /bin/sh. Trên Debian và Ubuntu, nó trỏ đến dash, nên phép kiểm tra bằng dấu ngoặc vuông kép, array và source sẽ lỗi cú pháp. Thêm dòng #!/bin/bash vào script rồi gọi script, hoặc đặt SHELL ở đầu crontab.
  • Working directory không phải thư mục hiện tại của session. Dùng absolute path ở mọi nơi, hoặc dùng cd để chuyển đến thư mục đó ngay ở dòng đầu tiên của script. Relative path là lý do phổ biến nhất khiến một job "chạy được khi tôi chạy thủ công".
  • Locale không giống session của bạn. Bất kỳ thứ gì format ngày tháng hoặc số, hoặc sort văn bản, đều có thể tạo output khác dưới một LANG khác. Nếu bước sau parse output đó, hãy đặt locale trong script thay vì phỏng đoán.
  • Không có TTY (terminal). Command yêu cầu xác nhận, mở editor hoặc vẽ progress bar có thể bị treo hoặc thoát. Thêm flag non-interactive mà tool cung cấp.
  • Không có SSH agent. SSH_AUTH_SOCK không nằm trong environment của cron, nên command ssh hoặc rsync trước đó chạy được vì agent của bạn đã nạp key nay sẽ không xác thực được. Cấp cho job một key riêng, do user chạy job sở hữu.
  • Không có user session bus, nên systemctl --user từ cron job sẽ fail cho đến khi XDG_RUNTIME_DIR được set. System unit là cách phù hợp hơn.

Trên Fedora, Rocky và Alma còn có một khả năng khác. SELinux giới hạn cron job, nên một job truy cập path có label không đúng sẽ bị từ chối ngay cả khi file permission có vẻ chính xác. Kiểm tra các lần từ chối bằng sudo ausearch -m avc -ts recent, rồi đọc kiến thức cơ bản về SELinux cho server trước khi tắt bất kỳ cơ chế nào.

Probe một phút cho thấy môi trường của cron

Đừng đoán môi trường của cron chứa gì. Hãy đọc nó. Viết một script ghi toàn bộ môi trường ra file, lập lịch chạy mỗi phút, chờ rồi đọc file đó.

cat > /home/deploy/cron-probe.sh <<'EOF'
#!/bin/bash
echo "=== probe ==="
date -Is
pwd
id
echo "SHELL=$SHELL"
echo "LANG=$LANG"
command -v node || echo "node is not on this PATH"
env | sort
EOF
chmod +x /home/deploy/cron-probe.sh

Thêm một dòng vào crontab của user chạy job thực tế, dùng đường dẫn tuyệt đối ở cả hai phía.

* * * * * /home/deploy/cron-probe.sh >> /home/deploy/cron-probe.log 2>&1

Chờ một phút, rồi đọc /home/deploy/cron-probe.log và so sánh với kết quả của cùng các lệnh khi chạy trong shell của bạn. Dòng PATH, thư mục làm việc và locale thường đã đủ để giải thích lỗi. Lưu ý 2 chi tiết trong cấu hình này: dấu phần trăm nằm bên trong script, nơi quy tắc của cron không áp dụng, và đường dẫn log là đường dẫn mà user chạy job có quyền ghi.

Xóa dòng crontab đó ngay khi có câu trả lời. Một job chạy mỗi phút và append vào file có thể làm đầy disk nhỏ, và nó sẽ làm việc đó một cách âm thầm.

Lịch này có đúng là lịch bạn muốn không?

Một dòng crontab của user bắt đầu bằng 5 trường: phút, giờ, ngày trong tháng, tháng, ngày trong tuần. Hai trường trong số đó tương tác với nhau theo cách dễ gây nhầm lẫn.

Khi cả ngày trong tháng và ngày trong tuần đều bị giới hạn, nghĩa là cả hai trường đều không phải *, cron chạy job khi một trong hai trường khớp. 0 0 13 * 5 không có nghĩa là “thứ Sáu ngày 13”. Nó chạy lúc nửa đêm vào ngày 13 của mỗi tháng và lúc nửa đêm vào mỗi thứ Sáu. Để chạy vào một ngày cụ thể, hãy để một trong hai trường là * và kiểm tra trường còn lại bên trong script.

cron dùng timezone của hệ thống. Nhiều image VPS được cấu hình là UTC (giờ phối hợp quốc tế), nên job bạn lên lịch lúc 03:00 sẽ chạy lúc 03:00 UTC, có thể là giữa buổi chiều theo giờ địa phương của bạn. timedatectl in ra timezone mà máy của bạn thực sự đang dùng. Hãy kiểm tra timezone của máy thay vì giả định nó giống laptop.

Có thêm 2 lỗi về lịch mà bạn nên biết. @reboot chạy khi chính cron khởi động. Thời điểm này không đồng nghĩa với lúc network đã sẵn sàng, nên job cần DNS hoặc remote host có thể fail khi boot rồi lại chạy thành công trong mọi lần chạy thủ công sau đó. Ngoài ra, không có gì ngăn một job chạy chậm khởi động lại trong khi bản trước vẫn đang chạy. Hãy bọc job bằng lock.

*/5 * * * * /usr/bin/flock -n /tmp/backup-site.lock /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1

flock -n thoát ngay khi lock đã được giữ, nên lần chạy bị chồng lấp sẽ dừng thay vì tiếp tục chạy chồng lên lần đầu.

Khi systemd timer là lựa chọn phù hợp hơn

cron chỉ làm tốt một việc: chạy lệnh này vào thời điểm này. Ở các khía cạnh khác, cron khá hạn chế. timer cung cấp journal mà không cần redirect, cho phép truy vấn exit status sau đó, sắp xếp thứ tự chạy với network-online.target và thêm độ trễ ngẫu nhiên để hàng trăm server không cùng khởi động ở một giây. Khi job cần bất kỳ tính năng nào trong số đó, một systemd service và timer trên VPS sẽ ít công sức hơn so với việc phải xử lý các vấn đề phát sinh từ một dòng crontab. Cơ chế retry cũng nên đặt ở đó, vì các chính sách restart của systemd quyết định điều gì xảy ra sau một lần lỗi, còn cron hoàn toàn không có cơ chế trả lời cho vấn đề này.

Hãy tiếp tục dùng cron cho các job nhỏ. Chuyển mọi job có dependency hoặc retry policy sang timer. Cả hai đều có thể chạy trên cùng một server, nên bạn không cần hoàn tất việc chuyển đổi trong một lần.

FAQ

Vì sao cron job chạy thủ công nhưng lại lỗi khi chạy từ cron?

Vì môi trường của shell và cron khác nhau. Login shell đọc /etc/profile~/.bashrc, các file này thiết lập PATH, locale và biến của agent. cron khởi chạy lệnh mà không có các thiết lập đó, từ một working directory khác và đôi khi dùng shell khác. Dùng absolute path cho mọi command, thiết lập các giá trị cần dùng ở đầu crontab hoặc bên trong script, rồi lên lịch một probe job chạy env | sort, pwdid vào file log trong 1 phút để đọc môi trường thực tế của cron thay vì đoán.

Làm thế nào để kiểm tra cron có thực sự chạy job của tôi không?

Đọc log của daemon. Dùng journalctl -u cron trên Debian và Ubuntu, hoặc journalctl -u crond trên Fedora, Rocky và Alma; một số image chuyển các message qua rsyslog vào file bên dưới /var/log. Tìm entry tại phút được ghi trong lịch và kiểm tra entry đó có tên command của bạn không. Không có entry nghĩa là cron chưa nhận được schedule, vì vậy hãy xác nhận bạn đã sửa đúng crontab. Có entry nhưng không có kết quả nghĩa là command đã khởi chạy rồi thoát, vì vậy hãy capture output bằng redirect.

Vì sao date +%Y bị lỗi trong crontab?

cron xử lý % là ký tự đặc biệt trong command field. % đầu tiên không được escape sẽ kết thúc command, mọi nội dung sau đó được truyền cho command dưới dạng standard input, và mỗi % tiếp theo sẽ trở thành một newline. Vì vậy, filename có định dạng ngày không đến được program mà bạn viết cho nó. Escape từng dấu phần trăm thành \%, hoặc chuyển command vào một script rồi gọi script đó từ cron, vì bên trong script dấu phần trăm không có ý nghĩa đặc biệt.

Output của cron job đi đâu?

Output được gửi đến local mail system, tới chủ sở hữu của crontab hoặc địa chỉ mà MAILTO chỉ định. Hầu hết VPS image không cài mail transfer agent, nên message bị loại bỏ và job có vẻ như không tạo output. Redirect output vào file bằng >> /path/to/log 2>&1, giữ đúng thứ tự đó để standard error đi theo standard output, hoặc pipe output qua logger -t myjob rồi đọc lại bằng journalctl -t myjob. Không dùng > /dev/null 2>&1 khi bạn vẫn đang debug.

Nên dùng cron hay systemd timer?

Dùng cron cho command đơn giản chạy vào một thời điểm cố định, nhất là khi bạn có thể cần chuyển command đó sang máy không chạy systemd. Dùng timer khi muốn output nằm trong journal mà không cần redirect, muốn truy vấn exit status, sắp xếp chạy sau khi network hoạt động, thêm độ trễ khởi chạy ngẫu nhiên hoặc thiết lập chính sách retry sau khi lỗi. Cả hai đều có thể chạy trên cùng một server, vì vậy bạn có thể chuyển từng job một khi cần.