Dubbo

一、分布式基础理论

1.1什么是分布式系统

《分布式系统原理与泛型》中定义:“分布式系统是独立计算机的集合,这些计算机对于用户来说就像单个相关系统”

随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用框架已经无法应对,分布式服务架构以及流动计算机架构势在必行,亟需==一个治理系统==确保架构有条不紊的演进。

1.2发展演变

image

  • All In One 单一应用架构:当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架 (ORM) 是关键。

  • Vertical Appliation 垂直应用架构:当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,提升效率的方法之一是将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的 Web 框架 (MVC) 是关键。

  • Distributed Server 分布式服务架构:当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架 (RPC) 是关键。

  • Elastic Computing 流动计算架构:当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。

1.3RPC

  • RPC(Remote Procedure Call)远程过程调用,简单的理解是一个节点请求另一个节点提供的服务

  • 本地过程调用:如果需要将本地 student 对象的 age+1,可以实现一个 addAge() 方法,将 student 对象传入,对年龄进行更新之后返回即可,本地方法调用的函数体通过函数指针来指定。

  • 远程过程调用:上述操作的过程中,如果 addAge() 这个方法在服务端,执行函数的函数体在远程机器上,如何告诉机器需要调用这个方法呢?

RPC调用过程

image-20200705101927036

  1. 客户端请求调用服务
  2. 序列化请求及参数
  3. 网络传输
  4. 服务器反序列化
  5. 服务器接收请求
  6. 服务器返回响应
  7. 序列化返回响应
  8. 网络传输
  9. 客户端反序列化返回响应
  10. 客户端获取返回响应

==RPC 的核心模块:网络通信模块和序列化模块,决定了ROC框架的效率==

常见的ROC框架

Dubbo、gRPC、Thrift、HSF

##二、Dubbo 核心概念

2.1 Dubbo 的特性

  • 面向接口代理的高性能RPC调用
  • 智能负载均衡
  • 服务自动注册与发现
  • 高度可扩展能力
  • 运行期流量调度
  • 可视化的服务治理与运维

2.2 Dubbo 架构

dubbo-architucture

2.2.1 节点角色说明

节点 角色说明
Provider 暴露服务的服务提供方
Consumer 调用远程服务的服务消费方
Registry 服务注册与发现的注册中心
Monitor 统计服务的调用次数和调用时间的监控中心
Container 服务运行容器

2.2.2 调用关系说明

  1. 服务容器负责启动,加载,运行服务提供者。

  2. 服务提供者在启动时,向注册中心注册自己提供的服务。

  3. 服务消费者在启动时,向注册中心订阅自己所需的服务。

  4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

  5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

  6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

三、 快速开始 Dubbo

3.1 注册中心

支持的注册中心

Multicast

==Zookeeper(推荐)==

Nacos

Redis

Simple

3.2 安装Zookeeper

3.3 集成SpringBoot

创建 common 接口工程、provider 工程和 consumer 工程,添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-spring-boot-starter -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes
2.7.7的dubbo-springboot-starter包中没有zk客户端包了,需要另外导入-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.1.0</version>
</dependency>
<!--这是需要暴露服务的接口jar包,独立写一个工程打成jar包,方便 provider 和 consumer 引用-->
<dependency>
<groupId>com.dubbo.commonservice</groupId>
<artifactId>commondemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<systemPath>${project.basedir}/src/main/resources/lib/commondemo-0.0.1.jar</systemPath>
</dependency>

编写测试用服务接口

1
2
3
4
5
package com.dubbo.provider.service;

public interface TestProviderService {
String hello();
}

Provider 配置文件及 暴露服务实现

1
2
3
4
5
6
7
8
9
10
11
12
dubbo:
application:
name: providerdemo

registry:
address: localhost:2181
protocol: zookeeper

protocol:
name: dubbo
port: 20880

1
2
3
4
5
6
7
8
@Service
@DubboService//暴露服务
public class TestProviderServiceImpl implements TestProviderService {
@Override
public String hello() {
return "hello1";
}
}

Consumer 订阅服务中心配置及请求服务

1
2
3
4
5
6
7
8
9
10
11
12
13
dubbo:
application:
name: consumerdemo

registry:
#注册中心地址及端口
address: localhost:2181
# 注册中心协议
protocol: zookeeper

protocol:
name: dubbo
port: 20880
1
2
3
4
5
6
7
8
9
10
@Service
public class TestConsumerServiceImpl {

@DubboReference//订阅服务
TestProviderService testProviderService;

public String getHello(){
return testProviderService.hello();
}
}

在 SpringBootApplication 中添加 @EnableDubbo 注解 ,启动工程

四、Dubbo常见配置及问题

4.1 启动时检查

Dubbo 缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止 Spring 初始化完成,以便上线时,能及早发现问题,默认 check="true"

