Linux 自動化 systemd cron 系統管理

別再只會用 crontab:systemd Timer 讓排程任務可觀測、可除錯

cron 簡單好用,但出了問題你完全不知道發生什麼事。systemd timer 整合 journald 日誌、支援相依性管理、處理機器關機後的補跑,本文說明何時該換用 systemd timer 以及如何從 cron 遷移。

cron 用了幾十年,它哪裡不好?

問題不在功能,而在可觀測性。一個 cron job 失敗,你通常不知道——除非有另一套機制在監控它。crontab -l 看不出上次有沒有跑、跑了多久、有沒有出錯。排錯方式是翻 /var/log/syslog 找幾行不完整的 log,或是把 stdout/stderr 自己導向某個檔案。

systemd timer 解決的核心問題就是這個:每次執行都有完整的 log,journalctl 一個指令就能看,失敗有 exit code、有 timestamp、有完整的 stderr 輸出。

這篇文章不是說 cron 不好,而是幫你判斷什麼情況下應該換成 systemd timer——以及怎麼換。

systemd timer 的基本結構

一個 systemd timer 由兩個檔案組成:.service 定義要跑什麼,.timer 定義什麼時候跑。這比 cron 多一個檔案,但換來的是完整的 systemd 整合。

以一個每天凌晨 3 點備份資料庫為例:

建立 service 檔/etc/systemd/system/db-backup.service):

1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description=Daily PostgreSQL Backup
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=oneshot
User=deploy
ExecStart=/usr/local/bin/db-backup.sh
StandardOutput=journal
StandardError=journal

建立 timer 檔/etc/systemd/system/db-backup.timer):

1
2
3
4
5
6
7
8
9
10
[Unit]
Description=Run db-backup daily at 03:00

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target

啟用並啟動 timer

1
2
3
4
5
6
sudo systemctl daemon-reload
sudo systemctl enable --now db-backup.timer

# 確認 timer 狀態
sudo systemctl status db-backup.timer
sudo systemctl list-timers db-backup.timer

list-timers 會告訴你下次觸發時間是什麼時候,這是 cron 完全沒有的能力。

為什麼 Persistent=true 很重要

cron 有個行為:如果機器在預定的執行時間是關機狀態,那次任務就直接跳過,永遠不補跑。對 24/7 運行的伺服器影響不大,但 VPS 有時候會重開機維護、或是意外宕機。

Persistent=true 告訴 systemd:如果上次預定的執行時間被錯過了,機器開機後立刻補跑一次。備份任務、日誌清理、資料同步,這些都應該設定這個選項。

看執行結果只需要一個指令

這是 systemd timer 最明顯的優勢:

1
2
3
4
5
6
7
8
# 看最近的執行結果
journalctl -u db-backup.service -n 20

# 看今天的執行結果
journalctl -u db-backup.service --since today

# 即時追蹤(debug 時用)
journalctl -u db-backup.service -f

stdout 和 stderr 都自動進 journal,不需要在 script 裡手動 redirect。執行時間、exit code、完整輸出,全部都有。

相比之下,傳統 cron 的除錯方式是:

1
2
3
4
5
6
# 找這個 cron job 有沒有跑
grep CRON /var/log/syslog | grep backup

# 然後發現 log 只有一行:
# CRON[12345]: (root) CMD (/usr/local/bin/db-backup.sh)
# 成功還是失敗?不知道。輸出在哪?不知道。

相依性管理

假設你有一個備份腳本,需要在 PostgreSQL 服務啟動之後才能跑,cron 沒辦法表達這個關係——你只能在 script 裡自己檢查。

systemd 直接支援:

1
2
3
[Unit]
After=postgresql.service
Requires=postgresql.service

加了 Requires=postgresql.service,如果 PostgreSQL 沒有跑起來,這個任務就不會執行。After= 確保執行順序,Requires= 確保相依關係。

另一個實用場景是「開機後等 N 分鐘再執行」:

1
2
3
[Timer]
OnBootSec=10min
OnUnitActiveSec=6h

這個設定讓任務在開機後 10 分鐘執行第一次,之後每 6 小時執行一次。cron 的 @reboot 沒辦法加延遲,只能在 script 裡 sleep,又髒又不可靠。

資源限制:避免備份任務把 CPU 吃光

因為 service 是 systemd 管理的單元,可以直接用 systemd 的 cgroup 機制限制資源:

1
2
3
4
5
[Service]
CPUQuota=30%
MemoryMax=512M
Nice=15 # 降低 CPU 排程優先度
IOSchedulingClass=idle # 磁碟 I/O 用最低優先度

備份或壓縮任務通常不急,但如果不限制資源,跑起來可能讓線上服務的回應變慢。cron job 沒有這個機制,你只能在 script 開頭加 nice -n 19 調整 CPU 優先度,磁碟 I/O 則完全沒辦法控制。

systemd-analyze calendar 驗證時間表達式

systemd 的時間語法和 cron 不同,可以在套用前先驗證:

1
2
# 驗證並看下幾次觸發時間
systemd-analyze calendar "Mon,Wed,Fri *-*-* 09:00:00" --iterations=5

輸出會告訴你接下來五次的觸發時間,避免寫錯語法結果 job 從來沒跑。常見的 cron 語法對照:

cron systemd OnCalendar
0 3 * * * *-*-* 03:00:00
*/5 * * * * *:0/5
0 0 * * 1 Mon *-*-* 00:00:00
@reboot OnBootSec=0

手動觸發測試

不用等到預定時間,可以直接觸發 service 測試:

1
2
3
4
5
sudo systemctl start db-backup.service

# 看結果
sudo systemctl status db-backup.service
journalctl -u db-backup.service -n 30

這比 cron 方便太多。cron 要測試通常得改時間、等執行、看 log,或是直接跑 script 但環境變數又跟 cron 執行環境不一樣。

什麼時候繼續用 cron

systemd timer 不是在所有情況下都更好。這些場景繼續用 cron:

容器環境:Docker 容器裡通常沒有 systemd,cron 還是標準選擇。在容器裡需要排程任務,裝 cron 或用 supercronic 更適合。

簡單的一次性任務:如果只是 0 4 * * * /usr/bin/some-command,而且你也不在意 log,cron 一行搞定,沒必要為此建兩個 unit 檔。

需要跨平台:腳本需要在 FreeBSD、macOS 或 minimal Linux 上跑,cron 是唯一可靠的選項。

對於在 systemd 系統上的新排程任務,特別是需要可靠性和可觀測性的生產環境任務,systemd timer 是更好的預設選擇。


如果你在 NCSE Network 的 VPS 上有排程任務需要管理,NVMe SSD 搭配臺灣機房讓你的自動化腳本跑得穩、跑得快。詳情見 ncse.tw

需要穩定的雲端主機?

NCSE Network 提供企業級 VPS,7 天免費試用,臺灣是方電訊機房,99% SLA 保證。

查看 VPS 方案 →