分佈式存儲初探 原 薦

分佈式存儲初探


緣起

最近公司內部在做dmp服務,目前的方案都是搭建不同的redis集羣,將數據灌到redis集羣中系統查詢服務供線上使用。但是隨着數據量的增大以及數據源的多樣性,再加上線上服務需要多機房的支持,後續繼續使用redis集羣必然導致成本過高。 當然也考慮過使用hbase來支持線上服務,但是線上服務對請求相應要求高,而hbase有延遲高的風險,所以有了本次對分佈式kv數據庫的一些調研性工作。

爲什麼需要分佈式數據庫

在使用分佈式數據庫之前,我們一般使用mysql來支持一般的線上業務,即使在單機存儲有限的情況下,我們也可以使用sharding的方式分庫分表來支撐數據量大的情況,但是sharding又有其自身各種各樣的弊端,例如其跨節點join的複雜性和網絡傳輸問題。所以由於單機的數據存儲有限,無法滿足我們對數據的存儲和查詢,於是分佈式存儲應運而生。

分佈式數據庫需要解決哪些基本問題

  1. 數據如何存儲
  2. 數據如何查詢,如何索引
  3. 如何保證HA
  4. 如何保證一致性

下面將分別對如上4個問題介紹現在業界內的比較成熟的開源產品是如何解決的。

數據的存儲和查詢

任何持久化存儲,最後都要落到磁盤上。而且根據數據的實際應用,數據的存儲和數據的查詢必然緊密聯繫的。目前業界內比較成熟的存儲引擎在索引數據時使用的數據結構包括:B-Tree,B+Tree和LSM-Tree,下面我將詳細講解這三個結構的異同。

B-Tree

B樹是一種多路自平衡搜索樹,它類似普通的二叉樹,但是B書允許每個節點有更多的子節點。B樹示意圖如下:

b_tree

B樹的特點:

  1. 所有鍵值分佈在整個樹中
  2. 任何關鍵字出現且只出現在一個節點中
  3. 搜索有可能在非葉子節點結束
  4. 在關鍵字全集內做一次查找,性能逼近二分查找算法

B+ Tree

B+樹是B樹的變體,也是一種多路平衡查找樹,B+樹的示意圖爲:

b_plus_tree

從圖中也可以看到,B+樹與B樹的不同在於:

  1. 所有關鍵字存儲在葉子節點,非葉子節點不存儲真正的data
  2. 爲所有葉子節點增加了一個鏈指針

B/B+樹普遍在文件系統和mysql中被用來做索引的實現。大家知道mysql是基於磁盤的數據庫,索引是以索引文件的形式存在於磁盤中的,索引的查找過程就會涉及到磁盤IO消耗,磁盤IO的消耗相比較於內存IO的消耗要高好幾個數量級,所以索引的組織結構要設計得在查找關鍵字時要儘量減少磁盤IO的次數。根據mysql內部對記錄按照頁管理的方式,其查找索引過程中需要磁盤IO的次數只需要樹的高度h-1次,而$ O(h)=O(log_dN) $,其中d爲每個節點的出度,N爲記錄個數,通常d的值是非常大的數字,因此h非常小,通常不超過3。

另一方面,mysql選擇使用B+樹而不使用B樹的原因如下:

  1. B+樹更適合外部存儲(一般指磁盤存儲),由於內節點(非葉子節點)不存儲data,所以一個節點可以存儲更多的內節點,每個節點能索引的範圍更大更精確。也就是說使用B+樹單次磁盤IO的信息量相比較B樹更大,IO效率更高。
  2. mysql是關係型數據庫,經常會按照區間來訪問某個索引列,B+樹的葉子節點間按順序建立了鏈指針,加強了區間訪問性,所以B+樹對索引列上的區間範圍查詢很友好。而B樹每個節點的key和data在一起,無法進行區間查找。

LSM樹(Log Structured Merge Tree)

首先我們要了解一個事實,隨機讀寫磁盤是非常慢的,但是順序讀寫磁盤卻要比隨機讀寫主存快至少3個數量級。

LSM樹的設計目的是爲了實現順序寫磁盤,而順序寫意味着我們省去了隨機寫的磁盤尋道時間,這樣可以爲隨機讀磁盤提供更多的磁盤IO機會,以此來提高讀的性能。

所以LSM的設計思想就呼之欲出了:將對數據的修改增量保持在內存中,當到達執行的大小限制後將這些修改操作批量寫入磁盤。

但是LSM具體是如何實現這個思想的呢?

