【數據結構與算法】一致性Hash算法及Java實踐

  追求極致才能突破極限算法

1、案例背景

1.1 系統簡介

  首先看一下系統架構,方便解釋:緩存

  頁面給用戶展現的功能就是,能夠查看任何一臺機器的某些屬性(如下簡稱系統信息)。架構

  消息流程是,頁面發起請求查看指定機器的系統信息到後臺,後臺能夠查詢到有哪些server在提供服務,根據負載均衡算法(簡單的輪詢)指定由哪一個server進行查詢,並將消息發送到Kafka,而後全部的server消費Kafka的信息,當發現消費的信息要求本身進行查詢時,就鏈接指定的machine進行查詢,並將結果返回回去。負載均衡

  Server是集羣架構,可能動態增長或減小。分佈式

  至於架構爲何這麼設計,不是重點,只能說這是符合當時環境的最優架構。性能

1.2 遇到問題

  遇到的問題就是慢,特別慢,通過初步覈實,最耗時的事是server鏈接machine的時候,基本都要5s左右,這是不能接受的。學習

1.3 初步優化

  由於耗時最大的是server鏈接machine的時候,因此決定在server端緩存machine的鏈接,通過測試若是經過使用的鏈接緩存進行查詢,那麼耗時將控制在1秒之內,知足了用戶的要求,不過還有一個問題所以產生,那就是根據現有負載均衡算法,假如server1已經緩存了到machine1的鏈接,可是再次查詢時,請求就會發送到下一個server,如server2,這就致使了兩個問題,一是,從新創建了鏈接耗時較長,二是,兩個server同時緩存着到machine1的鏈接,形成了鏈接浪費。測試

1.4 繼續優化

  一開始想到最簡單的就是將查詢的machine進行hash計算,併除sever的數量取餘,這樣保證了查詢同一個machine時會要求同一個server進行操做,知足了初步的需求。可是由於server端是集羣,機器有可能動態的增長或減小,假如根據hash計算,指定的 machine會被指定的server鏈接,以下圖:優化

  而後又增長了一個server,那麼根據當前的hash算法,server和machine的鏈接就會變成以下:編碼

 

  能夠發現,四個machine和server的鏈接關係發生變化了,這將致使4次鏈接的初始化,以及四個鏈接的浪費,雖然server集羣變更的概率很小,可是每變更一次將有一半的鏈接做廢掉,這仍是不能接受的,當時想的最理想的結果是:

  • 當新增機器的時候,原有的鏈接分一部分給新機器,可是除去分出的鏈接之外保持不變
  • 當減小機器的時候,將減小機器的鏈接分給剩下的機器,但剩下機器的原有鏈接不變

  簡單來講,就是變更不可避免可是讓變更最小化。根據這種思想,就想到了一致性hash,以爲這個應該能夠知足要求。

2、使用一致性Hash解決問題

  一致性Hash的定義或者介紹在第三節,如今寫出一致性Hash的Java的解決方法。只寫出示例實現代碼,首先最重要的就是Hash算法的選擇,根據現有狀況以及已有Hash算法的表現,選擇了FNV Hash算法,如下是其實現:

public static int FnvHash(String key) {
  final int p = 16777619;
  long hash = (int) 2166136261L;
  for (int i = 0,n = key.length(); i < n; i++){
    hash = (hash ^ key.charAt(i)) * p;
  }
  hash += hash << 13;
  hash ^= hash >> 7;
  hash += hash << 3;
  hash ^= hash >> 17;
  hash += hash << 5;
  return ((int) hash & 0x7FFFFFFF);
}

  而後是對能提供服務的server進行預處理:

public static ConcurrentSkipListMap<Integer, String> init(){
  //建立排序Map方便後面的計算
  ConcurrentSkipListMap<Integer,String> servers=new ConcurrentSkipListMap<>();
  //得到能夠提供服務的server
  List<String> serverUrls=Arrays.asList("192.168.2.1:8080","192.168.2.2:8080","192.168.2.3:8080");
  //將server依次添加到Map中
  for(String serverUrl:serverUrls){
    servers.put(FnvHash(serverUrl), serverUrl);
    //如下三個是當前server的三個虛擬節點,Hash不一樣
    servers.put(FnvHash(serverUrl+"#1"), serverUrl);
    servers.put(FnvHash(serverUrl+"#2"), serverUrl);
    servers.put(FnvHash(serverUrl+"#3"), serverUrl);
  }
  return servers;
}

  這段代碼將能提供的server放入排序Map,鍵爲其Hash值,值爲server的主機和IP,接下來就要對每個請求的要鏈接的machin計算須要哪個server進行鏈接:

