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


  追求極致才能突破極限

一、案例背景

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,覺得這個應該可以滿足要求。

二、使用一致性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了,下面進行介紹。

三、一致性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:

  通過上面的圖例講解,應該可以看出環形設計的好處,那就是不管新增還是減少機器,變動的都是變動機器附近的請求,已有請求的映射不會變動到已有的節點上。 

四、對一致性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的理解,以及我在項目中的應用,希望可以幫助到有需要的人。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM