使用 Rust 構建分佈式 Key-Value Store

歡迎你們前往騰訊雲社區,獲取更多騰訊海量技術實踐乾貨哦~git

引子

構建一個分佈式 Key-Value Store 並非一件容易的事情,咱們須要考慮不少的問題,首先就是咱們的系統到底須要提供什麼樣的功能,譬如:github

  • 一致性:咱們是否須要保證整個系統的線性一致性,仍是能容忍短期的數據不一致,只支持最終一致性。算法

  • 穩定性:咱們可否保證系統 7 x 24 小時穩定運行。系統的可用性是 4 個 9,還有 5 個 9?若是出現了機器損壞等災難狀況,系統可否作的自動恢復。編程

  • 擴展性:當數據持續增多,可否經過添加機器就自動作到數據再次平衡,而且不影響外部服務。緩存

  • 分佈式事務:是否須要提供分佈式事務支持,事務隔離等級須要支持到什麼程度。安全

上面的問題在系統設計之初,就須要考慮好,做爲整個系統的設計目標。爲了實現這些特性,咱們就須要考慮到底採用哪種實現方案,取捨各個方面的利弊等。網絡

後面,我將以咱們開發的分佈式 Key-Value TiKV 做爲實際例子,來講明下咱們是如何取捨並實現的。併發

TiKV

TiKV 是一個分佈式 Key-Value store,它使用 Rust 開發,採用 Raft 一致性協議保證數據的強一致性,以及穩定性,同時經過 Raft 的 Configuration Change 機制實現了系統的可擴展性。框架

TiKV 提供了基本的 KV API 支持,也就是一般的 Get,Set,Delete,Scan 這樣的 API。TiKV 也提供了支持 ACID 事務的 Transaction API,咱們可使用 Begin 開啓一個事務,在事務裏面對 Key 進行操做,最後再用 Commit 提交一個事務,TiKV 支持 SI 以及 SSI 事務隔離級別,用來知足用戶的不一樣業務場景。異步

Rust

在規劃好 TiKV 的特性以後,咱們就要開始進行 TiKV 的開發。這時候,咱們面臨的第一個問題就是採用什麼樣的語言進行開發。當時,擺在咱們眼前的有幾個選擇:

  • Go,Go 是咱們團隊最擅長的一門語言,並且 Go 提供的 goroutine,channel 這些機制,天生的適合大規模分佈式系統的開發,但靈活方便的同時也有一些甜蜜的負擔,首先就是 GC,雖然如今 Go 的 GC 愈來愈完善,但總歸會有短暫的卡頓,另外 goroutine 的調度也會有切換開銷,這些均可能會形成請求的延遲增高。

  • Java,如今世面上面有太多基於 Java 作的分佈式系統了,但 Java 同樣有 GC 等開銷問題,同時咱們團隊在 Java 上面沒有任何開發經驗,因此沒有采用。

  • C++,C++ 能夠認爲是開發高性能系統的代名詞,但咱們團隊沒有特別多的同窗能熟練掌握 C++,因此開發大型 C++ 項目並非一件很是容易的事情。雖然使用現代 C++ 的編程方式能大量減小 data race,dangling pointer 等風險,咱們仍然可能犯錯。

當咱們排除了上面幾種主流語言以後,咱們發現,爲了開發 TiKV,咱們須要這門語言具備以下特性:

  • 靜態語言,這樣才能最大限度的保證運行性能。

  • 無 GC,徹底手動控制內存。

  • Memory safe,儘可能避免 dangling pointer,memory leak 等問題。

  • Thread safe,不會遇到 data race 等問題。

  • 包管理,咱們能夠很是方便的使用第三方庫。

  • 高效的 C 綁定,由於咱們還可能使用一些 C library,因此跟 C 交互不能有開銷。

綜上,咱們決定使用 Rust,Rust 是一門系統編程語言,它提供了咱們上面想要的語言特性,但選擇 Rust 對咱們來講也是頗有風險的,主要有兩點:

  1. 咱們團隊沒有任何 Rust 開發經驗,所有都須要花時間學習 Rust,而恰恰 Rust 有一個很是陡峭的學習曲線。

  2. 基礎網絡庫的缺失,雖然那個時候 Rust 已經出了 1.0,但咱們發現不少基礎庫都沒有,譬如在網絡庫上面只有 mio,沒有好用的 RPC 框架,HTTP 也不成熟。

但咱們仍是決定使用 Rust,對於第一點,咱們團隊花了將近一個月的時間來學習 Rust,跟 Rust 編譯器做鬥爭,而對於第二點,咱們就徹底開始本身寫。

