1. 简介
Dubbo是阿里巴巴开源的RPC和微服务框架。
除此之外,它有助于加强服务治理,并使传统的整体应用程序能够顺利重构为可扩展的分布式架构。
在本文中,我们将介绍Dubbo及其最重要的功能。
2. 架构
Dubbo区分了几个角色:
- 提供者:服务公开的地方;提供者将其服务注册到注册中心
- 容器:服务启动、加载和运行的地方
- 消费者:调用远程服务;消费者将订阅注册中心中所需的服务
- 注册中心:服务注册和发现的地方
- 监控:记录服务的统计信息,例如给定时间间隔内的服务调用频率
(来源:http://dubbo.io/images/dubbo-architecture.png)
提供者、消费者和注册中心之间的连接是持久的,因此每当服务提供者发生故障时,注册中心都可以检测到故障并通知消费者。
注册中心和监控器都是可选的,消费者可以直接连接服务提供者,但是这样会影响整个系统的稳定性。
3. Maven依赖
在深入研究之前,让我们在pom.xml中添加以下依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.5.7</version>
</dependency>
最新版本可以在这里找到。
4. 引导
现在我们来尝试一下Dubbo的基本功能。
这是一个微侵入式框架,它的很多功能依赖于外部配置或注解。
官方建议我们使用XML配置文件,因为它依赖于Spring容器(目前是Spring 4.3.10)。
我们将使用XML配置演示其大部分功能。
4.1 多播注册中心–服务提供者
作为快速入门,我们只需要一个服务提供者、一个消费者和一个“不可见”的注册中心。由于我们使用的是多播网络,因此注册中心是不可见的。
在以下示例中,提供者仅向其消费者说“hi”:
public interface GreetingsService {
String sayHi(String name);
}
public class GreetingsServiceImpl implements GreetingsService {
@Override
public String sayHi(String name) {
return "hi, " + name;
}
}
为了进行远程过程调用,消费者必须与服务提供者共享一个公共接口,因此必须与消费者共享接口GreetingsService。
4.2 多播注册中心–服务注册
现在让我们将GreetingsService注册到注册中心,如果提供者和消费者都在同一个本地网络上,则一种非常方便的方法是使用多播注册中心:
<dubbo:application name="demo-provider" version="1.0"/>
<dubbo:registry address="multicast://224.1.1.1:9090"/>
<dubbo:protocol name="dubbo" port="20880"/>
<bean id="greetingsService" class="cn.tuyucheng.taketoday.dubbo.remote.GreetingsServiceImpl"/>
<dubbo:service interface="cn.tuyucheng.taketoday.dubbo.remote.GreetingsService"
ref="greetingsService"/>
通过上述Bean配置,我们刚刚将GreetingsService暴露给dubbo://127.0.0.1:20880下的url,并将该服务注册到<dubbo:registry/>中指定的多播地址。
在提供者的配置中,我们还分别通过<dubbo:application/>,<dubbo:service/>和<Beans/>声明了我们的应用程序元数据,要发布的接口及其实现。
dubbo协议是框架支持的众多协议之一,它建立在Java NIO非阻塞特性之上,是使用的默认协议。
我们将在本文后面更详细地讨论它。
4.3 多播注册中心–服务消费者
一般来说,消费者需要指定调用的接口,以及远程服务的地址,下面就是消费者需要的内容:
<dubbo:application name="demo-consumer" version="1.0"/>
<dubbo:registry address="multicast://224.1.1.1:9090"/>
<dubbo:reference interface="cn.tuyucheng.taketoday.dubbo.remote.GreetingsService"
id="greetingsService"/>
现在一切都设置好了,让我们看看它们是如何运作的:
public class MulticastRegistryTest {
@Before
public void initRemote() {
ClassPathXmlApplicationContext remoteContext
= new ClassPathXmlApplicationContext("multicast/provider-app.xml");
remoteContext.start();
}
@Test
public void givenProvider_whenConsumerSaysHi_thenGotResponse(){
ClassPathXmlApplicationContext localContext
= new ClassPathXmlApplicationContext("multicast/consumer-app.xml");
localContext.start();
GreetingsService greetingsService
= (GreetingsService) localContext.getBean("greetingsService");
String hiMessage = greetingsService.sayHi("tuyucheng");
assertNotNull(hiMessage);
assertEquals("hi, tuyucheng", hiMessage);
}
}
当提供者的remoteContext启动时,Dubbo将自动加载GreetingsService并将其注册到给定的注册中心。在本例中,它是一个多播注册中心。
消费者订阅多播注册中心并在上下文中创建GreetingsService的代理,当我们的本地客户端调用sayHi方法时,它实际上是在透明地调用远程服务。
我们提到注册中心是可选的,这意味着消费者可以通过公开的端口直接连接到提供者:
<dubbo:reference interface="cn.tuyucheng.taketoday.dubbo.remote.GreetingsService"
id="greetingsService" url="dubbo://127.0.0.1:20880"/>
基本上,该过程与传统的Web服务类似,但Dubbo使其变得简单、朴素和轻量级。
4.4 简单注册中心
请注意,使用“隐形”多播注册中心时,注册中心服务不是独立的。但是,它仅适用于受限的本地网络。
为了明确设置可管理的注册中心,我们可以使用SimpleRegistryService。
将以下Bean配置加载到Spring上下文后,将启动一个简单的注册服务:
<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" />
请注意,SimpleRegistryService类不包含在工件中,因此我们直接从GitHub仓库复制源代码。
然后我们需要调整提供者和消费者的注册中心配置:
<dubbo:registry address="127.0.0.1:9090"/>
SimpleRegistryService在测试时可以作为独立的注册中心使用,但不建议在生产环境中使用。
4.5 Java配置
还支持通过Java API、属性文件和注解进行配置。但是,属性文件和注解仅适用于我们的架构不太复杂的情况。
让我们看看如何将之前的多播注册中心XML配置转换为API配置。首先,提供者设置如下:
ApplicationConfig application = new ApplicationConfig();
application.setName("demo-provider");
application.setVersion("1.0");
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("multicast://224.1.1.1:9090");
ServiceConfig<GreetingsService> service = new ServiceConfig<>();
service.setApplication(application);
service.setRegistry(registryConfig);
service.setInterface(GreetingsService.class);
service.setRef(new GreetingsServiceImpl());
service.export();
现在该服务已经通过多播注册中心公开,让我们在本地客户端中使用它:
ApplicationConfig application = new ApplicationConfig();
application.setName("demo-consumer");
application.setVersion("1.0");
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("multicast://224.1.1.1:9090");
ReferenceConfig<GreetingsService> reference = new ReferenceConfig<>();
reference.setApplication(application);
reference.setRegistry(registryConfig);
reference.setInterface(GreetingsService.class);
GreetingsService greetingsService = reference.get();
String hiMessage = greetingsService.sayHi("tuyucheng");
虽然上面的代码片段和前面的XML配置示例一样好用,但它还是有点简单。目前,如果我们想充分利用Dubbo,XML配置应该是首选。
5. 协议支持
该框架支持多种协议,包括dubbo、RMI、hessian、HTTP、web service、thrift、memcached和redis。除了dubbo,大多数协议看起来都很熟悉。让我们看看这个协议有什么新东西。
dubbo协议在服务提供方和消费者之间保持长连接,长连接以及NIO非阻塞网络通信使得在传输小规模数据包(<100K)时有相当好的性能。
有几个可配置的属性,例如端口、每个消费者的连接数、最大接受连接数等。
<dubbo:protocol name="dubbo" port="20880"
connections="2" accepts="1000" />
Dubbo还支持同时通过不同的协议公开服务:
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:protocol name="rmi" port="1099" />
<dubbo:service interface="cn.tuyucheng.taketoday.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" />
是的,我们可以使用不同的协议公开不同的服务,如上面的代码片段所示。底层传输器、序列化实现和其他与网络相关的常见属性也是可配置的。
6. 结果缓存
支持本地远程结果缓存,以加快对热门数据的访问。只需向Bean引用添加cache属性即可:
<dubbo:reference interface="cn.tuyucheng.taketoday.dubbo.remote.GreetingsService"
id="greetingsService" cache="lru" />
这里我们配置了一个最近最少使用的缓存。为了验证缓存行为,我们将在之前的标准实现中做一些改动(我们称之为“特殊实现”):
public class GreetingsServiceSpecialImpl implements GreetingsService {
@Override
public String sayHi(String name) {
try {
SECONDS.sleep(5);
} catch (Exception ignored) { }
return "hi, " + name;
}
}
启动提供者后,我们可以在消费者端验证,在多次调用时结果是否被缓存:
@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("tuyucheng");
long timeElapsed = System.currentTimeMillis() - before;
assertTrue(timeElapsed > 5000);
assertNotNull(hiMessage);
assertEquals("hi, tuyucheng", hiMessage);
before = System.currentTimeMillis();
hiMessage = greetingsService.sayHi("tuyucheng");
timeElapsed = System.currentTimeMillis() - before;
assertTrue(timeElapsed < 1000);
assertNotNull(hiMessage);
assertEquals("hi, tuyucheng", hiMessage);
}
这里消费者调用的是特殊服务实现,所以第一次调用需要5秒多的时间才能完成。当我们再次调用时,sayHi方法几乎立即完成,因为结果是从缓存中返回的。
请注意,线程本地缓存和JCache也受支持。
7. 集群支持
Dubbo具有负载均衡能力和多种容错策略,可帮助我们自由扩展服务。这里假设我们有Zookeeper作为注册中心来管理集群中的服务,提供者可以像这样在Zookeeper中注册他们的服务:
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
请注意,我们需要POM中的这些额外依赖:
<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>
可以在这里和这里找到最新版本的zookeeper依赖和zkclient。
7.1 负载均衡
目前,框架支持几种负载平衡策略:
- 随机
- 循环
- 最不活跃
- 一致性哈希
在以下示例中,我们在集群中有两个服务实现作为提供者,请求使用循环方法进行路由。
首先,让我们设置服务提供者:
@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();
});
}
现在我们有一个可以立即响应的标准“快速提供者”,还有一个在每次请求时休眠5秒的特殊“慢速提供者”。
采用循环策略运行6次后,我们预计平均响应时间至少为2.5秒:
@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("tuyucheng");
assertNotNull(hiMessage);
elapseList.add(System.currentTimeMillis() - current);
}
OptionalDouble avgElapse = elapseList
.stream()
.mapToLong(e -> e)
.average();
assertTrue(avgElapse.isPresent());
assertTrue(avgElapse.getAsDouble() > 2500.0);
}
此外,还采用了动态负载均衡。下一个示例演示了使用循环策略,当新的服务提供者上线时,消费者会自动选择新的服务提供者作为候选。
系统启动后2秒才注册“慢速提供者”:
@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.6秒:
@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("tuyucheng");
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:reference interface="cn.tuyucheng.taketoday.dubbo.remote.GreetingsService"
id="greetingsService" loadbalance="roundrobin" />
7.2 容错
Dubbo支持多种容错策略,包括:
- 故障转移
- 故障安全
- 快速失败
- 故障恢复
- 分叉
在故障转移的情况下,当一个提供者发生故障时,消费者可以尝试集群中的其他服务提供者。
服务提供方的容错策略配置如下:
<dubbo:service interface="cn.tuyucheng.taketoday.dubbo.remote.GreetingsService"
ref="greetingsService" cluster="failover"/>
为了演示服务故障转移的实际操作,让我们创建GreetingsService的故障转移实现:
public class GreetingsFailoverServiceImpl implements GreetingsService {
@Override
public String sayHi(String name) {
return "hi, failover " + name;
}
}
我们可以回想一下,我们的特殊服务实现GreetingsServiceSpecialImpl对于每个请求都会休眠5秒。
当任何响应时间超过2秒时,对于消费者来说都会被视为请求失败,我们会有一个故障转移场景:
<dubbo:reference interface="cn.tuyucheng.taketoday.dubbo.remote.GreetingsService"
id="greetingsService" retries="2" timeout="2000" />
启动两个提供者后,我们可以使用以下代码片段验证故障转移行为:
@Test
public void whenConsumerSaysHi_thenGotFailoverResponse() {
ClassPathXmlApplicationContext localContext
= new ClassPathXmlApplicationContext(
"cluster/consumer-app-failtest.xml");
localContext.start();
GreetingsService greetingsService
= (GreetingsService) localContext.getBean("greetingsService");
String hiMessage = greetingsService.sayHi("tuyucheng");
assertNotNull(hiMessage);
assertEquals("hi, failover tuyucheng", hiMessage);
}
8. 总结
在本教程中,我们简单了解了Dubbo,大多数用户被它的简洁和丰富强大的功能所吸引。
除了本文介绍的内容之外,该框架还有许多功能尚待探索,例如参数验证、通知和回调、通用实现和引用、远程结果分组和合并、服务升级和向后兼容等等。
Post Directory
