一、负载均衡

1、集群容错

微服务项目中同一个服务可集群配置多个节点。dubbo的集群节点结构图:
image.png
各节点关系:

  • 这里的 InvokerProvider 的一个可调用 Service 的抽象,Invoker 封装了 Provider 地址及 Service 接口信息。
  • Directory 代表多个Invoker,可以把它看成 List<Invoker> ,但与 List 不同的是,它的值可能是动态变化的,比如注册中心推送变更。
  • ClusterDirectory 中的多个 Invoker 伪装成一个 Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个。
  • Router 负责从多个 Invoker 中按路由规则选出子集,比如读写分离,应用隔离等。
  • LoadBalance 负责从多个 Invoker 中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选。

dubbo 提供了多种集群服务调用容错方案:

  • Failfast Cluster(默认):快速失败,只发起一次调用,失败立即报错。通常用于非幂等性(幂等:调用结果不随调用次数变化)的写操作,比如新增记录。

默认的容错策略**Failover Cluster**会在失败时自动切换,出现失败会直接重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 **retries="n"** 来设置重试次数(不含第一次)。

  1. <dubbo:service retries="2" />
  2. <!--或-->
  3. <dubbo:reference retries="2" />
  4. <!--或-->
  5. <dubbo:reference>
  6. <dubbo:method name="findFoo" retries="2" />
  7. </dubbo:reference>
  • Failsafe Cluster :失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
  • Failback Cluster:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
  • Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 **forks="n"** 来设置最大并行数。
  • Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。

现在广播调用中,可以通过 broadcast.fail.percent 配置节点调用失败的比例,当达到这个比例后,BroadcastClusterInvoker 将不再调用其他节点,直接抛出异常。 broadcast.fail.percent 取值在 0~100 范围内。默认情况下当全部调用失败后,才会抛出异常。 broadcast.fail.percent 只是控制的当失败后是否继续调用其他节点,并不改变结果(任意一台报错则报错)。(dubbo2.7.10 及以上版本)

  1. // 当 20% 的节点调用失败就抛出异常,不再调用其他节点。
  2. @reference(cluster = "broadcast", parameters = {"broadcast.fail.percent", "20"})

集群模式配置
按照以下示例在服务提供方和消费方配置集群容错模式:

  1. <dubbo:service cluster="failsafe" />
  2. <!--或-->
  3. <dubbo:reference cluster="failsafe" />

2、负载策略

在集群负载均衡时,Dubbo 提供了多种均衡策略:

  • Random LoadBalance(默认值)
    • 随机,按权重设置随机概率。
    • 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
  • RoundRobin LoadBalance
    • 轮询,按公约后的权重设置轮询比率。
    • 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
  • LeastActive LoadBalance
    • 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
    • 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
  • ConsistentHash LoadBalance
    • 一致性 Hash,相同参数的请求总是发到同一提供者。
    • 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
    • 缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key="hash.arguments" value="0,1" />
    • 缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key="hash.nodes" value="320" />

配置方式:

  1. <!--服务端服务级别-->
  2. <dubbo:service interface="..." loadbalance="roundrobin" />
  3. <!--服务端方法级别-->
  4. <dubbo:service interface="...">
  5. <dubbo:method name="..." loadbalance="roundrobin"/>
  6. </dubbo:service>
  7. <!--客户端服务级别-->
  8. <dubbo:reference interface="..." loadbalance="roundrobin" />
  9. <!--客户端方法级别-->
  10. <dubbo:reference interface="...">
  11. <dubbo:method name="..." loadbalance="roundrobin"/>
  12. </dubbo:reference>

3、重试配置

