前幾天在使用 Terraform + cloud-init 批次初始化我的實驗室 Linux 機器。正好發現有一些定時場景需要使用到 cronjob, 進一步瞭解到 systemd timer 完全可以替換 cronjob, 並且 systemd timer 有一些非常有趣的功能。
迴歸話題:為什麼我推薦你使用 systemd timer 替代 cronjob? 因為相比 cronjob, systemd timer 有這些優勢:
接下來我們一一介紹。
首先我們通過系統自帶的 timer 來熟悉這個新玩意。
當 Ubuntu 或任何基於 systemd 的發行版安裝在一個新系統上時,它會建立幾個 timer,作為任何 Linux 主機後臺的系統維護程式的一部分。這些 timer 會觸發普通維護任務所需的事件,比如更新系統資料庫、清理臨時目錄、切割紀錄檔檔案等等。
我們使用systemctl status *timer
命令列出我的主機上的所有 timer:
casey@casey-Virtual-Machine:~$ systemctl status *timer
● plocate-updatedb.timer - Update the plocate database daily
Loaded: loaded (/lib/systemd/system/plocate-updatedb.timer; enabled; vendor preset: enabled)
Active: active (waiting) since Tue 2023-04-04 16:49:49 CST; 19s ago
Trigger: Wed 2023-04-05 00:40:16 CST; 7h left
Triggers: ● plocate-updatedb.service
4 月 04 16:49:49 casey-Virtual-Machine systemd[1]: Started Update the plocate database daily.
● fwupd-refresh.timer - Refresh fwupd metadata regularly
Loaded: loaded (/lib/systemd/system/fwupd-refresh.timer; enabled; vendor preset: enabled)
Active: active (waiting) since Tue 2023-04-04 16:49:49 CST; 19s ago
Trigger: Wed 2023-04-05 01:54:51 CST; 9h left
Triggers: ● fwupd-refresh.service
4 月 04 16:49:49 casey-Virtual-Machine systemd[1]: Started Refresh fwupd metadata regularly.
● update-notifier-motd.timer - Check to see whether there is a new version of Ubuntu available
Loaded: loaded (/lib/systemd/system/update-notifier-motd.timer; enabled; vendor preset: enabled)
Active: active (waiting) since Tue 2023-04-04 16:49:50 CST; 19s ago
Trigger: Sat 2023-04-08 03:19:02 CST; 3 days left
Triggers: ● update-notifier-motd.service
4 月 04 16:49:50 casey-Virtual-Machine systemd[1]: Started Check to see whether there is a new version of Ubuntu available.
● fstrim.timer - Discard unused blocks once a week
Loaded: loaded (/lib/systemd/system/fstrim.timer; enabled; vendor preset: enabled)
Active: active (waiting) since Tue 2023-04-04 16:49:49 CST; 19s ago
Trigger: Tue 2023-04-04 17:58:23 CST; 1h 8min left
Triggers: ● fstrim.service
Docs: man:fstrim
4 月 04 16:49:49 casey-Virtual-Machine systemd[1]: Started Discard unused blocks once a week.
...
每個 timer 至少有六行資訊與之相關:
Docs: man:fstrim
優勢之一:統一紀錄檔收集到 systemd 紀錄檔
為了更快了解 timer, 我們建立自己的 service unit 和 timer unit 來觸發。
具體用途為:每週定期更新 tailscale 的版本。
首先,建立 tailscale update 服務,如下:
[Unit]
Description=Tailscale update
Wants=tailscale-weekly-update.timer
[Service]
Type=oneshot
ExecStart=/usr/bin/tailscale update -yes
[Install]
WantedBy=multi-user.target
然後,建立 tailscale update timer, 如下:
[Unit]
Description=Tailscale update
Requires=tailscale-weekly-update.service
[Timer]
Unit=tailscale-weekly-update.service
OnCalendar=weekly
[Install]
WantedBy=timers.target
最後,啟用 timer:
systemctl enable tailscale-weekly-update.timer
這樣就可以了,但是為了演示,執行:systemctl start tailscale-weekly-update.service
手動執行一次。
輸出會直接整合到 systemd 紀錄檔裡,並可以通過 journalctl
檢視:(包含手動執行紀錄檔,和後續自動定期執行的紀錄檔)
$ sudo journalctl -S "2023-03-29 00:00:00" -u tailscale-weekly-update.service
4 月 02 09:14:28 casey-Virtual-Machine systemd[1]: Starting Tailscale node agent...
4 月 02 09:14:30 casey-Virtual-Machine tailscale[6898]: 獲取:1 https://pkgs.tailscale.com/stable/ubuntu jammy InRelease
4 月 02 09:14:30 casey-Virtual-Machine tailscale[6898]: 獲取:2 https://pkgs.tailscale.com/stable/ubuntu jammy/main amd64 Packages [7,853 B]
4 月 02 09:14:32 casey-Virtual-Machine tailscale[6898]: 已下載 13.9 kB,耗時 1 秒 (14.4 kB/s)
4 月 02 09:14:32 casey-Virtual-Machine tailscale[6898]: 正在讀取軟體包列表。..
4 月 02 09:14:33 casey-Virtual-Machine tailscale[7101]: 正在讀取軟體包列表。..
4 月 02 09:14:33 casey-Virtual-Machine tailscale[7101]: 正在分析軟體包的依賴關係樹。..
4 月 02 09:14:33 casey-Virtual-Machine tailscale[7101]: 正在讀取狀態資訊。..
4 月 02 09:14:33 casey-Virtual-Machine tailscale[7101]: 下列軟體包將被升級:
4 月 02 09:14:33 casey-Virtual-Machine tailscale[7101]: tailscale
4 月 02 09:14:34 casey-Virtual-Machine tailscale[7101]: 升級了 1 個軟體包,新安裝了 0 個軟體包,要解除安裝 0 個軟體包,有 4 個軟體包未被升級。
4 月 02 09:14:34 casey-Virtual-Machine tailscale[7101]: 需要下載 23.0 MB 的歸檔。
4 月 02 09:14:34 casey-Virtual-Machine tailscale[7101]: 解壓縮後將會空出 1,024 B 的空間。
4 月 02 09:14:34 casey-Virtual-Machine tailscale[7101]: 獲取:1 https://pkgs.tailscale.com/stable/ubuntu jammy/main amd64 tailscale amd64 1.38.3 [23.0 MB]
4 月 02 09:15:13 casey-Virtual-Machine tailscale[7115]: debconf: 無法初始化前端介面:Dialog
4 月 02 09:15:13 casey-Virtual-Machine tailscale[7115]: debconf: (系統未設定 TERM 環境變數,所以對話方塊介面將不可使用。)
4 月 02 09:15:13 casey-Virtual-Machine tailscale[7115]: debconf: 返回前端介面:Readline
4 月 02 09:15:13 casey-Virtual-Machine tailscale[7115]: debconf: 無法初始化前端介面:Readline
4 月 02 09:15:13 casey-Virtual-Machine tailscale[7115]: debconf: (這個介面要求可控制的 tty。)
4 月 02 09:15:13 casey-Virtual-Machine tailscale[7115]: debconf: 返回前端介面:Teletype
4 月 02 09:15:13 casey-Virtual-Machine tailscale[7115]: dpkg-preconfigure: 重新開啟標準輸入失敗:
4 月 02 09:15:13 casey-Virtual-Machine tailscale[7101]: 已下載 23.0 MB,耗時 40 秒 (577 kB/s)
4 月 02 09:15:14 casey-Virtual-Machine tailscale[7101]: [729B blob data]
4 月 02 09:15:14 casey-Virtual-Machine tailscale[7101]: 準備解壓 .../tailscale_1.38.3_amd64.deb ...
4 月 02 09:15:14 casey-Virtual-Machine tailscale[7101]: 正在解壓 tailscale (1.38.3) 並覆蓋 (1.38.2) ...
4 月 02 09:15:15 casey-Virtual-Machine tailscale[7101]: 正在設定 tailscale (1.38.3) ...
4 月 02 09:15:23 casey-Virtual-Machine tailscale[7325]: Running kernel seems to be up-to-date.
4 月 02 09:15:23 casey-Virtual-Machine tailscale[7325]: Services to be restarted:
4 月 02 09:15:23 casey-Virtual-Machine tailscale[7325]: systemctl restart tailscale-weekly-update.service
4 月 02 09:15:23 casey-Virtual-Machine tailscale[7325]: No containers need to be restarted.
4 月 02 09:15:23 casey-Virtual-Machine tailscale[7325]: No user sessions are running outdated binaries.
4 月 02 09:15:23 casey-Virtual-Machine tailscale[7325]: No VM guests are running outdated hypervisor (qemu) binaries on this host.
4 月 02 09:15:24 casey-Virtual-Machine systemd[1]: tailscale-weekly-update.service: Deactivated successfully.
4 月 02 09:15:24 casey-Virtual-Machine systemd[1]: Finished Tailscale node agent.
4 月 02 09:15:24 casey-Virtual-Machine systemd[1]: tailscale-weekly-update.service: Consumed 6.317s CPU time.
$ sudo journalctl -S "2023-03-29 00:00:00" -u tailscale-weekly-update.timer
4 月 02 09:14:28 casey-Virtual-Machine systemd[1]: Started Tailscale node agent.
4 月 02 20:01:52 casey-Virtual-Machine systemd[1]: tailscale-weekly-update.timer: Deactivated successfully.
4 月 02 20:01:52 casey-Virtual-Machine systemd[1]: Stopped Tailscale node agent.
如上面的紀錄檔,可以很方便地檢查 timer 和服務的狀態。
在紀錄檔這方面,你不需要做任何特別的事情,就可以使tailscale-weekly-update.service
unit 中的ExecStart
觸發器的STDOUT
儲存在紀錄檔中。這都是使用 systemd 執行服務的一部分。
優勢之一:針對時間精確度更詳細的設定項
從上面紀錄檔,如果細看,timer 不會在:00
秒的時候準確觸發,甚至不會在上一個範例的一分鐘內準確觸發。這是故意的,但如果有必要的話,可以覆蓋它的預設設定。
這種行為的原因是為了防止多個服務在完全相同的時間被觸發。例如,你可以使用時間規格,如每週、每天,等等。這些快捷方式都被定義為在它們被觸發的那一天的 00:00:00 時觸發。當多個 timer 被這樣指定時,它們很有可能會試圖同時啟動。
systemd timer 被有意設計成在指定時間內隨機觸發,以防止同時觸發。它們在一個時間視窗內半隨機地觸發。根據systemd.timer
手冊,這個觸發時間相對於所有其他定義的 timer 單位來說,保持在一個穩定的位置。
大多數時候,這種概率性的觸發時間是沒有問題的。當安排備份等任務執行時,只要它們在非工作時間執行,就不會有問題。一個系統管理員可以選擇一個確定的開始時間,如典型的 cronjob 規範中的 01:05:00,以不與其他任務衝突,但有很大範圍的時間值可以達到這個目的。啟動時間中的一分鐘隨機性通常是不相關的。
然而,對於某些任務,精確的觸發時間是一個絕對要求。對於這些任務,你可以通過在 timer unit 檔案的 Timer
部分新增這樣的設定來指定更高的觸發時間跨度精度(如精度在一微秒內):
AccuracySec=1us
時間跨度可用於指定所需的精度,以及為重複性或一次性事件定義時間跨度。它可以識別以下單位:
/usr/lib/systemd/system
中的所有預設 timer 都指定了一個更大的精度範圍,因為精確的時間並不關鍵。看看系統建立的 timer 中的一些規格:
$ grep Accur /usr/lib/systemd/system/*timer
/usr/lib/systemd/system/fstrim.timer:AccuracySec=1h
/usr/lib/systemd/system/logrotate.timer:AccuracySec=1h
/usr/lib/systemd/system/plocate-updatedb.timer:AccuracySec=20min
/usr/lib/systemd/system/snapd.snap-repair.timer:AccuracySec=10min
優勢之一:除了定時場景,還支援基於 event 的觸發
systemd timer 具有 cron 所不具備的其他功能,cron 只在特定的、重複的、實時的日期和時間觸發。但是,一個 timer 可以被設定為在系統啟動後,或在啟動後,或在某個定義的服務 unit 啟用後的特定時間內觸發。這些被稱為單調性 timer。單調指的是一個持續增加的計數或序列。這些 timer 不是持久的,因為它們在每次啟動後都會重置。
表 1 列出了單調的 timer 以及每個 timer 的簡短定義,還有 "OnCalendar" timer,它不是單調的,用於指定未來的時間,可能是重複的,也可能不是。
Timer | 單調性 | 定義 |
---|---|---|
OnActiveSec= |
X | 這定義了一個相對於 timer 被啟用的時刻的 timer。 |
OnBootSec= |
X | 這定義了一個相對於機器啟動時間的 timer。 |
OnStartupSec= |
X | 這定義了一個相對於服務管理器首次啟動時間的計時器。對於系統 timer unit,這與OnBootSec= 非常相似,因為系統服務管理器通常在啟動時很早就啟動。當設定在每個使用者服務管理器中執行的單元時,它主要是有用的,因為使用者服務管理器一般只在第一次登入時啟動,而不是在啟動時。 |
OnUnitActiveSec= |
X | 這定義了一個相對於要啟用的 timer 最後一次被啟用的時間。 |
OnUnitInactiveSec= |
X | 這定義了一個相對於要啟用的 timer 最後被停用的時間的定時器。 |
OnCalendar= |
這就用日曆事件表示式定義了實時 timer。更多關於日曆事件表示式的語法資訊請參見systemd.time(7) 。否則,其語意與OnActiveSec= 及相關設定類似。這個 timer 是最像那些與 cron 服務一起使用的 timer。 |
表 1: systemd timer 定義
單調 timer 的時間跨度可以使用與前面提到的AccuracySec
語句相同的快捷名稱,但 systemd 將這些名稱規範化為秒。例如,你可能想指定一個 timer,在系統啟動 5 天后觸發一次事件,可以這樣寫: OnBootSec=5d
。如果主機在2020-06-15 09:45:27
啟動,timer 將在2020-06-20 09:45:27
或之後一分鐘內觸發。
優勢之一:相比 cronjob 更靈活的語法
Calendar event 定義是在所需的重複時間觸發 timer 的關鍵部分。首先看一下OnCalendar
設定中使用的一些規格。
systemd 及其 timer 使用的時間和日期規格與 crontab 中使用的格式不同。它比 crontab 更靈活,允許以at
命令的方式模糊日期和時間。
使用OnCalendar=
的 systemdtimer 的基本格式是DOW YYYY-MM-DD HH:MM:SS
。DOW(星期)是可選的,其他欄位可以使用星號(*)來匹配該位置的任何值。所有日曆時間形式都被轉換為規範化的形式。如果沒有指定時間,則假定其為 00:00:00。如果沒有指定日期但指定了時間,那麼下一個匹配可能是今天或明天,這取決於當前的時間。名稱或數位可用於月份和星期。可以指定每個單位的逗號分隔的列表。單位範圍可以在開始和結束值之間用...
來指定。
有幾個有趣的選項用於指定日期。波浪號(~)可以用來指定該月的最後一天或該月最後一天之前的指定天數。"/"可以用來指定一週中的某一天作為修飾語。
下面是一些在OnCalendar
語句中使用的典型時間規格的例子:
Calendar event 定義 | 描述 |
---|---|
DOW YYYY-MM-DD HH:MM:SS |
|
*-*-* 00:15:30 |
每年的每個月的每一天,在午夜後的 15 分鐘 30 秒。 |
Weekly |
每個星期一的 00:00:00 |
Mon *-*-* 00:00:00 |
與每週相同 |
Mon |
與每週相同 |
Wed 2020-*-* |
2020 年的每個星期三,00:00:00 |
Mon..Fri 2021-*-* |
2021 年的每個工作日的 00:00:00 |
2023-6,7,8-1,15 01:15:00 |
2023 年 6 月、7 月和 8 月的 1 日和 15 日凌晨 01:15:00 |
Mon *-05~03 |
任何一年的 5 月的下一個星期一,也是月末的第三天。 |
Mon..Fri *-08~04 |
任何年份的 8 月底前的第 4 天,如果該天也是工作日,則為 8 月底。 |
*-05~03/2 |
從五月底開始的第三天,兩天後再來一次。每年都會重複。請注意,這個表示式使用了(~)。 |
*-05-03/2 |
五月的第三天,然後在五月的其餘時間裡每隔一天。每年重複一次。注意,這個表示式使用了破折號(-)。 |
表 2: 範例OnCalendar
event 定義
優勢之一:更豐富的使用/運維命令集
systemd 提供了一個很好的工具來驗證和檢查 timer 中的日曆時間事件規範。systemd-analyze calendar
工具解析了一個日曆時間事件規範,並提供了規範化的形式以及其他有趣的資訊,比如下一個 "elapse"(即匹配)的日期和時間,以及達到觸發時間前的大致時間。
首先,看一下未來的一個沒有時間的日期:
$ systemd-analyze calendar 2030-06-17
Original form: 2030-06-17
Normalized form: 2030-06-17 00:00:00
Next elapse: Mon 2030-06-17 00:00:00 CST
(in UTC): Sun 2030-06-16 16:00:00 UTC
From now: 7 years 2 months left
現在新增一個時間。在這個例子中,日期和時間作為非相關實體被單獨分析:
$ systemd-analyze calendar 2030-06-17 15:21:16
Original form: 2030-06-17
Normalized form: 2030-06-17 00:00:00
Next elapse: Mon 2030-06-17 00:00:00 CST
(in UTC): Sun 2030-06-16 16:00:00 UTC
From now: 7 years 2 months left
Original form: 15:21:16
Normalized form: *-*-* 15:21:16
Next elapse: Wed 2023-04-05 15:21:16 CST
(in UTC): Wed 2023-04-05 07:21:16 UTC
From now: 21h left
要把日期和時間作為一個 unit 來分析,需要用引號把它們括起來。
$ systemd-analyze calendar "2030-06-17 15:21:16"
Normalized form: 2030-06-17 15:21:16
Next elapse: Mon 2030-06-17 15:21:16 CST
(in UTC): Mon 2030-06-17 07:21:16 UTC
From now: 7 years 2 months left
現在測試表 2 中的條目。選一個複雜的:
$ systemd-analyze calendar "2023-6,7,8-1,15 01:15:00"
Original form: 2023-6,7,8-1,15 01:15:00
Normalized form: 2023-06,07,08-01,15 01:15:00
Next elapse: Thu 2023-06-01 01:15:00 CST
(in UTC): Wed 2023-05-31 17:15:00 UTC
From now: 1 month 26 days left
讓我們看一個例子,在這個例子中,我們列出了時間戳表示式的下五個執行時間:
$ systemd-analyze calendar --iterations=5 "Mon *-05~3"
Original form: Mon *-05~3
Normalized form: Mon *-05~03 00:00:00
Next elapse: Mon 2023-05-29 00:00:00 CST
(in UTC): Sun 2023-05-28 16:00:00 UTC
From now: 1 month 23 days left
Iter. #2: Mon 2028-05-29 00:00:00 CST
(in UTC): Sun 2028-05-28 16:00:00 UTC
From now: 5 years 1 month left
Iter. #3: Mon 2034-05-29 00:00:00 CST
(in UTC): Sun 2034-05-28 16:00:00 UTC
From now: 11 years 1 month left
Iter. #4: Mon 2045-05-29 00:00:00 CST
(in UTC): Sun 2045-05-28 16:00:00 UTC
From now: 22 years 1 month left
Iter. #5: Mon 2051-05-29 00:00:00 CST
(in UTC): Sun 2051-05-28 16:00:00 UTC
From now: 28 years 1 month left
這應該給你足夠的資訊來開始測試你的OnCalendar
時間規格。
systemd timer 可以用來執行與 cron 工具相同型別的任務,但在觸發事件的 calendar 和單調的時間規格方面提供了更多的靈活性。
除此之外,systemd timer 還有的優勢包括:
快去嘗試遷移你的 cronjob 到 systemd timer 吧~