Elasticell-Multi-Raft實現

什麼是Multi-Raft

這裏引用Cockroach(Multi-Raft的先驅,出來的比TiDB早,哈哈)對Multi-Raft的定義:node

In CockroachDB, we use the Raft consensus algorithm to ensure that your data remains consistent even when machines fail. In most systems that use Raft, such as etcd and Consul, the entire system is one Raft consensus group. In CockroachDB, however, the data is divided into ranges, each with its own consensus group. This means that each node may be participating in hundreds of thousands of consensus groups. This presents some unique challenges, which we have addressed by introducing a layer on top of Raft that we call MultiRaft.git

簡單來講,Multi-Raft是在整個系統中,把所管理的數據按照必定的方式切片,每個切片的數據都有本身的副本,這些副本之間的數據使用Raft來保證數據的一致性,在全局來看整個系統中同時存在多個Raft-Group,就像這個樣子:github

延伸閱讀:架構

 

Multi-Raft須要解決的問題

單個Raft-Group在KV的場景下存在一些弊端:分佈式

  1. 系統的存儲容量受制於單機的存儲容量(使用分佈式存儲除外)ide

  2. 系統的性能受制於單機的性能(讀寫請求都由Leader節點處理)性能

Multi-Raft須要解決的一些核心問題:優化

  1. 數據何如分片

  2. 分片中的數據愈來愈大,須要分裂產生更多的分片,組成更多Raft-Group

  3. 分片的調度,讓負載在系統中更平均(分片副本的遷移,補全,Leader切換等等)

  4. 一個節點上,全部的Raft-Group複用連接(不然Raft副本之間兩兩建鏈,連接爆炸了)

  5. 如何處理stale的請求(例如Proposal和Apply的時候,當前的副本不是Leader、分裂了、被銷燬了等等)

  6. Snapshot如何管理(限制Snapshot,避免帶寬、CPU、IO資源被過分佔用)

要實現一個Multi-Raft仍是很複雜和頗有挑戰的一件事情。

 

技術選型

2017年初,咱們剛開始作Elasticell的時候,開源的Multi-Raft實現不多,當時咱們知道開源的實現有CockroachTiDB(兩者都是受Google的Spanner和F1的啓發)。Cockroach是Go語言實現,TiDB是Rust實現,Raft基礎庫都是使用Etcd的實現(TiDB是把Etcd的Raft移植到了Rust上)。二者在架構上一個很重要的不一樣是TiDB是一個分離式的設計,整個架構上有PD、TiDB、TiKV三個。咱們當時以爲元信息使用PD獨立出來管理,架構更清晰,工程實現也相對簡單,因此咱們決定參照TiDB來實現Multi-Raft。

 

Elasticell參考的是2017年3月份左右的TiDB的版本,大致思路基本一致,實現方式上有一些不同的地方,更多的是語言的差別。TiDB的實現是Rust的實現,Elasticell是pure Go的實現。

 

CGO和GC的開銷問題

在咱們決定用Go開發Elasticell的時候,就有些擔憂CGO和GC的開銷問題,當時還諮詢了PingCAP的黃東旭,最後認爲在KV場景下,這個開銷應該能夠接受。後來開發完成後,咱們作了一些常見的優化(合併一些CGO調用,使用對象池,內存池等),發現系統的瓶頸基本在IO上,目前CGO和GC的開銷是能夠接受的。

 

Elasticell實現細節-數據如何分片

Elasticell支持兩種分片方式適用於不一樣的場景

  1. 按照用戶的Key作字典序,系統一開始只有1個分片,分片個數隨着系統的數據量逐漸增大而不斷分裂(這個實現和TiKV一致)

  2. 按照Key的hash值,映射到一個uint64的範圍,能夠初始化的時候就能夠設置N個分片,讓系統一開始就能夠支持較高的併發,後續隨着數據量的增大繼續分裂

 

Elasticell實現細節-分片如何調度

