當前位置:
首頁 > 最新 > 分散式系統中的監工:Overseer

分散式系統中的監工:Overseer

最近從無趣的工作中發現了有趣的事情,工作和業餘時間都撲了些精力上去,本待上周末最終的成果出來後再寫文章的,無奈事情太多,代碼還沒寫完,二月上旬已過,再不寫文章春節就過去了,所以這次程序君先上車,再補票。

需求

事情是這樣的:兩周前同事催促我升級我之前做的一個輪子 merlin - 見我去年的文章:停下來,歇口氣,造輪子。

在那篇文章里我提到了為什麼會有需要做這樣一個內部的 release 構建工具。自那時起,merlin 為我們內部的幾個 elixir service 的 release 保駕護航幾個月,總體表現不錯。然而,當時在需求和設計上的一些缺陷,導致這款產品有這些問題:

太依賴 github release —— 如果不生成新的 release,就無法自動構建。這對依賴 staging 進行集成測試的服務不友好。使用者需要在 pull request 里升級版本(才能生成一個 build)。因此,多個開發者間需要在 pull request 中把版本錯開,很不方便。開發頻繁的時候,我們一天的 patch version(SemVar 里第三位),五個十個地狂跳,比舊金山 TaxiCab 的計價器跳動還要可怕。

merlin 使用的兩台機器都是 t2.medium,一次 release build(還包括一次 release upgrade build)花費十來分鐘。當構建繁忙的時候,在隊列後面的請求要很久才能排到(Latency 不友好)。

所以我要在下一個版本中,將這些問題解決。初步的考慮是,當構建請求來臨時,啟動一個強大的 spot instance,處理構建任務,構建完成並上傳 S3 後,spot instance 自行了斷。構建請求可以是來自 github release message(兼容上一版本),也可以是 API —— 進而,我們可以製作 CLI 工具,讓用戶在 shell 下對任意 git commit 觸發構建。有了粗淺的想法後,我們理一理需求:

用戶(包括 github)可以通過 API 觸發構建

構建被觸發後,啟動一個 spot instance(或 ECS fargate,不過 spot instance 實在太便宜了,如 C5.large $0.03/hour,所以 ECS 沒有啥價格優勢)

spot instance 基於一個 prebuild 的 AMI(如果 ECS,則 docker)啟動,AMI 里包含處理構建的軟體。這樣啟動起來之後,就能自動處理構建任務

描述構建任務的 metadata 放置於 spot instance 啟動時的 user data 中,構建軟體通過 訪問之

構建過程中可能要發送一些 telemetry 到 merlin

構建完成,把狀態和構建的信息(比如 tarball 在哪裡)發回給 merlin,然後自盡

這個需求算是比較清晰,實現起來也沒有什麼難點,無非就是時間問題 —— 對於像我們這樣的 startup 來說,它是可以立即擼起袖子幹活,逢山開路遇水填橋的那種活兒。

merlin 之前的坑是我埋的,這個業務即不性感,也不緊急;backend 的隊友們都撲在一些 visibility 高的,光是名字聽起來就熱血沸騰的項目上,騰不出手,且我也不捨得就這麼浪費他們的時間 —— 所以我只能 eat my own dogshit。我這人白天瞎忙,晚上躲懶 —— 除非有什麼能戳到 G 點讓我不吃不喝不睡覺也要搞的創意,否則像 merlin 這種一眼就從頭看到腳,沒有太多挑戰的項目,激發不出我的小宇宙。於是需求定下,反正也不著急,我就懶懶地,有一搭沒一搭地在腦海中想著。

事實證明,這種懶散,而非全力以赴,促成了我更多,更深的思考。有功夫我把整個思考的過程撰寫成文,相信對大家也能有小小的啟發。

實現

在上面的需求中,merlin 由一個服務被拆成了兩個部分:control plane 和 data plane(請饒恕一個曾經的網路工程師對區分路徑的這種骨子裡的執著)。簡單來說,control plane 負責派活和監控,是個 scheduler,類似於老鴇;data plane 負責幹活,是一堆 resource,就好像蘇小小,柳如是,李師師們。而一個個構建任務,是要完成的 task,就是趙佶,柳永,阮郁等的不期而至。

把 merlin 的需求稍稍泛化一下:

調用者可以通過 API 觸發一個 task

Control plane 接到 task 後,分配到 data plane 上的某個 resource 上執行

data plane 向 control plane 匯總 telemetry

data plane 完成 task 之後,向 control plane 彙報結果,進入到 idle 狀態等待下次調度

為了符合社會主義核心價值觀,我們換個比喻:Control plane 類似於 erlang/OTP 里的 Supervisor;data plane 類似於 GenServer。對於 erlang 不太熟悉的同學可以看我的文章:上帝說:要有一門面向未來的語言,於是有了 erlang。你不必理解代碼,但需要理解思想。

然而,erlang/OTP 里的 Supervisor 只負責啟動和監控 process,如果要啟動和監控 node,有很多問題:

如何在 cloud 里動態啟動一個節點?

如何讓這個節點自動加入到 cluster 里?

如何讓這個節點有運行 task 所必須的軟體?

control plane 如何和 data plane 方便地通信?

如何把上面的所有細節屏蔽起來,啟動和監控一個節點,像 Supervisor 啟動和監控一個 GenServer 一樣簡單,且對程序員友好?