Dubbo 服务在尝试调用一次之后,如出现非业务异常(服务突然不可用、超时等),默认会进行额外的最多2次重试。
重试次数支持两种自定义配置:

  • 通过注解/xml进行固定配置

    1. <dubbo:consumer retries="2"></dubbo:consumer>
  • 通过RpcContext进行运行时动态配置,优先级高于注解/xml进行的固定配置(两者都配置的情况下,以RpcContext配置为准)。

    1. // dubbo服务调用前,通过RpcContext动态设置本次调用的重试次数
    2. RpcContext rpcContext = RpcContext.getContext();
    3. rpcContext.setAttachment("retries", 5);

    二、服务降级

    1、降级处理

    可以通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略。
    向注册中心写入动态配置覆盖规则:

    1. RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
    2. Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181"));
    3. 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 表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。

  • 还可以改为 mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。

    2、服务伪装

    本地伪装通常用于服务降级,比如某验权服务,当服务提供方全部挂掉后,客户端不抛出异常,而是通过 Mock 数据返回授权失败。
    在 spring 配置文件中按以下方式配置:
    1. <dubbo:reference interface="com.foo.BarService" mock="true" />
    2. <!--或-->
    3. <dubbo:reference interface="com.foo.BarService" mock="com.foo.BarServiceMock" />
    然后在工程中提供 Mock 实现:
    1. package com.foo;
    2. public class BarServiceMock implements BarService {
    3. public String sayHello(String name) {
    4. // 可以伪造容错数据,此方法只在出现RpcException时被执行
    5. return "容错数据";
    6. }
    7. }
    如果服务的消费方经常需要 try-catch 捕获异常,如:
    1. Offer offer = null;
    2. try {
    3. offer = offerService.findOffer(offerId);
    4. } catch (RpcException e) {
    5. logger.error(e);
    6. }
    请考虑改为 Mock 实现,并在 Mock 实现中 return null。如果只是想简单的忽略异常可用(2.0.11 以上版本):
    1. <dubbo:reference interface="com.foo.BarService" mock="return null" />

    3、Mock

    Mock 是 Stub 的一个子集,便于服务提供方在客户端执行容错逻辑,因经常需要在出现 RpcException (比如网络失败,超时等)时进行容错,而在出现业务异常(比如登录用户名密码错误)时不需要容错。

return
使用 return 来返回一个字符串表示的对象,作为 Mock 的返回值。合法的字符串可以是:

  • empty : 代表空,基本类型的默认值,或者集合类的空值
  • null: null
  • true : true
  • false : false
  • JSON 格式 : 反序列化 JSON 所得到的对象

throw
使用 throw 来返回一个 Exception 对象,作为 Mock 的返回值。
当调用出错时,抛出一个默认的 RPCException:

  1. <dubbo:reference interface="com.foo.BarService" mock="throw" />

当调用出错时,抛出指定的 Exception:

  1. <dubbo:reference interface="com.foo.BarService" mock="throw com.foo.MockException" />

force和fail
可以开始在 Spring XML 配置文件中使用 fail:force: (2.6.6 以上版本)
**force**:代表强制使用 Mock 行为,在这种情况下不会走远程调用。
**fail**:与默认行为一致,只有当远程调用发生错误时才使用 Mock 行为。
force:fail: 都支持与 throw 或者 return 组合使用。
强制返回指定值:

  1. <dubbo:reference interface="com.foo.BarService" mock="force:return fake" />

强制抛出指定异常:

  1. <dubbo:reference interface="com.foo.BarService" mock="force:throw com.foo.MockException" />

在方法级别配置Mock
Mock 可以在方法级别上指定,假定 com.foo.BarService 上有好几个方法,我们可以单独为 sayHello() 方法指定 Mock 行为。具体配置如下所示,只要 sayHello() 被调用到时,强制返回 “fake”:

  1. <dubbo:reference id="demoService" check="false" interface="com.foo.BarService">
  2. <dubbo:parameter key="sayHello.mock" value="force:return fake"/>
  3. </dubbo:reference>

三、路由

1、路由规则

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

  • 条件路由。支持以服务或 Consumer 应用为粒度配置路由规则。
  • 标签路由。以 Provider 应用为粒度配置路由规则。

    条件路由

    可以在服务治理控制台 Dubbo-Admin 写入路由规则

  • 应用粒度 ```

    app1的消费者只能消费所有端口为20880的服务实例

    app2的消费者只能消费所有端口为20881的服务实例


scope: application force: true runtime: true enabled: true key: governance-conditionrouter-consumer conditions:

  • application=app1 => address=*:20880
  • application=app2 => address=*:20881 … ```
  • 服务粒度 ```

    DemoService的sayHello方法只能消费所有端口为20880的服务实例

    DemoService的sayHi方法只能消费所有端口为20881的服务实例


scope: service force: true runtime: true enabled: true key: org.apache.dubbo.samples.governance.api.DemoService conditions:

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

Conditions规则体
conditions部分是规则的主体,由1到任意多条规则组成,每个规则的配置语法做详细说明:
1)格式

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

2)表达式
参数支持:

  • 服务调用信息,如:method, argument 等,暂不支持参数路由
  • URL 本身的字段,如:protocol, host, port
  • 以及 URL 上的所有参数,如:application, organization

条件支持:

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

值支持:

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

3)Condition示例

  1. # 排除预发布机
  2. => host != 172.22.3.91
  3. # 白名单
  4. register.ip != 10.20.153.10,10.20.153.11 =>
  5. # 黑名单
  6. register.ip = 10.20.153.10,10.20.153.11 =>
  7. # 服务寄宿在应用上,只暴露一部分的机器,防止整个集群挂掉
  8. => host = 172.22.3.1*,172.22.3.2*
  9. # 为重要应用提供额外的机器
  10. application != kylin => host != 172.22.3.95,172.22.3.96
  11. # 读写分离
  12. method = find*,list*,get*,is* => host = 172.22.3.94,172.22.3.95,172.22.3.96
  13. method != find*,list*,get*,is* => host = 172.22.3.97,172.22.3.98
  14. # 前后台分离
  15. application = bops => host = 172.22.3.91,172.22.3.92,172.22.3.93
  16. application != bops => host = 172.22.3.94,172.22.3.95,172.22.3.96
  17. # 隔离不同机房网段
  18. host != 172.22.3.* => host != 172.22.3.*
  19. # 提供者与消费者部署在同集群内,本机只访问本机的服务
  20. => host = $host

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

标签路由

标签路由通过将某一个或多个服务的提供者划分到同一个分组,约束流量只在指定分组中流转,从而实现流量隔离的目的,可以作为蓝绿发布、灰度发布等场景的能力基础。
Provider
标签主要是指对Provider端应用实例的分组,目前有两种方式可以完成实例分组,分别是动态规则打标静态规则打标,其中动态规则相较于静态规则优先级更高,而当两种规则同时存在且出现冲突时,将以动态规则为准。

  • 动态规则打标,可随时在服务治理控制台下发标签归组规则 ```

    governance-tagrouter-provider应用增加了两个标签分组tag1和tag2

    tag1包含一个实例 127.0.0.1:20880

    tag2包含一个实例 127.0.0.1:20881


