PowerJob 的自實現高可用方案,妙妙妙!

削微寒發表於2020-08-12

本文適合有 Java 基礎知識的人群

作者:HelloGitHub-Salieri

HelloGitHub 推出的《講解開源專案》系列。

碎碎念

高可用放到今天已經不是一個新穎的詞彙了,怎麼實現高可用大家也已經瞭然於心。多例項部署 + 服務註冊 + 服務發現這一套組合拳打下來,實現高可用那還不是分分鐘的事情。所以很多人看到 PowerJob 的介紹頁面中寫了任意元件支援叢集部署以實現高可用,想當然的以為也是走了上述的那套流程。然後看到系統依賴元件時,發現......emmm...... Zookeeper 呢?沒看著。那找找 Nacos ?emmm......也沒找著......不僅沒找著,還發現文件中明明白白的寫著,最小依賴僅為關係型資料庫。許多使用者看到這裡就有點百思不得其解了,正常來講都會有兩個疑惑。

首先,為什麼不用註冊中心呢?

要做到分散式環境下的高可用,肯定是需要服務註冊、服務發現這樣的概念的。沒有外部註冊中心,說白了就是自己去實現了一套類似的機制。那為什麼要怎麼做呢?

其實答案很簡單——成本。這個成本指的是使用者的接入成本。對於一個需要部署的重型開源專案來說,每少一個外部依賴,就多一份潛在的使用者。額外的系統依賴代表著額外的技術棧和額外的維護成本,如果企業本身沒有這一套技術體系(比如沒用到 zookeeper),而 PowerJob 又強依賴 zookeeper,那大概率只能說再見嘍~

第一個問題解決了,接下來進入第二個問題~

簡單高“可用”

PowerJob 系統中的基礎元件為排程伺服器 server 和執行器 worker,server 負責排程定時任務,並派發到 worker 執行,是一個典型的 C/S 架構。

C/S 架構下,如果目標是 server 和 client 可以相互聯通的“高可用”,那麼實現起來其實非常容易。

首先,啟動多個 server 應用例項,叢集部署。然後將多個 server 的 IP 地址統統填入 worker 的配置檔案中,worker 啟動時,隨機找一個 IP 進行連線,失敗則重試。一旦成功連線到某一臺 server,就開始上報自己的地址資訊。server 通過持有這個資訊也可以和 worker 進行通訊。如此一來,一個最簡單版本的“高可用”叢集就搭建完成了。但是......它真的可用嗎?

答案顯然是否定的(否則也不會有這篇文章了是不是~)。以上方案主要存在兩個問題:

  1. 任務排程需要保證唯一性,即某個任務在某一個時刻只能被一臺機器排程,否則就會導致重複執行。而前文提及的方案中,每一臺 server 都是完全等價的,因此只能依靠分散式鎖來保證唯一性,即搶到鎖的 server 執行排程,其他 server 只能充當戰地記者,默默地邊緣 OB。這種方案下,無論部署多少臺 server,系統整體的排程效能其實是固定的,多例項部署只能做到高可用,而不能做到高效能。
  2. server 無法持有完整的 worker 叢集資訊。PowerJob 的定位是任務排程中介軟體,旨在為企業下各部門各業務線提供精準的排程和分散式計算能力。因此肯定會有叢集分組的概念,就像 RocketMQ 中存在 ProducerGroup 和 ConsumerGroup 一樣,PowerJob 有著 AppName 的概念。一個 AppName 邏輯上對應了某個應用下的一組任務,物理上對應了這個應用所部署的叢集。為了便於 server 統一管理以及一些額外功能的實現(分散式計算),server 持有某一個 AppName 下完整的叢集資訊是一個強訴求,而前文提及的“瞎貓撞上死耗子”式方案,顯然沒辦法做到這一點。

基於以上兩點,征途是星辰大海的 PowerJob 需要探索出一種更合理、更強大的高可用架構。

分組隔離

其實根據前面遇到的問題,這一套機制的雛形也差不多出來了。

server 既然需要持有某一個分組下完整的叢集資訊,那麼可以順其自然的想到,能不能讓某一個分組的所有 worker 都連線到某一臺 server 呢?一旦某個分組下所有機器全部連線到了某一臺 server,那麼其實這就形成了一個小型的子系統。雖然整個 PowerJob 系統中存在著多臺 server 和多個 worker 叢集,但是對於這個分組的執行來說,只要有這個分組對應的 worker 叢集以及它們連線的那一臺 server 就夠了。那麼在這個小型“子系統”內部,只存在著一臺 server,也就不存在重複排程問題了(server 只排程連線到它的 AppName 下面的任務就能實現這一點)。

所以,經過一層層的剝絲抽繭,問題已經轉化為了:如何讓某個分組下的所有機器都連線到同一臺 server 上去呢?

看到這個問題的時候,相信很多人會有和我當時一樣的想法,那就是:就這?

“讓所有機器都連線到同一臺 server 上去,那也太簡單了吧,你只配置一個 IP 不就行了嗎?”

“配置一個 IP 怎麼做高可用,怎麼利用多臺 server 資源?”

