一致性哈希做负载均衡,基于dubbo的简化版本,超级简单容易理解!!!
一致性哈希算法原理以及做分布式存储。一定先看:一致性哈希算法
dubbo提供了四种负载均衡实现:权重随机算法,最少活跃调用数算法,一致性哈希算法,加权轮询算法。
本文基于开源项目:guide-rpc-framework的一致性哈希算法做的负载均衡,这个项目的负载均衡是dubbo一致性哈希的简化版。
代码如下:
/**
* refer to dubbo consistent hash load balance: https://github.com/apache/dubbo/blob/2d9583adf26a2d8bd6fb646243a9fe80a77e65d5/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/loadbalance/ConsistentHashLoadBalance.java
*
* @author RicardoZ
* @createTime 2020年10月20日 18:15:20
*/
@Slf4j
public class ConsistentHashLoadBalance extends AbstractLoadBalance {
private final ConcurrentHashMap<String, ConsistentHashSelector> selectors = new ConcurrentHashMap<>();
@Override
protected String doSelect(List<String> serviceAddresses, String rpcServiceName) {
int identityHashCode = System.identityHashCode(serviceAddresses);
ConsistentHashSelector selector = selectors.get(rpcServiceName);
// check for updates
if (selector == null || selector.identityHashCode != identityHashCode) {
selectors.put(rpcServiceName, new ConsistentHashSelector(serviceAddresses, 160, identityHashCode));
selector = selectors.get(rpcServiceName);
}
return selector.select(rpcServiceName);
}
static class ConsistentHashSelector {
private final TreeMap<Long, String> virtualInvokers;
private final int identityHashCode;
ConsistentHashSelector(List<String> invokers, int replicaNumber, int identityHashCode) {
this.virtualInvokers = new TreeMap<>();
this.identityHashCode = identityHashCode;
for (String invoker : invokers) {
for (int i = 0; i < replicaNumber / 4; i++) {
byte[] digest = md5(invoker + i);
for (int h = 0; h < 4; h++) {
long m = hash(digest, h);
virtualInvokers.put(m, invoker);
}
}
}
}
static byte[] md5(String key) {
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
byte[] bytes = key.getBytes(StandardCharsets.UTF_8);
md.update(bytes);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e.getMessage(), e);
}
return md.digest();
}
static long hash(byte[] digest, int idx) {
return ((long) (digest[3 + idx * 4] & 255) << 24 | (long) (digest[2 + idx * 4] & 255) << 16 | (long) (digest[1 + idx * 4] & 255) << 8 | (long) (digest[idx * 4] & 255)) & 4294967295L;
}
public String select(String rpcServiceName) {
byte[] digest = md5(rpcServiceName);
return selectForKey(hash(digest, 0));
}
public String selectForKey(long hashCode) {
Map.Entry<Long, String> entry = virtualInvokers.tailMap(hashCode, true).firstEntry();
if (entry == null) {
entry = virtualInvokers.firstEntry();
}
return entry.getValue();
}
}
}
客户端要调用服务的名字即为rpcServiceName
,客户端要通过服务名发送请求之前,先进行负载均衡,通过负载均衡找到合适的服务器ip地址,然后依据此ip地址发送请求。
分析ConsistentHashSelector
这个类可以将多个服务器ip地址放到环形hash空间上,然后通过服务名找到一个ip地址。
那什么存储数据呢?virtualInvokers
用来模拟环形hash空间用来放置ip地址和服务名。
invokers为ip地址列表。
ConsistentHashSelector(List<String> invokers, int replicaNumber, int identityHashCode) {
this.virtualInvokers = new TreeMap<>();
this.identityHashCode = identityHashCode;
// 对于每一个ip地址
for (String invoker : invokers) {
// 针对每一个ip地址创建(replicaNumber / 4)个重复节点
for (int i = 0; i < replicaNumber / 4; i++) {
byte[] digest = md5(invoker + i);
// 针对每个一个重复节点将其等间隔的分布在环形hash空间上
for (int h = 0; h < 4; h++) {
// 计算节点hash值
long m = hash(digest, h);
// 将节点放到环形hash空间上
virtualInvokers.put(m, invoker);
}
}
}
}
我们知道TreeMap不是环形的,他就是一个用来放东西的容器,这里只是为了迎合一致性哈希算法的概念中的环形hash空间。
比如说replicaNumber的值为8,那么就会有两个重复节点,对于每个重复节点又会计算4个hash值,如此一来在环形hash空间上也就是TreeMap上,会有2乘4个也就是8个value相同(value就是ip地址),而hash值不同的键值对。
如下图(为了简单起见,hash值就都是两位数以内了哈~~)
现在我们就把这些服务器的ip地址都安置好了。
那么接下来就是让rpcServiceName
依据自己的hash值顺时针在环形hash空间上找到第一个离他最近的ip地址啦。
代码如何实现的顺时针寻找第一个最近的ip地址节点呢?
public String selectForKey(long hashCode) {
Map.Entry<Long, String> entry = virtualInvokers.tailMap(hashCode, true).firstEntry();
if (entry == null) {
entry = virtualInvokers.firstEntry();
}
return entry.getValue();
}
通过TreeMap的tailMap方法可以进行顺时针寻找。通过firstEntry可以找到第一个最近的ip地址节点。
如此对于不同的服务则会根据自己的hash值去顺时针寻找离自己最近的服务器的ip地址。