Dubbo入门

RPC Dubbo

Posted by Ekko on August 8, 2020

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

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

参考资料Java团长搜狐深度学习掘金Dubbo-github源码Dubbo官方文档

[TOC]


什么是Dubbo

Dubbo 是一款高性能、轻量级 Java RPC 架构。它实现了面向接口代理的 RPC 调用,服务注册和发现,负载均衡,容错,扩展性等等功能

它最大的特点是按照分层的方式来架构,使用这种方式可以使各个层之间解耦合(或者最大限度地松耦合)。从服务模型的角度来看,Dubbo采用的是一种非常简单的模型,要么是提供方提供服务,要么是消费方消费服务,所以基于这一点可以抽象出服务提供方(Provider)和服务消费方(Consumer)两个角色

Dubbo包图.png

从以上这个图可以清晰的看到各个模块之间依赖关系,其实以上的图只是展示了关键的模块依赖关系,还有部分模块比如dubbo-bootstrap清理模块等

  • dubbo-registry——注册中心模块: 基于注册中心下发地址的集群方式,以及对各种注册中心的抽象
  • dubbo-cluster——集群模块: 将多个服务提供方伪装为一个提供方,包括:负载均衡, 容错,路由等,集群的地址列表可以是静态配置的,也可以是由注册中心下发
  • dubbo-common——公共逻辑模块: 包括 Util 类和通用模型
  • dubbo-config——配置模块: 是 Dubbo 对外的 API,用户通过 Config 使用Dubbo,隐藏 Dubbo 所有细节(用户使用配置来使用dubbo,dubbo也提供了四种配置方式,包括XML配置、属性配置、API配置、注解配置,配置模块就是实现了这四种配置的功能)
  • dubbo-rpc——远程调用模块: 抽象各种协议,以及动态代理,只包含一对一的调用,不关心集群的管理
  • dubbo-remoting——远程通信模块: 相当于 Dubbo 协议的实现,如果 RPC 用 RMI协议则不需要使用此包
  • dubbo-container——容器模块: 是一个 Standlone 的容器,以简单的 Main 加载 Spring 启动,因为服务通常不需要 Tomcat/JBoss 等 Web 容器的特性,没必要用 Web 容器去加载服务
  • dubbo-monitor——监控模块: 统计服务调用次数,调用时间的,调用链跟踪的服务
  • dubbo-bootstrap——清理模块: 这个模块只有一个类,是作为dubbo的引导类,并且在停止期间进行清理资源
  • dubbo-demo——示例模块: 这个模块是快速启动示例,其中包含了服务提供方和调用方,注册中心用的是multicast,用XML配置方法,具体的介绍可以看官方文档 示例
  • dubbo-filter——过滤器模块: 供了内置的一些过滤器
  • ubbo-plugin——插件模块: 该模块提供了内置的插件
  • dubbo-serialization——序列化模块: 该模块中封装了各类序列化框架的支持实现
  • dubbo-test——测试模块: 这个模块封装了针对dubbo的性能测试、兼容性测试等功能

Dubbo分层

Dubbo主要分为三层:业务层、RPC层、Remoting层

Dubbo分层.png

Dubbo总体架构

Dubbo总体架构.png

Dubbo框架设计一共划分了10个层,分别提供了各自需要关心和扩展的接口,构建整个服务生态系统

  • 服务接口层(Service): 该层是与实际业务逻辑相关的,根据服务提供方和服务消费方的业务设计对应的接口和实现。
  • 配置层(Config): 对外配置接口,以ServiceConfig和ReferenceConfig为中心,可以直接new配置类,也可以通过spring解析配置生成配置类。
  • 服务代理层(Proxy): 服务接口透明代理,生成服务的客户端Stub和服务器端Skeleton,以ServiceProxy为中心,扩展接口为ProxyFactory。
  • 服务注册层(Registry): 封装服务地址的注册与发现,以服务URL为中心,扩展接口为RegistryFactory、Registry和RegistryService。可能没有服务注册中心,此时服务提供方直接暴露服务。
  • 集群层(Cluster): 封装多个提供者的路由及负载均衡,并桥接注册中心,以Invoker为中心,扩展接口为Cluster、Directory、Router和LoadBalance。将多个服务提供方组合为一个服务提供方,实现对服务消费方来透明,只需要与一个服务提供方进行交互。
  • 监控层(Monitor): RPC调用次数和调用时间监控,以Statistics为中心,扩展接口为MonitorFactory、Monitor和MonitorService。
  • 远程调用层(Protocol): 封将RPC调用,以Invocation和Result为中心,扩展接口为Protocol、Invoker和Exporter。Protocol是服务域,它是Invoker暴露和引用的主功能入口,它负责Invoker的生命周期管理。Invoker是实体域,它是Dubbo的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起invoke调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
  • 信息交换层(Exchange): 封装请求响应模式,同步转异步,以Request和Response为中心,扩展接口为Exchanger、ExchangeChannel、ExchangeClient和ExchangeServer。
  • 网络传输层(Transport): 抽象mina和netty为统一接口,以Message为中心,扩展接口为Channel、Transporter、Client、Server和Codec。
  • 数据序列化层(Serialize): 可复用的一些工具,扩展接口为Serialization、 ObjectInput、ObjectOutput和ThreadPool。

