1、Hash算法的主要应用场景
- 请求的负载均衡(⽐如nginx的ip_hash策略)
Nginx的IP_hash策略可以在客户端ip不变的情况下,将其发出的请求始终路由到同⼀个⽬标服务器上,实现会话粘滞,避免处理session共享问题
如果没有IP_hash策略,那么如何实现会话粘滞?
可以维护⼀张映射表,存储客户端IP或者sessionid与具体⽬标服务器的映射关系<ip,tomcat>
缺点:
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个,之前所有的求模都需要重新计算。

public static void main(String[] args) {
//定义三台客户端IP
String[] clientId = new String[]{"10.78.12.3","192.25.63.1","126.12.3.8"};
//定义服务器数量
int serverCount = 5;
//根据index锁定应该路由到的tomcat服务器
for (String client : clientId) {
//这里使用jdk自带的hashcode算法
int hash = Math.abs(client.hashCode());
int index = hash%serverCount;
System.out.println("客户端:"+client+",对应的服务器:"+index);
}
}
服务器5台时运行结果:
客户端:10.78.12.3,对应的服务器:4
客户端:192.25.63.1,对应的服务器:3
客户端:126.12.3.8,对应的服务器:1
服务器4台时运行结果:
客户端:10.78.12.3,对应的服务器:0
客户端:192.25.63.1,对应的服务器:3
客户端:126.12.3.8,对应的服务器:1
如果在真实⽣产情况下,后台服务器很多台,客户端也有很多,那么影响是很⼤的,缩容和扩容都会存在这样的问题,⼤量⽤户的请求会被路由到其他的⽬标服务器处理,⽤户在原来服务器中的会话都会丢失。
3、一致性Hash算法
一致性Hash算法思路:我们把服务器的ip或者主机名求hash值然后对应到hash环上,那么针对客户端⽤户,也根据它的ip进⾏hash求值,对应到环上某个位置,按照顺时针⽅向找最近的服务器节点。

假如将服务器3下线,服务器3下线后,原来路由到3的客户端重新路由到服务器4,对于其他客户端没有影响只是这⼀⼩部分受影响(请求的迁移达到了最⼩,这样的算法对分布式集群来说⾮常合适的,避免了⼤量请求迁移 )

增加服务器5之后,原来路由到3的部分客户端路由到新增服务器5上,对于其他客户端没有影响只是这⼀⼩部分受影响(请求的迁移达到了最⼩,这样的算法对分布式集群来说⾮常合适的,避免了⼤量请求迁移 )

代码展示:
public static void main(String[] args) {
//step1 初始化:把服务器节点IP的哈希值对应到哈希环上
//定义服务器IP
String[] serverId = new String[]{"123.111.0.0","123.101.3.1","111.20.35.2","123.98.26.3"};
//定义一个map集合存储服务器hash值和服务器ip的对应关系
TreeMap<Integer,String> serverMap= new TreeMap<>();
for (String server : serverId) {
// 求出每⼀个ip的hash值,对应到hash环上,存储hash值与ip的对应关系
int index = Math.abs(server.hashCode());
// 存储hash值与ip的对应关系
serverMap.put(index,server);
}
//step2 针对客户端IP求出hash值
String[] clientId = new String[]{"10.78.12.3","113.25.63.1","126.12.3.8"};
for (String client : clientId) {
int index = Math.abs(client.hashCode());
SortedMap<Integer, String> sortedMap = serverMap.tailMap(index);
if (sortedMap.isEmpty()){
Integer firstKey = serverMap.firstKey();
System.out.println("客户端:"+client+" 被路由到IP:"+serverMap.get(firstKey)+"服务器");
}else {
Integer firstKey = sortedMap.firstKey();
System.out.println("客户端:"+client+" 被路由到IP:"+serverMap.get(firstKey)+"服务器");
}
}
}
正常情况:
客户端:10.78.12.3 被路由到IP:111.20.35.2服务器
客户端:113.25.63.1 被路由到IP:123.98.26.3服务器
客户端:126.12.3.8 被路由到IP:111.20.35.2服务器
去掉最后一个服务器
客户端:10.78.12.3 被路由到IP:111.20.35.2服务器
客户端:113.25.63.1 被路由到IP:111.20.35.2服务器
客户端:126.12.3.8 被路由到IP:111.20.35.2服务器
这里就是只影响到了一个客户端,对于其他是没有影响的。
但是,⼀致性哈希算法在服务节点太少时,容易因为节点分部不均匀⽽造成数据倾斜问题。例如系统中只有两台服务器,其环分布如下,节点2只能负责⾮常⼩的⼀段,⼤量的客户端请求落在了节点1上,这就是数据(请求)倾斜问题
为了解决这种数据倾斜问题,⼀致性哈希算法引⼊了虚拟节点机制,即对每⼀个服务节点计算多个哈希,每个计算结果位置都放置⼀个此服务节点,称为虚拟节点。
具体做法可以在服务器ip或主机名的后⾯增加编号来实现。⽐如,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “节点1的ip#1”、“节点1的ip#2”、“节点1的ip#3”、“节点2的ip#1”、“节点2的ip#2”、“节点2的ip#3”的哈希值,于是形成六个虚拟节点,当客户端被路由到虚拟节点的时候其实是被路由到该虚拟节点所对应的真实节点

代码实现:
public static void main(String[] args) {
//step1 初始化:把服务器节点IP的哈希值对应到哈希环上
//定义服务器IP
String[] serverId = new String[]{"123.111.0.0","123.101.3.1","111.20.35.2","123.98.26.3"};
//定义一个map集合存储服务器hash值和服务器ip的对应关系
TreeMap<Integer,String> serverMap= new TreeMap<>();
Integer virtual = 3;
for (String server : serverId) {
// 求出每⼀个ip的hash值,对应到hash环上,存储hash值与ip的对应关系
int index = Math.abs(server.hashCode());
// 存储hash值与ip的对应关系
serverMap.put(index,server);
for (Integer integer = 0; integer < virtual; integer++) {
int abs = Math.abs((server + "#" + integer).hashCode());
serverMap.put(abs,"有虚拟节点"+integer+"映射而来的请求:"+server);
}
}
//step2 针对客户端IP求出hash值
String[] clientId = new String[]{"10.78.12.3","113.25.63.1","126.12.3.8"};
for (String client : clientId) {
int index = Math.abs(client.hashCode());
SortedMap<Integer, String> sortedMap = serverMap.tailMap(index);
if (sortedMap.isEmpty()){
Integer firstKey = serverMap.firstKey();
System.out.println("客户端:"+client+" 被路由到IP:"+serverMap.get(firstKey)+"服务器");
}else {
Integer firstKey = sortedMap.firstKey();
System.out.println("客户端:"+client+" 被路由到IP:"+serverMap.get(firstKey)+"服务器");
}
}
}
运行结果:
客户端:10.78.12.3 被路由到IP:111.20.35.2服务器
客户端:113.25.63.1 被路由到IP:有虚拟节点2映射而来的请求:111.20.35.2服务器
客户端:126.12.3.8 被路由到IP:有虚拟节点0映射而来的请求:123.101.3.1服务器

本文介绍了Hash算法在负载均衡中的应用,特别是nginx的IP_hash策略。讨论了普通Hash算法在服务器数量变动时的问题,引出了使用一致性哈希算法解决会话粘滞和请求迁移问题的方案。一致性哈希通过在哈希环上分布服务器和客户端,实现了高效的数据路由。为了解决数据倾斜问题,引入了虚拟节点的概念。最后,展示了如何通过Java代码实现一致性哈希算法。
2103

被折叠的 条评论
为什么被折叠?