/**
 * @param machine 要鏈接的機器
 * @param servers 可提供服務的server
 * @return
 */
private static String getServer(int machine, ConcurrentSkipListMap<Integer, String> servers) {
  int left=Integer.MAX_VALUE;
  int right=Integer.MAX_VALUE;
  int leftDis=0;
  int rightDis=0;
  for(Entry<Integer, String> server:servers.entrySet()){
    int key=server.getKey();
    if(key<machine){
      left=key;
    }else{
      right=key;
    }
    if(right!=Integer.MAX_VALUE){
      break;
    }
  }
  if(left==Integer.MAX_VALUE){
    left=servers.lastKey();
    leftDis=Integer.MAX_VALUE-left+machine;
  }else{
    leftDis=machine-left;
  }
  if(right==Integer.MAX_VALUE){
    right=servers.firstKey();
    rightDis=Integer.MAX_VALUE-machine+right;
  }else{
    rightDis=right-machine;
  }
  return servers.get(rightDis<=leftDis?right:left);
}

  這個方法就是計算,具體邏輯能夠在看完下一節有更深的瞭解。

  通過上面的三個方法就解決了上面提出的要求,通過測試也完美,或許算法還看不懂,也或許一致Hash算法還不知道是什麼,虛擬節點是什麼,可是如今應該瞭解需求是怎麼產生的,已經經過什麼知足了要求,如今惟一要作的就是了解一致性Hash了,下面進行介紹。

3、一致性Hash介紹

3.1 理論簡介

  一致性Hash的簡介,摘自百度百科。

  一致性哈希算法在1997年由麻省理工學院提出,設計目標是爲了解決因特網中的熱點(Hot spot)問題。一致性哈希提出了在動態變化的Cache環境中,哈希算法應該知足的4個適應條件:

均衡性(Balance):

  平衡性是指哈希的結果可以儘量分佈到全部的緩衝中去,這樣可使得全部的緩衝空間都獲得利用。不少哈希算法都可以知足這一條件。
單調性(Monotonicity):

  單調性是指若是已經有一些內容經過哈希分派到了相應的緩衝中,又有新的緩衝區加入到系統中,那麼哈希的結果應可以保證原有已分配的內容能夠被映射到新的緩衝區中去,而不會被映射到舊的緩衝集合中的其餘緩衝區。(這段翻譯信息有負面價值的,當緩衝區大小變化時一致性哈希(Consistent hashing)儘可能保護已分配的內容不會被從新映射到新緩衝區。)

分散性(Spread):

  在分佈式環境中,終端有可能看不到全部的緩衝,而是隻能看到其中的一部分。當終端但願經過哈希過程將內容映射到緩衝上時,因爲不一樣終端所見的緩衝範圍有可能不一樣,從而致使哈希的結果不一致,最終的結果是相同的內容被不一樣的終端映射到不一樣的緩衝區中。這種狀況顯然是應該避免的,由於它致使相同內容被存儲到不一樣緩衝中去,下降了系統存儲的效率。分散性的定義就是上述狀況發生的嚴重程度。好的哈希算法應可以儘可能避免不一致的狀況發生,也就是儘可能下降分散性。
負載(Load):

  負載問題其實是從另外一個角度看待分散性問題。既然不一樣的終端可能將相同的內容映射到不一樣的緩衝區中,那麼對於一個特定的緩衝區而言,也可能被不一樣的用戶映射爲不一樣的內容。與分散性同樣,這種狀況也是應當避免的,所以好的哈希算法應可以儘可能下降緩衝的負荷。

