前言
在实际的生产中,为了应用的高可用,势必会采用集群的方式去建设服务的消费者和提供者。集群容错就是在这样多提供者的情况下,如何调用(负载均衡)、失败处理(容错机制),针对这些问题的一个方面的处理课题。下面的这张图就是 Dubbo 集群容错相关的组件和组件之间的执行顺序。
集群容错在 Dubbo 10层架构中就是 Cluster 层,其设计目的就是解决 Dubbo 在多提供者等集群环境中的调用问题,这里注意区分 Cluster 层和上图的 Cluster 接口组件的区别:
- Cluster 接口是容错机制的接口;
- Cluster 层是 Dubbo 10层架构中的一环,是对集群调用的抽象,其包含容错机制、负载均衡、路由、获取 invoker和发起 RPC调用。
下面服务调用顺序,介绍一下组件的功能和运行顺序。
- Cluster 是容错机制的接口;
- DIrectory 的用途是保存 Invoker,可简单类比为 List< Invoker>,并且 invoker 可以动态根据注册中心的信息变化;
- Router 提供路由,根据注册中心的信息,过滤不符合的 invoker ,使其不参与后面的负载均衡选择;
- LoanBanlance 提供负载均衡能力,也就是从前面选择之后的 invoker 中最终选择一个调用的提供者。
- 最后的 invoker 就是去发起 RPC 调用。
一个 invoker 对应一个服务提供者。
容错机制
Dubbo 提供了9种容错机制,如下表。
机制名 | 简介 |
---|---|
Failover 默认 | 失败重试。默认失败后在重试1次,重试其他服务器。通常使用在读/幂等写的场景。会对下游服务造成较大压力。 |
Failfast | 快速失败。失败就返回异常。使用在非幂等写的场景。但受网络影响大。 |
Failsafe | 失败不做处理,直接忽略异常。使用在不关心调用结果,且成功与否不重要的场景。 |
Failback | 失败后放入队列,并定时重试。使用在要保持最终一致或异步处理的场景。 |
Forking | 并行调用多个服务,只要一个成功就可以。使用在实时性要求高的场景,但会造成消费者资源浪费。 |
Broadcast | 广播给所有提供者,有一个失败就失败。 |
Mock | 出现异常就会使用默认的返回内容,使用在服务降级的场景。 |
Available | 不用负载均衡,找到第一个可用提供者就调用。 |
Mergeable | 把多个节点的返回结果合并。 |
服务降级
服务降级是 Dubbo 容错机制的一种(MockClusterInvoker)。Dubbo 服务降级有两种方式:force 和 fail。主要使用的场景:网络流量激增,为了快速响应客户端,不影响业务,会将一些非关键业务屏蔽(force);或者希望服务调用失败之后会有一个默认的结果(fail)。
举个例子,购物下单之后会再给用户推荐相似商品,而在大促的时候,用户并发大,为了把服务器资源倾斜给核心业务,也就是用户购买商品,就可能当调用推荐商品的服务异常的时候把这个异常放弃,会给默认的数据即可,甚至直接屏蔽推荐商品服务。
public Result invoke(Invocation invocation) throws RpcException {
if (value.length() == 0 || value.equalsIgnoreCase("false")) {
// no mock
result = this.invoker.invoke(invocation);
} else if (value.startsWith("force")) {
//force-mock 直接不发起RPC调用了
result = doMockInvoke(invocation, null);
} else {
try {
// fail-mock 先发起RPC调用,然后捕获异常再走mock
result = this.invoker.invoke(invocation);
} catch (RpcException e) {
if (e.isBiz()) {
throw e;
} else {
result = doMockInvoke(invocation, e);
}
}
}
return result;
}
上面的源码就是 MockClusterInvoker 使用的方式,主要就是3种:
- no mock。没有 mock,正常发起调用也不会进行异常处理;
- Force-mock。直接屏蔽了,不发起 RPC 调用;
- fail-mock。先发起 RPC 调用,失败了再走 mock。
MockClusterInvoker 是一个包装类,创建的时候就会传入一个 ClusterInvoker。
Directory
Directory 对于 Cluster 层而言,是保存 invoker 的接口,其提供了动态订阅注册中心消费者的元数据信息。反正只要记住: Directory 是获取 invoker 列表的,并且这个列表是实时根据注册中心信息变化的。(也可以是静态不变的)
路由策略
路由策略是从 Directory 获取了所有了的 invoker 之后,还会根据路由策略过滤掉不符合的 invoker。路由有三种方式:条件路由、文件路由、脚本路由。路由规则是在 Dubbo 控制台进行配置。
负载均衡
在经过路由策略过滤了 invoker 之后,剩下的 invoker 就会进行负载均衡的选择,Dubbo 提供了4种负载均衡:
- 权重随机负载均衡(默认);
- 权重轮询负载均衡;
- 一致性 hash 负载均衡;
- 最小活跃数负载均衡。
权重随机负载均衡
Dubbo 负载均衡可以配置权重,这里的随机负载均衡也是基于权重的随机选择,执行的逻辑如下:
- 如果所有的 invoker 的权重相同,直接 nextInt 随机选择一个 invoker 即可;
- 如果 invoker 的权重不是都相等,那么需要根据权重随机选择一个。
对于第2点,Dubbo的实现很巧妙,也很简单,我们看个例子就明白了:假设现在有3个 invoker,权重分别是1,2,3,那么随机选择的概率分别是 1/6,2/6,3/6。那这时我们选择负载的时候,会在0~6随机一个数字,假设随机的数字是5,Dubbo 随机负载均衡就会用随机到的数字5,依次去减每个 invoker 的权重(1,2,3),当减去之后小于0,那么就选择这个 invoker 作为服务调用的提供者。
权重轮询负载均衡
普通轮询负载均衡(没有权重)不能根据服务器性能调整轮询比例,性能差的服务很有可能被打挂了,所以 Dubbo 选择权重轮询负载均衡。
而权重轮询负载均衡又分为普通权重轮询负载均衡和平滑权重轮询负载均衡,普通权重轮询负载均衡就是按照顺序和权重轮询去请求服务,举个例子,3个 invoker 权重分别为1,2,3,那普通权重轮询负载均衡访问的次序就是:invoker1、invoker2、invoker2、invoker3、invoker3、invoker3。这种方式同一个服务(invoker3)短时间内可能被多次调用,节点流量暴增。
平滑权重轮询负载均衡就可以避免上面的这种情况,其算法分为下面几步:
- 初始状态,所有 invoker 的当前权重为0;
- 所有 invoker 的当前权重加上自己的权重;
- 选择当前权重最大的 invoker 进行服务调用,然后这个 invoker 减去 S(S= 所有 invoker 的权重和);
- 重复执行2,3步骤 S 次,就完成了轮询。
假设3个 invoker,权重分别为5,1,1。那么用平滑权重轮询负载均衡其选择顺序如下:
轮数 | 选择前的当前权重 | 选择节点 | 选择后的当前权重 |
---|---|---|---|
1 | {5, 1, 1} | Invoker1 | {-2, 1, 1} |
2 | {3, 2, 2} | Invoker1 | {-4, 2, 2} |
3 | {1, 3, 3} | Invoker2 | {1, -4, 3} |
4 | {6, -3, 4} | Invoker1 | {-1, -3, 4} |
5 | {4, -2, 5} | Invoker3 | {4, -2, -2} |
6 | {9, -1, -1} | Invoker1 | {2, -1, -1} |
7 | {7, 0, 0} | Invoker1 | {0, 0, 0} |
可以看到一轮下来,3个 invoker 分别按照权重比例进行了合理的选择,并且选择更加平滑,没有一直都是 invoker1 的情况。
一致性hash负载均衡
一致性hash可以让一个消费者访问到同一个服务提供者,主要实现的方式就是通过消费者的 IP 等元信息做 hash 然后在 hash 环上顺时针找最近的提供者节点,如果 cache1 挂了,那么请求会顺时针找到 cache2。
Dubbo 中实现 hash 环是用了一个 TreeMap ,并且把服务提供者 IP 和一个递增数字做 MD5 之后,再 hash 得到的是 Map 的 key,服务提供者就是 value。并且 Dubbo 为了避免 hash 环上节点分布不均衡导致的,负载不平衡的问题(如下图1),采用了虚拟节点的方式来避免这种分布不均衡的问题,默认会为每个 invoker 建立160个虚拟节点 (如下图2)。
最小活跃数负载均衡
服务消费者每次发起调用,都会把对应的 invoker 的调用次数 +1 。然后下次使用最小活跃数负载均衡选择 invoker 的时候,会先遍历所有的 invoker ,取调用次数最小的 invoker,如果有多个次数相同,就再用随机负载的方式选择一个 invoker。