Dubbo入门

从分层架构、服务暴露到调用链路

Posted by Ekko on August 8, 2020

这篇笔记围绕经典 Dubbo 2.x 架构展开,主线是服务暴露、服务引用、注册发现、集群容错和远程调用。阅读重点不在配置清单,而在于把一次 RPC 调用在框架内部是如何被拆成 ProxyInvokerProtocolRegistryClusterTransport 这些层次说明白。

文中引用的源码片段使用的是 com.alibaba.dubbo 包名,因此对应的是阿里时期的经典实现视角。放到今天看,Apache Dubbo 3 在协议和服务发现上已经继续演进,例如 Triple 协议和应用级服务发现,但 Provider / Consumer / Registry / Cluster 这条主干并没有改变。

参考资料:

官方文档:Apache Dubbo DocsCluster Fault Tolerance

官方仓库:apache/dubbo

实践参考:Java团长搜狐深度学习掘金

[TOC]


阅读边界

这篇笔记如果直接与当前 Dubbo 3 文档逐段对照,会感觉有些术语和实现入口并不完全一致,原因在于观察视角不同。这里先把边界说明清楚:

维度 本文主线 需要补充理解的现代变化
源码视角 Dubbo 2.x 经典分层和 com.alibaba.dubbo 包结构为主 Apache Dubbo 迁移后包名变为 org.apache.dubbo
通信协议 重点理解经典 dubbo 协议调用链 Dubbo 3 增加 Triple,更强调 HTTP/2 兼容能力
服务发现 以注册中心推送地址列表的经典模型为主 大规模场景下继续演进出应用级服务发现与更多治理能力

什么是Dubbo

Dubbo 是一个面向 Java 生态的高性能 RPC 服务框架。它把一次远程调用拆成接口代理、服务注册与发现、负载均衡、容错、协议编解码和网络传输等几个可扩展层次,从而让服务间调用在使用方式上尽量接近本地方法调用。

Dubbo 的核心设计之一就是分层。分层的意义不只是“模块划分清楚”,更重要的是把代理、协议、注册中心、集群容错和底层传输拆开,各层通过扩展点协作,避免业务代码与具体通信实现强耦合。从服务模型上看,可以抽象成两个基础角色:服务提供方(Provider)和服务消费方(Consumer)。

Dubbo包图.png

从图里可以看到各个模块的主要依赖关系。这里展示的是经典 Dubbo 的核心模块,而不是完整源码仓库的全部子模块,因此更适合理解主干职责,而不是一一对应到今天的所有工程目录。

  • dubbo-registry——注册中心模块: 基于注册中心下发地址的集群方式,以及对各种注册中心的抽象
  • dubbo-cluster——集群模块: 将多个服务提供方伪装为一个提供方,包括:负载均衡, 容错,路由等,集群的地址列表可以是静态配置的,也可以是由注册中心下发
  • dubbo-common——公共逻辑模块: 包括 Util 类和通用模型
  • dubbo-config——配置模块: 对外提供统一配置入口,用户主要通过这一层声明协议、注册中心、服务暴露和服务引用等信息;常见配置方式包括 XML、属性配置、API 配置和注解配置。
  • dubbo-rpc——远程调用模块: 抽象各种协议,以及动态代理,只包含一对一的调用,不关心集群的管理
  • dubbo-remoting——远程通信模块: 抽象底层网络通信与请求交换模型,经典 dubbo 协议主要依赖这一层完成传输。
  • dubbo-container——容器模块: 提供轻量级启动容器,以 Main 方法加载 Spring 容器启动服务,适合不依赖 Web 容器特性的服务进程。
  • dubbo-monitor——监控模块: 负责采集调用次数、调用耗时等监控数据。
  • dubbo-bootstrap——引导模块: 负责 Dubbo 的启动与关闭编排,便于通过编程式方式完成初始化和资源回收。
  • dubbo-demo——示例模块: 用于快速启动示例,其中包含服务提供方和调用方,早期示例常使用 multicast 注册中心和 XML 配置方式,具体可以参考官方示例文档。
  • dubbo-filter——过滤器模块: 提供内置过滤器扩展。
  • dubbo-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 调用次数和耗时,以 MonitorFactoryMonitorMonitorService 为核心扩展点。
  • 远程调用层(Protocol): 封装 RPC 调用语义,以 InvocationResult 为中心,扩展接口为 ProtocolInvokerExporterProtocol 是服务暴露与服务引用的主入口,负责 Invoker 的生命周期管理;Invoker 是 Dubbo 的核心抽象,表示一个可以被执行的调用实体,它既可能对应本地实现,也可能对应远程实现或集群实现。
  • 信息交换层(Exchange): 封装请求响应模式,同步转异步,以Request和Response为中心,扩展接口为Exchanger、ExchangeChannel、ExchangeClient和ExchangeServer。
  • 网络传输层(Transport): 抽象mina和netty为统一接口,以Message为中心,扩展接口为Channel、Transporter、Client、Server和Codec。
  • 数据序列化层(Serialize): 负责对象与字节流之间的转换,扩展接口为 SerializationObjectInputObjectOutput

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

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

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

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

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

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

