如何設(shè)計(jì)穩(wěn)定性橫跨全球的Cron服務(wù)
這篇文章主要來描述下 Google 是如何實(shí)現(xiàn)一套可靠的分布式 Cron 服務(wù),服務(wù)于內(nèi)部那些需要絕大多數(shù)計(jì)算作業(yè)定時(shí)調(diào)度的團(tuán)隊(duì)。 在這個(gè)系統(tǒng)的實(shí)踐過程中,我們收獲了很多,包括如何設(shè)計(jì)、如何實(shí)現(xiàn)使得它看上去像一個(gè)靠譜的基礎(chǔ)服務(wù)。 在這里,我們來討論下分布式 Cron 可能會(huì)遇到哪些問題,以及如何解決它。
Cron 是 UNIX 中一個(gè)常見的工具,用來定期執(zhí)行一些用戶指定的任意任務(wù)。我們先來分析下 Cron 的基本原則和它最常見的實(shí)現(xiàn),然后我們來回顧下像 Cron 這樣的服務(wù)應(yīng)該如何運(yùn)行在一個(gè)大型的、分布式的環(huán)境中,這樣即使單機(jī)故障也不會(huì)對(duì)系統(tǒng)可用性造成影響。 我們將會(huì)介紹了一個(gè)建立在少量機(jī)器上的 Cron 系統(tǒng),然后結(jié)合數(shù)據(jù)中心的調(diào)度服務(wù),從而可以在整個(gè)數(shù)據(jù)中心中運(yùn)行 Cron 任務(wù)。
在我們?cè)诿枋鋈绾芜\(yùn)行一個(gè)靠譜的分布式 Cron 服務(wù)之前,讓我們先來從一個(gè) SRE 的角度來回顧下 Cron。
Cron 是一個(gè)通用的工具,無論是管理員還是普通用戶都可以用它來在系統(tǒng)上運(yùn)行指定的命令,以及指定何時(shí)運(yùn)行命令,這些指定運(yùn)行的命令可以是定期垃圾回收,也可以是定期數(shù)據(jù)分析。 最常見的時(shí)間指定格式被稱為crontab,它不僅支持簡(jiǎn)單的時(shí)間周期(如,每天中午一次,每個(gè)小時(shí)一次),也支持較復(fù)雜的時(shí)間周期,如每個(gè)周六、每個(gè)月的第 30 天等等。
Cron 通常只包含一個(gè)組件,被稱為 crond,它是一個(gè)后臺(tái)守護(hù)程序,加載所有需要運(yùn)行的 cron 定時(shí)任務(wù),根據(jù)它們接下來的運(yùn)行時(shí)間來進(jìn)行排序,然后這個(gè)守護(hù)進(jìn)程將會(huì)等待直到***個(gè)任務(wù)開始執(zhí)行。在這個(gè)時(shí)刻,crond 將會(huì)加載執(zhí)行這個(gè)任務(wù),之后將它放入隊(duì)列等待下一次運(yùn)行。
可靠性Reliability
從可靠性的角度來看一個(gè)服務(wù),需要有很多注意的地方。
***,比如 crond,它的故障域本質(zhì)上來說只是一臺(tái)機(jī)器,如果這個(gè)機(jī)器沒有運(yùn)行,不論是 cron 調(diào)度還是加載的任務(wù)都是不可運(yùn)行的。因此,考慮一個(gè)非常簡(jiǎn)單的分布式的例子 ——— 我們使用兩臺(tái)機(jī)器,然后 cron 調(diào)度在其中一臺(tái)機(jī)器上運(yùn)行任務(wù)(比如通過 ssh)。然后產(chǎn)生了一個(gè)故障域了:調(diào)度任務(wù)和目標(biāo)服務(wù)器都可能失敗。
另外一個(gè)需要注意的地方是,即使是 crond 重啟(包括服務(wù)器重啟),上面部署的 crontab 配置也不應(yīng)該丟失。crond 執(zhí)行一個(gè)任務(wù)然后就‘忘記’了這個(gè)任務(wù)的狀態(tài),它并不會(huì)嘗試去跟蹤這個(gè)任務(wù)的執(zhí)行狀態(tài),包括是否該執(zhí)行是否已經(jīng)執(zhí)行。
anacron 是一個(gè)例外,它是 crontab 的一個(gè)補(bǔ)充,它嘗試運(yùn)行哪些因?yàn)榉?wù)器宕機(jī)而應(yīng)該執(zhí)行卻沒執(zhí)行的任務(wù)。這僅限于每日或者更小執(zhí)行頻率的任務(wù),但對(duì)于在工作站和筆記本電腦上運(yùn)行維護(hù)工作非常有用。通過維護(hù)一個(gè)包括***執(zhí)行時(shí)間的配置文件,使得運(yùn)行這些特殊的任務(wù)更加方便。
Cron 的任務(wù)和冪等性
Cron 的任務(wù)用來執(zhí)行定期任務(wù),但是除此之外,卻很難在進(jìn)一步知道它們的功能。讓我們先把要討論的主題拋開一邊,現(xiàn)在先來就 Cron 任務(wù)本身來做下探討,因?yàn)橹挥欣斫饬?Cron 任務(wù)的各種各樣的需求,才能知道它是如何影響我們需要的可靠性要求,而這一方面的探討也將貫穿接下來的文章。
有一些 Cron 任務(wù)是冪等性的,這樣在某些系統(tǒng)故障的情況下,可以很安全的執(zhí)行它們多次,比如,垃圾回收。然而有些 Cron 任務(wù)卻不應(yīng)該被執(zhí)行多次,比如某個(gè)發(fā)送郵件的任務(wù)。
還有更復(fù)雜的情況,有些 Cron 任務(wù)允許因?yàn)槟承┣闆r而“忘了”運(yùn)行,而某些 Cron 任務(wù)卻不能容忍這些,比如,垃圾回收的 Cron 任務(wù)每 5 分鐘調(diào)度一次,即使某一次沒有執(zhí)行也不會(huì)有太大的問題,然而,一個(gè)月一次的支付薪水的任務(wù),卻絕對(duì)不允許有失誤。
Cron 任務(wù)的各種不同的類型使得不可能有一個(gè)通用的解決方案,使得它可以應(yīng)對(duì)各種各樣的失敗。所以,在本文中上面說的那些情況,我們更傾向于錯(cuò)過某一次的運(yùn)行,而不是運(yùn)行它們兩次或者更多。Cron 任務(wù)的所有者應(yīng)該(也必須)監(jiān)控著它們的任務(wù),比如返回任務(wù)的調(diào)用結(jié)果,或者單獨(dú)發(fā)送運(yùn)行的日志給所屬者等等,這樣,即使跳過了任務(wù)的某次執(zhí)行,也能夠很方便的采取對(duì)應(yīng)的補(bǔ)救動(dòng)作。當(dāng)任務(wù)失敗時(shí),我們更傾向于將任務(wù)狀態(tài)置為 “fail closed” 來避免產(chǎn)生系統(tǒng)性的不良狀態(tài)。
大規(guī)模部署 Cron
當(dāng)從單機(jī)到集群部署 Cron 時(shí),需要重新思考如何使 Cron 在這種環(huán)境下良好的運(yùn)行。在對(duì) Google 的 Cron 進(jìn)行解說之前,讓我們先來討論下單機(jī)以及多機(jī)之間的區(qū)別,以及針對(duì)這變化如何設(shè)計(jì)。
擴(kuò)展基礎(chǔ)架構(gòu)
常規(guī)的 Cron 僅限于單個(gè)機(jī)器,而大規(guī)模部署的 Cron 解決方案不能僅僅綁定到一個(gè)單獨(dú)的機(jī)器。假設(shè)我們擁有一個(gè) 1000 臺(tái)服務(wù)器的數(shù)據(jù)中心,如果即使是 1/1000 的幾率造成服務(wù)器不可用都能摧毀我們整個(gè) Cron 服務(wù),這明顯不是我們所希望的。
所以,為了解決這個(gè)問題,我們必須將服務(wù)與機(jī)器解耦。這樣如果想運(yùn)行一個(gè)服務(wù),那么僅僅需要指定它運(yùn)行在哪個(gè)數(shù)據(jù)中心即可,剩下的事情就依賴于數(shù)據(jù)中心的調(diào)度系統(tǒng)(當(dāng)然前提是調(diào)度系統(tǒng)也應(yīng)該是可靠的),調(diào)度系統(tǒng)會(huì)負(fù)責(zé)在哪臺(tái)或者哪些機(jī)器上運(yùn)行服務(wù),以及能夠良好的處理機(jī)器掛掉這種情況。 那么,如果我們要在數(shù)據(jù)中心中運(yùn)行一個(gè)任務(wù),也僅僅是發(fā)送一條或多條 RPC 給數(shù)據(jù)中心的調(diào)度系統(tǒng)。
然而,這一過程顯然并不是瞬時(shí)完成的。比如,要檢查哪些機(jī)器掛掉了(機(jī)器健康檢查程序掛了怎么辦),以及在另外一些機(jī)器上重新運(yùn)行任務(wù)(服務(wù)依賴重新部署重新調(diào)用任務(wù))都是需要花費(fèi)一定時(shí)間的。
將程序轉(zhuǎn)移到另外一個(gè)機(jī)器上可能意味著損失一些存儲(chǔ)在老機(jī)器上的一些狀態(tài)信息(除非也采用動(dòng)態(tài)遷移),重新調(diào)度運(yùn)行的時(shí)間間隔也可能超過最小定義的一分鐘,所以,我們也必須考慮到上述這兩種情況。一個(gè)很直接的做法,將狀態(tài)文件放入分布式文件系統(tǒng),如 GFS,在任務(wù)運(yùn)行的整個(gè)過程中以及重新部署運(yùn)行任務(wù)時(shí),都是用它來記錄使用相關(guān)狀態(tài)。 然而,這個(gè)解決方案卻不能滿足我們預(yù)期的時(shí)效性這個(gè)需求,比如,你要運(yùn)行一個(gè)每五分鐘跑一次的 Cron 任務(wù),重新部署運(yùn)行消耗的 1-2 分鐘對(duì)這個(gè)任務(wù)來說也是相當(dāng)大的延遲了。
及時(shí)性的需求可能會(huì)促使各種熱備份技術(shù)的使用,這樣就能夠快速記錄狀態(tài)以及從原有狀態(tài)快速恢復(fù)。
需求擴(kuò)展
將服務(wù)部署在數(shù)據(jù)中心和單服務(wù)器的另一個(gè)實(shí)質(zhì)性的區(qū)別是,如何規(guī)劃任務(wù)所需要的計(jì)算資源,如 CPU 或內(nèi)存等。
單機(jī)服務(wù)通常是通過進(jìn)程來進(jìn)行資源隔離,雖然現(xiàn)在 Docker 變得越來越普遍,但是使用它來隔離一切目前也不太是很通用的做法,包括限制 crond 以及它所要運(yùn)行的任務(wù)。
大規(guī)模部署在數(shù)據(jù)中心經(jīng)常使用容器來進(jìn)行資源隔離。隔離是必要的,因?yàn)槲覀兛隙ㄏM麛?shù)據(jù)中心中運(yùn)行的某個(gè)程序不會(huì)對(duì)其它程序產(chǎn)生不良影響。為了隔離的有效性,在運(yùn)行前肯定得先預(yù)知運(yùn)行的時(shí)候需要哪些資源——包括 Cron 系統(tǒng)本身和要運(yùn)行的任務(wù)。這又會(huì)產(chǎn)生一個(gè)問題,即如果數(shù)據(jù)中心暫時(shí)沒有足夠的資源,那么這個(gè)任務(wù)可能會(huì)延遲運(yùn)行。這就要求我們不僅要監(jiān)控 Cron 任務(wù)加載的情況,也要監(jiān)控 Cron 任務(wù)的全部狀態(tài),包括開始加載到終止運(yùn)行。
現(xiàn)在,我們希望的 Cron 系統(tǒng)已經(jīng)從單機(jī)運(yùn)行的情況下解耦,如之前描述的那樣,我們可能會(huì)遇到部分任務(wù)運(yùn)行或加載失敗。這時(shí)候幸虧任務(wù)配置的通用性,在數(shù)據(jù)中心中運(yùn)行一個(gè)新的 Cron 任務(wù)就可以簡(jiǎn)單的通過 RPC 調(diào)用的方式來進(jìn)行,不過不幸的是,這樣我們只能知道 RPC 調(diào)用是否成功,卻無法具體知道任務(wù)失敗的具體地方,比如,任務(wù)在運(yùn)行的過程中失敗,那么恢復(fù)程序還必須將這些中間過程處理好。
在故障方面,數(shù)據(jù)中心遠(yuǎn)比一臺(tái)單一的服務(wù)器復(fù)雜。Cron 從原來僅僅的一個(gè)單機(jī)二進(jìn)制程序,到整個(gè)數(shù)據(jù)中心運(yùn)行,其期間增加了很多明顯或不明顯的依賴關(guān)系。作為像 Cron 這樣的一個(gè)基礎(chǔ)服務(wù),我們希望得到保證的是,即使在數(shù)據(jù)中心中運(yùn)行發(fā)生了一些 “Fail”(如,部分機(jī)器停電或存儲(chǔ)掛掉),服務(wù)依然能夠保證功能性正常運(yùn)行。為了提高可靠性,我們應(yīng)該將數(shù)據(jù)中心的調(diào)度系統(tǒng)部署在不同的物理位置,這樣,即使一個(gè)或一部分電源掛掉,也能保證至少 Cron 服務(wù)不會(huì)全部不可用。
Google 的 Cron 是如何建設(shè)的
現(xiàn)在讓我們來解決這些問題,這樣才能在一個(gè)大規(guī)模的分布式集群中部署可靠的 Cron 服務(wù),然后在著重介紹下 Google 在分布式 Cron 方面的一些經(jīng)驗(yàn)。
跟蹤 Cron 任務(wù)的狀態(tài)
向上面描述過的那樣,我們應(yīng)該跟蹤 Cron 任務(wù)的實(shí)時(shí)狀態(tài),這樣,即使失敗了,我們也更加容易恢復(fù)它。而且,這種狀態(tài)的一致性是至關(guān)重要的:相比錯(cuò)誤的多運(yùn)行 10 遍相同的 Cron 任務(wù),我們更能接受的是不去運(yùn)行它。回想下,很多 Cron 任務(wù),它并不是冪等性的,比如發(fā)送通知郵件。
我們有兩個(gè)選項(xiàng),將 Cron 任務(wù)的數(shù)據(jù)通通存儲(chǔ)在一個(gè)靠譜的分布式存儲(chǔ)中,或者僅僅保存任務(wù)的狀態(tài)。當(dāng)我們?cè)O(shè)計(jì)分布式 Cron 服務(wù)時(shí),我們采取的是第二種,有如下幾個(gè)原因:
分布式存儲(chǔ),如 GFS 或 HDFS,往往用來存儲(chǔ)大文件(如 網(wǎng)頁(yè)爬蟲程序的輸出等),然后我們需要存儲(chǔ)的 Cron狀態(tài)卻非常非常小。將如此小的文件存儲(chǔ)在這種大型的分布式文件系統(tǒng)上是非常昂貴的,而且考慮到分布式文件系統(tǒng)的延遲,也不是很適合。
像 Cron 服務(wù)這種基礎(chǔ)服務(wù),它需要的依賴應(yīng)該是越少越好。這樣,即使部分?jǐn)?shù)據(jù)中心掛掉,Cron 服務(wù)至少也能保證其功能性并持續(xù)一段時(shí)間。這并不意味著存儲(chǔ)應(yīng)該直接是 Cron 程序的一部分(這本質(zhì)上是一個(gè)實(shí)現(xiàn)細(xì)節(jié))。Cron 應(yīng)該是一個(gè)能夠獨(dú)立運(yùn)作的下游系統(tǒng),以便供用戶操作使用。
使用 Paxos
我們部署多個(gè)實(shí)例的 Cron 服務(wù),然后通過 Paxos 算法來同步這些實(shí)例間的狀態(tài)。
Paxos 算法和它其它的替代算法(如 Zab,Raft 等)在分布式系統(tǒng)中是十分常見的。具體描述 Paxos 不在本文范圍內(nèi),它的基本作用就是使多個(gè)不可靠節(jié)點(diǎn)間的狀態(tài)保持一致,只要大部分 Paxos 組成員可用,那么整個(gè)分布式系統(tǒng),就能作為一個(gè)整體處理狀態(tài)的變化。
分布式 Cron 使用一個(gè)獨(dú)立的主任務(wù),見下圖,只有它才能更改共享的狀態(tài),也只有它才能加載 Cron 任務(wù)。我們這里使用了 Paxos 的一個(gè)變體—— Fast Paxos,這里 Fast Paxos 的主節(jié)點(diǎn)也是 Cron 服務(wù)的主節(jié)點(diǎn)。
如果主節(jié)點(diǎn)掛掉,Paxos 的健康檢查機(jī)制會(huì)在秒級(jí)內(nèi)快速發(fā)現(xiàn),并選舉出一個(gè)新的主節(jié)點(diǎn)。一旦選舉出新的主節(jié)點(diǎn),Cron 服務(wù)也就隨著選舉出了一個(gè)新的 Cron 主節(jié)點(diǎn),這個(gè)新的 Cron 主節(jié)點(diǎn)將會(huì)接手前一個(gè)主節(jié)點(diǎn)留下的所有的未完成的工作。在這里 Cron 的主節(jié)點(diǎn)和 Paxos 的主節(jié)點(diǎn)是一樣的,但是 Cron 的主節(jié)點(diǎn)需要處理一下額外的工作而已??焖龠x舉新的主節(jié)點(diǎn)的機(jī)制可以讓我們大致可以容忍一分鐘的故障時(shí)間。
我們使用 Paxos 算法保持的最重要的一個(gè)狀態(tài)是,哪些 Cron 任務(wù)在運(yùn)行。對(duì)于每一個(gè)運(yùn)行的 Cron 任務(wù),我們會(huì)將其加載運(yùn)行的開始以及結(jié)束同步給一定數(shù)量的節(jié)點(diǎn)。
主節(jié)點(diǎn)和從節(jié)點(diǎn)角色
如上面描述的那樣,我們?cè)?Cron 服務(wù)中使用 Paxos 并部署,其擁有兩個(gè)不同的角色,主節(jié)點(diǎn)以及從節(jié)點(diǎn)。讓我們來就每個(gè)角色來做具體的描述。
主節(jié)點(diǎn)
主節(jié)點(diǎn)用來加載 Cron 任務(wù),它有個(gè)內(nèi)部的調(diào)度系統(tǒng),類似于單機(jī)的 crond,維護(hù)一個(gè)任務(wù)加載列表,在指定的時(shí)間加載任務(wù)。
當(dāng)任務(wù)加載的時(shí)刻到來,主節(jié)點(diǎn)將會(huì) “宣告” 它將會(huì)加載這個(gè)指定的任務(wù),并且計(jì)算這個(gè)任務(wù)下次的加載時(shí)間,就像 crond 的做法一樣。當(dāng)然,就像 crond 那樣,一個(gè)任務(wù)加載后,下一次的加載時(shí)間可能人為的改變,這個(gè)變化也要同步給從節(jié)點(diǎn)。簡(jiǎn)單的標(biāo)識(shí) Cron 任務(wù)還不夠,我們還應(yīng)該將這個(gè)任務(wù)與開始執(zhí)行時(shí)間相關(guān)聯(lián)綁定,以避免 Cron 任務(wù)在加載時(shí)發(fā)生歧義(特別是那些高頻的任務(wù),如一分鐘一次的那些)。這個(gè)“通告”通過 Paxos 來進(jìn)行。下圖展示了這一過程。
保持 Paxos 通訊同步非常重要,只有 Paxos 法定數(shù)收到了加載通知,這個(gè)指定的任務(wù)才能被加載執(zhí)行。Cron 服務(wù)需要知道每個(gè)任務(wù)是否已經(jīng)啟動(dòng),這樣即使主節(jié)點(diǎn)掛掉,也能決定接下來的動(dòng)作。如果不進(jìn)行同步,意味著整個(gè) Cron 任務(wù)運(yùn)行在主節(jié)點(diǎn),而從節(jié)點(diǎn)無法感知到這一切。如果發(fā)生了故障,很有可能這個(gè)任務(wù)就被再次執(zhí)行,因?yàn)闆]有節(jié)點(diǎn)知道這個(gè)任務(wù)已經(jīng)被執(zhí)行過了。
Cron 任務(wù)的完成狀態(tài)通過 Paxos 通知給其它節(jié)點(diǎn),從而保持同步,這里要注意一點(diǎn),這里的“完成” 狀態(tài)并不是表示任務(wù)是成功或者失敗。我們跟蹤 Cron 任務(wù)在指定調(diào)用時(shí)間被執(zhí)行的情況,我們同樣需要處理一點(diǎn)情況是,如果 Cron 服務(wù)在加載任務(wù)進(jìn)行執(zhí)行的過程中失敗后怎么辦,這點(diǎn)我們?cè)诮酉聛頃?huì)進(jìn)行討論。
主節(jié)點(diǎn)另一個(gè)重要的特性是,不管是出于什么原因主節(jié)點(diǎn)失去了其主控權(quán),它都必須立馬停止同數(shù)據(jù)中心調(diào)度系統(tǒng)的交互。主控權(quán)的保持對(duì)于訪問數(shù)據(jù)中心應(yīng)該是互斥了。如果不這樣,新舊兩個(gè)主節(jié)點(diǎn)可能會(huì)對(duì)數(shù)據(jù)中心的調(diào)度系統(tǒng)發(fā)起互相矛盾的操作請(qǐng)求。
從節(jié)點(diǎn)
從節(jié)點(diǎn)實(shí)時(shí)監(jiān)控從主節(jié)點(diǎn)傳來的狀態(tài)信息,以便在需要的時(shí)刻做出積極響應(yīng)。所有主節(jié)點(diǎn)的狀態(tài)變動(dòng)信息,都通過 Paxos 傳到各個(gè)從節(jié)點(diǎn)。和主節(jié)點(diǎn)類似的是,從節(jié)點(diǎn)同樣維持一個(gè)列表,保存著所有的 Cron 任務(wù)。這個(gè)列表必須在所有的節(jié)點(diǎn)保持一致(當(dāng)然還是通過 Paxos)。
當(dāng)接到加載任務(wù)的通知后,從節(jié)點(diǎn)會(huì)將此任務(wù)的下次加載時(shí)間放入本地任務(wù)列表中。這個(gè)重要的狀態(tài)信息變化(這是同步完成的)保證了系統(tǒng)內(nèi)部 Cron 作業(yè)的時(shí)間表是一致的。我們跟蹤所有有效的加載任務(wù),也就是說,我們跟蹤任務(wù)何時(shí)啟動(dòng),而不是結(jié)束。
如果一個(gè)主節(jié)點(diǎn)掛掉或者因?yàn)槟承┰蚴?lián)(比如,網(wǎng)絡(luò)異常等),一個(gè)從節(jié)點(diǎn)有可能被選舉成為一個(gè)新的主節(jié)點(diǎn)。這個(gè)選舉的過程必須在一分鐘內(nèi)運(yùn)行,以避免 Cron 任務(wù)丟失的情況。一旦被選舉為主節(jié)點(diǎn),所有運(yùn)行的加載任務(wù)(或部分失敗的),必須被重新驗(yàn)證其有效性。這個(gè)可能是一個(gè)復(fù)雜的過程,在 Cron 服務(wù)系統(tǒng)和數(shù)據(jù)中心的調(diào)度系統(tǒng)上都需要執(zhí)行這樣的驗(yàn)證操作,這個(gè)過程有必要詳細(xì)說明。
故障恢復(fù)
如上所述,主節(jié)點(diǎn)和數(shù)據(jù)中心的調(diào)度系統(tǒng)之間會(huì)通過 RPC 來加載一個(gè)邏輯 Cron 任務(wù),但是,這一系列的 RPC 調(diào)用過程是有可能失敗的,所以,我們必須考慮到這種情況,并且處理好。
回想下,每個(gè)加載的 Cron 任務(wù)會(huì)有兩個(gè)同步點(diǎn):開始加載以及執(zhí)行完成。這能夠讓我們區(qū)分開不同的加載任務(wù)。即使任務(wù)加載只需要調(diào)用一次 RPC,但是我們?cè)趺粗?RPC 調(diào)用實(shí)際真實(shí)成功呢?我們知道任務(wù)何時(shí)開始,但是如果主節(jié)點(diǎn)掛了我們就不會(huì)知道它何時(shí)結(jié)束。
為了解決這個(gè)問題,所有在外部系統(tǒng)進(jìn)行的操作,要么其操作是冪等性的(也就是說,我們可以放心的執(zhí)行它們多次),要么我們必須實(shí)時(shí)監(jiān)控它們的狀態(tài),以便能清楚的知道何時(shí)完成。
這些條件明顯增加了限制,實(shí)現(xiàn)起來也有一定的難度,但是在分布式環(huán)境中這些限制卻是保證 Cron 服務(wù)準(zhǔn)確運(yùn)行的根本,能夠良好的處理可能出現(xiàn)的 “fail”。如果不能妥善處理這些,將會(huì)導(dǎo)致 Cron 任務(wù)的加載丟失,或者加載多次重復(fù)的 Cron 任務(wù)。
大多數(shù)基礎(chǔ)服務(wù)在數(shù)據(jù)中心(比如 Mesos)加載邏輯任務(wù)時(shí)都會(huì)為這些任務(wù)命名,這樣方便了查看任務(wù)的狀態(tài),終止任務(wù),或者執(zhí)行其它的維護(hù)操作。解決冪等性的一個(gè)合理的解決方案是將執(zhí)行時(shí)間放在名字中 ——這樣不會(huì)在數(shù)據(jù)中心的調(diào)度系統(tǒng)里造成任務(wù)異變操作 —— 然后在將它們分發(fā)給 Cron 服務(wù)所有的節(jié)點(diǎn)。如果 Cron 服務(wù)的主節(jié)點(diǎn)掛掉,那么新的主節(jié)點(diǎn)只需要簡(jiǎn)單的通過預(yù)處理任務(wù)名字來查看其對(duì)應(yīng)的狀態(tài),然后加載遺漏的任務(wù)即可。
注意下,我們?cè)诠?jié)點(diǎn)間保持內(nèi)部狀態(tài)一致的時(shí)候,實(shí)時(shí)監(jiān)控調(diào)度加載任務(wù)的時(shí)間。同樣,我們也需要消除同數(shù)據(jù)中心調(diào)度交互時(shí)可能發(fā)生的不一致情況,所以這里我們以調(diào)度的加載時(shí)間為準(zhǔn)。比如,有一個(gè)短暫但是頻繁執(zhí)行的 Cron 任務(wù),它已經(jīng)被執(zhí)行了,但是在準(zhǔn)備把情況通告給其它節(jié)點(diǎn)時(shí),主節(jié)點(diǎn)掛了,并且故障時(shí)間持續(xù)的特別長(zhǎng)——長(zhǎng)到這個(gè) Cron 任務(wù)都已經(jīng)成功執(zhí)行完了。然后新的主節(jié)點(diǎn)要查看這個(gè)任務(wù)的狀態(tài),發(fā)現(xiàn)它已經(jīng)被執(zhí)行完成了,然后嘗試加載它。如果包含了這個(gè)時(shí)間,那么主節(jié)點(diǎn)就會(huì)知道,這個(gè)任務(wù)已經(jīng)被執(zhí)行過了,就不會(huì)重復(fù)執(zhí)行第二次。
在實(shí)際實(shí)施的過程中,狀態(tài)監(jiān)督是一個(gè)更加復(fù)雜的工作,它的實(shí)現(xiàn)過程和細(xì)節(jié)依賴與其它一些底層的基礎(chǔ)服務(wù),然而,上面并沒有包括相關(guān)系統(tǒng)的實(shí)現(xiàn)描述。根據(jù)你當(dāng)前可用的基礎(chǔ)設(shè)施,你可能需要在冒險(xiǎn)重復(fù)執(zhí)行任務(wù)和跳過執(zhí)行任務(wù) 之間做出折中選擇。
狀態(tài)保存
使用 Paxos 來同步只是處理狀態(tài)中遇到的其中一個(gè)問題。Paxos 本質(zhì)上只是通過一個(gè)日志來持續(xù)記錄狀態(tài)改變,并且隨著狀態(tài)的改變而進(jìn)行將日志同步。這會(huì)產(chǎn)生兩個(gè)影響:***,這個(gè)日志需要被壓縮,防止其***增長(zhǎng);第二,這個(gè)日志本身需要保存在一個(gè)地方。
為了避免其***增長(zhǎng),我們僅僅取狀態(tài)當(dāng)前的快照,這樣,我們能夠快速的重建狀態(tài),而不用在根據(jù)之前所有狀態(tài)日志來進(jìn)行重演。比如,在日志中我們記錄一條狀態(tài) “計(jì)數(shù)器加 1”,然后經(jīng)過了 1000 次迭代后,我們就記錄了 1000 條狀態(tài)日志,但是我們也可以簡(jiǎn)單的記錄一條記錄 “將計(jì)數(shù)器設(shè)置為 1000”來做替代。
如果日志丟失,我們也僅僅丟失當(dāng)前狀態(tài)的一個(gè)快照而已??煺掌鋵?shí)是最臨界的狀態(tài) —— 如果丟失了快照,我們基本上就得從頭開始了,因?yàn)槲覀儊G失了上一次快照與丟失快照期間所有的內(nèi)部狀態(tài)。從另一方面說,丟失日志,也意味著,將 Cron 服務(wù)拉回到有記錄的上一次快照所標(biāo)示的地方。
我們有兩個(gè)主要選擇來保存數(shù)據(jù): 存儲(chǔ)在外部的一個(gè)可用的分布式存儲(chǔ)服務(wù)中,或者,在內(nèi)部一個(gè)系統(tǒng)來存儲(chǔ) Cron 服務(wù)的狀態(tài)。當(dāng)我們?cè)O(shè)計(jì)系統(tǒng)時(shí),這兩點(diǎn)都需要考慮。
我們將 Paxos 日志存儲(chǔ)在 Cron 服務(wù)節(jié)點(diǎn)所在服務(wù)器本地的磁盤中。默認(rèn)的三個(gè)節(jié)點(diǎn)意味著,我們有三份日志的副本。我們同樣也將快照存儲(chǔ)在服務(wù)器本身,然而,因?yàn)槠浔旧硎欠浅V匾模覀円矊⑺诜植际酱鎯?chǔ)服務(wù)中做了備份,這樣,即使小概率的三個(gè)節(jié)點(diǎn)機(jī)器都故障了,也能夠服務(wù)恢復(fù)。
我們并沒有將日志本身存儲(chǔ)在分布式存儲(chǔ)中,因?yàn)槲覀冇X得,丟失日志也僅僅代表最近的一些狀態(tài)丟失,這個(gè)我們其實(shí)是可以接受的。而將其存儲(chǔ)在分布式存儲(chǔ)中會(huì)帶來一定的性能損失,因?yàn)樗旧碓诓粩嗟男∽止?jié)寫入不適用與分布式存儲(chǔ)的使用場(chǎng)景。同時(shí)三臺(tái)服務(wù)器全故障的概率太小,但是一旦這種情況發(fā)生了,我們也能自動(dòng)的從快照中恢復(fù),也僅僅損失從上次快照到故障點(diǎn)的這部分而已。當(dāng)然,就像設(shè)計(jì) Cron 服務(wù)本身一樣,如何權(quán)衡,也要根據(jù)自己的基礎(chǔ)設(shè)施情況來決定。
將日志和快照存本地,以及快照在分布式存儲(chǔ)備份,這樣,即使一個(gè)新的節(jié)點(diǎn)啟動(dòng),也能夠通過網(wǎng)絡(luò)從其它已經(jīng)運(yùn)行的節(jié)點(diǎn)處獲取這些信息。這意味著,啟動(dòng)節(jié)點(diǎn)與服務(wù)器本身并沒有任何關(guān)系,重新安排一個(gè)新的服務(wù)器(比如重啟)來?yè)?dān)當(dāng)某個(gè)節(jié)點(diǎn)的角色 其本質(zhì)上也是影響服務(wù)的可靠性的問題之一。
運(yùn)行一個(gè)大型的 Cron
還有一些其它的、小型的,但是同樣有趣的一些情況或能影響部署一個(gè)大型的 Cron 服務(wù)。傳統(tǒng)的 Cron 規(guī)模很?。鹤疃喟瑪?shù)十個(gè) Cron 任務(wù)。然而,如果在一個(gè)數(shù)據(jù)中心的超過千臺(tái)服務(wù)器來運(yùn)行 Cron 服務(wù),那么你就會(huì)遇到各種各樣的問題。
一個(gè)比較大的問題是,分布式系統(tǒng)常常要面臨的一個(gè)經(jīng)典問題:驚群?jiǎn)栴},在 Cron 服務(wù)的使用中會(huì)造成大量的尖峰情況。當(dāng)要配置一個(gè)每天執(zhí)行的 Cron 任務(wù),大多數(shù)人***時(shí)間想到的是在半夜執(zhí)行,然后它們就這么配置了。如果一個(gè) Cron 任務(wù)在一臺(tái)機(jī)器上執(zhí)行,那沒有問題,但是如果你的任務(wù)是執(zhí)行一個(gè)涉及數(shù)千 worker 的 mapreduce 任務(wù),或者,有 30 個(gè)不同的團(tuán)隊(duì)在數(shù)據(jù)中心中要配置這樣的一個(gè)每天運(yùn)行的任務(wù),那么我們就必須要擴(kuò)展下 crontab 的格式了。
傳統(tǒng)的 crontab,用戶通過定義“分鐘”,“小時(shí)”,“每月(或每周)第幾天”,“月數(shù)”來指定 cron 任務(wù)運(yùn)行的時(shí)間,或者通過星號(hào)(*)來代表每個(gè)對(duì)應(yīng)的值。如,每天凌晨運(yùn)行,它的 crontab 格式為0 0 * * *,代表每天的 0 點(diǎn) 0 分運(yùn)行。我們?cè)诖嘶A(chǔ)之上還推出了問號(hào)(?)這個(gè)符號(hào),它標(biāo)示,在這個(gè)對(duì)應(yīng)的時(shí)間軸上,任何時(shí)間都可以,Cron 服務(wù)就會(huì)自由選擇合適的值,在指定的時(shí)間段內(nèi)隨機(jī)選擇對(duì)應(yīng)的值,這樣使任務(wù)運(yùn)行更均衡。如 0 ? * * *,表示每天 0-23 點(diǎn)鐘,隨機(jī)一個(gè)小時(shí)的 0 分來運(yùn)行這個(gè)任務(wù)。
盡管加了這項(xiàng)變化,由 Cron 任務(wù)所造成的 load 值仍然有明顯的尖峰,下圖表示了 Google 中 cron 任務(wù)加載的數(shù)量。尖峰值往往表示那些需要固定頻率在指定時(shí)間運(yùn)行的任務(wù)。
總結(jié)
Cron 服務(wù)作為 UNIX 的基礎(chǔ)服務(wù)已經(jīng)有接近 10 年。當(dāng)前整個(gè)行業(yè)都朝著大型分布式系統(tǒng)演化,那時(shí),表示硬件的最小單位將會(huì)是數(shù)據(jù)中心,那么大量的技術(shù)棧需要對(duì)應(yīng)改變,Cron 也不會(huì)是例外。仔細(xì)審視下 Cron 服務(wù)所需要的服務(wù)特性,以及 Cron 任務(wù)的需求,都會(huì)推動(dòng)我們來進(jìn)行新的設(shè)計(jì)。
基于 Google 的解決方案,我們已經(jīng)討論了 Cron 服務(wù)在一個(gè)分布式系統(tǒng)中對(duì)應(yīng)的約束和可能的設(shè)計(jì)。這個(gè)解決方案需要在分布式環(huán)境中的強(qiáng)一致性保證,它的實(shí)現(xiàn)核心是通過 Paxos 這樣一種通用的算法,在一個(gè)不可靠的環(huán)境中達(dá)成最終一致。使用 Paxos,正確對(duì)大規(guī)模環(huán)境下 Cron 任務(wù)失敗情況的分析,以及分布式的環(huán)境的使用,共同造就了在 Google 內(nèi)部使用的健壯的 Cron 服務(wù)。