自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

圖解一致性哈希算法

開發(fā) 前端 算法
要了解一致性哈希,首先我們必須了解傳統(tǒng)的哈希及其在大規(guī)模分布式系統(tǒng)中的局限性。

[[380706]]

 本文轉(zhuǎn)載自微信公眾號(hào)「全棧修仙之路」,作者阿寶哥   。轉(zhuǎn)載本文請(qǐng)聯(lián)系全棧修仙之路公眾號(hào)。

要了解一致性哈希,首先我們必須了解傳統(tǒng)的哈希及其在大規(guī)模分布式系統(tǒng)中的局限性。簡(jiǎn)單地說,哈希就是一個(gè)鍵值對(duì)存儲(chǔ),在給定鍵的情況下,可以非常高效地找到所關(guān)聯(lián)的值。假設(shè)我們要根據(jù)其郵政編碼查找城市中的街道名稱。一種最簡(jiǎn)單的實(shí)現(xiàn)方式是將此信息以哈希字典的形式進(jìn)行存儲(chǔ) 。

當(dāng)數(shù)據(jù)太大而無法存儲(chǔ)在一個(gè)節(jié)點(diǎn)或機(jī)器上時(shí),問題變得更加有趣,系統(tǒng)中需要多個(gè)這樣的節(jié)點(diǎn)或機(jī)器來存儲(chǔ)它。比如,使用多個(gè) Web 緩存中間件的系統(tǒng)。那如何確定哪個(gè) key 存儲(chǔ)在哪個(gè)節(jié)點(diǎn)上?針對(duì)該問題,最簡(jiǎn)單的解決方案是使用哈希取模來確定。 給定一個(gè) key,先對(duì) key 進(jìn)行哈希運(yùn)算,將其除以系統(tǒng)中的節(jié)點(diǎn)數(shù),然后將該 key 放入該節(jié)點(diǎn)。同樣,在獲取 key 時(shí),對(duì) key 進(jìn)行哈希運(yùn)算,再除以節(jié)點(diǎn)數(shù),然后轉(zhuǎn)到該節(jié)點(diǎn)并獲取值。上述過程對(duì)應(yīng)的哈希算法定義如下:

  1. node_number = hash(key) % N # 其中 N 為節(jié)點(diǎn)數(shù)。 

下圖描繪了多節(jié)點(diǎn)系統(tǒng)中的傳統(tǒng)的哈希取模算法,基于該算法可以實(shí)現(xiàn)簡(jiǎn)單的負(fù)載均衡。

一、傳統(tǒng)哈希取模算法的局限性

