Dubbo 2 入门教程之五

大纲

前言

学习资源

Dubbo 核心特性

Dubbo-Admin 说明

  • 本文使用的 Dubbo-Admin 服务治理控制台,其连接的注册中心、配置中心和元数据中心均为同一个 Zookeeper 实例。
  • 因此,当文中介绍 Zookeeper 中的数据存储结构时,需要以此为前提,尤其是在涉及 Dubbo 路由规则的内容时。

路由规则

路由规则介绍

  • 路由规则在发起一次 RPC 调用前起到过滤目标服务器地址的作用,过滤后的地址列表将作为服务消费端最终发起 RPC 调用的备选地址。Dubbo 的路由规则分为以下几种:

    • 条件路由:支持以服务或 Consumer 应用为粒度配置路由规则。
    • 标签路由: 支持以 Provider 应用为粒度配置路由规则。
    • 脚本路由:可以使用 JDK 支持的引擎(比如 JavaScript、JRuby、Groovy 等)解析路由脚本,默认使用 JavaScript 引擎。
  • 路由规则的 URL 内容:

    • 条件路由规则 URL 内容的示例:
      1
      "route://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=" + URL.encode("host = 10.20.153.10 => host = 10.20.153.11")
    • 脚本路由规则 URL 内容的示例:
      1
      "script://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=" + URL.encode("(function route(invokers) { ... } (invokers))")
  • 动态配置路由规则

    • 对于脚本路由规则,不支持通过 Dubbo-Admin 服务治理控制台进行动态配置(动态调整),需要通过编程方式(Dubbo 原生 API)或者直接向注册中心写入路由规则来实现
    • 对于条件路由规则和标签路由规则,支持通过 Dubbo-Admin 服务治理控制台进行动态配置(动态调整),如下图所示:

条件路由规则

简单介绍
  • 条件路由规则用于将符合特定条件的请求定向到服务提供者的特定地址子集上。
  • 条件路由规则首先会根据请求参数进行匹配,然后将匹配成功的请求转发到包含特定服务提供者实例地址的子集。
  • 条件路由规则可以根据条件(如服务消费者的 IP、应用名称、服务名称、方法名称、参数等)来决定调用哪些服务提供者(Provider)或是否拒绝调用。
  • 条件路由规则影响的是服务消费者(Consumer)的 RPC 调用行为。
配置示例

Dubbo 支持在 Dubbo-Admin 服务治理控制台动态配置条件路由规则,且 Dubbo 项目无需引用额外的 Maven 依赖包即可支持。

  • 应用粒度的条件路由规则
1
2
3
4
5
6
7
8
9
10
11
12
# 应用名称为app1的服务消费者,只能消费所有端口为20880的服务提供者实例
# 应用名称为app2的服务消费者,只能消费所有端口为20881的服务提供者实例
---
scope: application
force: true
runtime: true
enabled: true
key: dubbo-consumer-application
conditions:
- application=app1 => address=*:20880
- application=app2 => address=*:20881
...


  • 服务粒度的条件路由规则
1
2
3
4
5
6
7
8
9
10
11
12
# DemoService的sayHello方法,只能消费所有端口为20880的服务提供者实例
# DemoService的sayHi方法,只能消费所有端口为20881的服务提供者实例
---
scope: service
force: true
runtime: true
enabled: true
key: com.clay.dubbo.service.DemoService
conditions:
- method=sayHello => address=*:20880
- method=sayHi => address=*:20881
...

规则详解
各字段的含义
  • scope 表示路由规则的作用粒度,scope 的取值会决定 key 的取值。必填
    • service 服务粒度
    • application 应用粒度
  • key 明确规则体作用在哪个应用或服务,必填
    • scope=application 时,key 取值为应用的名称
    • scope=service 时,key 取值为 [{group}:]{service}[:{version}] 的组合
  • enabled=true 当前路由规则是否生效,可不填,缺省生效。
  • force=false 当路由结果为空时,是否强制执行;如果不强制执行,路由结果为空的路由规则将自动失效,可不填,缺省为 false
  • runtime=false 是否在每次调用时执行路由规则,否则只在服务提供者地址列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。如果用了参数路由,必须设为 true,需要注意设置会影响调用的性能,可不填,缺省为 false
  • priority=1 路由规则的优先级,用于排序,优先级越大越靠前执行,可不填,缺省为 0
  • conditions 定义具体的路由规则内容。必填