LSM樹弄了很多個小的有序結構,比如每m個數據,在內存裏排序一次,下面m個數據,再排序一次,這樣一次做下去,我們就可以得到N/m個有序的小結構。在查詢的時候,因爲不知道某個數據到底在哪裏,所以就從最新的一個小的有序結構裏做二分查找,找到就返回,找不到就繼續找下一個有序小結構,一直到找到爲止。其複雜度爲$N/m * log_2m$。

但是上面這種方式會存在一些問題:

  1. 數據先寫到內存中,中間斷電或進程crash會造成數據丟失,所以需要寫WAL來作爲數據恢復的依據。
  2. 隨着小的有序結構越來越多,讀的性能會越來越差,這個時候就需要對小文件做合併,合併成大的有序結構。
  3. 這其實是一個優化項,LSM樹使用布隆過濾器來對小文件中是否存在要查找的數據做粗略的判斷。

目前使用LSM樹的數據庫包括hbase,leveldb,rocksdb。

高可用和一致性問題

HA包括服務高可用和數據完整性。針對第一點一個比較通用的方式就是以主備的形式來提供服務,像redis的master-slave架構,而第二點則通常將數據冗餘存儲在多臺服務器上來實現數據的備份達到高可用的目的,像hdfs和kafka,zookeeper更是同時滿足以上兩點的更典型的一個案例。

但是說到主備和數據冗餘,其意味着需要將數據或狀態同時存儲在多臺服務器上,那麼在發生主服務crash,需要從服務提供服務或啓用副本數據時,就要求其狀態和數據要與主服務或數據保持一致,這就是分佈式系統之間典型的一致性問題。

在目前的分佈式領域中,有關一致性問題的解決有幾個經典算法:Poxas,Zab和Raft,介於Poxas算法太難理解,這裏我們只介紹raft。

Raft算法

Raft 通過選舉一個高貴的領導人,然後給予他全部的管理複製日誌的責任來實現一致性。領導人從客戶端接收日誌條目,把日誌條目複製到其他服務器上,並且當保證安全性的時候告訴其他的服務器應用日誌條目到他們的狀態機中。 通過領導人的方式,Raft 將一致性問題分解成了三個相對獨立的子問題:

  1. Leader 選舉
  2. 日誌複製
  3. 安全性

在raft算法中,每個服務器都處於三種狀態之一:leader,candidate,follower。在通常情況下,系統中只有一個領導人並且其他的節點全部都是跟隨者。跟隨者都是被動的:他們不會發送任何請求,只是簡單的響應來自領導者或者候選人的請求。領導人處理所有的客戶端請求(如果一個客戶端和跟隨者聯繫,那麼跟隨者會把請求重定向給領導人)。第三種狀態,候選人,是用來在選舉新領導人時使用。下圖展示了這些狀態和他們之前轉換關係。

任期:在集羣中,時間被劃分成一個個的任期,每個任期開始都是一次選舉。在選舉成功後,領導人會管理整個集羣直到任期結束。如圖:

下面介紹一下raft算法是如何工作的。

領導人選舉

Raft 使用一種心跳機制來觸發領導人選舉。當服務器程序啓動時,他們都是跟隨者身份。之後他會根據不同的觸發條件轉換成其他狀態:

  1. 在一段時間之內接收到其他服務器發來的RPC請求,刷新本地超時計時,繼續處於跟隨者狀態。
  2. 在一段時間之內沒有接收到任何消息,即選舉超時,那麼他會認爲系統中沒有leader,其將自己變爲選舉者狀態,並向其他服務器發起RPC請求,請求他們投票給自己成爲leader。

在2的情況下,若一個候選者獲取了一個集羣中大多數服務器節點的投票,那麼他贏得這次的選舉稱爲領導人;當一次選舉中有兩個服務器同時半數投票,則此次選舉沒有leader,繼續下一屆選舉。在一次選舉內,每個服務器最多隻會對一個服務器進行投票,按照先來先服務原則。一旦某個候選人贏得此次選舉,他就立即稱爲領導人,然後他會定期向所有跟隨者發送心跳消息來建立自己的權威並阻止新的領導人產生。

在等待投票的時候,候選人可能會從其他的服務器接收到聲明它是領導人的附加日誌項RPC。如果這個領導人的任期號(包含在此次的 RPC中)不小於候選人當前的任期號,那麼候選人會承認領導人合法並回到跟隨者狀態。如果此次RPC中的任期號比自己小,那麼候選人就會拒絕這次的RPC並且繼續保持候選人狀態。

日誌複製

