分散式任務排程

銅板街技術發表於2019-03-04

前言

任務排程可以說是所有系統都必須要依賴的一箇中間系統,主要負責觸發一些需要定時執行的任務。傳統的非分散式系統中,只需要在應用內部內建一些定時任務框架,比如 spring 整合 quartz,就可以完成一些定時任務工作。在分散式系統中,這樣做的話,就會面臨任務重複執行的問題(多臺伺服器都會觸發)。另外,隨著公司專案的增加,也需要一個統一的任務管理中心來解決任務配置混亂的問題。

公司的任務排程系統經歷了兩個版本的開發,1.0 版本始於 2013 年,主要解決當時各個系統任務配置不統一,任務管理混亂的問題,1.0 版本提供了一個統一的任務管理平臺。2.0 版本主要解決 1.0 版本存在的單點問題。

任務排程系統 1.0

1.0 版本的任務排程系統架構如下圖1:由一臺伺服器負責管理所有需要執行的任務,任務的管理與觸發動作都由該機器來完成,通過內建的 quartz 框架,來完成定時任務的觸發,在配置任務的時候,指定客戶端 ip 與埠,任務觸發的時候,根據配置的路由資訊,通過 http 訊息傳遞的方式,完成任務指令的下達。

這裡存在一個比較嚴重的問題,任務排程服務只能部署一臺,所以該服務成為了一個單點,一旦當機或出現其他什麼問題,就會導致所有任務無法執行。

分散式任務排程

任務排程系統 2.0


2.0 版本主要為了解決 1.0 版本存在的單點問題,即將任務排程服務端調整為分散式系統,改造後的專案結構如下圖2:需要改造排程服務端,使其能夠支援多臺伺服器同時存在。這帶來一個問題,多臺排程伺服器中,只能有一臺伺服器允許傳送任務(如果所有伺服器都發任務的話,會導致一個任務在一個觸發時間被觸發多次),所以需要一個 Leader,只有 Leader 才有下達任務執行命令的許可權。其他非 Leader 伺服器這裡稱為 Flower,Flower 機器沒有執行任務的許可權,但是一旦 Leader 掛掉,需要從所有 Flower 機器中,重新選舉出一個新的 Leader,繼續執行後續任務。

分散式任務排程

另外一個問題是,如果某一個應用,比如說資產中心繫統,我們有 A,B,C 三臺機器,在凌晨12點要執行一個任務, 排程系統要如何發現 A,B,C 三臺機器 ?如果 B 機器在12點的時候,恰好當機,排程系統又要如何識別出來? 其實就是一個服務發現的問題。

群首選舉

當多臺任務排程伺服器同時存在時,如何選舉一個 Leader,是面臨的第一個問題。比較成熟的演算法如:基於 paxos 一致性演算法的 zookeeper、Raft 一致性演算法等等都可以實現。在該專案中,採用的是一個簡單的辦法,基於 zookeeper 的臨時(ephemeral)節點功能。

zookeeper 的節點分為2類,持久節點和臨時節點,持久節點需要手動 delete 才會被刪除,臨時節點則不需要這樣,當建立該臨時節點的客戶端崩潰或者與 zookeeper 的連線斷開,則該客戶端建立的所有臨時節點都會被刪除。

zookeeper 另外一個功能:監視點。某一個連線到 zookeeper 的客戶端,可以監視一個 zookeeper 節點的變化,比如 exists 監視操作,會監視節點是否存在,如果節點被刪除,那麼客戶端將會收到一條通知。

基於臨時節點和監視點這兩個特性,可以採用 zookeeper 實現一個簡單的群首選舉功能:每一臺任務排程伺服器啟動的時候,都嘗試建立群首選舉節點,並在節點中寫入當前機器 IP,如果節點建立成功,則當前機器為 Leader。如果節點已經存在,檢查節點內容,如果資料不等於當前機器 IP,則監視該節點,一旦節點被刪除,則重新嘗試建立群首選舉節點。

分散式任務排程

