分佈式存儲系統 面臨着的首要問題,就是如何將 大量的數據 分佈在 不一樣的存儲節點 上。不管上層接口是 KV 存儲、對象存儲、塊存儲、亦或是 列存儲,在這個問題上大致是一致的。本文將介紹如何 分佈式存儲系統 中 作數據分佈目標 及可選的 方案,並試着總結和權衡他們之間的關係及。算法
這裏假設 目標數據 是以 key 標識的 數據塊 或 對象。在一個包含 多個存儲節點 的集羣中,數據分佈算法 須要爲每個給定的 key 指定 一個 或 多個 對應的 存儲節點 負責,數據分佈算法 有兩個基本目標:負載均衡
能夠看出,這兩個目標在必定程度上是 相互矛盾 的。當有 存儲節點增長或刪除 時,爲了保持穩定應該 儘可能少 的進行 數據的移動 和 從新分配,而這樣又勢必會帶來 負載不均衡。一樣追求 極致均勻 也會致使較多的 數據遷移。分佈式
因此咱們但願在這兩個極端之間,找到一個點以得到合適的均勻性和穩定性。除了上述兩個基本目標外,工程中還須要從如下幾個方面考慮數據分佈算法的優劣:性能
看完算法的評價指標後,接下來介紹一些可能的方案演進,並分析他們的優劣。這裏假設 key 的值足夠分散。優化
一個簡單直觀的想法是直接用 Hash 來計算,簡單的以 Key 作 哈希 後 對節點數取模。能夠看出,在 key 足夠分散的狀況下,均勻性 能夠得到,但一旦有 節點加入 或 退出 時,全部的原有節點都會受到影響。穩定性 無從談起。3d
一致性 Hash 能夠很好的解決 穩定性問題,能夠將全部的 存儲節點 排列在收尾相接的 Hash 環上,每一個 key 在計算 Hash 後會 順時針 找到先遇到的 存儲節點 存放。而當有節點 加入 或退出 時,僅影響該節點在 Hash 環上 順時針相鄰 的 後續節點。但這有帶來 均勻性 的問題,即便能夠將存儲節點等距排列,也會在 存儲節點個數 變化時帶來 數據的不均勻。而這種可能成倍數的不均勻 在實際工程中是不可接受的。orm
一致性 Hash 有 節點變化時不均勻的問題。Google 在 2017 年提出了 Consistent Hashing with Bounded Loads 來控制這種 不均勻的程度。簡單的說,該算法給 Hash 環上的每一個節點一個 負載上限 爲 1 + e 倍的 平均負載,這個 e能夠自定義。當 key 在 Hash 環上 順時針找到合適的節點後,會判斷這個節點的 負載 是否已經 到達上限,若是 已達上限,則須要繼續找 以後的節點 進行分配。cdn
如上圖所示,假設每一個桶 當前上限 是 2,紅色的小球按序號訪問,當編號爲 6 的紅色小球到達時,發現順時針首先遇到的 B(3,4),C(1,5)都已經 達到上限,所以最終放置在桶 A 裏。對象
這個算法最吸引人的地方在於 當有節點變化 時,須要遷移的數據量是 1/e^2 相關,而與 節點數 或 數據數量 均無關。blog
也就是說當 集羣規模擴大 時,數據遷移量 並不會隨着顯著增長。另外,使用者能夠經過調整 e 的值來控制 均勻性 和 穩定性 之間的權衡,就是一種 以時間換空間 的算法。整體來講,不管是 一致性 Hash 仍是 帶負載限制 的 一致性 Hash,都沒法解決 節點異構 的問題。
爲了解決 負載不均勻 和 異構 的問題,能夠在 一致性 Hash 的基礎上引入 虛擬節點。即 hash 環上的 每一個節點 並非 實際 的 存儲節點,而是一個 虛擬節點。實際的 存儲節點 根據其 不一樣的權重,對應 一個 或 多個虛擬節點,全部落到相應虛擬節點上的 key 都由該 存儲節點負責。
以下圖所示,存儲節點 A 負責 (1,3],(4,8],(10, 14],存儲節點 B 負責 (14,1],(8,10]。
這個算法的問題在於,一個 實際存儲節點 的 加入 或 退出,會影響 多個虛擬節點的從新分配,進而引發 不少節點 參與到 數據遷移 中來。
另外,實踐中將一個 虛擬節點 從新分配給 新的實際節點 時,須要將這部分數據 遍歷 出來 發送給新節點。咱們須要一個更合適的 虛擬節點切分 和 分配方式,那就是 分片。
分片 將 哈希環 切割爲 相同大小的分片,而後將這些 分片 交給 不一樣的節點 負責。
注意這裏跟上面提到的 虛擬節點 有着很 本質的區別:分片的劃分和分片的分配被解耦。
一個 節點退出 時,其所負責的 分片 並不須要 順時針合併 給以後節點,而是能夠更靈活的 將整個分片 做爲一個 總體 交給 任意節點。在實踐中,一個 分片 多做爲 最小的數據遷移 和 備份單位。
而也正是因爲上面提到的 解耦,至關於將原先的 key 到 節點 的 映射 拆成了兩層。須要一個新的機制 來進行 分片 到 存儲節點 的 映射。因爲 分片數 相對 key 空間已經很小而且 數量肯定,能夠更精確地初始設置,並引入 中心目錄服務 來根據 節點存活 修改 分片的映射關係。同時將這個 映射信息 通知給全部的 存儲節點 和 客戶端。
上圖是 分佈式KV存儲 Zeppelin中的 分片方式,Key Space 經過 Hash 到 分片,分片及其副本 又經過一層映射到 最終的存儲節點 Node Server。
CRUSH 算法本質上也是一種 基於分片 的數據分佈方式,其試圖在如下幾個方面進行優化:
客戶端 或 存儲節點 利用 key、存儲節點 的 拓撲結構 和 分配算法,獨立的進行 分片位置 的計算,獲得一組負責對應 分片 及 副本 的 存儲位置。
如圖所示是 一次定位 的過程,最終選擇了一個 row 下的 cab21,cab23,cab24 三個機櫃下的三個存儲節點。
當 節點變化 時,因爲 節點拓撲 的變化,會影響 少許分片 數據進行遷移,以下圖是加入 新節點 引發的 數據遷移。經過良好的 分配算法,能夠獲得很好的 負載均衡 和 穩定性,CRUSH 提供了 Uniform、List、Tree、Straw 四種分配算法。
常見的 分佈式存儲系統 大多采用相似於 分片 的 數據分佈和定位方式: