RPC项目升级笔记(四):设计一个ZooKeeper注册中心 ,负载均衡

1,062 阅读4分钟

前提摘要

  1. 服务端与客户端通信的host与port是预先就确定好的,每一个客户端都必须知道对应服务的ip与端口号, 并且如果服务挂了或者换地址了,就很麻烦。扩展性也不强, 所以我们需要引入注册中心
  2. 在RPC架构中,一般一个服务会有多个提供者支持,如何分散服务提供者的压力?引入负载均衡 image.png RPC三大要素客户端和服务端我们都已经有了,现在就只剩下注册中心这最后一块拼图了!Let's go!🥳
  • Zookeeper

注册中心的地址是固定的(为了高可用一般是集群,我们看做黑盒即可),服务端上线时,在注册中心注册自己的服务与对应的地址,而客户端调用服务时,去注册中心根据服务名找到对应的服务端地址。

  • Curator

Curator是一套Zookeeper客户端框架,解决了很多 Zookeeper 客户端非常底层的细节开发工作,这次项目的升级便基于此。

Zookeeper注册中心

首先在本机安装Zookeeper,默认端口为2181。

定义服务注册接口

// 服务注册接口,两大基本功能,注册:保存服务与地址。 查询:根据服务名查找地址
public interface ServiceRegister {
    void register(String serviceName, InetSocketAddress serverAddress);
    InetSocketAddress serviceDiscovery(String serviceName);
}

服务注册接口的实现类为

public class ZkServiceRegister implements ServiceRegister {
    // curator 提供的zookeeper客户端
    private CuratorFramework client;
    // zookeeper根路径节点
    private static final String ROOT_PATH = "MyRPC";

    // 这里负责zookeeper客户端的初始化,并与zookeeper服务端建立连接
    public ZkServiceRegister(){
        // 指数时间重试
        RetryPolicy policy = new ExponentialBackoffRetry(1000, 3);
        // zookeeper的地址固定,不管是服务提供者还是,消费者都要与之建立连接
        // sessionTimeoutMs 与 zoo.cfg中的tickTime 有关系,
        // zk还会根据minSessionTimeout与maxSessionTimeout两个参数重新调整最后的超时值。默认分别为tickTime 的2倍和20倍
        // 使用心跳监听状态
        this.client = CuratorFrameworkFactory.builder().connectString("127.0.0.1:2181")
                .sessionTimeoutMs(40000).retryPolicy(policy).namespace(ROOT_PATH).build();
        this.client.start();
        System.out.println("zookeeper 连接成功");
    }

    @Override
    public void register(String serviceName, InetSocketAddress serverAddress){
        try {
            // serviceName创建成永久节点,服务提供者下线时,不删服务名,只删地址
            if(client.checkExists().forPath("/" + serviceName) == null){
                client.create()
                        .creatingParentsIfNeeded()
                        .withMode(CreateMode.PERSISTENT)
                        .forPath("/" + serviceName);
            }
            // 路径地址,一个/代表一个节点
            String path = "/" + serviceName +"/"+ getServiceAddress(serverAddress);
            // 临时节点,服务器下线就删除节点
            client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path);
        } catch (Exception e) {
            System.out.println("此服务已存在");
        }
    }
    // 根据服务名返回地址
    @Override
    public InetSocketAddress serviceDiscovery(String serviceName) {
        try {
            List<String> strings = client.getChildren().forPath("/" + serviceName);
            // 这里默认用的第一个,后面加负载均衡
            String string = strings.get(0);
            return parseAddress(string);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    // 地址 -> XXX.XXX.XXX.XXX:port 字符串
    private String getServiceAddress(InetSocketAddress serverAddress) {
        return serverAddress.getHostName() +
                ":" +
                serverAddress.getPort();
    }
    // 字符串解析为地址
    private InetSocketAddress parseAddress(String address) {
        String[] result = address.split(":");
        return new InetSocketAddress(result[0], Integer.parseInt(result[1]));
    }
}

客户端升级

实例客户端的时候不再需要传入host,post的值了,实例在发送请求即sendRequest的过程中会自动通过ZkServiceRegister获取到zookeeper注册中心获取到当前服务需要发送到地址(服务提供端的所在地。

\\不再需要传入host,post
RPCClient rpcClient = new NettyRPCClient();
\\获取注册中心存储的信息
InetSocketAddress address = serviceRegister.serviceDiscovery(request.getInterfaceName());
host = address.getHostName();
port = address.getPort();

服务端升级

客户端不需要再写入host,post了,相应的服务端就需要给注册中心传它能提供的服务以及它的host,port。

ServiceProvider serviceProvider = new ServiceProvider("127.0.0.1", 8899);

在服务提供类加入注册的功能

public class ServiceProvider {

    private Map<String, Object> interfaceProvider;
    private String host;
    private int port;
    private ServiceRegister serviceRegister;

    public ServiceProvider(String host, int port){
        this.host = host;
        this.port = port;
        this.serviceRegister = new ZkServiceRegister();
        this.interfaceProvider = new HashMap<>();
    }

    public void provideServiceInterface(Object service){
        Class<?>[] interfaces = service.getClass().getInterfaces();

        for(Class clazz : interfaces){
            interfaceProvider.put(clazz.getName(),service);
            // 在注册中心注册服务
            serviceRegister.register(clazz.getName(),new InetSocketAddress(host,port));
        }

    }

    public Object getService(String interfaceName){
        return interfaceProvider.get(interfaceName);
    }
}

负载均衡

public InetSocketAddress serviceDiscovery(String serviceName) {
    try {
        List<String> strings = client.getChildren().forPath("/" + serviceName);
        // 这里默认用的第一个,后面加负载均衡
        String string = strings.get(0);
        return parseAddress(string);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

上面的代码中其实还存在一个小问题,每次我们客户端问注册中心要服务对应的地址的时候,永远是给第一个服务者的地址,这显然是不合理的,我们需要分散服务提供者的压力。

  • 负载均衡接口
// 给服务器地址列表,根据不同的负载均衡策略选择一个
public interface LoadBalance {
    String balance(List<String> addressList);
}

/**
 * 随机负载均衡
 */
public class RandomLoadBalance implements  LoadBalance{
    @Override
    public String balance(List<String> addressList) {

        Random random = new Random();
        int choose = random.nextInt(addressList.size());
        System.out.println("负载均衡选择了" + choose + "服务器");
        return addressList.get(choose);
    }
}

/**
 * 轮询负载均衡
 */
public class RoundLoadBalance implements LoadBalance{
    private int choose = -1;
    @Override
    public String balance(List<String> addressList) {
        choose++;
        choose = choose%addressList.size();
        return addressList.get(choose);
    }
}

本次升级就告一段落了🥳🥳🥳,现代码已经上传至github.com/zhangyuxuan…