force: false runtime: true enabled: true key: governance-tagrouter-provider tags:

  1. - name: tag1
  2. addresses: ["127.0.0.1:20880"]
  3. - name: tag2
  4. addresses: ["127.0.0.1:20881"]

  1. - 静态打标
  2. ```xml
  3. <dubbo:provider tag="tag1"/>
  4. <!--或-->
  5. <dubbo:service tag="tag1"/>

命令形式:

  1. $ java -jar xxx-provider.jar -Ddubbo.provider.tag={the tag you want, may come from OS ENV}

Consumer
请求标签的作用域为每一次 invocation,使用 attachment 来传递请求标签,注意保存在 attachment 中的值将会在一次完整的远程调用中持续传递(RpcContext 是线程绑定的),得益于这样的特性,我们只需要在起始调用时,通过一行代码的设置,达到标签的持续传递:

  1. RpcContext.getContext().setAttachment(Constants.REQUEST_TAG_KEY,"tag1");

格式规则:

  • tags定义具体的标签分组内容,可定义任意n(n>=1)个标签并为每个标签指定实例列表。必填
    • name, 标签名称
  • addresses, 当前标签包含的实例列表

降级约定

  1. dubbo.tag=tag1 时优先选择标记了tag=tag1 的 provider。若集群中不存在与请求标记对应的服务,默认将降级请求 tag为空的provider;如果要改变这种默认行为,即找不到匹配tag1的provider返回异常,需设置dubbo.force.tag=true
  2. dubbo.tag未设置时,只会匹配tag为空的provider。即使集群中存在可用的服务,若 tag 不匹配也就无法调用,这与约定1不同,携带标签的请求可以降级访问到无标签的服务,但不携带标签/携带其他种类标签的请求永远无法访问到其他标签的服务。

    2、覆盖配置规则

    覆盖规则是 Dubbo 设计的在无需重启应用的情况下,动态调整 RPC 调用行为的一种能力。支持从服务应用两个粒度来调整动态配置(2.7.0以上版本)。

    可以在服务治理控制台 Dubbo-Admin 查看或修改覆盖规则。

  • 应用粒度 ```

    将应用demo(key:demo)在20880端口上提供(side:provider)的所有服务(scope:application)的权重修改为1000(weight:1000)。


configVersion: v2.7 scope: application key: demo enabled: true configs:

  • addresses: [“0.0.0.0:20880”] side: provider parameters: weight: 1000 … ```

  • 服务粒度 ```

    所有消费(side:consumer)DemoService服务(key:org.apache.dubbo.samples.governance.api.DemoService)的应用实例(addresses:[0.0.0.0]),超时时间修改为6000ms


configVersion: v2.7 scope: service key: org.apache.dubbo.samples.governance.api.DemoService enabled: true configs:

  • addresses: [0.0.0.0] side: consumer parameters: timeout: 6000 …
    1. 配置模板规则详解:

