OpenRaft 在交易撮合引擎中的應用

前言

由於工作需要,一直對原子多播應用有非常濃厚的興趣。通過一段時間的技術選型。我們非常幸運的得到了 Openraft[1] 實操分享 Databend[2] 社區的熱心支持。我也想通過我們的實際工作,對 Openraft 的未來應用盡一些微薄之力。

Openraft 是一個 Raft 的改進版(包括優化選舉衝突 [3], 解決網絡抖動對 leadership 的影響), 它在 Databend[4] 中爲 db, table 等元數據提供分佈式存儲和強一致讀寫, 爲 databend 的雲端數據庫集羣提供事務性保證.

我的實踐的上一篇文章反應了我們的選型過程,有興趣的人可以看一下。Raft in Rust (原子多播 + 撮合引擎)[5] 這篇文章更多的是想說明我們在使用 OpenRaft 的實際問題,並且通過我們的實現,揭祕 OpenRaft 的一些機制。

代碼倉庫

大家在使用 OpenRaft 的時候,我相信很多人都查看了手冊:Getting Started - openraftThe openraft user guide.[6]

當然,這是一個非常優秀的手冊。我們從這個手冊裏,會學習到如何使用 OpenRaft 實現自己的應用。而且,openraft/example-raft-kv[7] 這個例子確實能夠很好的說明如何實現一個簡單的應用。但是,這個例子是使用的內存來做持久化實現。當然內存不會真正做持久化,所以很容易在節點退出後,丟失狀態。而我們真正需要的示例是一個能夠持久化的例子。

另外一個實例就是 databend/metasrv[8] 而這個示例裏面,我們可以看到一個完整的 metadata 存儲服務的實現。實際上,metasrv 本身是一個類似於 etcd 的 kv 存儲。存儲整個 databend 集羣的 metadata。這個示例裏面,metasrv 使用了 sled 作爲底層存儲。sled 既存儲 log,也存儲 statemachine 的狀態。這個例子,statemachine 所有的更新都直接在 sled 存儲裏通過 sled 的事物完成了。所以,對於如何存儲 snapshot 這個問題,我們並不太容易看清楚。所以 snapshot 的產生和傳遞主要是在節點間同步的時候使用。

這裏,大家可以看到我們開放的源代碼。雖然這個示例是基於 example-raft-kv 示例,沒有達到 metasrv 的生產強度。但是我們還是非常全面的表現出了 openraft 對 log, snapshot 處理的行爲和能力。

GitHub - raymondshe/matchengine-raft[9]

應用場景

和 metasrv 的場景不同。我們需要我們的 statemachine 儘量在內存裏面更新,儘量少落盤。雖然 sled 本地落盤的速度也很快,但是內存操作的速度會更快。所以,我們基本上就是這樣進行操作的。

總體設計圖 [10]

所以在這個圖裏面,大家可以看到日誌是通過 sled 進行存儲的。而這些日誌由於通過 Raft 協議,實際上他們在每臺機器上的順序是一致的。所以,不同的 matchengine-raft 實例,在相同的日誌流情況下,對狀態機的操作就是一致的。所以,不管我們從哪一個日誌開始寫 snapshot,通過加載 snapshot 並且回放後續的日誌,我們都可以恢復到最新狀態。

按照設計圖中顯示,當前 StateMachine 的狀態是處理了第 9 個日誌裏的消息。這時候,系統保存了所有的消息到 sled。並且在第 3 個消息的時候落盤了一次 snapshot,並且在低 6 個消息的時候落盤了一次 snapshot。如果這臺機器當機,我們是可以從編號爲 3 的 snapshot 恢復狀態機,並且繼續處理 3,4,5,6,7,8,9 這 6 條消息來恢復當前狀態。當然,我們也可以從編號爲 6 的 snapshot 恢復狀態機,並且繼續處理 7,8,9 這 3 條消息來恢復當前狀態。

