Dubbo

简介

Dubbo是一个开源的、高性能的、广泛应用的远程过程调用(RPC)框架,用于在Java中构建分布式系统。它最初由阿里巴巴集团开发,并作为Apache Dubbo项目的一部分后来开源。Dubbo通过提供服务通信、负载均衡、容错等框架来简化分布式应用程序的开发,并且支持HTTP/2、REST、gRPC、JsonRPC、Thrift、Hessian2 等几乎所有主流的通信协议以及Dubbo2、Triple (兼容 gRPC) 高性能协议。

image-20230927165618154

以上是 Dubbo 的工作原理图,从抽象架构上分为两层:服务治理抽象控制面Dubbo 数据面

Dubbo模块图:

image-20231013150940802

十层模块的作用:

image-20231013151006247

Dubbo 服务治理

image-20231007155456387

以下是与Dubbo相关的一些关键特点和概念:

  • 地址发现

    Dubbo 服务发现具备高性能、支持大规模集群、服务级元数据配置等优势,默认提供 Nacos、Zookeeper、Consul 等多种注册中心适配,与 Spring Cloud、Kubernetes Service 模型打通,支持自定义扩展。

  • 负载均衡

    Dubbo 默认提供加权随机、加权轮询、最少活跃请求数优先、最短响应时间优先、一致性哈希和自适应负载等策略

  • 流量路由

    Dubbo 支持通过一系列流量规则控制服务调用的流量分布与行为,基于这些规则可以实现基于权重的比例流量分发、灰度验证、金丝雀发布、按请求参数的路由、同区域优先、超时配置、重试、限流降级等能力。

  • 链路追踪

    Dubbo 官方通过适配 OpenTelemetry 提供了对 Tracing 全链路追踪支持,用户可以接入支持 OpenTelemetry 标准的产品如 Skywalking、Zipkin 等。另外,很多社区如 Skywalking、Zipkin 等在官方也提供了对 Dubbo 的适配。

  • 可观测性

    Dubbo 实例通过 Prometheus 等上报 QPS、RT、请求次数、成功率、异常次数等多维度的可观测指标帮助了解服务运行状态,通过接入 Grafana、Admin 控制台帮助实现数据指标可视化展示。

RPC调用

基本同步调用

  1. 服务提供方:

    • 依赖:

      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
      40
      <dependencies>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
      </dependency>
      <!-- 注册中心-->
      <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
      </dependency>
      <!-- dubbo -->
      <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-spring-boot-starter</artifactId>
      </dependency>

      <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-registry-nacos</artifactId>
      <exclusions>
      <exclusion>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-common</artifactId>
      </exclusion>
      <exclusion>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-remoting-api</artifactId>
      </exclusion>
      <exclusion>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      </exclusion>
      </exclusions>
      </dependency>
      <!-- 此依赖是为了解决java.lang.NoClassDefFoundError: org/apache/commons/lang3/StringUtils 错误-->
      <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      </dependency>
      </dependencies>
    • 配置文件:

      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
      spring:
      application:
      name: provider
      cloud:
      nacos:
      discovery:
      server-addr: localhost:8848 # nacos服务地址
      username: nacos
      password: nacos

      server:
      port: 8001

      dubbo:
      protocol:
      name: dubbo
      port: 28080
      # 注册中心地址
      registry:
      address: nacos://127.0.0.1:8848
      provider: # 具体参数含义, 查看官方文档
      threadpool: fixed
      threads: 10
      loadbalance: roundrobin
      timeout: 8000
      retries: 3
    • 服务接口以及实现类:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      // 接口
      public interface HelloService {
      String hello(String s);
      }
      // 实现类
      @DubboService
      @Component
      public class HelloServiceImpl implements HelloService{
      @Value("${server.port}")
      private String port;

      @Override
      public String hello(String s) {
      return "Hello Nacos Discovery 端口:" + port +s ;
      }
      }

  2. 服务消费方:

    • 依赖:

      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
      40
      41
      42
      43
      44
      45
      <dependencies>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
      </dependency>
      <!-- 注册中心-->
      <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
      </dependency>
      <!-- dubbo -->
      <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-spring-boot-starter</artifactId>
      </dependency>

      <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-registry-nacos</artifactId>
      <exclusions>
      <exclusion>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-common</artifactId>
      </exclusion>
      <exclusion>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-remoting-api</artifactId>
      </exclusion>
      <exclusion>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      </exclusion>
      </exclusions>
      </dependency>
      <!-- web-->
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
      <groupId>com.wht</groupId>
      <artifactId>provider</artifactId>
      <version>0.0.1-SNAPSHOT</version>
      </dependency>
      </dependencies>
    • 配置:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      spring:
      application:
      name: consumer
      cloud:
      nacos:
      discovery:
      server-addr: localhost:8848 # nacos服务地址
      username: nacos
      password: nacos

      server:
      port: 8003

      dubbo:
      protocol:
      name: dubbo
      port: -1 # 端口不能与dubbo-provider重复;
      # 注册中心地址
      registry:
      address: nacos://127.0.0.1:8848
      consumer:
      check: true
    • 控制层:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @RestController
      public class HelloController {
      // 引用dubbo的服务
      @DubboReference
      private HelloService helloService;

      @GetMapping(value = "/echo/{str}")
      public String echo(@PathVariable(value = "str") String string){
      String s = helloService.hello(string);
      return s;
      }
      }
  3. 直接通过浏览器访问消费者接口即可

异步调用

如果服务接口某部分非常复杂又有点耗时的功能,遇到高流量时就会出现线程池耗尽问题(特别是对于一些 IO 耗时的操作,比较影响客户体验和使用性能的一些地方),这时候就需要异步调用来解决这个问题了。

  • 服务提供方:

    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
    // 实现类
    @DubboService
    @Component
    public class HelloServiceImpl implements HelloService{
    @Value("${server.port}")
    private String port;

    @Override
    public String hello(String s) {
    // 创建线程池对象
    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    // 开启异步化操作模式,标识异步化模式开始
    AsyncContext asyncContext = RpcContext.startAsync();
    // 利用线程池来处理 queryOrderById 的核心业务逻辑
    cachedThreadPool.execute(new Runnable() {
    @Override
    public void run(){
    // 将 hello 所在线程的上下文信息同步到该子线程中
    asyncContext.signalContextSwitch();
    // 这里模拟执行一段耗时的业务逻辑
    sleepInner(5000);
    // 利用 asyncContext 将 resultInfo 返回回去
    asyncContext.write(port);
    }
    }
    return null ;
    }
    }

    核心实现就 3 点:

    1. 定义线程池对象,通过 RpcContext.startAsync 方法开启异步模式;
    2. 在异步线程中通过 asyncContext.signalContextSwitch 同步父线程的上下文信息;
    3. 在异步线程中将异步结果通过 asyncContext.write 写入到异步线程的上下文信息中。
  • 服务消费方:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 此调用会立即返回null
    helloService.hello("world");
    // 拿到调用的Future引用,当结果返回后,会被通知和设置到此Future
    CompletableFuture<String> helloFuture = RpcContext.getServiceContext().getCompletableFuture();
    // 为Future添加回调
    helloFuture.whenComplete((retValue, exception) -> {
    if (exception == null) {
    System.out.println(retValue);
    } else {
    exception.printStackTrace();
    }
    });

异步总是不等待返回,你也可以设置是否等待消息发出

  • sent="true" 等待消息发出,消息发送失败将抛出异常。
  • sent="false" 不等待消息发出,将消息放入 IO 队列,即刻返回。
1
<dubbo:method name="hello" async="true" sent="true" />

如果你只是想异步,完全忽略返回值,可以配置 return="false",以减少 Future 对象的创建和管理成本

1
<dubbo:method name="hello" async="true" return="false" />

异步的应用场景主要有 3 类:IO 耗时、无业务牵连、无时序要求。

泛化调用

泛化调用允许您在不事先知道具体服务接口和方法的情况下,通过泛化的方式动态调用远程服务。

应用场景:

  • 透传式调用,发起方只是想调用提供者拿到结果,没有过多的业务逻辑诉求,即使有,也是拿到结果后再继续做分发处理。
  • 代理服务,所有的请求都会经过代理服务器,而代理服务器不会感知任何业务逻辑,只是一个通道,接收数据->发起调用->返回结果,调用流程非常简单纯粹。
  • 前端网关,有些内网环境的运营页面,对URL的格式没有那么严格的讲究,页面的功能都是和后端服务一对一的操作,非常简单直接。

实现:

  1. 实现控制层:

    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
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    @RestController
    public class CommonController {
    // 响应码为成功时的值
    public static final String SUCC = "000000";
    public static String nacosAddress = "nacos://127.0.0.1:8848";

    // 定义URL地址
    @PostMapping("/gateway/{className}/{mtdName}/{parameterTypeName}/request")
    public String commonRequest(@PathVariable String className,
    @PathVariable String mtdName,
    @PathVariable String parameterTypeName,
    @RequestBody String reqBody){
    // 将入参的req转为下游方法的入参对象,并发起远程调用
    return commonInvoke(className, parameterTypeName, mtdName, reqBody);
    }

    /**
    * <h2>模拟公共的远程调用方法.</h2>
    *
    * @param className:下游的接口归属方法的全类名。
    * @param mtdName:下游接口的方法名。
    * @param parameterTypeName:下游接口的方法入参的全类名。
    * @param reqParamsStr:需要请求到下游的数据。
    * @return 直接返回下游的整个对象。
    * @throws InvocationTargetException
    * @throws IllegalAccessException
    */
    public static String commonInvoke(String className,
    String mtdName,
    String parameterTypeName,
    String reqParamsStr) {
    // 然后试图通过类信息对象想办法获取到该类对应的实例对象
    ReferenceConfig<GenericService> referenceConfig = createReferenceConfig(className);

    // 远程调用
    GenericService genericService = referenceConfig.get();
    Object resp = genericService.$invoke(
    mtdName,
    new String[]{parameterTypeName},
    new Object[]{JSON.parseObject(reqParamsStr, Map.class)});

    // 判断响应对象的响应码,不是成功的话,则组装失败响应
    if(!SUCC.equals(OgnlUtils.getValue(resp, "respCode"))){
    return RespUtils.fail(resp);
    }

    // 如果响应码为成功的话,则组装成功响应
    return RespUtils.ok(resp);
    }

    private static ReferenceConfig<GenericService> createReferenceConfig(String className) {
    DubboBootstrap dubboBootstrap = DubboBootstrap.getInstance();

    // 设置应用服务名称
    ApplicationConfig applicationConfig = new ApplicationConfig();
    applicationConfig.setName(dubboBootstrap.getApplicationModel().getApplicationName());

    // 设置注册中心的地址
    RegistryConfig registryConfig = new RegistryConfig(nacosAddress);
    ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
    referenceConfig.setApplication(applicationConfig);
    referenceConfig.setRegistry(registryConfig);
    referenceConfig.setInterface(className);

    // 设置泛化调用形式
    referenceConfig.setGeneric("true");
    // 设置默认超时时间5秒
    referenceConfig.setTimeout(5 * 1000);
    return referenceConfig;
    }
    }

    代码中主要解决了怎么找到接口代理对象的核心逻辑问题,关键步骤是:

    • URL地址增加了一个方法参数类名的维度,意味着通过类名、方法名、方法参数类名可以访问后台的提供者;
    • 通过接口类名来创建 ReferenceConfig 对象,并设置 generic = true 的核心属性;
    • 通过 referenceConfig.get 方法得到 genericService 泛化对象;
    • 将方法名、方法参数类名、业务请求参数传入泛化对象的 $invoke 方法中进行远程Dubbo调用,并返回响应对象;
    • 通过 Ognl 表达式语言从响应对象取出 respCode 响应码判断并做最终返回。

点点直连

“点点直连”(或称为”点对点直连”)是一种Dubbo服务提供者和消费者之间的直接通信方式,而不通过注册中心。这意味着Dubbo消费者不会从注册中心获取服务提供者的地址,而是直接配置提供者的地址,消费者会将请求直接发送到提供者的地址。

常用场景:

  1. 修复产线事件,通过直连 + 泛化 + 动态代码编译执行,可以轻松临时解决产线棘手的问题。
  2. 绕过注册中心直接联调测试,有些公司由于测试环境的复杂性,有时候不得不采用简单的直连方式,来快速联调测试验证功能。
  3. 检查服务存活状态,如果需要针对多台机器进行存活检查,那就需要循环调用所有服务的存活检查接口。

实现:

例如:对某个服务漏发bug进行修护

解决思路:首先需要准备一个页面,填入 5 个字段信息,接口类名、接口方法名、接口方法参数类名、指定的 URL 节点、修复问题的 Java 代码,然后将这 5 个字段通过 HTTP 请求发往 Web 服务器,Web 服务器接收到请求后组装泛化所需对象,最后通过泛化调用的形式完成功能修复。

  • 定义修护请求对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Setter
    @Getter
    public class RepairRequest {
    /** <h2>接口类名,例:com.xyz.MonsterFacade</h2> **/
    private String className;
    /** <h2>接口方法名,例:heretical</h2> **/
    private String mtdName;
    /** <h2>接口方法参数类名,例:com.xyz.bean.HereticalReq</h2> **/
    private String parameterTypeName;
    /** <h2>指定的URL节点,例:dubbo://ip:port</h2> **/
    private String url;
    /** <h2>可以是调用具体接口的请求参数,也可以是修复问题的Java代码</h2> **/
    private String paramsMap;
    }
  • 调用泛化接口:

    1
    2
    3
    4
    5
    6
    7
    8
    // 然后试图通过类信息对象想办法获取到该类对应的实例对象
    ReferenceConfig<GenericService> referenceConfig = createReferenceConfig(repairRequest.getClassName(), repairRequest.getUrl());
    // 远程调用
    GenericService genericService = referenceConfig.get();
    Object resp = genericService.$invoke(
    repairRequest.getMtdName(),
    new String[]{repairRequest.getParameterTypeName()},
    new Object[]{JSON.parseObject(repairRequest.getParamsMap(), Map.class)});
  • ApplicationConfig创建时设置url:

    1
    2
    // 设置点对点连接的地址
    referenceConfig.setUrl(url);

    重点配置项说明

dubbo配置项会有一个覆盖策略:

全局配置优先级从大到小为:JVM参数配置、dubbo.xml参数配置、application.properties参数配置。

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

重点配置:

  • 启动时检查:

    • 消费者检查:dubbo.consumer.check=false(启动项目时检查服务提供方是否已经进行服务注册)
    • 注册中心检查:dubbo.registry.check=false
  • dubbo直连(不需要注册中心):

    • 在消费者配置:@Reference(url="提供者地址:端口")
  • 调用超时时间(可以配置在各级处方法、服务、全局等,覆盖原则:精确优先、消费优先):

    • 在服务引用处配置dubbo.reference.timeout=5000(超时时间默认为1000毫秒)
    • 在全局配置:dubbo.consumer.timeout=5000
  • 重试次数(不包含第一次,一般在幂等服务上配置):

    • 在服务引用处配置:dubbo.reference.retries=3
  • 多版本(用于灰度发布,指定消费者使用什么版本的服务;可配置为*表示随机版本):

    1. 提供方的服务申明处配置版本:dubbo.service.version=1.0.0
    2. 消费方的服务引用处配置引用的版本:dubbo.reference.version=1.0.0
  • 本地存根(用于在远程调用之前做一些操作,是在公共接口中做的,为了解耦consumer,又为了避免不必要的远程访问):

    1. 消费者实现远程接口:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      package com.foo;
      public class BarServiceStub implements BarService {
      private final BarService barService;

      // 构造函数传入真正的远程代理对象
      public BarServiceStub(BarService barService){
      this.barService = barService;
      }

      public String sayHello(String name) {
      // 此代码在客户端执行, 你可以在客户端做ThreadLocal本地缓存,或预先验证参数是否合法,等等
      try {
      return barService.sayHello(name);
      } catch (Exception e) {
      // 你可以容错,可以做任何AOP拦截事项
      return "容错数据";
      }
      }
      }
    2. 在消费者配置存根:dubbo.reference.stub=com.foo.BarServiceStub

    在SpringBoot下进行配置的三种方式:

    1. 导入dubbo-starter,开启@EnableDubbo在application.properties配置属性,使用@Service【暴露服务】使用@Reference【引用服务】。
    2. 使用原始配置文件方式,使用@ImportResource( locations="classpath:provider.xm1")添加配置。
    3. 使用注解API方式:自己创建一个@Configuration配置类通过@Bean配置。

    完整配置项见官网

其他功能

事件通知

Dubbo的事件通知机制提供了一种通用的方式,让开发者扩展Dubbo框架的功能,以满足特定的需求和场景,同时保持了系统的灵活性和可维护性。

事件通知机制的实现通常包括 onInvokeonReturnonThrow 这三个核心方法,它们允许开发者在服务提供者和消费者的方法调用过程中执行自定义逻辑。

  1. onInvoke 方法:在服务提供者端,在远程方法调用之前触发。你可以在这个方法中执行一些准备工作,例如在请求被处理之前进行验证或记录日志。
  2. onReturn 方法:在服务提供者端和服务消费者端,当远程方法成功返回结果时触发。你可以在这个方法中处理成功返回的结果,例如记录成功的响应或执行后续逻辑。
  3. onThrow 方法:在服务提供者端和服务消费者端,当远程方法抛出异常时触发。你可以在这个方法中处理异常,例如记录错误日志、执行回滚操作或者返回自定义异常信息。

常用场景:

  1. 职责分离,可以按照功能相关性剥离开,让各自的逻辑是内聚的、职责分明的。
  2. 解耦,把复杂的面向过程风格的一坨代码分离,可以按照功能是技术属性还是业务属性剥离。
  3. 事件溯源,针对一些事件的实现逻辑,如果遇到未知异常后还想再继续尝试重新执行的话,可以考虑事件持久化并支持在一定时间内重新回放执行。

实现:

  1. 实现事件对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Component("eventNotifyService")
    public class EventNotifyService {
    // 调用之前
    public void onInvoke(String name) {
    System.out.println("[事件通知][调用之前] onInvoke 执行.name:" + name);
    }
    // 调用之后
    public void onReturn(String result, String name) {
    System.out.println("[事件通知][调用之后] onReturn 执行.result:"+result + "name:"+name);
    // 远程调用后需要执行的操作
    }
    // 调用异常
    public void onThrow(Throwable ex, String name) {
    System.out.println("[事件通知][调用异常] onThrow 执行.ex:"+ex+"name:"+name);
    }
    }
  2. 引用服务:

    1
    2
    3
    // 引用dubbo的服务
    @DubboReference(methods = {@Method(name = "sayHello",oninvoke = "eventNotifyService.onInvoke",onreturn = "eventNotifyService.onReturn",onthrow = "eventNotifyService.onThrow")})
    private HelloService helloService;

    注意dubbo2.7.12之前只能使用xml配置方式进行配置,之后才对注解bug进行了修护

参数校验

参数验证功能是基于 JSR303 实现的,用户只需标识 JSR303 标准的验证 annotation,并通过声明 filter 来实现验证。

优点:

  • 轻松做到了既能在消费方提前预判参数的合法性,也能在提供方进行参数的兜底校验。
  • 让代码更加精简提升编码效率,减少大量枯燥无味的雷同代码。

应用场景:

  1. 单值简单规则判断,各个字段的校验逻辑毫无关联、相互独立。
  2. 提前拦截掉脏请求,尽可能把一些参数值不合法的情况提前过滤掉,对于消费方来说尽量把高质量的请求发往提供方,对于提供方来说,尽量把非法的字段值提前拦截,以此保证核心逻辑不被脏请求污染。
  3. 通用网关校验领域,在网关领域部分很少有业务逻辑,但又得承接请求,对于不合法的参数请求就需要尽量拦截掉,避免不必要的请求打到下游系统,造成资源浪费。

实现:

  1. 导入校验依赖:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
    </dependency>
    <dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.0.Final</version>
    </dependency>
  2. 给参数对象添加校验注解:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Setter
    @Getter
    public class ValidateUserInfo implements Serializable {
    private static final long serialVersionUID = 1558193327511325424L;
    // 添加了 @NotBlank 注解
    @NotBlank(message = "id 不能为空")
    private String id;
    // 添加了 @Length 注解
    @Length(min = 5, max = 10, message = "name 必须在 5~10 个长度之间")
    private String name;
    // 无注解修饰
    private String sex;
    }
  3. consumer端校验(如果校验不通过, 请求是不会发给 Provider的):

    修改配置

    1
    2
    3
    consumer: # Dubbo 消费端配置
    check: false
    validation: true # 是否启用JSR303标准注解验证, 如果启用, 将对方法参数上的注解进行校验
  4. Provider端校验:

    1
    2
    3
    4
    5
    dubbo:
    provider: # Dubbo 服务端配置
    timeout: 600000 # 远程服务调用超时时间(毫秒)
    version: 1.0.0 # 服务版本
    validation: true # 是否启用JSR303标准注解验证, 如果启用, 将对方法参数上的注解进行校验

    缓存操作

Dubbo支持了服务端结果缓存和客户端结果缓存。用于加速热门数据的访问速度,Dubbo 提供声明式缓存,以减少用户加缓存的工作量。

缓存类型

目前Dubbo3.0版本及高于其的版本都支持以下几种内置的缓存策略:

  • lru 基于最近最少使用原则删除多余缓存,保持最热的数据被缓存。
  • lfu基于淘汰使用频次最低的原则来实现缓存策略。
  • expiring基于过期时间原则来实现缓存策略。
  • threadlocal 当前线程缓存,比如一个页面渲染,用到很多 portal,每个 portal 都要去查用户信息,通过线程缓存,可以减少这种多余访问。
  • jcacheJSR107 集成,可以桥接各种缓存实现。

应用场景:

  • 数据库缓存,对于从数据库查询出来的数据,如果多次查询变化差异较小的话,可以按照一定的维度缓存起来,减少访问数据库的次数,为数据库减压。
  • 业务层缓存,对于一些聚合的业务逻辑,执行时间过长或调用次数太多,而又可以容忍一段时间内数据的差异性,也可以考虑采取一定的维度缓存起来。

实现:

  1. 在各个粒度进行缓存设置:
    • 消费端:
      • 接口:@DubboReference(cache = "lru")
      • 方法:@DubboReference(methods = {@Method(name="sayHello",cache = "lru")})
    • 服务端:
      • 接口:@DubboService(cache = "lru")
      • 方法:@DubboService(methods = {@Method(name="sayHello",cache = "lru")})

负载均衡

在集群负载均衡时,Dubbo提供了多种均衡策略,缺省为random随机调用。

负载均衡策略:

算法 特性 备注 配置值
Weighted Random LoadBalance 加权随机 默认算法,默认权重相同 random (默认)
RoundRobin LoadBalance 加权轮询 借鉴于 Nginx 的平滑加权轮询算法,默认权重相同, roundrobin
LeastActive LoadBalance 最少活跃优先 + 加权随机 背后是能者多劳的思想 leastactive
Shortest-Response LoadBalance 最短响应优先 + 加权随机 更加关注响应速度 shortestresponse
ConsistentHash LoadBalance 一致性哈希 确定的入参,确定的提供者,适用于有状态请求 consistenthash
P2C LoadBalance Power of Two Choice 随机选择两个节点后,继续选择“连接数”较小的那个节点。 p2c
Adaptive LoadBalance 自适应负载均衡 在 P2C 算法基础上,选择二者中 load 最小的那个节点 adaptive

配置(同样可以在各级配置):

  • 在消费者服务引用处配置:dubbo.reference.loadbalance=roundrobin

服务降级

服务降级是指服务在非正常情况下进行降级应急处理。

使用场景

  • 某服务或接口负荷超出最大承载能力范围,需要进行降级应急处理,避免系统崩溃
  • 调用的某非关键服务或接口暂时不可用时,返回模拟数据或空,业务还能继续可用
  • 降级非核心业务的服务或接口,腾出系统资源,尽量保证核心业务的正常运行
  • 某上游基础服务超时或不可用时,执行能快速响应的降级预案,避免服务整体雪崩

配置:

  • 在消费者服务引用处配置:dubbo.reference.mock="[fail|force]return|throw xxx"
    • fail 或 force 关键字可选,表示调用失败或不调用强制执行 mock 方法,如果不指定关键字默认为 fail
    • return 表示指定返回结果,throw 表示抛出指定异常
    • xxx 根据接口的返回类型解析,可以指定返回值或抛出自定义的异常

集群容错

集群容错是指,多个服务器部署同一集群中,运行同一应用程序,如果一台服务器出现故障,其他服务器将接管负载,确保应用程序对用户仍然可用。

使用方式

  • Failover Cluster(故障自动恢复,默认策略):Failover 是 Dubbo 的默认集群容错策略。在该策略下,Dubbo 会自动重试其他可用的服务提供者,直到找到一个可用的提供者或达到最大重试次数。这个策略适用于对可用性要求较高的场景。
  • Failfast Cluster(快速失败):Failfast 策略在发生失败时立即报错,不会进行重试。这个策略适用于对性能要求较高,但可用性要求较低的场景。
  • Failsafe Cluster(安全失败):Failsafe 策略在发生失败时会忽略错误,直接返回一个空结果。这个策略适用于一些不希望因为服务提供者的故障而影响主调用者的场景。
  • Failback Cluster(失败自动恢复):Failback 策略会在服务提供者恢复后自动切换回正常的提供者。适用于一些希望服务提供者在恢复后能够重新参与请求处理的场景。
  • Forking Cluster(并行调用多个提供者):Forking 策略会同时调用多个服务提供者,然后返回最快响应的结果。适用于一些需要提高可用性和性能的场景。
  • Broadcast Cluster(广播调用所有提供者):Broadcast 策略会广播调用所有可用的服务提供者,然后将结果合并返回。适用于一些需要广播消息或者执行广播任务的场景。
  • Available Cluster(可用性优先):Available 策略会优先调用可用的服务提供者,如果没有可用的提供者,则会报错。适用于对可用性要求较高的场景。

在 Dubbo 的服务引用配置中通过配置cluster属性来指定使用哪种集群容错策略:dubbo.reference.cluster=failover

流量管控

Dubbo 的流量管控规则可以基于应用、服务、方法、参数等粒度精准的控制流量走向,根据请求的目标服务、方法以及请求体中的其他附加参数进行匹配,符合匹配条件的流量会进一步的按照特定规则转发到一个地址子集。流量管控规则有以下几种:

  • 条件路由规则
  • 标签路由规则
  • 动态配置规则
  • 脚本路由规则

条件路由规则

首先对请求中的参数进行匹配,符合匹配条件的请求将被转发到包含特定实例地址列表的子集。

条件路由规则的主体 conditions 主要包含两部分内容:

  • => 之前的为请求参数匹配条件,指定的 匹配条件指定的参数 将与 消费者的请求上下文 (URL)、甚至方法参数 进行对比,当消费者满足匹配条件时,对该消费者执行后面的地址子集过滤规则。
  • => 之后的为地址子集过滤条件,指定的过滤条件指定的参数将与提供者实例地址 (URL)进行对比,消费者最终只能拿到符合过滤条件的实例列表,从而确保流量只会发送到符合条件的地址子集。
    • 如果匹配条件为空,表示对所有请求生效,如:=> status != staging
    • 如果过滤条件为空,表示禁止来自相应请求的访问,如:application = product =>

条件路由规则还支持设置具体的机器地址如 ip 或 port:=> host != 172.22.3.91

条件路由还支持基于请求参数的匹配:method=getDetail&arguments[0]=dubbo => port=20880

标签路由规则

标签路由规则是一个非此即彼的流量隔离方案,也就是匹配标签的请求会 100% 转发到有相同标签的实例,没有匹配标签的请求会 100% 转发到其余未匹配的实例。

标签主要是指对 Provider 端应用实例的分组,目前有两种方式可以完成实例分组,分别是动态规则打标静态规则打标

  • 静态规则打标:

    1. 指定服务提供方实例的标签:<dubbo:provider tag="gray"/>
    2. 发起调用的一方,在每次请求前通过 tag 设置流量标签,确保流量被调度到带有同样标签的服务提供方:RpcContext.getContext().setAttachment(Constants.TAG_KEY, "gray");
  • 动态规则打标:

    1. 给实例打标:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      spec:
      containers:
      - name: detail
      image: apache/demo-detail:latest
      env:
      - name: DUBBO_ENV_KEYS
      value: "REGION, ENV"
      - name: REGION
      value: "hangzhou"
      - name: ENV
      value: "gray"
    2. 指定的匹配条件将 Provider 实例动态的划分到不同的流量分组中:

      1
      2
      3
      4
      5
      6
      7
      #匹配 env: gray 的实例被划分到 gray 分组,其余不匹配 env: gray 继续留在默认分组 (无 tag)。
      tags:
      - name: gray
      match:
      - key: env
      value:
      exact: gray

      动态配置规则

通过 Dubbo 提供的动态配置规则,您可以动态的修改 Dubbo 服务进程的运行时行为,整个过程不需要重启,配置参数实时生效。基于这个强大的功能,基本上所有运行期参数都可以动态调整,比如超时时间、临时开启 Access Log、修改 Tracing 采样率、调整限流降级参数、负载均衡、线程池配置、日志等级、给机器实例动态打标签等。

示例 - 修改超时时间

以下示例将 org.apache.dubbo.samples.UserService 服务的超时参数调整为 2000ms

1
2
3
4
5
6
7
8
configVersion: v3.0
scope: service
key: org.apache.dubbo.samples.UserService
enabled: true
configs:
- side: provider
parameters:
timeout: 2000

脚本路由规则

脚本路由是最直观的路由方式,同时它也是当前最灵活的路由规则,因为你可以在脚本中定义任意的地址筛选规则。如果我们为某个服务定义一条脚本规则,则后续所有请求都会先执行一遍这个脚本,脚本过滤出来的地址即为请求允许发送到的、有效的地址集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
configVersion: v3.0
key: demo-provider
type: javascript
enabled: true
script: |
(function route(invokers,invocation,context) {
var result = new java.util.ArrayList(invokers.size());
for (i = 0; i < invokers.size(); i ++) {
if ("10.20.3.3".equals(invokers.get(i).getUrl().getHost())) {
result.add(invokers.get(i));
}
}
return result;
} (invokers, invocation, context)); // 表示立即执行方法

完善日志

原生的日志:

image-20231009163036309

但实际开发中一定要考虑不同请求、不同线程这两个因素,不能确定两行日志一定是同一次请求、同一个线程打印出来的。

隐式传递:

为了尽可能降低侵入性,我们最好能在系统的入口和出口,把接收数据的操作以及发送数据的操作进行完美衔接。这就意味着需要在接收请求的内部、发送请求的内部做好数据的交换。

代码实现:

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
@Activate(group = PROVIDER, order = -9000)
public class ReqNoProviderFilter implements Filter {
public static final String TRACE_ID = "TRACE-ID";
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// 获取入参的跟踪序列号值
Map<String, Object> attachments = invocation.getObjectAttachments();
String reqTraceId = attachments != null ? (String) attachments.get(TRACE_ID) : null;

// 若 reqTraceId 为空则重新生成一个序列号值,序列号在一段相对长的时间内唯一足够了
reqTraceId = reqTraceId == null ? generateTraceId() : reqTraceId;

// 将序列号值设置到上下文对象中
RpcContext.getServerAttachment().setObjectAttachment(TRACE_ID, reqTraceId);

// 并且将序列号设置到日志打印器中,方便在日志中体现出来
MDC.put(TRACE_ID, reqTraceId);

// 继续后面过滤器的调用
return invoker.invoke(invocation);
}
}