下面我們來分析一下傳統(tǒng)的哈希及其在大規(guī)模分布式系統(tǒng)中的局限性。這里我們直接使用我之前所寫文章 布隆過濾器你值得擁有的開發(fā)利器 中定義的 SimpleHash 類,然后分別對(duì) semlinker、kakuqo 和 test 3 個(gè)鍵進(jìn)行哈希運(yùn)算并取余,具體代碼如下:

  1. public class SimpleHash { 
  2.     private int cap; 
  3.     private int seed; 
  4.  
  5.     public SimpleHash(int cap, int seed) { 
  6.         this.cap = cap; 
  7.         this.seed = seed; 
  8.     } 
  9.  
  10.     public int hash(String value) { 
  11.         int result = 0; 
  12.         int len = value.length(); 
  13.         for (int i = 0; i < len; i++) { 
  14.             result = seed * result + value.charAt(i); 
  15.         } 
  16.         return (cap - 1) & result; 
  17.     } 
  18.  
  19.     public static void main(String[] args) { 
  20.         SimpleHash simpleHash = new SimpleHash(2 << 12, 8); 
  21.         System.out.println("node_number=hash(\"semlinker\") % 3 -> " +  
  22.           simpleHash.hash("semlinker") % 3); 
  23.         System.out.println("node_number=hash(\"kakuqo\") % 3 -> " +  
  24.           simpleHash.hash("kakuqo") % 3); 
  25.         System.out.println("node_number=hash(\"test\") % 3 -> " +  
  26.           simpleHash.hash("test") % 3); 
  27.     } 

以上代碼成功運(yùn)行后,在控制臺(tái)會(huì)輸出以下結(jié)果:

  1. node_number=hash("semlinker") % 3 -> 1 
  2. node_number=hash("kakuqo") % 3 -> 2 
  3. node_number=hash("test") % 3 -> 0 

基于以上的輸出結(jié)果,我們可以創(chuàng)建以下表格:

1.1 節(jié)點(diǎn)減少的場(chǎng)景

在分布式多節(jié)點(diǎn)系統(tǒng)中,出現(xiàn)故障很常見。任何節(jié)點(diǎn)都可能在沒有任何事先通知的情況下掛掉,針對(duì)這種情況我們期望系統(tǒng)只是出現(xiàn)性能降低,正常的功能不會(huì)受到影響。 對(duì)于原始示例,當(dāng)節(jié)點(diǎn)出現(xiàn)故障時(shí)會(huì)發(fā)生什么?原始示例中有的 3 個(gè)節(jié)點(diǎn),假設(shè)其中 1 個(gè)節(jié)點(diǎn)出現(xiàn)故障,這時(shí)節(jié)點(diǎn)數(shù)發(fā)生了變化,節(jié)點(diǎn)個(gè)數(shù)從 3 減少為 2,此時(shí)表格的狀態(tài)發(fā)生了變化:

很明顯節(jié)點(diǎn)的減少會(huì)導(dǎo)致鍵與節(jié)點(diǎn)的映射關(guān)系發(fā)生變化,這個(gè)變化對(duì)于新的鍵來說并不會(huì)產(chǎn)生任何影響,但對(duì)于已有的鍵來說,將導(dǎo)致節(jié)點(diǎn)映射錯(cuò)誤,以 “semlinker” 為例,變化前系統(tǒng)有 3 個(gè)節(jié)點(diǎn),該鍵對(duì)應(yīng)的節(jié)點(diǎn)編號(hào)為 1,當(dāng)出現(xiàn)故障時(shí),節(jié)點(diǎn)數(shù)減少為 2 個(gè),此時(shí)該鍵對(duì)應(yīng)的節(jié)點(diǎn)編號(hào)為 0。

1.2 節(jié)點(diǎn)增加的場(chǎng)景

在分布式多節(jié)點(diǎn)系統(tǒng)中,對(duì)于某些場(chǎng)景比如節(jié)日大促,就需要對(duì)服務(wù)節(jié)點(diǎn)進(jìn)行擴(kuò)容,以應(yīng)對(duì)突發(fā)的流量。 對(duì)于原始示例,當(dāng)增加節(jié)點(diǎn)會(huì)發(fā)生什么?原始示例中有的 3 個(gè)節(jié)點(diǎn),假設(shè)進(jìn)行擴(kuò)容臨時(shí)增加了 1 個(gè)節(jié)點(diǎn),這時(shí)節(jié)點(diǎn)數(shù)發(fā)生了變化,節(jié)點(diǎn)個(gè)數(shù)從 3 增加為 4 個(gè),此時(shí)表格的狀態(tài)發(fā)生了變化:

很明顯節(jié)點(diǎn)的增加也會(huì)導(dǎo)致鍵與節(jié)點(diǎn)的映射關(guān)系發(fā)生變化,這個(gè)變化對(duì)于新的鍵來說并不會(huì)產(chǎn)生任何影響,但對(duì)于已有的鍵來說,將導(dǎo)致節(jié)點(diǎn)映射錯(cuò)誤,同樣以 “semlinker” 為例,變化前系統(tǒng)有 3 個(gè)節(jié)點(diǎn),該鍵對(duì)應(yīng)的節(jié)點(diǎn)編號(hào)為 1,當(dāng)增加節(jié)點(diǎn)時(shí),節(jié)點(diǎn)數(shù)增加為 4 個(gè),此時(shí)該鍵對(duì)應(yīng)的節(jié)點(diǎn)編號(hào)為 2。

當(dāng)集群中節(jié)點(diǎn)的數(shù)量發(fā)生變化時(shí),之前的映射規(guī)則就可能發(fā)生變化。如果集群中每個(gè)機(jī)器提供的服務(wù)沒有差別,這不會(huì)有什么影響。但對(duì)于分布式緩存這種的系統(tǒng)而言,映射規(guī)則失效就意味著之前緩存的失效,若同一時(shí)刻出現(xiàn)大量的緩存失效,則可能會(huì)出現(xiàn) “緩存雪崩”,這將會(huì)造成災(zāi)難性的后果。

要解決此問題,我們必須在其余節(jié)點(diǎn)上重新分配所有現(xiàn)有鍵,這可能是非常昂貴的操作,并且可能對(duì)正在運(yùn)行的系統(tǒng)產(chǎn)生不利影響。當(dāng)然除了重新分配所有現(xiàn)有鍵的方案之外,還有另一種更好的方案即使用一致性哈希算法。

二、一致性哈希算法

一致性哈希算法在 1997 年由麻省理工學(xué)院提出,是一種特殊的哈希算法,在移除或者添加一個(gè)服務(wù)器時(shí),能夠盡可能小地改變已存在的服務(wù)請(qǐng)求與處理請(qǐng)求服務(wù)器之間的映射關(guān)系。一致性哈希解決了簡(jiǎn)單哈希算法在分布式哈希表(Distributed Hash Table,DHT)中存在的動(dòng)態(tài)伸縮等問題 。

2.1 一致性哈希算法優(yōu)點(diǎn)

  • 可擴(kuò)展性。一致性哈希算法保證了增加或減少服務(wù)器時(shí),數(shù)據(jù)存儲(chǔ)的改變最少,相比傳統(tǒng)哈希算法大大節(jié)省了數(shù)據(jù)移動(dòng)的開銷 。
  • 更好地適應(yīng)數(shù)據(jù)的快速增長(zhǎng)。采用一致性哈希算法分布數(shù)據(jù),當(dāng)數(shù)據(jù)不斷增長(zhǎng)時(shí),部分虛擬節(jié)點(diǎn)中可能包含很多數(shù)據(jù)、造成數(shù)據(jù)在虛擬節(jié)點(diǎn)上分布不均衡,此時(shí)可以將包含數(shù)據(jù)多的虛擬節(jié)點(diǎn)分裂,這種分裂僅僅是將原有的虛擬節(jié)點(diǎn)一分為二、不需要對(duì)全部的數(shù)據(jù)進(jìn)行重新哈希和劃分。

虛擬節(jié)點(diǎn)分裂后,如果物理服務(wù)器的負(fù)載仍然不均衡,只需在服務(wù)器之間調(diào)整部分虛擬節(jié)點(diǎn)的存儲(chǔ)分布。這樣可以隨數(shù)據(jù)的增長(zhǎng)而動(dòng)態(tài)的擴(kuò)展物理服務(wù)器的數(shù)量,且代價(jià)遠(yuǎn)比傳統(tǒng)哈希算法重新分布所有數(shù)據(jù)要小很多。

2.2 一致性哈希算法與哈希算法的關(guān)系

一致性哈希算法是在哈希算法基礎(chǔ)上提出的,在動(dòng)態(tài)變化的分布式環(huán)境中,哈希算法應(yīng)該滿足的幾個(gè)條件:平衡性、單調(diào)性和分散性。

  • 平衡性:是指 hash 的結(jié)果應(yīng)該平均分配到各個(gè)節(jié)點(diǎn),這樣從算法上解決了負(fù)載均衡問題。
  • 單調(diào)性:是指在新增或者刪減節(jié)點(diǎn)時(shí),不影響系統(tǒng)正常運(yùn)行。
  • 分散性:是指數(shù)據(jù)應(yīng)該分散地存放在分布式集群中的各個(gè)節(jié)點(diǎn)(節(jié)點(diǎn)自己可以有備份),不必每個(gè)節(jié)點(diǎn)都存儲(chǔ)所有的數(shù)據(jù)。

三、一致性哈希算法原理

一致性哈希算法通過一個(gè)叫作一致性哈希環(huán)的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)。這個(gè)環(huán)的起點(diǎn)是 0,終點(diǎn)是 2^32 - 1,并且起點(diǎn)與終點(diǎn)連接,故這個(gè)環(huán)的整數(shù)分布范圍是 [0, 2^32-1],如下圖所示:

