歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Unix知識 >> Unix資訊

如何設計穩定性橫跨全球的Cron服務

這篇文章主要來描述下 Google 是如何實現一套可靠的分布式 Cron 服務,服務於內部那些需要絕大多數計算作業定時調度的團隊。 在這個系統的實踐過程中,我們收獲了很多,包括如何設計、如何實現使得它看上去像一個靠譜的基礎服務。 在這裡,我們來討論下分布式 Cron 可能會遇到哪些問題,以及如何解決它。

Cron 是 UNIX 中一個常見的工具,用來定期執行一些用戶指定的任意任務。我們先來分析下 Cron 的基本原則和它最常見的實現,然後我們來回顧下像 Cron 這樣的服務應該如何運行在一個大型的、分布式的環境中,這樣即使單機故障也不會對系統可用性造成影響。 我們將會介紹了一個建立在少量機器上的 Cron 系統,然後結合數據中心的調度服務,從而可以在整個數據中心中運行 Cron 任務。

在我們在描述如何運行一個靠譜的分布式 Cron 服務之前,讓我們先來從一個 SRE 的角度來回顧下 Cron。

Cron 是一個通用的工具,無論是管理員還是普通用戶都可以用它來在系統上運行指定的命令,以及指定何時運行命令,這些指定運行的命令可以是定期垃圾回收,也可以是定期數據分析。 最常見的時間指定格式被稱為crontab,它不僅支持簡單的時間周期(如,每天中午一次,每個小時一次),也支持較復雜的時間周期,如每個周六、每個月的第 30 天等等。

Cron 通常只包含一個組件,被稱為 crond,它是一個後台守護程序,加載所有需要運行的 cron 定時任務,根據它們接下來的運行時間來進行排序,然後這個守護進程將會等待直到第一個任務開始執行。在這個時刻,crond 將會加載執行這個任務,之後將它放入隊列等待下一次運行。

可靠性Reliability

從可靠性的角度來看一個服務,需要有很多注意的地方。

第一,比如 crond,它的故障域本質上來說只是一台機器,如果這個機器沒有運行,不論是 cron 調度還是加載的任務都是不可運行的。因此,考慮一個非常簡單的分布式的例子 ——— 我們使用兩台機器,然後 cron 調度在其中一台機器上運行任務(比如通過 ssh)。然後產生了一個故障域了:調度任務和目標服務器都可能失敗。

另外一個需要注意的地方是,即使是 crond 重啟(包括服務器重啟),上面部署的 crontab 配置也不應該丟失。crond 執行一個任務然後就‘忘記’了這個任務的狀態,它並不會嘗試去跟蹤這個任務的執行狀態,包括是否該執行是否已經執行。

anacron 是一個例外,它是 crontab 的一個補充,它嘗試運行哪些因為服務器宕機而應該執行卻沒執行的任務。這僅限於每日或者更小執行頻率的任務,但對於在工作站和筆記本電腦上運行維護工作非常有用。通過維護一個包括最後執行時間的配置文件,使得運行這些特殊的任務更加方便。

Cron 的任務和冪等性

Cron 的任務用來執行定期任務,但是除此之外,卻很難在進一步知道它們的功能。讓我們先把要討論的主題拋開一邊,現在先來就 Cron 任務本身來做下探討,因為只有理解了 Cron 任務的各種各樣的需求,才能知道它是如何影響我們需要的可靠性要求,而這一方面的探討也將貫穿接下來的文章。

有一些 Cron 任務是冪等性的,這樣在某些系統故障的情況下,可以很安全的執行它們多次,比如,垃圾回收。然而有些 Cron 任務卻不應該被執行多次,比如某個發送郵件的任務。

還有更復雜的情況,有些 Cron 任務允許因為某些情況而“忘了”運行,而某些 Cron 任務卻不能容忍這些,比如,垃圾回收的 Cron 任務每 5 分鐘調度一次,即使某一次沒有執行也不會有太大的問題,然而,一個月一次的支付薪水的任務,卻絕對不允許有失誤。

Cron 任務的各種不同的類型使得不可能有一個通用的解決方案,使得它可以應對各種各樣的失敗。所以,在本文中上面說的那些情況,我們更傾向於錯過某一次的運行,而不是運行它們兩次或者更多。Cron 任務的所有者應該(也必須)監控著它們的任務,比如返回任務的調用結果,或者單獨發送運行的日志給所屬者等等,這樣,即使跳過了任務的某次執行,也能夠很方便的采取對應的補救動作。當任務失敗時,我們更傾向於將任務狀態置為 “fail closed” 來避免產生系統性的不良狀態。

大規模部署 Cron