當然我們可以選擇多少個消息進行一次落盤。當然落盤的次數越多越可靠,但是性能影響比較大。好在 snapshot 的生成和落盤是異步的方式做的。

有興趣的朋友可以看一下 akka 的 EventSroucing[11]  模式。這種模式和 Raft 單節點非常相像。不同的是 OpenRaft 強調多實例一致性,而 Akka 則提供了非常多的方式來存儲 Log(Journal) 和 Snapshot。

實現細節

談到實現細節。我們還是回到官方文檔 geting-started[12] 來。我們也按照這個文檔的順序進行說明。

Raft 對於從應用開發着的角度,我們可以簡化到下面的這張圖裏。Raft 的分佈式共識就是要保證驅動狀態機的指令能夠在 Log 裏被一致的複製到各個節點裏。

Raft 有兩個重要的組成部分:

基於 OpenRaft 實現我們自己的 Raft 應用其實並不複雜,只需要以下三部分:

好,那我們就開始吧:

1. 定義好客戶端的請求和應答

請求有可能是客戶端發出的驅動 Raft 狀態機的數據,而應答則是 Raft 狀態機會打給客戶端的數據。

請求和應答需要實現 AppData 和 AppDataResponse 這裏,我們的實現如下:

#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum ExampleRequest {
    Set { key: String, value: String },
    Place { order: Order },
    Cancel { order: Order}
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ExampleResponse {
    pub value: Option<String>,
}

這兩個類型完全是應用相關的,而且和 RaftStrage 的狀態機實現相關。

  1. 這裏,Set 是 example-raft-kv 原示例就有的命令。

  2. 大家也注意到了,命令都是對狀態機有驅動的命令。也就是會對狀態機狀態造成改變的命令。如果我們需要取狀態機內部數據的值返回給客戶端。我們大可不必定義到這裏。

2. 實現 RaftStorage

這是整個項目非常關鍵的一部分。

只要實現好 trait RaftStorage,我們就把數據存儲和消費的行爲定義好。RaftStoreage 可以包裝一個像 RocksDB[13],Sled[14] 的本地 KV 存儲或者遠端的 SQL DB。

RaftStorage 定義了下面一些 API

fn save_vote(vote:&Vote)
fn read_vote() -> Result<Option<Vote>>
fn get_log_state() -> Result<LogState> fn try_get_log_entries(range) -> Result<Vec<Entry>>
fn append_to_log(entries)
fn delete_conflict_logs_since(since:LogId)
fn purge_logs_upto(upto:LogId)
fn last_applied_state() -> Result<(Option<LogId>,Option<EffectiveMembership>)>
fn apply_to_state_machine(entries) -> Result<Vec<AppResponse>>
fn build_snapshot() -> Result<Snapshot> fn get_current_snapshot() -> Result<Option<Snapshot>>
fn begin_receiving_snapshot() -> Result<Box<SnapshotData>>
fn install_snapshot(meta, snapshot)

在 ExampleStore[15], 這些內存化存儲行爲是非常明確簡單的。而我們不是要真正落盤了嗎?那我們就看一下 matchengine-rust 是怎麼實現的。

這裏是 matchengine-raft/src/store[16]

fi6DH8

我們說明一些設計要點

ExampleStore 的數據:

ExchangeStore 裏面主要是包含下面的成員變量。

#[derive(Debug)]
pub struct ExampleStore {

    last_purged_log_id: RwLock<Option<LogId<ExampleNodeId>>>,

    /// The Raft log.

    pub log: sled::Tree,
    
    /// The Raft state machine.
    pub state_machine: RwLock<ExampleStateMachine>,

    /// The current granted vote.
    vote: sled::Tree,

    snapshot_idx: Arc<Mutex<u64>>,

    current_snapshot: RwLock<Option<ExampleSnapshot>>,

    config : Config,

    pub node_id: ExampleNodeId,

}

幫助我們落盤的成員主要是 log, vote。而需要產生 snapshot 進行落盤的所有內容都在 state_machine.

  1. last_purged_log_id:這是最後刪除的日誌 ID。刪除日誌本身可以節約存儲,但是,對我們來講,我了保證數據存儲的安全。在刪除日誌之前,我們必須有這條日誌 index 大的 snapshot 產生。否則,我們就沒有辦法通過 snapshot 來恢復數據。

  2. log:這是一個 sled::Tree,也就是一個 map。如果看着部分代碼的話,我們就可以清楚的明白 log 對象的結構。key 是一個 log_id_index 的 Big Endian 的字節片段。value 是通過 serd_json 進行序列化的內容

    #[tracing::instrument(level = "trace", skip(self, entries))]
    async fn append_to_log(
        &mut self,
        entries: &[&Entry<ExampleTypeConfig>],
    ) -> Result<(), StorageError<ExampleNodeId>> {

        let log = &self.log;
        for entry in entries {
            log.insert(entry.log_id.index.to_be_bytes(), IVec::from(serde_json::to_vec(&*entry).unwrap())).unwrap();
        }
        Ok(())
    }
  1. state_machine:這裏就是通過日誌驅動的所有狀態的集合
#[derive(Serialize, Deserialize, Debug, Default, Clone)]pub struct ExampleStateMachine {

    pub last_applied_log: Option<LogId<ExampleNodeId>>,

    // TODO: it should not be Option.
    pub last_membership: EffectiveMembership<ExampleNodeId>,

    /// Application data.
    pub data: BTreeMap<String, String>,

    // ** Orderbook
    pub orderbook: OrderBook,

}

StateMachine 裏面最重要的數據就是 orderbook 這部分就是撮合引擎裏面重要的訂單表。存放買方和賣方的未成交訂單信息。這是主要的業務邏輯。data 這部分是原來例子中的 kv 存儲。我們還在這裏沒有刪除。

這裏 last_applied_loglast_menbership 這些狀態和業務邏輯沒有太大關係。所以,如果您要實現自己的 StateMachine。還是儘量和例子保持一致。主要是因爲這兩個狀態是通過 apply_to_state_machine() 這個接口更新。也正好需要持久化。如果需要進一步隱藏 Raft 的細節,我們還是建議 openraft 能將這兩個狀態進一步進行隱藏封裝。

對 state_machine 的落盤操作主要集中在這裏:store/store.rs[17]。有興趣的可以看一下。這裏面比較有意思的問題是 orderbook 本身無法被默認的serde_json 序列化 / 反序列化。所以我們纔在 matchengine/mod.rs[18] 加了這段代碼:

pub mod vectorize {
    use serde::{Deserialize, Deserializer, Serialize, Serializer};
    use std::iter::FromIterator;

    pub fn serialize<'a, T, K, V, S>(target: T, ser: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
        T: IntoIterator<Item = (&'a K, &'a V)>,
        K: Serialize + 'a,
        V: Serialize + 'a,
    {
        let container: Vec<_> = target.into_iter().collect();
        serde::Serialize::serialize(&container, ser)
    }

    pub fn deserialize<'de, T, K, V, D>(des: D) -> Result<T, D::Error>
    where
        D: Deserializer<'de>,
        T: FromIterator<(K, V)>,
        K: Deserialize<'de>,
        V: Deserialize<'de>,
    {
        let container: Vec<_> = serde::Deserialize::deserialize(des)?;
        Ok(T::from_iter(container.into_iter()))
    }
}

/// main OrderBook structure
#[derive(Clone, Default, Serialize, Deserialize, Debug)]
pub struct OrderBook {
    #[serde(with = "vectorize")]
    pub bids: BTreeMap<BidKey, Order>,
    #[serde(with = "vectorize")]
    pub asks: BTreeMap<AskKey, Order>,
    pub sequance: u64,
}
  1. vote:就是對最後一次 vote 的存儲。具體請看, 這段代碼倒不是因爲這段代碼有多重要,只是由於代碼比較簡單,看可以少寫一些說明
