Dubbo框架学习总结

Dubbo的定义

官方定义:Apache Dubbo是一个由阿里提供开源的基于Java的高性能的RPC框架。遵循RPC框架的设计原则,Dubbo也定义了服务的概念,可以远程根据参数和返回值来调用具体的方法。在服务侧,服务侧实现这个接口并运行一个Dubbo服务来处理客户端的请求,在客户端保存有服务端相同的方法存根。

Dubbo有三个主要特性:

  • 基于接口的远程的调用
  • 容错和负载均衡
  • 服务的子自动注册和发现。

Dubbo相关的文档:
Dubbo用户手册
Dubbo开发手册
Dubbo管理手册
Dubbo GitHub

Dubbo结构

Alt text

  1. 服务提供者-对外暴露自己的服务,服务提供者会将自己提供的服务注册到注册中心
  2. 容器-初始化服务,加载服务,运行服务
  3. 服务消费者-调用远程服务,会向注册中心订阅他需要的服务。
  4. 注册中心-服务注册与发现
  5. 监控中心-记录服务相关的数据,比如服务的调用频率,调用次数等

服务提供者,服务消费者和注册中心的连接是持久的,所以当一个服务提供者挂掉之后,注册中心可以检测到服务提供者的失效并将此信息通知给服务消费者

注册中心和监控中心是可选的,服务消费者可以直接连接到服务提供者,但是这样会影响整个系统的稳定性,不推荐直接连接

调用关系说明

  1. 服务容器负责启动,加载,运行服务提供者。
  2. 服务提供者在启动时,向注册中心注册自己提供的服务。
  3. 服务消费者在启动时,向注册中心订阅自己需要的服务。
  4. 注册中心返回服务提供者列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者经调用,如果调用失败,再选一台调用。
  6. 服务消费者和提供者,在内存中累积调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

Dubbo架构的特点

连通性

  • 注册中心负责服务地址的注册于查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发消息,压力较小。
  • 监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示。
  • 服务提供者向注册中心注册其提供的服务,并汇报调用时间到监控中心,此时间不包含网络开销。
  • 服务消费者向注册中心获取服务提供者地址列表,并根据负载算法直接调用提供者,同时汇报调用时间到监控中心,此时间包含网络开销。
  • 注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心不是。
  • 注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者。
  • 注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地魂村了提供者列表。
  • 注册中心和监控中心都是可选的,服务消费者可以直连服务提供者。

健壮性

  • 监控中心宕机不影响使用,只是丢失部分统计数据。
  • 数据库宕机,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
  • 注册中心对等集群,任意一台宕机之后,将自动切换到另一台
  • 注册中心全部宕机后,服务提供者和服务消费者仍能通过本地换粗通讯
  • 服务提供者无状态,任意一台宕机后,不影响使用。
  • 服务提供者全部宕机之后,服务消费者应用将无法使用,并无限次重连等待服务提供者回复。

伸缩性

  • 注册中心为了对等集群,可动态增加机器部署实例,所有客户端将自动发现新的注册中心
  • 服务提供者无状态,可动态增加及其部署实例,注册中心将推送新的服务提供者信息给消费者。

升级性

  • 当服务集群规模进一步扩大,带动IT治理结构进一步升级,需要实现动态部署,进行流动计算,现有的分布式架构不会带来阻力,下图为一种可能结果:

Alt text

Dubbo依赖要求

  • 工具版本要求:

    • JDK:6及以上
    • Maven:3及以上
  • Maven依赖配置:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>${dubbo.version}</version>
</dependency>
  • Dubbo缺省依赖以下库:
