Dubbo 3 入门教程之一

大纲

前言

学习资源

Dubbo 注册中心

注册中心的类型

  • Dubbo 目前支持的主流注册中心包括(不限于):
注册中心说明
ZookeeperDubbo 最早、最常用的注册中心实现,基于树形节点结构存储服务信息,具备良好的数据一致性与可靠的 Watch 机制。
Nacos 阿里巴巴开源的服务发现与配置管理平台,支持动态注册与配置推送,适合微服务与云原生场景。
ConsulHashiCorp 开源的基于 Go 的服务发现与配置系统,支持多数据中心与 KV 存储,Dubbo 可通过 Consul 实现跨语言注册。
EtcdCoreOS 开源的基于 Go 的高可用分布式键值存储,使用 Raft 协议保证一致性,常用于云原生和 Kubernetes 环境下的配置共享、服务发现与协调。
Redis 轻量级的注册中心实现,利用 Redis 的发布 / 订阅机制进行服务发现,适合测试或小规模集群。
MulticastMulticast 注册中心不需要启动任何中心节点,只要广播地址一样,就可以互相发现。
  • Dubbo 默认支持 ZooKeeper 与 Nacos 作为注册中心,同时还支持 Kubernetes、Mesh 体系的服务发现。
  • Dubbo 扩展生态提供了 Consul、Etcd、Redis、Multicast 等注册中心的扩展实现。
  • 通过 Dubbo SPI 扩展机制,用户还可以将更多的注册中心实现到 Dubbo 生态。
  • Dubbo 支持在一个应用中指定多个注册中心,并将服务根据注册中心分组,这样做使得服务分组管理或服务迁移变得更容易。

多注册中心注册

  • Dubbo 支持同一个服务向多个注册中心同时注册,或者不同的服务分别注册到不同的注册中心上去,甚至可以同时引用注册在不同注册中心上的同名服务。

多注册中心的使用

  • 同一个服务注册到不同的注册中心。比如:中文站有些服务来不及在青岛部署,只在杭州部署,而青岛的其它应用需要引用此服务,就可以将青岛的服务同时注册到两个注册中心,具体配置如下图所示:

  • 不同的服务注册到不同的注册中心。比如:CRM 系统中有些服务是专门为国际站设计的,有些服务是专门为中文站设计的,具体配置如下图所示:

  • 单独引用不同注册中心的服务。比如:CRM 系统中需要同时调用中文站和国际站的 PC2 服务,而 PC2 服务在中文站和国际站均有部署,接口及版本号都一样,但连接的数据库不一样,具体配置如下图所示:

  • 同时引用不同注册中心的同名服务,可以使用竖号分隔多个不同注册中心地址。比如:CRM 系统中需要同时调用中文站和国际站的 Pay 服务,而 Pay 服务在中文站和国际站均有部署,接口及版本号都一样,连接的数据库也一样,具体配置如下图所示:

一个服务多个注册中心的应用场景

Dubbo 线程模型

消费端线程模型

提供端线程模型

线程池隔离实现

线程池隔离的介绍

  • 核心概念

    • Dubbo 服务提供端除了可以通过 <dubbo:protocol name="dubbo" port="20880" dispatcher="all" threadpool="fixed" threads="100"/> 的配置方式实现线程池隔离。
    • Dubbo 3 还有一种新的线程池管理方式,使得提供者应用内各个服务的线程池隔离开来,互相独立,某个服务的线程池资源耗尽不会影响其他正常服务。支持线程池可配置化,由用户手动指定,详细介绍请看 Dubbo 官方文档(最新版本)
    • 使用线程池隔离来确保 Dubbo 用于调用远程方法的线程与微服务用于执行其任务的线程是分开的。可以通过防止线程阻塞或相互竞争来帮助提高系统的性能和稳定性。目前可以以 API、XML、Annotation 的方式进行配置线程池隔离。
  • 配置参数

    • ApplicationConfig 新增了 executor-management-mode 参数,配置值为 defaultisolation,默认为 default
      • executor-management-mode = default 使用原有以协议端口为粒度、服务间共享的线程池管理方式。
      • executor-management-mode = isolation 使用新增的以服务三元组为粒度、服务间隔离的线程池管理方式。
    • ServiceConfig 新增了 executor 参数,用以服务间隔离的线程池,可以由用户配置化、提供自己实现的线程池;若没有指定,则会根据协议配置(ProtocolConfig)信息构建默认的线程池用以服务隔离。
      • ServiceConfig 新增的 executor 配置参数,只有在 ApplicationConfig 中指定 executor-management-mode = isolation 时才会生效。