#[tracing::instrument(level = "trace", skip(self))]
    async fn save_vote(&mut self, vote: &Vote<ExampleNodeId>) -> Result<(), StorageError<ExampleNodeId>> {
        self.vote.insert(b"vote", IVec::from(serde_json::to_vec(vote).unwrap())).unwrap();
        Ok(())
    }

    async fn read_vote(&mut self) -> Result<Option<Vote<ExampleNodeId>>, StorageError<ExampleNodeId>> {
        let value = self.vote.get(b"vote").unwrap();
        match value {
            None ={Ok(None)},
            Some(val) =>  {Ok(Some(serde_json::from_slice::<Vote<ExampleNodeId>>(&*val).unwrap()))}
        }
    }

但是這兒確實有個小坑,之前我沒有注意到 vote 需要持久化,開始調試的時候產生了很多問題。直到找到 Openraft 作者 Zhang Yanpo[19] 才解決。也是觸發我想開源這個 openraft 文件持久化實現的誘因吧。感謝 Zhang Yanpo,好樣的。

  1. 其他的成員變量其實沒什麼太好說的了。和原例子一樣。

對日誌和快照的控制:

日誌,快照相互配合,我們可以很好的持久化狀態,並且恢復最新狀態。多久寫一次快照,保存多少日誌。在這裏我們使用了下面的代碼。

let mut config = Config::default().validate().unwrap();
    config.snapshot_policy = SnapshotPolicy::LogsSinceLast(500);
    config.max_applied_log_to_keep = 20000;
    config.install_snapshot_timeout = 400;

強烈建議大家看一下 Config in openraft::config - Rust[20]

重點看 snapshot_policy, 代碼裏可以清楚的標識,我們需要 500 次 log 寫一次快照。也就是 openraft 會調用 build_snapshot() 函數創建 snapshot。原示例裏,snapshot 只是在內存裏保存在 current_snapshot 變量裏。而我們需要真實的落盤。請注意這段代碼的 self.write_snapshot()

#[async_trait]
impl RaftSnapshotBuilder<ExampleTypeConfig, Cursor<Vec<u8>>> for Arc<ExampleStore> {
    #[tracing::instrument(level = "trace", skip(self))]
    async fn build_snapshot(
        &mut self,
    ) -> Result<Snapshot<ExampleTypeConfig, Cursor<Vec<u8>>>, StorageError<ExampleNodeId>> {
        let (data, last_applied_log);

        {
            // Serialize the data of the state machine.
            let state_machine = self.state_machine.read().await;
            data = serde_json::to_vec(&*state_machine)
                .map_err(|e| StorageIOError::new(ErrorSubject::StateMachine, ErrorVerb::Read, AnyError::new(&e)))?;

            last_applied_log = state_machine.last_applied_log;
        }

        let last_applied_log = match last_applied_log {
            None ={
                panic!("can not compact empty state machine");
            }
            Some(x) => x,
        };

        let snapshot_idx = {
            let mut l = self.snapshot_idx.lock().unwrap();
            *l += 1;
            *l
        };

        let snapshot_id = format!(
            "{}-{}-{}",
            last_applied_log.leader_id, last_applied_log.index, snapshot_idx
        );

        let meta = SnapshotMeta {
            last_log_id: last_applied_log,
            snapshot_id,
        };

        let snapshot = ExampleSnapshot {
            meta: meta.clone(),
            data: data.clone(),
        };

        {
            let mut current_snapshot = self.current_snapshot.write().await;
            *current_snapshot = Some(snapshot);
        }

        self.write_snapshot().await.unwrap();

        Ok(Snapshot {
            meta,
            snapshot: Box::new(Cursor::new(data)),
        })
    }

這下我們有了 snapshot,當然 snapshot 一方面可以用來在節點之間同步狀態。另一方面就是在啓動的時候恢復狀態。而 openraft 的實現非常好。實際上恢復狀態只需要回覆到最新的 snapshot 就行。只要本地日誌完備,openraft 會幫助你調用 apply_to_statemachine() 來恢復到最新狀態。所以我們就有了 restore() 函數。

#[async_trait]
impl Restore for Arc<ExampleStore> {
    #[tracing::instrument(level = "trace", skip(self))]
    async fn restore(&mut self) {
        tracing::debug!("restore");
        let log = &self.log;

        let first = log.iter().rev()
        .next()
        .map(|res| res.unwrap()).map(|(_, val)|
            serde_json::from_slice::<Entry<ExampleTypeConfig>>(&*val).unwrap().log_id);

        match first {
            Some(x) ={
                tracing::debug!("restore: first log id = {:?}", x);
                let mut ld = self.last_purged_log_id.write().await;
                *ld = Some(x);
            },
            None ={}
        }

        let snapshot = self.get_current_snapshot().await.unwrap();

        match snapshot {
            Some (ss) ={self.install_snapshot(&ss.meta, ss.snapshot).await.unwrap();},
            None ={}
        }
    }
}

大家注意一下 snapshot 的操作。當然,在這裏,我們也恢復了 last_purged_log_id。

當然 store 這個函數會在 ExampleStore 剛剛構建的時候調用。

