w 這裏將主要列舉一致性Hash算法、Gossip協議、QuorumNWR算法、PBFT算法、PoW算法、ZAB協議,Paxos會分開單獨講。node
一致性Hash算法是爲了解決Hash算法的遷移成本,以一個10節點的集羣爲例,若是向集羣中添加節點時,若是使用了哈希 算法,須要遷移高達 90.91% 的數據,使用一致哈希的話,只須要遷移 6.48% 的數據。算法
因此使用一致性Hash算法實現哈希尋址時,能夠經過增長節點數下降節點 宕機對整個集羣的影響,以及故障恢復時須要遷移的數據量。後續在須要時,你能夠經過增 加節點數來提高系統的容災能力和故障恢復效率。而作數據遷移時,只須要遷移部分數據,就能實現集羣的穩定。緩存
咱們都知道普通的Hash算法是經過取模來進行路由尋址的,同理一致性Hash用了取模運算,但與哈希算法不一樣的是,哈希算法是對節點的數量進行取模 運算,而一致哈希算法是對 2^32 進行取模運算。你能夠想象下,一致哈希算法,將整個 哈希值空間組織成一個虛擬的圓環,也就是哈希環:服務器
在一致哈希中,你能夠經過執行哈希算法,將節點映射到哈希環上,從而每一個節點就能肯定其在哈希環上的位置了:網絡
而後當要讀取指定key的值的時候,經過對key作一個hash,並肯定此 key 在環上的位置,從這個位置沿着哈希環順時針「行走」,遇到的第一節點就是 key 對應的節點。分佈式
這個時候,若是節點C宕機了,那麼節點B和節點A的數據實際上不會受影響,只有原來在節點C的數據會被從新定位到節點A,從而只要節點C的數據作遷移便可。ide
若是此時集羣不能知足業務的需求,須要擴容一個節點:函數
你能夠看到,key-0一、key-02 不受影響,只有 key-03 的尋址被重定位到新節點 D。通常 而言,在一致哈希算法中,若是增長一個節點,受影響的數據僅僅是,會尋址到新節點和前 一節點之間的數據,其它數據也不會受到影響。區塊鏈
實現代碼以下:設計
/** * 不帶虛擬節點的一致性Hash算法 */ public class ConsistentHashingWithoutVirtualNode { /** * 待添加入Hash環的服務器列表 */ private static String[] servers = {"192.168.0.0:111", "192.168.0.1:111", "192.168.0.2:111", "192.168.0.3:111", "192.168.0.4:111"}; /** * key表示服務器的hash值,value表示服務器的名稱 */ private static SortedMap<Integer, String> sortedMap = new TreeMap<Integer, String>(); /** * 程序初始化,將全部的服務器放入sortedMap中 */ static { for (int i = 0; i < servers.length; i++) { int hash = getHash(servers[i]); System.out.println("[" + servers[i] + "]加入集合中, 其Hash值爲" + hash); sortedMap.put(hash, servers[i]); } System.out.println(); } /** * 獲得應當路由到的結點 */ private static String getServer(String node) { // 獲得帶路由的結點的Hash值 int hash = getHash(node); // 獲得大於該Hash值的全部Map SortedMap<Integer, String> subMap = sortedMap.tailMap(hash); // 第一個Key就是順時針過去離node最近的那個結點 Integer i = subMap.firstKey(); // 返回對應的服務器名稱 return subMap.get(i); } public static void main(String[] args) { String[] nodes = {"127.0.0.1:1111", "221.226.0.1:2222", "10.211.0.1:3333"}; for (int i = 0; i < nodes.length; i++) System.out.println("[" + nodes[i] + "]的hash值爲" + getHash(nodes[i]) + ", 被路由到結點[" + getServer(nodes[i]) + "]"); } }
上面的hash算法可能會形成數據分佈不均勻的狀況,也就是 說大多數訪問請求都會集中少許幾個節點上。因此咱們能夠經過虛擬節點的方式解決數據分佈不均的狀況。
其實,就是對每個服務器節點計算多個哈希值,在每一個計算結果位置上,都放置一個虛擬 節點,並將虛擬節點映射到實際節點。好比,能夠在主機名的後面增長編號,分別計算 「Node-A-01」,「Node-A-02」,「Node-B-01」,「Node-B-02」,「Node-C01」,「Node-C-02」的哈希值,因而造成 6 個虛擬節點:
增長了節點後,節點在哈希環上的分佈就相對均勻了。這時,若是有訪 問請求尋址到「Node-A-01」這個虛擬節點,將被重定位到節點 A。
具體代碼實現以下:
/** * 帶虛擬節點的一致性Hash算法 */ public class ConsistentHashingWithVirtualNode { /** * 待添加入Hash環的服務器列表 */ private static String[] servers = {"192.168.0.0:111", "192.168.0.1:111", "192.168.0.2:111", "192.168.0.3:111", "192.168.0.4:111"}; /** * 真實結點列表,考慮到服務器上線、下線的場景,即添加、刪除的場景會比較頻繁,這裏使用LinkedList會更好 */ private static List<String> realNodes = new LinkedList<String>(); /** * 虛擬節點,key表示虛擬節點的hash值,value表示虛擬節點的名稱 */ private static SortedMap<Integer, String> virtualNodes = new TreeMap<Integer, String>(); /** * 虛擬節點的數目,這裏寫死,爲了演示須要,一個真實結點對應5個虛擬節點 */ private static final int VIRTUAL_NODES = 5; static { // 先把原始的服務器添加到真實結點列表中 for (int i = 0; i < servers.length; i++) realNodes.add(servers[i]); // 再添加虛擬節點,遍歷LinkedList使用foreach循環效率會比較高 for (String str : realNodes) { for (int i = 0; i < VIRTUAL_NODES; i++) { String virtualNodeName = str + "&&VN" + String.valueOf(i); int hash = getHash(virtualNodeName); System.out.println("虛擬節點[" + virtualNodeName + "]被添加, hash值爲" + hash); virtualNodes.put(hash, virtualNodeName); } } System.out.println(); } /** * 獲得應當路由到的結點 */ private static String getServer(String node) { // 獲得帶路由的結點的Hash值 int hash = getHash(node); // 獲得大於該Hash值的全部Map SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash); // 第一個Key就是順時針過去離node最近的那個結點 Integer i = subMap.firstKey(); // 返回對應的虛擬節點名稱,這裏字符串稍微截取一下 String virtualNode = subMap.get(i); return virtualNode.substring(0, virtualNode.indexOf("&&")); } public static void main(String[] args) { String[] nodes = {"127.0.0.1:1111", "221.226.0.1:2222", "10.211.0.1:3333"}; for (int i = 0; i < nodes.length; i++) System.out.println("[" + nodes[i] + "]的hash值爲" + getHash(nodes[i]) + ", 被路由到結點[" + getServer(nodes[i]) + "]"); } }
Gossip 協議,顧名思義,就像流言蜚語同樣,利用一種隨機、帶有傳染性的方式,將信息 傳播到整個網絡中,並在必定時間內,使得系統內的全部節點數據一致。Gossip 協議經過上面的特性,能夠保證系統能在極端狀況下(好比集羣中只有一個節點在運行)也能運行。
Gossip數據傳播方式分別有:直接郵寄(Direct Mail)、反熵(Anti-entropy)和謠言傳播 (Rumor mongering)。
直接郵寄(Direct Mail):就是直接發送更新數據,當數據發送失敗時,將數據緩存下來,而後重傳。直接郵寄雖然實現起來比較容易,數據同步也很及時,但可能會由於 緩存隊列滿了而丟數據。也就是說,只採用直接郵寄是沒法實現最終一致性的。
反熵(Anti-entropy):反熵指的是集羣中的節點,每隔段時間就隨機選擇某個其餘節點,而後經過互相交換本身的 全部數據來消除二者之間的差別,實現數據的最終一致性。
在實現反熵的時候,主要有推、拉和推拉三種方式。推方式,就是將本身的全部副本數據,推給對方,修復對方副本中的熵,拉方式,就是拉取對方的全部副本數據,修復本身副本中的熵。
謠言傳播 (Rumor mongering):指的是當一個節點有了新數據後,這個節點變成活躍狀態,並週期性地聯繫其餘節點向其發送新數據,直到全部的節點都存儲了該新數據。因爲謠言傳播很是具備傳染性,它適合動態變化的分佈式系統
Quorum NWR 中有三個要素,N、W、R。
N 表示副本數,又叫作複製因子(Replication Factor)。也就是說,N 表示集羣中同一份 數據有多少個副本,就像下圖的樣子:
在這個三節點的集羣中,DATA-1 有 2 個副本,DATA-2 有 3 個副 本,DATA-3 有 1 個副本。也就是說,副本數能夠不等於節點數,不一樣的數據能夠有不一樣 的副本數。
W,又稱寫一致性級別(Write Consistency Level),表示成功完成 W 個副本更新。
R,又稱讀一致性級別(Read Consistency Level),表示讀取一個數據對象時須要讀 R 個副本。
經過 Quorum NWR,你能夠自定義一致性級別,經過臨時調整寫入或者查詢的方式,當 W + R > N 時,就能夠實現強一致性了。
因此假如要讀取節點B,咱們再假設W(2) + R(2) > N(3)這個公式,也就是當寫兩個節點,讀的時候也同時讀取兩個節點,那麼讀取數據的時候確定是讀取返回給客戶端確定是最新的那份數據。
關於 NWR 須要你注意的是,N、W、R 值的不一樣組合,會產生不一樣的一致性效 果,具體來講,有這麼兩種效果:
當 W + R > N 的時候,對於客戶端來說,整個系統能保證強一致性,必定能返回更新後的那份數據。
當 W + R < N 的時候,對於客戶端來說,整個系統只能保證最終一致性,可能會返回舊數據。
PBFT 算法很是實用,是一種能在實際場景中落地的拜占庭容錯算法。
咱們從一個例子入手,看看PBFT 算法的具體實現:
假設蘇秦再一次帶隊抗秦,這一天,蘇秦和 4 個國家的 4 位將軍趙、魏、韓、楚商量軍機 要事,結果剛商量完沒多久蘇秦就接到了情報,情報上寫道:聯軍中可能存在一個叛徒。這 時,蘇秦要如何下發做戰指令,保證忠將們正確、一致地執行下發的做戰指令,而不是被叛 徒干擾呢?
須要注意的是,全部的消息都是簽名消息,也就是說,消息發送者的身份和消息內容都是 沒法僞造和篡改的(好比,楚沒法僞造一個僞裝來自趙的消息)。
首先,蘇秦聯繫趙,向趙發送包含做戰指令「進攻」的請求(就像下圖的樣子)。
當趙接收到蘇秦的請求以後,會執行三階段協議(Three-phase protocol)。
趙將進入預準備(Pre-prepare)階段,構造包含做戰指令的預準備消息,並廣播給其餘 將軍(魏、韓、楚)。
由於魏、韓、楚,收到消息後,不能確認本身接收到指令和其餘人接收到的指令是相同的。因此須要進入下一個階段。
接收到預準備消息以後,魏、韓、楚將進入準備(Prepare)階段,並分別廣播包含做戰 指令的準備消息給其餘將軍。
好比,魏廣播準備消息給趙、韓、楚(如圖所示)。爲了 方便演示,咱們假設叛徒楚想經過不發送消息,來干擾共識協商(你能看到,圖中的楚 是沒有發送消息的)。
由於魏不能確認趙、韓、楚是否收到了 2f(這裏的 2f 包括本身,其中 f 爲叛徒數,在個人演示中是 1) 個一致的包含做戰指令的準備消 息。因此須要進入下一個階段Commit。
進入提交階段後,各將軍分別廣播提交消息給其餘將軍,也就是告訴其餘將軍,我已經準備好了,能夠執行指令了。
最後,當某個將軍收到 2f + 1 個驗證經過的提交消息後,大部分的將軍們已經達成共識,這時能夠執行做戰指 令了,那麼該將軍將執行蘇秦的做戰指令,執行完畢後發送執行成功的消息給蘇秦。
最後,當蘇秦收到 f+1 個相同的響應(Reply)消息時,說明各位將軍們已經就做戰指令達 成了共識,並執行了做戰指令。
在上面的這個例子中:
能夠將趙、魏、韓、楚理解爲分佈式系統的四個節點,其中趙是主節點(Primary node),魏、韓、楚是從節點(Secondary node);
將蘇秦理解爲業務,也就是客戶端;
將消息理解爲網絡消息;
將做戰指令「進攻」,理解成客戶端提議的值,也就是但願被各節點達成共識,並提交 給狀態機的值。
最終的共識是否達成,客戶端是會作判斷的,若是客戶端在指定時間內未 收到請求對應的 f + 1 相同響應,就認爲集羣出故障了,共識未達成,客戶端會從新發送請 求。
PBFT 算法經過視圖變動(View Change)的方式,來處理主節點做 惡,當發現主節點在做惡時,會以「輪流上崗」方式,推舉新的主節點。感興趣的能夠本身去查閱。
相比 Raft 算法徹底不適應有人做惡的場景,PBFT 算法能容忍 (n 1)/3 個惡意節點 (也能夠是故障節點)。另外,相比 PoW 算法,PBFT 的優勢是不消耗算 力。PBFT 算法是O(n ^ 2) 的消息複雜度的算法,因此以及隨着消息數 的增長,網絡時延對系統運行的影響也會越大,這些都限制了運行 PBFT 算法的分佈式系統 的規模,也決定了 PBFT 算法適用於中小型分佈式系統。
工做量證實 (Proof Of Work,簡稱 PoW),就是一份證實,用 來確認你作過必定量的工做。具體來講就是,客戶端須要作必定難度的工做才能得出一個結果,驗 證方卻很容易經過結果來檢查出客戶端是否是作了相應的工做。
具體的工做量證實過程,就像下圖中的樣子:
因此工做量證實一般用於區塊鏈中,區塊鏈經過工做量證實(Proof of Work)增長了壞人做惡的成本,以此防止壞 人做惡。
哈希函數(Hash Function),也叫散列函數。就是說,你輸入一個任意長度的字符串,哈 希函數會計算出一個長度相同的哈希值。
在瞭解了什麼是哈希函數以後,那麼如何經過哈希函數進行哈希運算,從而證實工做量呢?
例如,咱們能夠給出一個工做量的要求:基於一個基本的字符串,你能夠在這個字 符串後面添加一個整數值,而後對變動後(添加整數值) 的字符串進行 SHA256 哈希運 算,若是運算後獲得的哈希值(16 進制形式)是以"0000"開頭的,就驗證經過。
爲了達到 這個工做量證實的目標,咱們須要不停地遞增整數值,一個一個試,對獲得的新字符串進行 SHA256 哈希運算。
經過這個示例你能夠看到,工做量證實是經過執行哈希運算,通過一段時間的計算後,獲得 符合條件的哈希值。也就是說,能夠經過這個哈希值,來證實咱們的工做量。
首先看看什麼是區塊鏈:
區塊鏈的區塊,是由區塊頭、區塊體 2 部分組成的:
區塊頭(Block Head):區塊頭主要由上一個區塊的哈希值、區塊體的哈希值、4 字節 的隨機數(nonce)等組成的。
在區塊鏈中,擁有 80 字節固定長度的區塊頭,就是用於區塊鏈工做量證實的哈希運算中輸 入字符串,並且經過雙重 SHA256 哈希運算(也就是對 SHA256 哈希運算的結果,再執行 一次哈希運算),計算出的哈希值,只有小於目標值(target),纔是有效的,不然哈希值 是無效的,必須重算。
因此,在區塊鏈中是經過對區塊頭執行 SHA256 哈希運算,獲得小於目標 值的哈希值,來證實本身的工做量的。
計算出符合條件的哈希值後,礦工就會把這個信息廣播給集羣中全部其餘節點,其餘節點驗 證經過後,會將這個區塊加入到本身的區塊鏈中,最終造成一串區塊鏈,就像下圖的樣子:
因此,就是***者掌握了較多的算力,能挖掘一條比原鏈更長的***鏈,並將***鏈 向全網廣播,這時呢,按照約定,節點將接受更長的鏈,也就是***鏈,丟棄原鏈。就像下 圖的樣子:
Zab協議 的全稱是 Zookeeper Atomic Broadcast (Zookeeper原子廣播)。Zookeeper 是經過 Zab 協議來保證分佈式事務的最終一致性。ZAB 協議的最核心設計目標就是如何實現操做的順序性。
因爲ZAB不基於狀態機,而是基於主備模式的 原子廣播協議(Atomic Broadcast),最終實現了操做的順序性。
主要有如下幾點緣由致使了ZAB實現了操做的順序性:
首先,ZAB 實現了主備模式,也就是全部的數據都以主節點爲準:
其次,ZAB 實現了 FIFO 隊列,保證消息處理的順序性。
最後,ZAB 還實現了當主節點崩潰後,只有日誌最完備的節點才能當選主節點,由於日誌 最完備的節點包含了全部已經提交的日誌,因此這樣就能保證提交的日誌不會再改變。