最上面的Service层(业务层)是留给实际想要使用Dubbo开发分布式服务的开发者实现业务逻辑的接口层。图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口, 位于中轴线上的为双方都用到的接口

protocol 层和 proxy 层放在 rpc 模块中,构成 rpc 核心,不考虑集群环境,可以只使用这两层完成 RPC 调用。如果不需要达到透明化调用效果,使用 protocol 层就可以实现低层次的远程方法调用;在 protocol 基础之上,添加 proxy 封装透明化动态代理,调用远程方法就如同调用本地方法

透明化调用实现之后,就需要考虑负载均衡和集群容错机制,cluster 层承载了这方面的功能

registry 和 monitor 提供服务路由和治理相关辅助功能

remoting 模块实现 Dubbo 协议,transport 层和 exchange 层都放在 remoting 模块中,如果不使用 dubbo 协议,则该层不会使用

而在现有各种序列化工具的基础上为了提升网络传输性能和扩展功能,remoting 层实现了自定义 dubbo 协议作为整个框架的一大扩展点

serialize 层放在 common 模块中以便更大程度复用


Dubbo架构图

Dubbo架构图.png

上述图所描述的调用流程如下:

  • 服务提供方发布服务到服务注册中心
  • 服务消费方从服务注册中心订阅服务
  • 服务消费方调用已经注册的可用服务
角色 角色说明
Provider 暴露服务的服务提供方
Container 服务运行容器
Consumer 调用远程服务的服务消费方
Registry 服务注册与发现的注册中心
Monitor 统计服务的调用次数和调用时间的监控中心

服务容器负责启动,加载,运行服务提供者

Provider在启动的时候会向注册中心注册元数据(IP/端口之类的)

Consumer在启动的时候会向注册中心订阅(第一次订阅会拉去全部数据)服务方的元数据

注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给订阅的Consumer

服务消费者Consumer,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用

在获取到服务方的元数据之后可以进行RPC调用,在RPC调用后会向监控中心上报统计信息(比如并发数和调用的接口),定时每分钟发送一次统计数据到监控中心,开发人员可以在监控中心看到服务的调用情况

Dubbo调用流程图展开.png


Dubbo 调用工作流

Dubbo调用工作流.png

Dubbo 框架是用来处理分布式系统中,服务发现与注册以及调用问题的,并且管理调用过程