   // Create a instance of where the Raft data will be stored.
   let es = ExampleStore::open_create(node_id);

   //es.load_latest_snapshot().await.unwrap();

   let mut store = Arc::new(es);

   store.restore().await;

如何確定 RaftStorage 是對的:

請查閱 Test suite for RaftStorage[21]如果通過這個測試,一般來講, OpenRaft 就可以使用他了。

#[test]
pub fn test_mem_store() -> anyhow::Result<(){ openraft::testing::Suite::test_all(MemStore::new) }

RaftStorage 的競爭狀態:

在我們的設計裏,在一個時刻,最多有一個線程會寫狀態,但是,會有多個線程來進行讀取。比方說,可能有多個複製任務在同時度日誌和存儲。

實現必須保證數據持久性

調用者會假設所有的寫操作都被持久化了。而且 Raft 的糾錯機制也是依賴於可靠的存儲。

實現必須保證數據持久性:

調用者會假設所有的寫操作都被持久化了。而且 Raft 的糾錯機制也是依賴於可靠的存儲。

實現 RaftNetwork

爲了節點之間對日誌能夠有共識,我們需要能夠讓節點之間進行通訊。trait RaftNetwork 就定義了數據傳輸的需求。RaftNetwork 的實現可以是考慮調用遠端的 Raft 節點的服務

pub trait RaftNetwork<D>: Send + Sync + 'static where D: AppData {
    async fn send_append_entries(&self, target: NodeId, node:Option<Node>, rpc: AppendEntriesRequest<D>) -> Result<AppendEntriesResponse>;
    async fn send_install_snapshot( &self, target: NodeId, node:Option<Node>, rpc: InstallSnapshotRequest,) -> Result<InstallSnapshotResponse>;
    async fn send_vote(&self, target: NodeId, node:Option<Node>, rpc: VoteRequest) -> Result<VoteResponse>;
}

ExampleNetwork[22]  顯示瞭如何調用傳輸消息。每一個 Raft 節點都應該提供有這樣一個 RPC 服務。當節點收到 raft rpc,服務會把請求傳遞給 raft 實例,並且通過 raft-server-endpoint[23] 返回應答。

在實際情況下可能使用  Tonic gRPC[24] 是一個更好的選擇。 databend-meta[25] 裏有一個非常好的參考實現。

在我們的 matchengen-raft 實現裏,我們解決了原示例中大量重連的問題。

  1. 維護一個可服用量的 client

這段代碼在:network/raft_network_impl.rs

     let clients = Arc::get_mut(&mut self.clients).unwrap();
     let client = clients.entry(url.clone()).or_insert(reqwest::Client::new());
  1. 在服務器端引入 keep_alive

這段代碼在:lib.rs[26]

