05 ZooKeeper分布式RMI协调实战

ZooKeeper分布式RMI协调实战
摘要由CSDN通过智能技术生成


传送门:


05 分布式RMI协调实战

  如何实现“跨虚拟机”的调用?

它就是 RMI(Remote Method Invocation,远程方法调用)。例如,服务 A 在 JVM1 中运行,服务 B 在 JVM2 中运行,服务 A 与 服务 B 可相互进行远程调用,就像调用本地方法一样,这就是 RMI。在分布式系统中,我们使用 RMI技术可轻松将服务提供者(Service Provider)与 服务消费者(Service Consumer)进行分离,充分体现组件之间的弱耦合,系统架构更易于扩展

image-20211221125606382

我们先从通过一个最简单的 RMI 服务与调用示例,快速掌握 RMI 的使用方法,然后指RMI 的局限性,最后笔者对此问题提供了一种简单的解决方案,即使用 ZooKeeper 轻松解决 RMI 调用过程中所涉及的问题。

1. Java原生RMI实现

  RMI细节流程如下:

image-20211221130117784

再实现RMI服务前,我们搭建框架如下:

image-20211221133019205

1.1 发布RMI服务

  发布RMI服务,我们只需要做三件事:

  1. 定义一个RMI接口
  2. 编写RMI接口的实现类
  3. 通过JNDI发布RMI服务
1.1.1 定义一个RMI接口

  在common包下,新建HelloService类,接口选择:Interface接口。

image-20211221133338923

  RMI接口实际上还是一个普通的Java接口,只是RMI接口必须继承java.rmi.Remote , 此 外 , 每个RMI接口的方法必须声明抛出一个java.rmi.RemoteException 异常,就像下面这样:

package com.bjsxt.remote.common;

import java.rmi.Remote;
import java.rmi.RemoteException;

/**编写普通的java接口,要求继承remote接口
 *定义的方法需要抛出RemoteException异常
 */
public interface HelloService extends Remote {
   
    public String sayHello(String name) throws RemoteException;
}
1.1.2 编写RMI接口的实现类

   在server包下,新建HelloServiceImpl类,接口选择:Class接口。

image-20211221134330425

  实现以上的 HelloService 是一件非常简单的事情,但需要注意的是,我们必须让实现类继承 java.rmi.server.UnicastRemoteObject 类,此外,必须提供一个构造器,并且构造器必须抛出 java.rmi.RemoteException 异常。我们既然使用 JVM 提供的这套 RMI 框架,那么就必须按照这个要求来实现,否则是无法成功发布 RMI 服务的,一 句话:我们得按规矩出牌!

package com.bjsxt.remote.server;

import com.bjsxt.remote.common.HelloService;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

/** 实现类除了实现HelloService以外,还需要继承UnicastRemoteObject类;
 * 添加一个构造方法,需要抛出RemoteException异常
 */
public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
   
    protected HelloServiceImpl() throws RemoteException {
   
    }
    @Override
    public String sayHello(String name) throws RemoteException {
   
        return "Hello"+name;
    }
}
1.1.3 通过JNDI发布RMI服务

   在server包下,新建RMIServer类,接口选择:Class接口。

image-20211221140141849

发布 RMI 服务,我们需要告诉 JNDI 三个基本信息:

  1. 域名或 IP 地址(host)、

  2. 端口号(port)、

  3. 服务名(service),

它们构成了 RMI 协议的 URL(或称为“RMI 地址”):

rmi://<host>:<port>/<service>

如果我们是在本地发布 RMI 服务,那么 host 就是“localhost”。此外,RMI 默认的 port 是“1099”,我们也可以自行设置 port 的值(只要不与其它端口冲突即可)。 service 实际上是一个基于同一 host 与 port 下唯一的服务名,我们不妨使用 Java完全类名(包名.类名)来表示,这样也比较容易保证 RMI 地址的唯一性。

对于我们的示例而言,RMI 地址为:

rmi://localhost:1099/com.bjsxt.remote.server.HelloServiceImpl

我们只需简单提供一个 main() 方法就能发布 RMI 服务,就像下面这样:

package com.bjsxt.remote.server;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

public class RMIServer {
   
