Dubbo 简介
1. 简介
Dubbo 是阿里巴巴开源的 RPC 和微服务框架。
除其他外,它有助于增强服务治理,并使传统的单体应用程序可以平滑地重构为可扩展的分布式架构。
在本文中,我们将介绍 Dubbo 及其最重要的特性。
2. 架构
Dubbo 区分了几个角色:
- 提供者——服务暴露的地方;提供者将其服务注册到注册表
- 容器——服务启动、加载和运行的地方
- 消费者——调用远程服务的人;消费者将订阅注册表中所需的服务
- Registry——注册和发现服务的地方
- Monitor – 记录服务的统计信息,例如,给定时间间隔内的服务调用频率
提供者、消费者和注册中心之间的连接是持久的,因此每当服务提供者关闭时,注册中心可以检测到故障并通知消费者。
注册表和监视器是可选的。消费者可以直接连接到服务提供商,但整个系统的稳定性会受到影响。
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="com.blogdemo.dubbo.remote.GreetingsServiceImpl"/>
<dubbo:service interface="com.blogdemo.dubbo.remote.GreetingsService"
ref="greetingsService"/>
通过上面的 beans 配置,我们刚刚将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="com.blogdemo.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("blogdemo");
assertNotNull(hiMessage);
assertEquals("hi, blogdemo", hiMessage);
}
}
当提供者的remoteContext启动时,Dubbo 会自动加载GreetingsService并将其注册到给定的注册中心。在这种情况下,它是一个多播注册表。
消费者订阅多播注册并在上下文中创建GreetingsService的代理。当我们的本地客户端调用sayHi方法时,它是透明地调用远程服务。
我们提到注册表是可选的,这意味着消费者可以通过暴露的端口直接连接到提供者:
<dubbo:reference interface="com.blogdemo.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("blogdemo");
尽管上面的代码片段与前面的 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="com.blogdemo.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 引用中添加缓存属性一样简单:
<dubbo:reference interface="com.blogdemo.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("blogdemo");
long timeElapsed = System.currentTimeMillis() - before;
assertTrue(timeElapsed > 5000);
assertNotNull(hiMessage);
assertEquals("hi, blogdemo", hiMessage);
before = System.currentTimeMillis();
hiMessage = greetingsService.sayHi("blogdemo");
timeElapsed = System.currentTimeMillis() - before;
assertTrue(timeElapsed < 1000);
assertNotNull(hiMessage);
assertEquals("hi, blogdemo", 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("blogdemo");
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("blogdemo");
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="com.blogdemo.dubbo.remote.GreetingsService"
id="greetingsService" loadbalance="roundrobin" />
7.2. 容错
Dubbo 支持多种容错策略,包括:
- fail-over
- fail-safe
- fail-fast
- fail-back
- forking
在故障转移的情况下,当一个提供者出现故障时,消费者可以尝试与集群中的其他一些服务提供者一起尝试。
服务提供商的容错策略配置如下:
<dubbo:service interface="com.blogdemo.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="com.blogdemo.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("blogdemo");
assertNotNull(hiMessage);
assertEquals("hi, failover blogdemo", hiMessage);
}