1
2
3
4
[INFO] +- com.alibaba:dubbo:jar:2.5.9-SNAPSHOT:compile
[INFO] | +- org.springframework:spring-context:jar:4.3.10.RELEASE:compile
[INFO] | +- org.javassist:javassist:jar:3.21.0-GA:compile
[INFO] | \- org.jboss.netty:netty:jar:3.2.5.Final:compile
  • 可选依赖
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
netty-all 4.0.35.Final
mina: 1.1.7
grizzly: 2.1.4
httpclient: 4.5.3
hessian_lite: 3.2.1-fixed
fastjson: 1.2.31
zookeeper: 3.4.9
jedis: 2.9.0
xmemcached: 1.3.6
hessian: 4.0.38
jetty: 6.1.26
hibernate-validator: 5.4.1.Final
zkclient: 0.2
curator: 2.12.0
cxf: 3.0.14
thrift: 0.8.0
servlet: 3.0 6
validation-api: 1.1.0.GA 6
jcache: 1.0.0 6
javax.el: 3.0.1-b08 6
kryo: 4.0.1
kryo-serializers: 0.42
fst: 2.48-jdk-6
resteasy: 3.0.19.Final
tomcat-embed-core: 8.0.11
slf4j: 1.7.25
log4j: 1.2.16

Spring中Dubbo应用示例

  • 定义服务接口:由于服务的提供者和消费者都依赖于同一个接口,因此强烈建议将接口定义在一个单独的模块里面,从而方便服务提供者模块和消费者模块来依赖。
1
2
3
4
5
package com.alibaba.dubbo.demo;
public interface DemoService {
String sayHello(String name);
}
  • 实现服务的提供者
1
2
3
4
5
6
7
8
package com.alibaba.dubbo.demo.provider;
import com.alibaba.dubbo.demo.DemoService;
public class DemoServiceImpl implements DemoService {
public String sayHello(String name) {
return "Hello " + name;
}
}
  • 配置服务的提供者–在spring中如何配置Dubbo服务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="hello-world-app" />
<!-- 使用multicast广播注册中心暴露服务地址 -->
<dubbo:registry address="multicast://224.5.6.7:1234" />
<!-- 用dubbo协议在20880端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20880" />
<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService" />
<!-- 和本地bean一样实现服务 -->
<bean id="demoService" class="com.alibaba.dubbo.demo.provider.DemoServiceImpl" />
</beans>

从配置文件中可以看到我们只将demoService的接口暴露给一个URL,dubbo://127.0.0.1:20880,并将该服务注册给一个多媒体地址:multicast://224.5.6.7:1234

dubbo协议是Dubbo框架支持的众多协议之一,dubbo协议是在Java NIO特性的基础之上的,为Dubbo架构的默认协议。

  • 开启服务
1
2
3
4
5
6
7
8
9
10
11
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Provider {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
new String[] {"META-INF/spring/dubbo-demo-provider.xml"});
context.start();
// press any key to exit
System.in.read();
}
}
  • 配置服务消费者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 -->
<dubbo:application name="consumer-of-helloworld-app" />
<!-- 使用multicast广播注册中心暴露发现服务地址 -->
<dubbo:registry address="multicast://224.5.6.7:1234" />
<!-- 生成远程服务代理,可以和本地bean一样使用demoService -->
<dubbo:reference id="demoService" interface="com.alibaba.dubbo.demo.DemoService" />
</beans>

这个过程看起来和传统的web服务调用很想,但是dubbo协议使得其调用更加简单,轻量化,效率相对要高。

  • 运行服务消费者
1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.alibaba.dubbo.demo.DemoService;
public class Consumer {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"http://10.20.160.198/wiki/display/dubbo/consumer.xml"});
context.start();
DemoService demoService = (DemoService)context.getBean("demoService"); // 获取远程服务代理
String hello = demoService.sayHello("world"); // 执行远程方法
System.out.println( hello ); // 显示调用结果
}
}
  • 配置简单注册中心