3.1 將對(duì)象放置到哈希環(huán)

假設(shè)我們有 "semlinker"、"kakuqo"、"lolo"、"fer" 四個(gè)對(duì)象,分別簡(jiǎn)寫為 o1、o2、o3 和 o4,然后使用哈希函數(shù)計(jì)算這個(gè)對(duì)象的 hash 值,值的范圍是 [0, 2^32-1]:

圖中對(duì)象的映射關(guān)系如下:

  1. hash(o1) = k1; hash(o2) = k2; 
  2. hash(o3) = k3; hash(o4) = k4; 

3.2 將服務(wù)器放置到哈希環(huán)

接著使用同樣的哈希函數(shù),我們將服務(wù)器也放置到哈希環(huán)上,可以選擇服務(wù)器的 IP 或主機(jī)名作為鍵進(jìn)行哈希,這樣每臺(tái)服務(wù)器就能確定其在哈希環(huán)上的位置。這里假設(shè)我們有 3 臺(tái)緩存服務(wù)器,分別為 cs1、cs2 和 cs3:

圖中服務(wù)器的映射關(guān)系如下:

  1. hash(cs1) = t1; hash(cs2) = t2; hash(cs3) = t3; # Cache Server 

3.3 為對(duì)象選擇服務(wù)器

將對(duì)象和服務(wù)器都放置到同一個(gè)哈希環(huán)后,在哈希環(huán)上順時(shí)針查找距離這個(gè)對(duì)象的 hash 值最近的機(jī)器,即是這個(gè)對(duì)象所屬的機(jī)器。 以 o2 對(duì)象為例,順序針找到最近的機(jī)器是 cs2,故服務(wù)器 cs2 會(huì)緩存 o2 對(duì)象。而服務(wù)器 cs1 則緩存 o1,o3 對(duì)象,服務(wù)器 cs3 則緩存 o4 對(duì)象。