幸運的,當咱們越過 Rust 那段陣痛期以後,發現用 Rust 開發 TiKV 異常的高效,這也就是爲啥咱們能在短期開發出 TiKV 並在生產環境中上線的緣由。

一致性協議

對於分佈式系統來講,CAP 是一個不得不考慮的問題,由於 P 也就是 Partition Tolerance 是必定存在的,因此咱們就要考慮究竟是選擇 C - Consistency 仍是 A - Availability。

咱們在設計 TiKV 的時候就決定 - 徹底保證數據安全性,因此天然就會選擇 C,但其實咱們並無徹底放棄 A,由於多數時候,畢竟斷網,機器停電不會特別頻繁,咱們只須要保證 HA - High Availability,也就是 4 個 9 或者 5 個 9 的可用性就能夠了。

既然選擇了 C,咱們下一個就考慮的是選用哪種分佈式一致性算法,如今流行的無非就是 Paxos 或者 Raft,而 Raft 由於簡單,容易理解,以及有不少現成的開源庫能夠參考,天然就成了咱們的首要選擇。

在 Raft 的實現上,咱們直接參考的 etcd 的 Raft。etcd 已經被大量的公司在生產環境中使用,因此它的 Raft 庫質量是頗有保障的。雖然 etcd 是用 Go 實現的,但它的 Raft library 是相似 C 的實現,因此很是便於咱們用 Rust 直接翻譯。在翻譯的過程當中,咱們也給 etcd 的 Raft fix 了一些 bug,添加了一些功能,讓其變得更加健壯和易用。

如今 Raft 的代碼仍然在 TiKV 工程裏面,但咱們很快會將獨立出去,變成獨立的 library,這樣你們就能在本身的 Rust 項目中使用 Raft 了。

使用 Raft 不光能保證數據的一致性,也能夠藉助 Raft 的 Configuration Change 機制實現系統的水平擴展,這個咱們會在後面的文章中詳細的說明。

存儲引擎

選擇了分佈式一致性協議,下一個就要考慮數據存儲的問題了。在 TiKV 裏面,咱們會存儲 Raft log,而後也會將 Raft log 裏面實際的客戶請求應用到狀態機裏面。

首先來看狀態機,由於它會存放用戶的實際數據,而這些數據徹底多是隨機的 key - value,爲了高效的處理隨機的數據插入,天然咱們就考慮使用如今通用的 LSM Tree 模型。而在這種模型下,RocksDB 能夠認爲是現階段最優的一個選擇。

RocksDB 是 Facebook 團隊在 LevelDB 的基礎上面作的高性能 Key-Value Storage,它提供了不少配置選項,能讓你們根據不一樣的硬件環境去調優。這裏有一個梗,說的是由於 RocksDB 配置太多,以致於連 RocksDB team 的同窗都不清楚全部配置的意義。

關於咱們在 TiKV 中如何使用,優化 RocksDB,以及給 RocksDB 添加功能,fix bug 這些,咱們會在後面文章中詳細說明。

而對於 Raft Log,由於任意 Log 的 index 是徹底單調遞增的,譬如 Log 1,那麼下一個 Log 必定是 Log 2,因此 Log 的插入能夠認爲是順序插入。這種的,最一般的作法就是本身寫一個 Segment File,但如今咱們仍然使用的是 RocksDB,由於 RocksDB 對於順序寫入也有很是高的性能,也能知足咱們的需求。但咱們不排除後面使用本身的引擎。

由於 RocksDB 提供了 C API,因此能夠直接在 Rust 裏面使用,你們也能夠在本身的 Rust 項目裏面經過 rust-rocksdb 這個庫來使用 RocksDB。

分佈式事務

要支持分佈式事務,首先要解決的就是分佈式系統時間的問題,也就是咱們用什麼來標識不一樣事務的順序。一般有幾種作法:

  • TrueTime,TrueTime 是 Google Spanner 使用的方式,不過它須要硬件 GPS + 原子鐘支持,並且 Spanner 並無在論文裏面詳細說明硬件環境是如何搭建的,外面要本身實現難度比較大。

  • HLC,HLC 是一種混合邏輯時鐘,它使用 Physical Time 和 Logical Clock 來肯定事件的前後順序,HLC 已經在一些應用中使用,但 HLC 依賴 NTP,若是 NTP 精度偏差比較大,極可能會影響 commit wait time。

  • TSO,TSO 是一個全局授時器,它直接使用一個單點服務來分配時間。TSO 的方式很簡單,但會有單點故障問題,單點也可能會有性能問題。

