5.负载均衡和SPI机制的实现

该部分的源码地址:https://github.com/543211494/MyRpc/tree/MyRpc-loadbalance-spi

下一章:重试和容错机制的实现

1.负载均衡

在上个版本实现的服务注册/发现中心中存在一个问题:当多个相同服务都注册至Zookeeper中时该如何选择服务。

因此,我们需要实现负载均衡策略在服务列表中进行选择,目前实现了随机负载均衡,带权重的随机负载均衡和轮询负载均衡策略。

首先定义一个负载均衡接口便于后续拓展:

public interface LoadBalancer {

    /**
     * 选择要调用的服务
     */
    public ServiceInfo select(List<ServiceInfo> serviceInfoList);
}

以下是目前实现的负载均衡策略,用户可根据需求选择使用:

随机负载均衡

public class RandomLoadBalancer implements LoadBalancer{

    private final Random random = new Random();

    @Override
    public ServiceInfo select(List<ServiceInfo> serviceInfoList) {
        int index = random.nextInt(serviceInfoList.size());
        return serviceInfoList.get(index);
    }
}

带权重的随机负载均衡

public class WeightedRandomLoadBalancer implements LoadBalancer {

    private final Random random = new Random();

    @Override
    public ServiceInfo select(List<ServiceInfo> serviceInfoList) {
        int totalWeight = 0;
        for(int i = 0;i<serviceInfoList.size();i++){
            totalWeight += serviceInfoList.get(i).getWeight();
        }
        int number = random.nextInt(totalWeight);
        for(int i = 0;i<serviceInfoList.size();i++){
            number -= serviceInfoList.get(i).getWeight();
            if(number<0){
                return serviceInfoList.get(i);
            }
        }
        return serviceInfoList.get(0);
    }
}

轮询负载均衡

public class RoundRobinLoadBalancer implements LoadBalancer{
    /**
     * 当前轮询的下标
     */
    private final AtomicInteger currentIndex = new AtomicInteger(0);

    @Override
    public ServiceInfo select(List<ServiceInfo> serviceMetaInfoList) {
        int size = serviceMetaInfoList.size();
        int current = currentIndex.getAndIncrement();
        int index = current%size;
        /**
         * 使用自旋锁更新currentIndex值
         */
        if(current>=size){
            int next;
            do {
                current = currentIndex.get();
                next = (current) % size;
            } while (!currentIndex.compareAndSet(current, next));
        }
        return serviceMetaInfoList.get(index);
    }
}

当客户端查询到服务端列表时,使用负载均衡策略进行选择

public class ServiceProxy implements InvocationHandler {

    private static final Serializer serializer = new JdkSerializer();

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        RpcRequest rpcRequest = new RpcRequest();
        String serviceName = method.getDeclaringClass().getName();
        /* 不能用完整路径,得用接口名 */
        serviceName = serviceName.substring(serviceName.lastIndexOf('.')+1);
        rpcRequest.setServiceName(serviceName);
        rpcRequest.setMethodName(method.getName());
        rpcRequest.setParameterTypes(method.getParameterTypes());
        rpcRequest.setArgs(args);