3.4 服務(wù)器增加的情況

假設(shè)由于業(yè)務(wù)需要,我們需要增加一臺(tái)服務(wù)器 cs4,經(jīng)過同樣的 hash 運(yùn)算,該服務(wù)器最終落于 t1 和 t2 服務(wù)器之間,具體如下圖所示:

圖片

對(duì)于上述的情況,只有 t1 和 t2 服務(wù)器之間的對(duì)象需要重新分配。在以上示例中只有 o3 對(duì)象需要重新分配,即它被重新到 cs4 服務(wù)器。在前面我們已經(jīng)分析過,如果使用簡(jiǎn)單的取模方法,當(dāng)新添加服務(wù)器時(shí)可能會(huì)導(dǎo)致大部分緩存失效,而使用一致性哈希算法后,這種情況得到了較大的改善,因?yàn)橹挥猩俨糠謱?duì)象需要重新分配。

3.5 服務(wù)器減少的情況

假設(shè) cs3 服務(wù)器出現(xiàn)故障導(dǎo)致服務(wù)下線,這時(shí)原本存儲(chǔ)于 cs3 服務(wù)器的對(duì)象 o4,需要被重新分配至 cs2 服務(wù)器,其它對(duì)象仍存儲(chǔ)在原有的機(jī)器上。

3.6 虛擬節(jié)點(diǎn)

到這里一致性哈希的基本原理已經(jīng)介紹完了,但對(duì)于新增服務(wù)器的情況還存在一些問題。新增的服務(wù)器 cs4 只分擔(dān)了 cs1 服務(wù)器的負(fù)載,服務(wù)器 cs2 和 cs3 并沒有因?yàn)?cs4 服務(wù)器的加入而減少負(fù)載壓力。如果 cs4 服務(wù)器的性能與原有服務(wù)器的性能一致甚至可能更高,那么這種結(jié)果并不是我們所期望的。

