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 應用其實並不複雜,只需要以下三部分:
-
定義好客戶端的請求和應答
-
實現好存儲 RaftStore 來持久化狀態
-
實現一個網絡層,來保證 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 的狀態機實現相關。
-
這裏,Set 是 example-raft-kv 原示例就有的命令。
-
大家也注意到了,命令都是對狀態機有驅動的命令。也就是會對狀態機狀態造成改變的命令。如果我們需要取狀態機內部數據的值返回給客戶端。我們大可不必定義到這裏。
2. 實現 RaftStorage
這是整個項目非常關鍵的一部分。
只要實現好 trait RaftStorage
,我們就把數據存儲和消費的行爲定義好。RaftStoreage
可以包裝一個像 RocksDB[13],Sled[14] 的本地 KV 存儲或者遠端的 SQL DB。
RaftStorage
定義了下面一些 API
- 讀寫 Raft 狀態,比方說 term,vote(term:任期,vote:投票結果)
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>>
- 創建和安裝快照(snapshot)
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]
我們說明一些設計要點
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.
-
last_purged_log_id
:這是最後刪除的日誌 ID。刪除日誌本身可以節約存儲,但是,對我們來講,我了保證數據存儲的安全。在刪除日誌之前,我們必須有這條日誌 index 大的 snapshot 產生。否則,我們就沒有辦法通過 snapshot 來恢復數據。 -
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(())
}
- 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_log
, last_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,
}
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,好樣的。
- 其他的成員變量其實沒什麼太好說的了。和原例子一樣。
對日誌和快照的控制:
日誌,快照相互配合,我們可以很好的持久化狀態,並且恢復最新狀態。多久寫一次快照,保存多少日誌。在這裏我們使用了下面的代碼。
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 實現裏,我們解決了原示例中大量重連的問題。
- 維護一個可服用量的 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());
- 在服務器端引入
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 有了真正的存儲。
爲了能夠運行集羣:
-
啓動三個沒有初始化的 raft 節點;
-
初始化其中一臺 raft 節點;
-
把其他的 raft 節點加入到這個集羣裏;
-
更新 raft 成員配置。 example-raft-kv[27] 的 readme 文檔裏面把這些步驟都介紹的比較清楚了。
-
下面兩個測試腳本是非常有用的:test-cluster.sh[28] 這個腳本可以簡練的掩飾如何用 curl 和 raft 集羣進行交互。在腳本里,您可以看到所有 http 請求和應答。
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 集羣來完成撮合引擎的分佈式管理。我們相信真正把這個玩具撮合引擎推向產品環境,我們還是需要進行很多工作:
-
優化序列化方案,
serd_json
固然好,但是通過字符串進行編解碼還是差點兒意思。至少用到 bson 或者更好的用 protobuf, avro 等,提高編解碼速度,傳輸和存儲的開銷。 -
優化 RaftNetwork, 在可以使用 multi-cast 的情況下使用 pgm,如果不行,可以使用 grpc。
-
撮合結果的分發。這部分在很多情況下依賴消息隊列中間件比較好。
-
增加更多的撮合算法。這部分完全是業務需求,和 openraft 無關。我們就不在這個文章裏討論了。
-
完善測試和客戶端的調用。
-
完善壓測程序,準備進一步調優。
結論
通過這個簡單的小項目,我們:
-
實現了一個簡單的玩具撮合引擎。
-
驗證了 OpenRaft 在功能上對撮合引擎場景的支持能力。
-
給 OpenRaft 提供了一個基於 sled KV 存儲的日誌存儲的參考實現。
-
給 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。
-
Databend 文檔:https://databend.rs/
-
Twitter:https://twitter.com/Datafuse_Labs
-
Slack:https://datafusecloud.slack.com/
-
Wechat:Databend
-
GitHub :https://github.com/datafuselabs/databend
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/DKh-cqOq6dcKNdBxpn4Vjw