天天看點

使用Chronos執行whenever任務

對于業務上需要定時執行的背景任務,我們使用ruby的whenever子產品定義執行時間及其對應的rake任務,然後将所有任務放入一個長期運作的docker容器,由裡面的crond服務執行配置的cron任務。

在我們将容器排程架構遷移到mesos+marathon之後,所有容器都需要設定資源配額。執行背景任務的容器閑置時白白占用了配額,而且配額的設定也隻能以所有任務中最重的一個為準,是以這種方式造成了較大的資源浪費。

這個解析器和whenever本身的功能類似,whenever将<code>schedule.rb</code>轉換為crontab格式,我們的解析器則将其轉化為chronos支援的格式。

由于<code>schedule.rb</code>是ruby代碼,是以使用ruby來實作解析器自然也最為友善。隻需要實作對應的<code>every()</code>方法以及<code>rake()</code>方法,在<code>every()</code>和<code>rake()</code>方法中将解析到的執行時間和執行語句存入執行個體變量<code>@schedules</code>中即可。

雖然代碼邏輯很簡單,但依然有幾點是需要注意。

我們将形如<code>10.minutes</code>、<code>1.day</code>以及<code>1.month</code>的時間表示定義為<code>Fixnum</code>的對應方法,最終其會被轉換為秒數,這在rails中相當常見。在<code>every()</code>方法内拿到的間隔時間就是轉換之後的秒數,但是對于2592000這個數,它究竟是表示<code>30.days</code>天還是<code>1.month</code>呢?

好在這并不是一個問題,因為whenever也是這樣處理的。whenever會将時間解析為盡可能大的機關,是以2592000表示1個月而不是30天。我們在實作上沿用這個邏輯。

另外,受crontab的限制,whenever無法表示<code>every(45.days)</code>這樣的時間,原因是crontab中日期字段不在0-31之間的話(<code>0 0 */45 * *</code>),最終的執行效果是每天執行一次。如果非要用crontab表示每45天執行一次,應該是這樣的:

大概是沒有必要為這種小衆需求實作複雜的邏輯,whenever會将45天簡化為1個月。在chronos中則沒有這個問題,可以放心地使用這樣的時間。

不同于crontab預設總是從零時零分開始計算下次執行時間,chronos的schedule格式需要自己指定起始時間。

以斜杠分隔的第二部分即為開始執行時間,第三部分為執行間隔時間,下一次應該什麼時候執行都是從第二部分指定的時間開始計算的。為了保證每一次的執行時間都符合預期,這裡需要一個絕對時間作為開始執行時間。但又不能簡單地設為過去某個時間點,原因和chronos另一個規則有關:當一個任務部署後,chronos會判斷其開始執行時間,如果大于目前時間則等到了開始時間再執行任務,小于的話則立即執行一次任務。也就是說,如果開始時間為過去時間,那麼任務每次被部署後都會被執行一次。

這裡正确的做法大緻是:在代碼中設定一個過去時間作為基準時間,部署時根據基準時間和間隔時間計算出下次的執行時間。

這個基準時間的計算還是稍稍有些麻煩的:

基準時間對于每個任務都可能不一樣,因為every方法中有一個<code>at</code>參數可以指定任務的開始時間,如果基準時間設為絕對時間,那<code>at</code>參數就沒有用了。

關于<code>at</code>參數的處理,我們和whenever保持一緻,使用<code>chronic</code>子產品來解析。如果有指定<code>at</code>參數,使用<code>chronic</code>解析得到一個Time類型,否則就使用一個絕對的BaseTime作為基準時間。

<code>chronic</code>得到的Time類型,其實也是動态的。比如解析<code>1:10 am</code>這個<code>at</code>參數,得到的是當天的時間<code>2017-09-21 01:10:00</code>。為了每次部署時都得到一個不變的基準時間,需要将其與BaseTime合并。

合并的政策是,判斷間隔時間的最小機關,比間隔時間機關小的機關,從解析<code>at</code>得到的Time中選取,否則從BaseTime中選取。例如<code>every 2.days, at: '1:10 am'</code>,基準時間中的時、分、秒,來自<code>at</code>,即<code>01:10:00</code>。

有了基準時間,再根據間隔時間就可以計算出下一次的執行時間了。

whenever中,并不關注任務的名稱。但在chronos中,每個rake都被拆分成了獨立的任務,是以就需要一個名字來辨別每一個任務。我們使用rake方法的參數——rake task name——作為任務名,當然,還要處理特殊字元以及重名的情況。

我們将<code>schedule.rb</code>中定義的所有任務拆分然後部署到chronos,如果<code>schedule.rb</code>中某個任務被删除或者改名了,那麼舊的任務還保留在chronos中。對于這種情況的處理,我們在部署的時候記錄下本次部署的所有任務名,部署完成後周遊chronos上的所有任務,所有不在本次部署中的任務則是需要删除的。

為了叙述上的簡單,這裡假設chronos上的所有任務都是由一個<code>schedule.rb</code>建立的。實際上我們的chronos上運作着多種類型的任務,我們使用一個字首作為命名空間來區分不同類型的任務。

拆分成多個chronos任務後,每個任務以短程序的方式運作,執行完畢即退出,釋放相應資源,而且還能對不同的任務配置不同的配額,大大提高了資源使用率。此外,這次遷移還帶來了一些額外的改進。

通過chronos的Web UI,可以檢視所有任務的上次執行狀态,以及下次執行時間,在Mesos Web UI上也可以檢視每個失敗任務的錯誤日志。以往的運作模式隻能登陸到容器中檢視cron的日志才能知道任務執行的具體情況。

使用Chronos執行whenever任務

crontab沒有重試機制,如果想失敗後重新執行,需要在每個業務代碼中實作相應的邏輯。作為一個定位于Fault Tolerant的任務排程架構,chronos當然是預設支援重試的。如果任務執行失敗,chronos會嘗試重新執行,直到成功或者達到最大重試次數。

mesos是一個分布式排程架構,chronos任務會被mesos配置設定到合适的節點上執行。配合重試機制,當任務因某一個節點故障而執行失敗後,chronos會在其他節點重新執行該任務。

任務被放入chronos,其類型和marathon上的服務一樣,都屬于mesos任務。這樣就統一了自動化監控的邏輯,隻需要監控所有mesos任務即可,不用為不同類型的任務編寫不同的監控邏輯。