分佈式任務調度

前言算法

任務調度能夠說是全部系統都必需要依賴的一箇中間系統,主要負責觸發一些須要定時執行的任務。傳統的非分佈式系統中,只須要在應用內部內置一些定時任務框架,好比 spring 整合 quartz,就能夠完成一些定時任務工做。在分佈式系統中,這樣作的話,就會面臨任務重複執行的問題(多臺服務器都會觸發)。另外,隨着公司項目的增長,也須要一個統一的任務管理中心來解決任務配置混亂的問題。spring

公司的任務調度系統經歷了兩個版本的開發,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月加入團隊,目前主要負責資金團隊後端的項目開發。

                                


                      更多精彩內容,請掃碼關注 「銅板街科技」 微信公衆號。 

相關文章
相關標籤/搜索