当我们使用多点传播注册中心时,注册服务不是单独的,因此适用于局域网场景。为了适应更加便于管理的注册中心,可以使用SimpleRegistryService。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dubbo:application name="simple-registry" />
<dubbo:protocol port="9090" />
<dubbo:service interface="com.alibaba.dubbo.registry.RegistryService"
ref="registryService" registry="N/A" ondisconnect="disconnect">
<dubbo:method name="subscribe">
<dubbo:argument index="1" callback="true" />
</dubbo:method>
<dubbo:method name="unsubscribe">
<dubbo:argument index="1" callback="true" />
</dubbo:method>
</dubbo:service>
<bean class="com.alibaba.dubbo.registry.simple.SimpleRegistryService"
id="registryService" />

可以用于测试,但是实际工程中不推荐使用。

Dubbo配置

XML配置

xml配置Dubbo可以参照上一节的实例,spring中可以直接通过dubbo标签库来对其进行配置设定等操作,使用广泛且条理清晰。所有的标签都支持自定义参数,用于不同扩展点实现的特殊配置:

1
2
3
<dubbo:protocol name="jms">
<dubbo:parameter key="queue" value="your_queue" />
</dubbo:protocol>

或者

1
2
//2.1之后开始支持这种自定义参数的方式
<dubbo:protocol name="jms" p:queue="your_queue" />
  • 常用标签
标签 用途 解释
<dubbo:service/> 服务配置 用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心
<dubbo:reference/> 引用配置 用于创建一个远程服务代理,一个引用可以指向多个注册中心
<dubbo:protocol/> 协议配置 用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受
<dubbo:application/> 应用配置 用于配置当前应用信息,不管该应用是提供者还是消费者
<dubbo:module/> 模块配置 用于配置当前模块信息,可选
<dubbo:registry/> 注册中心配置 用于配置连接注册中心相关信息
<dubbo:monitor/> 监控中心配置 用于配置连接监控中心相关信息,可选
<dubbo:provider/> 提供方配置 当ProtocolConfig 和 ServiceConfig 某属性没有配置时,采用此缺省值,可选
<dubbo:consumer/> 消费方配置 当 ReferenceConfig 某属性没有配置时,采用此缺省值,可选
<dubbo:method/> 方法配置 用于 ServiceConfig 和 ReferenceConfig 指定方法级的配置信息
<dubbo:argument/> 参数配置 用于指定方法参数配置
  • 标签之间的关系

Alt text

配置覆盖关系为方法级别的优先,接口几倍的次之,全局配置再次之,级别一样消费者优先,提供者次之

属性配置

公共配置简单,没有多注册中心,多协议等情况,或者多个Spring容器想要共享配置,可以使用dubbo.properties作为缺省配置。Dubbo会自动加载classpath路径下的dubbo.properties文件,可以通过JVM启动参数-Ddubbo.properties.file=xxx.properties改动缺省位置。

  • 映射规则:将XML配置的标签名,属性名,用点分隔,多个属性拆分成多行。
1
2
3
dubbo.application.name=foo
dubbo.application.owner=bar
dubbo.registry.address=10.20.153.10:9090
  • 覆盖策略:JVM启动,-D参数优先,XML次之,Properties文件最后。只有XML没有配置时,dubbo.properties才会生效。

Java API配置Dubbo

