⼀致性Hash算法
# 1、⼀致性Hash算法
Hash 算法,比如说在安全加密领域 MD5、SHA 等加密算法,在数据存储和查找方⾯有 Hash 表等, 以上都应用到了 Hash 算法。
# 为什么需要使用 Hash?
Hash 算法较多的应用在数据存储和查找领域,最经典的就是 Hash 表,它的查询效率非常之⾼,其中的哈希算法如果设计的比较 ok 的话,那么 Hash 表的数据查询时间复杂度可以接近于 O(1),示例
需求:提供⼀组数据 1,5,7,6,3,4,8,对这组数据进行存储,然后随便给定⼀个数 n,请你判断 n 是否存在于刚才的数据集中?
list:List[1,5,7,6,3,4,8]
// 通过循环判断来实现
for(int element: list) {
if(element == n) {
如果相等,说明n存在于数据集中
}
}
2
3
4
5
6
7
8
9
10
11
以上这种方法叫做顺序查找法 :这种方式我们是通过循环来完成,比较原始,效率也不⾼
⼆分查找:排序之后折半查找,相对于顺序查找法会提⾼⼀些效率,但是效率也并不是特别好我能否不循环!不⼆分!⽽是通过⼀次查询就把数据 n 从数据集中查询出来???可以!
定义⼀个数组,数组⻓度大于等于数据集⻓度,此处⻓度为 9,数据 1 就存储在下标为 1 的位置,3 就存储在下标为 3 的元素位置,,,依次类推。
这个时候,我想看下 5 存在与否,只需要判断 list.get(5) array[5] 是否为空,如果为空,代表 5 不存在于数据集,如果不为空代表 5 在数据集当中,通过⼀次查找就达到了目的,时间复杂度为 O(1)。
这种方式叫做“直接寻址法”:直接把数据和数组的下标绑定到⼀起,查找的时候,直接 array[n]就取出了数据
优点:速度快,⼀次查找得到结果
缺点:
1)浪费空间,比如 1,5,7,6,3,4,8,12306 ,最大值 12306 ,按照上述方式需要定义⼀个比如⻓度为 12307 的数组,但是只存储零星的⼏个数据,其他位置空间都浪费着
2)数据如: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 得到的 hash 值是一样的,导致 hash 冲突
在此基础上采用开放寻址法(了解)
开放寻址法:1 放进去了,6 再来的时候,向前或者向后找空闲位置存放,不好的地方,如果数组⻓度定义好了比如 10,⻓度不能扩展,来了 11 个数据,不管 Hash 冲突不冲突,肯定存不下这么多数据
拉链法:数据⻓度定义好了,怎么存储更多内容呢,算好 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 是常量)
hashcode 其实也是通过⼀个 Hash 算法得来的
# 1、 Hash算法应用场景
Hash 算法在分布式集群架构中的应用场景
Hash 算法在很多分布式集群产品中都有应用,比如分布式集群架构 Redis、Hadoop、ElasticSearch,Mysql 分库分表,Nginx 负载均衡等
主要的应用场景归纳起来两个
请求的负载均衡(比如 nginx 的 ip_hash 策略)
Nginx 的 IP_hash 策略可以在客户端 ip 不变的情况下,将其发出的请求始终路由到同⼀个目标服务
器上,实现会话粘滞,避免处理 session 共享问题
如果没有 IP_hash 策略,那么如何实现会话粘滞?
可以维护⼀张映射表,存储客户端 IP 或者 sessionid 与具体目标服务器的映射关系
<ip,tomcat1>
缺点
1)那么,在客户端很多的情况下,映射表非常大,浪费内存空间
2)客户端上下线,目标服务器上下线,都会导致重新维护映射表,映射表维护成本很大
如果使用哈希算法,事情就简单很多,我们可以对 ip 地址或者 sessionid 进行计算哈希值,哈希值与服务器数量进行取模运算,得到的值就是当前请求应该被路由到的服务器编号,如此,同⼀个客户端 ip 发送过来的请求就可以路由到同⼀个目标服务器,实现会话粘滞。
分布式存储
以分布式内存数据库 Redis 为例,集群中有 redis1,redis2,redis3 三台 Redis 服务器那么,在进行数据存储时,<key1,value1>数据存储到哪个服务器当中呢?针对 key 进行 hash 处理 hash(key1)%3=index, 使用余数 index 锁定存储的具体服务器节点
# 2、普通Hash算法存在的问题
普通 Hash 算法存在⼀个问题,以 ip_hash 为例,假定下载用户 ip 固定没有发生改变,现在 tomcat3 出现了问题,down 机了,服务器数量由 3 个变为了 2 个,之前所有的求模都需要重新计算。
如果在真实生产情况下,后台服务器很多台,客户端也有很多,那么影响是很大的,缩容和扩容都会存在这样的问题,大量用户的请求会被路由到其他的目标服务器处理,用户在原来服务器中的会话都会丢失。
# 3、⼀致性Hash算法
⼀致性哈希算法思路如下:
⾸先有⼀条直线,直线开头和结尾分别定为为 1 和 2 的 32 次方减 1,这相当于⼀个地址,对于这样⼀条线,弯过来构成⼀个圆环形成闭环,这样的⼀个圆环称为 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) { // 定义客户端IP String[] clients = new String[] {"10.78.12.3","113.25.63.1","126.12.3.8"}; // 定义服务器数量 int serverCount = 5;// (编号对应0,1,2) // hash(ip)%node_counts=index //根据index锁定应该路由到的tomcat服务器 for(String client: clients) { int hash = Math.abs(client.hashCode()); int index = hash%serverCount; System.out.println("客户端:" + client + " 被路由到服务器编号为:" + index); } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 一致性 hash 算法
public class ConsistentHashNoVirtual {
public static void main(String[] args) {
//step1 初始化:把服务器节点IP的哈希值对应到哈希环上
// 定义服务器ip
String[] tomcatServers = new String[]
{"123.111.0.0", "123.101.3.1", "111.20.35.2", "123.98.26.3"};
TreeMap<Integer, String> hashServerMap = new TreeMap<>();
for (String tomcatServer : tomcatServers) {
// 求出每⼀个ip的hash值,对应到hash环上,存储hash值与ip的对应关系
int serverHash = Math.abs(tomcatServer.hashCode());
// 存储hash值与ip的对应关系
hashServerMap.put(serverHash,tomcatServer);
}
//step2 针对客户端IP求出hash值
// 定义客户端IP
String[] clients = new String[]
{"10.78.12.3","113.25.63.1","126.12.3.8"};
for(String client : clients) {
int clientHash = Math.abs(client.hashCode());
//step3 针对客户端,找到能够处理当前客户端请求的服务器(哈希环上顺时针最近)
// 根据客户端ip的哈希值去找出哪⼀个服务器节点能够处理()
SortedMap<Integer, String> integerStringSortedMap =
hashServerMap.tailMap(clientHash);
if(integerStringSortedMap.isEmpty()) {
// 取哈希环上的顺时针第⼀台服务器
Integer firstKey = hashServerMap.firstKey();
System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
}else{
Integer firstKey = integerStringSortedMap.firstKey();
System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 含虚拟节点的一致性 hash 算法
public class ConsistentHashWithVirtual {
public static void main(String[] args) {
//step1 初始化:把服务器节点IP的哈希值对应到哈希环上
// 定义服务器ip
String[] tomcatServers = new String[]
{"123.111.0.0", "123.101.3.1", "111.20.35.2", "123.98.26.3"};
TreeMap<Integer, String> hashServerMap = new TreeMap<>();
// 定义针对每个真实服务器虚拟出来⼏个节点
int virtaulCount = 3;
for (String tomcatServer : tomcatServers) {
// 求出每⼀个ip的hash值,对应到hash环上,存储hash值与ip的对应关系
int serverHash = Math.abs(tomcatServer.hashCode());
// 存储hash值与ip的对应关系
hashServerMap.put(serverHash,tomcatServer);
for (int i = 0; i < virtaulCount; i++) {
int virtualHash = Math.abs((tomcatServer + "#" + i).hashCode());
hashServerMap.put(virtualHash,"----由虚拟节点"+ i + "映射过来的请求:"+ tomcatServer);
}
}
//step2 针对客户端IP求出hash值
// 定义客户端IP
String[] clients = new String[]
{"10.78.12.3","113.25.63.1","126.12.3.8"};
for(String client : clients) {
int clientHash = Math.abs(client.hashCode());
//step3 针对客户端,找到能够处理当前客户端请求的服务器(哈希环上顺时针最近)
// 根据客户端ip的哈希值去找出哪⼀个服务器节点能够处理()
SortedMap<Integer, String> integerStringSortedMap =
hashServerMap.tailMap(clientHash);
if(integerStringSortedMap.isEmpty()) {
// 取哈希环上的顺时针第⼀台服务器
Integer firstKey = hashServerMap.firstKey();
System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
}else{
Integer firstKey = integerStringSortedMap.firstKey();
System.out.println("==========>>>>客户端:" + client + " 被路由到服务器:" + hashServerMap.get(firstKey));
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 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 (opens new window)
2)将下载的压缩包上传到 nginx 服务器,并解压
3)我们已经编译安装过 nginx,此时进入当时 nginx 的源码目录,执行如下命令
./confifigure —add-module=/root/ngx_http_consistent_hash-master
make
make install
2
3
4)Nginx 就可以使⽤啦,在 nginx.conf 文件中配置
# 参考
- 01
- idea 热部署插件 JRebel 安装及破解,不生效问题解决04-10
- 02
- spark中代码的执行位置(Driver or Executer)12-12
- 03
- 大数据技术之 SparkStreaming12-12