工作流涉及到服务提供者(Provider),注册中心(Registration),网络(Network)和服务消费者(Consumer):

  • 服务提供者在启动的时候,会通过读取一些配置将服务实例化。
  • Proxy 封装服务调用接口,方便调用者调用。客户端获取 Proxy 时,可以像调用本地服务一样,调用远程服务。
  • Proxy 在封装时,需要调用 Protocol 定义协议格式,例如:Dubbo Protocol。
  • 将 Proxy 封装成 Invoker,它是真实服务调用的实例。
  • 将 Invoker 转化成 Exporter,Exporter 只是把 Invoker 包装了一层,是为了在注册中心中暴露自己,方便消费者使用。
  • 将包装好的 Exporter 注册到注册中心。
  • 服务消费者建立好实例,会到服务注册中心订阅服务提供者的元数据。元数据包括服务 IP 和端口以及调用方式(Proxy)。
  • 消费者会通过获取的 Proxy 进行调用。通过服务提供方包装过程可以知道,Proxy 实际包装了 Invoker 实体,因此需要使用 Invoker 进行调用。
  • 在 Invoker 调用之前,通过 Directory 获取服务提供者的 Invoker 列表。在分布式的服务中有可能出现同一个服务,分布在不同的节点上。
  • 通过路由规则了解,服务需要从哪些节点获取。
  • Invoker 调用过程中,通过 Cluster 进行容错,如果遇到失败策略进行重试。
  • 调用中,由于多个服务可能会分布到不同的节点,就要通过 LoadBalance 来实现负载均衡。
  • Invoker 调用之前还需要经过 Filter,它是一个过滤链,用来处理上下文,限流和计数的工作。
  • 生成过滤以后的 Invoker。
  • 用 Client 进行数据传输。
  • Codec 会根据 Protocol 定义的协议,进行协议的构造。
  • 构造完成的数据,通过序列化 Serialization 传输给服务提供者。
  • Request 已经到达了服务提供者,它会被分配到线程池(ThreadPool)中进行处理。
  • Server 拿到请求以后查找对应的 Exporter(包含有 Invoker)。
  • 由于 Export 也会被 Filter 层层包裹
  • 通过 Filter 以后获得 Invoker
  • 最后,对服务提供者实体进行调用

上面调用步骤经历了这么多过程,其中出现了 Proxy,Invoker,Exporter,Filter。

实际上都是调用实体在不同阶段的不同表现形式,本质是一样的,在不同的使用场景使用不同的实体。

例如 Proxy 是用来方便调用者调用的。Invoker 是在调用具体实体时使用的。Exporter 用来注册到注册中心的等等


服务暴露实现原理

上面讲到的服务调用流程中,开始服务提供者会进行初始化,将暴露给其他服务调用。服务消费者也需要初始化,并且在注册中心注册自己

Dubbo服务暴露实现原理.png

首先来看看服务提供者暴露服务的整体机制:

Dubbo服务提供者暴露服务流程.png

Config 核心组件,在服务提供者初始化的时候,会通过 Config 组件中的 ServiceConfig 读取服务的配置信息

这个配置信息有三种形式,分别是 XML 文件,注解(Annoation)和属性文件(Properties 和 yaml)

在读取配置文件生成服务实体以后,会通过 ProxyFactory 将 Proxy 转换成 Invoker

此时,Invoker 会被定义 Protocol,之后会被包装成 Exporter。最后,Exporter 会发送到注册中心,作为服务的注册信息。上述流程主要通过 ServiceConfig 中的 doExport 完成

com.alibaba.dubbo.config.ServiceConfig

针对多协议多注册中心

1
2
3
4
5
6
7
8
9
10
11
private void doExportUrls() {
    // 获取当前服务对应注册中心实例
    List<URL> registryURLs = this.loadRegistries(true);
    Iterator i$ = this.protocols.iterator();

    while(i$.hasNext()) {
        ProtocolConfig protocolConfig = (ProtocolConfig)i$.next();
        // 依次获取服务暴露协议
        this.doExportUrlsFor1Protocol(protocolConfig, registryURLs);
    }
}

doExportUrlsFor1Protocol(protocolConfig, registryURLs) 方法的整个暴露过程分为7个步骤:

  • 读取其他配置信息到 map 中,用来后面构造 URL
  • 读取全局配置信息
  • 配置不是 remote,也就是暴露本地服务
  • 如果配置了监控地址,则服务调用信息会上报
  • 通过 Proxy 转化成 Invoker,RegistryURL 存放的是注册中心的地址
  • 暴露服务以后,向注册中心注册服务信息
  • 没有注册中心直接暴露服务

一旦服务注册到注册中心以后,注册中心会通过 RegistryProtocol 中的 Export 方法将服务暴露出去,并依次做以下操作:

  • 委托具体协议进行服务暴露,创建 NettyServer 监听端口,并保持服务实例。
  • 创建注册中心对象,创建对应的 TCP 连接。
  • 注册元数据到注册中心。
  • 订阅 Configurators 节点。
  • 如果需要销毁服务,需要关闭端口,注销服务信息。

消费者消费服务机制

Dubbo服务消费者消费服务机制.png

服务消费者首先持有远程服务实例生成的 Invoker,然后把 Invoker 转换成用户接口的动态代理引用

