Dubbo

Dubbo基本概念和使用

基础

  • 架构演变:

    image-20230503131208169

    • 网站流量很小时,只需一个应用,将所有功能都部署在一起,因此使用数据访问框架(ORM)

    • 访问量逐渐增大,需要将应用拆成互不相干的几个应用(垂直应用框架,加速前端页面开发)(MVC)——切分业务,实现各个模块独立部署

      • 每个独⽴部署的服务之间,公共的部分需要部署多份。对公共部分的修改、部署、更新都需要重复的操作,带来⽐较⼤的成本
      image-20230503131352008
    • 垂直应用越来越多,应用之间交互不可避免。抽取核心业务作为独立的服务,形成稳定的服务中心,以需要提高业务复用(分布式服务框架)

    • 服务越来越多,需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率(SOA,Service Oriented Architecture)

      image-20230503131608135
  • RPC:远程过程调用

    • 一种技术的思想,允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的函数,而不用显式编码远程调用的细节——无论调用本地的还是远程的函数,本质上编写的调用代码基本相同

    • 核心模块:通讯,序列化

      image-20230503131727071

      image-20230503131811344

Dubbo

  • 高性能、轻量级的开源Java RPC框架

    • 面向接口的远程方法调用
    • 容错和负载均衡
    • 服务自动注册和发现
  • 服务消费者去注册中心订阅到服务提供者的信息。然后通过dubbo进行远程调⽤

  • 基本概念:

    • 服务提供者(Provider):暴露服务的服务提供方
      • 启动时,向注册中心注册自己提供的服务
    • 服务消费者(Consumer):调用远程服务的消费方
      • 启动时,向注册中心订阅自己所需的服务
      • 从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用
    • 注册中心(Registry):
      • 返回服务提供者地址列表给消费者
      • 如果有变更,将基于长连接推送变更数据给消费者
    • 监控中心(Monitor):Provider和Consumer在内存中累计调用次数和调用时间,每分钟发送一次统计数据到监控中心
    • 服务容器负责启动,加载,运行Provider

    image-20230503131929968

  • 环境:

    • 安装zookeeper(作为注册中心)
    • 安装dubbo-admin
      • dubbo是一个jar包,能够使java程序连接到zookeeper,利用zookeeper消费、提供服务
      • dubbo-admin是一个可视化的监控服务
  • 其他注册中心包括:

    • nacos:nacos既可以作为注册中心使⽤,也可以作为分布式配置中心使用
    • eureka
    • redis
  • 注意:

    • 每个服务方法应代表一个功能,而不是某功能的一个步骤,否则将面临分布式事务问题,Dubbo 暂未提供分布式事务支持
    • 服务接口建议以业务场景为单位划分

