《把玩Dubbo系列》盘盘Dubbo和RPC

1,670 阅读14分钟

每日一笑:一天,妈妈临走时跟我和爸爸说:“现在我们家要好好打扫卫生”,于是在门上贴了一张纸,写的是“爱护环境,人人有责”,然后就走了。 我想:我要观察牵牛花,没时间,于是在人上加一个横,变成了“爱护环境,大人有责”,爸爸也不想打扫卫生,于是在大底下加一个点,把另一个人上加一个横,再在底下加一个点,变成了“爱护环境,太太有责”。 妈妈回来了,看看那张纸上的字:爱护环境,太太有责。怒发冲冠的说:“啊,这是怎么回事! ”

Dubbo出现的背景

乱世出英雄:随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进。

架构演进

单一应用架构

当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。

垂直应用架构

当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,提升效率的方法之一是将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。

分布式服务架构

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。

流动计算架构

当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。

对于各种架构的个人理解

上面说了各种架构,在我的理解中:

  • 单一应用架构:其实就是我们最开始的ssm(spring+springmvc+mybatis)或者ssh(spring+springmvc+hibernate)就是把所有的功能都部署在一个工程里,这种架构中,用来简化增删改查工作量的数据访问框架(ORM)是关键。

  • 垂直应用架构:由于随着业务量扩展,一个应用已经涵盖了太多的东西,以电商系统为例:把商品的功能(上新品、类目、品牌等功能)和营销的功能(优惠券、活动等功能)和交易的功能都在一个应用中,项目启动要10分钟,各个功能随便动一动都得发布,造成了极大的困扰,所以需要把各个将一个应用拆分成不同的应用,应用之间独立部署,每个应用都需要有对应的前端页面,所以这种架构下,用于加速前端页面开发的Web框架(MVC)是关键。

  • 分布式服务架构:垂直应用架构后,应用会越来越多,越来越多,应用与应用之间的交互肯定也会越来越紧密,这种时候应用之间很容易造成循环依赖,等到发布的时候又是一头包,所以将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,在这种架构下,用于提高业务复用及整合的分布式服务框架(RPC)是关键。

  • 流动计算架构:拆分成各个服务后,服务越来越多,随着业务的侧重点不同,有些边缘化的业务也会占用资源,服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。

Dubbo到底是什么

上面简述了各个架构,其中在分布式架构中提到了由于垂直应用架构带来的应用多的问题,在这种场景下,需要一种用于提高业务服用及整合的RPC框架,而我们的dubbo,就是一款高性能的,基于java的RPC开源框架。

什么是RPC框架

RPC(Remote Procedure Call)远程过程调用,简单的理解是一个节点请求另一个节点提供的服务

既然是远程服务调用肯定要和本地服务调用作对比:

  • 本地服务调用:如果需要将本地teacher对象的age+1,可以实现一个addAge()方法,将teacher对象传入,对年龄进行更新之后返回即可,本地方法调用的函数体通过函数指针来指定。

  • 远程服务调用:上述操作的过程中,如果addAge()这个方法在服务端

实现远程服务调用需要满足什么?因为执行函数的函数体在远程机器上,如何告诉机器需要调用这个方法呢?而这个也就是rpc框架需要做的事。

远程服务调用的条件

要满足远程服务调用,要满足以下三个条件:

  • 首先客户端需要告诉服务器,需要调用的函数,这里函数和进程ID存在一个映射,客户端远程调用时,需要查一下函数,找到对应的ID,然后执行函数的代码(找到方法)。

  • 客户端需要把本地参数传给远程函数,本地调用的过程中,直接压栈即可,但是在远程调用过程中不再同一个内存里,无法直接传递函数的参数,因此需要客户端把参数转换成字节流,传给服务端,然后服务端将字节流转换成自身能读取的格式,是一个序列化和反序列化的过程(找到参数)。

  • 数据准备好了之后,需要进行传输,网络传输层需要把调用的ID和序列化后的参数传给服务端,然后把计算好的结果序列化传给客户端,因此需要有各种网络协议(TCP、HTTP等等)