框架进行服务引用的入口点在 ReferenceBean 中的 getObject 方法,会将实体转换成 ReferenceBean,它是集成与 ReferenceConfig 类的

这里一起来看看 createProxy 的源代码:

com.alibaba.dubbo.config.spring.ReferenceBean

1
2
3
4
public Object getObject() throws Exception {
    return this.get();
}

ReferenceBean 继承 ReferenceConfig 类,this.get()在父类 ReferenceConfig 中实现

com.alibaba.dubbo.config.ReferenceConfig

1
2
3
4
5
6
7
8
9
10
11
public synchronized T get() {
    if (this.destroyed) {
        throw new IllegalStateException("Already destroyed!");
    } else {
        if (this.ref == null) {
            this.init();
        }

        return this.ref;
    }
}

ReferenceConfig 中 this.init() 方法,调用 createProxy 方法,消费者服务在调用服务提供者时,做了以下动作:

  • 检查是否是同一个 JVM 内部引用
  • 如果是同一个 JVM 的引用,直接使用 injvm 协议从内存中获取实例
  • 获取注册中心地址后,添加 refer 存储服务消费元数据信息
  • 远程引用调用的接口
  • 依次获取注册中心的服务,并且添加到 Invokers 列表中
  • 通过 Cluster 将多个 Invoker 转换成一个 Invoker
  • 把 Invoker 转换成接口代理

com.alibaba.dubbo.config.ReferenceConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
private T createProxy(Map<String, String> map) {
    URL tmpUrl = new URL("temp", "localhost", 0, map);
    // 1. 检查是否是同一个JVM内部引用
    boolean isJvmRefer;
    if (this.isInjvm() == null) {
        if (this.url != null && this.url.length() > 0) {
            isJvmRefer = false;
        } else if (InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl)) {
            isJvmRefer = true;
        } else {
            isJvmRefer = false;
        }
    } else {
        isJvmRefer = this.isInjvm();
    }

    if (isJvmRefer) {
        URL url = (new URL("injvm", "127.0.0.1", 0, this.interfaceClass.getName())).addParameters(map);

        // 2. 直接使用 injvm 协议从内存中获取实例
        this.invoker = refprotocol.refer(this.interfaceClass, url);
        if (logger.isInfoEnabled()) {
            logger.info("Using injvm service " + this.interfaceClass.getName());
        }
    } else {
        URL u;
        URL url;
        if (this.url != null && this.url.length() > 0) {
            String[] us = Constants.SEMICOLON_SPLIT_PATTERN.split(this.url);
            if (us != null && us.length > 0) {
                String[] arr$ = us;
                int len$ = us.length;

                for(int i$ = 0; i$ < len$; ++i$) {
                    String u = arr$[i$];
                    URL url = URL.valueOf(u);
                    if (url.getPath() == null || url.getPath().length() == 0) {
                        url = url.setPath(this.interfaceName);
                    }

                    if ("registry".equals(url.getProtocol())) {
                        this.urls.add(url.addParameterAndEncoded("refer", StringUtils.toQueryString(map)));
                    } else {

                        //3. 获取注册中心地址后,添加 refer 存储服务消费元数据信息
                        this.urls.add(ClusterUtils.mergeUrl(url, map));
                    }
                }
            }
        } else {
            List<URL> us = this.loadRegistries(false);
            if (us != null && us.size() > 0) {
                for(Iterator i$ = us.iterator(); i$.hasNext(); this.urls.add(u.addParameterAndEncoded("refer", StringUtils.toQueryString(map)))) {
                    u = (URL)i$.next();
                    url = this.loadMonitor(u);
                    if (url != null) {
                        map.put("monitor", URL.encode(url.toFullString()));
                    }
                }
            }

            if (this.urls == null || this.urls.size() == 0) {
                throw new IllegalStateException("No such any registry to reference " + this.interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");
            }
        }

        if (this.urls.size() == 1) {
            // 4. 远程引用调用的接口
            this.invoker = refprotocol.refer(this.interfaceClass, (URL)this.urls.get(0));
        } else {
            // 5. 依次获取注册中心的服务,并且添加到 Invokers 列表中
            List<Invoker<?>> invokers = new ArrayList();
            URL registryURL = null;
            Iterator i$ = this.urls.iterator();

            while(i$.hasNext()) {
                url = (URL)i$.next();
                invokers.add(refprotocol.refer(this.interfaceClass, url));
                if ("registry".equals(url.getProtocol())) {
                    registryURL = url;
                }
            }

            if (registryURL != null) {
                // 6. 通过 Cluster 将多个 Invoker 转换成一个 Invoker
                u = registryURL.addParameter("cluster", "available");
                this.invoker = cluster.join(new StaticDirectory(u, invokers));
            } else {
                this.invoker = cluster.join(new StaticDirectory(invokers));
            }
        }
    }

    Boolean c = this.check;
    if (c == null && this.consumer != null) {
        c = this.consumer.isCheck();
    }

    if (c == null) {
        c = true;
    }

    if (c && !this.invoker.isAvailable()) {
        throw new IllegalStateException("Failed to check the status of the service " + this.interfaceName + ". No provider available for the service " + (this.group == null ? "" : this.group + "/") + this.interfaceName + (this.version == null ? "" : ":" + this.version) + " from the url " + this.invoker.getUrl() + " to the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion());
    } else {
        if (logger.isInfoEnabled()) {
            logger.info("Refer dubbo service " + this.interfaceClass.getName() + " from url " + this.invoker.getUrl());
        }

        // 7. 把 Invoker 转换成接口代理
        return proxyFactory.getProxy(this.invoker);
    }
}

Dubbo注册中心

Dubbo 通过注册中心实现了分布式环境中服务的注册和发现

注册中心工作流程:

Dubbo注册中心工作流程.png

主要工作流程可以分为如下几步:

  • 服务提供者启动之后,会将服务注册到注册中心
  • 消费者启动之后主动订阅注册中心上提供者服务,从而获取到当前所有可用服务,同时留下一个回调函数
  • 若服务提供者新增或下线,注册中心将通过第二步的注册的回调函数通知消费者和服务治理中心
  • dubbo-admin(服务治理中心)启动时,会同时订阅所有消费者,提供者,路由和配置元数据的信息,从而可以在控制台管理所有服务提供者以及消费者

Dubbo 有四种注册中心的实现,分别是 ZooKeeper,Redis,Simple 和 Multicast

这里着重介绍一下 ZooKeeper 的实现。ZooKeeper 是负责协调服务式应用的

它通过树形文件存储的 ZNode 在 /dubbo/Service 目录下面建立了四个目录,分别是

  • Providers 目录下面,存放服务提供者 URL 和元数据。
  • Consumers 目录下面,存放消费者的 URL 和元数据。
  • Routers 目录下面,存放消费者的路由策略。
  • Configurators 目录下面,存放多个用于服务提供者动态配置 URL 元数据信息

客户端第一次连接注册中心的时候,会获取全量的服务元数据,包括服务提供者和服务消费者以及路由和配置的信息

根据 ZooKeeper 客户端的特性,会在对应 ZNode 的目录上注册一个 Watcher,同时让客户端和注册中心保持 TCP 长连接

如果服务的元数据信息发生变化,客户端会接受到变更通知,然后去注册中心更新元数据信息。变更时根据 ZNode 节点中版本变化进行


Dubbo 集群容错

Dubbo集群容错.png

分布式服务多以集群形式出现,会涉及到 Cluster,Directory,Router,LoadBalance 几个核心组件

Dubbo集群容错架构图.png

Dubbo集群容错架构图2.png

生成 Invoker 对象。根据 Cluster 实现的不同,生成不同类型的 ClusterInvoker 对象。通过 ClusertInvoker 中的 Invoker 方法启动调用流程。

获取可调用的服务列表,可以通过 Directory 的 List 方法获取。这里有两类服务列表的获取方式:

  • RegistryDirectory:属于动态 Directory 实现,会自动从注册中心更新 Invoker 列表,配置信息,路由列表
  • StaticDirectory:它是 Directory 的静态列表实现,将传入的 Invoker 列表封装成静态的 Directory 对象

在 Directory 获取所有 Invoker 列表之后,会调用路由接口(Router)。其会根据用户配置的不同策略对 Invoker 列表进行过滤,只返回符合规则的 Invoker

假设用户配置接口 A 的调用,都使用了 IP 为 192.168.1.1 的节点,则 Router 会自动过滤掉其他的 Invoker,只返回 192.168.1.1 的 Invoker

这里介绍一下 RegistryDirectory 的实现,它通过 Subscribe 和 Notify 方法,订阅和监听注册中心的元数据

Subscribe,订阅某个 URL 的更新信息。Notify,根据订阅的信息进行监听。包括三类信息,配置 Configurators,路由 Router,以及 Invoker 列表

管理员会通过 dubbo-admin 修改 Configurators 的内容,Notify 监听到该信息,就更新本地服务的 Configurators 信息

同理,路由信息更新了,也会更新服务本地路由信息。如果 Invoker 的调用信息变更了(服务提供者调用信息),会根据具体情况更新本地的 Invoker 信息

通过前面生成的 Invoker 需要调用最终的服务,但是服务有可能分布在不同的节点上面。所以,需要经过 LoadBalance,LoadBalance负责从多个Invoker中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选

Dubbo 的负载均衡策略有四种:

Random LoadBalance

  • 随机,按照权重设置随机概率做负载均衡
  • 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重

RoundRobin LoadBalance

  • 轮询,按照公约后的权重设置轮询比例
  • 存在慢的提供者累积请求问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上

LeastActive LoadBalance

  • 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差
  • 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大

ConsistentHash LoadBalance

  • 一致性 Hash,相同参数的请求总是发到同一个提供者
  • 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动
  • 缺省只对第一个参数Hash,如果要修改,请配置<dubbo:parameter key="hash.arguments" value="0,1" />
  • 缺省用160份虚拟节点,如果要修改,请配置<dubbo:parameter key="hash.nodes" value="320" />

最后进行 RPC 调用。如果调用出现异常,针对不同的异常提供不同的容错策略。Cluster 接口定义了 9 种容错策略,这些策略对用户是完全透明的

Failover Cluster:

  • 出现失败,立即重试其他服务器
  • 通常用于读操作,但重试会带来更长延迟
  • 可通过retries=”2”来设置重试次数(不含第一次)

Failfast Cluster:

  • 快速失败,只发起一次调用,失败立即报错
  • 通常用于非幂等性的写操作,比如新增记录

Failsafe Cluster:

  • 失败安全,出现异常时,直接忽略
  • 通常用于写入审计日志等操作

Failback Cluster:

  • 请求失败后,将失败记录放到失败队列中,通过定时线程扫描该队列,并定时重试
  • 通常用于消息通知操作

Forking Cluster:

  • 并行调用多个服务器,其中任意一个服务返回,就立即返回结果
  • 通常用于实时性要求较高的读操作,但需要浪费更多服务资源
  • 可通过forks=”2”来设置最大并行数

Broadcast Cluster:

  • 广播调用所有提供者,逐个调用,任意一台报错则报错。(2.1.0开始支持)
  • 通常用于通知所有提供者更新缓存或日志等本地资源信息

Mock Cluster:

  • 响应失败时返回伪造的响应结果

Available Cluster:

  • 通过遍历的方式查找所有服务列表,找到第一个可以返回结果的节点,并且返回结果

Mergable Cluster:

  • 将多个节点请求合并进行返回

Dubbo 远程调用

服务消费者经过容错,Invoker 列表,路由和负载均衡以后,会对 Invoker 进行过滤,之后通过 Client 编码,序列化发给服务提供者

Dubbo远程调用.png

(过滤,发送请求,编码,序列化发送给服务提供者)

从上图可以看出在服务消费者调用服务提供者的前后,都会调用 Filter(过滤器)。

可以针对消费者和提供者配置对应的过滤器,由于过滤器在 RPC 执行过程中都会被调用,所以为了提高性能需要根据具体情况配置。

Dubbo 系统有自带的系统过滤器,服务提供者有 11 个,服务消费者有 5 个。过滤器的使用可以通过 @Activate 的注释,或者配置文件实现

1
2
3
4
5
6
7
8
9
10
11
<!-- 消费者过滤器配置 -->
<dubbo:reference filter = "filter01,filter02" />

<!-- 消费者默认过滤器配置拦截reference过滤器 -->
<dubbo:consumer filter = "filter03,filter04" />

<!-- 提供者过滤器配置 -->
<dubbo:service filter = "filter05" />

<!-- 提供者过滤器配置拦截service过滤器 -->
<dubbo:provider filter = "filter06,filter07" />

过滤器的使用遵循以下几个规则:

  • 过滤器顺序,过滤器执行是有顺序的。例如,用户定义的过滤器的过滤顺序默认会在系统过滤器之后。 又例如,上图中 filter=“filter01, filter02”,filter01 过滤器执行就在 filter02 之前。
  • 过滤器失效,如果针对某些服务或者方法不希望使用某些过滤器,可以通过“-”(减号)的方式使该过滤器失效。例如,filter=“-filter01”。
  • 过滤器叠加,如果服务提供者和服务消费者都配置了过滤器,那么两个过滤器会被叠加生效

由于,每个服务都支持多个过滤器,而且过滤器之间有先后顺序。因此在设计上 Dubbo 采用了装饰器模式,将 Invoker 进行层层包装,每包装一层就加入一层过滤条件。在执行过滤器的时候就好像拆开一个一个包装一样

调用请求经过过滤以后,会以 Invoker 的形式对 Client 进行调用。Client 会交由底层 I/O 线程池处理,其包括处理消息读写,序列化,反序列化等逻辑

同时会对 Dubbo 协议进行编码和解码操作。Dubbo 协议基于 TCP/IP 协议,包括协议头和协议体

协议体包含了传输的主要内容,其意义不言而喻,它是由 16 字节长的报文组成,每个字节包括 8 个二进制位

内容如下:

  • 0-7 位,“魔法数”高位。
  • 8-15 位,“魔法数”低位。前面两个字节的“魔法数”,是用来区别两个不同请求。好像编程中使用的“;”“/”之类的符号将两条记录分开。PS:魔法数用固定的“0xdabb”表示,
  • 16 位,数据包的类型,因为 RPC 调用是双向的,0 表示 Response,1 表示 Request。
  • 17 位,调用方式,0 表示单项,1 表示双向。
  • 18 位,时间标识,0 表示请求/响应,1 表示心跳包。
  • 19-23 位,序列化器编号,就是告诉协议用什么样的方式进行序列化。例如:Hessian2Serialization 等等。
  • 24-31 位,状态位。20 表示 OK,30 表示 CLIENT_TIMEOUT 客户端超时,31 表示 SERVER_TIMEOUT 服务端超时,40 表示 BAD_REQUEST 错误的请求,50 表示 BAD_RESPONSE 错误的响应。
  • 32-95 位,请求的唯一编号,也就是 RPC 的唯一 ID。
  • 96-127,消息体包括 Dubbo 版本号,服务接口名,服务接口版本,方法名,参数类型,方法名,参数类型,方法参数值和请求额外参数

服务消费者在调用之前会将上述服务消息体,根据 Dubbo 协议打包好。框架内部会调用 DefaultFuture 对象的 get 方法进行等待

在准备发送请求的时候,才创建 Request 对象,这个对象会保存在一个静态的 HashMap 中,当服务提供者处理完 Request 之后,将返回的 Response 放回到 Futures 的 HashMap 中

在 HashMap 中会找到对应的 Request 对象,并且返回给服务消费者

Dubbo服务消费者请求和响应图.png

协议打包好以后就需要给协议编码和序列化。这里需要用到 Dubbo 的编码器,其过程是将信息传化成字节流

Dubbo 协议编码请求对象分为使用 ExchangeCodec 中的两个方法,encodeRequest 负责编码协议头和 encodeRequestData 编码协议体

同样通过 encodeResponse 编码协议头,encodeResponseData 编码协议体

服务消费者和提供者都通过 decode 和 decodeBody 两个方法进行解码,不同的是解码有可能在 IO 线程或者 Dubbo 线程池中完成

虽然,编码和解码的细节在这里不做展开,但是以下几点需要注意

  • 构造 16 字节的协议头,特别是需要创建前面两个字节的魔法数,也就是“0xdabb”,它是用来分割两个不同请求的。
  • 生成唯一的请求/响应 ID,并且根据这个 ID 识别请求和响应协议包。
  • 通过协议头中的 19-23 位的描述,进行序列化/反序列化操作。
  • 为了提高处理效率,每个协议都会放到 Buffer 中处理

当服务提供者收到请求协议包以后,先将其放到 ThreadPool 中,然后依次处理

由于服务提供者在注册中心是通过 Exporter 的方式暴露服务的,服务消费者也是通过 Exporter 作为接口进行调用的

Exporter 是将 Invoker 进行了包装,将拆开的 Invoker 进行 Filter 过滤链条进行过滤以后,再去调用服务实体。最后,将信息返回给服务消费者