configVersion: v2.7 scope: application/service key: app-name/group+service+version enabled: true configs:

  • addresses: [“0.0.0.0”] providerAddresses: [“1.1.1.1:20880”, “2.2.2.2:20881”] side: consumer applications/services: [] parameters: timeout: 1000 cluster: failfase loadbalance: random
  • addresses: [“0.0.0.0:20880”] side: provider applications/services: [] parameters: threadpool: fixed threads: 200 iothreads: 4 dispatcher: all weight: 200 … ``` 其中:

  • configVersion表示 dubbo 的版本

  • scope表示配置作用范围,分别是应用(application)或服务(service)粒度。必填
  • key指定规则体作用在哪个服务或应用。必填
    • scope=service时,key取值为[{group}:]{service}[:{version}]的组合
  • scope=application时,key取值为application名称
  • enabled=true 覆盖规则是否生效,可不填,缺省生效。
  • configs定义具体的覆盖规则内容,可以指定n(n>=1)个规则体。必填
    • side
    • applications
    • services
    • parameters
    • addresses
    • providerAddresses

对于绝大多数配置场景,只需要理清楚以下问题基本就知道配置该怎么写了:

  1. 要修改整个应用的配置还是某个服务的配置。
    • 应用:scope: application, key: app-name(还可使用services指定某几个服务)。
    • 服务:scope: service, key:group+service+version
  2. 修改是作用到消费者端还是提供者端。
    • 消费者:side: consumer ,作用到消费端时(还可以进一步使用providerAddress, applications选定特定的提供者示例或应用)。
    • 提供者:side: provider
  3. 配置是否只对某几个特定实例生效。
    • 所有实例:addresses: ["0.0.0.0"]addresses: ["0.0.0.0:*"]具体由side值决定。
    • 指定实例:addersses[实例地址列表]
  4. 要修改的属性是哪个。

    示例

    1)禁用提供者:(通常用于临时踢除某台提供者机器,相似的,禁止消费者访问请使用路由规则) ```

configVersion: v2.7 scope: application key: demo-provider enabled: true configs:

  • addresses: [“10.20.153.10:20880”] side: provider parameters: disabled: true …
    1. 2)调整权重:(通常用于容量评估,缺省权重为 200)

configVersion: v2.7 scope: application key: demo-provider enabled: true configs:

  • addresses: [“10.20.153.10:20880”] side: provider parameters: weight: 200 …
    1. 3)调整负载均衡策略:(缺省负载均衡策略为 `random`)

configVersion: v2.7 scope: application key: demo-consumer enabled: true configs:

  • side: consumer parameters: loadbalance: random …
    1. 4)服务降级:(通常用于临时屏蔽某个出错的非关键服务)