线程池隔离的使用

  • 原生 API 配置方式
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
public void test() {
// provider app
DubboBootstrap providerBootstrap = DubboBootstrap.newInstance();

ServiceConfig serviceConfig1 = new ServiceConfig();
serviceConfig1.setInterface(DemoService.class);
serviceConfig1.setRef(new DemoServiceImpl());
serviceConfig1.setVersion(version1);
// set executor1 for serviceConfig1, max threads is 10
NamedThreadFactory threadFactory1 = new NamedThreadFactory("DemoService-executor");
ExecutorService executor1 = Executors.newFixedThreadPool(10, threadFactory1);
serviceConfig1.setExecutor(executor1);

ServiceConfig serviceConfig2 = new ServiceConfig();
serviceConfig2.setInterface(HelloService.class);
serviceConfig2.setRef(new HelloServiceImpl());
serviceConfig2.setVersion(version2);
// set executor2 for serviceConfig2, max threads is 100
NamedThreadFactory threadFactory2 = new NamedThreadFactory("HelloService-executor");
ExecutorService executor2 = Executors.newFixedThreadPool(100, threadFactory2);
serviceConfig2.setExecutor(executor2);

ServiceConfig serviceConfig3 = new ServiceConfig();
serviceConfig3.setInterface(HelloService.class);
serviceConfig3.setRef(new HelloServiceImpl());
serviceConfig3.setVersion(version3);
// Because executor is not set for serviceConfig3, the default executor of serviceConfig3 is built using
// the threadpool parameter of the protocolConfig ( FixedThreadpool , max threads is 200)
serviceConfig3.setExecutor(null);

// It takes effect only if [executor-management-mode=isolation] is configured
ApplicationConfig applicationConfig = new ApplicationConfig("provider-app");
applicationConfig.setExecutorManagementMode("isolation");

providerBootstrap
.application(applicationConfig)
.registry(registryConfig)
// export with tri and dubbo protocol
.protocol(new ProtocolConfig("tri", 20001))
.protocol(new ProtocolConfig("dubbo", 20002))
.service(serviceConfig1)
.service(serviceConfig2)
.service(serviceConfig3);

providerBootstrap.start();
}
  • XML 配置方式
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
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">

<!-- NOTE: we need config executor-management-mode="isolation" -->
<dubbo:application name="demo-provider" executor-management-mode="isolation"/>

<dubbo:config-center address="zookeeper://127.0.0.1:2181"/>
<dubbo:metadata-report address="zookeeper://127.0.0.1:2181"/>
<dubbo:registry id="registry1" address="zookeeper://127.0.0.1:2181?registry-type=service"/>

<dubbo:protocol name="dubbo" port="-1"/>
<dubbo:protocol name="tri" port="-1"/>

<!-- expose three service with dubbo and tri protocol-->
<bean id="demoServiceV1" class="org.apache.dubbo.config.spring.impl.DemoServiceImpl"/>
<bean id="helloServiceV2" class="org.apache.dubbo.config.spring.impl.HelloServiceImpl"/>
<bean id="helloServiceV3" class="org.apache.dubbo.config.spring.impl.HelloServiceImpl"/>

<!-- customized thread pool -->
<bean id="executor-demo-service"
class="org.apache.dubbo.config.spring.isolation.spring.support.DemoServiceExecutor"/>
<bean id="executor-hello-service"
class="org.apache.dubbo.config.spring.isolation.spring.support.HelloServiceExecutor"/>