// Start the actix-web server.
let server = HttpServer::new(move || {
    App::new()
        .wrap(Logger::default())
        .wrap(Logger::new("%a %{User-Agent}i"))
        .wrap(middleware::Compress::default())
        .app_data(app.clone())
        // raft internal RPC
        .service(raft::append)
        .service(raft::snapshot)
        .service(raft::vote)
        // admin API
        .service(management::init)
        .service(management::add_learner)
        .service(management::change_membership)
        .service(management::metrics)
        // application API
        .service(api::write)
        .service(api::read)
        .service(api::consistent_read)
}).keep_alive(Duration::from_secs(5));

這樣的改動確實是對性能有一些提升。但是真的需要更快的話,我們使用 grpc,甚至使用 reliable multicast,比方說 pgm。

啓動集羣

由於我們保留了之前的 key/value 實現。所以之前的腳本應該還是能夠工作的。而且之前的 key/value 有了真正的存儲。

爲了能夠運行集羣:

test_cluster.rs[29] 這個 rust 程序顯示了怎麼使用 ExampleClient 操作集羣,發送請求和接受應答。

這裏我們要強調的是,在初始化 raft 集羣的時候。我們需要上述的過程。如果集羣已經被初始化,並且我們已經持久化了相應的狀態 (menbership, vote, log) 以後,再某些節點退出並且重新加入,我們就不需要再過多幹預了。

在使用 metasrv 啓動 meta service 的時候,我也遇到了相同的情況。所以還是要先啓動一個 single node 以保證這個節點作爲種子節點被合理初始化了。

Deploy a Databend Meta Service Cluster | Databend[30]

爲了更好的啓動管理集羣,我們在項目裏添加了 test.sh[31]。用法如下:

./test.sh <command> <node_id>

我們可以在不同階段調用不同的命令。大家有興趣的話可以看一下代碼。這部分是主程序部分,包含了我們實現的所有命令。

echo "Run command $1"
case $1 in
"place-order")
    place_order $2
    ;;
"metrics")
    get_metrics $2
    ;;

"kill-node")
    kill_node $2
    ;;
"kill")
    kill
    ;;
"start-node")
    start_node $2
    ;;
"get-seq")
    rpc 2100$2/read  '"orderbook_sequance"'
    ;;
"build-cluster")
    build_cluster $2
    ;;
"change-membership")
 change_membership $2
    ;;

"clean")
    clean
    ;;
  *)
    "Nothing is done!"
    ;;
esac

未來的工作

當前我們實現的 matchengine-raft 只是爲了示例怎麼通過 raft 應用到撮合引擎這樣一個對性能,穩定性,高可用要求都非常苛刻的應用場景。通過 raft 集羣來完成撮合引擎的分佈式管理。我們相信真正把這個玩具撮合引擎推向產品環境,我們還是需要進行很多工作:

  1. 優化序列化方案,serd_json 固然好,但是通過字符串進行編解碼還是差點兒意思。至少用到 bson 或者更好的用 protobuf, avro 等,提高編解碼速度,傳輸和存儲的開銷。

  2. 優化 RaftNetwork, 在可以使用 multi-cast 的情況下使用 pgm,如果不行,可以使用 grpc。

  3. 撮合結果的分發。這部分在很多情況下依賴消息隊列中間件比較好。

  4. 增加更多的撮合算法。這部分完全是業務需求,和 openraft 無關。我們就不在這個文章裏討論了。

  5. 完善測試和客戶端的調用。

  6. 完善壓測程序,準備進一步調優。

結論

通過這個簡單的小項目,我們:

  1. 實現了一個簡單的玩具撮合引擎。

  2. 驗證了 OpenRaft 在功能上對撮合引擎場景的支持能力。

  3. 給 OpenRaft 提供了一個基於 sled KV 存儲的日誌存儲的參考實現。

  4. 給 OpenRaft 提供了一個基於本地文件的快照存儲的參考實現。

給大家透露一個小祕密,SAP 也在使用 OpenRaft[32] 來構建關鍵應用。大家想想,都用到 Raft 協議了,一定是非常重要的應用。