Conditions 规则体

字段 conditions 是路由规则的主体,由一条到任意多条规则组成,下面为每个路由规则的配置语法做详细说明:

Conditions 的格式

  • 基于条件表达式的路由规则,如:host = 10.20.153.10 => host = 10.20.153.11
  • => 之前的为服务消费者匹配条件,所有参数和服务消费者的 URL 进行对比,当服务消费者满足匹配条件时,对该服务消费者执行后面的过滤规则。
  • => 之后为服务提供者地址列表的过滤条件,所有参数和服务提供者的 URL 进行对比,服务消费者最终只拿到过滤后的地址列表。
  • 如果 => 之前的匹配条件为空,表示对所有服务消费者允许访问,如:=> host = 10.20.153.11
  • 如果 => 之后的过滤条件为空,表示对所有服务消费者禁止访问,如:host = 10.20.153.10 =>

Conditions 的表达式

  • 参数支持:

    • 服务调用信息,如:methodargument 等,暂不支持参数路由
    • URL 本身的字段,如:protocolhostport
    • 以及 URL 上的所有参数,如:applicationorganization
  • 条件支持:

    • 等号 = 表示 “匹配”,如:host = 10.20.153.10
    • 不等号 != 表示 “不匹配”,如:host != 10.20.153.10
  • 值支持:

    • 以逗号 , 分隔多个值,如:host != 10.20.153.10,10.20.153.11
    • 以星号 * 结尾,表示通配,如:host != 10.20.*
    • 以美元符 $ 开头,表示引用服务消费者参数,如:host = $host

Condition 的配置示例

  • 排除预发布机器:
1
=> host != 172.22.3.91
  • 白名单:
1
register.ip != 10.20.153.10,10.20.153.11 =>

注意

一个服务只能有一条白名单规则,否则两条规则交叉,就都被筛选掉了。

  • 黑名单:
1
register.ip = 10.20.153.10,10.20.153.11 =>
  • 服务寄宿在应用上,只暴露一部分的机器,防止整个集群挂掉:
1
=> host = 172.22.3.1*,172.22.3.2*
  • 为重要应用提供额外的机器:
1
application != kylin => host != 172.22.3.95,172.22.3.96
  • 读写分离:
1
2
method = find*,list*,get*,is* => host = 172.22.3.94,172.22.3.95,172.22.3.96
method != find*,list*,get*,is* => host = 172.22.3.97,172.22.3.98
  • 前后台分离:
1
2
application = bops => host = 172.22.3.91,172.22.3.92,172.22.3.93
application != bops => host = 172.22.3.94,172.22.3.95,172.22.3.96
  • 隔离不同机房网段:
1
host != 172.22.3.* => host != 172.22.3.*
  • 服务提供者与服务消费者部署在同集群内,本机只访问本机的服务:
1
=> host = $host

标签路由规则

简单介绍

标签路由通过将一个或多个服务提供者(Provier)划分到同一个分组,约束流量只在指定分组中流转,从而实现流量隔离的目的,可以作为灰度发布、蓝绿发布等场景的能力基础。

从上图中可以看到有两个机房分别是机房 A、机房 B,其中机房 A 要求只能访问到 Service A 和 Service B ,而机房 B 要求只能访问到 Service C 和 Service D。要实现上面这种场景,就需要使用到 Dubbo 的标签路由。从机房 A 发起的 RPC 调用携带标签 TAG_A 访问到 Service A 和 Service B,而从机房 B 发起的 RPC 调用携带标签 TAG_B 访问到 Service C 和 Service D。

配置示例
Provider 端