<!-- this service use [executor="executor-demo-service"] as isolated thread pool-->
<dubbo:service executor="executor-demo-service"
interface="org.apache.dubbo.config.spring.api.DemoService" version="1.0.0" group="Group1"
timeout="3000" ref="demoServiceV1" registry="registry1" protocol="dubbo,tri"/>

<!-- this service use [executor="executor-hello-service"] as isolated thread pool-->
<dubbo:service executor="executor-hello-service"
interface="org.apache.dubbo.config.spring.api.HelloService" version="2.0.0" group="Group2"
timeout="5000" ref="helloServiceV2" registry="registry1" protocol="dubbo,tri"/>

<!-- not set executor for this service, the default executor built using threadpool parameter of the protocolConfig -->
<dubbo:service interface="org.apache.dubbo.config.spring.api.HelloService" version="3.0.0" group="Group3"
timeout="5000" ref="helloServiceV3" registry="registry1" protocol="dubbo,tri"/>

</beans>
  • 注解配置方式
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
@Configuration
@EnableDubbo(scanBasePackages = "org.apache.dubbo.config.spring.isolation.spring.annotation.provider")
public class ProviderConfiguration {
@Bean
public RegistryConfig registryConfig() {
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://127.0.0.1:2181");
return registryConfig;
}

// NOTE: we need config executor-management-mode="isolation"
@Bean
public ApplicationConfig applicationConfig() {
ApplicationConfig applicationConfig = new ApplicationConfig("provider-app");
applicationConfig.setExecutorManagementMode("isolation");
return applicationConfig;
}

// expose services with dubbo protocol
@Bean
public ProtocolConfig dubbo() {
ProtocolConfig protocolConfig = new ProtocolConfig("dubbo");
return protocolConfig;
}

// expose services with tri protocol
@Bean
public ProtocolConfig tri() {
ProtocolConfig protocolConfig = new ProtocolConfig("tri");
return protocolConfig;
}

// customized thread pool
@Bean("executor-demo-service")
public Executor demoServiceExecutor() {
return new DemoServiceExecutor();
}

// customized thread pool
@Bean("executor-hello-service")
public Executor helloServiceExecutor() {
return new HelloServiceExecutor();
}
}
  • 自定义的线程池实现
1
2
3
4
5
6
7
// customized thread pool
public class DemoServiceExecutor extends ThreadPoolExecutor {
public DemoServiceExecutor() {
super(10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(),
new NamedThreadFactory("DemoServiceExecutor"));
}
}
1
2
3
4
5
6
7
// customized thread pool
public class HelloServiceExecutor extends ThreadPoolExecutor {
public HelloServiceExecutor() {
super(100, 100, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(),
new NamedThreadFactory("HelloServiceExecutor"));
}
}
  • 自定义的业务服务实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// "executor-hello-service" is beanName
@DubboService(executor = "executor-demo-service", version = "1.0.0", group = "Group1")
public class DemoServiceImplV1 implements DemoService {

@Override
public String sayName(String name) {
return "server name";
}

@Override
public Box getBox() {
return null;
}
}
1
2
3
4
5
6
7
8
9
10
// not set executor for this service, the default executor built using threadpool parameter of the protocolConfig
@DubboService(version = "3.0.0", group = "Group3")
public class HelloServiceImplV2 implements HelloService {
private static final Logger logger = LoggerFactory.getLogger(HelloServiceImplV2.class);

@Override
public String sayHello(String name) {
return "server hello";
}
}
1
2
3
4
5
6
7
8
9
@DubboService(executor = "executor-hello-service", version = "2.0.0", group = "Group2")
public class HelloServiceImplV3 implements HelloService {
private static final Logger logger = LoggerFactory.getLogger(HelloServiceImplV3.class);

@Override
public String sayHello(String name) {
return "server hello";
}
}

线程池状态导出

概念介绍

  • Dubbo 可以通过 jstack 自动导出线程堆栈来保留现场,方便排查问题。
  • Dubbo 线程池状态的默认导出策略:
    • 导出开关:默认打开
    • 导出路径:user.home 标识的用户主目录
    • 导出间隔:最短间隔允许每隔 10 分钟导出一次
  • 当业务线程池满时,往往需要知道线程都在等待哪些资源、条件,以找到系统的瓶颈点或异常点。