configVersion: v2.7 scope: service key: org.apache.dubbo.samples.governance.api.DemoService enabled: true configs:

  • side: consumer parameters: force: return null … ```

    四、异步

    1、异步执行

    Provider端异步执行将阻塞的业务从Dubbo内部线程池切换到业务自定义线程,避免Dubbo线程池的过度占用,有助于避免不同服务间的互相影响。异步执行无益于节省资源或提升RPC响应性能,因为如果业务执行需要阻塞,则始终还是要有线程来负责执行。

    注意:Provider 端异步执行和 Consumer 端异步调用是相互独立的,可以分别设置。

服务接口定义:

  1. public interface AsyncService {
  2. CompletableFuture<String> sayHello(String name);
  3. }

服务实现:

  1. public class AsyncServiceImpl implements AsyncService {
  2. @Override
  3. public CompletableFuture<String> sayHello(String name) {
  4. RpcContext savedContext = RpcContext.getContext();
  5. // 建议为supplyAsync提供自定义线程池,避免使用JDK公用线程池
  6. return CompletableFuture.supplyAsync(() -> {
  7. System.out.println(savedContext.getAttachment("consumer-key1"));
  8. try {
  9. Thread.sleep(5000);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. return "async response from provider.";
  14. });
  15. }
  16. }

通过 return CompletableFuture.supplyAsync(),业务执行已从 Dubbo 线程切换到业务线程,避免了对 Dubbo 线程池的阻塞。

  • 使用AsyncContext

Dubbo 提供了一个类似 Serverlet 3.0 的异步接口AsyncContext,在没有 CompletableFuture 签名接口的情况下,也可以实现 Provider 端的异步执行。
服务接口定义:

  1. public interface AsyncService {
  2. String sayHello(String name);
  3. }

服务暴露,和普通服务完全一致:

  1. <bean id="asyncService" class="org.apache.dubbo.samples.governance.impl.AsyncServiceImpl"/>
  2. <dubbo:service interface="org.apache.dubbo.samples.governance.api.AsyncService" ref="asyncService"/>

服务实现:

  1. public class AsyncServiceImpl implements AsyncService {
  2. public String sayHello(String name) {
  3. final AsyncContext asyncContext = RpcContext.startAsync();
  4. new Thread(() -> {
  5. // 如果要使用上下文,则必须要放在第一句执行
  6. asyncContext.signalContextSwitch();
  7. try {
  8. Thread.sleep(500);
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. // 写回响应
  13. asyncContext.write("Hello " + name + ", response from provider.");
  14. }).start();
  15. return null;
  16. }
  17. }

2、异步调用

从 2.7.0 开始,Dubbo 的所有异步编程接口开始以 CompletableFuture 为基础。
基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小。
image.png

  • 使用 CompletableFuture 签名的接口

需要服务提供者事先定义 CompletableFuture 签名的服务接口:

  1. public interface AsyncService {
  2. CompletableFuture<String> sayHello(String name);
  3. }

XML引用服务:

  1. <dubbo:reference id="asyncService" timeout="10000" interface="com.alibaba.dubbo.samples.async.api.AsyncService"/>

调用远程服务:

  1. // 调用直接返回CompletableFuture
  2. CompletableFuture<String> future = asyncService.sayHello("async call request");
  3. // 增加回调
  4. future.whenComplete((v, t) -> {
  5. if (t != null) {
  6. t.printStackTrace();
  7. } else {
  8. System.out.println("Response: " + v);
  9. }
  10. });
  11. // 早于结果输出
  12. System.out.println("Executed before response return.");
  • 使用 RpcContext

在 consumer.xml 中配置:

  1. <dubbo:reference id="asyncService" interface="org.apache.dubbo.samples.governance.api.AsyncService">
  2. <dubbo:method name="sayHello" async="true" />
  3. </dubbo:reference>

调用代码:

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

也可以这样做异步调用:

  1. CompletableFuture<String> future = RpcContext.getContext().asyncCall(
  2. () -> {
  3. asyncService.sayHello("oneway call request1");
  4. }
  5. );
  6. future.get();
  • 重载服务接口

如果只有这样的同步服务定义:

  1. public interface GreetingsService {
  2. String sayHi(String name);
  3. }

也可以不用RpcContext实现异步。可以利用 Java 8 提供的 default 接口实现,重载一个带有 CompletableFuture 签名的方法。

CompletableFuture 签名的方法目前只支持 Dubbo 协议,其他协议由于第三方实现问题,需要视具体情况而定。

有两种方式来实现:
1)提供方或消费方自己修改接口签名

  1. public interface GreetingsService {
  2. String sayHi(String name);
  3. // AsyncSignal is totally optional, you can use any parameter type as long as java allows your to do that.
  4. default CompletableFuture<String> sayHi(String name, AsyncSignal signal) {
  5. return CompletableFuture.completedFuture(sayHi(name));
  6. }
  7. }

Dubbo 官方提供 compiler hacker,编译期自动重写同步方法。
可以设置是否等待消息发出 (异步总是不等待返回):

  • sent="true" 等待消息发出,消息发送失败将抛出异常。
  • sent="false" 不等待消息发出,将消息放入 IO 队列,即刻返回。

    1. <dubbo:method name="findFoo" async="true" sent="true" />

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

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

    五、缓存

    1、结果缓存

    用于加速热门数据的访问速度,Dubbo 提供声明式缓存,以减少用户加缓存的工作量(2.1.0 以上版本)。
    缓存类型

  • lru 缓存淘汰算法,基于最近最少使用原则删除多余缓存,保持最热的数据被缓存。

  • threadlocal 当前线程缓存,比如一个页面渲染,用到很多 portal,每个 portal 都要去查用户信息,通过线程缓存,可以减少这种多余访问。
  • jcacheJSR107 集成,可以桥接各种缓存实现。

    1. <dubbo:reference interface="com.foo.BarService" cache="lru" />
    2. <!--或-->
    3. <dubbo:reference interface="com.foo.BarService">
    4. <dubbo:method name="findBar" cache="lru" />
    5. </dubbo:reference>

    2、本地存根

    在 Dubbo 中利用本地存根在客户端执行部分逻辑。
    远程服务后,客户端通常只剩下接口,而实现全在服务器端,但提供方有些时候想在客户端也执行部分逻辑,比如:做 ThreadLocal 缓存,提前验证参数,调用失败后伪造容错数据等等,此时就需要在 API 中带上 Stub,客户端生成 Proxy 实例,会把 Proxy 通过构造函数传给 Stub (Stub 必须有可传入 Proxy 的构造函数),然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy。
    image.png
    1)在 spring 配置文件中按以下方式配置:

    1. <dubbo:service interface="com.foo.BarService" stub="true" />
    2. <!--或-->
    3. <dubbo:service interface="com.foo.BarService" stub="com.foo.BarServiceStub" />

    2)提供 Stub 的实现(在 interface 旁边放一个 Stub 实现,它实现 BarService 接口,并有一个传入远程 BarService 实例的构造函数)

    1. package com.foo;
    2. public class BarServiceStub implements BarService {
    3. private final BarService barService;
    4. // 构造函数传入真正的远程代理对象
    5. public BarServiceStub(BarService barService){
    6. this.barService = barService;
    7. }
    8. public String sayHello(String name) {
    9. // 此代码在客户端执行, 你可以在客户端做ThreadLocal本地缓存,或预先验证参数是否合法,等等
    10. try {
    11. return barService.sayHello(name);
    12. } catch (Exception e) {
    13. // 你可以容错,可以做任何AOP拦截事项
    14. return "容错数据";
    15. }
    16. }
    17. }

    六、连接控制

    1、连接控制

  • 服务端连接控制

限制服务器端接受的连接不能超过 10 个(因为连接在 Server上,所以配置Provider):

  1. <dubbo:provider protocol="dubbo" accepts="10" />
  2. <!--或-->
  3. <dubbo:protocol name="dubbo" accepts="10" />
  • 客户端连接控制

限制客户端服务使用连接不能超过 10 个(如果是长连接,比如 Dubbo 协议,connections 表示该服务对每个提供者建立的长连接数):

  1. <dubbo:reference interface="com.foo.BarService" connections="10" />
  2. <!--或-->
  3. <dubbo:service interface="com.foo.BarService" connections="10" />

如果 <dubbo:service><dubbo:reference> 都配了 connections,<dubbo:reference> 优先

2、并发控制

配置案例
1)限制 com.foo.BarService 的每个方法,服务器端并发执行(或占用线程池线程数)不能超过 10 个:

  1. <dubbo:service interface="com.foo.BarService" executes="10" />

2)限制 com.foo.BarServicesayHello 方法,服务器端并发执行(或占用线程池线程数)不能超过 10 个:

  1. <dubbo:service interface="com.foo.BarService">
  2. <dubbo:method name="sayHello" executes="10" />
  3. </dubbo:service>

3)限制 com.foo.BarService 的每个方法,每客户端并发执行(或占用连接的请求数)不能超过 10 个:

  1. <dubbo:service interface="com.foo.BarService" actives="10" />
  2. <!--或-->
  3. <dubbo:reference interface="com.foo.BarService" actives="10" />

4)限制 com.foo.BarServicesayHello 方法,每客户端并发执行(或占用连接的请求数)不能超过 10 个:

  1. <dubbo:service interface="com.foo.BarService">
  2. <dubbo:method name="sayHello" actives="10" />
  3. </dubbo:service>
  4. <!--或-->
  5. <dubbo:reference interface="com.foo.BarService">
  6. <dubbo:method name="sayHello" actives="10" />
  7. </dubbo:reference>

如果 <dubbo:service><dubbo:reference> 都配了actives,<dubbo:reference> 优先

Load Balance均衡
配置服务的客户端的 loadbalance 属性为 leastactive,此 Loadbalance 会调用并发数最小的 Provider(Consumer端并发数)。

  1. <dubbo:reference interface="com.foo.BarService" loadbalance="leastactive" />
  2. <!--或-->
  3. <dubbo:service interface="com.foo.BarService" loadbalance="leastactive" />

3、延迟连接

延迟连接用于减少长连接数。当有调用发起时,再创建长连接。

  1. <dubbo:protocol name="dubbo" lazy="true" />

4、粘滞连接

粘滞连接用于有状态服务,尽可能让客户端总是向同一提供者发起调用,除非该提供者挂了,再连另一台。
粘滞连接将自动开启延迟连接,以减少长连接数。

  1. <dubbo:reference id="xxxService" interface="com.xxx.XxxService" sticky="true" />

Dubbo 支持方法级别的粘滞连接,如果想进行更细粒度的控制,还可以这样配置:

  1. <dubbo:reference id="xxxService" interface="com.xxx.XxxService">
  2. <dubbo:mothod name="sayHello" sticky="true" />
  3. </dubbo:reference>

5、连接安全

在2.7.5 以上版本,dubbo对内置的 Dubbo Netty Server 和新引入的 gRPC 协议都提供了基于 TLS 的安全链路传输机制。
TLS 的配置都有统一的入口,如下所示:
provider端:

  1. SslConfig sslConfig = new SslConfig();
  2. sslConfig.setServerKeyCertChainPath("path to cert");
  3. sslConfig.setServerPrivateKeyPath(args[1]);
  4. // 如果开启双向 cert 认证
  5. if (mutualTls) {
  6. sslConfig.setServerTrustCertCollectionPath(args[2]);
  7. }
  8. ProtocolConfig protocolConfig = new ProtocolConfig("dubbo/grpc");
  9. protocolConfig.setSslEnabled(true);

Consumer 端 :
  1. if (!mutualTls) {}
  2. sslConfig.setClientTrustCertCollectionPath(args[0]);
  3. } else {
  4. sslConfig.setClientTrustCertCollectionPath(args[0]);
  5. sslConfig.setClientKeyCertChainPath(args[1]);
  6. sslConfig.setClientPrivateKeyPath(args[2]);
  7. }

6、连接校验

通过令牌验证在注册中心控制权限,以决定要不要下发令牌给消费者,可以防止消费者绕过注册中心访问提供者,另外通过注册中心可灵活改变授权方式,而不需修改或升级提供者。
image.png
可以全局设置开启令牌验证:

  1. <!--随机token令牌,使用UUID生成-->
  2. <dubbo:provider token="true" />

或:

  1. <!--固定token令牌,相当于密码-->
  2. <dubbo:provider token="123456" />

也可在服务级别设置:

  1. <!--随机token令牌,使用UUID生成-->
  2. <dubbo:service interface="com.foo.BarService" token="true" />

或:

  1. <!--固定token令牌,相当于密码-->
  2. <dubbo:service interface="com.foo.BarService" token="123456" />

七、实践总结

1、配置

1)在 Provider 端尽量多配置 Consumer 端属性

原因如下:

  • 作为服务的提供方,比服务消费方更清楚服务的性能参数,如调用的超时时间、合理的重试次数等
  • 在 Provider 端配置后,Consumer 端不配置则会使用 Provider 端的配置,即 Provider 端的配置可以作为 Consumer 的缺省值。否则,Consumer 会使用 Consumer 端的全局设置,这对于 Provider 是不可控的,并且往往是不合理的。

    配置的覆盖规则:1) 方法级别配置优于接口级别,即小 Scope 优先 2) Consumer 端配置优于 Provider 端配置,优于全局配置,最后是 Dubbo 硬编码的配置值

Provider 端尽量多配置 Consumer 端的属性,让 Provider 的实现者一开始就思考 Provider 端的服务特点和服务质量等问题。
示例:

  1. <dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService"
  2. timeout="300" retries="2" loadbalance="random" actives="0" />
  3. <dubbo:service interface="com.alibaba.hello.api.WorldService" version="1.0.0" ref="helloService"
  4. timeout="300" retries="2" loadbalance="random" actives="0" >
  5. <dubbo:method name="findAllPerson" timeout="10000" retries="9" loadbalance="leastactive" actives="5" />
  6. <dubbo:service/>

建议在 Provider 端配置的 Consumer 端属性有:

  1. timeout:方法调用的超时时间。
  2. retries:失败重试次数,缺省是 2。``
  3. loadbalance:负载均衡算法,缺省是随机 random。还可以配置轮询 roundrobin、最不活跃优先 leastactive 和一致性哈希 consistenthash 等。

    leastactive指从 Consumer 端并发调用最好的 Provider,可以减少对响应慢的 Provider 的调用,因为响应慢更容易累积并发调用。

  4. actives:消费者端的最大并发调用限制,即当 Consumer 对一个服务的并发调用到上限后,新调用会阻塞直到超时,在方法上配置 dubbo:method 则针对该方法进行并发限制,在接口上配置 dubbo:service,则针对该服务进行并发限制。

2)在 Provider 端配置合理的 Provider 端属性

  1. <dubbo:protocol threads="200" />
  2. <dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService"
  3. executes="200" >
  4. <dubbo:method name="findAllPerson" executes="50" />
  5. </dubbo:service>

建议在 Provider 端配置的 Provider 端属性有:

  1. threads:服务线程池大小。
  2. executes:一个服务提供者并行执行请求上限,即当 Provider 对一个服务的并发调用达到上限后,新调用会阻塞,此时 Consumer 可能会超时。在方法上配置 dubbo:method 则针对该方法进行并发限制,在接口上配置 dubbo:service,则针对该服务进行并发限制。

    3)配置管理信息

    目前有负责人信息和组织信息用于区分站点。以便于在发现问题时找到服务对应负责人,建议至少配置两个人以便备份。负责人和组织信息可以在运维平台 (Dubbo Ops) 上看到。
    在应用层面配置负责人、组织信息:

    1. <dubbo:application owner=”ding.lid,william.liangf” organization=”intl” />

    在服务层面(服务端)配置负责人:

    1. <dubbo:service owner=”ding.lid,william.liangf” />

    在服务层面(消费端)配置负责人:

    1. <dubbo:reference owner=”ding.lid,william.liangf” />

    若没有配置服务层面的负责人,则默认使用 dubbo:application 设置的负责人。

    4)配置 Dubbo 缓存文件

    提供者列表缓存文件:

    1. <dubbo:registry file=”${user.home}/output/dubbo.cache” />

    注意:

  3. 可以根据需要调整缓存文件的路径,保证这个文件不会在发布过程中被清除;

  4. 如果有多个应用进程,请注意不要使用同一个文件,避免内容被覆盖;

该文件会缓存注册中心列表和服务提供者列表。配置缓存文件后,应用重启过程中,若注册中心不可用,应用会从该缓存文件读取服务提供者列表,进一步保证应用可靠性。

5)监控配置

  1. 使用固定端口暴露服务,而不要使用随机端口这样在注册中心推送有延迟的情况下,消费者通过缓存列表也能调用到原地址,保证调用成功。
  2. 使用 Dubbo Admin 监控注册中心上的服务提供方使用 Dubbo Admin 监控服务在注册中心上的状态,确保注册中心上有该服务的存在。
  3. 服务提供方可使用 Dubbo Qos 的 telnet 或 shell 监控项监控服务提供者端口状态:echo status | nc -i 1 20880 | grep OK | wc -l,其中的 20880 为服务端口。
  4. 服务消费方可通过将服务强制转型为 EchoService,并调用 $echo() 测试该服务的提供者是可用如 assertEqauls(“OK”, ((EchoService)memberService).$echo(“OK”))

    6)不要使用 dubbo.properties 文件配置,推荐使用对应 XML 配置

    Dubbo 中所有的配置项都可以配置在 Spring 配置文件中,并且可以针对单个服务配置。
    dubbo.properties 中属性名与 XML 的对应关系
  • 应用名 dubbo.application.name

    1. <dubbo:application name="myalibaba" >
  • 注册中心地址 dubbo.registry.address

    1. <dubbo:registry address="11.22.33.44:9090" >
  • 调用超时 dubbo.service.*.timeout

可以在多个配置项设置超时 timeout,由上至下覆盖(即上面的优先)``,其它的参数(retries、loadbalance、actives等)的覆盖策略与 timeout 相同。示例如下:

  • 提供者端特定方法的配置

    1. <dubbo:service interface="com.alibaba.xxx.XxxService" >
    2. <dubbo:method name="findPerson" timeout="1000" />
    3. </dubbo:service>
  • 提供者端特定接口的配置

    1. <dubbo:service interface="com.alibaba.xxx.XxxService" timeout="200" />
  • 服务提供者协议 dubbo.service.protocol、服务的监听端口 dubbo.service.server.port

    1. <dubbo:protocol name="dubbo" port="20880" />
  • 服务线程池大小 dubbo.service.max.thread.threads.size

    1. <dubbo:protocol threads="100" />
  • 6.消费者启动时,没有提供者是否抛异常 alibaba.intl.commons.dubbo.service.allow.no.provider

    1. <dubbo:reference interface="com.alibaba.xxx.XxxService" check="false" />

    2、约定

    1)分包

    建议将服务接口、服务模型、服务异常等均放在 API 包中,因为服务模型和异常也是 API 的一部分,这样做也符合分包原则:重用发布等价原则(REP),共同重用原则(CRP)。
    如果需要,也可以考虑在 API 包中放置一份 Spring 的引用配置,这样使用方只需在 Spring 加载过程中引用此配置即可。配置建议放在模块的包目录下,以免冲突,如:com/alibaba/china/xxx/dubbo-reference.xml

    2)粒度

    服务接口尽可能大粒度,每个服务方法应代表一个功能,而不是某功能的一个步骤,否则将面临分布式事务问题,Dubbo 暂未提供分布式事务支持
    服务接口建议以业务场景为单位划分,并对相近业务做抽象,防止接口数量爆炸。
    不建议使用过于抽象的通用接口,如:Map query(Map),这样的接口没有明确语义,会给后期维护带来不便。

    3)版本

    每个接口都应定义版本号,为后续不兼容升级提供可能,如: <dubbo:service interface="com.xxx.XxxService" version="1.0" />
    建议使用两位版本号,因为第三位版本号通常表示兼容升级,只有不兼容时才需要变更服务版本。
    当不兼容时,先升级一半提供者为新版本,再将消费者全部升为新版本,然后将剩下的一半提供者升为新版本。


参考资料: Dubbo 官方文档