3.2 設計實現

  通常的一致性Hash的設計實現都是按照以下方式:

  首先全部的Hash值應該構成一個環,就像鐘錶的時刻同樣,也就是說有明確的Hash最大值,環內Hash的數量通常爲2的32次方:

  將server經過Hash計算映射到環上,注意選取能區別開server的惟一屬性,好比ip加端口:

  而後全部的把全部的請求使用惟一的屬性計算Hash值,而後請求到最近的server上面:

  假若有新機器加入時:

 

  新機器相鄰的請求會被從新定向到新的server,若是有機器掛掉的話,掛掉機器的請求也會從新分配給就近的server:

  經過上面的圖例講解,應該能夠看出環形設計的好處,那就是無論新增仍是減小機器,變更的都是變更機器附近的請求,已有請求的映射不會變更到已有的節點上。 

4、對一致性Hash的理解

4.1 應用場景

  經過一致性Hash的特性來看,一致性Hash極力保證變更的最小化,比較適用於有狀態鏈接,若是鏈接是無狀態的,那麼徹底不必使用這種算法,輪詢或者隨機都是能夠的。效率要比一致性Hash高,省去了每一次請求的計算過程。

4.2 環的Hash數量的選擇

  本質上沒有特殊的要求,選取的原則能夠考慮如下幾點:

  1. Hash數量最好最夠多,由於要考慮將來新增server的狀況,以及虛擬節點的添加
  2. Hash數量的最大值在int範圍內便可,int最大值已經足夠大,大於int的會相對增長計算和存儲成本
  3. Hash數量的最大值的另外一個參考要點,就是選取Hash算法的最大值

  因此上面的例子,環Hash數量選擇了2^32,剛好fnv Hash算法的最大值也是它,FNV Hash算法參照此

4.3 虛擬節點的做用

  看過上面代碼的應該知道,對server進行Hash的時候,會同時建立server的幾個虛擬節點,它們一樣表明着它們的server,有以下做用:

  1. 防止server的Hash重複,雖然Hash重複的機率少之又少,可是依然不能徹底避免,因此經過使用多個虛擬節點,能夠避免因server的Hash重複致使server被徹底覆蓋掉
  2. 有利於負載均衡,若是每一個server只有一個節點,那麼有可能分佈的不均勻,這時候經過多個虛擬節點,能夠增長均勻分佈的可能性,固然這依賴於Hash算法的選擇

  至於虛擬節點的數量,這個沒有硬性要求,節點的數量越多,負載均衡越好,可是計算量也越大,若是考慮到server集羣的易變性,每一次請求都須要從新計算server及其虛擬節點的Hash值,那麼節點的數量不要太大,否則也是一個性能的瓶頸。

4.4 Hash算法的選擇

  Hash算法有不少種,上面fnv hash的能夠參考一下,至於其餘的,考慮如下幾點就能夠:

  • 不要本身寫Hash算法,用已有的就能夠,出於學習的目的能夠寫,生產環境用已有的Hash算法
  • 算法速度必定要快
  • 同一個輸入的值,要有相同的輸出
  • Hash值足夠散列,Hash碰撞機率低

  考慮以上幾點就能夠了,後續會針對Hash算法,寫一篇博客。

4.5 一致性Hash的替代

  不用一致Hash可不能夠,能不能知足相同的需求,答案是能夠的,那就是主動維護一個路由表。基本要作如下操做:

  1. 首先得到當前提供服務的server
  2. 當有請求來臨時,先判斷當前請求是否已有對應的server,如有交由對應的server,若無,選擇負載最低的一個server,並存記錄
  3. 當server掛掉之後,新的請求從新走2步驟
  4. 當有新的server加入時,能夠主動負載均衡,也能夠從新走2步驟

  優缺點簡單說一下:

優勢:

    • 負載更加均衡,甚至能夠保證徹底的均衡,由於不依賴Hash的不肯定性
    • 整個分配過程人爲掌握,當某些請求必須分配到指定的server上,修改更簡單

缺點:

    • 編碼量大,須要嚴格測試
    • 須要主動維護一個路由表,存儲是一個須要考慮的問題
    • 請求量大時,路由表容量會增大,能夠考慮存入Redis中

 

  以上就是我對一致Hash的理解,以及我在項目中的應用,但願能夠幫助到有須要的人。

相關文章
相關標籤/搜索