TiKV 採用了 TSO 的方式進行全局授時,主要是爲了簡單。至於單點故障問題,咱們經過 Raft 作到了自動 fallover 處理。而對於單點性能問題,TiKV 主要針對的是 PB 以及 PB 如下級別的中小規模集羣,因此在性能上面只要能保證每秒百萬級別的時間分配就能夠了,而網絡延遲上面,TiKV 並無全球跨 IDC 的需求,在單 IDC 或者同城 IDC 狀況下,網絡速度都很快,即便是異地 IDC,也由於有專線不會有太大的延遲。

解決了時間問題,下一個問題就是咱們採用何種的分佈式事務算法,最一般的就是使用 2 PC,但一般的 2 PC 算法在一些極端狀況下面會有問題,因此業界要不經過 Paxos,要不就是使用 3 PC 等算法。在這裏,TiKV 參考 Percolator,使用了另外一種加強版的 2 PC 算法。

這裏先簡單介紹下 Percolator 的分佈式事務算法,Percolator 使用了樂觀鎖,也就是會先緩存事務要修改的數據,而後在 Commit 提交的時候,對要更改的數據進行加鎖處理,而後再更新。採用樂觀鎖的好處在於對於不少場景能提升整個系統的併發處理能力,但在衝突嚴重的狀況下反而沒有悲觀鎖高效。

對於要修改的一行數據,Percolator 會有三個字段與之對應,Lock,Write 和 Data:

  • Lock,就是要修改數據的實際 lock,在一個 Percolator 事務裏面,有一個 primary key,還有其它 secondary keys, 只有 primary key 先加鎖成功,咱們纔會再去嘗試加鎖後續的 secondary keys。

  • Write,保存的是數據實際提交寫入的 commit timestamp,當一個事務提交成功以後,咱們就會將對應的修改行的 commit timestamp 寫入到 Write 上面。

  • Data,保存實際行的數據。

當事務開始的時候,咱們會首先獲得一個 start timestamp,而後再去獲取要修改行的數據,在 Get 的時候,若是這行數據上面已經有 Lock 了,那麼就可能終止當前事務,或者嘗試清理 Lock。

當咱們要提交事務的時候,先獲得 commit timestamp,會有兩個階段:

  1. Prewrite:先嚐試給 primary key 加鎖,而後嘗試給 second keys 加鎖。若是對應 key 上面已經有 Lock,或者在 start timestamp 以後,Write 上面已經有新的寫入,Prewrite 就會失敗,咱們就會終止此次事務。在加鎖的時候,咱們也會順帶將數據寫入到 Data 上面。

  2. Commit:當全部涉及的數據都加鎖成功以後,咱們就能夠提交 primay key,這時候會先判斷以前加的 Lock 是否還在,若是還在,則刪掉 Lock,將 commit timestamp 寫入到 Write。當 primary key 提交成功以後,咱們就能夠異步提交 second keys,咱們不用在意 primary keys 是否能提交成功,即便失敗了,也有機制能保證數據被正常提交。

在 TiKV 裏面,事務的實現主要包括兩塊,一個是集成在 TiDB 中的 tikv client,而另外一個則是在 TiKV 中的 storage mod 裏面,後面咱們會詳細的介紹。

RPC 框架

RPC 應該是分佈式系統裏面經常使用的一種網絡交互方式,但實現一個簡單易用而且高效的 RPC 框架並非一件容易的事情,幸運的是,如今有不少能夠供咱們進行選擇。

TiKV 從最開始設計的時候,就但願使用 gRPC,但 Rust 當時並無能在生產環境中可用的 gRPC 實現,咱們只能先基於 mio 本身作了一個 RPC 框架,但隨着業務的複雜,這套 RPC 框架開始不能知足需求,因而咱們決定,直接使用 Rust 封裝 Google 官方的 C gRPC,這樣就有了 grpc-rs

這裏先說一下爲何咱們決定使用 gRPC,主要有以下緣由:

  • gRPC 應用普遍,不少知名的開源項目都使用了,譬如 Kubernetes,etcd 等。

  • gRPC 有多種語言支持,咱們只要定義好協議,其餘語言都能直接對接。

  • gRPC 有豐富的接口,譬如支持 unary,client streaming,server streaming 以及 duplex streaming。

  • gRPC 使用 protocol buffer,能高效的處理消息的編解碼操做。

  • gRPC 基於 HTTP/2,一些 HTTP/2 的特性,譬如 duplexing,flow control 等。

