一、负载均衡
我们发现在很多容错策略中都会使用负载均衡方法 , 并且所有的容错策略中的负载均衡都使用了抽象父类 Abstractclusterinvoker 中定义的 Invoker select 方法 , 而并不是直接使用 LoadBalance 方法 。 因为抽象父类在LoadBalance 的基础上又封装了一些新的特性 :
(1) 粘滞连接 。 Dubbo 中有一种特性叫粘滞连接 , 以下内容摘自官方文档 :粘滞连接用于有状态服务 , 尽可能让客户端总是向同一提供者发起调用 , 除非该提供者 “ 挂了 ” , 再连接另一台 。粘滞连接将自动开启延迟连接 , 以减少长连接数 。<dubbo:protocol name= “dubbo” sticky=“true” />
(2) 可用检测 。 Dubbo 调用的 URL 中 , 如果含有 cluster.availablecheck=false, 则不会检测远程服务是否可用 , 直接调用 。 如果不设置 , 则默认会开启检查 , 对所有的服务都做是否可用的检查 , 如果不可用 , 则再次做负载均衡 。
(3) 避免重复调用 。 对于已经调用过的远程服务 , 避免重复选择 , 每次都使用同一个节点 。这种特性主要是为了避免并发场景下 , 某个节点瞬间被大量请求 。整个逻辑过程大致可以分为 4 步 :
(1) 检查 URL 中是否有配置粘滞连接 , 如果有则使用粘滞连接的 Invoker 。 如果没有配置粘滞连接 , 或者重复调用检测不通过 、 可用检测不通过 , 则进入第 2 步 。
(2) 通过 ExtensionLoader 获取负载均衡的具体实现 , 并通过负载均衡做节点的选择 。 对选择出来的节点做重复调用 、 可用性检测 , 通过则直接返回 , 否则进入第 3 步 。
(3) 进行节点的重新选择 。 如果需要做可用性检测 , 则会遍历 Directory 中得到的所有节点 , 过滤不可用和已经调用过的节点 , 在剩余的节点中重新做负载均衡 ; 如果不需要做可用性检测 , 那么也会遍历 Directory 中得到的所有节点 , 但只过滤已经调用过的 , 在剩余的节点中重新做负载均衡 。 这里存在一种情况 , 就是在过滤不可用或已经调用过的节点时 , 节点全部被过滤 , 没有剩下任何节点 , 此时进入第 4 步 。
(4) 遍历所有已经调用过的节点 , 选出所有可用的节点 , 再通过负载均衡选出一个节点并返回 。 如果还找不到可调用的节点 , 则返回 null 。从上述逻辑中 , 我们可以得知 , 框架会优先处理粘滞连接 。 否则会根据可用性检测或重复调用检测过滤一些节点 , 并在剩余的节点中做负载均衡 。 如果可用性检测或重复调用检测把节点都过滤了 , 则兜底的策略是 : 在己经调用过的节点中通过负载均衡选择出一个可用的节点 。以上就是封装过的负载均衡的实现 , 下面讲解原始的 LoadBalance 是如何实现的
负载均衡算法主要有Random LoadBalance, RoundRobin LoadBalance, LeastActive LoadBalance,
ConsistentHash LoadBalance。
1.Random 负载均衡
Random 负载均衡是按照权重设置随机概率做负载均衡的 。 这种负载均衡算法并不能精确地平均请求 , 但是随着请求数量的增加 , 最终结果是大致平均的 。 它的负载计算步骤如下 :
(1) 计算总权重并判断每个 Invoker 的权重是否一样 。 遍历整个 Invoker 列表 , 求和总权重 。 在遍历过程中 , 会对比每个 Invoker 的权重 , 判断所有 Invoker 的权重是否相同 。
(2) 如果权重相同 , 则说明每个 Invoker 的概率都一样 , 因此直接用 nextlnt 随机选一个Invoker 返回即可 。
(3) 如果权重不同 , 则首先得到偏移值 , 然后根据偏移值找到对应的 Invoker
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size(); // Number of invokers
int totalWeight = 0; // The sum of weights
boolean sameWeight = true; // 通过这个标志判断是否具有同样的权重
for (int i = 0; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation); //获得这个invoker 的权重
totalWeight += weight; // Sum
if (sameWeight && i > 0
&& weight != getWeight(invokers.get(i - 1), invocation)) {
sameWeight = false;
}
}
if (totalWeight > 0 && !sameWeight) { //如果权重不同执行下面逻辑
// If (not every invoker has the same weight & at least one invoker's weight>0), select randomly based on totalWeight.
int offset = random.nextInt(totalWeight); // 获得偏移值
// Return a invoker based on the random value.
for (int i = 0; i < length; i++) {
offset -= getWeight(invokers.get(i), invocation); //不断减去权重,当偏移值为零时,那么这个Invoker被选中
if (offset < 0) {
return invokers.get(i);
}
}
}
//权重相同直接返回随机的Invoker
return invokers.get(random.nextInt(length));
}
2.RoundRobin 负载均衡
步骤如下:
(1)初始化权重缓存 Map 。 以每个 Invoker 的 URL 为 key, 对象 WeightedRoundRobin 为 value 生成一个 ConcurrentMap, 并把这个 Map 保存到全局的 methodWeightMap 中 : ConcurrentMap<String, ConcurrentMap<String , WeightedRoundRobin>> methodWeightMap 。 methodWeightMap 的 key 是每个接口 + 方法名 。 这一步只会生成这个缓存 Map, 但里面是空的 , 第 2 步才会生成每个 Invoker 对应的键值 。
(2) 遍历所有 Invoker 。 首先 , 在遍历的过程中把每个 Invoker 的数据填充到第 1 步生成的权重缓存 Map 中 。 其次 , 获取每个 Invoker 的预热权重 , 新版的框架 RoundRobin 也支持预热 ,通过和 Random 负载均衡中相同的方式获得预热阶段的权重 。 如果预热权重和 Invoker 设置的权重不相等 , 则说明还在预热阶段 , 此时会以预热权重为准 。 然后 , 进行平滑轮询 。 每个 Invoker会把权重加到自己的 current 属性上 , 并更新当前 Invoker 的 lastUpdate 。 同时累加每个 Invoker的权重到 totalweight 最终 , 遍历完后 , 选出所有 Invoker 中 current 最大的作为最终要调用的节点 。
(3) 清除已经没有使用的缓存节点 。 由于所有的 Invoker 的权重都会被封装成一个weightedRoundRobin 对象 , 因此如果可调用的 Invoker 列表数量和缓存 weightedRoundRobin 对象的 Map 大小不相等 , 则说明缓存 Map 中有无用数据 ( 有些 Invoker 己经不在了 , 但 Map 中还有缓存 ) 。
(4) 返回 Invoker 。 注意 , 返回之前会把当前 Invoker 的 current 减去总权重 。 这是平滑权重轮询中重要的一步
下面介绍平滑权重轮询过程:
(1) 每次请求做负载均衡时 , 会遍历所有可调用的节点 ( Invoker 列表 ) 。 对于每个 Invoker,让它的 current = current + weight 。 属性含义见 weightedRoundRobin 对象 。 同时累加每个Invoker 的 weight 到 totalWeight, 即 totalweight = totalweight + weight
(2) 遍历完所有 Invoker 后 , current 值最大的节点就是本次要选择的节点 。 当然如果存在多个最大的,选取第一个。最后 , 把该节点的 current 值减去 totalWeight, 即 current = current - totalweight
假设有3个Invoker ,A ,B,C它们的权重分别是1,2,3初始current都是0,则过程如下表
请求次数 | 被选中前的current值 | 被选中后的值 | 选中的节点 |
---|---|---|---|
1 | {1,2,3} | {1,2,-3} | C |
2 | {2,4,0} | {2, -2,0} | B |
3 | {3,0,3} | {-3,0,3} | A |
4 | {-2,2,6} | {-2,2,0} | C |
5 | {-1,4,3} | {-1,-2,3} | B |
6 | {0,0,6} | {0,0,0} | C |
如上表,正好1+2+3=6次为一周期,并且他们的调用次数与权重比相同,A权重为1,调用次数为1,B权重为2调用次数为2,C权重为3调用次数为3.
3.LeastActive负载均衡
LeastActive 负载均衡称为最少活跃调用数负载均衡 , 即框架会记下每个 Invoker 的活跃数 ,每次只从活跃数最少的 Invoker 里选一个节点 。 这个负载均衡算法需要配合 ActiveLimitFilter过滤器来计算每个接口方法的活跃数 。 最少活跃负载均衡可以看作 Random 负载均衡的 “ 加强版 ” , 因为最后根据权重做负载均衡的时候 , 使用的算法和 Random 的一样 。
4.一致性Hash负载均衡
这里我就不多讲了,https://www.jianshu.com/p/528ce5cd7e8f这个博主写的蛮好,原理都是一样的。
二、Merger的实现
当一个接口有多种实现 , 消费者又需要同时引用不同的实现时 , 可以用 group 来区分不同的实现 。
如果我们需要并行调用不同 group 的服务 , 并且要把结果集合并起来 , 贝懦要用到 Merger特性 。 Merger 实现了多个服务调用后结果合并的逻辑 。 虽然业务层可以自行实现这个能力 , 但Dubbo 直接封装到框架中 , 作为一种扩展点能力 , 简化了业务开发的复杂度 。
MergeableClusterlnvoker 串起了整个合并器逻辑 , 在讲解 MergeableClusterlnvoker 的机制之前 , 我们先回顾一下整个调用的过程 : MergeableCluster#join 方法中直接生成并返回了MergeableClusterlnvoker, MergeableClusterInvoker#invoke 方法又通过 MergerFactory 工厂获取不同的 Merger 接口实现 , 完成了合并的具体逻辑 。
MergeableCluster 并没有继承抽象的 Cluster 实现 , 而是独立完成了自己的逻辑 。 因此 , 它的整个逻辑和之前的 Failover 等机制不同 , 其步骤如下 :
(1) 前置准备 。 通过 directory 获取所有 Invoker 列表 。
(2) 合并器检查 。 判断某个方法是否有合并器 , 如果没有 , 则不会并行调用多个 group,找到第一个可以调用的 Invoker 直接调用就返回了 。 如果有合并器 , 则进入第 3 步 。
(3) 获取接口的返回类型 。 通过反射获得返回类型 , 后续要根据这个返回值查找不同的合并器 。
(4) 并行调用 。 把 Invoker 的调用封装成一个个 Callable 对象 , 放到线程池中执行 , 保存线程池返回的 future 对象到 HashMap 中 , 用于等待后续结果返回 。
(5) 等待 fixture 对象的返回结果 。 获取配置的超时参数 , 遍历 (4) 中得到的 fixture 对象 ,设置 Future#get 的超时时间 , 同步等待得到并行调用的结果 。 异常的结果会被忽略 , 正常的结果会被保存到 list 中 。 如果最终没有返回结果 , 则直接返回一个空的 RpcResult ; 如果只有一个结果 ,那么也直接返回 , 不需要再做合并 ; 如果返回类型是 void, 则说明没有返回值 , 也直接返回 。
(6) 合并结果集 。 如果配置的是 merger^ 1 .addAll", 则直接通过反射调用返回类型中的 .addAll 方法合并结果集 。 例如 : 返回类型是 Set, 则调用 Set.addAll 来合并结果
三、Mock
在 Cluster 中 , 还有最后一个 MockClusterWrapper, 由它实现了 Dubbo 的本地伪装 。 这个功能的使用场景较多 , 通常会应用在以下场景中 : 服务降级 ; 部分非关键服务全部不可用 , 希望主流程继续进行 ; 在下游某些节点调用异常时 , 可以以 Mock 的结果返回 。
当接口配置了 Mock, 在 RPC 调用抛出 RpcException 时就会执行 Mock 方法 。 最后一种 return null 的配置方式通常会在想直接忽略异常的时候使用
服务的降级是在 dubbo-admin 中通过 override 协议更新 Invoker 的 Mock 参数实现的 。 如果Mock 参数设置为 mock=force: return+null, 则表明是强制 Mock, 强制 Mock 会让消费者对该服务的调用直接返回 null, 不再发起远程调用 。 通常使用在非重要服务己经不可用的时候 , 可以屏蔽下游对上游系统造成的影响 。 此外 , 还能把参数设置为印 mock=fail:return+null, 这样消费者还是会发起远程调用 , 不过失败后会返回 null, 但是不抛出异常 。最后 , 如果配置的参数是以 throw 开头的 , 即 mock= throw,则直接抛出 RpcException, 不会发起远程调用 。