- 一、负载均衡
- 二、服务降级
- 三、路由
- app1的消费者只能消费所有端口为20880的服务实例
- app2的消费者只能消费所有端口为20881的服务实例
- DemoService的sayHello方法只能消费所有端口为20880的服务实例
- DemoService的sayHi方法只能消费所有端口为20881的服务实例
- governance-tagrouter-provider应用增加了两个标签分组tag1和tag2
- tag1包含一个实例 127.0.0.1:20880
- tag2包含一个实例 127.0.0.1:20881
- 将应用demo(key:demo)在20880端口上提供(side:provider)的所有服务(scope:application)的权重修改为1000(weight:1000)。
- 所有消费(side:consumer)DemoService服务(key:org.apache.dubbo.samples.governance.api.DemoService)的应用实例(addresses:[0.0.0.0]),超时时间修改为6000ms
- 四、异步
- 五、缓存
- 六、连接控制
- 6、连接校验
- 七、实践总结
一、负载均衡
1、集群容错
微服务项目中同一个服务可集群配置多个节点。dubbo的集群节点结构图:
各节点关系:
- 这里的
Invoker
是Provider
的一个可调用Service
的抽象,Invoker
封装了Provider
地址及Service
接口信息。 Directory
代表多个Invoker
,可以把它看成List<Invoker>
,但与List
不同的是,它的值可能是动态变化的,比如注册中心推送变更。Cluster
将Directory
中的多个Invoker
伪装成一个Invoker
,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个。Router
负责从多个Invoker
中按路由规则选出子集,比如读写分离,应用隔离等。LoadBalance
负责从多个Invoker
中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选。
dubbo 提供了多种集群服务调用容错方案:
- Failfast Cluster(默认):快速失败,只发起一次调用,失败立即报错。通常用于非幂等性(幂等:调用结果不随调用次数变化)的写操作,比如新增记录。
默认的容错策略**Failover Cluster**
会在失败时自动切换,出现失败会直接重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 **retries="n"**
来设置重试次数(不含第一次)。
<dubbo:service retries="2" />
<!--或-->
<dubbo:reference retries="2" />
<!--或-->
<dubbo:reference>
<dubbo:method name="findFoo" retries="2" />
</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 及以上版本)
// 当 20% 的节点调用失败就抛出异常,不再调用其他节点。
@reference(cluster = "broadcast", parameters = {"broadcast.fail.percent", "20"})
集群模式配置
按照以下示例在服务提供方和消费方配置集群容错模式:
<dubbo:service cluster="failsafe" />
<!--或-->
<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" />
配置方式:
<!--服务端服务级别-->
<dubbo:service interface="..." loadbalance="roundrobin" />
<!--服务端方法级别-->
<dubbo:service interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:service>
<!--客户端服务级别-->
<dubbo:reference interface="..." loadbalance="roundrobin" />
<!--客户端方法级别-->
<dubbo:reference interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:reference>
3、重试配置
Dubbo 服务在尝试调用一次之后,如出现非业务异常(服务突然不可用、超时等),默认会进行额外的最多2次重试。
重试次数支持两种自定义配置:
通过注解/xml进行固定配置
<dubbo:consumer retries="2"></dubbo:consumer>
通过RpcContext进行运行时动态配置,优先级高于注解/xml进行的固定配置(两者都配置的情况下,以RpcContext配置为准)。
// dubbo服务调用前,通过RpcContext动态设置本次调用的重试次数
RpcContext rpcContext = RpcContext.getContext();
rpcContext.setAttachment("retries", 5);
二、服务降级
1、降级处理
可以通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略。
向注册中心写入动态配置覆盖规则: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
表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。- 还可以改为
mock=fail:return+null
表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。2、服务伪装
本地伪装通常用于服务降级,比如某验权服务,当服务提供方全部挂掉后,客户端不抛出异常,而是通过 Mock 数据返回授权失败。
在 spring 配置文件中按以下方式配置:
然后在工程中提供 Mock 实现:<dubbo:reference interface="com.foo.BarService" mock="true" />
<!--或-->
<dubbo:reference interface="com.foo.BarService" mock="com.foo.BarServiceMock" />
如果服务的消费方经常需要 try-catch 捕获异常,如:package com.foo;
public class BarServiceMock implements BarService {
public String sayHello(String name) {
// 可以伪造容错数据,此方法只在出现RpcException时被执行
return "容错数据";
}
}
请考虑改为 Mock 实现,并在 Mock 实现中 return null。如果只是想简单的忽略异常可用(2.0.11 以上版本):Offer offer = null;
try {
offer = offerService.findOffer(offerId);
} catch (RpcException e) {
logger.error(e);
}
<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:
<dubbo:reference interface="com.foo.BarService" mock="throw" />
当调用出错时,抛出指定的 Exception:
<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
组合使用。
强制返回指定值:
<dubbo:reference interface="com.foo.BarService" mock="force:return fake" />
强制抛出指定异常:
<dubbo:reference interface="com.foo.BarService" mock="force:throw com.foo.MockException" />
在方法级别配置Mock
Mock 可以在方法级别上指定,假定 com.foo.BarService
上有好几个方法,我们可以单独为 sayHello()
方法指定 Mock 行为。具体配置如下所示,只要 sayHello() 被调用到时,强制返回 “fake”:
<dubbo:reference id="demoService" check="false" interface="com.foo.BarService">
<dubbo:parameter key="sayHello.mock" value="force:return fake"/>
</dubbo:reference>
三、路由
1、路由规则
路由规则在发起一次RPC调用前起到过滤目标服务器地址的作用,过滤后的地址列表,将作为消费端最终发起RPC调用的备选地址。
- 条件路由。支持以服务或 Consumer 应用为粒度配置路由规则。
-
条件路由
可以在服务治理控制台 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 … ```
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示例
# 排除预发布机
=> host != 172.22.3.91
# 白名单
register.ip != 10.20.153.10,10.20.153.11 =>
# 黑名单
register.ip = 10.20.153.10,10.20.153.11 =>
# 服务寄宿在应用上,只暴露一部分的机器,防止整个集群挂掉
=> host = 172.22.3.1*,172.22.3.2*
# 为重要应用提供额外的机器
application != kylin => host != 172.22.3.95,172.22.3.96
# 读写分离
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
# 前后台分离
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
# 隔离不同机房网段
host != 172.22.3.* => host != 172.22.3.*
# 提供者与消费者部署在同集群内,本机只访问本机的服务
=> 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:
- name: tag1
addresses: ["127.0.0.1:20880"]
- name: tag2
addresses: ["127.0.0.1:20881"]
…
- 静态打标
```xml
<dubbo:provider tag="tag1"/>
<!--或-->
<dubbo:service tag="tag1"/>
命令形式:
$ java -jar xxx-provider.jar -Ddubbo.provider.tag={the tag you want, may come from OS ENV}
Consumer
请求标签的作用域为每一次 invocation,使用 attachment 来传递请求标签,注意保存在 attachment 中的值将会在一次完整的远程调用中持续传递(RpcContext 是线程绑定的),得益于这样的特性,我们只需要在起始调用时,通过一行代码的设置,达到标签的持续传递:
RpcContext.getContext().setAttachment(Constants.REQUEST_TAG_KEY,"tag1");
格式规则:
- tags定义具体的标签分组内容,可定义任意n(n>=1)个标签并为每个标签指定实例列表。必填。
- name, 标签名称
- addresses, 当前标签包含的实例列表
降级约定
dubbo.tag=tag1
时优先选择标记了tag=tag1
的 provider。若集群中不存在与请求标记对应的服务,默认将降级请求 tag为空的provider;如果要改变这种默认行为,即找不到匹配tag1的provider返回异常,需设置dubbo.force.tag=true
。dubbo.tag
未设置时,只会匹配tag为空的provider。即使集群中存在可用的服务,若 tag 不匹配也就无法调用,这与约定1不同,携带标签的请求可以降级访问到无标签的服务,但不携带标签/携带其他种类标签的请求永远无法访问到其他标签的服务。2、覆盖配置规则
覆盖规则是 Dubbo 设计的在无需重启应用的情况下,动态调整 RPC 调用行为的一种能力。支持从服务和应用两个粒度来调整动态配置(2.7.0以上版本)。可以在服务治理控制台 Dubbo-Admin 查看或修改覆盖规则。
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
…
配置模板规则详解:
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
对于绝大多数配置场景,只需要理清楚以下问题基本就知道配置该怎么写了:
- 要修改整个应用的配置还是某个服务的配置。
- 应用:
scope: application
,key: app-name
(还可使用services指定某几个服务)。 - 服务:
scope: service
,key:group+service+version
。
- 应用:
- 修改是作用到消费者端还是提供者端。
- 消费者:
side: consumer
,作用到消费端时(还可以进一步使用providerAddress, applications选定特定的提供者示例或应用)。 - 提供者:
side: provider
。
- 消费者:
- 配置是否只对某几个特定实例生效。
- 所有实例:
addresses: ["0.0.0.0"]
或addresses: ["0.0.0.0:*"]
具体由side值决定。 - 指定实例:
addersses[实例地址列表]
。
- 所有实例:
- 要修改的属性是哪个。
示例
1)禁用提供者:(通常用于临时踢除某台提供者机器,相似的,禁止消费者访问请使用路由规则) ```
configVersion: v2.7 scope: application key: demo-provider enabled: true configs:
- addresses: [“10.20.153.10:20880”]
side: provider
parameters:
disabled: true
…
2)调整权重:(通常用于容量评估,缺省权重为 200)
configVersion: v2.7 scope: application key: demo-provider enabled: true configs:
- addresses: [“10.20.153.10:20880”]
side: provider
parameters:
weight: 200
…
3)调整负载均衡策略:(缺省负载均衡策略为 `random`)
configVersion: v2.7 scope: application key: demo-consumer enabled: true configs:
- side: consumer
parameters:
loadbalance: random
…
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 端异步调用是相互独立的,可以分别设置。
- 定义 CompletableFuture(异步任务执行对象,详解:使用CompletableFuture) 签名的接口。
服务接口定义:
public interface AsyncService {
CompletableFuture<String> sayHello(String name);
}
服务实现:
public class AsyncServiceImpl implements AsyncService {
@Override
public CompletableFuture<String> sayHello(String name) {
RpcContext savedContext = RpcContext.getContext();
// 建议为supplyAsync提供自定义线程池,避免使用JDK公用线程池
return CompletableFuture.supplyAsync(() -> {
System.out.println(savedContext.getAttachment("consumer-key1"));
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "async response from provider.";
});
}
}
通过 return CompletableFuture.supplyAsync()
,业务执行已从 Dubbo 线程切换到业务线程,避免了对 Dubbo 线程池的阻塞。
- 使用AsyncContext
Dubbo 提供了一个类似 Serverlet 3.0 的异步接口AsyncContext,在没有 CompletableFuture 签名接口的情况下,也可以实现 Provider 端的异步执行。
服务接口定义:
public interface AsyncService {
String sayHello(String name);
}
服务暴露,和普通服务完全一致:
<bean id="asyncService" class="org.apache.dubbo.samples.governance.impl.AsyncServiceImpl"/>
<dubbo:service interface="org.apache.dubbo.samples.governance.api.AsyncService" ref="asyncService"/>
服务实现:
public class AsyncServiceImpl implements AsyncService {
public String sayHello(String name) {
final AsyncContext asyncContext = RpcContext.startAsync();
new Thread(() -> {
// 如果要使用上下文,则必须要放在第一句执行
asyncContext.signalContextSwitch();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 写回响应
asyncContext.write("Hello " + name + ", response from provider.");
}).start();
return null;
}
}
2、异步调用
从 2.7.0 开始,Dubbo 的所有异步编程接口开始以 CompletableFuture 为基础。
基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小。
- 使用 CompletableFuture 签名的接口
需要服务提供者事先定义 CompletableFuture 签名的服务接口:
public interface AsyncService {
CompletableFuture<String> sayHello(String name);
}
XML引用服务:
<dubbo:reference id="asyncService" timeout="10000" interface="com.alibaba.dubbo.samples.async.api.AsyncService"/>
调用远程服务:
// 调用直接返回CompletableFuture
CompletableFuture<String> future = asyncService.sayHello("async call request");
// 增加回调
future.whenComplete((v, t) -> {
if (t != null) {
t.printStackTrace();
} else {
System.out.println("Response: " + v);
}
});
// 早于结果输出
System.out.println("Executed before response return.");
- 使用 RpcContext
在 consumer.xml 中配置:
<dubbo:reference id="asyncService" interface="org.apache.dubbo.samples.governance.api.AsyncService">
<dubbo:method name="sayHello" async="true" />
</dubbo:reference>
调用代码:
// 此调用会立即返回null
asyncService.sayHello("world");
// 拿到调用的Future引用,当结果返回后,会被通知和设置到此Future
CompletableFuture<String> helloFuture = RpcContext.getContext().getCompletableFuture();
// 为Future添加回调
helloFuture.whenComplete((retValue, exception) -> {
if (exception == null) {
System.out.println(retValue);
} else {
exception.printStackTrace();
}
});
也可以这样做异步调用:
CompletableFuture<String> future = RpcContext.getContext().asyncCall(
() -> {
asyncService.sayHello("oneway call request1");
}
);
future.get();
- 重载服务接口
如果只有这样的同步服务定义:
public interface GreetingsService {
String sayHi(String name);
}
也可以不用RpcContext实现异步。可以利用 Java 8 提供的 default 接口实现,重载一个带有 CompletableFuture 签名的方法。
CompletableFuture 签名的方法目前只支持 Dubbo 协议,其他协议由于第三方实现问题,需要视具体情况而定。
有两种方式来实现:
1)提供方或消费方自己修改接口签名
public interface GreetingsService {
String sayHi(String name);
// AsyncSignal is totally optional, you can use any parameter type as long as java allows your to do that.
default CompletableFuture<String> sayHi(String name, AsyncSignal signal) {
return CompletableFuture.completedFuture(sayHi(name));
}
}
Dubbo 官方提供 compiler hacker,编译期自动重写同步方法。
可以设置是否等待消息发出 (异步总是不等待返回):
sent="true"
等待消息发出,消息发送失败将抛出异常。sent="false"
不等待消息发出,将消息放入 IO 队列,即刻返回。<dubbo:method name="findFoo" async="true" sent="true" />
2)如果只是想异步,完全忽略返回值,可以配置
return="false"
,以减少 Future 对象的创建和管理成本:<dubbo:method name="findFoo" async="true" return="false" />
五、缓存
1、结果缓存
用于加速热门数据的访问速度,Dubbo 提供声明式缓存,以减少用户加缓存的工作量(2.1.0 以上版本)。
缓存类型:lru
缓存淘汰算法,基于最近最少使用原则删除多余缓存,保持最热的数据被缓存。threadlocal
当前线程缓存,比如一个页面渲染,用到很多 portal,每个 portal 都要去查用户信息,通过线程缓存,可以减少这种多余访问。jcache
与 JSR107 集成,可以桥接各种缓存实现。<dubbo:reference interface="com.foo.BarService" cache="lru" />
<!--或-->
<dubbo:reference interface="com.foo.BarService">
<dubbo:method name="findBar" cache="lru" />
</dubbo:reference>
2、本地存根
在 Dubbo 中利用本地存根在客户端执行部分逻辑。
远程服务后,客户端通常只剩下接口,而实现全在服务器端,但提供方有些时候想在客户端也执行部分逻辑,比如:做 ThreadLocal 缓存,提前验证参数,调用失败后伪造容错数据等等,此时就需要在 API 中带上 Stub,客户端生成 Proxy 实例,会把 Proxy 通过构造函数传给 Stub (Stub 必须有可传入 Proxy 的构造函数),然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy。
1)在 spring 配置文件中按以下方式配置:<dubbo:service interface="com.foo.BarService" stub="true" />
<!--或-->
<dubbo:service interface="com.foo.BarService" stub="com.foo.BarServiceStub" />
2)提供 Stub 的实现(在 interface 旁边放一个 Stub 实现,它实现 BarService 接口,并有一个传入远程 BarService 实例的构造函数)
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 "容错数据";
}
}
}
六、连接控制
1、连接控制
服务端连接控制
限制服务器端接受的连接不能超过 10 个(因为连接在 Server上,所以配置Provider):
<dubbo:provider protocol="dubbo" accepts="10" />
<!--或-->
<dubbo:protocol name="dubbo" accepts="10" />
- 客户端连接控制
限制客户端服务使用连接不能超过 10 个(如果是长连接,比如 Dubbo 协议,connections 表示该服务对每个提供者建立的长连接数):
<dubbo:reference interface="com.foo.BarService" connections="10" />
<!--或-->
<dubbo:service interface="com.foo.BarService" connections="10" />
如果
<dubbo:service>
和<dubbo:reference>
都配了 connections,<dubbo:reference>
优先
2、并发控制
配置案例
1)限制 com.foo.BarService
的每个方法,服务器端并发执行(或占用线程池线程数)不能超过 10 个:
<dubbo:service interface="com.foo.BarService" executes="10" />
2)限制 com.foo.BarService
的 sayHello
方法,服务器端并发执行(或占用线程池线程数)不能超过 10 个:
<dubbo:service interface="com.foo.BarService">
<dubbo:method name="sayHello" executes="10" />
</dubbo:service>
3)限制 com.foo.BarService
的每个方法,每客户端并发执行(或占用连接的请求数)不能超过 10 个:
<dubbo:service interface="com.foo.BarService" actives="10" />
<!--或-->
<dubbo:reference interface="com.foo.BarService" actives="10" />
4)限制 com.foo.BarService
的 sayHello
方法,每客户端并发执行(或占用连接的请求数)不能超过 10 个:
<dubbo:service interface="com.foo.BarService">
<dubbo:method name="sayHello" actives="10" />
</dubbo:service>
<!--或-->
<dubbo:reference interface="com.foo.BarService">
<dubbo:method name="sayHello" actives="10" />
</dubbo:reference>
如果
<dubbo:service>
和<dubbo:reference>
都配了actives,<dubbo:reference>
优先
Load Balance均衡
配置服务的客户端的 loadbalance
属性为 leastactive
,此 Loadbalance 会调用并发数最小的 Provider(Consumer端并发数)。
<dubbo:reference interface="com.foo.BarService" loadbalance="leastactive" />
<!--或-->
<dubbo:service interface="com.foo.BarService" loadbalance="leastactive" />
3、延迟连接
延迟连接用于减少长连接数。当有调用发起时,再创建长连接。
<dubbo:protocol name="dubbo" lazy="true" />
4、粘滞连接
粘滞连接用于有状态服务,尽可能让客户端总是向同一提供者发起调用,除非该提供者挂了,再连另一台。
粘滞连接将自动开启延迟连接,以减少长连接数。
<dubbo:reference id="xxxService" interface="com.xxx.XxxService" sticky="true" />
Dubbo 支持方法级别的粘滞连接,如果想进行更细粒度的控制,还可以这样配置:
<dubbo:reference id="xxxService" interface="com.xxx.XxxService">
<dubbo:mothod name="sayHello" sticky="true" />
</dubbo:reference>
5、连接安全
在2.7.5 以上版本,dubbo对内置的 Dubbo Netty Server 和新引入的 gRPC 协议都提供了基于 TLS 的安全链路传输机制。
TLS 的配置都有统一的入口,如下所示:
provider端:
SslConfig sslConfig = new SslConfig();
sslConfig.setServerKeyCertChainPath("path to cert");
sslConfig.setServerPrivateKeyPath(args[1]);
// 如果开启双向 cert 认证
if (mutualTls) {
sslConfig.setServerTrustCertCollectionPath(args[2]);
}
ProtocolConfig protocolConfig = new ProtocolConfig("dubbo/grpc");
protocolConfig.setSslEnabled(true);
Consumer 端 :
if (!mutualTls) {}
sslConfig.setClientTrustCertCollectionPath(args[0]);
} else {
sslConfig.setClientTrustCertCollectionPath(args[0]);
sslConfig.setClientKeyCertChainPath(args[1]);
sslConfig.setClientPrivateKeyPath(args[2]);
}
6、连接校验
通过令牌验证在注册中心控制权限,以决定要不要下发令牌给消费者,可以防止消费者绕过注册中心访问提供者,另外通过注册中心可灵活改变授权方式,而不需修改或升级提供者。
可以全局设置开启令牌验证:
<!--随机token令牌,使用UUID生成-->
<dubbo:provider token="true" />
或:
<!--固定token令牌,相当于密码-->
<dubbo:provider token="123456" />
也可在服务级别设置:
<!--随机token令牌,使用UUID生成-->
<dubbo:service interface="com.foo.BarService" token="true" />
或:
<!--固定token令牌,相当于密码-->
<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 端的服务特点和服务质量等问题。
示例:
<dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService"
timeout="300" retries="2" loadbalance="random" actives="0" />
<dubbo:service interface="com.alibaba.hello.api.WorldService" version="1.0.0" ref="helloService"
timeout="300" retries="2" loadbalance="random" actives="0" >
<dubbo:method name="findAllPerson" timeout="10000" retries="9" loadbalance="leastactive" actives="5" />
<dubbo:service/>
建议在 Provider 端配置的 Consumer 端属性有:
timeout
:方法调用的超时时间。retries
:失败重试次数,缺省是 2。``loadbalance
:负载均衡算法,缺省是随机random
。还可以配置轮询roundrobin
、最不活跃优先leastactive
和一致性哈希consistenthash
等。leastactive指从 Consumer 端并发调用最好的 Provider,可以减少对响应慢的 Provider 的调用,因为响应慢更容易累积并发调用。
actives
:消费者端的最大并发调用限制,即当 Consumer 对一个服务的并发调用到上限后,新调用会阻塞直到超时,在方法上配置 dubbo:method 则针对该方法进行并发限制,在接口上配置dubbo:service
,则针对该服务进行并发限制。
2)在 Provider 端配置合理的 Provider 端属性
<dubbo:protocol threads="200" />
<dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService"
executes="200" >
<dubbo:method name="findAllPerson" executes="50" />
</dubbo:service>
建议在 Provider 端配置的 Provider 端属性有:
threads
:服务线程池大小。executes
:一个服务提供者并行执行请求上限,即当 Provider 对一个服务的并发调用达到上限后,新调用会阻塞,此时 Consumer 可能会超时。在方法上配置dubbo:method
则针对该方法进行并发限制,在接口上配置dubbo:service
,则针对该服务进行并发限制。3)配置管理信息
目前有负责人信息和组织信息用于区分站点。以便于在发现问题时找到服务对应负责人,建议至少配置两个人以便备份。负责人和组织信息可以在运维平台 (Dubbo Ops) 上看到。
在应用层面配置负责人、组织信息:<dubbo:application owner=”ding.lid,william.liangf” organization=”intl” />
在服务层面(服务端)配置负责人:
<dubbo:service owner=”ding.lid,william.liangf” />
在服务层面(消费端)配置负责人:
<dubbo:reference owner=”ding.lid,william.liangf” />
若没有配置服务层面的负责人,则默认使用
dubbo:application
设置的负责人。4)配置 Dubbo 缓存文件
提供者列表缓存文件:
<dubbo:registry file=”${user.home}/output/dubbo.cache” />
注意:
可以根据需要调整缓存文件的路径,保证这个文件不会在发布过程中被清除;
- 如果有多个应用进程,请注意不要使用同一个文件,避免内容被覆盖;
该文件会缓存注册中心列表和服务提供者列表。配置缓存文件后,应用重启过程中,若注册中心不可用,应用会从该缓存文件读取服务提供者列表,进一步保证应用可靠性。
5)监控配置
- 使用固定端口暴露服务,而不要使用随机端口这样在注册中心推送有延迟的情况下,消费者通过缓存列表也能调用到原地址,保证调用成功。
- 使用 Dubbo Admin 监控注册中心上的服务提供方使用 Dubbo Admin 监控服务在注册中心上的状态,确保注册中心上有该服务的存在。
- 服务提供方可使用 Dubbo Qos 的 telnet 或 shell 监控项监控服务提供者端口状态:
echo status | nc -i 1 20880 | grep OK | wc -l
,其中的 20880 为服务端口。 - 服务消费方可通过将服务强制转型为 EchoService,并调用 $echo() 测试该服务的提供者是可用如
assertEqauls(“OK”, ((EchoService)memberService).$echo(“OK”))
。6)不要使用 dubbo.properties 文件配置,推荐使用对应 XML 配置
Dubbo 中所有的配置项都可以配置在 Spring 配置文件中,并且可以针对单个服务配置。
dubbo.properties 中属性名与 XML 的对应关系
应用名 dubbo.application.name
<dubbo:application name="myalibaba" >
注册中心地址 dubbo.registry.address
<dubbo:registry address="11.22.33.44:9090" >
调用超时
dubbo.service.*.timeout
可以在多个配置项设置超时 timeout,由上至下覆盖(即上面的优先)``,其它的参数(retries、loadbalance、actives等)的覆盖策略与 timeout 相同。示例如下:
提供者端特定方法的配置
<dubbo:service interface="com.alibaba.xxx.XxxService" >
<dubbo:method name="findPerson" timeout="1000" />
</dubbo:service>
提供者端特定接口的配置
<dubbo:service interface="com.alibaba.xxx.XxxService" timeout="200" />
服务提供者协议
dubbo.service.protocol
、服务的监听端口dubbo.service.server.port
<dubbo:protocol name="dubbo" port="20880" />
服务线程池大小
dubbo.service.max.thread.threads.size
<dubbo:protocol threads="100" />
6.消费者启动时,没有提供者是否抛异常
alibaba.intl.commons.dubbo.service.allow.no.provider
<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 官方文档