普通hash算法在集群中的应用
Nginx ip_hash策略示意图
下面是上面策略的简单代码实现,模拟出它是如何将客户端请求分配到不同节点,而且还保证session的一致性。
package demo;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author qiu
*
*/
public class HashUtils {
public static void main(String[] args) {
//模拟客户端IP
List<String> clientList = new ArrayList<>();
clientList.add("10.20.12.1");
clientList.add("126.10.53.3");
clientList.add("101.0.2.4");
//定义服务器数量(编号对应0,1,2)
int serverCount = 3;
//路由计算
clientList.forEach( c-> {
int hash = Math.abs(c.hashCode()); //hash值自定义
int index = hash % serverCount;
System.out.println("客户端:"+c+"被路由到编号为》》》》"+index+"的服务器");
});
}
}
客户端:10.20.12.1被路由到编号为》》》》0的服务器
客户端:126.10.53.3被路由到编号为》》》》1的服务器
客户端:101.0.2.4被路由到编号为》》》》1的服务器
问题
以上hash算法思想也存在一些问题,比如当服务器节点,扩容或宕机时由于服务器的节点改变,在重新求模运算时,会导致客户端 落到和之前不同的节点服务器上,session会话就消失了。在真实环境中会有大量请求命中不到原来目标服务器上。
解决方案(一致性Hash算法)
思想:
1.服务器节点定位
每台服务器节点都会有主机名(这里就取节点服务器的ip地址)下图中hash环中绿色的点就代表着节点服务器ip经过某个hash算法后得到的一个整数,这个整数自然就会落到上面直线的区间中的某个位置。每个服务器节点都经过一次取值后落到下面hash环上。
2.客户端请求处理定位
客户端(它们也都有自己的IP地址)每一次请求经过hash算法取值后也都会落到hash环上如下图(蓝色笑脸节点),定位到hash环上之后,客户端就会路由到离自己节点最近的服务器节点上(注意:顺势针方向搜索)
3.客服端扩容和缩容
缩容
假设服务器3下线后,原来路由到3的客户端,重新(它会去找离它最近的服务器节点4)路由到服务器4,对于其它客户端没有影响,只是一小部分受到影响(请求迁移达到最小,这样的算法对分布式集群来说非常适合,避免了大量请求迁移)
扩容
总结
由此可见以上一致性hash算法解决了服务器节点扩容或宕机时避免大量请求迁移
问题
当服务器节点比较少时,容易因为节点分布不均匀而造成数据倾斜问题
节点1只负责一小段,大量请求落到节点2上。
代码模拟
package demo;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
/**
*
* @author qiu
*
*/
public class HashUtils {
public static void main(String[] args) {
//初始化:把服务器节点IP的哈希值对应到哈希环上
List<String> tomcatServerList = new ArrayList<>();
tomcatServerList.add("10.20.12.1");
tomcatServerList.add("126.10.53.3");
tomcatServerList.add("101.0.2.4");
//模拟hash环(有序)
SortedMap<Integer,String> hashServermap = new TreeMap<>();
tomcatServerList.forEach( t-> {
//求出每一个ip的hash值,对应到hash环上,存储hash值与ip的对应关系
int serverHash = Math.abs(t.hashCode()); //hash值自定义
hashServermap.put(serverHash, t);
});
//针对客户端IP求hash值
List<String> clientList = new ArrayList<>();
clientList.add("101.40.12.7");
clientList.add("226.30.53.6");
clientList.add("301.10.2.4");
clientList.forEach(c->{
int clientHash = Math.abs(c.hashCode()); //hash值自定义
//客户端找到能够处理当前请求的服务器(hash环上顺时针最近)
//根据客户端ip的hash值去找出哪一个服务器节点能够处理
//tailMap方法返回比当前key大的新集合
SortedMap<Integer,String> tailHashServermap = hashServermap.tailMap(clientHash);
if (tailHashServermap.isEmpty()) {
//取hash环上的顺时针第一台服务器
Integer firstKey = hashServermap.firstKey();
System.out.println("客户端:"+c+"被路由到编号为》》》》"+hashServermap.get(firstKey)+"的服务器");
} else {
Integer firstKey = tailHashServermap.firstKey();
System.out.println("客户端:"+c+"被路由到编号为》》》》"+hashServermap.get(firstKey)+"的服务器");
}
});
}
}
解决方案(一致性Hash算法+虚拟节点)
为了解决一致性Hash算法中的数据倾斜问题,一致性hash算法引入了虚拟节点机制,即对每一个服务器节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。当请求落到某个虚拟节点负责的段时,它就会去找真实节点,来处理请求,这样就会让请求处理的均匀了。
如下图:分别对节点1和节点2计算三个虚拟节点:#1,#2,#3.
代码模拟
package demo;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
/**
*
* @author qiu
*
*/
public class HashUtils {
public static void main(String[] args) {
//初始化:把服务器节点IP的哈希值对应到哈希环上
List<String> tomcatServerList = new ArrayList<>();
tomcatServerList.add("10.20.12.1");
tomcatServerList.add("126.10.53.3");
tomcatServerList.add("101.0.2.4");
//模拟hash环(有序)
SortedMap<Integer,String> hashServermap = new TreeMap<>();
//定义针对每个真实服务器虚拟出来几个节点
int virtaulCount = 3;
tomcatServerList.forEach( t-> {
//求出每一个ip的hash值,对应到hash环上,存储hash值与ip的对应关系
int serverHash = Math.abs(t.hashCode()); //hash值自定义
hashServermap.put(serverHash, t);
//处理虚拟节点
for(int i=0;i<virtaulCount;i++) {
int virtaulHash = Math.abs((t+"#"+i).hashCode());
hashServermap.put(virtaulHash, "由虚拟节点转换成的真实节点》》"+i+":"+t);
}
});
//针对客户端IP求hash值
List<String> clientList = new ArrayList<>();
clientList.add("101.40.12.7");
clientList.add("226.30.53.6");
clientList.add("301.10.2.4");
clientList.forEach(c->{
int clientHash = Math.abs(c.hashCode()); //hash值自定义
//客户端找到能够处理当前请求的服务器(hash环上顺时针最近)
//根据客户端ip的hash值去找出哪一个服务器节点能够处理
//tailMap方法返回比当前key大的新集合
SortedMap<Integer,String> tailHashServermap = hashServermap.tailMap(clientHash);
if (tailHashServermap.isEmpty()) {
//取hash环上的顺时针第一台服务器
Integer firstKey = hashServermap.firstKey();
System.out.println("客户端:"+c+"被路由到编号为》》》》"+hashServermap.get(firstKey)+"的服务器");
} else {
Integer firstKey = tailHashServermap.firstKey();
System.out.println("客户端:"+c+"被路由到编号为》》》》"+hashServermap.get(firstKey)+"的服务器");
}
});
}
}
客户端:101.40.12.7被路由到编号为》》》》由虚拟节点转换成的真实节点》》2:10.20.12.1的服务器
客户端:226.30.53.6被路由到编号为》》》》由虚拟节点转换成的真实节点》》2:101.0.2.4的服务器
客户端:301.10.2.4被路由到编号为》》》》101.0.2.4的服务器
总结
由此可见一致性Hash算法+虚拟节点可以解决数据倾斜问题