當從單機到集群部署 Cron 時,需要重新思考如何使 Cron 在這種環境下良好的運行。在對 Google 的 Cron 進行解說之前,讓我們先來討論下單機以及多機之間的區別,以及針對這變化如何設計。

擴展基礎架構

常規的 Cron 僅限於單個機器,而大規模部署的 Cron 解決方案不能僅僅綁定到一個單獨的機器。假設我們擁有一個 1000 台服務器的數據中心,如果即使是 1/1000 的幾率造成服務器不可用都能摧毀我們整個 Cron 服務,這明顯不是我們所希望的。

所以,為了解決這個問題,我們必須將服務與機器解耦。這樣如果想運行一個服務,那麼僅僅需要指定它運行在哪個數據中心即可,剩下的事情就依賴於數據中心的調度系統(當然前提是調度系統也應該是可靠的),調度系統會負責在哪台或者哪些機器上運行服務,以及能夠良好的處理機器掛掉這種情況。 那麼,如果我們要在數據中心中運行一個任務,也僅僅是發送一條或多條 RPC 給數據中心的調度系統。

然而,這一過程顯然並不是瞬時完成的。比如,要檢查哪些機器掛掉了(機器健康檢查程序掛了怎麼辦),以及在另外一些機器上重新運行任務(服務依賴重新部署重新調用任務)都是需要花費一定時間的。

將程序轉移到另外一個機器上可能意味著損失一些存儲在老機器上的一些狀態信息(除非也采用動態遷移),重新調度運行的時間間隔也可能超過最小定義的一分鐘,所以,我們也必須考慮到上述這兩種情況。一個很直接的做法,將狀態文件放入分布式文件系統,如 GFS,在任務運行的整個過程中以及重新部署運行任務時,都是用它來記錄使用相關狀態。 然而,這個解決方案卻不能滿足我們預期的時效性這個需求,比如,你要運行一個每五分鐘跑一次的 Cron 任務,重新部署運行消耗的 1-2 分鐘對這個任務來說也是相當大的延遲了。

及時性的需求可能會促使各種熱備份技術的使用,這樣就能夠快速記錄狀態以及從原有狀態快速恢復。

需求擴展

將服務部署在數據中心和單服務器的另一個實質性的區別是,如何規劃任務所需要的計算資源,如 CPU 或內存等。

單機服務通常是通過進程來進行資源隔離,雖然現在 Docker 變得越來越普遍,但是使用它來隔離一切目前也不太是很通用的做法,包括限制 crond 以及它所要運行的任務。

大規模部署在數據中心經常使用容器來進行資源隔離。隔離是必要的,因為我們肯定希望數據中心中運行的某個程序不會對其它程序產生不良影響。為了隔離的有效性,在運行前肯定得先預知運行的時候需要哪些資源——包括 Cron 系統本身和要運行的任務。這又會產生一個問題,即如果數據中心暫時沒有足夠的資源,那麼這個任務可能會延遲運行。這就要求我們不僅要監控 Cron 任務加載的情況,也要監控 Cron 任務的全部狀態,包括開始加載到終止運行。

現在,我們希望的 Cron 系統已經從單機運行的情況下解耦,如之前描述的那樣,我們可能會遇到部分任務運行或加載失敗。這時候幸虧任務配置的通用性,在數據中心中運行一個新的 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 使用一個獨立的主任務,見下圖,只有它才能更改共享的狀態,也只有它才能加載 Cron 任務。我們這裡使用了 Paxos 的一個變體—— Fast Paxos,這裡 Fast Paxos 的主節點也是 Cron 服務的主節點。

如果主節點掛掉,Paxos 的健康檢查機制會在秒級內快速發現,並選舉出一個新的主節點。一旦選舉出新的主節點,Cron 服務也就隨著選舉出了一個新的 Cron 主節點,這個新的 Cron 主節點將會接手前一個主節點留下的所有的未完成的工作。在這裡 Cron 的主節點和 Paxos 的主節點是一樣的,但是 Cron 的主節點需要處理一下額外的工作而已。快速選舉新的主節點的機制可以讓我們大致可以容忍一分鐘的故障時間。

我們使用 Paxos 算法保持的最重要的一個狀態是,哪些 Cron 任務在運行。對於每一個運行的 Cron 任務,我們會將其加載運行的開始以及結束同步給一定數量的節點。

主節點和從節點角色

如上面描述的那樣,我們在 Cron 服務中使用 Paxos 並部署,其擁有兩個不同的角色,主節點以及從節點。讓我們來就每個角色來做具體的描述。

主節點

主節點用來加載 Cron 任務,它有個內部的調度系統,類似於單機的 crond,維護一個任務加載列表,在指定的時間加載任務。

