【数据结构与算法】一致性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的理解,以及我在项目中的应用,但愿能够帮助到有须要的人。

相关文章
相关标签/搜索