@Activate(group = CONSUMER, order = Integer.MIN_VALUE + 1000)
public class ReqNoConsumerFilter implements Filter, Filter.Listener {
public static final String TRACE_ID = "TRACE-ID";
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// 从上下文对象中取出跟踪序列号值
String existsTraceId = RpcContext.getServerAttachment().getAttachment(TRACE_ID);

// 然后将序列号值设置到请求对象中
invocation.getObjectAttachments().put(TRACE_ID, existsTraceId);
RpcContext.getClientAttachment().setObjectAttachment(TRACE_ID, existsTraceId);

// 继续后面过滤器的调用
return invoker.invoke(invocation);
}
}

修改一下日志器的打印日志模式:

1
%d{yyyy-MM-dd HH:mm:ss.SSS} [${APP_NAME}, %thread, %X{X-TraceId}] %-5level %c{1} -%msg%n

源码分析

Wrapper机制

Dubbo的Wrapper机制是一种用于封装和解封装普通POJO(Plain Old Java Object)对象的扩展机制。这个机制的目的是将普通Java对象包装成Dubbo的特殊代理对象,以便在Dubbo的RPC(远程过程调用)框架中进行远程方法调用。Wrapper机制通常用于将普通Java对象转换为Dubbo可识别的形式,以便进行网络传输。

Wrapper机制在Dubbo中的存在有几个重要原因和用途:

  1. 数据序列化和反序列化:在分布式系统中,数据需要在不同的进程之间传输。Dubbo的Wrapper机制负责将原始Java对象转换为可传输的数据流,以及在接收端将数据流还原为Java对象。这是RPC通信中必不可少的一环,而Wrapper机制简化了这个过程。
  2. 跨语言兼容性:Dubbo支持多种编程语言,包括Java、JavaScript、Python等。不同编程语言可能使用不同的数据表示方式,Wrapper机制可以将Java对象转换为Dubbo通用的数据格式,以便其他语言的消费方能够正确解析和使用数据。
  3. 数据类型转换:Wrapper机制能够将Java基本数据类型和集合类型转换为Dubbo支持的格式,确保数据的正确传输。这对于处理不同语言和平台的数据兼容性非常重要。
  4. 服务元数据:Wrapper机制可以添加额外的元数据信息,如服务方法的名称、参数类型等,这有助于Dubbo在消费方正确调用远程服务。
  5. 动态代理:Wrapper机制允许Dubbo在运行时动态生成代理对象,这使得Dubbo能够处理各种不同的服务提供者,并适应不同的服务接口。这提高了Dubbo的灵活性和可扩展性。