針對(duì)這個(gè)問題,我們可以通過引入虛擬節(jié)點(diǎn)來解決負(fù)載不均衡的問題。即將每臺(tái)物理服務(wù)器虛擬為一組虛擬服務(wù)器,將虛擬服務(wù)器放置到哈希環(huán)上,如果要確定對(duì)象的服務(wù)器,需先確定對(duì)象的虛擬服務(wù)器,再由虛擬服務(wù)器確定物理服務(wù)器。

圖中 o1 和 o2 表示對(duì)象,v1 ~ v6 表示虛擬服務(wù)器,s1 ~ s3 表示物理服務(wù)器。

四、一致性哈希算法實(shí)現(xiàn)

這里我們只介紹不帶虛擬節(jié)點(diǎn)的一致性哈希算法實(shí)現(xiàn):

  1. import java.util.SortedMap; 
  2. import java.util.TreeMap; 
  3.  
  4. public class ConsistentHashingWithoutVirtualNode { 
  5.     //待添加入Hash環(huán)的服務(wù)器列表 
  6.     private static String[] servers = {"192.168.0.1:8888""192.168.0.2:8888",  
  7.       "192.168.0.3:8888"}; 
  8.  
  9.     //key表示服務(wù)器的hash值,value表示服務(wù)器 
  10.     private static SortedMap<Integer, String> sortedMap = new TreeMap<Integer, String>(); 
  11.  
  12.     //程序初始化,將所有的服務(wù)器放入sortedMap中 
  13.     static { 
  14.         for (int i = 0; i < servers.length; i++) { 
  15.             int hash = getHash(servers[i]); 
  16.             System.out.println("[" + servers[i] + "]加入集合中, 其Hash值為" + hash); 
  17.             sortedMap.put(hash, servers[i]); 
  18.         } 
  19.     } 
  20.  
  21.     //得到應(yīng)當(dāng)路由到的結(jié)點(diǎn) 
  22.     private static String getServer(String key) { 
  23.         //得到該key的hash值 
  24.         int hash = getHash(key); 
  25.         //得到大于該Hash值的所有Map 
  26.         SortedMap<Integer, String> subMap = sortedMap.tailMap(hash); 
  27.         if (subMap.isEmpty()) { 
  28.             //如果沒有比該key的hash值大的,則從第一個(gè)node開始 
  29.             Integer i = sortedMap.firstKey(); 
  30.             //返回對(duì)應(yīng)的服務(wù)器 
  31.             return sortedMap.get(i); 
  32.         } else { 
  33.             //第一個(gè)Key就是順時(shí)針過去離node最近的那個(gè)結(jié)點(diǎn) 
  34.             Integer i = subMap.firstKey(); 
  35.             //返回對(duì)應(yīng)的服務(wù)器 
  36.             return subMap.get(i); 
  37.         } 
  38.     } 
  39.  
  40.     //使用FNV1_32_HASH算法計(jì)算服務(wù)器的Hash值 
  41.     private static int getHash(String str) { 
  42.         final int p = 16777619; 
  43.         int hash = (int) 2166136261L; 
  44.         for (int i = 0; i < str.length(); i++) 
  45.             hash = (hash ^ str.charAt(i)) * p; 
  46.         hash += hash << 13; 
  47.         hash ^= hash >> 7; 
  48.         hash += hash << 3; 
  49.         hash ^= hash >> 17; 
  50.         hash += hash << 5; 
  51.  
  52.         // 如果算出來的值為負(fù)數(shù)則取其絕對(duì)值 
  53.         if (hash < 0) 
  54.             hash = Math.abs(hash); 
  55.         return hash; 
  56.     } 
  57.  
  58.     public static void main(String[] args) { 
  59.         String[] keys = {"semlinker""kakuqo""fer"}; 
  60.         for (int i = 0; i < keys.length; i++) 
  61.             System.out.println("[" + keys[i] + "]的hash值為" + getHash(keys[i]) 
  62.                     + ", 被路由到結(jié)點(diǎn)[" + getServer(keys[i]) + "]"); 
  63.     } 
  64.  

以上代碼成功運(yùn)行后,在控制臺(tái)會(huì)輸出以下結(jié)果:

  1. [192.168.0.1:8888]加入集合中, 其Hash值為1326271016 
  2. [192.168.0.2:8888]加入集合中, 其Hash值為1132535844 
  3. [192.168.0.3:8888]加入集合中, 其Hash值為115798597 
  4.  
  5. [semlinker]的hash值為1549041406, 被路由到結(jié)點(diǎn)[192.168.0.3:8888] 
  6. [kakuqo]的hash值為463104755, 被路由到結(jié)點(diǎn)[192.168.0.2:8888] 
  7. [fer]的hash值為1677150790, 被路由到結(jié)點(diǎn)[192.168.0.3:8888] 

上面我們只介紹了不帶虛擬節(jié)點(diǎn)的一致性哈希算法實(shí)現(xiàn),如果有的小伙伴對(duì)帶虛擬節(jié)點(diǎn)的一致性哈希算法感興趣,可以參考 一致性Hash(Consistent Hashing)原理剖析及Java實(shí)現(xiàn) 這篇文章。

五、總結(jié)

本文通過示例介紹了傳統(tǒng)的哈希取模算法在分布式系統(tǒng)中的局限性,進(jìn)而在針對(duì)該問題的解決方案中引出了一致性哈希算法。一致性哈希算法在 1997 年由麻省理工學(xué)院提出,是一種特殊的哈希算法,在移除或者添加一個(gè)服務(wù)器時(shí),能夠盡可能小地改變已存在的服務(wù)請(qǐng)求與處理請(qǐng)求服務(wù)器之間的映射關(guān)系。在介紹完一致性哈希算法的作用和優(yōu)點(diǎn)等相關(guān)知識(shí)后,我們以圖解的形式生動(dòng)介紹了一致性哈希算法的原理,最后給出了不帶虛擬節(jié)點(diǎn)的一致性哈希算法的 Java 實(shí)現(xiàn)。

六、參考資源

百度百科 - 一致性哈希

知乎 - 面試必備:什么是一致性 Hash 算法

Leo - 一致性Hash-Consistent-Hashing 原理剖析

CSDN - 一致性Hash(Consistent Hashing)原理剖析及Java實(shí)現(xiàn)

Codeproject - consistent-hashing

 

責(zé)任編輯:武曉燕 來源: 全棧修仙之路
相關(guān)推薦

2020-07-20 08:30:37

算法哈希分布式系統(tǒng)

2016-12-19 18:41:09

哈希算法Java數(shù)據(jù)

2021-07-27 08:57:10

算法一致性哈希哈希算法

2019-10-11 23:27:19

分布式一致性算法開發(fā)

2021-02-02 12:40:50

哈希算法數(shù)據(jù)

2018-07-05 09:41:08

一致性哈希算法

2019-11-01 09:13:37

算法哈希緩存

2023-12-12 08:00:50

節(jié)點(diǎn)哈希算法

2021-06-30 21:13:49

CPUCache數(shù)據(jù)

2021-09-15 07:46:42

哈希一致性哈希算法

2023-06-25 09:44:00

一致性哈希數(shù)據(jù)庫

2023-06-26 07:17:48

負(fù)載均衡策略Dubbo

2022-03-22 09:54:22

Hash算法

2023-12-20 08:11:02

Redis節(jié)點(diǎn)通信

2023-12-05 14:44:01

2017-07-25 14:38:56

數(shù)據(jù)庫一致性非鎖定讀一致性鎖定讀

2021-11-12 08:38:26

一致性哈希算法數(shù)據(jù)結(jié)構(gòu)

2022-01-27 08:31:20

一致性哈希

2022-11-10 07:49:09

hash算法代碼

2020-03-16 11:55:28

PaxosRaft協(xié)議
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)