可以通过 check="false" 关闭检查,比如,测试时,有些服务不关心,或者出现了循环依赖,必须有一方先启动。

另外,如果你的 Spring 容器是懒加载的,或者通过 API 编程延迟引用服务,请关闭 check,否则服务临时不可用时,会抛出异常,拿到 null 引用,如果 check="false",总是会返回引用,当服务恢复时,能自动连上。

4.2 集群容错

  • Failfast Cluster

快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。

  • Failsafe Cluster

失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。

  • Failback Cluster

失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。

  • Forking Cluster

并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。

  • Broadcast Cluster

广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。

4.3 负载均衡策略

  • Random LoadBalance

    • 随机,按权重设置随机概率。
    • 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
  • RoundRobin LoadBalance

    • 轮询,按公约后的权重设置轮询比率。
    • 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
  • LeastActive LoadBalance

    • 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
    • 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
  • ConsistentHash LoadBalance

    • 一致性 Hash,相同参数的请求总是发到同一提供者。
    • 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
    • 算法参见:http://en.wikipedia.org/wiki/Consistent_hashing
    • 缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key="hash.arguments" value="0,1" />
    • 缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key="hash.nodes" value="320" />

4.4 服务直连

在开发及测试环境下,经常需要绕过注册中心,只测试指定服务提供者,这时候可能需要点对点直连,点对点直连方式,将以服务接口为单位,忽略注册中心的提供者列表,A 接口配置点对点,不影响 B 接口从注册中心获取列表。

/user-guide/images/dubbo-directly.jpg

4.5 不同粒度的配置文件覆盖关系

以 timeout 为例,下图显示了配置的查找顺序,其它 retries, loadbalance, actives 等类似:

  • 方法级优先,接口级次之,全局配置再次之。
  • 如果级别一样,则消费方优先,提供方次之。

其中,服务提供方配置,通过 URL 经由注册中心传递给消费方。

dubbo-config-override

(建议由服务提供方设置超时,因为一个方法需要执行多长时间,服务提供方更清楚,如果一个消费方同时引用多个服务,就不需要关心每个服务的超时设置)。

理论上 ReferenceConfig 中除了interface这一项,其他所有配置项都可以缺省不配置,框架会自动使用ConsumerConfig,ServiceConfig, ProviderConfig等提供的缺省配置。

4.6 Dubbo 支持多版本、多注册中心、多协议暴露服务

  • 支持的协议

    • dubbo
    • rmi
    • hessian
    • http
    • webservice
    • thrift
    • Memcached
    • redis
    • rest
    • grpc

    重点:dubbo协议、长连接、hessian序列化、NIO异步通信

    • dubbo协议(默认):单一长连接,基于hessian序列化。NIO异步通信。适合传输数据量小(100kb以内),高并发的场景
    • rmi协议:基于java二进制序列化,适用于文件传输,使用较少。
    • hessian协议:基于hessian序列化,适用于provider比consumer还多的场景,适用于文件传输,较少使用。
    • http协议:基于json序列化。
    • webservice协议:基于soap文本序列化。

详见:DUBBO用户文档

4.7Dubbo 的服务降级

在微服务开发中,可能会遇到多种问题:

  1. 多个服务之间可能由于服务没有启动或者网络不通,调用中会出现远程调用失败
  2. 服务请求过大,需要停止部分服务以保证核心业务的正常运行

以上两个问题可以使用Dubbo的服务降级来实现;
即:在服务宕掉或者并发数太高导致的RpcException异常时,进行友好的处理或者提示,而不是内部报错导致系统不可用。

解决方法

dubbo提供了mock配置,可以很好的实现dubbo服务降级。mock主要有两种配置方式:

第1种
在远程调用异常时,服务端直接返回一个固定的字符串(也就是写死的字符串)
具体配置:
在服务调用方配置mock参数:

1
<dubbo:reference id="xxxService" interface="com.x..service.xxxxService" check="false" mock="return 123456..." />

说明:配置了mock参数之后,假设在调用服务的时候,远程服务没有启动,或者各种网络异常了,那远程服务会把这个mock配置的值返回,也就是会返回123456…
通过这种方式就可以避免了因为服务调用不了而出现异常错误而带来的程序不可用(起码是有值返回的,然后可以根据值进行业务逻辑处理判断等等)。
注:除了配置mock参数之外,其它地方不用变。

第2种
在远程调用异常时,服务端根据自定义mock业务处理类进行返回)
具体配置:
在服务调用方配置mock参数:

1
<dubbo:reference id="xxxService" interface="com.x..service.xxxxService" check="false" mock="true" />

