【第三弹】详解分布式中间件Dubbo
五、Dubbo的高级特性
这部分根据官网的介绍,知道Dubbo提供的三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。除此之外,还有高度可扩展能力(Dubbo的SPI机制),运行期流量调度(路由规则),Dubbo服务的监控和管理(Dubbo管理控制台 dubbo-admin)等,这些实现基于它的一些高级特性。
1. Dubbo的SPI机制
1.1 SPI介绍
SPI(Service Provider Interface),即服务提供者接口的意思。简单说SPI就是一种扩展机制,我们在相应配置文件中定义好某个接口的实现类,然后再根据这个接口去这个配置文件中加载这个实现类并实例化。即用户调用接口时按照SPI的机制可以调用到想要的实现类。
常见的SPI机制有:
(1)Java的SPI机制:例如JDBC驱动加载案例,即利用Java的SPI机制,可以根据不同的数据库厂商来引入不同的JDBC驱动包;
(2)SpringBoot的SPI机制:例如自动装载配置类案例,我们可以在spring.factories
中加上我们自定义的自动配置类,事件监听器或初始化器等;
(3)Dubbo的SPI机制:Dubbo基本上自身的每个功能点都提供了扩展点,从官网上可以看到,比如提供了集群扩展,路由扩展和负载均衡扩展等差不多接近30个扩展点。如果Dubbo的某个内置实现不符合我们的需求,那么我们只要利用其SPI机制将我们的实现类替换掉Dubbo的实现类即可,即通过实现同一接口的前提下,可以进行定制自己的实现类。
1.2 Dubbo中SPI机制的使用
下面使用三个项目来演示Dubbo中SPI机制的使用,一个主项目main,一个服务接口项目api,一个
服务实现项目impl。
api项目创建:
(1)导入Dubbo依赖
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.5</version>
</dependency>
(2)创建接口
在接口上使用@SPI,也可以使用@SPI(“xxx”),添加默认的实现
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;
@SPI("human")
public interface HelloService {
String sayHello();
@Adaptive
String sayHello(URL url);
}
impl项目创建:
(1)导入api项目的依赖
<dependency>
<groupId>com.lagou</groupId>
<artifactId>dubbo_spi_demo_api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
(2)建立实现类,为了表达支持多个实现的目的,这里分别创建两个实现。分别为HumanHelloService和DogHelloService
import com.lagou.service.HelloService;
import org.apache.dubbo.common.URL;
public class HumanHelloService implements HelloService{
public String sayHello() {
return "hello 你好";
}
public String sayHello(URL url) {
return "hello url";
}
}
public class DogHelloService implements HelloService{
public String sayHello() {
return "wang wang";
}
public String sayHello(URL url) {
return "wang url";
}
}
(3)SPI进行声明操作,在resources目录下创建目录META-INF/dubbo目录,在目录下创建名称为
com.lagou.dubbo.study.spi.demo.api.HelloService的文件,文件内部配置两个实现类名称和对应的全限定名:
human=com.lagou.service.impl.HumanHelloService
dog=com.lagou.service.impl.DogHelloService
main项目创建:
(1)导入接口项目依赖和实现类项目依赖
<dependency>
<groupId>com.lagou</groupId>
<artifactId>dubbo_spi_demo_api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.lagou</groupId>
<artifactId>dubbo_spi_demo_impl</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
(2)创建DubboSpiMain
测试时,Dubbo有对其进行自我重新实现,借助ExtensionLoader查询出所有的已知实现,并且调用。
import com.lagou.service.HelloService;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class DubboAdaptiveMain {
public static void main(String[] args) {
URL url = URL.valueOf("test://localhost/hello?hello.service=dog");
HelloService adaptiveExtension = ExtensionLoader.getExtensionLoader(HelloService.class).getAdaptiveExtension();
String msg = adaptiveExtension.sayHello(url);
System.out.println(msg);
}
}
1.3 Dubbo中SPI机制的Activate和Adaptive
Dubbo中的Activate功能,主要表示了一个扩展类被获取到的的条件,符合条件就被获取,不符合条件就不获取 ,@Activate注解在类型和方法上,根据 @Activate中的 group 、value属性来过滤 。
Dubbo中的Adaptive功能,主要解决的问题是如何动态的选择具体的扩展点。通过
getAdaptiveExtension统一对指定接口对应的所有扩展点进行封装,通过URL的方式对扩展点来进行
动态选择。(Dubbo中所有的注册信息都是通过URL的形式进行处理的)这里同样采用相同的方式进行
实现。具体实现见上面代码。
2. Dubbo的注册中心
2.1 Dubbo的注册中心介绍
Dubbo通过注册中心实现了分布式环境中服务的注册和发现。
Dubbo 有五种注册中心的实现,分别是Multicast,Zookeeper,Redis,Simple和Nacos 。
Dubbo的注册中心实现了如下的功能:
(1)动态注册服务。服务提供者通过注册中心,把自己暴露给消费者,无须消费者逐个更新配置文件。
(2)动态发现服务。消费者动态感知新的配置,路由规则和新的服务提供者。
(3)参数动态调整。支持参数的动态调整,新参数自动更新到所有服务节点。
(4)服务统一配置。统一连接到注册中心的服务配置。
2.2 Dubbo注册中心的工作流程
Dubbo注册中心的工作流程如下:
(1)提供者(Provider)启动时,会向注册中心写入自己的元数据信息(调用方式)。
(2)消费者(Consumer)启动时,也会在注册中心写入自己的元数据信息,并且订阅服务提供者,路由和配置元数据的信息。
(3)服务治理中心(Duubo-admin)启动时,会同时订阅所有消费者,提供者,路由和配置元数据的信息。
(4)当提供者离开或者新提供者加入时,注册中心发现变化会通知消费者和服务治理中心。
2.3 Dubbo注册中心Zookeeper
注册中心Zookeeper工作原理:
Zookeeper通过树形文件存储的数据节点ZNode在/dubbo/Service目录下面建立了四个目录,分别是:
(1)Providers目录下面,存放服务提供者URL和元数据。
(2)Consumers目录下面,存放消费者的URL和元数据。
(3)Routers目录下面,存放消费者的路由策略。
(4)Configurators目录下面,存放多个用于服务提供者动态配置URL元数据信息。
客户端第一次连接注册中心的时候,会获取全量的服务元数据,包括服务提供者和服务消费者以及路由和配置的信息。根据ZooKeeper客户端的特性,会在对应ZNode的目录上注册一个Watcher,同时让客户端和注册中心保持TCP长连接。如果服务的元数据信息发生变化,客户端会接受到变更通知,然后去注册中心更新元数据信息。变更时根据ZNode节点中版本变化进行。
3. Dubbo的容错策略
Dubbo的消费者在提供者数据的时候,它timeout=0代表永不超时,这样就很容易阻塞过多,为了防止这种服务雪崩的情况,Dubbo提供了一些容错处理策略。
Dubbo 主要提供了这样几种容错方式:
Failover,失败自动切换,失败时会重试其它服务器,可以设置重试次数。
Failfast, 快速失败,请求失败后快速返回异常结果,不进行重试。
Failsafe,失败安全,出现异常,直接忽略,会对请求做负载均衡。
Failback, 失败自动恢复, 请求失败后,会自动记录请求到失败队列中,通过定时线程扫描该队列,并定时重试。
Forking, 并行调用多个服务提供者,其中有一个返回,则立即返回结果。
Broadcast,广播调用所有可以连接的服务,任意一个服务返回错误,就任务调用失败。
Mock,响应失败时返回伪造的响应结果。
Available,通过遍历的方式查找所有服务列表,找到第一个可以返回结果的节点,并且返回结果。
Mergable,将多个节点请求合并进行返回。
策略名称 | 优点 | 缺点 | 主要应用场景 |
---|---|---|---|
Failover | 对调用者屏蔽调用失败的信息 | 增加RT,额外资源开销,资源浪费 | 对调用RT不敏感的场景 |
Failfast | 业务快速感知失败状态进行自主决策 | 产生较多报错的信息 | 调用非幂等性接口,需要快速感知失败的场景 |
Failsafe | 即使失败了也不会影响核心流程 | 对于失败的信息不敏感,需要额外的监控 | 旁路系统,失败不影响核心流程正确性的场景 |
Failback | 失败自动异步重试 | 重试任务可能堆积 | 对于实时性要求不高,且不需要返回值的一些异步操作 |
Forking | 并行发起多个调用,降低失败概率 | 消耗额外的机器资源,需要确保操作幂等性 | 资源充足,且对于失败的容忍度较低,实时性要求高的场景 |
Broadcast | 支持对所有的服务提供者进行操作 | 资源消耗很大 | 通知所有提供者更新缓存或日志等本地资源信息 |
4. Dubbo的服务降级
4.1 什么是服务降级
服务降级,当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务有策略的降低服务级别,以释放服务器资源,保证核心任务的正常运行。
4.2 为什么要服务降级
防止分布式服务发生雪崩效应,即当一个请求发生超时,一直等待着服务响应,那么在高并发情况下,很多请求都是因为这样一直等着响应,直到服务资源耗尽产生宕机,而宕机之后会导致分布式其他服务调用该宕机的服务也会出现资源耗尽宕机,这样下去将导致整个分布式服务都瘫痪,这就是雪崩。
4.3 Dubbo服务降级实现方式
(1)在Dubbo管理控制台配置服务降级(即屏蔽和容错)
mock=force:return+null 表示消费方对该服务的方法调用都直接返回null值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。
mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回null值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。
(2)指定返回简单值或者null
<dubbo:reference id="xxService" check="false" interface="com.xx.XxService"
timeout="3000" mock="return null" />
<dubbo:reference id="xxService2" check="false" interface="com.xx.XxService2"
timeout="3000" mock="return 1234" />
如果是标注,则使用@Reference(mock=“return null”) @Reference(mock=“return 简单值”),也支持 @Reference(mock=“force:return null”)
(3)使用java代码动态写入配置中心
RegistryFactory registryFactory =
ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://IP:PORT"));
registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?
category=configurators&dynamic=false&application=foo&mock=force:return+null"));
(4)整合hystrix实现服务降级,见第八部分项目实战。
Dubbo服务降级的真实含义:并不是对provider进行操作,而是告诉consumer,调用服务时要做哪些动作。
5. Dubbo的负载均衡
Dubbo中为了消费者能很好的调用到对应的服务提供者,也为了保证服务调用的高可用提供了多种负载均衡策略。
Dubbo的负载均衡策略有四种(缺省为Random随机调用):
Random, 随机策略,按照权重设置随机概率做负载均衡。
RoundRobin, 轮询策略,按照公约后的权重设置轮询比例。
LeastActive, 按照活跃数调用策略,活跃度差的被调用的次数多。活跃度相同的Invoker进行随机调用。
ConsistentHash, 一致性Hash策略,相同参数的请求总是发到同一个提供者。
6. Dubbo的过滤器
6.1 Dubbo的Filter介绍
在服务的调用过程中,在服务消费者调用服务提供者的前后,都会调用Filter(过滤器)。可以针对消费者和提供者配置对应的过滤器,由于过滤器在RPC执行过程中都会被调用,所以为了提高性能需要根据具体情况配置。Dubbo框架中有自带的系统过滤器,服务提供者有11个,服务消费者有5个。
6.2 Dubbo的Filter使用
Dubbo中过滤器的使用可以通过@Activate的注释,或者配置文件实现。
举例:配置文件实现过滤器
<!-- 消费者过滤器配置 -->
<dubbo:reference filter="filter01,filter02"/>
<!-- 消费者默认过滤器配置,拦截reference过滤器 -->
<dubbo:consumer filter="filter03,filter04"/>
<!-- 提供者过滤器配置 -->
<dubbo:service filter="filter05"/>
<!-- 提供者默认过滤器配置,拦截service过滤器 -->
<dubbo:provider filter="filter06,filter07"/>
过滤器的使用遵循以下几个规则:
(1)过滤器顺序,过滤器执行是有顺序的。例如,用户定义的过滤器的过滤顺序默认会在系统过滤器之后。又例如,上图中 filter=“filter01, filter02”,filter01 过滤器执行就在filter02之前。
(2)过滤器失效,如果针对某些服务或者方法不希望使用某些过滤器,可以通过“-”(减号)的方式使该过滤器失效。例如,filter=“-filter01”。
(3)过滤器叠加,如果服务提供者和服务消费者都配置了过滤器,那么两个过滤器会被叠加生效。
由于,每个服务都支持多个过滤器,而且过滤器之间有先后顺序。因此在设计上Dubbo采用了装饰器模式,将Invoker进行层层包装,每包装一层就加入一层过滤条件。在执行过滤器的时候就好像拆开一个一个包装一样。
7. Dubbo的协议
7.1 Dubbo 支持的通信协议
(1)dubbo协议
Dubbo默认的通信协议就是dubbo协议,单一长连接,进行的是NIO异步通信,基于hessian作为序列化协议。适用场景:传输数据量小(每次请求在100kb以内),但是并发量很高。
(2)rmi协议
支持java二进制序列化,多个短连接。适用场景:消费者和提供者数量差不多的情况,适用于文件的传输,一般较少用。
(3)hessian协议
支持hessian序列化协议,多个短连接。适用场景:提供者数量比消费者数量多的情况,适用于文件的传输,跨语言传输,一般较少用。
(4)http协议
支持json序列化协议,多个短连接,采用同步传输。适用场景:提供者数量比消费者数量多的情况,数据包混合。
(5)webservice协议
支持SOAP文本序列化协议,多个短连接,采用同步传输。适用场景:系统集成和跨语言调用。
7.2 Dubbo支持的序列化协议
Dubbo 支持hession、java二进制序列化、json序列化、SOAP文本序列化多种序列化协议。但是 hessian是其默认的序列化协议。
7.3 Dubbo协议详解
Dubbo的缺省协议,使用基于mina 1.1.7和hessian 3.2.1的tbremoting交互。
连接个数:单连接
连接方式:长连接
传输协议:TCP协议
传输方式:NIO异步传输
序列化协议:Hessian二进制序列化协议
适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用 dubbo协议传输大文件或超大字符串。
适用场景:常规远程服务方法调用
Dubbo协议采用固定长度的消息头(16字节)和不定长度的消息体来进行数据传输,消息头定义了底层框架(netty)在IO线程处理时需要的信息,协议的报文格式如下:
协议体包含了传输的主要内容,它是由16字节长的报文组成,每个字节包括8个二进制位。协议详情如下:
Magic - Magic High & Magic Low (16 bits)
协议:0xdabb
Serialization ID (5 bit)
fastjson 的值为6。
Event (1 bit)
标识是否是事件消息,例如,心跳事件。如果这是一个事件,则设置为1。
Two Way (1 bit)
Req/Res 为1(请求)时才有用,标记是否期望从服务器返回值。如果需要来自服务器
的返回值,则设置为1。
(1 bit)
1; 响应:0。
Status (8 bits)
Req/Res 为0(响应)时有用,用于标识响应的状态。
20 - OK
30 - CLIENT_TIMEOUT
31 - SERVER_TIMEOUT
40 - BAD_REQUEST
50 - BAD_RESPONSE
60 - SERVICE_NOT_FOUND
70 - SERVICE_ERROR
80 - SERVER_ERROR
90 - CLIENT_ERROR
100 - SERVER_THREADPOOL_EXHAUSTED_ERROR
Request ID (64 bits)
标识唯一请求。类型为long。
Data Length (32 bits)
序列化后的内容长度(可变部分),按字节计数。int类型。
Variable Part
ID 标识)序列化后,每个部分都是一个 byte [] 或者 byte
( Req/Res = 1),则每个部分依次为:
Dubbo version
Service name
Service version
Method name
Method parameter types
Method arguments
Attachments
0),则每个部分依次为: =
返回值类型(byte),标识从服务器端返回的值类型:
2
RESPONSE_VALUE 1
0
返回值:从服务端返回的响应bytes
注意:对于(Variable Part)变长部分,当前版本的Dubbo框架使用json序列化时,在每部分内容间额外增加了换行符作为分隔,请在Variable Part的每个part后额外增加换行符, 如:
Dubbo version bytes (换行符)
Service name bytes (换行符)
...
优点:
协议设计上很紧凑,能用1个bit表示的,不会用一个byte来表示,比如boolean类型的标识。请求、响应的header 一致,通过序列化器对content组装特定的内容,代码实现起来简单。
可以改进的点:
(1)类似于http请求,通过header就可以确定要访问的资源,而Dubbo需要涉及到用特定序列化协议才可以将服务名、方法、方法签名解析出来,并且这些资源定位符是string类型或者string数组,很容易转成bytes,因此可以组装到header中。类似于http2的header压缩,对于RPC调用的资源也可以协商出来一个int来标识,从而提升性能,如果在header上组装资源定位符的话,该功能则更易实现。
(2)通过req/res是否是请求后,可以精细定制协议,去掉一些不需要的标识和添加一些特定的标识。比如status , twoWay标识可以严格定制,去掉冗余标识。还有超时时间是作为Dubbo的attachment进行传输的,理论上应该放到请求协议的header中,因为超时是网络请求中必不可少的。提到 attachment ,通过实现可以看到attachment中有一些是跟协议content中已有的字段是重复的,比如 path和version 等字段,这些会增大协尺寸。
(3)Dubbo会将服务名com.alibaba.middleware.hsf.guide.api.param.ModifyOrderPriceParam ,转换为com/alibaba/middleware/hsf/guide/api/param/ModifyOrderPriceParam; ,理论上是不必要的,最后追加一个 ; 即可。
(4)Dubbo协议没有预留扩展字段,没法新增标识,扩展性不太好,比如新增响应上下文的功能,只有改协议版本号的方式,但是这样要求客户端和服务端的版本都进行升级,对于分布式场景很不友好。
入骨相思知不知
玲珑骰子安红豆
入我相思门,知我相思苦,长相思兮长相忆,短相思兮无穷极。