bifrost : Rust 下的分散式系統框架

我前一段時間在忙一個純 Rust 的分散式框架。儘管已經存在很多類似並且成熟的框架,但是沒有一個是為了 Rust 設計的,有很多輪子要造。

bifrost 是一個用於建造健壯的分散式系統的框架。它不依賴任何第三方的軟體比如 Zookeeper,etcd 或者 consul 來保證一致性。bifrost 具備自己的基於 raft 一致性演算法的狀態機實現作為多項功能的基石,因為目前開源社區並不存在類似的庫。bifrost 包含易用的,可定製的並且快速的 RPC 框架,不需要制定任何協議文件和第三方程序生成客戶端存根和服務端 trait。bifrost 實現了簡單,同時靈活的 raft 狀態機框架,可以用於創建從簡單的諸如表數據結構,到檢測客戶端成員變化的系統。它也具備建造帶主從複製的分散式存儲引擎的潛力。

使用 raft 一致性演算法的原因是為了保證高可用性,通過把日誌同步到多數的從伺服器上再返回給客戶端結果。少數崩潰或者緩慢的伺服器並不會對數據完整性造成影響。它的讀性能理論上可以任意擴展但是寫性能會被集群中單個伺服器限制。

定義一個 raft 狀態機,需要用戶自己選擇函數類別,決定了狀態機和和客戶端具體該如何執行函數。目前 bifrost 支持 3 種不同的函數類別:命令,查詢和訂閱。對於命令函數,客戶端必須要把請求發送到主伺服器,主伺服器會把日誌同步到多數從伺服器上後執行該命令並返回結果給客戶端。對於查詢函數,客戶端會把請求發送到集群中任意伺服器上,伺服器會直接執行請求並把結果連帶最後一條提交的日誌 ID 返回給客戶端,客戶端會將收到的日誌 ID 和已經記錄的最後日誌 ID 作比較,如果收到的日誌 ID 小於記錄的最後一條,會查詢其他伺服器,否則就接受返回值。對於訂閱函數,客戶端會要求初始化一個接受訂閱消息的服務,發送訂閱請求到主伺服器,主伺服器會把訂閱請求作為配置變化的日誌同步到從伺服器,當相應的事件觸發時,只有主伺服器會給客戶端發送訂閱消息。

比如,一個非常簡單的狀態機可以這樣定義

raft_state_machine! {n def cmd set(v: $t);n def qry get() -> $t;n def sub on_changed() -> ($t, $t);n}n

用戶需要慎重考慮函數類型,因為在查詢(qry)請求中,狀態機是不可變的。如果需要狀態機可變,就必須要定義命令(cmd)函數。

impl StateMachineCmds for Value {n fn set(&mut self, v: $t) -> Result<(),()> {n if let Some(ref callback) = self.callback {n let old = self.val.clone();n subs.notify(&commands::on_changed{}, Ok((old, v.clone())));n }n self.val = v;n Ok(())n }n fn get(&self) -> Result<$t, ()> {n Ok(self.val.clone())n }n}n

raft_state_machine 宏不會為了訂閱(sub)生成特徵函數。在狀態機特徵函數的實現中,訂閱需要像上面前 4 行那樣觸發。你可以在這裡看到完整的樣例。

使用訂閱,只需要傳入接收消息的閉包和需要匹配的參數(參數可以留空)。比如一個訂閱函數可以這樣定義

def sub on_inserted() -> ($kt, $vt);n

而在客戶端使用它,可以這樣調用

sm_client.on_inserted(move |res| {n if let Ok((key, value)) = res {n println!("GOT INSERT CALLBACK {:?} -> {:?}", key, value);n assert_eq!(inserted_stash.get(&key).unwrap(), &value);n }n});n

有些時候需要對訂閱的函數做一些限制,bifrost 可以在訂閱時指定參數。這類訂閱函數可以通過給括弧中加入參數定義

def sub on_key_inserted(k: $kt) -> $vt;n

在服務端狀態機觸發消息,需要帶上參數

callback.notify(&commands::on_key_inserted{k: k.clone()}, Ok(v.clone()));n

接收這些消息,客戶端在訂閱時需要帶上要訂閱的參數,比如下面的例子是 sk1 的一份副本

