负载均衡设备或者负载均衡服务器来实现负载均衡的,而是由 RPC 框架本身实现的,服务调用者可以自主选择服务节点,发起服务调用。RPC 框架不再需要依赖专门的负载均衡设备,可以节约成本;还减少了与负载均衡设备间额外的网络传输,提升了传输效率;并且均衡策略可配,便于服务治理。
那么具体在我们的RPC项目中如何使用呢?首先我们定义一个负载均衡接口:
public interface LoadBalancer {
/**
*从一系列Instance中选择一个
*/
Instance select(List<Instance> instances);
}
我们先来实现两种最简单的负载均衡算法:轮询和随机
public class RandomLoadBalancer implements LoadBalancer {
//随机算法
@Override
public Instance select(List<Instance> instances) {
return instances.get(new Random().nextInt(instances.size()));
}
}
public class RoundRobinLoadBalancer implements LoadBalancer {
//轮询算法
private int index = 0;
@Override
public Instance select(List<Instance> instances) {
if(index >= instances.size()) {
index %= instances.size();
}
return instances.get(index++);
}
}
可以看到随机算法借用了Random,从集群中随机选择单个rpc调用实例。而轮询算法按照顺序依次选择第一个、第二个、第三个……最后又回到第一个重新开始。
在进行服务调用的时候,调用方可自己选择负载均衡算法 ,默认实现的是随机负载均衡:
public class NacosServiceDiscovery implements ServiceDiscovery {
private static final Logger logger = LoggerFactory.getLogger(NacosServiceDiscovery.class);
private final LoadBalancer loadBalancer;
public NacosServiceDiscovery(LoadBalancer loadBalancer) {
if(loadBalancer == null) {
this.loadBalancer = new RandomLoadBalancer();
}else{
this.loadBalancer = loadBalancer;
}
}
@Override
public InetSocketAddress lookupService(String serviceName) {
try {
List<Instance> instances = NacosUtil.getAllInstance(serviceName);
if(instances.size() == 0) {
logger.error("找不到对应的服务: " + serviceName);
throw new RpcException(RpcError.SERVICE_NOT_FOUND);
}
Instance instance = loadBalancer.select(instances);
return new InetSocketAddress(instance.getIp(), instance.getPort());
} catch (NacosException e) {
logger.error("获取服务时有错误发生:", e);
}
return null;
}
}
最后来看我们的重头戏,一致性哈希环算法的实现!!关于一致性hash算法的原理,本文不做概述。本文说的是如何在本项目中用Java实现一致性Hash算法,大家可以先了解一下该算法以及项目注册中心的内容:
(3条消息) 每天进步一点点——五分钟理解一致性哈希算法(consistent hashing)_cywosp的博客_一致性hash算法
(1条消息) 自定义RPC项目——常见问题及详解(注册中心)_李孛欢的博客
代码如下:
public class IpHashLoadBalancer implements LoadBalancer{
@Override
public Instance select(List<Instance> instances , String address) {
ConsistentHash ch = new ConsistentHash(instances, 100);
HashMap<String,Instance> map = ch.map;
return map.get(ch.getServer(address));
}
}
//一致性hash 利用treemap实现
class ConsistentHash {
//TreeMap中的key表示服务器的hash值,value表示服务器。模拟一个哈希环
private static TreeMap<Integer,String> Nodes = new TreeMap();
private static int VIRTUAL_NODES = 160;//虚拟节点个数,用户指定,默认160
private static List<Instance> instances = new ArrayList<>();//真实物理节点集合
public ConsistentHash(List<Instance> instances, int VIRTUAL_NODES){
this.instances = instances;
this.VIRTUAL_NODES = VIRTUAL_NODES;
}
public static HashMap<String,Instance> map = new HashMap<>();//将服务实例与ip地址一一映射
//预处理 形成哈希环
static {
//程序初始化,将所有的服务器(真实节点与虚拟节点)放入Nodes(底层为红黑树)中
for (Instance instance : instances) {
String ip = instance.getIp();
Nodes.put(getHash(ip),ip);
map.put(ip,instance);
for(int i = 0; i < VIRTUAL_NODES; i++) {
int hash = getHash(ip+"#"+i);
Nodes.put(hash,ip);
}
}
}
//得到Ip地址
public String getServer(String clientInfo) {
int hash = getHash(clientInfo);
//得到大于该Hash值的子红黑树
SortedMap<Integer,String> subMap = Nodes.tailMap(hash);
//获取该子树最小元素
Integer nodeIndex = subMap.firstKey();
//没有大于该元素的子树 取整树的第一个元素
if (nodeIndex == null) {
nodeIndex = Nodes.firstKey();
}
return Nodes.get(nodeIndex);
}
//使用FNV1_32_HASH算法计算服务器的Hash值,这里不使用重写hashCode的方法,最终效果没区别
private static int getHash(String str) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < str.length(); i++) {
hash = (hash^str.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;
}
}
注意要点:
- 我们这里的LoadBalancer 接口的抽象方法内多增加了一个参数Address,这是因为我们之前实现的轮询和随机算法与客户端的地址无关,而我们这里是同一个客户端在服务端没有发生变化的前提下都要打到同一台服务器上,这与客户端的地址有关。
- 哈希环采用TreeMap模拟实现,在TreeMap的api中有一个tailMap() 函数,输入一个fromKey,输出的是一个SortedMap有序Map,再使用firstKey()拿到最近一个大于客户端哈希值的元素。
- 用虚拟节点来防止某个节点的流量过大而导致的一些问题
- 使用Hashmap将服务实例和ip地址对应,因为getService()得到的是ip地址,而我们最后需要返回的是instance,可以看到,每个真实节点和其对应的虚拟节点均对应着同一个instance
- 实例化一致性哈希环时需要传入实例列表Instances和每个实例对应的虚拟节点个数,更贴切这个项目。