标签主要是指对 Provider 端(服务提供者)实例的分组,目前有两种方式可以完成实例分组,分别是 动态规则打标静态规则打标,其中动态规则相较于静态规则优先级更高,而当两种规则同时存在且出现冲突时,将以动态规则为准。

  • Provider 端的动态规则打标(支持在 Dubbo-Admin 服务治理控制台里面实现 Provider 端的动态规则打标,且 Dubbo 项目无需引用额外的 Maven 依赖包即可支持)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # dubbo-provider-application应用增加了两个标签分组tag1和tag2
    # tag1包含一个实例 127.0.0.1:20880
    # tag2包含一个实例 127.0.0.1:20881
    ---
    force: false
    runtime: true
    enabled: true
    key: dubbo-provider-application
    tags:
    - name: tag1
    addresses: ["127.0.0.1:20880"]
    - name: tag2
    addresses: ["127.0.0.1:20881"]
    ...

  • Provider 端的静态打标

    • 全局设置标签
      • XML 设置方式
        1
        <dubbo:provider tag="tag1"/>
      • YML 设置方式
        1
        2
        3
        dubbo:
        provider:
        tag: tag1
      • 原生 API 设置方式
        1
        2
        ProviderConfig providerConfig = new ProviderConfig();
        providerConfig.setTag("tag1");
      • JVM 启动参数设置方式
        1
        java -jar xxx-provider.jar -Ddubbo.provider.tag={the tag you want, may come from OS ENV}
    • 针对指定服务接口设置标签
      • XML 设置方式
        1
        <dubbo:service interface="com.xxx.DemoService" ref="demoServiceImpl" tag="tag1"/>
      • 注解设置方式
        1
        @DubboService(tag = "tag1")