除了spring配置文件来配置Dubbo之外,通过Java API,property文件以及注解来配置Dubbo同样可以。配置文件和注解配置适用于结构不是很复杂的场景。下面介绍如何使用API来配置Dubbo。

  • 服务提供者
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
import com.alibaba.dubbo.rpc.config.ApplicationConfig;
import com.alibaba.dubbo.rpc.config.RegistryConfig;
import com.alibaba.dubbo.rpc.config.ProviderConfig;
import com.alibaba.dubbo.rpc.config.ServiceConfig;
import com.xxx.XxxService;
import com.xxx.XxxServiceImpl;
// 服务实现
XxxService xxxService = new XxxServiceImpl();
// 当前应用配置
ApplicationConfig application = new ApplicationConfig();
application.setName("xxx");
// 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("10.20.130.230:9090");
registry.setUsername("aaa");
registry.setPassword("bbb");
// 服务提供者协议配置
ProtocolConfig protocol = new ProtocolConfig();
protocol.setName("dubbo");
protocol.setPort(12345);
protocol.setThreads(200);
// 注意:ServiceConfig为重对象,内部封装了与注册中心的连接,以及开启服务端口
// 服务提供者暴露服务配置
ServiceConfig<XxxService> service = new ServiceConfig<XxxService>(); // 此实例很重,封装了与注册中心的连接,请自行缓存,否则可能造成内存和连接泄漏
service.setApplication(application);
service.setRegistry(registry); // 多个注册中心可以用setRegistries()
service.setProtocol(protocol); // 多个协议可以用setProtocols()
service.setInterface(XxxService.class);
service.setRef(xxxService);
service.setVersion("1.0.0");
// 暴露及注册服务
service.export();
  • 此时,服务已经暴露给多点传播的注册中心了,然后配置服务消费者:
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
import com.alibaba.dubbo.rpc.config.ApplicationConfig;
import com.alibaba.dubbo.rpc.config.RegistryConfig;
import com.alibaba.dubbo.rpc.config.ConsumerConfig;
import com.alibaba.dubbo.rpc.config.ReferenceConfig;
import com.xxx.XxxService;
// 当前应用配置
ApplicationConfig application = new ApplicationConfig();
application.setName("yyy");
// 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("10.20.130.230:9090");
registry.setUsername("aaa");
registry.setPassword("bbb");
// 注意:ReferenceConfig为重对象,内部封装了与注册中心的连接,以及与服务提供方的连接
// 引用远程服务
ReferenceConfig<XxxService> reference = new ReferenceConfig<XxxService>(); // 此实例很重,封装了与注册中心的连接以及与提供者的连接,请自行缓存,否则可能造成内存和连接泄漏
reference.setApplication(application);
reference.setRegistry(registry); // 多个注册中心可以用setRegistries()
reference.setInterface(XxxService.class);
reference.setVersion("1.0.0");
// 和本地bean一样使用xxxService
XxxService xxxService = reference.get(); // 注意:此代理对象内部封装了所有通讯细节,对象较重,请缓存复用

注解配置

服务提供方

  • Service注解暴露服务
1
2
3
4
5
6
import com.alibaba.dubbo.config.annotation.Service;
@Service(timeout = 5000)
public class AnnotateServiceImpl implements AnnotateService {
// ...
}
  • javaconfig形式配置公共模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class DubboConfiguration {
@Bean
public ApplicationConfig applicationConfig() {
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("provider-test");
return applicationConfig;
}
@Bean
public RegistryConfig registryConfig() {
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://127.0.0.1:2181");
registryConfig.setClient("curator");
return registryConfig;
}
}
  • 指定Java扫描路径
1
2
3
4
5
@SpringBootApplication
@DubboComponentScan(basePackages = "com.alibaba.dubbo.test.service.impl")
public class ProviderTestApp {
// ...
}

服务消费者

  • reference 注解引用服务
1
2
3
4
5
6
7
public class AnnotationConsumeService {
@com.alibaba.dubbo.config.annotation.Reference
public AnnotateService annotateService;
// ...
}
  • javaconfig形式配置公共模块
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
@Configuration
public class DubboConfiguration {
@Bean
public ApplicationConfig applicationConfig() {
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("consumer-test");
return applicationConfig;
}
@Bean
public ConsumerConfig consumerConfig() {
ConsumerConfig consumerConfig = new ConsumerConfig();
consumerConfig.setTimeout(3000);
return consumerConfig;
}
@Bean
public RegistryConfig registryConfig() {
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://127.0.0.1:2181");
registryConfig.setClient("curator");
return registryConfig;
}
}
  • 指定dubbo扫描路径
1
2
3
4
5
@SpringBootApplication
@DubboComponentScan(basePackages = "com.alibaba.dubbo.test.service")
public class ConsumerTestApp {
// ...
}

可以看出,还是XML配置更加的简洁明了。

Dubbo支持的协议

