cron 工作不執行?5 個原因與排查方法
cron 工作不執行,常見原因有 5 個:PATH 過短、未跳脫百分比符號、使用錯誤的 crontab、輸出寄到郵件,以及指令碼依賴 shell 環境。
為何你的 cron 工作未執行
「完全未執行」的 cron 工作幾乎總是曾經執行過。它在不同於你 shell 的環境中執行,於第一秒便失敗,而訊息則被送到你未查看的位置。幾乎所有這類問題都可由 5 個原因解釋:搜尋路徑、百分比符號、錯誤的 crontab 檔案、輸出被寄送至郵件,以及指令碼預期存在登入工作階段。
cron 是一個 daemon(背景服務),會讀取 crontab 檔案,並依排程啟動命令。它不會讀取你的 .bashrc,不會開啟終端機,不會啟動 login shell,也不會在命令失敗時通知你。以下每個原因都源自這 4 個事實。
請依順序檢查,並先回答所有問題的核心:cron 是否確實觸發?「cron 從未啟動工作」與「工作已啟動但隨即終止」是不同問題,兩者沒有共同原因,因此應先確認這一點。
Cron 是否確實執行?
不同發行版系列使用不同的 daemon unit 名稱。請檢查兩者,再讀取日誌。
systemctl status cron
systemctl status crond
journalctl -u cron --since "2 hours ago"
journalctl -u crond --since "2 hours ago"Debian 和 Ubuntu 將此 unit 稱為 cron。Fedora、Rocky 和 Alma 則稱為 crond。特定機器上只會存在其中一個名稱,因此其中一個指令回報找不到 unit 屬於正常情況,不代表發生錯誤。
請讀取系統自身寫入的項目。不要尋找指南中複製的特定日誌行,因為不同 cron 實作與日誌設定的文字可能不同。你只需要確認兩件事:排程指定的分鐘是否有對應項目,以及該項目是否列出你的命令。列出你的命令表示 cron 已完成自身工作,故障位於命令內部。完全沒有項目表示 cron 從未取得你的排程,原因是下方的原因 3。
某些映像檔會透過 rsyslog 將 cron 訊息寫入檔案,而不是寫入 journal。請在 /var/log 中尋找以 cron 或 syslog 命名的檔案,然後讀取檔案末端的內容。
ls -l /var/log
sudo tail -n 50 /var/log/syslog如果 unit 和日誌都不存在,可能只是尚未安裝 cron。精簡的雲端映像檔與容器通常不會預先安裝 cron。
dpkg -l cron
rpm -q cronie
sudo apt install cron
sudo dnf install cronie
sudo systemctl enable --now cron原因 1:cron 沒有您的 PATH
互動式 shell 會從 /etc/profile、~/.profile、~/.bashrc 以及這些檔案所載入的內容建立 PATH。cron 工作不會執行這些內容。cron 會使用精簡的環境啟動命令,因此位於標準系統目錄以外的程式會找不到。/usr/local/bin、/opt 底下的程式、語言版本管理工具、Python 虛擬環境或 Go 工作區都可能受到影響。工作會在第一行失敗,而 shell 會寫入類似「找不到」的錯誤;實際文字取決於執行該工作的 shell。
找出工作所使用每個命令的實際路徑。
command -v docker
command -v node
readlink -f "$(command -v node)"接著,將這些絕對路徑寫入工作,或在 crontab 頂端設定一次 PATH。
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * /usr/local/bin/mytool run使用自己電腦上的 echo "$PATH" 取得這份清單,並移除只存在於互動式工作階段中的項目。這裡有一項規則很重要:cron 不會展開這些指派行中的變數。PATH=$PATH:/usr/local/bin 會儲存字面文字 $PATH:/usr/local/bin,因此工作最後取得的搜尋路徑完全沒有可用的目錄。請寫出完整清單。
版本管理工具不只需要路徑。nvm、pyenv、rbenv 和 asdf 會從您的 .bashrc 安裝 shell 函式或 shims 目錄,而 cron 工作永遠不會讀取該檔案。請使用絕對路徑呼叫指定版本的二進位檔,或將版本管理工具的初始化指令碼作為您自行撰寫之 script 的第一行。
原因 2:百分號會結束命令
在 crontab 的命令欄位中,% 不是一般字元。第一個未逸出的 % 會結束命令。其後的所有內容會以標準輸入傳給命令,而後續每個 % 都會轉換成換行。這是 cron 用來將短內容傳給程式的實際功能,也是日期標記檔名經典錯誤 crontab 項目的原因。
寫入 0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +%F).tar.gz /srv/site 時,tar 永遠不會取得格式化後的日期。cron 會在第一個 % 處截斷該行,因此 shell 會收到未完成的命令替換,而該行剩餘內容則會以標準輸入傳入。請在每個百分號前加上反斜線。
0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +\%F).tar.gz /srv/site這一行會依序由兩個層次讀取。\% 是由 cron 在啟動任何程序前套用的 cron 規則。$(date +\%F) 是由 cron 啟動的 shell 稍後套用的 命令替換。掌握每個字元由哪個層次處理,就是解決問題的關鍵。
較安全的做法是完全不要在 crontab 中放入邏輯。請將邏輯寫入 script,因為在 script 中百分號沒有特殊意義。
#!/bin/bash
set -euo pipefail
stamp="$(date +%F)"
tar -czf "/srv/backups/site-${stamp}.tar.gz" /srv/site如此一來,crontab 行只包含路徑和重新導向,不含其他內容。一眼就能讀懂的 crontab,才容易進行除錯。
原因 3:你編輯的是哪個 crontab?
crontab 並不只有一個。系統中有多個檔案,擁有者與欄位數量各不相同。若將工作寫入錯誤的檔案,系統就不會讀取它。
crontab -e會編輯執行該命令之使用者的 crontab。sudo crontab -e則會編輯 root 的 crontab。兩個人排查同一台伺服器時,經常會讀到兩個不同的檔案。sudo crontab -l -u deploy會列出其他使用者的 crontab。你可以藉此確認應執行該工作的帳號目前實際安裝了哪些工作。/etc/crontab以及/etc/cron.d中的每個檔案,都會在排程與命令之間多出一個欄位,用來指定執行命令的使用者。若將包含 5 個欄位的使用者 crontab 工作列貼到/etc/cron.d,命令的第一個字就會被當成使用者名稱。/etc/cron.d中的檔案名稱只能包含字母、數字、底線與連字號。名稱為backup.sh或site.conf的檔案會因為檔名不符合規則而被略過。將它重新命名為backup,再檢查 log。/etc/cron.d中的檔案應由 root 擁有,且群組與其他使用者不得具備寫入權限。ls -l /etc/cron.d可以一次顯示這兩項資訊。- 放入
/etc/cron.daily及其同類目錄的 script 也必須符合相同的命名規則,並具備執行權限。缺少執行權限會使檔案直接被略過,且不會產生明顯訊息。 /etc/cron.allow與/etc/cron.deny會決定哪些使用者可以安裝 crontab。如果你的伺服器上存在其中任一檔案,請先讀取它,再假設你的使用者可以安裝 crontab。
請使用 crontab 命令安裝使用者 crontab,不要手動編輯 spool 檔案,因為 crontab 會在安裝前剖析該檔案。儲存後,請讀取命令回傳的內容。如果檔案遭拒絕,先前的版本仍會繼續生效,你的變更也不會套用。這種情況看起來就像 cron 忽略了你的設定。
擁有者也會決定權限。root 的 crontab 中的工作會建立由 root 擁有的檔案,讀取這些檔案的應用程式可能無法寫入。一般使用者 crontab 中的工作則無法讀取只有 root 能存取的目錄。請讓擁有者符合工作需求:應用程式維護工作應由該應用程式自己的帳號執行,這也是 以 system cron job 取代 WordPress wp-cron 背後的原因。工作建立之檔案的模式來自它所繼承的 umask,而該值可能與 shell 使用的 umask 不同。因此,如果工作的輸出檔案無法讀取,建議參閱 umask 如何設定檔案權限。
原因 4:輸出寄到沒有人閱讀的郵件
cron 會收集工作寫入標準輸出與標準錯誤的所有內容。只要工作產生任何輸出,cron 就會將文字交給本機郵件系統,寄給 crontab 擁有者,或寄給 MAILTO 指定的對象。在精簡的 VPS 上,通常未安裝 MTA (mail transfer agent),因此沒有任何元件會傳送這些郵件。錯誤訊息只存在片刻,之後就無處可去。這就是損壞的工作看起來沒有任何錯誤訊息的完整原因。
請改為將輸出寫入由你控制的檔案。
0 3 * * * /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1>> 會將標準輸出附加至檔案。2>&1 會將標準錯誤導向目前標準輸出的目的地,因此必須放在該重新導向之後。若順序相反,寫成 2>&1 >> file,標準錯誤會保留原本的目的地,而你要追查的錯誤正是未寫入檔案的部分。
journal 也是適合的目標。logger 會使用你指定的標籤寫入 syslog。
0 3 * * * /usr/local/sbin/backup-site.sh 2>&1 | logger -t backup-site使用 journalctl -t backup-site 讀取這些內容。這會將工作本身的輸出放在 cron 項目旁邊,方便追蹤時間順序。如果還需要記錄哪位使用者在主機上執行了哪些命令,那是另一套系統;稽核伺服器上的使用者命令涵蓋這項需求。
在 crontab 頂端加入 MAILTO="",即可關閉其下方工作的郵件通知。只有在系統存在可運作的 MTA 時,將 MAILTO 設為實際地址才有作用,因此在依賴這項功能前,請先確認郵件確實能離開主機。
除錯時請遵守一項規則:絕不要附加 > /dev/null 2>&1。這是每個 crontab 中最常見的一行,卻會丟棄你唯一擁有的證據。等工作正常運作後,若仍有需要,再將它加回去。
原因 5:指令碼假設了 cron 不會提供的環境
找到指令並擷取其輸出後,剩下的問題就是工作階段自動提供給你的其他環境條件。
- shell 不一定是 bash。使用
ls -l /bin/sh檢查。在 Debian 和 Ubuntu 上,該指令會指向 dash,因此雙中括號測試、陣列及source會因語法錯誤而失敗。為指令碼加入#!/bin/bash行並直接呼叫指令碼,或在 crontab 頂端設定SHELL。 - 工作目錄不是你原本所在的目錄。所有位置都使用絕對路徑,或在指令碼第一行使用
cd切換至指定目錄。相對路徑是工作「手動執行時正常」的最常見單一原因。 - locale 不是工作階段使用的 locale。任何會格式化日期或數字,或排序文字的程式,在不同的
LANG下都可能產生不同輸出。若後續步驟會剖析該輸出,請在指令碼中設定 locale,不要依賴預設值。 - 沒有 TTY(終端機)。需要確認、開啟編輯器或繪製進度列的指令可能會停住或結束。加入該工具提供的非互動旗標。
- 沒有 SSH agent。cron 的環境中沒有
SSH_AUTH_SOCK,因此原本因 agent 已載入而能正常驗證的ssh或rsync指令,現在會驗證失敗。請為工作建立專用 key,並將其擁有者設為執行該工作的使用者。 - 沒有 user session bus,因此 cron 工作中的
systemctl --user會失敗,直到設定XDG_RUNTIME_DIR為止。使用 system unit 是較好的做法。
在 Fedora、Rocky 和 Alma 上,還有另一個可能原因。SELinux 會限制 cron 工作,因此即使檔案權限正確,工作存取標籤異常的路徑時仍會遭到拒絕。使用 sudo ausearch -m avc -ts recent 檢查拒絕事件,並先閱讀 伺服器的 SELinux 基礎知識,再停用任何設定。
顯示 cron 環境的 1 分鐘探測
不要再猜測 cron 的環境包含哪些內容,直接讀取它。建立一個會傾印所有內容的指令碼,設定為每分鐘執行一次,等待後再讀取檔案。
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在實際執行該工作之使用者的 crontab 中加入一行,兩側都使用絕對路徑。
* * * * * /home/deploy/cron-probe.sh >> /home/deploy/cron-probe.log 2>&1等待 1 分鐘,然後讀取 /home/deploy/cron-probe.log,再與您在自己的 shell 中執行相同指令的結果比較。PATH 行、工作目錄與 locale 通常就能自行說明失敗原因。請注意設定中的兩個細節:百分比符號位於指令碼內,因此不受 cron 規則影響;日誌路徑則必須是該工作使用者可以寫入的位置。
找到答案後立即刪除該 crontab 行。每分鐘執行一次並持續附加內容的工作會填滿小容量磁碟,而且過程不會發出明顯提示。
排程是你原本要設定的嗎?
使用者 crontab 的一行開頭包含 5 個欄位:分鐘、小時、每月第幾日、月份、星期幾。其中 2 個欄位會互相作用,容易造成誤解。
當每月第幾日和星期幾都有限制時,也就是兩者都不是 *,cron 會在任一欄位符合時執行工作。0 0 13 * 5 不是「13 號星期五」。它會在每月 13 號的午夜,以及每個星期五的午夜執行。若要指定單一日期,請將其中一個欄位保留為 *,再在腳本中檢查另一個欄位。
cron 使用系統時區。許多 VPS 映像檔預設設為 UTC(協調世界時),因此你排定在 03:00 執行的工作,實際上會在 03:00 UTC 執行,這可能是你下午的時段。timedatectl 會顯示你的主機實際使用的時區。請查看主機設定,不要假設它與筆電相同。
另外還有 2 個值得注意的排程陷阱。@reboot 會在 cron 本身啟動時觸發,這不等同於網路已就緒。因此,需要 DNS 或遠端主機的工作可能會在開機時失敗,但之後每次手動執行都成功。此外,若工作執行時間較長,前一個執行個體尚未結束時,下一個執行個體仍可能啟動。請使用鎖定機制包住工作。
*/5 * * * * /usr/bin/flock -n /tmp/backup-site.lock /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1flock -n 在鎖定已被持有時會立即結束,因此重疊的執行個體會停止,不會與第一個執行個體同時持續執行。
systemd timer 是較適合的工具
cron 擅長的事情只有一項:在指定時間執行命令。除此之外,它的功能都較弱。timer 不需要重新導向即可提供 journal,也能查詢後續的結束狀態、針對 network-online.target 排定執行順序,並加入隨機延遲,避免數百台伺服器在同一秒開始執行。當工作需要其中任何一項功能時,在 VPS 上設定 systemd service 與 timer,比維護一行 crontab 更省力。撰寫 service 檔案時,還會遇到 cron 從未要求你思考的問題:unit 如何判定工作確實已開始。因此,請先閱讀 Type= 對 simple、forking 與 notify unit 的意義。在預設類型下自行 daemonize 的 script,會讓 unit 看似處於 active 狀態,實際上卻沒有任何程序在背後執行。重試行為也應在此處理,因為 systemd restart policy 會決定失敗後的處置方式,而 cron 完全無法回答這個問題。
小型工作仍可使用 cron。具有相依條件或重試政策的工作,則應改用 timer。兩者可以在同一台伺服器上並存,因此不必一次完成整個遷移。
FAQ
為什麼我的 cron 工作手動執行正常,從 cron 執行卻失敗?
因為你的 shell 環境與 cron 的環境不同。登入 shell 會讀取 /etc/profile 與 ~/.bashrc,這些檔案會設定 PATH、locale 及 agent 變數。cron 啟動命令時不會載入這些設定,工作目錄也不同,有時使用的 shell 也不同。每個命令都使用絕對路徑,並在 crontab 頂端或 script 內設定所需項目。此外,排程一個每分鐘執行的探測工作,將 env | sort、pwd 與 id 的結果寫入 log 檔案。這樣即可讀取 cron 的實際環境,不必猜測。
如何確認 cron 確實執行了我的工作?
讀取 daemon 的 log。在 Debian 與 Ubuntu 上使用 journalctl -u cron;在 Fedora、Rocky 與 Alma 上使用 journalctl -u crond。某些映像檔會改由 rsyslog 將訊息寫入 /var/log 下的檔案。尋找排程指定的那一分鐘是否有記錄,並確認記錄中包含你的命令。沒有記錄表示 cron 根本沒有取得該排程,因此請確認你編輯的是正確的 crontab。有記錄但沒有結果,表示命令已啟動後即終止,因此請使用 redirect 擷取其輸出。
為什麼 date +%Y 在 crontab 內會出錯?
cron 會將命令欄位中的 % 視為特殊字元。第一個未跳脫的 % 會結束命令,後續內容會以 standard input 的形式傳給該命令,而之後每個 % 都會變成換行。因此,使用日期格式產生的檔名不會傳給原本要執行它的程式。將每個 percent 字元跳脫為 \%,或將命令移至 script,再由 cron 呼叫該 script;在 script 內,percent sign 沒有特殊意義。
cron 工作的輸出會寫到哪裡?
輸出會傳送至本機 mail system,收件者是 crontab 的擁有者,或由 MAILTO 指定的對象。大多數 VPS 映像檔未安裝 mail transfer agent,因此訊息會被丟棄,工作看起來就像沒有輸出。使用 >> /path/to/log 2>&1 將輸出 redirect 至檔案,並維持該順序,讓 standard error 跟隨 standard output;也可以透過 logger -t myjob 傳送,再使用 journalctl -t myjob 讀回。仍在除錯時,不要使用 > /dev/null 2>&1。
應該使用 cron 還是 systemd timer?
固定時間執行簡單命令時使用 cron,尤其是你可能需要將工作移至未執行 systemd 的機器時。若需要在不使用 redirect 的情況下將輸出寫入 journal、查詢 exit status、在 network 啟動後排序執行、設定隨機啟動延遲,或在失敗後採用 retry policy,則使用 timer。兩者可以在同一台伺服器上並存,因此可在各項工作逐漸需要這些功能時逐一遷移。