参考:
什么是一致性Hash算法
几种经典的hash算法
一致性哈希算法
一致性哈希算法学习及JAVA代码实现分析
原理
一致性Hash算法的原理
因为对于hash(k)的范围在int范围,所以我们将0~2^32作为一个环。其步骤为:
1,求出每个服务器的hash(服务器ip)值,将其配置到一个 0~2^n 的圆环上(n通常取32)。
2,用同样的方法求出待存储对象的主键 hash值,也将其配置到这个圆环上,然后从数据映射到的位置开始顺时针查找,将数据分布到找到的第一个服务器节点上。
其分布如图:
Java实现
import java.util.ArrayList;
import java.util.Random;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 服务器节点类
*/
class ServerNode {
//节点ip
private String ip;
//节点其他属性.....
public ServerNode(String ip) {
this.ip = ip;
}
/**
* 这里简单的重写了一下hashCode方法,利用FNV1_32_HASH算法
* 添加其他属性时,要及时更新该方法
*/
@Override
public int hashCode() {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < ip.length(); i++)
hash = (hash ^ ip.charAt(i)) * p;
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
// 如果算出来的值为负数则取其绝对值
if (hash < 0)
hash = Math.abs(hash);
return hash;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
}
/**
* 任务节点
*/
class TaskNode {
//任务id
private String id;
//任务其他属性
public TaskNode(String id) {
this.id = id;
}
/**
* 这里简单的重写了一下hashCode方法,利用FNV1_32_HASH算法
* 添加其他属性时,要及时更新该方法
*/
@Override
public int hashCode() {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < id.length(); i++)
hash = (hash ^ id.charAt(i)) * p;
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
// 如果算出来的值为负数则取其绝对值
if (hash < 0)
hash = Math.abs(hash);
return hash;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
/**
* 不带虚拟节点的一致性Hash算法
*
* @author zf
*/
public class ConsistentHashing {
//key表示服务器的hash值,value表示服务器
private SortedMap<Integer, ServerNode> sortedMap = new TreeMap<>();
/**
* 初始化
*
* @param serverNodeArrayList 服务器节点列表
*/
public ConsistentHashing(ArrayList<ServerNode> serverNodeArrayList) {
for (ServerNode node : serverNodeArrayList) {
sortedMap.put(node.hashCode(), node);
}
}
/**
* 获取该任务节点分配到的服务器节点
*
* @return
*/
public ServerNode getServerNode(TaskNode taskNode) {
int taskHash = taskNode.hashCode();
// 得到大于该Hash值的所有Map
SortedMap<Integer, ServerNode> subMap = sortedMap.tailMap(taskHash);
//为空时,即访问到最后一个节点,其顺时针下一个节点为一号节点
if (subMap.size() == 0) {
return sortedMap.get(sortedMap.firstKey());
}
// 第一个Key就是顺时针过去离node最近的那个结点
Integer i = subMap.firstKey();
// 返回对应的服务器节点
return subMap.get(i);
}
/**
* 向集群内添加新的服务器节点(待实现)
*
* @return
*/
public boolean addServerNode(ServerNode serverNode) {
return true;
}
/**
* 从集群中移除一个服务器节点(待实现)
*
* @param serverNode 服务器节点
* @return
*/
public boolean removeServerNode(ServerNode serverNode) {
return true;
}
public static void main(String[] args) {
//待添加入Hash环的服务器列表ip
String[] serverIp = {"192.168.0.0:111", "192.168.0.1:111",
"192.168.0.2:111", "192.168.0.3:111", "192.168.0.4:111"};
ArrayList<ServerNode> serverNodeArrayList = new ArrayList<>();
for (String ip : serverIp) {
serverNodeArrayList.add(new ServerNode(ip));
}
ConsistentHashing consistentHashing = new ConsistentHashing(serverNodeArrayList);
for (int i = 0; i < 100; i++) {
System.out.println(i + "节点分配的服务器ip为:" + consistentHashing.getServerNode(new TaskNode("" + new Random().nextInt())).getIp());
}
}
}
和一般的取模,分段方法相比一致性hash的优势
传统的取模方式
例如10条数据,3个节点,如果按照取模的方式,那就是
node a: 0,3,6,9
node b: 1,4,7
node c: 2,5,8
当增加一个节点的时候,数据分布就变更为
node a:0,4,8
node b:1,5,9
node c: 2,6
node d: 3,7
总结:数据3,4,5,6,7,8,9在增加节点的时候,都需要做搬迁,成本太高
一致性哈希方式
最关键的区别就是,对节点和数据,都做一次哈希运算,然后比较节点和数据的哈希值,数据取和节点最相近的节点做为存放节点。这样就保证当节点增加或者减少的时候,影响的数据最少。
还是拿刚刚的例子,(用简单的字符串的ascii码做哈希key):
十条数据,算出各自的哈希值
0:192
1:196
2:200
3:204
4:208
5:212
6:216
7:220
8:224
9:228
有三个节点,算出各自的哈希值
node a: 203
node g: 209
node z: 228
这个时候比较两者的哈希值,如果大于228,就归到前面的203,相当于整个哈希值就是一个环,对应的映射结果:
node a: 0,1,2
node g: 3,4
node z: 5,6,7,8,9
这个时候加入node n, 就可以算出node n的哈希值:
node n: 216
这个时候对应的数据就会做迁移:
node a: 0,1,2
node g: 3,4
node n: 5,6
node z: 7,8,9
这个时候只有5和6需要做迁移
优势:一致性Hash算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
为什么要设置虚拟节点
原因1:Hash环的数据倾斜问题
一致性Hash算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题,例如系统中只有两台服务器,其环分布如下:
此时必然造成大量数据集中到Node A上,而只有极少量会定位到Node B上。为了解决这种数据倾斜问题,一致性Hash算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器IP或主机名的后面增加编号来实现。
例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点:
同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到Node A上。这样就解决了服务节点少时数据倾斜的问题。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。
原因2:雪崩效应
上面会造成雪崩效应的原因分析:
如果不存在热点数据的时候,每台机器的承受的压力是M/2(假设每台机器的最高负载能力为M),原本是不会有问题的,但是,这个时候A服务器由于有热点数据挂了,然后A的数据迁移至B,导致B所需要承受的压力变为M(还不考虑热点数据访问的压力),所以这个失败B是必挂的,然后C至少需要承受1.5M的压力。。。。然后大家一起挂。。。
所以我们通过上面可以看到,之所以会大家一起挂,原因在于如果一台机器挂了,那么它的压力全部被分配到一台机器上,导致雪崩。
如上图:A节点对应A1,A2,BCD节点同理。这时候,如果A节点挂了,A节点的数据迁移情况是:A1数据会迁移到C2,A2数据迁移到D1。这就相当于A的数据被C和D分担了,这就避免了雪崩效应的发送,而且虚拟节点我们可以自定义设置,使其适用于我们的应用。