Dubbo框架支持多种协议,包括dubbo、RMI、hessian、HTTP、web service、thrift、memcached、redis等。大多数协议之前都已经了解过,除了dubbo这个协议。

dubbo协议在服务提供者和消费者之间建立了长连接,这种长连接和非阻塞的网络传输在传输数据小于100k的时候效率非常高。dubbo协议中的参数包含端口,每个消费者的连接数,最大接受的连接数等。

<dubbo:protocol name="dubbo" port="20880" connections="2" accepts="1000" />

Dubbo还支持通过不同协议来同时暴露服务。

1
2
3
4
5
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:protocol name="rmi" port="1099" />
<dubbo:service interface="com.baeldung.dubbo.remote.GreetingsService" version="1.0.0" ref="greetingsService" protocol="dubbo" />
<dubbo:service interface="com.bealdung.dubbo.remote.AnotherService" version="1.0.0" ref="anotherService" protocol="rmi" />

结果缓存

Dubbo对于远程调用的结果进行了本地的缓存来提高对于热点数据的访问速度。实现添加cache属性即可:

1
<dubbo:reference interface="com.baeldung.dubbo.remote.GreetingsService" id="greetingsService" cache="lru" />

缓存策略选择的是最少最近使用缓存,当我们队服务的提供者的实现类进行修改之后:

1
2
3
4
5
6
7
8
9
public class GreetingsServiceSpecialImpl implements GreetingsService {
@Override
public String sayHi(String name) {
try {
SECONDS.sleep(5);
} catch (Exception ignored) { }
return "hi, " + name;
}
}

对调用速度进行一下测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void givenProvider_whenConsumerSaysHi_thenGotResponse() {
ClassPathXmlApplicationContext localContext
= new ClassPathXmlApplicationContext("multicast/consumer-app.xml");
localContext.start();
GreetingsService greetingsService
= (GreetingsService) localContext.getBean("greetingsService");
long before = System.currentTimeMillis();
String hiMessage = greetingsService.sayHi("baeldung");
long timeElapsed = System.currentTimeMillis() - before;
assertTrue(timeElapsed > 5000);
assertNotNull(hiMessage);
assertEquals("hi, baeldung", hiMessage);
before = System.currentTimeMillis();
hiMessage = greetingsService.sayHi("baeldung");
timeElapsed = System.currentTimeMillis() - before;
assertTrue(timeElapsed < 1000);
assertNotNull(hiMessage);
assertEquals("hi, baeldung", hiMessage);
}

除了第一次调用,后面的调用几乎是立刻完成的,说明缓存生效了。

zookeeper

Dubbo通过负载均衡和集中容错策略可以支持我们来自由的扩展我们的服务。假定我们使用zookeeper来做我们管理服务的集群。服务提供者在zookeeper上注册服务:

1
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>

为了引入使用以上的注册方式,需要引入zookeeper的相关依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.11</version>
</dependency>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>

负载均衡

  • 随机负载均衡
    • 随机,按照权重设置随机概率
    • 在一个截面上碰撞的概率高,单调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
  • 轮询负载均衡
    • 按公约后的权重设置轮询比率。
    • 存在慢的提供者累积请求的问题,比如第二台机器很慢,当请求调到第二台就卡在那里,时间久了所有请求都卡在第二台机器上。
  • 最少活跃数负载均衡
    • 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
    • 使慢的提供者受到更少的请求,因为越慢的提供者的调用前后计数差会越大。
  • 一致性哈希负载均衡
    • 相同参数的请求总是发到同一个提供者
    • 当某一台提供者挂掉时,原本发往该提供者的请求,基于虚拟节点,平摊到其他提供者,不会引起剧烈变动。
    • 缺醒只对第一个参数哈希,也可以配置修改。
    • 缺醒使用160份虚拟节点,如果要修改,请配置