一旦一個領導人被選舉出來,他就開始爲客戶端提供服務。客戶端的每一個請求都包含一條被複制狀態機執行的指令。領導人把這條指令作爲一條新的日誌條目附加到日誌中去,然後並行的發起附加條目RPCs給其他的服務器,讓他們複製這條日誌條目。當這條日誌條目被安全的複製,領導人會應用這條日誌條目到它的狀態機中然後把執行的結果返回給客戶端。如果跟隨者崩潰或者運行緩慢,再或者網絡丟包,領導人會不斷的重複嘗試附加日誌條目RPCs(儘管已經回覆了客戶端)直到所有的跟隨者都最終存儲了所有的日誌條目。

每一個日誌條目存儲一條狀態機指令和從領導人收到這條指令時的任期號。日誌中的任期號用來檢查是否出現不一致的情況,同時也都有一個整數索引值來表明它在日誌中的位置。

raft通過維護以下特性來保證不同服務器的日誌之間的高層次的一致性:

  • 如果在不同的日誌中的兩個條目擁有相同的索引和任期號,那麼他們存儲了相同的指令。
  • 如果在不同的日誌中的兩個條目擁有相同的索引和任期號,那麼他們之前的所有日誌條目也全部相同。

這種特性其實更像是一種數學歸納法。

然後我們來談一談當發生領導人崩潰,導致一些服務器上的日誌不一致時,新的領導人要如何來處理。在Raft算法中,領導人處理不一致是通過強制跟隨者直接複製自己的日誌來解決了。這意味着在跟隨者中的衝突的日誌條目會被領導人的日誌覆蓋。但是這種覆蓋會通過一些限制來保證這樣的操作是正確的,安全的。

具體的保證日誌一致性的操作如下:

  1. 領導人針對每個跟隨者維護了一個nextIndex,指下一次需要發給跟隨者的日誌條目的索引地址。
  2. 當一個領導人剛獲得權利的時候,他初始化所有的nextIndex爲他自己的最後一條日誌的index + 1。
  3. 如果一個跟隨者的日誌和領導人不一致,那麼在下次的附加日誌RPC時的一致性檢查會失敗,在被跟隨者拒絕之後,領導人就會減小nextIndex值並進行重試。
  4. 最終nextIndex會在某個位置使得領導人和跟隨者的日誌達成一致。

當這種情況發生,附加日誌RPC就會成功,這時就會把跟隨者衝突的日誌條目全部刪除並且加上領導人的日誌。一旦附加日誌 RPC 成功,那麼跟隨者的日誌就會和領導人保持一致,並且在接下來的任期裏一直繼續保持。 領導人從來不會覆蓋或者刪除自己的日誌。

安全性

上面說到在新選舉領導人和跟隨者的日誌發生衝突時,會通過一些限制來保證日誌覆蓋的正確性,這些限制體現爲,當進行選舉時,保證任何領導人對於給定的任期號,都擁有之前任期的所有被提交的日誌條目。raft限制只有一個候選人包含了所有已經提交的日誌條目,它纔可以贏得選舉。選舉人爲了贏得選舉,必須聯繫集羣中的大部分節點,同時每一個已經提交的日誌也必然出現在集羣中的大部分節點,而兩個過半集合肯定有一個交集。這就保證了至少有一個以上的節點持有集羣中所有已經提交的日誌,同時投票人會拒絕掉那些日誌沒有自己新的投票請求。在比較日誌時,投票給候選人的條件爲:

  • 請求投票的最新日誌的任期應大於等於投票人的最新日誌的任期,即req.lastLogTerm >= lastEntry.term
  • 如果req.lastLogTerm == lastEntry.term,請求投票的最新日誌的索引值應大於等於投票人的最新日誌的索引值,即req.lastLogIndex >= lastEntry.index。

raft演示demo

更加詳細的介紹見raft paper

幾個成熟的分佈式數據庫架構

hbase

HBase是一個分佈式的、面向列的開源數據庫,它不同於一般的關係數據庫,是一個適合於非結構化數據存儲的數據庫。另一個不同的是HBase基於列的而不是基於行的模式。HBase使用和BigTable非常相同的數據模型。用戶存儲數據行在一個表裏。一個數據行擁有一個可選擇的鍵和任意數量的列,一個或多個列組成一個ColumnFamily,一個Family下的列位於一個HFile中,易於緩存數據。表是疏鬆的存儲的,因此用戶可以給行定義各種不同的列。在HBase中數據按主鍵排序,同時表按主鍵劃分爲多個Region。