RPC demo


客户端:
public class RPCClient<T> {
    public static <T> T getRemoteProxyObj(final Class<?> serviceInterface, final InetSocketAddress addr) {
        // 1.将本地的接口调用转换成JDK的动态代理,在动态代理中实现接口的远程调用
        return (T) Proxy.newProxyInstance(serviceInterface.getClassLoader(), new Class<?>[]{serviceInterface},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Socket socket = null;
                        ObjectOutputStream output = null;
                        ObjectInputStream input = null;
                        try{
                            // 2.创建Socket客户端,根据指定地址连接远程服务提供者
                            socket = new Socket();
                            socket.connect(addr);

                            // 3.将远程服务调用所需的接口类、方法名、参数列表等编码后发送给服务提供者
                            output = new ObjectOutputStream(socket.getOutputStream());
                            output.writeUTF(serviceInterface.getName());
                            output.writeUTF(method.getName());
                            output.writeObject(method.getParameterTypes());
                            output.writeObject(args);

                            // 4.同步阻塞等待服务器返回应答,获取应答后返回
                            input = new ObjectInputStream(socket.getInputStream());
                            return input.readObject();
                        }finally {
                            if (socket != null){
                                socket.close();
                            }
                            if (output != null){
                                output.close();
                            }
                            if (input != null){
                                input.close();
                            }
                        }
                    }
                });
    }
}

服务端:
public class ServiceCenter implements Server {

    private static ExecutorService executor = Executors.newFixedThreadPool
    (Runtime.getRuntime().availableProcessors());

    private static final HashMap<String, Class> serviceRegistry = 
    new HashMap<String, Class>();

    private static boolean isRunning = false;

    private static int port;


    public ServiceCenter(int port){
        ServiceCenter.port = port;
    }


    @Override
    public void start() throws IOException {
        ServerSocket server = new ServerSocket();
        server.bind(new InetSocketAddress(port));
        System.out.println("Server Start .....");
        try{
            while(true){
                executor.execute(new ServiceTask(server.accept()));
            }
        }finally {
            server.close();
        }
    }

    @Override
    public void register(Class serviceInterface, Class impl) {
        serviceRegistry.put(serviceInterface.getName(), impl);
    }

    @Override
    public boolean isRunning() {
        return isRunning;
    }

    @Override
    public int getPort() {
        return port;
    }

    @Override
    public void stop() {
        isRunning = false;
        executor.shutdown();
    }
   private static class ServiceTask implements Runnable {
        Socket client = null;

        public ServiceTask(Socket client) {
            this.client = client;
        }