    //定义main方法来发布RMI服务
    public static void main(String[] args) throws RemoteException, MalformedURLException {
   
        int port = 1099; //定义端口
        //定义URL
        String url = "rmi://localhost:1099/com.bjsxt.remote.server.HelloServiceImpl";
        //绑定端口号
        LocateRegistry.createRegistry(port);
        //注册具体的服务
        Naming.rebind(url,new HelloServiceImpl());

    }
}

需要注意的是,我们通过 LocateRegistry.createRegistry() 方法在 JNDI 中创建一个注册表,只需提供一个 RMI 端口号即可。此外,通过 Naming.rebind() 方法绑定 RMI 地址与 RMI 服务实现类,这里使用了 rebind() 方法,它相当于先后调用Naming 的 unbind() 与 bind() 方法,只是使用 rebind() 方法来得更加痛快而已,所以我们选择了它。

运行这个 main() 方法,RMI 服务就会自动发布,剩下要做的就是写一个 RMI 客户端来调用已发布的 RMI 服务。

1.2 调用RMI服务

   在client包下,新建RMIClient类,接口选择:Class接口。

image-20211221141908935

  同样我们也使用一个 main() 方法来调用 RMI 服务,相比发布而言,调用会更加简单,我们只需要知道两个东西:

  1. RMI 请求路径、
  2. RMI 接口(一定不需要 RMI 实现类,否则就是本地调用了)

数行代码就能调用刚才发布的 RMI 服务,就像下面这样:

package com.bjsxt.remote.client;

import com.bjsxt.remote.common.HelloService;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class RMIClient {
   
    public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException {
   
        //定义URL
        String url = "rmi://localhost:1099/com.bjsxt.remote.server.HelloServiceImpl";
        // 发现服务
        HelloService hs = (HelloService) Naming.lookup(url);
        //调用远程服务
        String result = hs.sayHello("Jack");
        //打印result
        System.out.println(result);
    }
}

运行时,先运行RMIServer.java(服务器端),再运行RMIClient(客户端)。在控制台中看到“Hello Jack”输出,就表明 RMI 调用成功。

image-20211221143146615

我们在客户端远程调用了服务器端的服务,这就是RMI。

1.3 RMI服务的局限性

  借助 JNDI (Java Naming and Directory Interface)这个所谓的命名与目录服务,我们成功地发布并调用了 RMI 服务。实际上,JNDI 就是一个注册表,服务端将服务对象放入到注册表中,客户端从注册表中获取服务对象。在服务端我们发布了 RMI 服务,并在 JNDI 中进行了注册,此时就在服务端创建了一个 Skeleton(骨架),当客户端第一次成功连接 JNDI 并获取远程服务对象后,立马就在本地创建了一个 Stub(存根),远程通信实际上是通过 Skeleton 与 Stub 来完成的,数据是基于 TCP/IP 协议,在“传输层”上发送的。毋庸置疑,理论上 RMI 一定比 WebService 要快,毕竟 WebService 是基于 HTTP 的,而 HTTP 所携带的数据是通过“应用层”来传输的,传输层较应用层更为底层,越底层越快。

image-20211221144416207

既然 RMI 比 WebService 快,使用起来也方便,那么为什么我们有时候还要用 WebService 呢?

其实原因很简单,WebService 可以实现跨语言系统之间的调用,而 RMI 只能实现 Java系统之间的调用。也就是说,RMI 的跨平台性不如 WebService 好,假如我们的系统都是用 Java 开发的,那么当然首选就是 RMI 服务了。

RMI服务主要有以下两点局限性

  1. RMI 使用了 Java 默认的序列化方式,对于性能要求比较高的系统,可能需要使用其它序列化方案来解决(例如:Protobuf)。
  2. RMI 服务在运行时难免会存在出故障,例如,如果 RMI 服务无法连接了,就会导致客户端无法响应的现象,存在类似于“单点故障”的问题。

  在一般的情况下,Java 默认的序列化方式确实已经足以满足我们的要求了,如果性能方面不是问题的话,我们需要解决的实际上是第二点,也就是说,让系统具备 HA(High Availability,高可用性)。

备注:

  1. https://www.cnblogs.com/wlzjdm/p/7856356.html(JNDI理解)

    JNDI是一个命名目录接口,提供与外界的一个访问关系,只要相关应用、设备能提供服务,那么我们就可以通过JNDI来连接处理。

  2. OSI模型七层网络结构

    img