在分佈式的生產環境中,HBase 需要運行在 HDFS 之上,以 HDFS 作爲其基礎的存儲設施。HBase 上層提供了訪問的數據的 Java API 層,供應用訪問存儲在 HBase 的數據。在 HBase 的集羣中主要由 Master 和 Region Server 組成,以及 Zookeeper,具體模塊如下圖所示:

簡單介紹一下 HBase 中相關模塊的作用:

  • Master HBase Master用於協調多個RegionServer,偵測各個RegionServer之間的狀態,並平衡RegionServer之間的負載。HBaseMaster還有一個職責就是負責分配Region給RegionServer。HBase允許多個Master節點共存,但是這需要Zookeeper的幫助。不過當多個Master節點共存時,只有一個Master是提供服務的,其他的Master節點處於待命的狀態。當正在工作的Master節點宕機時,其他的Master則會接管HBase的集羣。
  • Region Server 對於一個RegionServer而言,其包括了多個Region。RegionServer的作用只是管理表格,以及實現讀寫操作。Client直接連接RegionServer,並通信獲取HBase中的數據。對於Region而言,則是真實存放HBase數據的地方,也就說Region是HBase可用性和分佈式的基本單位。如果當一個表格很大,並由多個CF組成時,那麼表的數據將存放在多個Region之間,並且在每個Region中會關聯多個存儲的單元(Store)。
  • Zookeeper 對於HBase而言,Zookeeper的作用是至關重要的。首先Zookeeper是作爲HBase Master的HA解決方案。也就是說,是Zookeeper保證了至少有一個HBase Master 處於運行狀態。並且Zookeeper負責Region和Region Server的註冊。其實Zookeeper發展到目前爲止,已經成爲了分佈式大數據框架中容錯性的標準框架。不光是HBase,幾乎所有的分佈式大數據相關的開源框架,都依賴於Zookeeper實現HA。

tair

通常情況下,一個集羣中包含2臺configserver及多臺dataServer。兩臺configserver互爲主備並通過維護和dataserver之間的心跳獲知集羣中存活可用的dataserver,構建數據在集羣中的分佈信息(對照表)。dataserver負責數據的存儲,並按照configserver的指示完成數據的複製和遷移工作。client在啓動的時候,從configserver獲取數據分佈信息,根據數據分佈信息和相應的dataserver交互完成用戶的請求。其架構圖如下:

  • ConfigServer功能
    1. 通過維護和dataserver心跳來獲知集羣中存活節點的信息
    2. 根據存活節點的信息來構建數據在集羣中的分佈表。
    3. 提供數據分佈表的查詢服務。
    4. 調度dataserver之間的數據遷移、複製。
  • DataServer功能
    1. 提供存儲引擎
    2. 接受client的put/get/remove等操作
    3. 執行數據遷移,複製等
    4. 插件:在接受請求的時候處理一些自定義功能
    5. 訪問統計

tidb

整體架構如下:

TiDB 集羣主要分爲三個組件:

  • TiDB Server TiDB Server 負責接收 SQL 請求,處理 SQL 相關的邏輯,並通過 PD 找到存儲計算所需數據的 TiKV 地址,與 TiKV 交互獲取數據,最終返回結果。 TiDB Server 是無狀態的,其本身並不存儲數據,只負責計算,可以無限水平擴展,可以通過負載均衡組件(如LVS、HAProxy 或 F5)對外提供統一的接入地址。

  • PD Server Placement Driver (簡稱 PD) 是整個集羣的管理模塊,其主要工作有三個: 一是存儲集羣的元信息(某個 Key 存儲在哪個 TiKV 節點);二是對 TiKV 集羣進行調度和負載均衡(如數據的遷移、Raft group leader 的遷移等);三是分配全局唯一且遞增的事務 ID。PD 是一個集羣,需要部署奇數個節點,一般線上推薦至少部署 3 個節點。

  • TiKV Server TiKV Server 負責存儲數據,從外部看 TiKV 是一個分佈式的提供事務的 Key-Value 存儲引擎。存儲數據的基本單位是 Region,每個 Region 負責存儲一個 Key Range (從 StartKey 到 EndKey 的左閉右開區間)的數據,每個 TiKV 節點會負責多個 Region 。TiKV 使用 Raft 協議做複製,保持數據的一致性和容災。副本以 Region 爲單位進行管理,不同節點上的多個 Region 構成一個 Raft Group,互爲副本。數據在多個 TiKV 之間的負載均衡由 PD 調度,這裏也是以 Region 爲單位進行調度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章