“?好像有點道理,那就 hash(appName) 取餘作為下標,這樣就能保證同一個同一個分組下所有機器的初始 IP 相同,不同分組也能連線到不同的 server”

“那,萬一連線的 server 掛了怎麼辦?”

“這好辦,可以採取類似於解決雜湊衝突的那個什麼開放定址法,從掛掉的 server 下標開始,依次向後重試就行了,同一個分組叢集內所有的機器都從某個下標依次向後重試,還是能連線到同一臺 server 的”

“?好像很有道理,哼,worker 選主也就不過如此,方案搞定,英雄聯盟 啟動!”

正當我浴血奮戰直指敵將首級時,畫面...永遠定格在了見血前的那一瞬。“正在嘗試重新連線”幾個大字映入眼簾,也把我帶入了深深的沉思。

雖說每次玩遊戲必罵騰訊那***的土豆伺服器,但罵歸罵,心裡其實還是明白,大部分情況下都是自己網路波動導致遊戲掉線(誰叫我貪便宜辦了個移動寬頻呢,哎,雷電法王楊永信也就圖一樂,真要戒網癮還得看移動寬頻)。

嗯?自己的原因?網路波動?掉線?重連?這一連串詞彙,把我拉回了剛剛設計的方案之中,然後給我當頭一棒。一旦 worker 因為自己的網路波動導致它以為 server 不可用,而重新連線另一臺 server,就會導致所有 worker 都連線同一臺 server 這個約束被破壞......因此,這個方案自然也就是一個充滿漏洞的不可行方案。

在這之後的一週,可以說是支離破碎的一週。為了解決這個問題,我設計了無數個堪稱“奇珍異獸”的方案,然後再一個個否定和槍斃。

其實這段經歷現在回過頭來想特別搞笑,也有被自己蠢到。那無數個方案的失敗原因其實都是同一個,也就是出發點錯了。我一直在嘗試讓 worker 決定連線哪臺 server,卻一而再再而三忽略 worker 永遠不可能獲取 server 真正的存活資訊(比如心跳無法傳達,可能是 worker 本身的網路故障),因此 worker 不應該決定連線哪臺 server,這應該由 server 來決定。worker 能做的,只有服務發現。想明白了這點,具體的方案也就應運而生了。

PS:這個方案的誕生,我大概付出了1斤腦細胞的代價(不得不說這個減肥方法還蠻好的)...腦細胞不能白死,儘管那些奇奇怪怪得方案沒有活到正式版本,但沒有他們就無法通往真理的大門。為了表達紀念和“哀悼”之情,我將最終的設計命名為——V4:喪鐘為誰鳴。

V4:喪鐘為誰鳴

想明白了不能由 Worker 發起 Server 的重新選舉,這個問題就基本上解決了......由於篇幅原因以及網上已經有小夥伴寫了這一塊原始碼分析的部落格,我這裡就不重複“造輪子”了,在這裡主要講一下設計思路。

就像前面說的那樣,worker 因為沒辦法獲取 server 的準確狀態,所以不能由 worker 來決定連線哪一臺 server。因此,worker 需要做的,只是服務發現。即定時使用 HTTP 請求任意一臺 server,請求獲取當前該分組(appName)對應的 server。

而 server 收到來自 worker 的服務發現請求後,其實就是進行了一場小型的分散式選主:server 依賴的資料庫中存在著 server_info 表,其中記錄了每一個分組(appName)所對應的 server 資訊。如果該 server 發現表中存在記錄,那就說明該 worker 叢集中已經有別的 worker 事先請求 server 進行選舉,那麼此時只需要傳送 PING 請求檢測該 server 是否存活。如果發現該 server 存活,那麼直接返回該 server 的資訊作為該分組的 server。否則就完成篡位,將自己的資訊寫入資料庫表中,成為該分組的 server。

細心的小夥伴可能又要問了?傳送 PING 請求檢測該 server 是否存活,不還是有和剛才一樣的問題嗎?請求不同,傳送方和接收方都有可能出問題,憑什麼認為是原先的 server 掛了呢?

確實,在這個方案下,依舊沒辦法解決 server 到底掛沒掛這個堪比“真假美猴王”的玄學問題。但是,這還重要嗎?我們的目標是某個分組下所有的 worker 都連線到同一臺 server,因此,即便產生那種誤打誤撞篡位的情況,在服務發現機制的加持下,整個叢集最終還是會連線到同一臺 server,完美實現我們的需求。

至此,耗時 6 天,從原來的懷疑人生,到完美方案的落地實現,真是曲折~

最後

最後,貼上兩位小夥伴貢獻的原始碼分析文章,我親自 check 過,沒有質量問題(這話說的我感覺自己好飄哈哈哈),請各位觀眾老爺放心檢視~

那麼以上就是本篇文章全部的內容了~相信通過這篇文章和上篇文章,大家已經對 PowerJob 的排程層和高可用高效能架構有了一定的瞭解了。接下來就是下期預告環節了~

為了保留神祕感,這次就選擇不預告了(才不會告訴你是我還沒想好具體寫什麼)~

所有驚喜,下期再見~

專案地址:

https://github.com/KFCFans/PowerJob


關注公眾號加入交流群(作者在 Java 群)

相關文章