**作者:青芒@有赞
本篇主要对dubbo集群容错进行剖析,主要下面几个模块
- cluster容错方案
- Directory目录服务
- route 路由解析
- loadBalance 软负载均衡
一、调用链路
二、容错方案
集群模式的配置
服务提供方服务消费方
集群容错实现
接口类 com.alibaba.dubbo.rpc.cluster.Cluster
1.AvailableCluster 获取可用的调用。遍历所有Invokers判断Invoker.isAvalible,只要一个有为true直接调用返回,不管成不成功
2.BroadcastCluster 广播调用。遍历所有Invokers, 逐个调用每个调用catch住异常不影响其他invoker调用
3.FailbackCluster 失败自动恢复, 对于invoker调用失败, 后台记录失败请求,任务定时重发, 通常用于通知
//FailbackClusterInvoker
//记录失败的调用
private
final
ConcurrentMap
<
Invocation
,
AbstractClusterInvoker
<?>> failed =
new
ConcurrentHashMap
<
Invocation
,
AbstractClusterInvoker
<?>>();
protected
Result
doInvoke(
Invocation
invocation,
List
<
Invoker
<T>> invokers,
LoadBalance
loadbalance)
throws
RpcException
{
try
{
checkInvokers(invokers, invocation);
Invoker
<T> invoker =
select
(loadbalance, invocation, invokers,
null
);
return
invoker.invoke(invocation);
}
catch
(
Throwable
e) {
//失败后调用 addFailed
addFailed(invocation,
this
);
return
new
RpcResult
();
// ignore
}
}
private
void
addFailed(
Invocation
invocation,
AbstractClusterInvoker
<?> router) {
if
(retryFuture ==
null
) {
synchronized
(
this
) {
if
(retryFuture ==
null
) {
retryFuture = scheduledExecutorService.scheduleWithFixedDelay(
new
Runnable
() {
public
void
run() {
// 收集统计信息
try
{
retryFailed();
}
catch
(
Throwable
t) {
// 防御性容错
logger.error(
"Unexpected error occur at collect statistic"
, t);
}
}
}, RETRY_FAILED_PERIOD, RETRY_FAILED_PERIOD,
TimeUnit
.MILLISECONDS);
}
}
}
failed.put(invocation, router);
}
//失败的进行重试,重试成功后移除当前map
void
retryFailed() {
if
(failed.size() ==
0
) {
return
;
}
for
(
Map
.
Entry
<
Invocation
,
AbstractClusterInvoker
<?>> entry :
new
HashMap
<
Invocation
,
AbstractClusterInvoker
<?>>(
failed).entrySet()) {
Invocation
invocation = entry.getKey();
Invoker
<?> invoker = entry.getValue();
try
{
invoker.invoke(invocation);
failed.remove(invocation);
}
catch
(
Throwable
e) {
logger.error(
"Failed retry to invoke method "
+ invocation.getMethodName() +
", waiting again."
, e);
}
}
}
4.FailfastCluster 快速失败,只发起一次调用,失败立即保错,通常用于非幂等性操作
5.FailoverCluster default
失败转移,当出现失败,重试其它服务器,通常用于读操作,但重试会带来更长延迟 (1) 目录服务directory.list(invocation) 列出方法的所有可调用服务 获取重试次数,默认重试两次
int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIESKEY, Constants.DEFAULTRETRIES) + 1;
(2) 根据LoadBalance负载策略选择一个Invoker (3) 执行invoker.invoke(invocation)调用 (4) 调用成功返回 调用失败小于重试次数,重新执行从3)步骤开始执行,调用次数大于等于重试次数抛出调用失败异常
6.FailsafeCluster 失败安全,出现异常时,直接忽略,通常用于写入审计日志等操作。
7.ForkingCluster 并行调用,只要一个成功即返回,通常用于实时性要求较高的操作,但需要浪费更多服务资源。
注: 还有 MergeableCluster 和 MockClusterWrapper策略,但是个人没有用过所以就不说了
三、Directory目录服务
1. StaticDirectory
静态目录服务, 它的所有Invoker通过构造函数传入, 服务消费方引用服务的时候, 服务对多注册中心的引用,将Invokers集合直接传入 StaticDirectory构造器
-
public StaticDirectory(URL url, List<Invoker<T>> invokers, List<Router> routers) {
-
super(url == null && invokers != null && invokers.size() > 0 ? invokers.get(0).getUrl() : url, routers);
-
if (invokers == null || invokers.size() == 0)
-
throw new IllegalArgumentException("invokers == null");
-
this.invokers = invokers;
-
}
StaticDirectory的list方法直接返回所有invoker集合
@Override
protected
List
<
Invoker
<T>> doList(
Invocation
invocation)
throws
RpcException
{
return
invokers;
}
2. RegistryDirectory
注册目录服务, 它的Invoker集合是从注册中心获取的, 它实现了NotifyListener接口实现了回调接口notify(List)。
比如消费方要调用某远程服务,会向注册中心订阅这个服务的所有服务提供方,订阅时和服务提供方数据有变动时回调消费方的NotifyListener服务的notify方法NotifyListener.notify(List) 回调接口传入所有服务的提供方的url地址然后将urls转化为invokers, 也就是refer应用远程服务到此时引用某个远程服务的RegistryDirectory中有对这个远程服务调用的所有invokers。
RegistryDirectory.list(invocation)就是根据服务调用方法获取所有的远程服务引用的invoker执行对象
四、服务路由
dubbo路由功能貌似用的不多,目的主要是对已注册的服务进行过滤,比如只能调用某些配置的服务,或者禁用某些服务。
1. ConditionRouter条件路由
dubbo-admin 后台进行配置。
路由代码入口
public
<T>
List
<
Invoker
<T>> route(
List
<
Invoker
<T>> invokers, URL url,
Invocation
invocation)
throws
RpcException
{
if
(invokers ==
null
|| invokers.size() ==
0
) {
return
invokers;
}
try
{
if
(!matchWhen(url, invocation)) {
return
invokers;
}
List
<
Invoker
<T>> result =
new
ArrayList
<
Invoker
<T>>();
if
(thenCondition ==
null
) {
logger.warn(
"The current consumer in the service blacklist. consumer: "
+
NetUtils
.getLocalHost() +
", service: "
+ url.getServiceKey());
return
result;
}
.............................
2. ScriptRouter脚本路由
按照dubbo脚本规则进行编写,程序识别
五、软负载均衡
1. RandomLoadBalance default
随机,按权重设置随机概率。权重default= 100
在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
protected
<T>
Invoker
<T> doSelect(
List
<
Invoker
<T>> invokers, URL url,
Invocation
invocation) {
int
length = invokers.size();
// 总个数
int
totalWeight =
0
;
// 总权重
boolean
sameWeight =
true
;
// 权重是否都一样
for
(
int
i =
0
; i < length; i++) {
int
weight = getWeight(invokers.
get
(i), invocation);
totalWeight += weight;
// 累计总权重
if
(sameWeight && i >
0
&& weight != getWeight(invokers.
get
(i -
1
), invocation)) {
sameWeight =
false
;
// 计算所有权重是否一样
}
}
if
(totalWeight >
0
&& !sameWeight) {
// 如果权重不相同且权重大于0则按总权重数随机
int
offset = random.nextInt(totalWeight);
// 并确定随机值落在哪个片断上
for
(
int
i =
0
; i < length; i++) {
offset -= getWeight(invokers.
get
(i), invocation);
if
(offset <
0
) {
return
invokers.
get
(i);
}
}
}
// 如果权重相同或权重为0则均等随机
return
invokers.
get
(random.nextInt(length));
}
算法含义 如果所有的服务权重都一样,就采用总服务数进行随机。如果权重不一样,则按照权重出随机数,然后用随机数减去服务权重,结果为负数则使用当前循环的服务。其实也就是一个概率性问题 每个服务的概率就是 当前服务的权重/ 总服务权重
2. RoundRobinLoadBalance
轮循,按公约后的权重设置轮循比率。 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
该负载算法维护着一个方法调用顺序计数
private final ConcurrentMapsequences = new ConcurrentHashMap();,>,>
以方法名作为key
轮循分为 普通轮询和加权轮询。权重一样时,采用取模运算普通轮询,反之加权轮询。
下面看下具体的实现 RoundRobinLoadBalance#doSelect
i.普通轮询
AtomicPositiveInteger
sequence = sequences.
get
(key);
if
(sequence ==
null
) {
sequences.putIfAbsent(key,
new
AtomicPositiveInteger
());
sequence = sequences.
get
(key);
}
//获取本次调用的服务器序号,并+1
int
currentSequence = sequence.getAndIncrement();
//当前序号和服务总数取模
return
invokers.
get
(currentSequence % length);
ii.加权轮询 下面贴下核心实现代码。注意几个变量
weightSum
= 服务权重之和 invokerToWeightMap
= 权重>0的 invoker map
int
currentSequence = sequence.getAndIncrement();
if
(maxWeight >
0
&& minWeight < maxWeight) {
// 权重不一样
// mod < weightSum,下面for循环进行weight递减,weight大的服务被调用的概率大
int
mod = currentSequence % weightSum;
for
(
int
i =
0
; i < maxWeight; i++) {
for
(
Map
.
Entry
<
Invoker
<T>,
IntegerWrapper
> each : invokerToWeightMap.entrySet()) {
final
Invoker
<T> k = each.getKey();
final
IntegerWrapper
v = each.getValue();
if
(mod ==
0
&& v.getValue() >
0
) {
return
k;
}
if
(v.getValue() >
0
) {
v.decrement();
mod--;
}
}
}
}
可以举个例子 两个服务 A 和 B,权重分别是1和2 那么 mod=[0,1,2],经过上面的逻辑,调用概率是 A B B A B B A B B ..... 显然B的概率更大一些
3. LeastActiveLoadBalance
最少活跃调用数优先,活跃数指调用前后计数差。使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
每个服务有一个活跃计数器,我们假如有A,B两个提供者.计数均为0.当A提供者开始处理请求,该计数+1,此时A还没处理完,当处理完后则计数-1.而B请求接收到请求处理得很快.B处理完后A还没处理完,所以此时A,B的计数为1,0.那么当有新的请求来的时候,就会选择B提供者(B的活跃计数比A小).这就是文档说的,使慢的提供者收到更少请求。
int leastCount = 0; // 相同最小活跃数的个数 int[] leastIndexs = new int[length]; // 相同最小活跃数的下标
i.最小活跃服务个数=1, 该服务优先
-
if (leastCount == 1) {
-
// 如果只有一个最小则直接返回
-
return invokers.get(leastIndexs[0]);
-
}
ii.最小活跃服务个数>1, 最小活跃的服务按照权重随机
-
if (!sameWeight && totalWeight > 0) {
-
// 如果权重不相同且权重大于0则按总权重数随机
-
int offsetWeight = random.nextInt(totalWeight);
-
// 并确定随机值落在哪个片断上
-
for (int i = 0; i < leastCount; i++) {
-
int leastIndex = leastIndexs[i];
-
//权重越大,offsetWeight越快减成负数
-
offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
-
if (offsetWeight <= 0)
-
return invokers.get(leastIndex);
-
}
-
}
iii. 最小活跃服务个数>1, 权重相同,服务个数随机
<dubbo:reference
id
=
"demoService"
interface
=
"com.youzan.dubbo.api.DemoService"
loadbalance
=
"consistenthash"
>
<!--缺省只对第一个参数 Hash-->
<dubbo:parameter
key
=
"hash.arguments"
value
=
"0,1"
/>
<!--缺省用 160 份虚拟节点,-->
<dubbo:parameter
key
=
"hash.nodes"
value
=
"160"
/>
</dubbo:reference>
4. ConsistentHashLoadBalance
- 一致性 Hash,相同参数的请求总是发到同一提供者。
- 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
- 算法参见:http://en.wikipedia.org/wiki/Consistent_hashing
-
缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameterkey="hash.arguments"value="0,1"/>
-
缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameterkey="hash.nodes"value="320"/>
配置样例
<dubbo:reference
id
=
"demoService"
interface
=
"com.youzan.dubbo.api.DemoService"
loadbalance
=
"consistenthash"
>
<!--缺省只对第一个参数 Hash-->
<dubbo:parameter
key
=
"hash.arguments"
value
=
"0,1"
/>
<!--缺省用 160 份虚拟节点,-->
<dubbo:parameter
key
=
"hash.nodes"
value
=
"160"
/>
</dubbo:reference>
算法解析
ConsistentHashLoadBalance为使用该算法的服务维护了一个 selectors
,
key=invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName() eg: com.youzan.dubbo.api.DemoService.sayHello
#com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance
private
final
ConcurrentMap
<
String
,
ConsistentHashSelector
<?>> selectors =
new
ConcurrentHashMap
<
String
,
ConsistentHashSelector
<?>>();
@SuppressWarnings
(
"unchecked"
)
@Override
protected
<T>
Invoker
<T> doSelect(
List
<
Invoker
<T>> invokers, URL url,
Invocation
invocation) {
String
key = invokers.
get
(
0
).getUrl().getServiceKey() +
"."
+ invocation.getMethodName();
int
identityHashCode =
System
.identityHashCode(invokers);
//获取该服务的ConsistentHashSelector,并跟进本次调用获取对应invoker
ConsistentHashSelector
<T> selector = (
ConsistentHashSelector
<T>) selectors.
get
(key);
if
(selector ==
null
|| selector.getIdentityHashCode() != identityHashCode) {
selectors.put(key,
new
ConsistentHashSelector
<T>(invokers, invocation.getMethodName(), identityHashCode));
selector = (
ConsistentHashSelector
<T>) selectors.
get
(key);
}
return
selector.
select
(invocation);
}
ConsistentHashSelector作为ConsistentHashLoadBalance的内部类, 就是具体的一致性hash实现。
- ConsistentHashSelector内部元素
#com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance.ConsistentHashSelector
//该服务的所有hash节点
private
final
TreeMap
<
Long
,
Invoker
<T>> virtualInvokers;
//虚拟节点数量
private
final
int
replicaNumber;
//该服务的唯一hashcode,通过System.identityHashCode(invokers)获取
private
final
int
identityHashCode;
- 如何构建该服务的虚拟节点?
-
public
ConsistentHashSelector
(
List
<
Invoker
<T>> invokers,
String
methodName,
int
identityHashCode) {
// 创建TreeMap 来保存结点
this
.virtualInvokers =
new
TreeMap
<
Long
,
Invoker
<T>>();
// 生成调用结点HashCode
this
.identityHashCode =
System
.identityHashCode(invokers);
// 获取Url
//dubbo://192.168.0.4:20880/com.youzan.dubbo.api.DemoService?anyhost=true&application=consumer-of-helloworld-app&check=false&class=com.youzan.dubbo.provider.DemoServiceImpl&dubbo=2.5.4&generic=false&hash.arguments=0,1&hash.nodes=160&interface=com.youzan.dubbo.api.DemoService&loadbalance=consistenthash&methods=sayHello&pid=32710&side=consumer×tamp=1527383363936
URL url = invokers.
get
(
0
).getUrl();
// 获取所配置的结点数,如没有设置则使用默认值160
this
.replicaNumber = url.getMethodParameter(methodName,
"hash.nodes"
,
160
);
// 获取需要进行hash的参数数组索引,默认对第一个参数进行hash
String
[] index =
Constants
.COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName,
"hash.arguments"
,
"0"
));
argumentIndex =
new
int
[index.length];
for
(
int
i =
0
; i < index.length; i ++) {
argumentIndex[i] =
Integer
.parseInt(index[i]);
}
// 创建虚拟结点
// 对每个invoker生成replicaNumber个虚拟结点,并存放于TreeMap中
for
(
Invoker
<T> invoker : invokers) {
for
(
int
i =
0
; i < replicaNumber /
4
; i++) {
// 根据md5算法为每4个结点生成一个消息摘要,摘要长为16字节128位。
byte
[] digest = md5(invoker.getUrl().toFullString() + i);
// 随后将128位分为4部分,0-31,32-63,64-95,95-128,并生成4个32位数,存于long中,long的高32位都为0
// 并作为虚拟结点的key。
for
(
int
h =
0
; h <
4
; h++) {
long
m = hash(digest, h);
virtualInvokers.put(m, invoker);
}
}
}
}
代码如果看的不是很懂,也不用去深究了(我就没看懂,瞻仰了网上大神的文章贴了帖注释),大家可以就粗略的认为,这段代码就是尽可能的构建出散列均匀的服务hash表。
- 如何从virtualInvokers选取本次调用的invoker?
-
// 选择invoker
public
Invoker
<T>
select
(
Invocation
invocation) {
// 根据调用参数来生成Key
String
key = toKey(invocation.getArguments());
// 根据这个参数生成消息摘要
byte
[] digest = md5(key);
//调用hash(digest, 0),将消息摘要转换为hashCode,这里仅取0-31位来生成HashCode
//调用sekectForKey方法选择结点。
Invoker
<T> invoker = sekectForKey(hash(digest,
0
));
return
invoker;
}
private
String
toKey(
Object
[] args) {
StringBuilder
buf =
new
StringBuilder
();
// 由于hash.arguments没有进行配置,因为只取方法的第1个参数作为key
for
(
int
i : argumentIndex) {
if
(i >=
0
&& i < args.length) {
buf.append(args[i]);
}
}
return
buf.toString();
}
//根据hashCode选择结点
private
Invoker
<T> sekectForKey(
long
hash) {
Invoker
<T> invoker;
Long
key = hash;
// 若HashCode直接与某个虚拟结点的key一样,则直接返回该结点
if
(!virtualInvokers.containsKey(key)) {
// 若不一致,找到一个比传入的key大的第一个结点。
SortedMap
<
Long
,
Invoker
<T>> tailMap = virtualInvokers.tailMap(key);
// 若不存在,那么选择treeMap中第一个结点
// 使用TreeMap的firstKey方法,来选择最小上界。
if
(tailMap.isEmpty()) {
key = virtualInvokers.firstKey();
}
else
{
// 若存在则返回
key = tailMap.firstKey();
}
}
invoker = virtualInvokers.
get
(key);
return
invoker;
}
- 一致性hash环是什么东东?和上面的算法什么关系? ConsistentHashSelector.virtualInvokers这个东西就是我们的服务hash节点,单纯的从数据结构上的确看不到什么环状的存在,可以先示意下,当前的数据结构
我们的服务节点只是一个普通的 map数据存储而已,如何形成环呢?其实所谓的环只是逻辑上的展现, ConsistentHashSelector.sekectForKey()
方法里通过 TreeMap.tailMap()、TreeMap.tailMap().firstKey、TreeMap.tailMap().firstKey() 结合case实现了环状逻辑。下面我们画图说话。
第一步原始数据结构,我们按照hash从小到大排列 A,B,C表示我们提供的服务,改示意图假设服务节点散列均匀
第二步选择服务节点
i. 假设本地调用得到的key=2120, 代码逻辑(指 ConsistentHashSelector.sekectForKey
)走到tailMap.firstKey()
那么读取到 3986
A服务ii.假设本地调用得到的key=9991, tailMap为空,逻辑走到 virtualInvokers.firstKey()
回到起点 读取到 1579 A服务
上述两部情况基本已经能够描述清楚节点的选择逻辑,至于hash直接命中,那么读取对应的服务即可,无需多讲。
最后环状形成 上面两部的介绍已经描述hash算法,那么我们所谓的环状是怎么一回事呢?其实也就是为了方便更好的理解这个逻辑,我们将线性的hash排列作为环状,然后hash的选择按照顺时针方向选择节点(等价于上面hash比较大小) 节点选择算法与上面等价,本图主要用来示意,理想的hash环hash差距应该是等差,均匀的排列。
参考: https://www.jianshu.com/p/53feb7f5f5d9
长按二维码,扫扫关注哦
✬如果你喜欢这篇文章,欢迎分享和点赞✬