使用 zookeeper 臨時節點做群首選舉的缺陷:有的時候,即使某一臺任務排程伺服器能夠正常連線到 zookeeper,也並不表示該機器是可用的,比如一個極端場景,伺服器無法連線到資料庫,但是可以正常連線到 zookeeper,這個時候,基於 zookeeper 的臨時節點功能,是無法剝離這一臺異常機器的(但是可以通過一些手段處理這個問題,比如本地開發一套自檢程式,檢測所有可能導致服務不可用的異常,如資料庫異常等等,一旦自檢程式失敗,則不再傳送 zookeeper 心跳包,從而剝離異常機器)。

腦裂問題

群首選舉中,我們選舉出了一個 Leader,我們也希望系統中只有一個 Leader,但是在一些特殊情況下,會出現多個 Leader 同時發號施令的現象,即腦裂問題。

有以下幾種情況會導致出現腦裂問題:

  • zookeeper 本身叢集配置有問題,導致 zookeeper 本身腦裂了。

  • 同一個叢集裡面的多個伺服器的 zookeeper 配置不一致。

  • 同一個 IP,部署了多臺任務排程伺服器。

  • 任務排程服務主備切換時候的瞬時腦裂問題。

其中前三個屬於配置問題,應用程式沒有辦法解決。

第四個主備切換時候的瞬時腦裂,具體場景如下圖4:

分散式任務排程

現象:

  • A 先連線上了 zookeeper,併成功建立 /leader 節點。

  • t1: A 與 zookeeper 失去連線, 此時 A 依然認為自己是 Leader。

  • t2: zookeeper 發現 A 超時,所以刪除 A 的所有臨時節點,包括 /leader 節點。由於此時B 正在監視 /leader 節點,故 zookeeper 在刪除該節點的同時,也會通知 B 伺服器,B 收到通知之後立即嘗試建立 /leader 節點。

  • t3: B 建立 /leader 節點成功,當選為 Leader。

  • t4: A 網路恢復,重新訪問 ZK 時,發現失去 Leader 許可權,更新本地 Leader-Flag = false。

可以看出

如果 A 機器,在 T1 發現無法連線到 zookeeper 之後,如果不失效本地 Leader 許可權,那麼,在 T3-T4 時間段內,就有可能會出現腦裂現象,即 A、B 兩臺機器同時成為了Leader。(這裡 A 發現超時之後,之所以不立即失效 Leader 許可權,是出於系統可用性的一個權衡:儘可能減少沒有 Leader 的時間。因為一旦 A 發現超時,馬上就失效Leader 許可權的話,會導致 T1-T3 這一段時間,沒有任何一個 Leader 存在,相比於出現2個 Leader 來說,沒有 Leader 的影響更嚴重)。

腦裂出現的原因很多,一些配置性問題導致的腦裂,無法通過程式去解決,腦裂現象無法完全避免,必須通過其他方式保障系統在腦裂情況下的資料一致性。

系統採用的是基於資料庫的唯一主鍵約束:任務每一次觸發,都會有一個觸發時間(Schedule Time),該時間精確到秒,如果對於同一個任務,每一次觸發執行的時候,在資料庫插入一條任務執行流水,該流水錶使用任務觸發時間 + 任務 Id 來作為唯一主鍵,即可避免腦裂時帶來的影響。兩臺伺服器如果同時觸發任務,且都具有 Leader 許可權,此時,其中一臺伺服器會因為資料庫唯一主鍵約束,導致任務執行失敗,從而取消執行。(由於在分散式環境下,多臺 Legends 伺服器時鐘可能會有一些誤差, 如果任務觸發時間過短,還是有可能出現併發執行的問題:A 機器執行01秒的任務,B 機器執行02秒的任務。所以不建議任務的觸發時間過短)。

發現存活的客戶端

服務端傳送任務之前,需要知道有哪些伺服器是存活的,具體實現方式如下:

應用伺服器客戶端啟動成功之後,會向 zookeeper 註冊本機 IP(即建立臨時節點)

任務排程伺服器通過監視 /clients 節點的子節點資料,來發現有哪些機器是可用(這裡通過監視點來永久監視客戶端節點的變化情況)。

當該系統有任務需要傳送的時候,排程伺服器只需要查詢本地快取資料,就可以知道有哪些機器是存活狀態,之後根據任務配置的策略,傳送任務到 zookeeper 中指定客戶端的待執行任務列表中即可。

