传统hash算法的思路:
局限性很大 不便于修改 动态扩容时及其麻烦 需要重新rebalance 且做不到负载均衡
改进:
使用哈希环 计算出下标后 顺时针 直到寻找到第一个节点
出现动态扩缩容的时候 也能很好应对
将hash值均匀的分布在一个区间、采用环状的数据结构(treemap) 、为每一个节点创建若干虚拟节点 防止产生严重的流量倾斜
设计思路:
因为负载均衡策略很多 为了保证可扩展性 设计之初就应该构思清晰 我选择采用模板方法设计模式
不仅为抽象出负载策略接口 还抽象出选择器接口
目录结构
父类接口
public interface LoadBalancer {
/**
* 根据服务名获取一个可用的服务
* @param serviceName 服务名称
* @param group
* @return 服务地址
*/
InetSocketAddress selectServiceAddress(String serviceName,String group);
/**
* 当感知节点发生了动态上下线,我们需要重新进行负载均衡
* @param serviceName 服务的名称
* @param addresses 服务列表
*/
void reLoadBalance(String serviceName, List<InetSocketAddress> addresses);
}
public interface Selector {
/**
* 根据服务列表执行一种算法获取一个服务节点
* @return 具体的服务节点
*/
InetSocketAddress getNext();
}
public abstract class AbstractLoadBalancer implements LoadBalancer {
/**
* 一个服务会匹配一个selector
* 一个服务 会有多个节点 选择节点的算法也不相同
*/
private Map<String, Selector> cache = new ConcurrentHashMap<>(8);
/**
* 寻找服务地址
*
* @param serviceName 服务名称
* @return
*/
@Override
public InetSocketAddress selectServiceAddress(String serviceName,String group) {
Selector selector = cache.get(serviceName);
if (selector == null) {
// 对于这个负载均衡器,内部应该维护服务列表作为缓存
List<InetSocketAddress> serviceList = RpcBootstrap.getInstance()
.getConfiguration().getRegistryConfig().getRegistry().lookup(serviceName,group);
// 提供一些算法负责选取合适的节点
selector = getSelector(serviceList);
// 将select放入缓存当中
cache.put(serviceName, selector);
}
// 获取可用节点
return selector.getNext();
}
@Override
public void reLoadBalance(String serviceName, List<InetSocketAddress> addresses) {
// 根据新的服务列表生成新的selector
cache.put(serviceName, getSelector(addresses));
}
/**
* 由子类进行扩展
*
* @param serviceList 服务列表
* @return 负载均衡算法选择器
*/
protected abstract Selector getSelector(List<InetSocketAddress> serviceList);
}
子类实现
@Slf4j
public class ConsistentHashBalancer extends AbstractLoadBalancer {
@Override
protected Selector getSelector(List<InetSocketAddress> serviceList) {
return new ConsistentHashSelector(serviceList, 128);
}
/**
* 一致性hash的具体算法实现
*/
private static class ConsistentHashSelector implements Selector {
// hash环用来存储服务器节点
private SortedMap<Integer, InetSocketAddress> circle = new TreeMap<>();
// 虚拟节点的个数
private int virtualNodes;
public ConsistentHashSelector(List<InetSocketAddress> serviceList, int virtualNodes) {
//把每个节点转换成虚拟节点
this.virtualNodes = virtualNodes;
serviceList.forEach(this::addNodeToCircle);
}
/**
* 根据请求参数 选择 虚拟节点
*
* @return
*/
@Override
public InetSocketAddress getNext() {
RpcRequest rpcRequest = RpcBootstrap.REQUEST_THREAD_LOCAL.get();
// 我们想根据请求的一些特征来选择服务器 id
String requestId = Long.toString(rpcRequest.getRequestId());
// 请求的id做hash string的hash有问题 连续的string计算出的hash也是连续的
int hash = hash(requestId);
// 判断该hash值是否能直接落在一个节点上
if (!circle.containsKey(hash)) {
// 寻找最近的一个节点
// 获得了位于 hash 之后的所有键值对
SortedMap<Integer, InetSocketAddress> tailMap = circle.tailMap(hash);
// 如果给定的 hash 值在映射中没有对应的键,它会回到映射的起始处,形成一个环形
hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
return circle.get(hash);
}
/**
* 将每个节点挂载到hash环上
*
* @param inetSocketAddress
*/
private void addNodeToCircle(InetSocketAddress inetSocketAddress) {
// 为每一个节点生成匹配的虚拟节点进行挂载
for (int i = 0; i < virtualNodes; i++) {
//确保访问相同的地址 hash也不会相同 减少同一个节点的负荷
int hash = hash(inetSocketAddress.toString() + "-" + i);
// 关在到hash环上
circle.put(hash, inetSocketAddress);
if (log.isDebugEnabled()) {
log.debug("hash为[{}]的节点已经挂载到了哈希环上.", hash);
}
}
}
/**
* 删除节点
* @param inetSocketAddress
*/
private void removeNodeFomCircle(InetSocketAddress inetSocketAddress) {
// 为每一个节点生成匹配的虚拟节点进行挂载
for (int i = 0; i < virtualNodes; i++) {
int hash = hash(inetSocketAddress.toString() + "-" + i);
// 关在到hash环上
circle.remove(hash);
}
}
/**
* 具体的hash算法 todo 小小的遗憾,这样也是不均匀的 后面再慢慢琢磨 数字太大
* 原始的hash不满足
*
* @param s
* @return
*/
private int hash(String s) {
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
byte[] digest = md.digest(s.getBytes());
// md5得到的结果是一个字节数组,但是我们想要int 4个字节
int res = 0;
for (int i = 0; i < 4; i++) {
if (i != 0){
res = res << 8;
}
if( digest[i] < 0 ){
res = res | (digest[i] & 255);
} else {
res = res | digest[i];
}
}
return res;
}
}
}