Demo

  • 订单服务需要调用用户服务获取某个用户的所有地址

    • 订单服务:创建订单,位于A服务器
    • 用户服务:查询用户地址,位于B服务器
  • 公共接口层/模块(存放model,service,exception)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 项目名:pub-interface
    // Bean模型
    public class UserAddress implements Serializable{
    private Integer id;
    private String userAddress;
    private String userId;
    private String consignee;
    private String phoneNum;
    private String isDefault;
    }

    // UserService接口
    UserService
    public List<UserAddress> getUserAddressList(String userId)
  • User模块:

    • 引入公共接口依赖

      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
      <dependencies>
      <dependency>
      <groupId>xxxxx</groupId>
      <artifactId>pub-interface</artifactId>
      <version>0.0.1-SNAPSHOT</version>
      </dependency>
      <!-- 引入dubbo -->
      <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>dubbo</artifactId>
      <version>2.6.2</version>
      </dependency>
      <!-- 由于我们使用zookeeper作为注册中心,所以需要操作zookeeper
      dubbo 2.6以前的版本引入zkclient操作zookeeper
      dubbo 2.6及以后的版本引入curator操作zookeeper
      下面两个zk客户端根据dubbo版本2选1即可
      -->
      <dependency>
      <groupId>com.101tec</groupId>
      <artifactId>zkclient</artifactId>
      <version>0.10</version>
      </dependency>
      <!-- curator-framework -->
      <dependency>
      <groupId>org.apache.curator</groupId>
      <artifactId>curator-framework</artifactId>
      <version>2.12.0</version>
      </dependency>

      </dependencies>
    • 配置(将dubbo和spring ioc整合)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      <!--当前应用的名字  -->
      <dubbo:application name="user"></dubbo:application>
      <!--指定注册中心的地址 -->
      <dubbo:registry address="zookeeper://118.24.44.169:2181" />
      <!--配置当前这个服务在dubbo容器中的端⼝号,每个dubbo容器内部的服务的端⼝号必须是不⼀样的-->
      <dubbo:protocol name="dubbo" port="20880" />
      <!-- 指定需要暴露的服务,指明该服务具体的实现bean是userServiceImpl-->
      <dubbo:service interface="xxxxx.service.UserService" ref="userServiceImpl" />
      <!--将服务提供者的bean注⼊到ioc容器中-->
      <bean id="userServiceImpl" class="xxxxx.service.impl.SiteServiceImpl"/>
    • 实现UserService接口

      1
      2
      3
      4
      5
      6
      7
      8
      public class UserServiceImpl implements UserService {

      @Override
      public List<UserAddress> getUserAddressList(String userId) {
      // TODO Auto-generated method stub
      return userAddressDao.getUserAddressById(userId);
      }
      }
    • 启动服务,关联bean配置文件

      1
      2
      3
      4
      5
      6
      public static void main(String[] args) throws IOException {
      ClassPathXmlApplicationContext context =
      new ClassPathXmlApplicationContext("classpath:spring-beans.xml");

      System.in.read(); // 让当前服务⼀直在线,不会被关闭,按任意键退出
      }
  • Order模块(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
      <dependencies>
      <dependency>
      <groupId>xxxxx</groupId>
      <artifactId>pub-interface</artifactId>
      <version>0.0.1-SNAPSHOT</version>
      </dependency>
      <!-- 引入dubbo -->
      <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>dubbo</artifactId>
      <version>2.6.2</version>
      </dependency>
      <!-- 由于我们使用zookeeper作为注册中心,所以需要引入zkclient和curator操作zookeeper -->
      <dependency>
      <groupId>com.101tec</groupId>
      <artifactId>zkclient</artifactId>
      <version>0.10</version>
      </dependency>
      <!-- curator-framework -->
      <dependency>
      <groupId>org.apache.curator</groupId>
      <artifactId>curator-framework</artifactId>
      <version>2.12.0</version>
      </dependency>
      </dependencies>
    • 配置(将dubbo和spring ioc整合)

      1
      2
      3
      4
      5
      6
      <!-- 应用名 -->
      <dubbo:application name="order"></dubbo:application>
      <!-- 指定注册中心地址 -->
      <dubbo:registry address="zookeeper://118.24.44.169:2181" />
      <!-- 生成远程服务代理,可以和本地bean一样使用demoService -->
      <dubbo:reference id="userService" interface="xxxxx.service.UserService"></dubbo:reference>
    • 服务:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      public class OrderService {

      UserService userService;

      /**
      * 初始化订单,查询用户的所有地址并返回
      * @param userId
      * @return
      */
      public List<UserAddress> initOrder(String userId){
      return userService.getUserAddressList(userId);
      }
      }

Demo(注解)

  • Provider:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import com.alibaba.dubbo.config.annotation.Service;
    import xxxxx.bean.UserAddress;
    import xxxxx.service.UserService;
    import xxxxx.user.mapper.UserAddressMapper;

    @Service //使用dubbo提供的service注解,注册暴露服务
    public class UserServiceImpl implements UserService {

    @Autowired
    UserAddressMapper userAddressMapper;
  • Consumer:

    1
    2
    3
    4
    5
    @Controller
    public class OrderController {

    @Reference //使用dubbo提供的reference注解引用远程服务
    UserService userService;

Demo(SpringBoot)

  • 引入依赖:

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.alibaba.boot</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>0.2.0</version>
    </dependency>
  • 配置application.properties:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #提供者配置:
    dubbo.application.name=user
    dubbo.registry.protocol=zookeeper
    dubbo.registry.address=192.168.67.159:2181
    dubbo.scan.base-package=xxxxx
    dubbo.protocol.name=dubbo
    # application.name就是服务名,不能跟别的dubbo提供端重复
    # registry.protocol 是指定注册中心协议
    # registry.address 是注册中心的地址加端口号
    # protocol.name 是分布式固定是dubbo,不要改。
    # base-package 注解方式要扫描的包
    #消费者配置:
    dubbo.application.name=order
    dubbo.registry.protocol=zookeeper
    dubbo.registry.address=192.168.67.159:2181
    dubbo.scan.base-package=com.atguigu.gmall
    dubbo.protocol.name=dubbo
  • dubbo注解:@Service、@Reference(如果没有在配置中写dubbo.scan.base-package,需要在启动类上使用@EnableDubbo注解)

Dubbo配置

  • 配置的优先级:JVM 启动 -D 参数优先,XML 次之,Properties 最后

    image-20230503162146297
  • 重试:请求失败时自动切换另一个provider地址。通过 retries=”2” 来设置重试次数(不含第一次)

    1
    2
    3
    4
    5
    6
    7
    <dubbo:service retries="2" />

    <dubbo:reference retries="2" />

    <dubbo:reference>
    <dubbo:method name="findFoo" retries="2" />
    </dubbo:reference>
  • 超时时间设置:

    • Consumer:从发起服务调⽤到收到服务响应的整个过程的时间

      1
      2
      3
      4
      5
      6
      7
      全局超时配置
      <dubbo:consumer timeout="5000" />

      指定接口以及特定方法超时配置
      <dubbo:reference interface="com.foo.BarService" timeout="2000">
      <dubbo:method name="sayHello" timeout="3000" />
      </dubbo:reference>
      1
      @Reference(version = "timeout", timeout = 4000)
    • Provider:执⾏该服务的超时时间

      1
      2
      3
      4
      5
      6
      7
      全局超时配置
      <dubbo:provider timeout="5000" />

      指定接口以及特定方法超时配置
      <dubbo:provider interface="com.foo.BarService" timeout="2000">
      <dubbo:method name="sayHello" timeout="3000" />
      </dubbo:provider>
      1
      @Service(version = "timeout", timeout = 3000)
    • 推荐在Provider上尽量多配置Consumer端属性

      • Provider比Consumer更清楚服务性能参数,如调用的超时时间,合理的重试次数
      • Provider配置后,Consumer不配置则会使用Provider的配置值,即Provider配置可以作为Consumer的缺省值
  • 版本号:一个接口的实现出现不兼容升级时使用。此时用版本号过渡,版本号不同的服务相互间不引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    老版本服务提供者配置:
    <dubbo:service interface="com.foo.BarService" version="1.0.0" />

    新版本服务提供者配置:
    <dubbo:service interface="com.foo.BarService" version="2.0.0" />

    老版本服务消费者配置:
    <dubbo:reference id="barService" interface="com.foo.BarService" version="1.0.0" />

    新版本服务消费者配置:
    <dubbo:reference id="barService" interface="com.foo.BarService" version="2.0.0" />

    如果不需要区分版本,可以按照以下的方式配置:
    <dubbo:reference id="barService" interface="com.foo.BarService" version="*" />
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Service(version = "default")
    public class DefaultSiteServiceImpl implements SiteService {
    @Override
    public String siteName(String name) {
    return "default:"+name;
    }
    }
    @Service(version = "async")
    public class AsyncSiteServiceImpl implements SiteService {
    @Override
    public String siteName(String name) {
    return "async:" + name;
    }
    }

    @Reference(id = "siteService",version = "async")
    private SiteService siteService;

高可用

  • 宕机处理:

    • 监控中心宕掉不影响使用,只是丢失部分采样数据
    • 数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
    • 注册中心的任意一台宕掉后,将自动切换到另一台
    • 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
    • 服务提供者无状态,任意一台宕掉后,不影响使用
    • 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
  • 负载均衡:缺省为 random 随机调用

    1
    2
    @Service(version = "default",loadbalance = "roundrobin")
    @Reference(version = "default", loadbalance = "roundrobin")
    • Random LoadBalance

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

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

      • 最少活跃调用数,相同活跃数的随机,活跃数为调用前后计数差
      • 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大
      • 执行过程:
        • Consumer在本地缓存所有Provider
        • Consumer在调⽤某⼀个服务时,会选择本地的所有Provider中,属性active值最小的Provider
        • 选定该Provider,对其active属性+1
        • 开始调用该服务
        • 完成调用后,对该Provider的active属性-1
    • ConsistentHash LoadBalance

      • 一致性 Hash,相同参数的请求总是发到同一Provider

      • 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动

  • 服务熔断和降级

    • 服务降级:服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心交易正常运作或高效运作

    • 向注册中心写入动态配置覆盖规则

      1
      2
      3
      RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
      Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181"));
      registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null"));
      • mock=force:return+null:Consumer对该服务的方法调用都直接返回 null ,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响
      • mock=fail:return+null:Consumer对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响
    • 集群容错:缺省时为failover

      • Failover Cluster:当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟

        • 会出现幂等性问题
        • Provider在业务层⾯解决幂等性问题
          • 把数据的业务id作为数据库的联合主键,业务id不能重复
          • 使⽤分布式锁来解决重复消费问题
      • Failfast Cluster:快速失败。只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录

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

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

      • Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源

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

      • 配置:

        1
        2
        3
        <dubbo:service cluster="failsafe" />

        <dubbo:reference cluster="failsafe" />
  • 本地存根stub’:Provider可能需要在Consumer端执行部分逻辑,如:做 ThreadLocal 缓存,提前验证参数,调用失败后伪造容错数据

    • 核心:API 中带上 Stub,客户端生成 Proxy 实例,会把 Proxy 通过构造函数传给 Stub ,然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy

    • Stub使用代理模式包装原有的远程调用服务,让使用者在远程服务调用前后做一些通用处理

    • 本地存根执行顺序:

      • Consumer发起调用
      • 如果Consumer存在本地存根 Stub,会先执行本地存根
      • 本地存根 Stub 持有远程服务的 Proxy 对象。Stub 执行的时候,先执行自己的逻辑(before),然后通过Proxy 发起远程调用,最后在返回之前也执行自己的逻辑(after-returning)
![image-20230503174609719](Dubbo-1/image-20230503174609719.png)
  • 配置:如果默认将stub属性设置为true,则必须保证本地存根实现类以Stub命名结尾

  • Provider:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class SiteServiceStub implements SiteService {
    private final SiteService siteService;
    public SiteServiceStub(SiteService siteService) {
    this.siteService = siteService;
    }
    @Override
    public String siteName(String name) {
    try {
    return siteService.siteName(name);
    } catch (Exception e) {
    return "stub:"+name;
    }
    }
    }
  • Consumer:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @EnableAutoConfiguration
    public class StubDubboConsumer {
    @Reference(version = "timeout", timeout = 1000, stub = "true")
    private SiteService siteService;
    public static void main(String[] args) {
    ConfigurableApplicationContext context = SpringApplication.run(StubDubboConsumer.class);
    SiteService siteService = (SiteService) context.getBean(SiteService.class);
    String name = siteService.siteName("q-face");
    System.out.println(name);
    }
    }
  • 参数回调:Dubbo基于长连接生成反向代理,在服务端执行客户端的逻辑(消费者调用方法)

    • 接口层

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      public interface SiteService {
      //同步调用方法
      String siteName(String name);
      //回调方法
      //default关键字:接口可以有实现方法,而且不需要实现类去实现其方法
      default String siteName(String name, String key, SiteServiceListener siteServiceListener){
      return null;
      }
      }

      // 回调接口和实现类
      public interface SiteServiceListener {
      void changed(String data);
      }
      public class SiteServiceListenerImpl implements SiteServiceListener, Serializable {
      @Override
      public void changed(String data) {
      System.out.println("changed:" + data);
      }
      }
    • Provider

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @Service(version = "callback", methods = {@Method(name = "siteName", arguments = {@Argument(index = 2, callback = true)})}, callbacks = 3)
      public class CallbackSiteServiceImpl implements SiteService {
      @Override
      public String siteName(String name) {
      return null;
      }
      @Override
      public String siteName(String name, String key, SiteServiceListener siteServiceListener) {
      siteServiceListener.changed("provider data");
      return "callback:"+name;
      }
      }
    • Consumer

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @EnableAutoConfiguration
      public class CallbackDubboConsumer {
      @Reference(version = "callback")
      private SiteService siteService;
      public static void main(String[] args) {
      ConfigurableApplicationContext context = SpringApplication.run(CallbackDubboConsumer.class);
      SiteService siteService = (SiteService) context.getBean(SiteService.class);
      // key 目的是指明实现类在Provider和Consumer之间保证是同一个
      System.out.println(siteService.siteName("q-face", "c1", new SiteServiceListenerImpl()));
      System.out.println(siteService.siteName("q-face", "c2", new SiteServiceListenerImpl()));
      System.out.println(siteService.siteName("q-face", "c3", new SiteServiceListenerImpl()));
      }
      }
  • 异步调用:

    image-20230503191240939

    • 从 2.7.0 开始,Dubbo 的所有异步编程接口开始以 CompletableFuture 为基础

    • 基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销小

    • Consumer通过异步调用,不用等待Provider返回结果就⽴即完成任务,待有结果后再执行之前设定好的监听逻辑

    • 接口层

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public interface SiteService {
      //同步调用方法

      //回调方法

      //异步调用方法
      default CompletableFuture<String> siteNameAsync(String name){
      return null;
      }
      }
    • Provider

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      @Service(version = "async")
      public class AsyncSiteServiceImpl implements SiteService {
      @Override
      public String siteName(String name) {
      return "async:" + name;
      }
      @Override
      public CompletableFuture<String> siteNameAsync(String name) {
      System.out.println("异步调用:" + name);
      return CompletableFuture.supplyAsync(() -> {
      return siteName(name);
      });
      }
      }
    • Consumer

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      @EnableAutoConfiguration
      public class AsyncDubboConsumer {
      @Reference(version = "async")
      private SiteService siteService;
      public static void main(String[] args) {
      ConfigurableApplicationContext context =
      SpringApplication.run(AsyncDubboConsumer.class);
      SiteService siteService = (SiteService) context.getBean(SiteService.class);
      //调用异步方法
      CompletableFuture<String> future = siteService.siteNameAsync("q-face");
      //设置监听,非阻塞
      future.whenComplete((v, e) -> {
      if (e != null) {
      e.printStackTrace();
      } else {
      System.out.println("result:" + v);
      }
      });
      System.out.println("异步调用结束");
      }
      }

原理

  • 权重轮询算法:

    • 假如有三台服务器A、B、C,权重分别是6、2、2

    • 每台服务器确定两个权重变量:weight、currentWeight,前者不变,后者初始化为0,且currentWeight = currentWeight+weight

    • 集群选择currentWeight最大的服务器作为选择结果。并将该最大服务器的 currentWeight减去各服务器的weight总数

    • 调整currentWeight = currentWeight+weight,开始新⼀轮的选择

      image-20230503195154771