1/2/3 如果解決,4 可以直接通過封裝 RPC 解決。

2 我們上文中提過 —— 我們可以通過給新啟動的 instance 提供 UserData 來解決 —— 在 AWS 里,當我們啟動一個新的 instance,可以預設一些 json 數據進去,本地訪問 即可獲得,因而,我們可以把 cluster 的 cookie,control plane node 的 node name 都放進去,以便於新的節點可以自己加入 cluster。

我們看 1 和 3。最簡單解決 1/3 的方法是使用 prebuild AMI —— 把所有相關的,處理 data plane 的軟體都燒到 AMI 里,用 request-spot-instance 的 AWS API 創建節點即可。不過,這意味著每次 data plane 的代碼改變,我們都要重燒 AMI,即便燒 AMI 的動作 CI 自動化處理了,每次 control plane 還是需要確保使用正確的 AMI 啟動 data plane。有些麻煩。

程序員最不爽的就是麻煩。虛心使人進步,麻煩讓程序員創新。咋辦?我們能不能做個 loader,把一個編譯好的 module,甚至一個 release 動態載入到遠端的一個 node 上?

bingo!這是一個好問題,而好問題的價值遠勝於好的答案。於是大概兩周前的一個周末,我寫了幾百行代碼,做了一個初始版本的 ex_loader。見 github: tubitv/ex_loader。代碼已開源,MIT license。

ex_loader 讓你可以很簡單地干這樣的事情:

Joe Armstrong 曾經在一次會議上開心地談到過他自己會在 erlang node 上運行很多空的,什麼也不做,也不知道該做什麼的 process,但當他有需要的時候,讓這些 process 載入新的 module,就搖身一變讓其成為擁有某種特定功能 process。ex_loader 在此基礎上更進一步,你可以開一些空的 erlang node,有需要的時候,讓這些 node 載入你想讓其運行的 release,使其成為特定功能的 server。

ex_loader 簡化了 control plane 往 data plane 發布軟體的工作,我們有了一個更好的解決 1 和 3 的方案。然而,我們還沒有觸及到上文中所提到的 5。

這就是 Overseer,一個新的,類比 Supervisor 的 OTP behavior。我們先看怎麼用 Overseer:

定義一個 Overseer 很簡單:

我們大概講講 Overseer 幹些什麼:

start_link:啟動時,它接受一些參數,關於我們要啟動的 node 的 spec。node 目前支持兩種 adapter,local 和 ec2。我將其做成 adapter,是為了日後支持更多類型的 node(比如 ECS)。strategy 目前僅支持 simple_one_for_one,亦即所有 node 使用相同的 spec,在需要的時候由 Overseer 創建。

start_child:Overseer 可以根據預置的 spec 啟動一個 node —— 比如 ec2 spot instance。這個 node 啟動成功之後,初始的代碼會使用 UserData 裡面的 node_name 和 cookie 連接 Overseer。當 Overseer 監測到一個 node_up 的消息後,會在內部創建一個 Labor 的數據結構,並且把 spec 裡面定義的 release 發給這個 labor node 載入和運行。

pairing:比 Supervisor 複雜的是,Overseer 不但需要監聽 node up / down 的事件,做相應的決策(比如重啟一個新的 node)外,還需要接受 node 傳過來的 telemetry,所以 Overseer 所在的 process 要和 labor node 上面的某個 process 建立起關係。我把這個過程稱作為 pairing,類比藍牙設備間的配對。當 start_child 成功後,Overseer 會把自己的 pid 發送給 labor node 上的一個指定的介面,然後 labor node 會在這個介面里顯示地給 Overseer 發送 pair 請求,之後,兩個 process 就 link 起來。

作為一個類似於 Supervisor 的 GenServer,Overseer 把 labort node 監控的細節和狀態機都屏蔽掉,只暴露 connected / disconnected / telemetry 等一些上層軟體關心的事件。

下圖是大概一周前我手繪的 sequential diagram,當時名字還不叫 Overseer,叫 GenConnector,但基本思路一致:

Overseer 的源碼會在這幾天完成後釋出,敬請期待。

有了 ex_loader 和 Overseer,merlin 剩下要做的事情就簡單很多了:把代碼庫分割成 control plane 和 data plane,control plane 用 Overseer,data plane 沿用之前的代碼,稍作修改後我們就有了一個分散式的,可以隨意 scale 的構建系統。

最妙的是,ex_loader 和 Overseer 雖為 merlin 而生,卻由於不錯的抽象程度,能適用於幾乎任何 control plane + dynamic data plane 的這種分散式任務處理結構。在我之前的思考中,其實還更進一步,將這個系統設計成了一個叫 Fleet / Carrier / Fighter 結構的分散式系統,Carrier 是 Fleet 的 labor node,Fighter 是 Carrier 的 labor node,類比 Star War 中的帝國艦隊。在這個藍圖中,merlin 只是 Fleet 的一個 Carrier 而已(這個估計短期沒工夫實現):

好了,不說廢話了,我還是抓緊寫代碼去。提前祝各位叔叔阿姨哥哥姐姐弟弟妹妹,春節快樂!也祝各位同處本命年「伏吟」的小夥伴們,狗年紅紅火火,不犯太歲!:)

IOS 小夥伴打賞通道:


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序人生 的精彩文章:

相親遇到喜歡的IT男有感

TAG:程序人生 |