如果把这 10 层浓缩成一次最核心的调用链,可以抽象成下面这张图:

graph LR
    A[Consumer 业务代码] --> B[Proxy 代理]
    B --> C[Cluster 集群容错]
    C --> D[Directory / Router / LoadBalance]
    D --> E[Protocol]
    E --> F[Exchange / Transport]
    F --> G[Provider]

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


Dubbo架构图

Dubbo架构图.png

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

  • 服务提供方发布服务到服务注册中心
  • 服务消费方从服务注册中心订阅服务
  • 服务消费方调用已经注册的可用服务
sequenceDiagram
    participant Provider as Provider
    participant Registry as Registry
    participant Consumer as Consumer
    participant Monitor as Monitor

    Provider->>Registry: 注册服务 URL 与元数据
    Consumer->>Registry: 订阅服务
    Registry-->>Consumer: 推送提供者列表变更
    Consumer->>Provider: 发起 RPC 调用
    Consumer->>Monitor: 上报调用统计
    Provider->>Monitor: 上报调用耗时与次数
角色 角色说明
Provider 暴露服务的服务提供方
Container 服务运行容器
Consumer 调用远程服务的服务消费方
Registry 服务注册与发现的注册中心
Monitor 统计服务的调用次数和调用时间的监控中心

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

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

Consumer 在启动的时候会向注册中心订阅服务方的元数据,第一次订阅时会拉取全量数据。

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

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

在获取到服务方的元数据之后就可以发起 RPC 调用。调用完成后,消费者与提供者都可以按统计周期向监控中心上报并发数、调用次数、耗时等信息,开发人员可以据此观察服务运行情况。

Dubbo调用流程图展开.png


Dubbo 调用工作流

Dubbo调用工作流.png

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

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

  • 服务提供者启动后,会读取配置并构造服务暴露所需的运行时对象。
  • Proxy 负责把接口调用封装成远程调用入口,使消费端在使用方式上接近本地方法调用。
  • Protocol 定义服务暴露与服务引用的协议语义,例如经典的 Dubbo Protocol
  • 框架会把调用能力抽象成 Invoker;在提供者一侧,再通过 Exporter 包装后完成服务暴露。
  • 服务消费者启动后会向注册中心订阅服务元数据,元数据通常包括服务地址、协议和治理相关参数。
  • 消费者通过代理发起调用时,会先从 Directory 中拿到候选 Invoker 列表。
  • Router 根据路由规则过滤候选节点,LoadBalance 再从剩余节点中选出本次调用的目标提供者。
  • Cluster 负责把多个 Invoker 伪装成一个 Invoker,并在失败时执行重试、快速失败等容错策略。
  • 调用请求在真正发送前还会经过 Filter 链,用于处理上下文透传、日志、鉴权、限流等横切逻辑。
  • 请求经过 Codec 编码、Serialization 序列化后,由 Client 发送给服务提供者。
  • 服务端收到请求后,将其分发到线程池,由 Server 查找对应的 ExporterInvoker,再进入提供者侧过滤链并执行目标方法。

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

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

例如 Proxy 主要解决“如何像本地方法一样发起调用”的问题,Invoker 主要解决“如何抽象一个可执行调用实体”的问题,Exporter 则更偏向提供者侧“如何把服务暴露出去”的问题。三者不是彼此替代,而是处在调用链不同阶段的不同抽象。


服务暴露实现原理