Consumer 端
  • Consumer 端设置携带的标签(不支持在 Dubbo-Admin 服务治理控制台里面动态设置 Consumer 端携带的标签

    • 全局方式设置 Consumer 端携带的标签
      • XML 设置方式
        1
        <dubbo:consumer tag="tag1" />
      • YML 设置方式
        1
        2
        3
        dubbo:
        consumer:
        tag: tag1
    • 针对指定服务接口设置 Consumer 端携带的标签
      • XML 设置方式
        1
        <dubbo:reference id="demoService" interface="com.xxx.DemoService" tag="tag1" />
      • 注解设置方式
        1
        @DubboReference(tag = "tag1")
      • 原生 API 设置方式
        • 纯原生 API
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          ApplicationConfig app = new ApplicationConfig("demo-consumer");

          RegistryConfig registry = new RegistryConfig();
          registry.setProtocol("zookeeper");
          registry.setAddress("127.0.0.1:2181");

          ReferenceConfig<DemoService> reference = new ReferenceConfig<>();
          reference.setApplication(app);
          reference.setRegistry(registry);
          reference.setInterface(DemoService.class);

          DemoService demoService = reference.get();

          // 指定 Consumer 端携带的标签,必须写在发起 RPC 调用之前
          RpcContext.getContext().setAttachment(CommonConstants.TAG_KEY, "tag1");

          // 发起 RPC 调用
          String result = demoService.sayHi("Dubbo");

          System.out.println("result = " + result);
        • 或者原生 API + 注解
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          @RestController
          public class DemoController {

          @DubboReference
          private DemoService demoService;

          @GetMapping("/sayHello")
          public String sayHello() {
          // 指定 Consumer 端携带的标签,必须写在发起 RPC 调用之前
          RpcContext.getContext().setAttachment(CommonConstants.TAG_KEY, "tag1");
          // 发起 RPC 调用
          return demoService.sayHello();
          }

          @GetMapping("/sayHi")
          public String sayHi() {
          // 指定 Consumer 端携带的标签,必须写在发起 RPC 调用之前
          RpcContext.getContext().setAttachment(CommonConstants.TAG_KEY, "tag2");
          // 发起 RPC 调用
          return demoService.sayHi();
          }

          }
  • 请求标签的作用域为每一次 invocation,使用 attachment 来传递请求标签,注意保存在 attachment 中的值将会在一次完整的远程调用中持续传递,得益于这样的特性,开发者只需要在起始调用时,通过一行代码的设置,达到标签的持续传递。

  • 目前仅仅支持硬编码的方式设置 Dubbo 标签。请注意 RpcContext 是线程绑定的,若希望优雅地使用标签路由的特性,建议通过 Servlet 过滤器(在 Web 环境下),或者定制的 SPI 过滤器中设置 Dubbo 标签。

规则详解
各字段的含义
  • key 明确规则体作用到哪个应用。必填
  • enabled=true 当前路由规则是否生效,可不填,缺省生效。
  • force=false 当路由结果为空时,是否强制执行,如果不强制执行,路由结果为空的路由规则将自动失效,可不填,缺省为 false
  • runtime=false 是否在每次调用时执行路由规则,否则只在服务提供者地址列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。如果用了参数路由,必须设为 true,需要注意设置会影响调用的性能,可不填,缺省为 false
  • priority=1 路由规则的优先级,用于排序,优先级越大越靠前执行,可不填,缺省为 0
  • tags 定义具体的标签分组内容,可定义任意 n (n>= 1) 个标签并为每个标签指定实例列表。必填
  • name,标签名称
  • addresses,当前标签包含的实例列表
降级约定说明
  • 当配置 dubbo.tag=tag1
    • Dubbo 优先选择标记了 tag=tag1 的 Provider;
    • 若集群中不存在与请求标记对应的服务,默认将降级请求 tag 为空的 Provider;
    • 如果要改变这种默认行为,即找不到匹配 tag1 的 Provider 时返回异常,需要配置 dubbo.force.tag=true
  • dubbo.tag 未配置时
    • Dubbo 只会匹配 tag 为空的 Provider;
    • 即使集群中存在可用的服务,若 tag 不匹配也就无法调用;这与上面的约定不同,携带标签的请求可以降级访问到无标签的服务,但不携带标签或者携带其他种类标签的请求永远无法调用到带其他标签的服务

特别注意

脚本路由规则

简单介绍
  • 脚本路由规则为流量管理提供了最大的灵活性,所有流量在执行负载均衡选址之前,都会动态的执行一遍规则脚本,根据脚本执行的结果确定可用的地址子集。
  • 脚本路由规则只对服务消费者(Consumer)生效且只支持应用粒度管理,因此,key 字段必须设置为服务消费者(Consumer)的应用名称。
  • 脚本路由规则不支持通过 Dubbo-Admin 服务治理控制台进行动态配置(动态调整),需要通过编程方式(Dubbo 原生 API)或者直接向注册中心写入路由规则来实现。
  • 脚本语法支持多种,以 Dubbo Java SDK 为例,脚本语法支持 Javascript、Groovy、Kotlin 等,具体可参见每个语言实现的限制,缺省脚本语法为 Javascript。
  • 脚本路由规则 URL 内容的示例:"script://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=" + URL.encode("(function route(invokers) { ... } (invokers))")

特别注意

  • 在 Dubbo 的脚本路由规则中,脚本没有沙箱约束,可执行任意代码,存在后门安全风险。
  • 换句话说,由于 Dubbo 的脚本路由规则可以动态加载远端代码并执行,因此存在潜在的安全隐患;在启用脚本路由规则前,一定要确保脚本规则在安全沙箱内运行。
规则详解

脚本路由规则主体由以下字段构成,其负责定义脚本规则生效的目标服务消费者应用(Consumer)、流量过滤脚本以及一些特定场景下的行为。

字段类型描述必填
configVersionstring脚本规则定义的版本,目前可用版本为 v3.0
keystring本规则所应用的目标应用标识符
typestring用于编写 script 的脚本语言类型
enabledbool是否启用此规则,设置为 false 即禁用
scriptstring用于过滤 Dubbo Provider 实例的脚本内容
forcebool当路由后实例集合为空时如何处理:true 表示直接返回无可用 Provider 的异常,false 表示忽略此规则
脚本示例

script 为脚本路由规则的主体,类型为一个具有符合结构的 string 字符串,具体取决于 type 指定的脚本语言。以下是 type: javascript 的一个脚本规则示例:

1
2
3
4
5
6
7
8
9
(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)); // 表示立即执行方法

