Cron 表達式揭密:五個欄位、範圍、步進與星期 OR 陷阱
五個欄位及其有效範圍
每個標準的 cron 表達式由恰好五個以空白字元分隔的欄位組成,由左至右依序為:分鐘(0–59)、小時(0–23)、日期(1–31)、月份(1–12)以及星期(0–7)。分鐘始終是最左邊的欄位——常見錯誤是從右到左讀取 cron 行,這會使意義完全相反。星期欄位同時接受 0 和 7 作為星期日,這是繼承自 Unix 傳統的設計,允許兩種慣例都能正常運作。
月份欄位在支援 Vixie cron 慣例的實作中也可以接受 jan 到 dec 的三字母縮寫,星期欄位則接受 sun 到 sat。但為了在不同環境之間的可攜性,使用數字值更為安全。Quartz Scheduler(用於 Java 應用程式)在最左側增加了一個秒欄位,AWS EventBridge 等雲端排程器則增加了可選的第七個年份欄位——這些擴充格式與 POSIX 五欄位標準不相容。
萬用字元、逗號清單、連字號範圍與斜線步進
四種特殊字元控制欄位的匹配方式:星號(*)匹配該欄位的每個有效值——* * * * * 每分鐘觸發一次。逗號(,)建立離散值清單:月份欄位中的 1,3,5 代表一月、三月和五月。連字號(-)定義包含端點的範圍:小時欄位中的 9-17 匹配 9 點到 17 點的每個整點,適用於「僅限上班時間」的排程。這三種字元可以在單一欄位中組合使用:星期欄位中的 1-5,0 匹配週一到週五加上週日。
斜線(/)引入步進值。使用星號後時,分鐘欄位中的 */15 產生集合 {0, 15, 30, 45}——從欄位最小值開始的每個 15 的倍數。使用範圍後時,0-30/5 產生 {0, 5, 10, 15, 20, 25, 30}——該範圍內每隔 5 個值。步進始終從範圍的左邊界開始(對於 * 則從 0 開始),而非從隨機偏移量開始。這就是為什麼分鐘欄位中的 */5 無論何時啟動 cron 或新增任務,都會在 0、5、10……觸發。
日期與星期 OR 陷阱
cron 中最令人意外的行為發生在排程中同時設定日期欄位和星期欄位為非萬用字元值時。直覺上,你可能認為 0 2 15 * 5 意味著「僅在當月 15 日且為週五時凌晨 2 點執行」——AND 條件。然而在實際上,包括 Vixie cron 和 dcron 在內的大多數實作將此視為 OR 條件:任務會在每月 15 日凌晨 2 點觸發,同時也會在每個週五凌晨 2 點觸發,無論日期為何。cron(5) 手冊頁明確記載了這一點:「如果同時指定了 DOM 和 DOW,當任一欄位匹配當前時間時,命令就會執行。」
當開發者試圖表達複合日期條件時,這種行為往往造成困擾。如果你真的需要「每月第三個週五」,cron 無法直接表達。慣用的解決方案是排程在每個週五執行(0 2 * * 5),並在命令開頭加入 shell 測試:[ $(date +\%d) -ge 15 ] && [ $(date +\%d) -le 21 ] && /path/to/script。另一種方案是使用原生支援行事曆循環規則(RFC 5545 RRULE)的高階排程器,例如 systemd 計時器的 OnCalendar= 指令。
預定義捷徑:@reboot、@hourly、@daily、@weekly、@monthly
Vixie cron 引入了一組具名捷徑,用於替代常見排程的五欄位語法。@reboot 在 cron 守護程序本身啟動後立即執行一次任務——適用於輕量級啟動工作,但對於正式服務,建議使用適當的 init 系統(如 systemd unit)。@hourly 等同於 0 * * * *,在每小時的第 0 分觸發。@daily(別名 @midnight)等同於 0 0 * * *;@weekly 等同於 0 0 * * 0(週日午夜);@monthly 等同於 0 0 1 * *(每月第一天午夜);@yearly(別名 @annually)等同於 0 0 1 1 *(1 月 1 日午夜)。
這些捷徑提高了 crontab 檔案的可讀性,但依賴於 cron 守護程序識別 @ 語法——POSIX 並未定義這些語法,因此在某些精簡或嵌入式 cron 實作中無法使用。為雲端排程器(AWS EventBridge、Google Cloud Scheduler、GitHub Actions)撰寫 cron 表達式時,請查閱提供者文件,因為大多數雲端平台不支援 @ 捷徑,需要明確的五欄位格式。
步進值與「不是從現在起每 N 分鐘」的陷阱
關於步進語法的常見誤解是將 */N 理解為「從此任務建立時起每 N 個單位」。事實並非如此。步進是對絕對時間值的取模過濾。分鐘欄位中的 */5 是集合 {x : x mod 5 = 0, 0 ≤ x ≤ 59} = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55} 的簡寫——每小時 12 個固定時鐘位置。cron 守護程序將這些位置與當前時鐘分鐘對比;它不記憶你的任務是何時新增的。
這導致 0 */6 * * * 產生一個微妙問題。此表達式在小時 0、6、12 和 18 觸發——午夜、上午 6 點、中午和下午 6 點——每天恰好四次。如果你在下午 3 點新增此任務並預期「六小時後的下午 9 點首次執行」,你會驚訝地發現:下一次執行是下午 6 點(小時 18)。若需要在下午 9 點執行,請使用明確清單 0 9,15,21,3 * * * 或使用 0 3/6 * * * 改變起始小時——但請注意,起始/步進 表示法是 Vixie cron 的擴充功能,並非所有地方都支援;為了最大可攜性,請始終使用明確的逗號分隔清單。
時區:cron 排程中的隱形錯誤
傳統 cron 守護程序在其所在機器的系統時區中執行所有任務。在設定為 UTC 的伺服器上——這是大多數 Docker 容器基礎映像和許多雲端 VM 的預設值——0 8 * * 1-5 並不意味著你所在地的上午 8 點;它意味著 UTC 上午 8 點。如果你的團隊在 UTC+9(日本標準時間),該任務會在本地時間下午 5 點觸發。AWS CloudWatch Events(現已改名為 EventBridge)的 cron 表達式記載為僅使用 UTC。GitHub Actions 的 on: schedule: 也是 UTC。忽視這一點可能導致每週帳單報告在本地凌晨 3 點而非預期的上午 8 點執行。
日光節約時間(DST)引入了兩種額外的故障模式。當時鐘向前撥(例如從 01:59 跳至 03:00)時,排程在跳過小時內的 cron 任務在那天根本不會執行。當時鐘向後撥(例如從 02:59 回到 02:00)時,同一分鐘出現兩次;任務執行一次還是兩次取決於守護程序的實作。最安全的生產策略是讓 cron 伺服器永遠以 UTC 運行,並在應用層進行本地時間轉換,或使用接受明確 TimeZone 參數的排程器,例如具有 TimeZone= 的 systemd 計時器、加入了 .spec.timeZone 的 Kubernetes 1.25+ CronJob,或接受 IANA 時區名稱的 AWS EventBridge Scheduler。