【noob-rpc】⑨扩展版RPC-负载均衡(服务消费端)
【noob-rpc】⑨扩展版RPC-负载均衡(服务消费端)
扩展说明
【1】负载均衡概念梳理、常见负载均衡算法
【2】引入轮询、随机、一致性Hash三种负载均衡算法
【3】自定义负载均衡器,提供扩展负载均衡器接口
需求分析
目前RPC框架已经可以从注册中心获取到服务提供者的注册信息了,同一个服务可能会有多个服务提供者,但是目前消费者始终读取了第一个服务提供者节点发起调用,不仅会增大单个节点的压力,而且没有利用好其他节点的资源。
引入负载均衡概念:可以从服务提供者节点中,选择一个服务提供者发起请求,而不是每次都请求同一个服务提供者(根据相应策略选择节点)
1.负载均衡概念
❓何为负载:可以把负载理解为要处理的工作和压力,比如网络请求、事务、数据处理任务等
❓何为均衡:把工作和压力平均地分配给多个工作者,从而分摊每个工作者的压力,保证大家正常工作
用个比喻,假设餐厅里只有一个服务员,如果顾客非常多,他可能会忙不过来,没法及时上菜、忙中生乱,而且他的压励会越来越大,最严重的情况下就累倒了无法继续工作。而如果有多个服务员,大家能够服务更多的顾客,即使有一个服务员生病了,其他服务员也能帮忙顶上。
所以,负载均衡是一种用来分配网络或计算负载到多个资源上的技术。它的目的是确保每个资源都能够有效地处理负载、增加系统的并发量、避免某些资源过载而导致性能下降或服务不可用的情况。回归到RPC框架,负载均衡的作用是从一组可用的服务提供者中选择一个进行调用。
常用的负载均衡实现技术有Nginx(七层负载均衡)、LVS(四层负载均衡)
2.常见负载均衡算法(按照什么策略选择资源)
轮询(Round Robin)
按照循环的顺序将请求分配给每个服务器,适用于各服务器性能相近的情况。
假如有5台服务器节点,请求调用顺序如下:
1,2,3,4,5,1,2,3,4,5,1,2,3,4,5
随机(Random)
随机选择一个服务器来处理请求,适用于服务器性能相近且负载均匀的情况。
假如有5台服务器节点,请求调用顺序如下:
3,2,4,1,3,5,2,4,1,2,5,3,1
加权轮询(Weighted Round Robin)
根据服务器的性能或权重分配请求,性能更好的服务器会获得更多的请求,适用于服务器性能不均的情况。
假如有1台千兆带宽的服务器节点和4台百兆带宽的服务器节点,请求调用顺序可能如下: .
1,1,1,1,2,1,1,1,1,3,1,1,1,1,4,1,1,1,1,5
加权随机(Weighted Random)
根据服务器的权重随机选择一个服务器处理请求, 适用于服务器性能不均的情况。
假如有2台千兆带宽的服务器节点和3台百兆带宽的服务器节点,请求调用顺序可能如下:
1,2,2,1,3,1,1,1,2,4,2,2,2,1,5
最小连接数(Least Connections)
选择当前连接数最少的服务器来处理请求,适用于长连接场景。
IP Hash
根据客户端IP地址的哈希值选择服务器处理请求,确保同一客户端的请求始终被分配到同一台服务器上,适用于需要保持会话一致性的场景
当然,也可以根据请求中的其他参数进行Hash,比如根据请求接口的地址路由到不同的服务器节点
3.分布式知识点
一致性Hash
一致性哈希 (Consistent Hashing)是一种经典的哈希算法,用于将请求分配到多个节点或服务器上,所以非常适用于负载均衡。
它的核心思想是将整个哈希值空间划分成一个环状结构,每个节点或服务器在环上占据一个位置 ,每个请求根据其哈希值映射到环上的一个点,然后顺时针寻找第一个大于或等于该哈希值的节点,将请求路由到该节点上。
如下图,请求A会交给服务器C来进行处理
一致性还解决了节点下线和倾斜问题:
节点下线
节点下线:当某个节点下线时,欺载会被平均分摊到其他节点上,而不会影响到整个系统的稳定性,因为只有部分请求会受到影响
如上图所示,服务器C下线后,请求A会交给服务器A来处理(顺时针寻找第一个大于或等于该哈希值的节点) ,而服务器B接收到的请求保持不变
如果是轮询取模算法,只要节点数变了,很有可能大多数服务器处理的请求都要发生变化,对系统的影响巨大。
倾斜问题
通过虚拟节点的引入,将每个物理节点映射到多个虚拟节点上,使得节点在哈希环上的分布更加均均,减少了节点间的负载差异。
步骤说明
涉及文件、代码结构梳理
# noob-rpc-core
loadbalancer:
- LoadBalancer
- RoundRobinLoadBalancer
- RandomLoadBalancer
- ConsistentHashLoadBalancer
- LoadBalancerKeys
- LoadBalancerFactory
config:
- RpcConfig
proxy:
- ServiceProxy
test:
- VertxTcpServerTest
# sample-consumer
application.properties
1.多种负载均衡器的实现
在学习负载均衡的时候,可以参考Nginx 的负载均衡算法实现,此处依次实现轮询、随机、一致性 Hash三种负载均衡算法。
在RPC项目中新建loadbalancer包,将所有负载均衡器相关的代码放到该包下。
负载均衡器通用接口
编写负载均衡器通用接口:提供一个选择服务方法,接受请求参数和可用服务列表,可以根据这些信息进行选择
/**
* 负载均衡器(消费端使用)
*/
public interface LoadBalancer {
/**
* 选择服务调用
*
* @param requestParams 请求参数
* @param serviceMetaInfoList 可用服务列表
* @return
*/
ServiceMetaInfo select(Map<String, Object> requestParams, List<ServiceMetaInfo> serviceMetaInfoList);
}
轮询负载均衡器
使用JUC包下的AtomicInteger实现院子计数器,防止并发冲突问题
/**
* 轮询负载均衡器
*/
public class RoundRobinLoadBalancer implements LoadBalancer {
/**
* 当前轮询的下标
*/
private final AtomicInteger currentIndex = new AtomicInteger(0);
@Override
public ServiceMetaInfo select(Map<String, Object> requestParams, List<ServiceMetaInfo> serviceMetaInfoList) {
if (serviceMetaInfoList.isEmpty()) {
return null;
}
// 只有一个服务,无需轮询
int size = serviceMetaInfoList.size();
if (size == 1) {
return serviceMetaInfoList.get(0);
}
// 取模算法轮询
int index = currentIndex.getAndIncrement() % size;
return serviceMetaInfoList.get(index);
}
}
随机负载均衡器
使用Java自带的Random类实现随机选择
/**
* 随机负载均衡器
*/
public class RandomLoadBalancer implements LoadBalancer {
private final Random random = new Random();
@Override
public ServiceMetaInfo select(Map<String, Object> requestParams, List<ServiceMetaInfo> serviceMetaInfoList) {
int size = serviceMetaInfoList.size();
if (size == 0) {
return null;
}
// 只有 1 个服务,不用随机
if (size == 1) {
return serviceMetaInfoList.get(0);
}
return serviceMetaInfoList.get(random.nextInt(size));
}
}
一致性 Hash负载均衡器
使用TreeMap实现一致性Hash环,该数据结构提供了ceilingEntry 和firstEntry两个方法,便于获取符合算法要求的节点。
此处实现注意两个点:
(1)根据requestParams对象计算Hash值,此处只是简单地调用了对象的hashCode方法,可以根据需求实现自己的Hash算法
(2)每次调用负载均衡器时,都会重新构造Hash环,这是为了能够即时处理节点的变化
/**
* 一致性哈希负载均衡器
*/
public class ConsistentHashLoadBalancer implements LoadBalancer {
/**
* 一致性 Hash 环,存放虚拟节点
*/
private final TreeMap<Integer, ServiceMetaInfo> virtualNodes = new TreeMap<>();
/**
* 虚拟节点数
*/
private static final int VIRTUAL_NODE_NUM = 100;
@Override
public ServiceMetaInfo select(Map<String, Object> requestParams, List<ServiceMetaInfo> serviceMetaInfoList) {
if (serviceMetaInfoList.isEmpty()) {
return null;
}
// 构建虚拟节点环
for (ServiceMetaInfo serviceMetaInfo : serviceMetaInfoList) {
for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
int hash = getHash(serviceMetaInfo.getServiceAddress() + "#" + i);
virtualNodes.put(hash, serviceMetaInfo);
}
}
// 获取调用请求的 hash 值
int hash = getHash(requestParams);
// 选择最接近且大于等于调用请求 hash 值的虚拟节点
Map.Entry<Integer, ServiceMetaInfo> entry = virtualNodes.ceilingEntry(hash);
if (entry == null) {
// 如果没有大于等于调用请求 hash 值的虚拟节点,则返回环首部的节点
entry = virtualNodes.firstEntry();
}
return entry.getValue();
}
/**
* Hash 算法,可自行实现
*
* @param key
* @return
*/
private int getHash(Object key) {
return key.hashCode();
}
}
2.支持配置和扩展负载均衡器
一个成熟的RPC框架可能会支持多个负载均衡器,像序列化器和注册中心-样,目前设定需求是让开发者能够填写配置来指定使用的负载均衡器,并且支持自定义负载均衡器,让框架更易用、更利于扩展。要实现这点,开发方式和序列化器、注册中心都是一样的,都可以使用工厂创建对象、使用SPI动态加载自定义的注册中心。
(1)定义LoadBalancerKeys负载均衡器常量:定义所有支持的负载均衡器键名
/**
* 负载均衡器键名常量
*/
public interface LoadBalancerKeys {
/**
* 轮询
*/
String ROUND_ROBIN = "roundRobin";
/**
* 随机
*/
String RANDOM = "random";
/**
* 一致性Hash
*/
String CONSISTENT_HASH = "consistentHash";
}
(2)工厂模式构建:LoadBalancerFactory(SpiLoader装配)
LoadBalancerFactory可参考此前的序列化器、注册中心相关进行修改即可
/**
* 负载均衡器工厂(工厂模式,用于获取负载均衡器对象)
*/
public class LoadBalancerFactory {
static {
SpiLoader.load(LoadBalancer.class);
}
/**
* 默认负载均衡器
*/
private static final LoadBalancer DEFAULT_LOAD_BALANCER = new RoundRobinLoadBalancer();
/**
* 获取实例
*
* @param key
* @return
*/
public static LoadBalancer getInstance(String key) {
return SpiLoader.getInstance(LoadBalancer.class, key);
}
}
(3)SPI机制装配:在META-INF的rpc/system目录下引入负载均衡接口的SPI配置文件
# SPI配置文件名称
com.noob.rpc.loadbalancer.LoadBalancer
# SPI配置文件内容
roundRobin=com.noob.rpc.loadbalancer.RoundRobinLoadBalancer
random=com.noob.rpc.loadbalancer.RandomLoadBalancer
consistentHash=com.noob.rpc.loadbalancer.ConsistentHashLoadBalancer
(4)为RpcConfig全局配置新增负载默认负载均衡器配置
@Data
public class RpcConfig {
/**
* 负载均衡配置
*/
private String loadBalancer = LoadBalancerKeys.ROUND_ROBIN;
}
3.应用负载均衡器(服务消费方)
修改ServiceProxy代码,将原有“固定调用第一个服务节点”调整为“调用负载均衡器获取一个服务节点”
// 方式2:调用负载均衡算法选择一个服务节点
LoadBalancer loadBalancer = LoadBalancerFactory.getInstance(rpcConfig.getLoadBalancer());
// 将调用方法名(请求路径)作为负载均衡参数
Map<String,Object> requestParams = new HashMap<>();
requestParams.put("methodName",rpcRequest.getMethodName());
ServiceMetaInfo selectedServiceMetaInfo = loadBalancer.select(requestParams,serviceMetaInfoList);
上述代码中,给负载均衡器传入了一个requestParams HashMap,并且将请求方法名作为参数放到了Map中。如果使用的是一致性 Hash算法,那么会根据requestParams计算Hash值,调用相同方法的请求Hash值肯定相同,所以总会请求到同一个服务器节点上。
4.测试
测试负载均衡算法
可替换不同的LoadBalancer测试负载均衡效果
/**
* 负载均衡器测试
*/
public class LoadBalancerTest {
final LoadBalancer loadBalancer = new ConsistentHashLoadBalancer();
@Test
public void select() {
// 请求参数
Map<String, Object> requestParams = new HashMap<>();
requestParams.put("methodName", "apple");
// 服务列表
ServiceMetaInfo serviceMetaInfo1 = new ServiceMetaInfo();
serviceMetaInfo1.setServiceName("myService");
serviceMetaInfo1.setServiceVersion("1.0");
serviceMetaInfo1.setServiceHost("localhost");
serviceMetaInfo1.setServicePort(1234);
ServiceMetaInfo serviceMetaInfo2 = new ServiceMetaInfo();
serviceMetaInfo2.setServiceName("myService");
serviceMetaInfo2.setServiceVersion("1.0");
serviceMetaInfo2.setServiceHost("blog.holic-x.com");
serviceMetaInfo2.setServicePort(80);
List<ServiceMetaInfo> serviceMetaInfoList = Arrays.asList(serviceMetaInfo1, serviceMetaInfo2);
// 连续调用 3 次
ServiceMetaInfo serviceMetaInfo = loadBalancer.select(requestParams, serviceMetaInfoList);
System.out.println(serviceMetaInfo);
Assert.assertNotNull(serviceMetaInfo);
serviceMetaInfo = loadBalancer.select(requestParams, serviceMetaInfoList);
System.out.println(serviceMetaInfo);
Assert.assertNotNull(serviceMetaInfo);
serviceMetaInfo = loadBalancer.select(requestParams, serviceMetaInfoList);
System.out.println(serviceMetaInfo);
Assert.assertNotNull(serviceMetaInfo);
}
}
测试负载均衡调用
sample-consumer中配置application.properties:配置指定的负载均衡算法:rpc.loadBalancer=random
首先在不同的端口启动2个服务提供者,然后启动服务消费者项目,通过Debug或者控制台输出来观察每次请求的节点地址。
扩展说明
扩展说明
(1)实现更多不同算法的负载均衡器
参考思路:比如最少活跃数负载均衡器,选择当前正在处理请求的数量最少的服务提供者
(2)自定义一致性Hash算法中的Hash算法
参考思路:比如根据请求客户端的IP地址来计算Hash值,保证同IP的请求发送给相同的服务提供者