當任務加載的時刻到來,主節點將會 “宣告” 它將會加載這個指定的任務,並且計算這個任務下次的加載時間,就像 crond 的做法一樣。當然,就像 crond 那樣,一個任務加載後,下一次的加載時間可能人為的改變,這個變化也要同步給從節點。簡單的標識 Cron 任務還不夠,我們還應該將這個任務與開始執行時間相關聯綁定,以避免 Cron 任務在加載時發生歧義(特別是那些高頻的任務,如一分鐘一次的那些)。這個“通告”通過 Paxos 來進行。下圖展示了這一過程。

保持 Paxos 通訊同步非常重要,只有 Paxos 法定數收到了加載通知,這個指定的任務才能被加載執行。Cron 服務需要知道每個任務是否已經啟動,這樣即使主節點掛掉,也能決定接下來的動作。如果不進行同步,意味著整個 Cron 任務運行在主節點,而從節點無法感知到這一切。如果發生了故障,很有可能這個任務就被再次執行,因為沒有節點知道這個任務已經被執行過了。

Cron 任務的完成狀態通過 Paxos 通知給其它節點,從而保持同步,這裡要注意一點,這裡的“完成” 狀態並不是表示任務是成功或者失敗。我們跟蹤 Cron 任務在指定調用時間被執行的情況,我們同樣需要處理一點情況是,如果 Cron 服務在加載任務進行執行的過程中失敗後怎麼辦,這點我們在接下來會進行討論。

主節點另一個重要的特性是,不管是出於什麼原因主節點失去了其主控權,它都必須立馬停止同數據中心調度系統的交互。主控權的保持對於訪問數據中心應該是互斥了。如果不這樣,新舊兩個主節點可能會對數據中心的調度系統發起互相矛盾的操作請求。

從節點

從節點實時監控從主節點傳來的狀態信息,以便在需要的時刻做出積極響應。所有主節點的狀態變動信息,都通過 Paxos 傳到各個從節點。和主節點類似的是,從節點同樣維持一個列表,保存著所有的 Cron 任務。這個列表必須在所有的節點保持一致(當然還是通過 Paxos)。

當接到加載任務的通知後,從節點會將此任務的下次加載時間放入本地任務列表中。這個重要的狀態信息變化(這是同步完成的)保證了系統內部 Cron 作業的時間表是一致的。我們跟蹤所有有效的加載任務,也就是說,我們跟蹤任務何時啟動,而不是結束。

如果一個主節點掛掉或者因為某些原因失聯(比如,網絡異常等),一個從節點有可能被選舉成為一個新的主節點。這個選舉的過程必須在一分鐘內運行,以避免 Cron 任務丟失的情況。一旦被選舉為主節點,所有運行的加載任務(或部分失敗的),必須被重新驗證其有效性。這個可能是一個復雜的過程,在 Cron 服務系統和數據中心的調度系統上都需要執行這樣的驗證操作,這個過程有必要詳細說明。

故障恢復

如上所述,主節點和數據中心的調度系統之間會通過 RPC 來加載一個邏輯 Cron 任務,但是,這一系列的 RPC 調用過程是有可能失敗的,所以,我們必須考慮到這種情況,並且處理好。

回想下,每個加載的 Cron 任務會有兩個同步點:開始加載以及執行完成。這能夠讓我們區分開不同的加載任務。即使任務加載只需要調用一次 RPC,但是我們怎麼知道 RPC 調用實際真實成功呢?我們知道任務何時開始,但是如果主節點掛了我們就不會知道它何時結束。

為了解決這個問題,所有在外部系統進行的操作,要麼其操作是冪等性的(也就是說,我們可以放心的執行它們多次),要麼我們必須實時監控它們的狀態,以便能清楚的知道何時完成。

這些條件明顯增加了限制,實現起來也有一定的難度,但是在分布式環境中這些限制卻是保證 Cron 服務准確運行的根本,能夠良好的處理可能出現的 “fail”。如果不能妥善處理這些,將會導致 Cron 任務的加載丟失,或者加載多次重復的 Cron 任務。

大多數基礎服務在數據中心(比如 Mesos)加載邏輯任務時都會為這些任務命名,這樣方便了查看任務的狀態,終止任務,或者執行其它的維護操作。解決冪等性的一個合理的解決方案是將執行時間放在名字中 ——這樣不會在數據中心的調度系統裡造成任務異變操作 —— 然後在將它們分發給 Cron 服務所有的節點。如果 Cron 服務的主節點掛掉,那麼新的主節點只需要簡單的通過預處理任務名字來查看其對應的狀態,然後加載遺漏的任務即可。

