跳至主要內容

【noob-rpc】⑨扩展版RPC-负载均衡(服务消费端)

holic-x...大约 11 分钟项目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来进行处理

image-20240414211930000

​ 一致性还解决了节点下线倾斜问题

节点下线

​ 节点下线:当某个节点下线时,欺载会被平均分摊到其他节点上,而不会影响到整个系统的稳定性,因为只有部分请求会受到影响

​ 如上图所示,服务器C下线后,请求A会交给服务器A来处理(顺时针寻找第一个大于或等于该哈希值的节点) ,而服务器B接收到的请求保持不变

​ 如果是轮询取模算法,只要节点数变了,很有可能大多数服务器处理的请求都要发生变化,对系统的影响巨大。

倾斜问题

​ 通过虚拟节点的引入,将每个物理节点映射到多个虚拟节点上,使得节点在哈希环上的分布更加均均,减少了节点间的负载差异。

image-20240414212422833image-20240414212520666

步骤说明

涉及文件、代码结构梳理

# noob-rpc-core
loadbalancer:
	- LoadBalancer
	- RoundRobinLoadBalancer
	- RandomLoadBalancer
	- ConsistentHashLoadBalancer
	- LoadBalancerKeys
	- LoadBalancerFactory

config:
	- RpcConfig

proxy:
	- ServiceProxy

test:
	- VertxTcpServerTest
	
# sample-consumer
application.properties

image-20240414223805267

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

image-20240414214330646

(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);

image-20240414215352801

​ 上述代码中,给负载均衡器传入了一个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或者控制台输出来观察每次请求的节点地址。

image-20240414221109625

扩展说明

扩展说明

(1)实现更多不同算法的负载均衡器

​ 参考思路:比如最少活跃数负载均衡器,选择当前正在处理请求的数量最少的服务提供者

(2)自定义一致性Hash算法中的Hash算法

​ 参考思路:比如根据请求客户端的IP地址来计算Hash值,保证同IP的请求发送给相同的服务提供者

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3