一、什么是负载均衡
Load balancing,即负载均衡,是一种计算机技术,用来在多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或其他资源中分配负载,以达到最优化资源使用、最大化吞吐率、最小化响应时间、同时避免过载的目的。在我们的rrpc中使用负载均衡将请求负载分配到不同的节点上。
OIS七层模型
负载均衡可以根据不同的标准进行分类。以下是一些常见的负载均衡分类:
负载均衡服务器在决定将请求转发到具体哪台真实服务器的时候,是通过负载均衡算法来实现的。负载均衡算法,是一个负载均衡服务器的核心。
就像电影院门口的引导员一样,他根据什么把排队人员分配到具体的入口呢?是哪个入口人少吗?还是哪个入口速度最快?还是哪个入口最近呢?
二、rrpc框架中的负载均衡
我们使用客户端负载均衡,让客户端直接选择合适的节点,使用轮询、加权轮询或一致性哈希等算法,实现负载均衡
注:使用服务端负载均衡,需要配置额外的硬件负载均衡器或云负载均衡服务会带来额外的成本,还会带来额外的网络跳转,另外负载均衡可能成为性能瓶颈,减少了灵活性和可定制性,因此不使用
三、客户端+轮询负载均衡方式
在客户端访问服务端时,一个客户端可能对应多个服务端,使用轮询负载均衡的方式,让客户端按顺序将请求分配给服务端
1、完成效果
启动三个服务端
客户端发送3次请求,将请求按顺序分配给客户端
2、代码实现
实现负载均衡的大体思路
使用模板方法设计模式,模板方法模式(Template Method Pattern)在一个抽象类公开定义了执行它的模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行;
1.在我们的rrpc中,定义了一个loadBalancer作为负载均衡的接口,定义了抽象类AbstractLoadBalancer implements loadBalancer,
2.在 AbstractLoadBalancer 中定义了getSelector作为模板方法,用于负载均衡的选择器
3.定义RoundRobinLoadBalancer继承AbstractLoadBalancer,重写getSelector方法,并在类中定义选择器RoundRobinSelector方法实现轮询负载均衡算法
4.创建方法reLoadBalance(),当服务上下线时进行重新的负载均衡
1.Selector接口:用于服务节点的选择
public interface Selector {
/**
*
* 根据服务列表执行一种算法 获取一个服务节点
* 获取可用节点
* @param
* @return 具体的服务节点
*/
InetSocketAddress getNext();
2.LoadBalancer接口
/**\
* 负载均衡器的接口
* 根据服务拉取列表 --> 对列表做缓存 --> 找到一个可用的服务
* 根据服务列表找到一个可用的服务
*/
public interface LoadBalancer {
/**
* 根据服务名找到一个可用的服务
* @param serviceName 服务名称
* @return 服务地址
*/
InetSocketAddress selectServiceAddress(String serviceName);
/**
* 当感知节点发生了动态上下线,我们需要进行重新的负载均衡
* @param serviceName 服务的名称
*/
void reLoadBalance(String serviceName, List<InetSocketAddress> addresses);
}
3.AbstractLoadBalancer抽象模板类
public abstract class AbstractLoadBalancer implements LoadBalancer{
//一个服务匹配一个selector
private Map<String,Selector> cache = new ConcurrentHashMap<>(8);
@Override
public InetSocketAddress selectServiceAddress(String serviceName) {
//第一部优先从cache中获取一个选择器
Selector selector = cache.get(serviceName);
//第二部如果没有缓存,就需要为service创建一个selector
if (selector == null){
//对于负载均衡器,内部维护服务列表作为缓存
List<InetSocketAddress> serviceList = RrpcBootstrap.getInstance()
.getRegistry().lookup(serviceName);
//提供一些算法负责选择合适的节点
selector = getSelector(serviceList);
cache.put(serviceName,selector);
}
//获取可用节点
return selector.getNext();
}
/**
* 当感知节点发生了动态上下线,我们需要进行重新的负载均衡
* @param serviceName 服务的名称
*/
@Override
public synchronized void reLoadBalance(String serviceName,List<InetSocketAddress> addresses) {
//根据新的服务列表生成新的selector
cache.put(serviceName,getSelector(addresses));
}
/**
* 模板设计模式
* 由子类进行扩展
* @param serviceList
* @return 负载均衡选择器
*/
protected abstract Selector getSelector(List<InetSocketAddress> serviceList) ;
}
4. RoundRobinLoadBalancer
@Slf4j
public class RoundRobinLoadBalancer extends AbstractLoadBalancer {
@Override
protected Selector getSelector(List<InetSocketAddress> serviceList) {
return new RoundRobinSelector(serviceList);
}
/**
* 选择器
* 将服务列表缓存进去
*/
private static class RoundRobinSelector implements Selector{
private List<InetSocketAddress> serviceList;
//需要一个原子性的游标,避免因高并发造成的线程安全问题,进行服务列表的选择
//众所周知,AtomicInteger用于多线程下线程安全的数据读写操作,
//避免使用锁同步,底层采用CAS实现,内部的存储值使用volatile修饰,因此多线程之间是修改可见的。
private AtomicInteger index;
public RoundRobinSelector(List<InetSocketAddress> serviceList) {
this.serviceList = serviceList;
this.index = new AtomicInteger(0);
}
@Override
public InetSocketAddress getNext() {
if (serviceList == null || serviceList.size() == 0){
log.error("进行负载均衡选取节点时,发现服务列表为空");
throw new LoadBalanceException();
}
InetSocketAddress address = serviceList.get(index.get());
//如果游标到了最后一位 返回index=0
if (index.get() == serviceList.size() - 1){
index.set(0);
}else {
//游标后移1位
index.incrementAndGet();
}
return address;
}
@Override
public void reBalance() {
}
}
}