说明:配置了mock参数之后,假设在调用服务的时候,远程服务没有启动,或者各种网络异常了,那远程服务会去寻找自定义的mock业务处理类进行业务处理。
因此还需配置一个自定义mock业务处理类
在接口服务xxxxService的目录下创建相应的mock业务处理类,同时实现业务接口xxxxService(),接口名要注意命名规范:接口名+Mock后缀,mock实现需要保证有无参的构造方法。

1
2
3
4
5
6
public class xxxxServiceMock implements xxxxService {
@Override
public String getXXXX(int id) {
return "this is exception 自定义....";
}
}

配置完成后,此时如果调用失败会调用自定义的Mock业务实现。
注:除了以上两处之外,其它地方不用变。

配置完成后,此时如果调用失败会调用自定义的Mock业务实现。
注:除了以上两处之外,其它地方不用变。

五、常见面试问题

5.1 dubbo 工作原理是什么?

重点:服务注册、注册中心、消费者、代理通信、负载均衡

/dev-guide/images/dubbo-framework.jpg

  • Dubbo框架设计一共划分了10个层,而最上面的Service层是留给实际想要使用Dubbo开发分布式服务的开发者实现业务逻辑的接口层。图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口, 位于中轴线上的为双方都用到的接口。
    下面,结合Dubbo官方文档,我们分别理解一下框架分层架构中,各个层次的设计要点:
    1. 服务接口层(Service):该层是与实际业务逻辑相关的,根据服务提供方和服务消费方的业务设计对应的接口和实现。
    2. 配置层(Config):对外配置接口,以ServiceConfig和ReferenceConfig为中心,可以直接new配置类,也可以通过spring解析配置生成配置类。
    3. 服务代理层(Proxy):服务接口透明代理,生成服务的客户端Stub和服务器端Skeleton,以ServiceProxy为中心,扩展接口为ProxyFactory。
    4. 服务注册层(Registry):封装服务地址的注册与发现,以服务URL为中心,扩展接口为RegistryFactory、Registry和RegistryService。可能没有服务注册中心,此时服务提供方直接暴露服务。
    5. 集群层(Cluster):封装多个提供者的路由及负载均衡,并桥接注册中心,以Invoker为中心,扩展接口为Cluster、Directory、Router和LoadBalance。将多个服务提供方组合为一个服务提供方,实现对服务消费方来透明,只需要与一个服务提供方进行交互。
    6. 监控层(Monitor):RPC调用次数和调用时间监控,以Statistics为中心,扩展接口为MonitorFactory、Monitor和MonitorService。
    7. 远程调用层(Protocol):封将RPC调用,以Invocation和Result为中心,扩展接口为Protocol、Invoker和Exporter。Protocol是服务域,它是Invoker暴露和引用的主功能入口,它负责Invoker的生命周期管理。Invoker是实体域,它是Dubbo的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起invoke调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
    8. 信息交换层(Exchange):封装请求响应模式,同步转异步,以Request和Response为中心,扩展接口为Exchanger、ExchangeChannel、ExchangeClient和ExchangeServer。
    9. 网络传输层(Transport):抽象mina和netty为统一接口,以Message为中心,扩展接口为Channel、Transporter、Client、Server和Codec。
    10. 数据序列化层(Serialize):可复用的一些工具,扩展接口为Serialization、 ObjectInput、ObjectOutput和ThreadPool。

服务暴露的过程

  1. 解析配置文件
  2. 在providerBean和consumerBean 初始化的时候吧配置信息set到Bean中
  3. 服务暴露
    1. 获取注册中心地址和协议
    2. proxyFactory获取Invoker执行器
    3. 包装并暴露执行器(注册中心协议(dubbo)和 传输协议(zk)Protocol 的export方法) ==SPI==
    4. 创建启动netty服务器监听provider端口,获取对应协议的(dubbo)invoker
    5. 保存(本地的服务订阅注册表)providerInvoker 信息

img

服务引用的过程

  1. 调用 FactoryBean(ReferenceBean)的get方法获取@DubboReference注解的对象
  2. 根据配置信息创建 需要的reference对象的代理对象 (注册中心协议(dubbo)和 传输协议(zk)Protocol 的refer方法)==SPI==
  3. 获取注册中心信息,并订阅对应服务
  4. 创建netty客户端并监听端口,获取 对应协议的(dubbo)invoker
  5. 保存(本地的服务订阅注册表)的consumerinvoker

img

方法调用过程

  1. @DubboReference 注入的Bean是一个proxyBean,执行.invoke方法
  2. 把需要执行的方法和参数封装成一个Rpcinvocation对象
  3. 进入Filter执行 local(Stub)/mock/cache 操作(本地存根、本地伪装(服务降级)、结果缓存)
  4. 进入 Cluster 执行集群容错相关操作(容错策略和重试)
  5. 从注册中心获取相关invoker List(多版本和多集群)
  6. 进入 LoadBalance 执行负载均衡策略 ,根据策略选择一个 invoker
  7. 进入监控统计信息 Filter 记录监控信息
  8. 进入协议 invoker ,根据对应协议使用netty客户端发送请求
  9. 客户端获取返回结果,封装原路返回