配置方式

  • 导出开关
1
2
# Properties 配置(比如 dubbo.properties)
dubbo.application.dump.enable=true
1
2
<!-- Spring XML 配置 -->
<dubbo:application name="demo-provider" dump-enable="false"/>
1
2
3
4
5
# YAML 配置(比如 application.yml)
dubbo:
application:
name: dubbo-springboot-demo-provider
dump-enable: false
  • 导出路径
1
2
# Properties 配置(比如 dubbo.properties)
dubbo.application.dump.directory=/tmp
1
2
<!-- Spring XML 配置 -->
<dubbo:application name="demo-provider" dump-directory="/tmp"/>
1
2
3
4
5
# YAML 配置(比如 application.yml)
dubbo:
application:
name: dubbo-springboot-demo-provider
dump-directory: /tmp

Dubbo 配置加载流程

Dubbo 官方文档

更多关于 Dubbo 配置加载的介绍请看 Dubbo 官方文档(最新版本)- 配置加载流程附文档截图

加载流程

Dubbo 的配置加载大概分为两个阶段:

  • 第一阶段为 DubboBootstrap 初始化之前,在 Spring Context 启动时解析处理 XML 配置 / 注解配置 / Java-Config 或者是执行 API 配置代码,创建 Config Bean 并且加入到 ConfigManager 中。
  • 第二阶段为 DubboBootstrap 初始化过程,从配置中心读取外部配置,依次处理实例级属性配置和应用级属性配置,最后刷新所有配置实例的属性,也就是属性覆盖。

属性覆盖

发生属性覆盖可能有两种情况,并且二者可能是会同时发生的:

  • 不同配置源配置了相同的配置项
  • 相同配置源,但在不同层次指定了相同的配置项

不同配置源

Dubbo 3 的配置来源和优先级高低如下所示,优先级高的会覆盖优先级低的

