⼀致性Hash算法
Hash算法,比如说在安全加密领域MD5、SHA等加密算法,在数据存储和查找方面有Hash表等,以上都应用到了Hash算法。
为什么需要使用Hash?
Hash算法较多的应用在数据存储和查找领域,最经典的就是Hash表,它的查询效率非常之高,其中的hash算法如果设计的比较OK的话,那么Hash表的数据查询复杂度可以接近于O(1)。
实例:从一组数据1,5,7,6,3,4,8,,判断一个数据是否存在于数据集中。
**顺序查找法:**通过循环来完成查找,比较原始,效率也不高
**二分查找:**排序之后折半查找,相对于顺序查找法会提高一些效率,但是效率也并不是特别好。
我们来说一些特殊的方式。
定义一个数组,数组长度等于数据最大值+1,此处长度为9,数据1就存储在下标为1的位置,3就存储在下标为3的位置,,,以此类推。
此时我们查找一个数是否存在只需要一次查找就可以,通过下标通过一次查找判断数据是否为空就能达到目的,时间复杂度为O(1)。
这种方式叫做“直接寻址法”:直接把数据和数组的下标绑定到一起,查找的时候,直接array[n]就取出了数据
**优点:**速度快,一次查找得到结果
缺点:
- 浪费空间,比如1,2,3,7890,最大值为7890,按照上述方式定义一个长度为7891的数组,存在大量的空间浪费。
- 数据如下:1,5,7,6,3,4,8,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2最⼤值12,⽐如开辟13个空间,存储 不了这么多内容
现在,换⼀种设计,如果数据是3,5,7,12306,⼀共4个数据,我们开辟任意个空间,⽐如5个,那 么具体数据存储到哪个位置呢,我们可以对数据进⾏求模(对空间位置数5),根据求模余数确定存储 位置的下标,⽐如3%5=3,就可以把3这个数据放到下标为3的位置上,12306%5=1,就把12306这个 数据存储到下标为1的位置上
上⾯对数据求模 (数据%空间位置数) 他就是⼀个hash算法,只不过这是⼀种⽐较普通⼜简单的hash 算法,这种构造Hash算法的⽅式叫做除留余数法
如果数据是1,6,7,8,把这4个数据存储到上⾯的数组中
在此基础上采⽤开放寻址法(了解)
**开放寻址法:**1放进去了,6再来的时候,向前或者向后找空闲位置存放,不好的地⽅,如果数组⻓度定 义好了⽐如10,⻓度不能扩展,来了11个数据,不管Hash冲突不冲突,肯定存不下这么多数据
**拉链法:**数据⻓度定义好了,怎么存储更多内容呢,算好Hash值,在数组元素存储位置放了⼀个链表
如果Hash算法设计的⽐较好的话,那么查询效率会更接近于O(1),如果Hash算法设计的⽐较low,那么 查询效率就会很低了
所以,Hash表的查询效率⾼不⾼取决于Hash算法,hash算法能够让数据平均分布,既能够节省空间⼜ 能提⾼查询效率。Hash算法的研究是很深的⼀⻔学问,⽐较复杂,⻓久以来,Hash表内部的Hash算法 也⼀直在更新,很多数学家也在研究。
除留余数法 3%5
线性构造Hash算法
直接寻址法也是⼀种构造Hash的⽅式,只不过更简单,表达式:H(key)=key
线性构造⽐如H(key)=a*key + b(a,b是常量)
1.Hash算法应用场景
Hash算法在分布式集群架构中的应⽤场景
Hash算法在很多分布式集群产品中都有应⽤,⽐如分布式集群架构Redis、Hadoop、ElasticSearch, Mysql分库分表,Nginx负载均衡等
主要的应⽤场景归纳起来两个
-
请求的负载均衡(⽐如nginx的ip_hash策略)
Nginx的IP_hash策略可以在客户端ip不变的情况下,将其发出的请求始终路由到同⼀个⽬标服务 器上,实现会话粘滞,避免处理session共享问题
如果没有IP_hash策略,那么如何实现会话粘滞? 可以维护⼀张映射表,存储客户端IP或者sessionid与具体⽬标服务器的映射关系
<ip,tomcat1>
缺点
- 在客户端很多的情况下,映射表⾮常⼤,浪费内存空间
- 客户端上下线,⽬标服务器上下线,都会导致重新维护映射表,映射表维护成本很⼤
如果使⽤哈希算法,事情就简单很多,我们可以对ip地址或者sessionid进⾏计算哈希值,哈希值与服务 器数量进⾏取模运算,得到的值就是当前请求应该被路由到的服务器编号,如此,同⼀个客户端ip发送 过来的请求就可以路由到同⼀个⽬标服务器,实现会话粘滞。
-
分布式存储
以分布式内存数据库Redis为例,集群中有redis1,redis2,redis3 三台Redis服务器
那么,在进⾏数据存储时,数据存储到哪个服务器当中呢?针对key进⾏hash处理 hash(key1)%3=index, 使⽤余数index锁定存储的具体服务器节点
2.普遍Hash算法存在的问题
普遍Hash算法存在一个问题,以ip_hash为例,假定下载用户ip固定没有发生改变,现在Tomcat3出现了问题,down机了,服务器的数量由3个变为了2个,之前所有的求模都需要重新计算。
如果 在真实生成情况下,后台服务器很多台,客户端也有很多,那么影响是很大的,缩容和扩容都会存在这样的问题,大量用户的请求会被路由到其他的目标服务器处理,用户在原来服务器中的会话都会丢失。
3. 一致性Hash算法
一致性Hash算法思路如下
首先一条直线,直线开头和结尾分别定位1和2的32次方减一,这相当于一个地址,对于这样一条线,弯过来构成一个圆环形成闭环,这样一个圆环称之为hash环。我们把服务器的ip或者主机名求hash值然后对应到hash环上,那么针对客户端用户,也根据它的ip进行hash求值,对应到环上某个位置,然后按照顺时针方向找最近的服务器节点。
假如服务器3下线,服务器3下线后,原来路由到3的客户端重新路由到服务器4,对于其他客户端没有影响知识这一小部分受影响(请求的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量的请求迁移)
增加服务器5之后,原来路由到3的部分客户端路由到新增服务器5上,对于其他客户端没有影响只是这 ⼀⼩部分受影响(请求的迁移达到了最⼩,这样的算法对分布式集群来说⾮常合适的,避免了⼤量请求 迁移 )
1)如前所述,每⼀台服务器负责⼀段,⼀致性哈希算法对于节点的增减都只需重定位环空间中的⼀⼩ 部分数据,具有较好的容错性和可扩展性。
但是,⼀致性哈希算法在服务节点太少时,容易因为节点分部不均匀⽽造成数据倾斜问题。例如系统中 只有两台服务器,其环分布如下,节点2只能负责⾮常⼩的⼀段,⼤量的客户端请求落在了节点1上,这就是数据(请求)倾斜问题
2)为了解决这种数据倾斜问题,⼀致性哈希算法引⼊了虚拟节点机制,即对每⼀个服务节点计算多个 哈希,每个计算结果位置都放置⼀个此服务节点,称为虚拟节点。
具体做法可以在服务器ip或主机名的后⾯增加编号来实现。⽐如,可以为每台服务器计算三个虚拟节 点,于是可以分别计算 “节点1的ip#1”、“节点1的ip#2”、“节点1的ip#3”、“节点2的ip#1”、“节点2的 ip#2”、“节点2的ip#3”的哈希值,于是形成六个虚拟节点,当客户端被路由到虚拟节点的时候其实是被 路由到该虚拟节点所对应的真实节点
4. 手写实现一致性Hash算法
- 普通Hash算法实现
public class GeneralHash {
public static void main(String[] args){
// 定义客户端
String[] cleients = new String[]{"10.78.12.3","113.25.63.1","126.12.3.8"};
// 定义服务器数量//编号对应0,1,2
int serverCount = 2;
// 路由计算
// 根据index锁定对应到的Tomcat服务器
for (String cleient : cleients) {
int hash = Math.abs(cleient.hashCode());
int i = hash%serverCount;
System.out.println("客户端:"+cleient+"路由到"+i+"服务器");
}
}
}
- ⼀致性Hash算法实现(不含虚拟节点)
public class ConsistentHashNoVirtual {
public static void main(String[] args) {
// 1.初始化,把服务器IP节点的哈希值映射到节点上
// 定义服务器ip
String[] tomcatServers = new String[]{"123.111.0.0","123.101.3.1","111.20.35.2","123.98.26.3"};
// 定义客户端
String[] clients = new String[]{"10.78.12.3","113.25.63.1","126.12.3.8"};
// 定义HashServerMap
SortedMap<Integer,String> hashServerMap = new TreeMap<>();
for (String tomcatServer : tomcatServers) {
// 求出每一个ip的hash值,对应到hash环上,存储hash值与IP的对应关系
int serverHash = Math.abs(tomcatServer.hashCode());
hashServerMap.put(serverHash,tomcatServer);
}
// 2.针对客户端IP求出hash值
for (String client : clients) {
int clientHash = Math.abs(client.hashCode());
// 根据客户端IP的哈希值找出哪一个服务器节点能够处理
SortedMap<Integer, String> sortedMap = hashServerMap.tailMap(clientHash);
if (!sortedMap.isEmpty()){
System.out.println("客户端:"+client+"路由到"+sortedMap.get(sortedMap.firstKey())+"服务器");
}else {
System.out.println("客户端:"+client+"路由到"+hashServerMap.get(hashServerMap.firstKey())+"服务器");
}
}
// 3.针对客户端找到能够处理当前客户端请求的服务器(哈希环上顺时针最近的服务器)
}
}
- ⼀致性Hash算法实现(含虚拟节点)
public class ConsistentHashWithVirtual {
public static void main(String[] args) {
// 1.初始化,把服务器IP节点的哈希值映射到节点上
// 定义服务器ip
String[] tomcatServers = new String[]{"123.111.0.0","123.101.3.1","111.20.35.2","123.98.26.3"};
// 定义客户端
String[] clients = new String[]{"10.78.12.3","113.25.63.1","126.12.3.8"};
// 定义针对每个真是服务器虚拟出几个虚拟节点
int virtaulCount = 3;
// 定义HashServerMap
SortedMap<Integer,String> hashServerMap = new TreeMap<>();
for (String tomcatServer : tomcatServers) {
// 求出每一个ip的hash值,对应到hash环上,存储hash值与IP的对应关系
int serverHash = Math.abs(tomcatServer.hashCode());
hashServerMap.put(serverHash,tomcatServer);
// 处理虚拟节点
for (int i = 0; i < virtaulCount; i++) {
int virtaulHash = Math.abs((tomcatServer+"#"+i).hashCode());
hashServerMap.put(virtaulHash,"虚拟节点"+i+"映射出的请求"+tomcatServer);
}
}
// 2.针对客户端IP求出hash值
for (String client : clients) {
int clientHash = Math.abs(client.hashCode());
// 根据客户端IP的哈希值找出哪一个服务器节点能够处理
SortedMap<Integer, String> sortedMap = hashServerMap.tailMap(clientHash);
if (!sortedMap.isEmpty()){
System.out.println("客户端:"+client+"路由到"+sortedMap.get(sortedMap.firstKey())+"服务器");
}else {
System.out.println("客户端:"+client+"路由到"+hashServerMap.get(hashServerMap.firstKey())+"服务器");
}
}
// 3.针对客户端找到能够处理当前客户端请求的服务器(哈希环上顺时针最近的服务器)
}
}
5. Nginx配置一致性Hash负载均衡策略
ngx_http_upstream_consistent_hash 模块是⼀个负载均衡器,使⽤⼀个内部⼀致性hash算法来选择 合适的后端节点。
该模块可以根据配置参数采取不同的⽅式将请求均匀映射到后端机器,
consistent_hash $remote_addr:可以根据客户端ip映射
consistent_hash $request_uri:根据客户端请求的uri映射
consistent_hash $args:根据客户端携带的参数进⾏映
ngx_http_upstream_consistent_hash 模块是⼀个第三⽅模块,需要我们下载安装后使⽤
1)github下载nginx⼀致性hash负载均衡模块 https://github.com/replay/ngx_http_consistent_hash
2)将下载的压缩包上传到nginx服务器,并解压
3)我们已经编译安装过nginx,此时进⼊当时nginx的源码⽬录,执⾏如下命令
./configure —add-module=/root/ngx_http_consistent_hash-master
make
make install
4)Nginx就可以使⽤啦,在nginx.conf⽂件中配置