最開始開發 rust gRPC 的時候,咱們先準備嘗試基於一個 rust 的版原本開發,但無奈遇到了太多的 panic,果斷放棄,因而就將目光放到了 Google gRPC 官方的庫上面。Google gRPC 庫提供了多種語言支持,譬如 C++,C#,Python,這些語言都是基於一個核心的 C gRPC 來作的,因此咱們天然選擇在 Rust 裏面直接使用 C gRPC。

由於 Google 的 C gRPC 是一個異步模型,爲了簡化在 rust 裏面異步代碼編寫的難度,咱們使用 rust Future 庫將其從新包裝,提供了 Future API,這樣就能按照 Future 的方式簡單使用了。

關於 gRPC 的詳細介紹以及 rust gRPC 的設計還有使用,咱們會在後面的文章中詳細介紹。

監控

很難想象一個沒有監控的分佈式系統是如何能穩定運行的。若是咱們只有一臺機器,可能時不時看下這臺機器上面的服務還在不在,CPU 有沒有問題這些可能就夠了,但若是咱們有成百上千臺機器,那麼勢必要依賴監控了。

TiKV 使用的是 Prometheus,一個很是強大的監控系統。Prometheus 主要有以下特性:

  • 基於時序的多維數據模型,對於一個 metric,咱們能夠用多種 tag 進行多維區分。

  • 自定義的報警機制。

  • 豐富的數據類型,提供了 Counter,Guage,Histogram 還有 Summary 支持。

  • 強大的查詢語言支持。

  • 提供 pull 和 push 兩種模式支持。

  • 支持服務的動態發現和靜態配置。

  • 能跟 Grafana 深度整合。

由於 Prometheus 並無 Rust 的客戶端,因而咱們開發了 rust-prometheus。Rust Prometheus 在設計上面參考了 Go Prometehus 的 API,但咱們只支持了 最經常使用的 Counter,Guage 和 Histogram,並無實現 Summary。

後面,咱們會詳細介紹 Prometheus 的使用,以及不一樣的數據類型的使用場景等。

測試

要作好一個分佈式的 Key-Value Store,測試是很是重要的一環。 只有通過了最嚴格的測試,咱們纔能有信心去保證整個系統是能夠穩定運行的。

從最開始開發 TiKV 的時候,咱們就將測試擺在了最重要的位置,除了常規的 unit test,咱們還作了更多,譬如:

  • Stability test,咱們專門寫了一個 stability test,隨機的干擾整個系統,同時運行咱們的測試程序,看結果的正確性。

  • Jepsen,咱們使用 Jepsen 來驗證 TiKV 的線性一致性。

  • Namazu,咱們使用 Namazu 來干擾文件系統以及 TiKV 線程調度。

  • Failpoint,咱們在 TiKV 不少關鍵邏輯上面注入了 fail point,而後在外面去觸發這些 fail,在驗證即便出現了這些異常狀況,數據仍然是正確的。

上面僅僅是咱們的一些測試案例,當代碼 merge 到 master 以後,咱們的 CI 系統在構建好版本以後,就會觸發全部的 test 執行,只有當全部的 test 都徹底跑過,咱們纔會放出最新的版本。

在 Rust 這邊,咱們根據 FreeBSD 的 Failpoint 開發了 fail-rs,並已經在 TiKV 的 Raft 中注入了不少 fail,後面還會在更多地方注入。咱們也會基於 Rust 開發更多的 test 工具,用來測試整個系統。

小結

上面僅僅列出了咱們用 Rust 開發 TiKV 的過程當中,一些核心模塊的設計思路。這篇文章只是一個簡單的介紹,後面咱們會針對每個模塊詳細的進行說明。還有一些功能咱們如今是沒有作的,譬如 open tracing,這些後面都會慢慢開始完善。

咱們的目標是經過 TiKV,在分佈式系統領域,提供一套 Rust 解決方案,造成一個 Rust ecosystem。這個目標很遠大,歡迎任何感興趣的同窗加入。

 

相關閱讀

網頁加速特技之 AMP

表格行與列邊框樣式處理的原理分析及實戰應用

 

EB級別雲存儲是如何漲成的?

 

 

此文已由做者受權騰訊雲技術社區發佈,轉載請註明原文出處

原文連接:https://cloud.tencent.com/community/article/906224?utm_source=bky

海量技術實踐經驗,盡在騰訊雲社區

相關文章
相關標籤/搜索