sm_client.on_key_inserted(|res| {n if let Ok(value) = res {n println!("GOT K1 CALLBACK {:?}", value);n assert_eq!(&String::from("v1"), &value);n }n}, sk1.clone());n

RPC,raft 狀態機框架都支持多路復用。RPC 的客戶端和服務端可以在一個埠上運行多個網路服務,而 raft 狀態機也支持多個子狀態機。

這項特性允許用戶靈活地復用現有資源,用戶需要自行組裝伺服器和狀態機。

let addr = String::from("127.0.0.1:2100");nlet raft_service = RaftService::new(Options {n storage: Storage::Default(),n address: addr.clone(),n service_id: 0,n});nlet server = Server::new(vec!((0, raft_service.clone())));nlet heartbeat_service = Membership::new(&server, &raft_service);nServer::listen_and_resume(server.clone(), &addr);nRaftService::start(&raft_service);nraft_service.bootstrap();n

用戶需要先定義服務,比如上面的 raft_service 和 heartbeat_service,並使用一個或者多個服務初始化伺服器,用戶也可以使用 register_service 在伺服器初始化後再註冊其他服務,就像在 Membership::new 里做的那樣。如果需要,用戶可以在一台伺服器上註冊多個 raft 狀態機或者其他服務。唯一需要特別照顧的是每個服務要使用不同的服務 ID。

raft 狀態機和 RPC 採取相同的理念,用戶可以註冊多個子狀態機到 RaftService 的引用上使得,raft 狀態機具有一定的意義。在客戶端成員變化服務中,Membership::new 初始化函數會通過一下方法為用戶做這些事

raft_service.register_state_machine(Box::new(Membership {n heartbeat: heartbeat_service.clone(),n groups: HashMap::new(),n members: HashMap::new(),n}));nserver.register_service(DEFAULT_SERVICE_ID, heartbeat_service);n

我在春節完成了對多路復用的升級。它從一定程度上提高了使用複雜度,但是可以更有效地利用資源完成相同的事情。

bifrost 還附帶了一些實用工具,其中值得一提的是從 Clojure 借來的綁定功能。用戶可以定義一個全局綁定變數並賦予一個默認值。此變數可以在任意時刻重新綁定,在綁定塊內一直生效,並且可以在內部任何地方訪問。此綁定可是線程可見的,意味著同時在不同線程中綁定此變數並不會相互影響。比如

def_bindings! {n bind val IS_LEADER: bool = false;n}n

會定義一個稱為 IS_LEADER 的綁定變數並且它的默認值是 false。它可以通過以下宏重新綁定。

with_bindings!(IS_LEADER: is_leader(meta) => {n meta.state_machine.write().commit_cmd(&entry)n})n

至此在 commit_cmd 的任何地方,IS_LEADER 一直可以通過調用 get 函數訪問到新的綁定值。在 with_bindings 宏以外,或者其他沒有綁定的線程中,綁定值始終為 false。

設計模式不期望通過函數傳遞某些參數時,這個功能會很實用。在上面的例子中,只有少數的子狀態機會需要知道當前是否為 raft 主伺服器,但一些例狀態機如訂閱功能需要用到這個變數(只有主伺服器才能發送訂閱消息),為給每個狀態機函數傳遞這個參數毫無必要。綁定可以減少冗餘定義,讓代碼更加乾淨。

bifrost 支持 2 種綁定。第一種是前面見過的值綁定,適合基本類型,比如 u64, i8, bool 等等。另一種是引用綁定,bifrost 會把默認或者綁定值套在 Arc 引用計數容器中,用戶每次通過 get 獲得的是它的一個引用。引用綁定適合用於對象,比如 String 和 HashMap。

有關 bifrost 還有很多可以介紹。現在它還主處於開發階段,有很多工作需要做才能作為一個可靠的庫發表。我會在後面會通過其他文章介紹 RPC 和 raft 狀態及框架的架構。

此文章內容翻譯自本人的博客: bifrost : distributed system framework for rust


推薦閱讀:

基於分散式環境下限流系統的設計
超級乾貨:Word2Vec課堂筆記(內附教學視頻)
聊聊一致性哈希
分散式系統理論基礎 - CAP
未來已來,Google Cloud Spanner 展開 NewSQL 時代

TAG:分布式系统 | 分布式一致性 | Rust编程 |