        @Override
        public void run() {
            ObjectInputStream input = null;
            ObjectOutputStream output = null;
            try{
                input = new ObjectInputStream(client.getInputStream());
                String serviceName = input.readUTF();
                String methodName = input.readUTF();
                Class<?>[] parameterTypes = (Class<?>[]) input.readObject();
                Object[] arguments = (Object[]) input.readObject();
                Class serviceClass = serviceRegistry.get(serviceName);
                if(serviceClass == null){
                    throw new ClassNotFoundException(serviceName + "not found!");
                }
                Method method = serviceClass.getMethod(methodName, parameterTypes);
                Object result = method.invoke(serviceClass.newInstance(), arguments);

                output = new ObjectOutputStream(client.getOutputStream());
                output.writeObject(result);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                if(output!=null){
                    try{
                        output.close();
                    }catch (IOException e){
                        e.printStackTrace();
                    }
                }
                if (input != null) {
                    try {
                        input.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (client != null) {
                    try {
                        client.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

这里客户端只需要知道Server端的接口ServiceProducer即可,服务端在执行的时候,
会根据具体实例调用实际的方法ServiceProducerImpl,符合面向对象过程中父类引用指向子类对象。

回过头再来看看dubbo,既然dubbo是一个RPC框架,那么肯定满足了上面的三点要求,接下来,我们根据dubbo工程的模块,来看看dubbo到底有一些什么东西。

Dubbo模块分包

在dubbo官方文档的框架设计栏中有下面这张图:

dubbo模块分包

从以上这个图我们可以清晰的看到各个模块之间依赖关系,其实以上的图只是展示了关键的模块依赖关系,还有部分模块比如dubbo-bootstrap清理模块等,下面我会对各个模块做个简单的介绍,至少弄明白各个模块的作用。

Dubbo模块

dubbo-registry——注册中心模块

注册中心模块

基于注册中心下发地址的集群方式,以及对各种注册中心的抽象。

dubbo-registry-api:抽象了注册中心的注册和发现,实现了一些公用的方法,让子类只关注部分关键方法。

剩余的包就是具体的注册中心的实现,dubbo的注册中心实现有Multicast注册中心、Zookeeper注册中心、Redis注册中心、Simple注册中心、consul注册中心、eureka注册中心、nacos注册中心等等,这个模块就是封装了dubbo所支持的注册中心的实现。

dubbo-cluster——集群模块

为了避免单点故障,现在的应用通常至少会部署在两台服务器上。对于一些负载比较高的服务,会部署更多的服务器。这样,在同一环境下的服务提供者数量会大于1。对于服务消费者来说,同一环境下出现了多个服务提供者。这时会出现一个问题,服务消费者需要决定选择哪个服务提供者进行调用。另外服务调用失败时的处理措施也是需要考虑的,是重试呢,还是抛出异常,亦或是只打印异常等。

为了处理这些问题,Dubbo 定义了集群接口 Cluster 以及 Cluster Invoker。集群 Cluster 用途是将多个服务提供者合并为一个 Cluster Invoker,并将这个 Invoker 暴露给服务消费者。这样一来,服务消费者只需通过这个 Invoker 进行远程调用即可,至于具体调用哪个服务提供者,以及调用失败后如何处理等问题,现在都交给集群模块去处理。

集群模块

将多个服务提供方伪装为一个提供方,包括:负载均衡, 容错,路由等,集群的地址列表可以是静态配置的,也可以是由注册中心下发。

集群模块是服务提供者和服务消费者的中间层,为服务消费者屏蔽了服务提供者的情况,这样服务消费者就可以专心处理远程调用相关事宜。比如发请求,接受服务提供者返回的数据等。这就是集群的作用。

  • configurator包:配置包,dubbo的基本设计原则是采用URL作为配置信息的统一格式,所有拓展点都通过传递URL携带配置信息,这个包就是用来根据统一的配置规则生成配置信息。

  • directory包:Directory 代表了多个 Invoker,并且它的值会随着注册中心的服务变更推送而变化 。这里介绍一下Invoker,Invoker是Provider的一个调用Service的抽象,Invoker封装了Provider地址以及Service接口信息。

  • loadbalance包:封装了负载均衡的实现,负责利用负载均衡算法从多个Invoker中选出具体的一个Invoker用于此次的调用,如果调用失败,则需要重新选择。

  • merger包:封装了合并返回结果,分组聚合到方法,支持多种数据结构类型。

  • router包:封装了路由规则的实现,路由规则决定了一次dubbo服务调用的目标服务器,路由规则分两种:条件路由规则和脚本路由规则,并且支持可拓展。

  • support包:封装了各类Invoker和cluster,包括集群容错模式和分组聚合的cluster以及相关的Invoker。

dubbo-common——公共逻辑模块

包括 Util 类和通用模型。

工具类就是一些公用的方法,通用模型就是贯穿整个项目的统一格式的模型,比如URL,上述就提到了URL贯穿了整个项目。

公共逻辑模块

目前都移动到org.apache.dubbo目录下

dubbo-config——配置模块

配置模块

是 Dubbo 对外的 API,用户通过 Config 使用Dubbo,隐藏 Dubbo 所有细节。dubbo也提供了四种配置方式,包括XML配置、属性配置、API配置、注解配置,配置模块就是实现了这四种配置的功能。

  • dubbo-config-api:实现了API配置和属性配置的功能。
  • dubbo-config-spring:实现了XML配置和注解配置的功能。

dubbo-rpc——远程调用模块

抽象各种协议,以及动态代理,只包含一对一的调用,不关心集群的管理。远程调用,最主要的肯定是协议,dubbo提供了许许多多的协议实现,不过官方推荐时使用dubbo自己的协议。

  • dubbo-rpc-api:抽象了动态代理和各类协议,实现一对一的调用
  • 另外的包都是各个协议的实现。
  • 这个模块依赖于dubbo-remoting模块,抽象了各类的协议。

dubbo-remoting——远程通信模块

相当于 Dubbo 协议的实现,如果 RPC 用 RMI协议则不需要使用此包。提供了多种客户端和服务端通信功能,比如基于Grizzly、Netty、Tomcat等等,RPC用除了RMI的协议都要用到此模块。

  • dubbo-remoting-api:定义了客户端和服务端的接口。
  • dubbo-remoting-grizzly:基于Grizzly实现的Client和Server。
  • dubbo-remoting-http:基于Jetty或Tomcat实现的Client和Server。
  • dubbo-remoting-mina:基于Mina实现的Client和Server。
  • dubbo-remoting-netty:基于Netty3实现的Client和Server。
  • dubbo-remoting-netty4:基于Netty4实现的Client和Server。
  • dubbo-remoting-p2p:P2P服务器,注册中心multicast中会用到这个服务器使用。
  • dubbo-remoting-zookeeper:封装了Zookeeper Client ,和 Zookeeper Server 通信。

dubbo-container——容器模块

以简单的 Main 加载 Spring 启动,因为服务通常不需要 Tomcat/JBoss 等 Web 容器的特性,没必要用 Web 容器去加载服务。

  • dubbo-container-api:定义了Container接口,实现了服务加载的Main方法。
  • 其他三个分别提供了对应的容器,供Main方法加载。

dubbo-monitor——监控模块

统计服务调用次数,调用时间的,调用链跟踪的服务。

  • dubbo-monitor-api:定义了monitor相关的接口,实现了监控所需要的过滤器。
  • dubbo-monitor-default:实现了dubbo监控相关的功能。

dubbo-demo——示例模块

快速启动示例,其中包含了服务提供方和调用方,注册中心用的是multicast,用XML配置方法,具体的介绍可以看官方文档。

快速启动示例地址

dubbo-filter——过滤器模块

这个模块提供了内置的一些过滤器。

  • dubbo-filter-cache:提供缓存过滤器。
  • dubbo-filter-validation:提供参数验证过滤器。

dubbo-plugin——插件模块

  • dubbo-qos:提供了在线运维的命令。
  • dubbo-auth:提供了auth验证的插件。

dubbo-serialization——序列化模块

该模块中封装了各类序列化框架的支持实现。

  • dubbo-serialization-api:定义了Serialization的接口以及数据输入输出的接口。
  • 其他的包都是实现了对应的序列化框架的方法。dubbo内置的就是这几类的序列化框架,序列化也支持扩展。

dubbo的maven配置文件

  • dubbo-bom/pom.xml,利用Maven BOM统一定义了dubbo的版本号。 dubbo-test和dubbo-demo的pom文件中都会引用dubbo-bom/pom.xml,以dubbo-demo都pom举例子:

  • dubbo-dependencies-bom/pom.xml:利用Maven BOM统一定义了dubbo依赖的第三方库的版本号

  • all/pow.xml:定义了dubbo的打包脚本,使用dubbo库的时候,需要引入改pom文件。

点关注,不迷路

好了各位,以上就是这篇文章的全部内容了。。。我后面会每周都更新常用技术栈相关的文章,敬请期待!!!如果这个文章写得还不错,觉得「小沙弥」我有点东西的话 求点赞👍 求关注❤️ 求分享👥 对我来说真的 非常有用!!!

白嫖不好,创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!

迷途小沙弥 | 文 【原创】

如果本篇博客有任何错误,请批评指教,不胜感激 !