        try {
            /* 序列化 */
            byte[] data = serializer.serialize(rpcRequest);
            /* 发送请求 */
            String url = RpcApplication.rpcConfig.getClient().getAddress();
            if(RpcApplication.registry!=null){
                List<ServiceInfo> services = RpcApplication.registry.serviceDiscovery(RpcApplication.rpcConfig.getClient().getServiceName());
                /**
                 * 使用选定的负载均衡策略选择服务
                 */
                if(services!=null&&!services.isEmpty()){
                    url = RpcApplication.loadBalancer.select(services).getAddress();
                }
            }
            try (HttpResponse httpResponse = HttpRequest.post(url)
                    .body(data)
                    .execute()) {
                byte[] result = httpResponse.bodyBytes();
                // 反序列化
                RpcResponse rpcResponse = serializer.deserialize(result, RpcResponse.class);
                return rpcResponse.getData();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

2.SPI机制

SPI机制的主要作用是将接口的多个实现类路径与用户的配置key关联起来,用于提高系统拓展性,以负载均衡接口为例进行说明。

框架中定义了LoadBalancer接口,并实现了一系列负载均衡实现类。为方便用户使用,希望在application.properties配置使用的负载均衡策略框架可以自动实例化对应的接口实现类,这就依赖于SPI机制。

首先在resource/META-INF/rpc/spi.properties中写负载均衡实现类的key和实现类路径的对对应关系,如下所示:

com.lzy.rpc.loadbalancer.LoadBalancer.random=com.lzy.rpc.loadbalancer.RandomLoadBalancer
com.lzy.rpc.loadbalancer.LoadBalancer.roundRobin=com.lzy.rpc.loadbalancer.RoundRobinLoadBalancer
com.lzy.rpc.loadbalancer.LoadBalancer.weightedRandom=com.lzy.rpc.loadbalancer.WeightedRandomLoadBalancer

在服务端启动时读取该文件,将对应关系存入map中,当需要用到负载均衡实例时,先获取用户配置,再利用反射实例化对应的实现类。

例如,用户在配置文件中写:

rpc.client.loadBalancerPolicy=random

那么就可以通过SPI可以的知要实例化的类是com.lzy.rpc.loadbalancer.RandomLoadBalancer,当需要使用负载均衡策略时即可实例化该类。SPI机制极大提高了系统拓展性,用户也可实现LoadBalancer接口自定义负载均衡策略

以下是SPI加载器的实现:

public class SpiLoader {

    /**
     * 存储已加载的类,其结构为:
     * 接口完整路径.实现类对应的标识:实现类
     */
    private static final Map<String, Class<?>> loaderMap = new HashMap<>();

    /**
     * SPI扫描目录
     */
    private static final String RPC_SPI_CONFIG = "META-INF/rpc/spi.properties";

    public static void init(){
        //System.out.println(LoadBalancer.class.getName());
        /**
         * 加载文件
         */
        List<URL> resources = ResourceUtil.getResources(SpiLoader.RPC_SPI_CONFIG);
        //System.out.println(resources.get(0).getPath());
        for (int i = 0;i<resources.size();i++) {
            InputStreamReader inputStreamReader = null;
            BufferedReader bufferedReader = null;
            try {
                inputStreamReader = new InputStreamReader(resources.get(i).openStream());
                bufferedReader = new BufferedReader(inputStreamReader);
                String line;
                while ((line = bufferedReader.readLine()) != null) {
                    //System.out.println(line);
                    String[] strArray = line.split("=");
                    if (strArray.length > 1) {
                        String key = strArray[0];
                        String className = strArray[1];
                        SpiLoader.loaderMap.put(key, Class.forName(className));
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                /**
                 * 释放
                 */
                try{
                    if(bufferedReader != null) {
                        bufferedReader.close();
                    }
                    if(inputStreamReader != null){
                        inputStreamReader.close();
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 获取指定接口的指定实现类类型
     */
    public static Class<?> getClazz(String interfaceName,String key){
        return SpiLoader.loaderMap.get(interfaceName+"."+key);
    }
}

3.配置信息

配置信息写于resource文件夹下的application.properties文件中,目前版本更新了一些配置参数,以下是详细介绍:

3.1服务端配置信息

#服务端地址,用于向注册中心注册
rpc.server.host=127.0.0.1
#服务端服务名称,用于向注册中心注册
rpc.server.serviceName=test
#服务端端口
rpc.server.port=8081
#服务权重
rpc.server.weight=2
#是否启用注册中心
rpc.useRegistry=true
#注册中心地址
rpc.registry.host=127.0.0.1
#注册中心端口号
rpc.registry.port=2181
#连接注册中心超时时间,单位毫秒
rpc.registry.timeout=10000
#连接注册中心最大重试次数
rpc.registry.maxRetries=3

3.2客户端配置信息

#要连接的服务端服务名称,用于向注册中心发现服务
rpc.client.serviceName=test
#要连接的服务端地址,用于不启用注册中心时连接服务端
rpc.client.serverHost=127.0.0.1
#要连接的服务端端口号,用于不启用注册中心时连接服务端
rpc.client.serverPort=8081
#负载均衡策略,目前支持random、weightedRandom、roundRobin三种负载均衡策略
rpc.client.loadBalancerPolicy=random
#是否启用注册中心
rpc.useRegistry=true
#注册中心地址
rpc.registry.host=127.0.0.1
#注册中心端口号
rpc.registry.port=2181
#连接注册中心超时时间,单位毫秒
rpc.registry.timeout=10000
#连接注册中心最大重试次数
rpc.registry.maxRetries=3

4.启动方法

项目为标准的Maven项目

provider下为服务端代码,执行其中的main函数即可启动服务端,服务端启动方式如下

public static void main(String[] args) {
    ProviderBootstrap.run();
}

目前服务端以实现了自动注解和读取配置文件application.properties

只需要在实现接口的类上加上注解@RpcService即可自动注册,无需手动注册,例如

@RpcService
public class CalculatorServiceImpl implements CalculatorService {

    @Override
    public int add(int a, int b) {
        return a+b;
    }
}

consumer文件夹下为客户端代码,通过代理工厂获取代理类,从而调用服务端的远程方法

public static void main(String[] args) {
    ConsumerBootstrap.init();
    CalculatorService calculatorService = ServiceProxyFactory.getProxy(CalculatorService.class);
    System.out.println(calculatorService.add(1,2));
}

附录:项目文件结构

.
├── common    公共接口,用于演示用法
│   ├── pom.xml
│   └── src
│       └── main
│           └── java
│               └── com
│                   └── lzy
│                       └── common
│                           └── CalculatorService.java
├── consumer   客户端,用于演示用法
│   ├── pom.xml
│   └── src
│       └── main
│           ├── java
│           │   └── com
│           │       └── lzy
│           │           └── consumer
│           │               └── Main.java
│           └── resources
│               └── application.properties
├── pom.xml
├── provider   服务端,用于演示用法
│   ├── pom.xml
│   └── src
│       └── main
│           ├── java
│           │   └── com
│           │       └── lzy
│           │           └── provider
│           │               ├── CalculatorServiceImpl.java
│           │               └── Main.java
│           └── resources
│               └── application.properties
├── README.md
└── rpc-core
    ├── pom.xml
    └── src
        ├── main
        │   ├── java
        │   │   └── com
        │   │       └── lzy
        │   │           └── rpc
        │   │               ├── anno  注解
        │   │               │   └── RpcService.java    用于实例化服务提供类的注解
        │   │               ├── bean
        │   │               │   ├── RpcRequest.java    RPC请求实体类
        │   │               │   ├── RpcResponse.java   RPC回复实体类
        │   │               │   └── ServiceInfo.java   服务端注册信息实体类
        │   │               ├── bootstrap  客户端/服务端启动类
        │   │               │   ├── ConsumerBootstrap.java
        │   │               │   └── ProviderBootstrap.java
        │   │               ├── config
        │   │               │   ├── ClientConfig.java    客户端配置类
        │   │               │   ├── Constant.java        常数类
        │   │               │   ├── RegistryConfig.java  注册中心配置类
        │   │               │   ├── RpcConfig.java       总配置类,存放所有配置类
        │   │               │   └── ServerConfig.java    服务端配置类
        │   │               ├── consumer   客户端调用部分
        │   │               │   └── proxy  客户端代理类及代理工厂
        │   │               │       ├── ServiceProxyFactory.java
        │   │               │       └── ServiceProxy.java
        │   │               ├── loadbalancer
        │   │               │   ├── LoadBalancer.java               负载均衡接口
        │   │               │   ├── LoadBalancerPolicy.java         标识负载均衡策略的常量
        │   │               │   ├── RandomLoadBalancer.java         随机负载均衡实现
        │   │               │   ├── RoundRobinLoadBalancer.java     带权重的随机负载均衡实现
        │   │               │   └── WeightedRandomLoadBalancer.java 轮询负载均衡实现
        │   │               ├── provider
        │   │               │   ├── registry
        │   │               │   │   ├── LocalRegistry.java   本地对象注册中心
        │   │               │   │   ├── Registry.java        注册中心接口
        │   │               │   │   └── ZooKeeperRegistry.java   zookeeper注册中心操作类
        │   │               │   └── server  netty服务器
        │   │               │       ├── NettyRpcServer.java
        │   │               │       └── NettyServerHandler.java
        │   │               ├── RpcApplication.java   存储配置类实例和注册中心类实例
        │   │               └── util
        │   │                   ├── ConfigUtil.java     配置加载类
        │   │                   ├── JdkSerializer.java  jdk序列化类
        │   │                   ├── Serializer.java     序列化接口
        │   │                   └── SpiLoader.java      spi加载器
        │   └── resources
        │       ├── log4j.properties  日志配置文件(用于关闭curator日志打印)
        │       └── META-INF
        │           └── rpc
        │               └── spi.properties   spi配置文件
        └── test
            └── java
  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黎汝聪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值