/dev-guide/images/dubbo-extension.jpg

  1. 第一步》provider启动并向注册中心注册服务

  2. 第二步》consumer去注册中心订阅服务

  3. 第三步》consumer调用provider

  4. 第四步》consumer和provider异步通知监控中心monitor

当consumer第一次访问注册中心服务后,会在本地生成缓存,注册中心挂掉了还是可以访问服务的。

  1. 业务实现属于service层,配置文件属于config层,请求发起后service层调用proxy层生成动态代理类

  2. proxy层动态代理类调用cluster集群层做负载均衡,确定服务地址,被monitor监控层监控。

  3. 调用protocol协议层组织请求、exchange信息交换层封装请求、serialize序列化层 、transport网络通信层(netty)

    ==netty–>NIO–>selector–>servicesocketchannel监听端口–>==

5.2 什么是SPI?怎样实现?

  • SPI(service provider interface) 扩展接口的实现类,比如jdk中只提供了jdbc的接口,需要自己去导入具体实现类jar包。
  • 实现方式:
    • JAVA SPI :META-INF/services 路径下创建一个文件名称为 要扩展的接口 的全限定名 。文件内容为实现类的全限定的类名
    • DUBBO SPI:META-INF/dubbo 路径下创建一个文件名称为 要扩展的接口 的全限定名。与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置。另外,在测试 Dubbo SPI 时,需要在 接口上标注 @SPI 注解。

5.3 基于dubbo如何进行服务治理、服务降级和重试?

  • 服务治理:
    1. 生成调用链路图
    2. 服务访问压力和时长统计(调用次数和访问延时TP50 TP90 TP99)
  • 服务降级:
    1. 在服务调用方配置mock参数:
      <dubbo:reference id="xxxService" interface="com.x..service.xxxxService" check="false" mock=“123456" /> 降级则返回“123456”
    2. 以上配置mock=“true”的时候,需要自定义mock处理类。在接口服务xxxxService的目录下创建相应的mock业务处理类,同时实现业务接口xxxxService(),接口名要注意命名规范:接口名+Mock后缀,mock实现需要保证有无参的构造方法。
  • 重试:

retries,timeout,在配置文件中添加retries(重试次数)和timeout(最长超时时间)参数。

5.4 分布式系统如何保证系统的幂等性?

  1. 在jvm或者缓存中做请求唯一记录,并做TTL(过期时间)和持久化(防止宕机主从切换时发生数据丢失)
  2. 记录请求记录状态信息(订单详情表)并设置唯一键。在接受到请求后先添加请求记录成功后再处理请求。
  3. 在请求记录中加入请求状态(比如订单处理状态),在处理订单钱先查询该订单状态。(加锁)

5.5 如何保证分布式系统接口调用的顺序性?

  1. 在调用分布式接口服务之前,加入一套接口接入服务,(比如一致性hash算法)保证同一个请求调用的接口转发到一台机器上。
  2. 如果分布式接口的服务是多线程的,可以用内存队列来保证同一个请求对数据库的操作在同一个队列中供线程获取。
  3. 如果特别要保证100%的顺序性,使用分布式锁。(需要请求标识和顺序标识)

如何设计一个类似dubbo的rpc框架?

  • 基于zk做一个注册中心=====》
  • 消费者需要去注册中心拿请求====》
  • 面向接口的动态代理获取对应部署的机器地址===》
  • 负载均衡算法 ===》
  • 发送请求 nio方式 hessian协议 ===》
  • 服务器动态代理监听某端口,代理本具体实现。

5.6 dubbo的负载均衡策略、集群容错策略、动态代理策略

  1. 负载均衡策略:
    • random loadbalance(默认) 根据配置权重随机分配
    • roundrobin loadbalace 轮循
    • leastactive loadbalance 自动感知不活跃的机器(性能差的)使其更少地分配到
    • consiistanthash loadbalance 一致性hash算法 使一类请求分布到一台机器上
  2. 集群容错策略:
    • failover cluster模式 (默认) 失败自动切换,常见于读操作
    • failfast cluster模式 一次失败立即报错,常见于写操作。
    • failsafe cluster模式 出现异常时忽略,常用语不重要的接口调用 比如日志
    • failback cluster模式 失败后自动记录请求,定时重发,适用于消息队列
    • forking cluster模式 并行调用多台机器,只要有一个返回就立即响应。
    • broadcast cluster模式 逐个调用所有机器
  3. 动态代理策略:

默认使用javassist动态字节码生成,创建代理类。也可以通过spi扩展机制配置自己的动态代理策略。

参考链接