在前面的调用流程里,服务提供者首先会完成初始化并把服务暴露出去;服务消费者也会完成引用初始化,并按配置决定是否向注册中心注册自身元数据。

Dubbo服务暴露实现原理.png

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

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

Config 是服务暴露入口的核心组件。服务提供者初始化时,会通过 ServiceConfig 读取服务定义、协议、注册中心等配置信息。

这些配置可以来自 XML、注解、属性文件(properties / yaml),也可以通过 API 编程式构造。

在读取配置并构造服务实体以后,会通过 ProxyFactory 生成对应的 Invoker

随后 Invoker 会交给 Protocol 完成服务暴露,并在需要注册中心时通过 RegistryProtocol 把服务 URL 和元数据注册出去。上述流程的主入口之一就是 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 转换成用户接口的动态代理引用

在 Spring 场景下,服务引用的入口通常是 ReferenceBeangetObject() 方法。ReferenceBean 继承自 ReferenceConfig,因此真正的引用逻辑仍然落在 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。它本质上是一个分布式协调服务,Dubbo 借助它来完成服务目录维护、订阅通知和变更推送。

在经典 Dubbo + ZooKeeper 的组织方式里,通常会在 /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

集群容错这一层的核心目标,是把多个提供者节点抽象成一个可调用对象。根据 Cluster 实现的不同,会生成不同类型的 ClusterInvoker,并通过它的 invoke() 方法启动调用流程。

获取可调用的服务列表,可以通过 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 调用最终服务时,服务可能分布在多个节点上,因此还需要经过 LoadBalanceLoadBalance 负责从多个 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 内置了一批系统过滤器,分别工作在消费者侧和提供者侧。过滤器既可以通过 @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 长连接传输,请求报文可以拆成协议头和消息体两部分。

其中固定长度为 16 字节的是协议头,消息体则承载方法调用的实际内容。

内容如下:

  • 0-7 位,“魔法数”高位。
  • 8-15 位,“魔法数”低位。前面两个字节的魔法数用于识别 Dubbo 协议报文,固定值为 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 等机制等待响应返回。

在准备发送请求时,会创建 Request 对象,并以请求 ID 为键放入 DefaultFuture 的静态映射中;当服务提供者处理完请求并返回 Response 后,消费端会根据请求 ID 找到对应的等待对象。

这样响应才能被正确唤醒并交还给原来的调用方。

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

协议打包好以后就需要继续完成编码和序列化。这里会用到 Dubbo 的编码器,把内存中的调用信息转成可传输的字节流。

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

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

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

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

  • 构造 16 字节的协议头,特别是需要创建前面两个字节的魔法数,也就是“0xdabb”,它是用来分割两个不同请求的。
  • 生成唯一的请求/响应 ID,并且根据这个 ID 识别请求和响应协议包。
  • 通过协议头中的 19-23 位的描述,进行序列化/反序列化操作。
  • 为了提高处理效率,请求和响应报文通常会先写入缓冲区,再交给底层网络层发送。

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

在这条链路里,提供者侧是通过 Exporter 暴露服务,消费者侧则主要通过代理对象和 Invoker 发起调用,二者分别位于调用链的两端。

Exporter 可以理解为“提供者侧已经暴露出去的 Invoker 句柄”。请求到达提供者后,会先穿过提供者侧 Filter 链,再落到真正的 Invoker 和业务实现,最后把结果编码返回给消费者。


站在 Dubbo 3 视角再看这篇文章

如果把这篇笔记作为继续阅读 Dubbo 3 的前置材料,可以重点保留下面三点:

关注点 这篇文章里最应该掌握的主线 进入 Dubbo 3 后需要继续补充的内容
调用抽象 Proxy -> Invoker -> Protocol -> Transport 这条调用链 Triple、多语言 SDK、流式调用模型
服务治理 Registry -> Directory -> Router -> LoadBalance -> Cluster 这条治理链 应用级服务发现、流量治理、可观测性
源码阅读 先理解经典 ServiceConfig / ReferenceConfig 的暴露与引用入口 再对照 org.apache.dubbo 包结构看现代实现

因此,这篇“入门”笔记最合适的阅读方式,不是把它当成一份最新版本功能清单,而是把它当成 Dubbo 经典调用模型的结构图。先把主干角色和调用链打通,再去看 Dubbo 3 的协议升级和治理能力,理解成本会低很多。