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