注意下,我們在節點間保持內部狀態一致的時候,實時監控調度加載任務的時間。同樣,我們也需要消除同數據中心調度交互時可能發生的不一致情況,所以這裡我們以調度的加載時間為准。比如,有一個短暫但是頻繁執行的 Cron 任務,它已經被執行了,但是在准備把情況通告給其它節點時,主節點掛了,並且故障時間持續的特別長——長到這個 Cron 任務都已經成功執行完了。然後新的主節點要查看這個任務的狀態,發現它已經被執行完成了,然後嘗試加載它。如果包含了這個時間,那麼主節點就會知道,這個任務已經被執行過了,就不會重復執行第二次。

在實際實施的過程中,狀態監督是一個更加復雜的工作,它的實現過程和細節依賴與其它一些底層的基礎服務,然而,上面並沒有包括相關系統的實現描述。根據你當前可用的基礎設施,你可能需要在冒險重復執行任務和跳過執行任務 之間做出折中選擇。

狀態保存

使用 Paxos 來同步只是處理狀態中遇到的其中一個問題。Paxos 本質上只是通過一個日志來持續記錄狀態改變,並且隨著狀態的改變而進行將日志同步。這會產生兩個影響:第一,這個日志需要被壓縮,防止其無限增長;第二,這個日志本身需要保存在一個地方。

為了避免其無限增長,我們僅僅取狀態當前的快照,這樣,我們能夠快速的重建狀態,而不用在根據之前所有狀態日志來進行重演。比如,在日志中我們記錄一條狀態 “計數器加 1”,然後經過了 1000 次迭代後,我們就記錄了 1000 條狀態日志,但是我們也可以簡單的記錄一條記錄 “將計數器設置為 1000”來做替代。

如果日志丟失,我們也僅僅丟失當前狀態的一個快照而已。快照其實是最臨界的狀態 —— 如果丟失了快照,我們基本上就得從頭開始了,因為我們丟失了上一次快照與丟失快照期間所有的內部狀態。從另一方面說,丟失日志,也意味著,將 Cron 服務拉回到有記錄的上一次快照所標示的地方。

我們有兩個主要選擇來保存數據: 存儲在外部的一個可用的分布式存儲服務中,或者,在內部一個系統來存儲 Cron 服務的狀態。當我們設計系統時,這兩點都需要考慮。

我們將 Paxos 日志存儲在 Cron 服務節點所在服務器本地的磁盤中。默認的三個節點意味著,我們有三份日志的副本。我們同樣也將快照存儲在服務器本身,然而,因為其本身是非常重要的,我們也將它在分布式存儲服務中做了備份,這樣,即使小概率的三個節點機器都故障了,也能夠服務恢復。

我們並沒有將日志本身存儲在分布式存儲中,因為我們覺得,丟失日志也僅僅代表最近的一些狀態丟失,這個我們其實是可以接受的。而將其存儲在分布式存儲中會帶來一定的性能損失,因為它本身在不斷的小字節寫入不適用與分布式存儲的使用場景。同時三台服務器全故障的概率太小,但是一旦這種情況發生了,我們也能自動的從快照中恢復,也僅僅損失從上次快照到故障點的這部分而已。當然,就像設計 Cron 服務本身一樣,如何權衡,也要根據自己的基礎設施情況來決定。

將日志和快照存本地,以及快照在分布式存儲備份,這樣,即使一個新的節點啟動,也能夠通過網絡從其它已經運行的節點處獲取這些信息。這意味著,啟動節點與服務器本身並沒有任何關系,重新安排一個新的服務器(比如重啟)來擔當某個節點的角色 其本質上也是影響服務的可靠性的問題之一。

運行一個大型的 Cron

還有一些其它的、小型的,但是同樣有趣的一些情況或能影響部署一個大型的 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 值仍然有明顯的尖峰,下圖表示了 Google 中 cron 任務加載的數量。尖峰值往往表示那些需要固定頻率在指定時間運行的任務。

總結

Cron 服務作為 UNIX 的基礎服務已經有接近 10 年。當前整個行業都朝著大型分布式系統演化,那時,表示硬件的最小單位將會是數據中心,那麼大量的技術棧需要對應改變,Cron 也不會是例外。仔細審視下 Cron 服務所需要的服務特性,以及 Cron 任務的需求,都會推動我們來進行新的設計。

基於 Google 的解決方案,我們已經討論了 Cron 服務在一個分布式系統中對應的約束和可能的設計。這個解決方案需要在分布式環境中的強一致性保證,它的實現核心是通過 Paxos 這樣一種通用的算法,在一個不可靠的環境中達成最終一致。使用 Paxos,正確對大規模環境下 Cron 任務失敗情況的分析,以及分布式的環境的使用,共同造就了在 Google 內部使用的健壯的 Cron 服務。

Copyright © Linux教程網 All Rights Reserved