雜記 › 技術筆記 › Crontab 表達式看不懂?線上解析並預測下次執行時間

Crontab 表達式看不懂?線上解析並預測下次執行時間

年底接手一台離職同事留下的伺服器,營運抱怨「月初的對帳郵件一週收到三封」。我去翻 crontab,只有一行 0 9 1 * 1,腦子裡的第一反應是「每月 1 號、而且正好是週一的那個 9 點」。直到把執行日誌按時間排開才發現不對:它在每月 1 號跑,每個週一也照樣跑——cron 裡「日」和「週」兩個欄位同時被限定時是或的關係。一次語意誤讀,就變成了每週一次的重複推送。這件事逼著我把 cron 的語法重新過了一遍,下面是真正會踩到的部分。

一、五個欄位就是「分 時 日 月 週」,邊界比想像的小

讀法是從左到右,一個空格分隔一個欄位,順序固定的:分鐘、小時、日、月、星期。很多人把「日」和「星期」的優先順序想複雜了,其實它們只是兩個獨立的篩選條件。

欄位取值範圍寫 * 的意思容易看錯的地方
分鐘0–59每一分鐘沒有 60,60 * * * * 是非法的
小時0–23每一小時0 是午夜 0 點,不是「沒有」
日1–31每天起始是 1,不是 0
月1–12每月不是 0–11,和某些程式語言不一樣
星期0–7每週每天0 和 7 都表示週日,1 是週一

所以 0 2 * * * 是「每天 02:00」,30 6 * * 1-5 是「工作日 06:30」。讀表達式時我習慣先盯最後兩個欄位:只要「日」和「週」都不是 *,就要按「或」來理解,這是最容易和直覺衝突的一條。

二、四個符號,*/5 的含義最容易想歪

  • * 任意值;, 列舉,如 0,15,30,45 是這個小時內固定的四個時刻。
  • - 範圍,如 9-18 是 9 到 18 這兩個端點都包含的閉區間。
  • /n 步長。關鍵點:cron 不是「上次跑完等 n 分鐘再跑」,而是在固定的時間網格上取整除的值。*/5 * * * * 落在每小時的第 0、5、10……55 分,整點必觸發;*/7 * * * * 是 0、7、14、21、28、35、42、49、56 分,到下一小時又從 0 開始,所以相鄰兩次的間隔在 4 分鐘和 7 分鐘之間跳,而不是恆定 7 分鐘。這就是「每 7 分鐘」這種需求永遠寫不準的原因。
  • 步長還能帶起點:5-59/10 是第 5、15、25、35、45、55 分,用來把任務錯開整點,避開大家擠在 :00 跑的時段。

三、我確認一個表達式是否寫對的流程

  1. 先在紙上把需求寫成中文句子,明確到「哪一天的幾點幾分」「一週觸發幾次」「一個月觸發幾次」,給出具體次數。
  2. 再翻譯成表達式,然後把它丟進Crontab 解析裡,看每個欄位被解釋成什麼含義,重點確認「日」和「週」是不是自己想要的組合。
  3. 看它預測的未來 10 次執行時間點,把星期和時刻逐個念一遍:我要求「每週三 03:30」,預測裡就必須全是週三,間隔必須是 7 天而不是 6 或 8 天。
  4. 最後 crontab -l 核對寫入磁碟的那一行,注意行首不要有 set -e 之類被當成任務行的內容,也不要有多餘的 Tab。

預測執行時間這一步省不掉。日誌出問題時,「到底幾點跑的」是唯一有效的證據,而表達式本身不會告訴你這個。