分散式任務排程

任務執行流程

任務觸發的具體流程如下圖6:

分散式任務排程

流程說明:

  1. Quartz 框架觸發任務執行 (如果發現當前機器非 Leader,則直接結束)。

  2. 伺服器查詢本地快取資料,找到對應的應用的存活伺服器列表,並根據配置的任務觸發策略,選取可以執行的客戶端。

  3. 向 ZK 中,需要執行任務的客戶端所對應的任務分配節點(/assign)寫入任務資訊 。

  4. 應用伺服器的發現分配的任務,獲取任務並執行任務。


這裡存在一個問題:在任務資料傳送到 zk 之後,如果存活的客戶端立即死亡要如何處理?因為任務排程伺服器一直在監視客戶端註冊節點的變化,一旦一臺應用伺服器死亡,任務排程伺服器會收到一條客戶端死亡的通知,此時,可以檢測該客戶端對應的任務分配節點下,是否有已經分配,但是還未來得及執行的任務,如果有,則刪除任務節點,回收未處理的任務,再重新將該任務分配到其他存活伺服器執行即可(這裡客戶端執行任務的操作是,先刪除 zookeeper 中的任務節點,之後再執行任務,如果一個任務節點已經被刪除,則表示該任務已經成功下達,由於刪除操作只有一個 zk 客戶端能夠執行成功,故任務要麼被服務端回收,要麼被客戶端執行)。

這個問題引申的還有一些其他問題,比如任務排程服務發現應用伺服器死亡,回收該應用伺服器未執行的任務之後,突然斷電或者失去了 Leader 許可權,導致記憶體資料丟失,此時會造成任務漏發現象。

任務變更的資訊流

當一個使用者在任務排程伺服器後臺修改或新增一個任務時,任務資料需要同步到所有的任務排程伺服器,由於任務資料儲存在 DB,ZK 以及每個排程伺服器的記憶體中,任務資料的一致性,是任務更新時要處理的主要問題。

任務資料的更新順序如圖7所示:

分散式任務排程

  1. 使用者連線到叢集中的某一臺 Server, 對任務資料做修改,提交。

  2. Server 接收到請求之後,先更新 DB 資料 ( version + 1 )。

  3. 非同步提交 ZK 資料變動( zookeeper 資料更新也是強制樂觀鎖更新的模式) 。

  4. 所有 Server 中的 JOB Watcher 監控到 ZK 中的任務 資料發生了變化,重新查詢 ZK 並更新本地 Quartz 中的記憶體資料。

由於 2,3,4 三步更新,都採用了樂觀鎖更新的模式,且所有任務資料的變動,都是按照一致的更新順序操作,所以解決了併發更新的問題。另外這裡之所以要採用非同步更新zookeeper 的原因,是由於 zookeeper 客戶端程式是單執行緒模式,任何同步的程式碼,都會阻塞所有的非同步呼叫,從而降低整個系統的效能,另外也有 SessionExpired 的風險( zookeeper 一個重量級的異常)。

三步操作,任何一步都有可能失敗,但是又無法做到強一致性,所以只能採用最終一致性來解決資料不一致的問題。採用的方案是用一個內建執行緒,查詢5分鐘內有過更新的任務資料,之後對三處資料做一個比對驗證,以使資料達到一致。

另外這裡也可以調整為:zookeeper 不儲存任務資料,只在任務資料有更新的時候,傳送給所有伺服器任務有更新的通知即可,排程伺服器接受到通知之後,直接查詢 DB 資料即可,資料只儲存在 DB 與各個排程伺服器。

實踐總結

任務排程系統 1.0 版本解決了公司的任務管理混亂的問題,提供了一個統一的任務管理平臺。2.0 版本解決了 1.0 版本存在的單點問題,任務的配置也相對更簡單,但是有一點過度依賴 zookeeper,編碼的時候應用層與會話層也沒有做好解耦,總的來說還是有很多可以優化的地方。

作者簡介

盧雲,銅板街資金端後臺開發工程師,2015年12月加入團隊,目前主要負責資金團隊後端的專案開發。

                                分散式任務排程

                      更多精彩內容,請掃碼關注 “銅板街技術” 微信公眾號。 


相關文章