优先级配置来源说明
1JVM System Properties(-D 参数)优先级最高,通过 JVM 启动参数指定,适合部署或临时覆盖配置,例如:-Ddubbo.protocol.port=20890
2System Environment(系统环境变量)JVM 进程的环境变量配置,例如通过操作系统的 exportset 命令设置
3Externalized Configuration(外部化配置)从远端的配置中心读取配置,例如 Apollo、Nacos、Zookeeper 等
4Application Configuration(应用属性配置)从 Spring 应用的 Environment 中提取以 dubbo 开头的属性集
5API / XML / 注解配置通过原生 API、XML 或注解形式直接编写的配置,属于用户直接定义的配置来源
6Classpath 配置文件(dubbo.properties优先级最低,从 Classpath 中读取 dubbo.properties 文件,作为默认或兜底配置

关于 dubbo.properties 配置文件

  • 如果在 Classpath 下存在多个 dubbo.properties 文件,比如,两个 Jar 包都各自包含了 dubbo.properties,Dubbo 将随机选择一个配置文件进行加载,并且打印错误日志。
  • Dubbo 可以自动加载 Classpath 根目录下的 dubbo.properties,但是用户同样可以使用 JVM 参数来指定 Properties 配置文件的路径,比如 -Ddubbo.properties.file=xxx.properties

相同配置源

属性覆盖是指用配置的属性值覆盖 Config Bean 实例的属性,类似 Spring 的 PropertyOverrideConfigurer 的作用,但与 PropertyOverrideConfigurer 的不同之处是,Dubbo 的属性覆盖有多个匹配格式,优先级从高到低依次是:

1
2
3
4
5
6
7
8
#1. 指定id的实例级配置
dubbo.{config-type}s.{config-id}.{config-item}={config-item-value}

#2. 指定name的实例级配置
dubbo.{config-type}s.{config-name}.{config-item}={config-item-value}

#3. 应用级配置(单数配置)
dubbo.{config-type}.{config-item}={config-item-value}

属性覆盖处理流程如下图所示,按照优先级从高到低依次查找,如果找到此前缀开头的属性,则选定使用这个前缀提取属性,忽略后面的配置。

外部化配置

外部化配置说明

  • Dubbo 外部化配置和其他本地配置在内容和格式上并没有任何区别,可以简单理解为 dubbo.properties 的外部化存储,配置中心更适合将一些公共配置(如注册中心、元数据中心等配置)抽取出来,以便做集中管理。
  • Dubbo 外部化配置的主要目的是实现配置的集中式管理,支持的成熟配置系统有:ZooKeeper、Apollo、Nacos、Etcd、Consul,而 Dubbo 做的主要是支持这些配置系统正常工作,让集成更加方便。
  • 举个例子:当 Dubbo 使用 Apollo、Nacos 等配置中心后,可以将以下全局配置(比如注册中心地址、元数据中心地址、协议、QOS 等)统一存储在配置中心,这样应用在本地就不再需要配置。
1
2
3
4
5
6
7
8
9
10
11
12
# 将注册中心地址、元数据中心地址、协议、QOS 等配置存储在配置中心(如 Nacos),这样可以做到统一管理、减少开发侧感知

dubbo.application.qos.qosEnable=true
dubbo.application.qos.port=33333

dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.registry.simplified=true

dubbo.metadata-report.address=zookeeper://127.0.0.1:2181

dubbo.protocol.name=dubbo
dubbo.protocol.port=20880
  • 优先级:外部化配置默认比本地配置拥有更高的优先级,因此这里配置的内容会覆盖本地配置值。

  • 作用域:外部化配置有全局和应用两个级别,全局配置是所有应用共享的,应用级配置是由每个应用自己维护且只对自身可见的。当前已支持的扩展实现有 Zookeeper、Apollo、Nacos 等。

  • 当 Dubbo 使用 ZooKeeper 作为配置中心时,ZooKeeper 中的数据存储结构如下图所示:

外部化配置使用

Dubbo 外部化配置的使用步骤如下:

  • (1) 增加 config-center 配置(用于指定配置中心地址),比如:<dubbo:config-center address="nacos://127.0.0.1:8848"/>

  • (2) 在相应的配置中心(Zookeeper、Nacos 等)增加全局配置项(如下图所示,以 Nacos 为例)

    • 开启外部化配置后,registrymetadata-reportprotocolqos 等全局范围的配置理论上都不再需要在应用中配置,应用开发侧专注业务服务配置,一些全局共享的全局配置转而由运维人员统一配置在远端配置中心。
    • 这样能做到的效果就是,应用只需要关心:
      • 服务暴露、订阅服务;
      • 配置中心地址当部署到不同的环境时,其他配置就能自动的被从对应的配置中心读取到。
    • 举例来说,每个应用中 Dubbo 相关的全局配置只要有以下内容可能就足够了,其余的都托管给相应环境下的配置中心:
      1
      2
      3
      4
      5
      dubbo
      application
      name: demo
      config-center
      address: nacos://127.0.0.1:8848

自行加载外部化配置

所谓 Dubbo 对配置中心的支持,本质上就是将 .properties 从远端拉取到本地,然后和本地的配置做一次融合。理论上只要 Dubbo 框架能拿到需要的配置就可以正常的启动,它并不关心这些配置是自己加载到的还是应用直接塞给它的,所以 Dubbo 还提供了以下 API,让用户将自己组织好的配置塞给 Dubbo 框架(配置加载的过程是用户自己完成的),这样 Dubbo 框架就不再直接和 Apollo 或 Zookeeper 做读取配置交互。

1
2
3
4
5
6
7
8
// 应用自行加载配置
Map<String, String> dubboConfigurations = new HashMap<>();
dubboConfigurations.put("dubbo.registry.address", "zookeeper://127.0.0.1:2181");
dubboConfigurations.put("dubbo.registry.simplified", "true");

// 将组织好的配置塞给Dubbo框架
ConfigCenterConfig configCenter = new ConfigCenterConfig();
configCenter.setExternalConfig(dubboConfigurations);