四、另外四個把人卡住的細節

  • 時區:cron 按伺服器本地時區解釋每個欄位。一台 UTC 機器上的 0 3 * * *,對應的是台北時間 11:00,正好撞在業務高峰上。核對辦法是比對 date +%s 和 date 的輸出,用時間戳轉換把同一個數字分別按 UTC 和本地時區展開,8 小時的偏差一眼就能看出來;較新的實作(如 cronie)還支援在檔案頂部寫 CRON_TZ 或 TZ 單獨指定某條任務的時區,但這類變數並非所有 cron 版本都認,用之前先在測試機上確認一次。
  • 百分號:% 在 crontab 裡表示「換行並把後續內容當標準輸入」,所以 date +%Y%m%d 必須寫成 date +\%Y\%m\%d,否則任務會無聲失敗。
  • 環境變數:cron 給的 PATH 很窄,通常只有 /usr/bin:/bin,腳本裡直接呼叫 node、python3、aws 經常報「command not found」,而手動執行完全正常。穩妥做法是腳本第一行明確載入環境,或全部用絕對路徑。
  • 沒人收的輸出:任務有輸出時 cron 會嘗試寄郵件,多數伺服器沒有 MTA,看起來就像「什麼也沒發生」。加一句 >> /var/log/cron_backup.log 2>&1,再把 MAILTO="" 關掉,至少留下時間戳線索。

五、常抄的表達式與它們的真實觸發次數

表達式含義一天觸發次數
* * * * *每分鐘1440
*/15 * * * *每 15 分鐘(:00 :15 :30 :45)96
5-59/10 9-18 * * *9 到 18 點內每小時的第 5、15、25、35、45、55 分60
30 3 * * 0每週日凌晨 3:30平均 0.14
0 2 1 * *每月 1 日 02:00平均 0.033
0 9 * * 1-5工作日 09:00平均 0.71
0 9 1 * 1每月 1 日 09:00 或 每週一 09:00約 0.17(一年約 62 次)

最後一行就是我踩的那個。真要「每月第一個週一」,cron 的欄位表達不出來,得讓腳本自己在被觸發時判斷 [ $(date +%d) -le 7 ],條件不滿足就直接退出。

六、常見疑問

Q:cron 能按秒執行嗎?

標準 crontab 是 5 位,最小粒度就是 1 分鐘,做不到秒級。需要更高頻率通常用 * * * * * 裡跑一個內部迴圈,或改用帶 6/7 位(含秒、年)欄位的排程框架,那類語法和 cron 並不完全通用。

Q:週日到底寫 0 還是 7?

兩個都可以,0 和 7 都指週日。團隊裡最好統一一個,混著寫會讓 1-7 和 0-6 這種區間變得難以判斷。

Q:改完 crontab 要重啟服務嗎?

用 crontab -e 編輯的話不用,儲存後常駐程式會自動重新載入;直接編輯 /etc/crontab 或 /etc/cron.d/ 下的檔案時,注意檔案擁有者和權限,權限過寬會被忽略。

Q:為什麼腳本手動跑沒問題,放進 cron 就失敗?

九成是環境差異:PATH 太短、HOME 沒設、相對路徑以 $HOME 為基準、目前工作目錄不是腳本目錄、或者非互動 shell 沒有載入你的 .bashrc。排查時在任務裡先執行一次 env > /tmp/cron_env.txt,和手動跑的 env 對比,差異會直接暴露出來。

Q:任務卡住沒跑完,下一次還會觸發嗎?

會。cron 不關心上一次是否結束,同一任務可能重疊執行。耗時不確定的備份要自己加鎖,常見做法是用 flock 包住命令,拿不到鎖就直接跳過。

回頭看那三封重複的對帳郵件,問題不在伺服器上,而在我把「和」讀成了「與」。表達式這種東西,寫出來只是一半,另一半是確認它到底會落在哪幾個時刻——需求先寫成一句中文,再用未來執行時間點去對,比翻文件管用。

📎 相關文章

PNG 壓了三遍只減 426 位元組:無損壓縮的真實邊界 2026-10-22 WebP 轉 PNG 後體積漲了幾倍:什麼時候必須轉,怎麼把漲幅壓住 2026-10-22 截圖縮到四分之一,小字糊成一團的三種原因 2026-10-22 同樣裁 300×300,頭像為什麼總偏右下角半格 2026-10-22 按每月 1.7 倍預估 18 個月,腳本和表格為什麼給出兩個數 2026-10-22