配置:

  • 服务端

    • 服务级别<dubbo:service interface="..." loadbalance="roundrobin" />
    • 方法级别
      1
      2
      3
      <dubbo:service interface="...">
      <dubbo:method name="..." loadbalance="roundrobin"/>
      </dubbo:service>
  • 客户端

    • 服务级别<dubbo:reference interface="..." loadbalance="roundrobin" />
    • 方法级别
      1
      2
      3
      <dubbo:reference interface="...">
      <dubbo:method name="..." loadbalance="roundrobin"/>
      </dubbo:reference>

轮询负载均衡举例

假定在一个集群里面存在两这个服务的实现作为服务提供者,请求时采用轮询方式做负载均衡。首先,创建服务提供者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Before
public void initRemote() {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(() -> {
ClassPathXmlApplicationContext remoteContext
= new ClassPathXmlApplicationContext("cluster/provider-app-default.xml");
remoteContext.start();
});
executorService.submit(() -> {
ClassPathXmlApplicationContext backupRemoteContext
= new ClassPathXmlApplicationContext("cluster/provider-app-special.xml");
backupRemoteContext.start();
});
}

然后我们有一个标准的高速服务提供者,可以立即给出相应结果,一个低速服务提供者,每个响应需要等候5s。执行六次请求,此时采用轮询的负载均衡侧率,则期待的是平均相应时间为2.5s。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void givenProviderCluster_whenConsumerSaysHi_thenResponseBalanced() {
ClassPathXmlApplicationContext localContext
= new ClassPathXmlApplicationContext("cluster/consumer-app-lb.xml");
localContext.start();
GreetingsService greetingsService
= (GreetingsService) localContext.getBean("greetingsService");
List<Long> elapseList = new ArrayList<>(6);
for (int i = 0; i < 6; i++) {
long current = System.currentTimeMillis();
String hiMessage = greetingsService.sayHi("baeldung");
assertNotNull(hiMessage);
elapseList.add(System.currentTimeMillis() - current);
}
OptionalDouble avgElapse = elapseList
.stream()
.mapToLong(e -> e)
.average();
assertTrue(avgElapse.isPresent());
assertTrue(avgElapse.getAsDouble() > 2500.0);
}

同时,动态负载均衡是支持的,下面的例子说明这一点,采用的是轮询的负载均衡策略,当新的服务注册时候,消费者会选择新的服务。低俗的服务提供者在系统启动后2s注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Before
public void initRemote() {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(() -> {
ClassPathXmlApplicationContext remoteContext
= new ClassPathXmlApplicationContext("cluster/provider-app-default.xml");
remoteContext.start();
});
executorService.submit(() -> {
SECONDS.sleep(2);
ClassPathXmlApplicationContext backupRemoteContext
= new ClassPathXmlApplicationContext("cluster/provider-app-special.xml");
backupRemoteContext.start();
return null;
});
}

服务提供者此时每秒调用一次服务,6次之后,我们期望平均响应时间大于1.6s。

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
@Test
public void givenProviderCluster_whenConsumerSaysHi_thenResponseBalanced()
throws InterruptedException {
ClassPathXmlApplicationContext localContext
= new ClassPathXmlApplicationContext("cluster/consumer-app-lb.xml");
localContext.start();
GreetingsService greetingsService
= (GreetingsService) localContext.getBean("greetingsService");
List<Long> elapseList = new ArrayList<>(6);
for (int i = 0; i < 6; i++) {
long current = System.currentTimeMillis();
String hiMessage = greetingsService.sayHi("baeldung");
assertNotNull(hiMessage);
elapseList.add(System.currentTimeMillis() - current);
SECONDS.sleep(1);
}
OptionalDouble avgElapse = elapseList
.stream()
.mapToLong(e -> e)
.average();
assertTrue(avgElapse.isPresent());
assertTrue(avgElapse.getAsDouble() > 1666.0);
}

负载均衡在服务提供者和服务消费者都可以使用。

集群容错

Dubbo支持多种容错机制,默认为failover重试。

Alt text

