追求极致才能突破极限

一、案例背景

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的理解,以及我在项目中的应用,希望可以帮助到有需要的人。

【数据结构与算法】一致性Hash算法及Java实践的更多相关文章

  1. 《算法 - 一致性 (hash) 算法》

    图片摘自: 每天进步一点点——五分钟理解一致性哈希算法(consistent hashing) 一:背景 - 一致性哈希算法在1997年由麻省理工学院的Karger等人在解决分布式Cache中提出的. ...

  2. 对一致性Hash算法,Java代码实现的深入研究

    一致性Hash算法 关于一致性Hash算法,在我之前的博文中已经有多次提到了,MemCache超详细解读一文中"一致性Hash算法"部分,对于为什么要使用一致性Hash算法.一致性 ...

  3. Java实现一致性Hash算法深入研究

    一致性Hash算法 关于一致性Hash算法,在我之前的博文中已经有多次提到了,MemCache超详细解读一文中”一致性Hash算法”部分,对于为什么要使用一致性Hash算法和一致性Hash算法的算法原 ...

  4. 对一致性Hash算法及java实现(转)

    一致性Hash算法 关于一致性Hash算法,在我之前的博文中已经有多次提到了,MemCache超详细解读一文中"一致性Hash算法"部分,对于为什么要使用一致性Hash算法.一致性 ...

  5. 对一致性Hash算法,Java代码实现的深入研究(转)

    转载:http://www.cnblogs.com/xrq730/p/5186728.html 一致性Hash算法 关于一致性Hash算法,在我之前的博文中已经有多次提到了,MemCache超详细解读 ...

  6. 【转载】对一致性Hash算法,Java代码实现的深入研究

    原文地址:http://www.cnblogs.com/xrq730/p/5186728.html 一致性Hash算法 关于一致性Hash算法,在我之前的博文中已经有多次提到了,MemCache超详细 ...

  7. 一致性Hash算法与代码实现

    一致性Hash算法: 先构造一个长度为232的整数环(这个环被称为一致性Hash环),根据节点名称的Hash值(其分布为[0, 232-1])将服务器节点放置在这个Hash环上,然后根据数据的Key值 ...

  8. 大数据 --> 一致性Hash算法

    一致性Hash算法 一致性Hash算法(Consistent Hash)

  9. Nginx+Memcache+一致性hash算法 实现页面分布式缓存(转)

    网站响应速度优化包括集群架构中很多方面的瓶颈因素,这里所说的将页面静态化.实现分布式高速缓存就是其中的一个很好的解决方案... 1)先来看看Nginx负载均衡 Nginx负载均衡依赖自带的 ngx_h ...

  10. 一致性Hash算法的原理与实现(分布式映射算法)

    一致性Hash算法解决的问题: 解决分布式系统中的负载均衡问题 背景问题:有N台服务器提供缓存服务,需要对服务器进行负载均衡,将请求平均发到每台服务器上,每台服务器负载1/N的服务 硬Hash映射:将 ...

随机推荐

  1. python基础--异常,对象和迭代器

    异常处理 面向对象 迭代器和生成器 python异常处理 下面代码触发了一个FileNotFoundError >>> open("notexist.txt") ...

  2. Redis学习-复制

    Redis支持简单且易用的主从复制(master-slave replication)功能, 该功能可以让从服务器(slave server)成为主服务器(master server)的精确复制品.以 ...

  3. [转]GET,POST,PUT,DELETE的区别

    原文链接:http://blog.csdn.net/mfe10714022/article/details/39692305 Http定义了与服务器交互的不同方法,最基本的方法有4种,分别是GET,P ...

  4. (one) 条件判断的总结

    第一:if语句的一般形式: if(expression)   statement1; statement2; 对于条件判断,我觉得要点在于“条件”(expression),它是一个结果为false或t ...

  5. 【shell脚本实例】一个恶作剧—— kill掉占用CPU较高的matlab进程

    我们实验室有台服务器,博士们在服务器上跑MATLAB,基本都是4核都是超过95%的CPU占用,想了个恶作剧的shell 定时kill掉MATLAB程序,是不是很邪恶啊,哈哈~~~  不过我只是干过一次 ...

  6. esclipse连接mysql数据库

    怎样在eclipse开发环境中连接数据库并测试连接是否成功 1)eclipse开发环境里没有集成mysql的驱动,需要从以下地址下载连接驱动程序mysql-connector-java-XX-XX-X ...

  7. JQuery中参数e,event

    与Flex类似,JavaScript中的事件也同样存在,捕获--触发--冒泡 三个节点.比较常见的情况是,在子DIV触发事件时,如果父DIV也监听同类事件,那么也会一起触发,并向上冒泡 jQuery对 ...

  8. itmacy_我的博客

    开通博客的第一天,并不希望自己以后像写流水账一样来写自己的博客,而是希望每一篇博客,无论是转载还是原创,都是经过深思熟虑,并且有意义的...

  9. 一个web应用的诞生(13)--冲向云端

    有句话叫所有的乐趣都在部署之前,也许这个小应用还有很多缺陷,也许它还不够完美,但是,仔细想想,其实没有什么能比自己的网站在互联网中上线更令人满足的了,但是满足的背后,总是存在着很多的风险,以至于几乎所 ...

  10. 基于NopCommerce的开发框架——缓存、网站设置、系统日志、用户操作日志

    最近忙于学车,抽时间将Nop的一些公用模块添加进来,反应的一些小问题也做了修复.另外有园友指出Nop内存消耗大,作为一个开源电商项目,性能方面不是该团队首要考虑的,开发容易,稳定,代码结构清晰简洁也是 ...