這部分的思路就和TiKV徹底一致了。PD負責調度指令的下發,PD經過心跳收集調度須要的數據,這些數據包括:節點上的分片的個數,分片中leader的個數,節點的存儲空間,剩餘存儲空間等等。一些最基本的調度:

  1. PD發現分片的副本數目缺乏了,尋找一個合適的節點,把副本補全

  2. PD發現系統中節點之間的分片數相差較多,就會轉移一些分片的副本,保持系統中全部節點的分片數目大體相同(存儲均衡)

  3. PD發現系統中節點之間分片的Leader數目不太一致,就會轉移一些副本的Leader,保持系統中全部節點的分片副本的Leader數目大體相同(讀寫請求均衡)

延伸閱讀:

 

Elasticell實現細節-新的分片如何造成Raft-Group

假設這個分片1有三個副本分別運行在N1,N2,N3三臺機器上,其中N1機器上的副本是Leader,分片的大小限制是1GB。

 

當分片1管理的數據量超過1GB的時候,分片1就會分裂成2個分片,分裂後,分片1修改數據範圍,更新Epoch,繼續服務。

 

分片2形也有三個副本,分別也在N1,N2,N3上,這些是元信息,可是隻有在N1上存在真正被建立的副本實例,N2,N3並不知道這個信息。這個時候N1上的副本會當即進行Campaign Leader的操做,這個時候,N2和N3會收到來自分片2的Vote的Raft消息,N2,N3發現分片2在本身的節點上並無副本,那麼就會檢查這個消息的合法性和正確性,經過後,當即建立分片2的副本,剛建立的副本沒有任何數據,建立完成後會響應這個Vote消息,也必定會選擇N1的副本爲Leader,選舉完成後,N1的分片2的Leader會給N2,N3的副本直接發送Snapshot,最終這個新的Raft-Group造成而且對外服務。

 

按照Raft的協議,分片2在N1副本稱爲Leader後不該該直接給N2,N3發送snapshot,可是這裏咱們沿用了TiKV的設計,Raft初始化的Log Index是5,那麼按照Raft協議,N1上的副本須要給N2,N3發送AppendEntries,這個時候N1上的副本發現Log Index小於5的Raft Log不存在,因此就會轉爲直接發送Snapshot。

 

Elasticell實現細節-如何處理stale的請求

因爲分片的副本會被調度(轉移,銷燬),分片自身也會分裂(分裂後分片所管理的數據範圍發生了變化),因此在Raft的Proposal和Apply的時候,咱們須要檢查Stale請求,如何作呢?其實仍是蠻簡單的,TiKV使用Epoch的概念,咱們沿用了下來。一個分片的副本有2個Epoch,一個在分片的副本成員發生變化的時候遞增,一個在分片數據範圍發生變化的時候遞增,在請求到來的時候記錄當前的Epoch,在Proposal和Apply的階段檢查Epoch,讓客戶端重試Stale的請求。

 

Elasticell實現細節-Snapshot如何管理

咱們的底層存儲引擎使用的是RocksDB,這是一個LSM的實現,支持對一個範圍的數據進行Snapshot和Apply Snapshot,咱們基於這個特性來作。Raft中有一個RPC用於發送Snapshot數據,可是若是把全部的數據放在這個RPC裏面,那麼會有不少問題:

  1. 一個RPC的數據量太大(取決於一個分片管理的數據,可能上GB,內存吃不消)

  2. 若是失敗,總體重試代價太大

  3. 難以流控

咱們修改成這樣:

  • Raft的snapshot RPC中的數據存放,snapshot文件的元信息(包括分片的ID,當前Raft的Term,Index,Epoch等信息)

  • 發送Raft snapshot的RPC後,異步發送具體數據文件

  • 數據文件分Chunk發送,重試的代價小

  • 發送 Chunk的連接和Raft RPC的連接不復用

  • 限制並行發送的Chunk個數,避免snapshot文件發送影響正常的Raft RPC

  • 接收Raft snapshot的分片副本阻塞,直到接收完畢完整的snapshot數據文件

 

後續文章

  1. 咱們在Raft上作的一些優化

  2. 如何支持全文索引

敬請期待

 

項目地址

https://github.com/deepfabric/elasticell

 

感謝

  • PingCAP團隊(健壯的Multi-Raft實現)

  • @Ed Huang (私下諮詢了不少問題)

相關文章
相關標籤/搜索