各节点的关系

  • Invoke是Provider的一个可调用Service的抽象,Invoker封装了Provider地址及Service接口信息。
  • Directory代表多个Invoker,List,值可以动态变化,比如注册中心推送变更
  • Cluster将Director中的多个Invoker封装成一个Invoker,对上层逃命,伪装过程包含了容错逻辑,如果失败后,重试另一个
  • Route负责从多个Invoker中按照路由规则选出子集没比如读写分离,应用隔离等
  • loadbalance负责从多个Invoker中选出一个具体用于本地调用,选的过程中包含了负载均衡该算法。

集群容错模式

  • fail-over
    • 当一个消费者获取一个服务失败的时候,会尝试访问集群中其他的服务提供者
    • 通常用于读操作,但是重试会带来更长的延迟
    • 可通过retries=“2”来设置重试次数,其中第一次不包含在内。
  • fail-safe
    • 失败安全,出现异常时,直接忽略
    • 通常用于写入审计日志等操作
  • fail-fast
    • 快速失败,只发起一次调用,失败立即报错
    • 通常用于非幂等性的写操作,比如新增记录
  • fail-back
    • 失败自动恢复,后台记录失败请求,定时重发
    • 通常用于消息通知操作
  • forking
    • 并行调度多个服务器,只要一个成功及返回。
    • 通常实时性要求比较高的读操作,但是需要浪费很多服务资源,可以通过forks=”2”来设置最大并行数
  • broadcast
    • 广播调用所有的提供者,逐个调用,任意一台报错则报错。
    • 通常用于通知所有提供者更新缓存或者日志等本地资源信息。

集群模式配置

1
2
3
4
//服务端
<dubbo:service cluster="failsafe" />
//客户端
<dubbo:reference cluster="failsafe" />

线性模型

Alt text

  • 如果事件的处理逻辑能够迅速完成,并且不会发起新的IO请求,比如只是在内存中标记个标识,则直接在IO线程上处理更快,因为这样减少了线程池的调度。
  • 如果事件处理逻辑较慢,或者需要发起新的IO请求,比如需要查询数据库,则必须派发到线程池,否则IO线程阻塞,将导致不能接受其他请求。
  • 如果用IO线程处理事件,又在事件处理过程中发起新的IO请求,比如在链接事件中发起登录请求,会报可能发生死锁异常。

针对不同的场景,需要设定相应的派发侧率和不同的线程池:

1
<dubbo:protocol name="dubbo" dispatcher="all" threadpool="fixed" threads="100" />

Dispatcher

  • all,所有消息都派发到线程池,包括请求,相应,连接事件,断开事件,心跳等。
  • direct,所有消息都不派发到线程池,全部在IO线程上直接执行
  • message,只有请求相应消息派发到线程池,其他直接在IO线程执行
  • execution,只将请求消息派发到线程池,其他直接在IO线程执行
  • connection,在IO线程上,将连接断开事件放入队列,顺序执行,其他消息派发到线程池

threadPool

  • fixed,固定大小线程池,启动时创建线程,不关闭,一致持有,缺省
  • cached,缓存线程池,线程空闲一分钟自动删除,需要时创建
  • limited,可伸缩性线程池,单线程池中的线程数只会增长不会收缩,避免收缩带来的性能问题。
  • eager,优先创建worker线程池,当任务数量大于corePoolSize但小于maximumPoolSize时,优先创建Worker来处理任务。当任务数量大于maximumpoolSize时,将任务放入阻塞队列,阻塞队列充满时抛出RejectedExecutionException。

客户端和服务端直连

测试过程中,可以绕过注册中心来直接连接,此时会忽略注册中心的提供者列表,A接口配置点对点,不影响B接口从注册中心获取列表。

配置方法:

  • XML:<dubbo:reference id="xxxService" interface="com.alibaba.xxx.XxxService" url="dubbo://localhost:20890" />

  • JVM:java -Dcom.alibaba.xxx.XxxService=dubbo://localhost:20890

  • properties: com.alibaba.xxx.XxxService=dubbo://localhost:20890

Donate comment here