2. 使用ZooKeeper提供高可用的RMI服务原理分析

  使用Java原生的RMI的例子中,我们的服务端存在单点故障。如果服务器端down掉,我们在使用客户端请求时会无响应。接下来,我们使用ZooKeeper实现高可用的RMI服务。

  要想解决 RMI 服务的高可用性问题,我们需要利用 ZooKeeper 充当一个服务注册表(Service Registry),让多个服务提供者(Service Provider)形成一个集群(每一个服务提供者在zookeeper树形结构上注册一个临时节点,临时节点里面存放服务提供者中RMI的url,客户端可以访问对应的节点来访问具体的服务提供者),让服务消费者(Service Consumer)通过服务注册表获取具体的服务访问地址(也就是 RMI 服务地址)去访问具体的服务提供者。如下图所示:

image-20211221155352657

详细实现原理图

image-20211221155853841

执行流程:

  1. 每一个服务提供者建立zk连接,并在zookeeper树形结构上注册一个顺序临时节点,临时节点里面存放服务提供者中RMI的url
  2. 服务消费者(Service Consumer)通过与服务注册表建立连接,获取节点中的数据来获取具体的服务访问地址,并添加监听节点
  3. 从列表中随机选取节点
  4. 通过RMI的url寻找对应的服务
  5. 获取远程服务对象xxServiceImpl
  6. 客户端调用RMI服务
  7. 返回结果

需要注意的是,服务注册表并不是 Load Balancer(负载均衡器),提供的不是“反向代理”服务,而是“服务注册”与“心跳检测”功能。利用服务注册表来注册 RMI 地址,这个很好理解,那么“心跳检测”又如何理解呢?说白了就是通过服务中心定时向各个服务提供者发送一个请求(实际上建立的是一个 Socket长连接),如果长期没有响应,服务中心就认为该服务提供者已经“挂了”,只会从还“活 着”的服务提供者中选出一个做为当前的服务提供者 。也许服务中心可能会出现单点故障,如果服务注册表都坏掉了,整个系统也就瘫痪了。看来要想实现这个架构,必须保证服务中心也具备高可用性。ZooKeeper 正好能够满足我们上面提到的所有需求。

  使用 ZooKeeper 的临时性 ZNode 来存放服务提供者的 RMI 地址,一旦与服务提供者的 Session 中断,会自动清除相应的 ZNode。让服务消费者去监听这些 ZNode,一旦发现 ZNode 的数据(RMI 地址)有变化,就会重新获取一份有效数据的拷贝。 ZooKeeper 与生俱来的集群能力(例如:数据同步与领导选举特性),可以确保服务注册表的高可用性。

3. 使用ZooKeeper实现RMI高可用代码剖析

3.1 通用接口

  在common包下,定义Constant接口:

image-20211221163424357

package com.bjsxt.remote.common;

public interface Constant {
   
    //定义zk连接地址
    String ZK_CONNECTION_STRING = "192.168.236.32.2181,192.168.236.33.2181,192.168.236.34.2181";
    //session失效时间50s
    int ZK_SESSION_TIMEOUT = 5000;
    //服务注册表中使用的父节点
    String ZK_REGISTRY_PATH = "/registry";
    //子节点
    String ZK_PROVIDER_PATH = ZK_REGISTRY_PATH + "/provide";
}
3.2 服务端

   在server包下,定义ServiceProvider类:
image-20211221170143607
  需要编写一个 ServiceProvider 类,来发布 RMI 服务,并将 RMI 地址注册到ZooKeeper 中(实际存放在 ZNode 上)。

package com.bjsxt.remote.server;

import com.bjsxt.remote.common.Constant;
import org.apache.zookeeper.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.util.concurrent.CountDownLatch;


public class ServiceProvider {
   
    public static final Logger LOGGER = (Logger) LoggerFactory.getLogger(ServiceProvider.class);

    // 用于等待SysConnected事件触发后继续执行当前线程
    private CountDownLatch latch = new CountDownLatch(1);

    //发布RMI服务并注册RMI地址到ZooKeeper中
    public void publish(Remote remote, String host, int port) {
   
        //发布RMI服务并返回RMI地址(url地址)
        
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值