可靠的 cron:最單純的定時任務,一分散就變難

· tech

#sre#reliability

📑 目錄

cron 大概是最單純的一種基礎設施:時間到,跑一個任務。單機上寫過 crontab 的人都覺得它理所當然。但只要加上兩個字——「可靠」(那台機器掛了,任務還是得照跑),它就從最簡單的東西,一夕變成一個會扯到分散式共識的硬問題。這篇講為什麼,以及它逼你面對的一個沒有完美解的抉擇。

單機 cron 很簡單,可靠的 cron 很難

單機 cron 的致命傷很明顯:它是單點故障。那台機器一掛,所有排程跟著停擺,而且你可能過了好幾個週期才發現。直覺的修法是把它做成分散式——多台副本、選一個 leader 來跑、掛了換人接手。但這一搬,立刻冒出一個新的硬問題:leader 換人接手的那一刻,「哪些任務已經跑過了」這份狀態絕不能丟——否則新接手的人根本不知道某個任務跑了沒,結果不是重跑就是漏跑。

單機 cron 很簡單,「可靠的」cron 很難 cron(單機)時間到就跑,超簡單 ✗ 機器一掛 → 排程全停(SPOF) 要它掛了也能跑 副本(leader) 副本 / 待命 副本 / 待命 共識日誌(Paxos)記錄:哪些任務跑過了 一台掛 → 重選 leader,狀態從共識還原 → 不重跑、不漏跑 把最單純的 cron 變可靠,底層就得用上分散式共識
分散式 cron 難的不是「選誰當 leader」,而是「哪些任務已經跑過了」這個狀態,必須在 leader 換人接手時一個位元都不能丟。因為新 leader 一旦搞不清楚某個任務跑了沒,結果不是重跑、就是漏跑。要讓這份紀錄可靠,底層就得靠上一篇的分散式共識——把「已跑清單」存進共識日誌,failover 才安全

換句話說,可靠的 cron 難點不在排程本身,而在狀態的持久性:那份「哪些任務、在哪個週期、跑過了沒」的帳,必須撐得過任何一台機器的崩潰與 leader 的改朝換代。而這正是分散式共識的應用題——用 Paxos 之類的共識,把這份帳存成一份大家都同意、且失效也不會丟的日誌。

沒有免費的 exactly-once:跳過 vs 重複,選一個

就算有了共識存狀態,還有一個更狡猾的問題,藏在「決定要跑」和「記錄跑過了」這兩個動作之間的空窗裡。leader 只要在這個空窗中崩潰,你就必然踩到兩種災難之一:

崩在空窗裡:跳過 vs 重複,沒有兩全 先記錄再啟動 記錄「已跑」 ⚡崩 啟動任務 跳過 miss:標記了卻沒跑 先啟動再記錄 啟動任務 ⚡崩 記錄「已跑」 重複 double:接手者再跑一次 分散式沒有免費的 exactly-once —— 你只能選:寧可跳過,還是寧可重複? 逃生門:把任務做成冪等 → 重複就無害 → 大膽選「先啟動再記錄」
兩種順序、兩種災難:先記錄再啟動,崩在中間就是「標記完成卻根本沒跑」的跳過;先啟動再記錄,崩在中間就是「跑了沒留紀錄、接手者又跑一次」的重複。分散式系統裡沒有免費的 exactly-once,你只能挑一種風險。而唯一的萬用解,是把任務寫成冪等——重複執行結果一樣,那你就能安心選「寧可重複」,穩穩落在 at-least-once

所以真正要先問的問題是:這個任務,跳過比較痛,還是重複比較痛? 寄一封帳單通知,重複寄兩次很尷尬,寧可有機制擋重複;產一份可覆寫的報表,漏產一次比產兩次糟,那寧可重跑。而最漂亮的解法是把選擇權拿掉——把任務做成冪等,重複執行也無害,你就能永遠選「至少跑一次」,睡得著覺。這正是我在Airflow 排程那篇一直強調的:冪等可重跑,是資料任務的地基,不是加分項。

還有一個坑:午夜的驚群

最後一個實務陷阱:大家排程都愛整點,尤其 0 0 * * *(午夜)。結果就是每天 00:00:00 那一瞬間,成百上千個任務同時湧出、同時搶資源、同時打同一個下游——這就是 thundering herd(驚群)。解法很簡單但常被忘記:加抖動(jitter),把觸發時間在一個小區間內隨機打散,別讓所有任務擠在同一秒。

反思

cron 是「簡單的東西一分散就變難」的最佳範例

我很喜歡拿 cron 當例子,因為它完美示範了分散式系統的一個殘酷規律:一個東西在單機上有多簡單,搬到分散式就有多難。 單機 cron 是新手都能寫的 crontab;可靠的 cron 卻要用上 leader 選舉、共識日誌、崩潰空窗分析——難度差了好幾個數量級,而需求聽起來只是「多加兩個字:可靠」。這讓我對「這需求很簡單吧?」這句話越來越警惕——很多時候,簡單的是happy path,真正的成本全藏在「掛了怎麼辦、剛好崩在中間怎麼辦」的邊界裡。估工時,要估的是那些邊界,不是那條快樂路徑。

跳過還是重複,先想清楚你的任務怕哪一個

「沒有免費的 exactly-once」是我覺得每個做排程、做訊息、做資料管線的人都該刻進骨子裡的一句話。太多人預設系統會「剛好跑一次」,然後在某次故障後,對著重複的帳單或漏掉的結算一臉錯愕。現實是:你必然要在跳過和重複之間選一個,那不如提早、清醒地選。而我的預設答案幾乎永遠是——把任務做成冪等,然後選「寧可重複」。因為冪等把一個「二選一的兩難」變成了「怎麼選都沒差」的舒適區,這是我看過投報率最高的一種防禦性設計,它跟資料管線的可重跑、訊息系統的去重,講的都是同一件事。

說到底,可靠的 cron 是共識的一道應用題

寫這篇最有意思的體會,是發現「可靠 cron」根本不是一個獨立的題目,而是分散式共識的應用。你以為在解排程,實際上在解的是「一群會掛的機器,如何對『這個任務跑過了沒』達成一致」——這跟選 leader、管分散式鎖是同一個問題的不同外衣。這也再次印證了我的一個信念:分散式系統的難題,翻來覆去其實就那幾個核心(共識、狀態、故障邊界),把核心啃透,遇到的各種「新問題」,多半只是老問題換了張臉。也因此,這種底層設施更該用被驗證過的現成方案,而不是每個團隊自己重造一次那道空窗。