Dubbo 概述
为什么使用 Dubbo
选型时一般需要考虑:
- 业务特点及可预见的后续的发展。
- 可用性要求。
- 团队的成熟度。一个成熟的团队可以很好地 Hold 住复杂的开源框架,甚至做定制化开发。
在选择使用 Dubbo 之后,又需要考虑很多细节,比如:
- Dubbo 底层走什么协议?如何对对象进行序列化,用了哪些序列化方式?如何处理异步转同步?
- 高并发高可用性。Dubbo 依赖了 ZooKeeper,但是万一 ZooKeeper 宕机了怎么办?
如果 ZooKeeper 假死,客户端对服务端的调用是否会全部下线?如果是该如何避免?
如何监控 Dubbo 的调用,并做到优雅的客户端无感发布?
最佳实践
- 模块化
推荐将服务接口、实体、异常等都放到 API 包内,它们都是 API 的一部分。 - 粗粒度
暴露的 Dubbo 接口的粒度应尽可能得粗,代表一个完整的功能,而不是其中的某一步,否则就不得不面对分布式事务问题了,而 Dubbo 当前并没有提供分布式事务支持。 - 版本
某露服务接口的配置最好增加版本,当有不兼容的升级(比如接口定义要加个参数)时,版本可以方便地实现平滑发布,而又不用引入多余的代码。
版本只需要两位即可,比如"1.0"
,因为升级并不是频繁的操作,因为不兼容的升级不会那么频繁。
升级时,先将一半的 provider 升级到新版本,然后将所有 consumer 升级,最后将其余的 provider 升级。 - 兼容性
向后兼容:接口加方法、对象加字段;
不兼容:删除方法、删除字段、枚举类型加字段。
不兼容的情况下,可以通过升级版本来实现平滑发布。 - 枚举类型
枚举是类型安全的,但是作为 Dubbo 接口的参数 / 返回值却不合适,因为 provider 会将枚举转换为字符串传输,接收方会尝试寻找该字符串所属的枚举 field,找不到就会直接报错。 - 序列化
传值没必要使用接口抽象,因为序列化需要接口实现类的元信息(包括 getter、setter),无法隐藏实现。
参数和返回值必须 byValue 而不是 byReference,因为 Dubbo 不支持远程对象,provider 引用的对象 consumer 就找不到了。 - 异常
最好直接抛异常而不是返回异常码,因为异常可以携带更多信息、语法上也更加友好。
provider 不要将 DAO 层的异常抛给 consumer 端,consumer 端不应该关注 provider 对服务是如何实现的。
开始使用 Dubbo
ZooKeeper
ZooKeeper 在 Dubbo 中可以作为注册中心使用。
下载 ZooKeeper,修改配置,配置文件位于{ZOOKEEPER_HOME}/conf/zoo.cfg:
1 | dataDir = /tmp/zk/data |
- dataDir:数据保存的目录
- clientPort:监听的端口
- tickTime:心跳检查间隔
- initLimit:Follower 启动从 Leader 同步数据时能忍受多少个心跳的时间间隔
- syncLimit:Leader 同步到 Follower 后,如果超过 syncLimit 个 tickTime 的时间过去,还没有收到 Follower 的响应,那么就认为该 Follower 已下线。
后台启动:
1 | ./bin/zkServer.sh start-foreground |
SDK
SDK 是一个被 provider 和 consumer 同时依赖的 jar 包,它的作用包括:
- 提供实体类的定义;
1
2
3public class Person {
...
} - 提供接口的定义;
1
2
3public interface UserServiceBo {
String sayHello(String name);
}
在设计 SDK 时包含一些注意要点,比如:
- 不要使用枚举,用字符串常量来替代,因为 Dubbo 反序列化时如果碰到不存在的枚举就会抛出异常,这个问题编译期无法发现,可能造成线上故障;
- 升级时不要随意修改接口定义,provider 和 consumer 接口定义不同会导致运行时故障,最佳实践是提升
dubbo:reference
和dubbo:service
的版本号,或者直接增加一个接口。
Provider
- 声明依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.6.6</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.0.35.Final</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency> - 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<?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="dubboProvider"/>
<!-- 使用zookeeper注册中心暴露服务地址 -->
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<!-- 用dubbo协议在20880端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20880"/>
<!-- 启用monitor模块 -->
<dubbo:monitor protocol="registry"/>
<bean id="userService" class="com.tallate.provider.UserServiceImpl"/>
<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="com.tallate.UserServiceBo" ref="userService"
group="dubbo" version="1.0.0" timeout="3000"/>
</beans> - 接口的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class UserServiceImpl implements UserServiceBo {
@Override
public String sayHello(String name) {
//让当前当前线程休眠2s
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return name;
}
} - 启动
原生 Spring 的启动方式:1
2
3
4
5public static void main(String[] arg) throws InterruptedException {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:provider.xml");
//挂起当前线程,如果没有改行代码,服务提供者进程会消亡,服务消费者就发现不了提供者了
Thread.currentThread().join();
}如果需要以 SpringBoot 或 Docker 方式启动可以参考官方的示例
Consumer
- 声明依赖
同 Provider - Dubbo 配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<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="dubboConsumer" />
<!-- 使用multicast广播注册中心暴露发现服务地址 -->
<dubbo:registry protocol="zookeeper" address="zookeeper://127.0.0.1:2181" />
<!-- 启动monitor-->
<dubbo:monitor protocol="registry" />
<!-- 生成远程服务代理,可以和本地bean一样使用demoService -->
<dubbo:reference id="userService" interface="com.tallate.UserServiceBo" group="dubbo" version="1.0.0" timeout="3000"/>
</beans>这里出现了一些以 dubbo 作为前缀的标签,它们是由 Dubbo 的扩展 DubboNamespaceHandler 来处理的,DubboBeanDefinitionParser 在解析完后会得到对应 BeanDefinition,然后生成对象放到 BeanFactory 中。
- 启动
1
2
3
4
5
6
7
8public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
new String[]{"classpath:consumer.xml"});
final UserServiceBo demoService = (UserServiceBo) context.getBean("userService");
System.out.println(demoService.sayHello("Hello World"));
}
调用 Dubbo 原生 API 启动
- Provider
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// 等价于<bean id="userService" class="com.test.UserServiceImpl" />
UserServiceBo userService = new UserServiceImpl();
// 等价于<dubbo:application name="dubboProvider" />
ApplicationConfig application = new ApplicationConfig();
application.setName("dubboProvider");
// 等价于<dubbo:registry address="zookeeper://127.0.0.1:2181" />
RegistryConfig registry = new RegistryConfig();
registry.setAddress("127.0.0.1:2181");
registry.setProtocol("zookeeper");
// 等价于<dubbo:protocol name="dubbo" port="20880" />
ProtocolConfig protocol = new ProtocolConfig();
protocol.setName("dubbo");
protocol.setPort(20880);
// 等价于<dubbo:monitor protocol="registry" />
MonitorConfig monitorConfig = new MonitorConfig();
monitorConfig.setProtocol("registry");
// 等价于<dubbo:service interface="com.test.UserServiceBo" ref="userService"
// group="dubbo" version="1.0.0" timeout="3000"/>
// 此实例很重,封装了与注册中心的连接,请自行缓存,否则可能造成内存和连接泄漏
ServiceConfig<UserServiceBo> service = new ServiceConfig<>();
service.setApplication(application);
service.setMonitor(monitorConfig);
// 多个注册中心可以用setRegistries()
service.setRegistry(registry);
// 多个协议可以用setProtocols()
service.setProtocol(protocol);
service.setInterface(UserServiceBo.class);
service.setRef(userService);
service.setVersion("1.0.0");
service.setGroup("dubbo");
service.setTimeout(3000);
service.export();
// 挂起当前线程
Thread.currentThread().join(); - Consumer
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// 等价于<dubbo:application name="dubboConsumer" />
ApplicationConfig application = new ApplicationConfig();
application.setName("dubboConsumer");
// 等价于<dubbo:registry protocol="zookeeper" address="zookeeper://127.0.0.1:2181" />
RegistryConfig registry = new RegistryConfig();
registry.setAddress("127.0.0.1:2181");
registry.setProtocol("zookeeper");
// 等价于 <dubbo:monitor protocol="registry" />
MonitorConfig monitorConfig = new MonitorConfig();
monitorConfig.setProtocol("registry");
//等价于<dubbo:reference id="userService" interface="com.test.UserServiceBo"
//group="dubbo" version="1.0.0" timeout="3000" />
// 此实例很重,封装了与注册中心的连接以及与提供者的连接,最好放缓存,否则可能造成内存和连接泄漏
ReferenceConfig<UserServiceBo> reference = new ReferenceConfig<>();
reference.setApplication(application);
// 多个注册中心可以用setRegistries()
reference.setRegistry(registry);
reference.setInterface(UserServiceBo.class);
reference.setVersion("1.0.0");
reference.setGroup("dubbo");
reference.setTimeout(3000);
reference.setInjvm(false);
reference.setMonitor(monitorConfig);
UserServiceBo userService = reference.get();
System.out.println(userService.sayHello("哈哈哈"));
Thread.currentThread().join();
泛化调用
正常情况下我们使用 Dubbo 时会将实体类和接口定义放到一个 SDK 包内,其实也可以不加入这个包、直接将要传的参数放到一个 Map 对象内,称为泛化调用,但是这种方式没有什么实践价值,在此就不赘述了。
Dubbo 架构
Dubbo 是一个分布式服务框架,是阿里巴巴 SOA 服务化治理方案的核心框架,致力于提供高性能和透明化的 RPC 远程服务调用方案,以及 SOA 服务治理方案。简而言之,Dubbo 是个远程服务调用的分布式框架(告别 Web Service 模式中的 WSdl,以服务提供者与消费者的方式在 dubbo 上注册)。
Apache Dubbo 是一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
Dubbo 的架构基本上可以概括为 RPC+服务发现,或者可以称之为弹性 RPC 框架。
CP+三大中心
图中的 Consumer 和 Provider 是抽象概念,只是想让看图者更直观的了解哪些类分属于客户端与服务器端,不用 Client 和 Server 的原因是 Dubbo 在很多场景下都使用 Provider、Consumer、Registry、Monitor 划分逻辑拓普节点,保持统一概念。
Provider: 暴露服务的服务提供方,启动时会注册自己提供的服务到注册中心。
Consumer: 调用远程服务的服务消费方,启动时会去注册中心订阅自己需要的服务,服务注册中心异步提供 Provider 的地址列表,Consumer 根据路由规则和预设的负载均衡算法选择一个 Provider 的 IP 进行调用,调用是直连的,失败后会调用另外一个。
Registry: 服务注册与发现的注册中心。
Monitor: 统计服务的调用次调和调用时间的监控中心,Provider 和 Consumer 在内存中累计调用次数和耗时,并定时每分钟发送一次统计数据到监控中心。
Container: 服务运行容器。
大数据量传输时适合用短连接,小数据量高并发适合用长连接。从上图中可以得知,Provider 和 Consumer 均通过长连接与注册中心通信,当消费方调用服务时,会创建一个连接,然后同时会创建一个心跳发送的定时线程池,每一分钟发送一次心跳包到注册中心,通过 ping-pong 来检查连接的存活性,同时还会启动断线重连定时线程池,每两秒钟检查一次连接状态,如果断开就重连,而当注册中心断开连接后,会回调通知 Consumer 销毁连接,同理,Provider 也是通过长连接与注册中心通信。
元数据中心
2.7 之后提供的一个新组件,容易和注册中心混淆,元数据和注册中心中的注册信息之间的区别如下:
- 元数据(Metadata)指的是服务分组、服务版本、服务名、方法列表、方法参数列表、超时时间等
- 注册信息指服务分组、服务版本、服务名、地址列表等。
元数据中心和注册中心包含了一些公共数据,另外,元数据中心还会存储方法列表即参数列表,注册中心存储了服务地址,其他的一些区别如下所示:
- | 元数据 | 注册信息
- | - | -
职责 | 描述服务,定义服务的基本属性 | 存储地址列表
变化频繁度 | 基本不变 | 随着服务上下线而不断变更
数据量 | 大 | 小
数据交互/存储模型 | 消费者/提供者上报,控制台查询 | PubSub 模型,提供者上报,消费者订阅
主要使用场景 | 服务测试、服务 | MOCK 服务调用
可用性要求 | 元数据中心可用性要求不高,不影响主流程 | 注册中心可用性要求高,影响到服务调用的主流程
Dubbo 层次化结构
Dubbo 的架构是分层的,使用这种方式可以使各个层之间解耦合(或者最大限度地松耦合)。从服务模型的角度来看,Dubbo 采用的是一种非常简单的模型,要么是提供方提供服务,要么是消费方消费服务。
- 服务接口层(Service):该层是与实际业务逻辑相关的,根据服务提供方和服务消费方的业务设计对应的接口和实现。
RPC 是 Dubbo 的核心:
- 配置层(Config)
对外配置接口,以 ServiceConfig 和 ReferenceConfig 为中心,可以直接 new 配置类,也可以通过 Spring 解析配置生成配置类。 - 服务代理层(Proxy)
服务接口透明代理。Proxy 层封装了所有接口的透明化代理,而在其它层都以 Invoker 为中心,只有到了暴露给用户使用时,才用 Proxy 将 Invoker 转成接口,或将接口实现转成 Invoker,也就是去掉 Proxy 层 RPC 是可以 Run 的,只是不那么透明,不那么看起来像调本地服务一样调远程服务。
Proxy 层封装了所有接口的透明化代理,而在其它层都以 Invoker 为中心,只有到了暴露给用户使用时,才用 Proxy 将 Invoker 转成接口,或将接口实现转成 Invoker,也就是去掉 Proxy 层 RPC 是可以 Run 的,只是不那么透明,不那么看起来像调本地服务一样调远程服务。 - 服务注册层(Registry)
封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory、Registry 和 RegistryService。可能没有服务注册中心,此时服务提供方直接暴露服务。 - 集群层(Cluster)
封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster、Directory、Router 和 LoadBalance。将多个服务提供方组合为一个服务提供方,实现对服务消费方来透明,只需要与一个服务提供方进行交互。 - 监控层(Monitor)
RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory、Monitor和 MonitorService。 - 远程调用层(Protocol)
封装 RPC 调用,扩展接口为 Protocol、Invoker 和 Exporter。Protocol 是服务域,它是 Invoker 暴露和引用的主功能入口,它负责 Invoker 的生命周期管理。Invoker 是实体域,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。Protocol 是核心层,也就是只要有 Protocol + Invoker + Exporter 就可以完成非透明的 RPC 调用,然后在 Invoker 的主过程上 Filter 拦截点。
Registry 和 Monitor 实际上不算一层,而是一个独立的节点,只是为了全局概览,用层的方式画在一起。
Cluster 是外围概念,Cluster 的目的是将多个 Invoker 伪装成一个 Invoker,这样其它人只要关注 Protocol 层 Invoker 即可,加上 Cluster 或者去掉 Cluster 对其它层都不会造成影响,因为只有一个提供者时,是不需要 Cluster 的。
Remoting 实现是 Dubbo 协议的实现,如果你选择 RMI 协议,整个 Remoting 都不会用上,Remoting 内部再划为 Transport 传输层和 Exchange 信息交换层,Transport 层只负责单向消息传输,是对 Mina、Netty、Grizzly 的抽象,它也可以扩展 UDP 传输,而 Exchange 层是在传输层之上封装了 Request-Response 语义。
- 交换层(Exchange):封装请求响应模式,同步转异步,以 Request 和 Response 为中心,扩展接口为Exchanger、ExchangeChannel、ExchangeClient和ExchangeServer。
- 网络传输层(Transport):抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为Channel、Transporter、Client、Server和Codec。
- 数据序列化层(Serialize):可复用的一些工具,扩展接口为Serialization、 ObjectInput、ObjectOutput和ThreadPool。
- dubbo-common 公共逻辑模块,包括 Util 类和通用模型。
- dubbo-remoting 远程通讯模块,相当于 Dubbo 协议的实现,如果 RPC 用 RMI 协议则不需要使用此包。
- dubbo-rpc 远程调用模块,抽象各种协议,以及动态代理,只包含一对一的调用,不关心集群的管理。
- dubbo-cluster 集群模块,将多个服务提供方伪装为一个提供方,包括:负载均衡、容错、路由等,集群的地址列表可以是静态配置的,也可以是由注册中心下发。
- dubbo-registry 注册中心模块,基于注册中心下发地址的集群方式,以及对各种注册中心的抽象。
- dubbo-monitor 监控模块,统计服务调用次数,调用时间的,调用链跟踪的服务。
- dubbo-config 配置模块,是 Dubbo 对外的 API,用户通过 Config 使用 Dubbo,隐藏 Dubbo 所有细节。
- dubbo-container 容器模块,是一个 Standalone 的容器,以简单的 Main 加载 Spring 启动,因为服务通常不需要 Tomcat/JBoss 等 Web 容器的特性,没必要用 Web 容器去加载服务。
集群 - Cluster
提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。
服务目录(Directory)
服务目录中存储了一些和服务提供者有关的信息,通过服务目录,服务消费者可获取到服务提供者的信息,比如 ip、端口、服务协议等。通过这些信息,服务消费者就可通过 Netty 等客户端进行远程调用。
服务目录与注册中心之间的区别:
- 注册中心存储服务提供者信息,在 Dubbo 中通过 ZooKeeper 实现;
- 服务目录是 Invoker 的集合,且这个集合中的元素会随注册中心的变化而进行动态调整。
服务目录会在客户端启动时初始化完成,并订阅注册中心的更新:com.alibaba.dubbo.registry.support.FailbackRegistry#FailbackRegistry
com.alibaba.dubbo.registry.support.FailbackRegistry#subscribe
Directory 继承结构
Directory 接口包含了一个获取配置信息的方法 getUrl,实现该接口的类可以向外提供配置信息。Directory 有多个实现。
- StaticDirectory
获取一次 Invoker 列表后就不变了。 - RegistryDirectory
实现了 NotifyListener 接口,当注册中心服务配置发生变化后,RegistryDirectory 可收到与当前服务相关的变化,然后根据配置变更信息刷新 Invoker 列表。
刷新 Invoker 列表代码:com.alibaba.dubbo.registry.integration.RegistryDirectory#refreshInvoker
路由(Router)
服务目录中包含多个 Invoker,需要通过路由规则来选择调用哪个,Dubbo 提供了 3 种路由实现:条件路由 ConditionRouter、脚本路由 ScriptRouter 和标签路由 TagRouter。
条件路由(ConditionRouter)
容错方案
Dubbo 提供多种集群的容错方案,默认情况下为 Failover。com.alibaba.dubbo.rpc.cluster.Cluster
Failover
失败自动切换,当出现失败,重试其它服务器 (该配置为默认配置)。通常用于读操作,但重试会带来更长时间的延迟。
1 | <!--配置集群容错模式为失败自动切换 --> |
通常用于幂等操作,多次调用副作用相同,譬如只读请求,Failover 使用得较多,推荐使用,但重试会带来更长延迟,应用于消费者和提供者的服务调用。
Failfast
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录和修改数据,Failfast 使用得较多,但如果有机器正在重启,可能会出现调用失败,应用于消费者和提供者的服务调用。
Failsafe
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作,Failsafe 使用得不多,但调用信息会丢失,应用于发送统计信息到监控中心。
Failback
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作,使用得很少,不可靠,重启会丢失,应用于注册服务到注册中心。
Forking
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,使用得很少,但需要浪费更多服务资源。
Broadcast
广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通知所有提供者更新缓存或日志等本地资源信息,速度慢,任意一台报错则报错,使用得很少。
负载均衡
Random LoadBalance
随机调用(默认配置),按权重设置随机概率,在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重,使用较多,推荐使用,但重试时,可能出现瞬间压力不均。
1 | <!-- 服务端方法基本负载均衡设置 --> |
RoundRobin LoadBalance
轮循调用,按公约后的权重设置轮循比率,存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上,极端情况可能产生雪崩。
LeastActive LoadBalance
最少活跃数调用,相同活跃数的随机,活跃数指调用前后计数差,使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差(与时间有关)会越大,但不支持权重。
ConsistentHash LoadBalance
一致性 Hash,相同参数的请求总是发到同一提供者,当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。缺省只对第一个参数 Hash,如果要修改,请配置:
1 | <dubbo:parameter key="hash.arguments" value="0,1" /> |
缺省用 160 份虚拟节点,如果要修改,请配置:
1 | <dubbo:parameter key="hash.nodes" value="320" /> |
由于是通过哈希算法分摊调用,有可能出现调用不均匀的情况
远程通信 - Transport
提供对多种基于长连接的 NIO 框架抽象封装,包括多种线程模型,序列化,以及“请求-响应”模式的信息交换方式。
Dubbo 支持如下网络通信框架:
- Mina
- Netty
- Grizzly
序列化 - Serialize
反射
通过缓存加载的 Class、setAccessible(false)去掉安全校验等来提高反射效率,或者使用反射包ReflectASM。
序列化
对性能敏感,对开发体验要求不高的内部系统 thrift 或 protobuf
对开发体验敏感,性能有要求的内外部系统 hessian2
对序列化后的数据要求有良好的可读性 jackson/gson/xml
对兼容性和性能要求较高的系统 protobuf 或 kryo ,它们的性能相差不多,但是 protobuf 有个缺点就是要传输的每一个类的结构都要生成对应的 proto 文件。
Filter
ProtocolFilterWrapper#export:如果当前 protocol 不是 registry,则调用 buildInvokerChain
-> ProtocolFilterWrapper#buildInvokerChain
-> ExtensionLoader#getActivateExtension(URL url, String key, String group):获取系统自动激活的 Filter 和用户自定义的 Filter,最后合并返回
更多功能
限流
限流最好配置在 Provider 端,因为 Consumer 可能有很多个服务器实例,如果他们同时发起对同一 Provider 实例的请求可能会超出机器的处理能力上限。
1 | <!-- 限制接口OrderService里的每个方法,服务提供者端的执行线程不超过10个 --> |
上述配置限制的是线程数,即并发连接数,Consumer 和 Provider 默认通过一条共享的 TCP 长连接通信,连接成功的情况下请求线程交由 IO 线程池异步读写数据,数据被反序列化后交由业务线程池处理具体业务,也就是对应的 Impl 实现类的具体方法。
服务隔离
1 | <!--当同一个接口有多个实现时,可以通过group来隔离 --> |
通过版本号,也可以实现消费者和提供者服务端直接连接,因为发起调用默认使用随机调用端负载均衡模式,当有多台提供者的时候,会随机选取,通常联调阶段都会调用指定服务进行联调,直连一般用在调试,开发阶段,只需要消费者和提供者 version 相同即可。
灰度发布
有三台服务器 A、B、C 要上线,现在三台服务器都是旧版本代码,那首先从 Ngnix 负载均衡列表里移除 A 服务器的配置,切断对 A 的访问,然后在 A 服务器不受新的代码,重新把 A 配置进 Ngnix 负载均衡列表。如果在线使用没有问题,则继续升级 B、C 服务器,否则回滚,恢复旧版本代码,这是针对三端(PC 端,微信端,移动端)跟网关系统的。
如果是针对子系统,譬如用户系统、订单系统等,可以通过分组 group 来实现子系统的灰度发布。服务提供者有两组,One、Two,将新版本代码 group 改为 Two,旧版本 group 还是 One,将新版本的消费者 group 改为 Two,这时请求定位到新的消费者再调用新的提供者,而且旧的消费者还是请求旧的提供者,如果线上没有问题,那就把提供者 group 为 One 的组改为 Two,并部署新代码,旧的消费者也改成 Two 并部署新代码如果有问题,那消费端和提供端都回滚到旧版本。
异步调用
Dubbo 默认情况下是同步调用的,就是调用后立刻返回,但如果消费端调用服务端创建文件并转化成 PDF 格式的文件这种在 IO 密集操作时,消费端同步调用需要等待对方转换结束才返回,很消耗性能,这时选择异步调用和回调调用更合适。
1 | <!-- |
可以在 onthrow 事件里实现服务降级的方法,譬如遇到网络抖动,调用超时返回时可在 onthrow 里 return null。
- 调用方
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15@Test
public void testQueryOrder() {
// 此时调用会立即拿到null值
List<Order> list = this.orderService.queryOrderList();
// 拿到Future的引用,在提供方返回结果后,结果值会被设置进Future
Future<String> orderFuture = RpcContext.getContext().getFuture();
try {
// 该方法是阻塞方法,在拿到值之前一直等待,直到拿到值才会被唤醒,该方法会抛出异常,可以捕获
String returnValue = orderFuture.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
} - 回调方
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 回调接口
interface ICallBack {
// 第一个参数是返回值,第二个参数是原参数
public void onreturn(String returnValue, String initParameter);
// 第一个参数是异常,第二个参数是原参数
public void onthrow(Throwable ex, String initParameter);
}
// 实现类
class CallBackImpl implements ICallBack {
public void onreturn(String returnValue, String initParameter) {
// do something
};
public void onthrow(Throwable ex, String initParameter) {
// do something
};
}
调用方有一个用户线程池用于处理调用请求(比如 Tomcat 里那个线程池),请求被转发到 IO 线程池,由 IO 线程来发起对提供方的调用,此时 IO 线程会新建一个 Future 对象进 RpcContext,用户线程可以继续继续自己的业务逻辑,然后在需要的时候调用 Future 的 get 方法阻塞等待,而服务端只需要将结果返回给 IO 线程,由 IO 线程调用 notify 方法唤醒阻塞等待中的用户线程。
服务降级
服务降级用于在服务高峰期将次要服务降级,仅保留关键服务,从而降低系统负载、提升可用性。比如,订单列表正常情况下展示所有订单,但是如果是在网站开展秒杀之类的大促活动时,就可以降级展示当月的订单而不是所有,再其次,如果服务器宕机了,也最好展示兜底页而不是 504。
1 | <dubbo:service interface="com.bubbo.service.OrderService" mock="com.dubbo.service.MonthOderMock"/> |
热点缓存
1 | <!--服务调用者 --> |
如果查询的对象改变很少但又数据量很大的时候,如首页目录,可以避免每次都频繁调用服务端,可以设置本地缓存,加快热点数据的访问,Dubbo 的缓存类型 LRU 缓存,最近最少使用的数据会被清除,使用频繁的数据被保留,Thredlocal 缓存,当前线程的缓存,假如当前线程有多次请求,每次请求都需要相同的用户信息,那就适用,避免每次都去查询用户基本信息。
源码分析
环境配置比较简单,就是 zk->provider->consumer,在此不再赘述。
失败重试
Dubbo 中的失败重试机制比较丰富,基本考虑到常用的场景
http://dubbo.apache.org/zh-cn/docs/user/demos/fault-tolerent-strategy.html
FailoverClusterInvoker、FailfastClusterInvoker 等,以 FailoverClusterInvoker 为例:
FailoverClusterInvoker.doInvoke 重试几次,把失败的添加到 invoked 列表里
-> AbstractClusterInvoker.select 选一个可用的调用,如果是已经被选过或因为其他条件不可用则 reselect
负载均衡
http://dubbo.apache.org/zh-cn/docs/user/demos/loadbalance.html
幂等
Dubbo 没有提供幂等性检查功能,需要自定义。
限流
Dubbo 中的限流比较简单,采用的是计数器算法,单位时间内超出阈值的流量会被直接丢弃,而且只支持 PORVIDER 端的限流,而且为了让它生效还要搞复杂的 SPI 配置。
https://www.jianshu.com/p/7112a8d3d869
入口:TpsLimitFilter.invoke
-> TPSLimiter.isAllowable 为每个 Service 创建一个计数器 StatItem(粒度是整个 Service 有没有太大了)
降级
Dubbo 里的降级比较水,即调用出错就改成调用 Mock 接口,没有 Hystrix 中那么复杂的逻辑:
http://dubbo.apache.org/zh-cn/docs/user/demos/service-downgrade.html
https://www.cnblogs.com/java-zhao/p/8320519.html
入口:ReferenceConfig.createProxy 创建代理
-> ProxyFactory.getProxy
-> InvokerInvocationHandler.invoke
-> MockClusterInvoker.invoke 如果配置中有 fail 开头,则在远程调用失败后调用 doMockInvoke,大概逻辑是实例化一个 XxxServiceMock 服务然后调用
优雅停机
https://www.jianshu.com/p/6e4d1ecb0815
QA
说一下你们怎么用 Dubbo 的(考对 Dubbo 的应用能力)
说一下 Dubbo 的工作原理
描述 Registry、Consumer、Provider 之间的关系。
Dubbo 负载均衡策略和集群容错策略都有哪些
负载均衡策略和集群容错策略见上面的《集群》小节。
Dubbo 的动态代理策略
javassist,类似 CGLIB,通过继承目标类以生成代理类。
说一下服务注册(导出)过程
分本地暴露和远程暴露两种
说一下服务消费(引入)过程
服务的运行过程中,如果 ZooKeeper 挂掉了,这时还能正常请求吗?
说一下 Dubbo 协议
Dubbo 有几种容错机制
dubbo 有几种服务降级机制
dubbo 有几种服务降级机制
参考
启动过程
- 研究优雅停机时的一点思考
kill -9
与kill -15
的区别,SpringBoot 的停机机制。 - 一文聊透 Dubbo 优雅停机
- 一文聊透 Dubbo 优雅上线
- Spring-boot+Dubbo 应用启停源码分析
- 服务导出
- 服务引入
SPI
协议
- 【RPC 专栏】深入理解 RPC 之协议篇
- Dubbo 在跨语言和协议穿透性方向的探索:支持 HTTP/2 gRPC
- 一文详细解读 Dubbo 中的 http 协议
- 聊聊 TCP 长连接和心跳那些事
- Dubbo 中的 URL 统一模型
- 研究网卡地址注册时的一点思考
- RFC 5234 - Augmented BNF for Syntax Specifications: ABNF
- 服务端经典的 C10k 问题(译)