先問一個問題:你的鬧鐘是怎麼設的?
我以前的做法是手機上一排鬧鐘,七點、七點十分、七點二十,每一個都叫「起床」。有一天室友問我,為什麼不設一個就好,我說因為我不相信自己。她說那你需要的不是更多鬧鐘,是一個知道你會賴床的鬧鐘。這句話我一直記得,因為後來在 Linux 上排程的時候,我發現 cron 就是那一排長得一樣的鬧鐘,而 systemd timer 是那個知道你會賴床的。
兩條路
在 Linux 上定時跑一件事,最短的路是 cron:crontab 加一行,結束。五個欄位寫分、時、日、月、星期,後面接指令。它從 1970 年代活到現在,簡單到不會壞。
systemd 提供另一條路,一個 .timer 檔配一個 .service 檔。多寫一個檔案聽起來很煩,我一開始也覺得這是 Linux 世界典型的「把簡單的事弄複雜」。用了之後才發現多出來的那幾行,每一行都在回答一個 cron 從來不問的問題。
兩個檔案各管什麼
.service 描述要跑什麼:指令、以哪個使用者執行、環境變數檔、逾時、能碰哪些目錄。.timer 只描述什麼時候跑,OnCalendar 填 *:0/15 是每十五分鐘,填 daily 是每天午夜,語法比 cron 的五個欄位好讀,至少我不用每次都去查星期是放在第幾格。兩個檔案同名,timer 觸發同名的 service。
systemctl list-timers 一張表列出每個 timer 上次跑的時間和下次要跑的時間。這個指令我超級推薦,它就像一張貼在冰箱上的行事曆,一眼看完誰該來了、誰剛走。cron 沒有這種東西,你要自己翻日誌,然後在一堆時間戳裡找那一行。
Persistent:錯過要不要補
Persistent=true 表示機器關機期間錯過的那一輪,開機後立刻補跑一次。備份這類工作應該開,錯過一天就少一份,補回來是對的。
但不是每件事都該補。想像一個會把東西發布出去的工作,例如自動上架文章。機器不知道為什麼重開了,一開機就把積壓的東西全部發出去,而這時候還沒有人看過那台機器是不是正常。我寧可它什麼都不做,等下一個正常的時間點。同一個選項,在兩個工作上的答案相反,這就是為什麼它是一個選項而不是一個預設。
RandomizedDelaySec:別跟別人擠同一秒
很多工作都設在整點或整刻,備份、日誌輪替、別人的排程,全部擠在同一秒起跑,那一秒硬碟會很忙。加一個 RandomizedDelaySec=120 讓它在兩分鐘內隨機起跑,錯開人潮。這有點像下課鐘響不要馬上衝去福利社,晚個兩分鐘,隊伍就短很多。
AccuracySec 則反過來允許 systemd 為了省電把觸發時間合併,預設是一分鐘。需要準時的話要自己調小,不然你設的整點可能會變成整點零五十秒,你會以為自己看錯。
沙箱是我用它的主因
.service 裡的幾行指令可以把一個工作關進很小的空間:ProtectSystem=strict 把整個檔案系統變成唯讀,ProtectHome=read-only 保護家目錄,ReadWritePaths= 逐一列出真正需要寫的目錄,PrivateTmp=true 給它自己的暫存目錄,NoNewPrivileges 設為 true 擋住提權。一支腳本就算被弄壞了,它能寫到的地方也只有你列出的那幾個。
用宿舍來比喻,cron 是把整棟樓的鑰匙交給一個工讀生,說「你去三樓倒垃圾」;systemd 的沙箱是只給他三樓垃圾間那一把鑰匙。就算他迷路了、就算鑰匙被別人拿走,能開的門也只有那一扇。
沙箱也會咬人。SQLite 在 WAL 模式下需要在資料庫旁邊建立 -wal 和 -shm 檔,如果那個目錄不在 ReadWritePaths 裡,你會看到一個很誤導的錯誤「unable to open database file」,明明檔案就在那裡,權限也對,你會開始懷疑檔案系統、懷疑硬碟、懷疑人生。其實只是垃圾間的鑰匙開不了旁邊的儲藏室。
手動跑過不算數
這是我學到最貴的一課,貴到值得單獨一節。
在 shell 裡手動執行腳本,不經過 systemd 的沙箱,是另一條路徑。你在終端機裡跑一百次都成功,不代表它在 timer 底下會成功,因為終端機裡的你是全樓鑰匙的那個人。裝好 unit 之後一定要用 systemctl start 觸發一次,看它在真正的環境裡會不會失敗。我碰過手動跑幾十次都好好的腳本,第一次由 timer 觸發就因為權限和唯讀檔案出錯,而且是半夜出錯,早上才發現。
那天早上的心情大概就像期末考前一晚把筆記整理得漂漂亮亮,考試當天發現帶錯科目。筆記沒有錯,是你根本沒有在考試的情境下試過。
退出碼代表什麼,自己決定
systemd 用退出碼判斷 service 成功還是失敗。如果你的工作有「正常地拒絕做某件事」這種結果,把它跟真正的環境錯誤分開:前者記錄到自己的收據或日誌,正常退出;只有環境壞掉才回非零。
否則 systemctl status 很快就一片紅。一片紅的儀表板跟沒有儀表板是一樣的,因為你會開始習慣它紅,然後真正壞掉的那次也是紅,你看不出差別。這跟手機通知一樣,什麼都跳通知的 app,最後你會把它整個關掉。
小劇場:那我到底該用哪個
「我只是想每天半夜清一下暫存檔,也要寫兩個檔案嗎?」
「不用,cron 一行。」
「那什麼時候要寫兩個檔案?」
「那支腳本會不會寫到你會心痛的資料?」
「會,它會動到使用者的東西。」
「那寫兩個檔案。多花你十分鐘,換來的是它就算壞掉也只能壞在你圈好的範圍裡。」
「聽起來像是在替未來的自己買保險。」
「對,而且保費是十分鐘。」
我什麼時候還是用 cron
沒有 root 權限的機器上裝不了系統層的 unit,使用者層的 timer 又依賴登入 session,這時 cron 一行最實在。只讀不寫、失敗了也不會留下半成品的工作,cron 也夠。會寫到重要資料的工作,我要沙箱,那就是 systemd。
回到鬧鐘。一排長得一樣的鬧鐘沒有錯,很多人靠它活過了大學四年。但如果你有一件事真的不能出錯,你會想要那個知道你會賴床、會在你關掉它之後再叫一次、而且不會把整間宿舍的人都吵醒的鬧鐘。systemd timer 就是那個。
關於 systemd 的一點八卦
systemd 在 Linux 世界是一個會引起爭吵的名字,2010 年由 Lennart Poettering 發表之後,有好幾年的時間,任何技術論壇只要出現這個字就會演變成幾百樓的口水戰。反對的人說它太大、管太多、違反「一個工具只做一件事」的哲學;支持的人說它解決了開機順序、服務相依、日誌管理這些大家忍了二十年的問題。
十幾年過去,主流的發行版幾乎都用它了,吵架的聲音小了很多,但你偶爾還是會在某個角落看到有人義憤填膺。我對這場戰爭沒有立場,我只是一個需要排程的人,而它的 timer 剛好很好用。有時候不表態也是一種表態。
順便說幾個好用的東西。journalctl -u 你的service 直接看某個服務的日誌,不用去猜檔案在哪。systemd-analyze 告訴你開機花了多久、卡在哪個服務。systemctl cat 你的unit 把 unit 檔印出來,包括所有覆寫,省得你去找檔案。這些指令我花了很久才知道,知道之後覺得之前的日子白過了。
常見的三個誤會
第一個誤會是「timer 只能取代 cron」。它還能做 cron 做不到的事:OnBootSec 開機後幾分鐘跑一次,OnUnitActiveSec 上次跑完後間隔多久再跑,這兩個 cron 都沒有對應的寫法。
第二個誤會是「寫 unit 檔很難」。最小的 timer 加 service 加起來不到十行,跟 crontab 那一行比起來是多,但那十行裡每一行都在做一件你看得懂的事。難的從來不是語法,是決定 Persistent 要不要開這種問題,而那個問題 cron 根本不讓你問。
第三個誤會是「沙箱會讓程式跑不起來」。會,第一次一定會,因為你的程式在某個你沒想到的地方寫了檔案。但那正是沙箱的價值:它把「這支程式到底碰了哪些東西」逼出來給你看。第一次跑不起來是功能,不是 bug。
一個週末可以試的小練習
找一台你有 root 的 Linux 機器,虛擬機也行。寫一個 service,ExecStart 是 /bin/sh -c 'date >> /tmp/hello.log',加上 ProtectSystem=strict 和 PrivateTmp=true。寫一個同名的 timer,OnCalendar 填 *:0/2,每兩分鐘一次。systemctl enable --now 它,然後等四分鐘,去看 /tmp/hello.log。
你會發現檔案是空的。因為 PrivateTmp=true 給了它自己的暫存目錄,它寫的 /tmp 不是你看的 /tmp。用 journalctl -u 看日誌,然後把 PrivateTmp 拿掉再試一次,這次就有了。恭喜,你剛剛體驗了沙箱咬人,而且是在一個完全無害的情境下。之後在真正的工作上碰到「明明寫了檔案卻找不到」,你會想起這個下午。
後記
寫完這篇我去看了一下自己手機上的鬧鐘。還是一排。有些習慣不是知道了就改得掉,但至少 Linux 上的排程我已經改用那個知道我會賴床的了。人的部分,慢慢來。
官方網站:systemd.io
留言(0)
還沒有留言。讀完有想法,歡迎留下第一則。