写入路由规则

路由规则的写入

Dubbo 往注册中心(比如 Zookeeper)写入条件路由规则或标签路由规则的操作,通常由 Dubbo-Admin 服务治理中心的 UI 界面来完成,其底层的代码实现如下:

1
2
3
4
// 使用 Dubbo 原生 API,往注册中心写入条件路由规则、标签路由规则
RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181"));
registry.register(URL.valueOf("route://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=" + URL.encode("host = 10.20.153.10 => host = 10.20.153.11")));

各字段的含义:

  • route:// 表示路由规则的类型,支持条件路由规则和脚本路由规则,可扩展,必填
  • 0.0.0.0 表示对所有 IP 地址生效,如果只想对某个 IP 的生效,请填入具体 IP,必填
  • com.foo.BarService 表示只对指定服务生效,必填
  • group=foo 对指定服务的指定 group 生效,不填表示对未配置 group 的指定服务生效
  • version=1.0 对指定服务的指定 version 生效,不填表示对未配置 version 的指定服务生效
  • category=routers 表示该数据为动态配置类型,必填
  • dynamic=false 表示该数据为持久数据,当注册方退出时,数据依然保存在注册中心,必填
  • enabled=true 覆盖规则是否生效,可不填,缺省生效。
  • force=false 当路由结果为空时,是否强制执行;如果不强制执行,路由结果为空的路由规则将自动失效,可不填,缺省为 false
  • runtime=false 是否在每次调用时执行路由规则,否则只在服务提供者地址列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。如果用了参数路由,必须设为 true,需要注意设置会影响调用的性能,可不填,缺省为 false
  • priority=1 路由规则的优先级,用于排序,优先级越大越靠前执行,可不填,缺省为 0
  • rule=URL.encode("host = 10.20.153.10 => host = 10.20.153.11") 表示路由规则的内容,必填

Dubbo 往注册中心(比如 Zookeeper)写入脚本路由规则的操作,不支持由 Dubbo-Admin 服务治理中心的 UI 界面来完成,需要通过编程方式(Dubbo 原生 API)或直接向注册中心写入路由规则来实现,比如:

1
2
3
4
// 使用 Dubbo 原生 API,往注册中心写入脚本路由规则
RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181"));
registry.register(URL.valueOf("script://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=" + URL.encode("(function route(invokers) { ... } (invokers))")));
路由规则的存储

当 Dubbo-Admin 使用 Zookeeper 作为配置中心,并通过可视化控制台添加路由规则时,这些路由规则会存储在 Zookeeper 中的不同路径,主要分为以下几类:

  • 全局级路由规则
    • 路径:/dubbo/config/dubbo/routers/
    • 说明:
      • 存放所有服务全局生效的路由规则
      • 适用于存储跨应用、跨服务的统一路由规则
  • 应用级路由规则
    • 路径:/dubbo/config/{application}/routers/
      • {application} 是具体应用的名称
    • 说明:
      • 存放在某个应用内生效的路由规则
      • 影响该应用下的所有服务接口,但不跨应用
  • 服务级路由规则
    • 路径:/dubbo/{serviceInterface}/routers/
      • {serviceInterface} 是具体服务接口的全限定名
    • 说明:
      • 存放某个服务接口专属的路由规则
      • 可覆盖全局级或应用级的路由规则

配置规则(动态配置)

配置规则(也叫 “动态配置” 或者 “动态配置规则”)是 Dubbo 设计的在无需重启应用的情况下,动态调整 RPC 调用行为的一种能力。Dubbo 从 2.7.0 版本开始,支持从应用和服务两个粒度来调整动态配置。

参考资料