對於 Databend 社區的幫助,我表示由衷的感謝。 作爲一個長期工作在軟件行業一線的老程序猿,看到中國開源軟件開始在基礎構建發力,由衷的感到欣慰。也希望中國開源社羣越來越好,越來越強大,走向軟件行業的頂端。

作者信息:沈勇 Decisive Density  CTO

引用鏈接

[1] Openraft: https://github.com/datafuselabs/openraft
[2] Databend: https://github.com/datafuselabs/databend
[3] 優化選舉衝突: https://datafuselabs.github.io/openraft/vote.html
[4] Databend: https://github.com/datafuselabs/databend
[5] Raft in Rust (原子多播 + 撮合引擎): http://t.csdn.cn/jcOnv
[6] Getting Started - openraftThe openraft user guide.: https://datafuselabs.github.io/openraft/getting-started.html
[7] openraft/example-raft-kv: https://github.com/datafuselabs/openraft/tree/main/examples/raft-kv-memstore
[8] databend/metasrv: https://github.com/datafuselabs/databend/tree/main/metasrv
[9] GitHub - raymondshe/matchengine-raft: https://github.com/raymondshe/matchengine-raft
[10] 總體設計圖: https://excalidraw.com/#json=kSzwFGNGr_WNjytPO65RN,CjvsM4m_3efHnSIGK37Sow
[11] EventSroucing: https://doc.akka.io/docs/akka/current/typed/index-persistence.html
[12] geting-started: https://datafuselabs.github.io/openraft/getting-started.html
[13] RocksDB: https://docs.rs/rocksdb/latest/rocksdb/
[14] Sled: https://github.com/spacejam/sled
[15] ExampleStore: https://github.com/datafuselabs/openraft/blob/main/examples/raft-kv-memstore/src/store/mod.rs
[16] matchengine-raft/src/store: https://github.com/raymondshe/matchengine-raft/tree/master/src/store
[17] store/store.rs: https://github.com/raymondshe/matchengine-raft/blob/master/src/store/store.rs
[18] matchengine/mod.rs: https://github.com/raymondshe/matchengine-raft/blob/master/src/matchengine/mod.rs
[19] Zhang Yanpo: https://github.com/drmingdrmer
[20] Config in openraft::config - Rust: https://docs.rs/openraft/latest/openraft/config/struct.Config.html
[21] Test suite for RaftStorage: https://github.com/datafuselabs/openraft/blob/main/memstore/src/test.rs
[22] ExampleNetwork: https://github.com/datafuselabs/openraft/blob/main/examples/raft-kv-memstore/src/network/raft_network_impl.rs
[23] raft-server-endpoint: https://github.com/datafuselabs/openraft/blob/main/example-raft-kv/src/network/raft.rs
[24] Tonic gRPC: https://github.com/hyperium/tonic
[25] databend-meta: #L89
[26] lib.rs: https://github.com/raymondshe/matchengine-raft/blob/master/src/lib.rs
[27] example-raft-kv: https://github.com/datafuselabs/openraft/tree/main/examples/raft-kv-memstore
[28] test-cluster.sh: https://github.com/datafuselabs/openraft/blob/main/examples/raft-kv-memstore/test-cluster.sh
[29] test_cluster.rs: https://github.com/datafuselabs/openraft/blob/main/examples/raft-kv-memstore/tests/cluster/test_cluster.rs
[30] Deploy a Databend Meta Service Cluster | Databend: https://databend.rs/doc/manage/metasrv/metasrv-deploy
[31] test.sh: https://github.com/raymondshe/matchengine-raft/blob/master/test.sh
[32] OpenRaft: https://github.com/datafuselabs/openraft

關於 Databend

Databend 是一款開源、彈性、低成本,基於對象存儲也可以做實時分析的新式數倉。期待您的關注,一起探索雲原生數倉解決方案,打造新一代開源 Data Cloud。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/DKh-cqOq6dcKNdBxpn4Vjw