本章主要内容:
• 集群容错总体实现;
• 普通容错策略的实现;
• Directory的实现原理;
• Router的实现原理;
• LoadBalance的实现原理;
• Merger的实现原理;
Cluster的总体工作流程可以分为以下几步:
(1) 生成Invoker对象。 不同的Cluster实现会生成不同类型的Clusterinvoker对象并返回。 然后调用Clusterinvoker的Invoker方法, 正式开始调用流程。
(2) 获得可调用的服务列表。 首先会做前置校验, 检查远程服务是否已被销毁。 然后通过Directory#list方法获取所有可用的服务列表。 接着使用Router接口处理该服务列表, 根据路由规则过滤一部分服务, 最终返回剩余的服务列表。
(3) 做负载均衡。 在第2步中得到的服务列表还需要通过不同的负载均衡策略选出一个服务, 用作最后的调用。 首先框架会根据用户的配置, 调用ExtensionLoader获取不同负载均衡策略的扩展点实现(具体负载均衡策略会在后面讲解) 。 然后做一些后置操作, 如果是异步调用则设置调用编号。 接着调用子类实现的dolnvoke方法(父类专门留了这个抽象方法让子类实现) ,子类会根据具体的负载均衡策略选出一个可以调用的服务。
(4) 做RPC调用。 首先保存每次调用的Invoker到RPC上下文, 并做RPC调用。 然后处理调用结果, 对于调用出现异常、 成功、 失败等情况, 每种容错策略会有不同的处理方式。
2容错机制的实现
2.1容错机制概述
Dubbo容错机制能增强整个应用的鲁棒性, 容错过程对上层用户是完全透明的, 但用户也可以通过不同的配置项来选择不同的容错机制。 每种容错机制又有自己个性化的配置项。 Dubbo中现有 Failover、Failfast、 Failsafe、 Fallback、 Forking、 Broadcast 等容错机制, 容错机制的特性如表7-1所示。
2.3 Failover 策略
2.6 Fallback 策略
Fallback如果调用失败, 则会定期重试。 FailbackClusterlnvoker里面定义了一个ConcurrentHashMap,专门用来保存失败的调用。 另外定义了一个定时线程池, 默认每5秒把所有失败的调用拿出来, 重试一次。 如果调用重试成功, 则会从ConcurrentHashMap中移除。
3 Directory 的实现
整个容错过程中首先会使用Directory#list来获取所有的Invoker列表。 Directory也有多种实现子类, 既可以提供静态的Invoker列表, 也可以提供动态的Invoker列表。 静态列表是用户自己设置的Invoker列表; 动态列表根据注册中心的数据动态变化, 动态更新Invoker列表的数据, 整个过程对上层透明
又是熟悉的“套路” , 使用了模板模式。 Directory是顶层的接口。 AbstractDirectory封装了通用的实现逻辑。 抽象类包含RegistryDirectory和StaticDirectory两个子类。 下面分别介绍每个类的职责和工作:
(1) AbstractDirectoryo封装了通用逻辑, 主要实现了四个方法: 检测Invoker是否可用,销毁所有Invoker, list方法, 还留了一个抽象的doList方法给子类自行实现。 list方法是最主要的方法, 用于返回所有可用的list,逻辑分为两步:
• 调用抽象方法doList获取所有Invoker列表, 不同子类有不同的实现 ;
• 遍历所有的router,进行Invoker的过滤, 最后返回过滤好的Invoker列表。
doList抽象方法则是返回所有的Invoker列表, 由于是抽象方法, 子类继承后必须要有自己的实现。
(2) RegistryDirectoryo属于Directory的动态列表实现, 会自动从注册中心更新Invoker列表、 配置信息、 路由列表。
(3) StaticDirectoryo Directory的静态列表实现, 即将传入的Invoker列表封装成静态的Directory对象, 里面的列表不会改变。
3.2 RegistryDirectory 的实现
RegistryDirectory中有两条比较重要的逻辑线, 第一条, 框架与注册中心的订阅, 并动态更新本地Invoker列表、 路由列表、 配置信息的逻辑; 第二条, 子类实现父类的doList方法
1. 订阅与动态更新
我们先看一下订阅和动态更新逻辑。 这个逻辑主要涉及subscribe、 notify> refreshinvoker三个方法, 其余是一些数据转换的辅助类方法, 如toConfigurators、toRouterSo
subscribe是订阅某个URL的更新信息。 Dubbo在引用每个需要RPC调用的Bean的时候,会调用directory. subscribe来订阅这个Bean的各种URL的变化(Bean的配置在配置中心中都是以URL的形式存放的)。
notify就是监听到配置中心对应的URL的变化, 然后更新本地的配置参数。 监听的URL分为三类: 配置configurators、路由规则router、 Invoker列表。
2. doList的实现
notify中更新的Invoker列表最终会转化为一个字典Map<Stringj List<Invoker<T>>>methodlnvokerMapo key是对应的方法名称, value是整个Invoker列表。 doList的最终目标就是在字典里匹配出可以调用的Invoker列表, 并返回给上层。 其主要步骤如下:
(1)检查服务是否被禁用。 如果配置中心禁用了某个服务, 则该服务无法被调用。 如果服务被禁用则会抛出异常。
(2)根据方法名和首参数匹配Invoker这是一个比较奇特的特性。 根据方法名和首参数查找对应的Invoker列表, 暂时没看到相关的应用场景。
4 路由的实现
Directory获取所有Invoker列表的时候, 就会调用到本节的路由接口。路由接口会根据用户配置的不同路由策略对Invoker列表进行过滤, 只返回符合规则的Invoker。
4.1路由的总体结构
路由分为条件路由、 文件路由、 脚本路由, 对应dubbo-admin中三种不同的规则配置方式。条件路由是用户使用Dubbo定义的语法规则去写路由规则;文件路由则需要用户提交一个文件,里面写着对应的路由规则, 框架基于文件读取对应的规则; 脚本路由则是使用JDK自身的脚本引擎解析路由规则脚本, 所有JDK脚本引擎支持的脚本都能解析, 默认是JavaScripto我们先来看一下接口之间的关系, 如图7.9所示。
5 负载均衡的实现
在整个集群容错流程中, 首先经过Directory获取所有Invoker列表, 然后经过Router根据路由规则过滤Invoker,最后幸存下来的Invoker还需要经过负载均衡这一关, 选出最终要调用的 Invoker
5.1包装后的负载均衡
整个逻辑过程大致可以分为4步:
(1) 检查URL中是否有配置粘滞连接, 如果有则使用粘滞连接的Invoker。 如果没有配置粘滞连接, 或者重复调用检测不通过、 可用检测不通过, 则进入第2步。
(2) 通过ExtensionLoader获取负载均衡的具体实现, 并通过负载均衡做节点的选择。 对选择出来的节点做重复调用、 可用性检测, 通过则直接返回, 否则进入第3步。
(3) 进行节点的重新选择。 如果需要做可用性检测, 则会遍历Directory中得到的所有节点, 过滤不可用和已经调用过的节点, 在剩余的节点中重新做负载均衡; 如果不需要做可用性检测, 那么也会遍历Directory中得到的所有节点, 但只过滤已经调用过的, 在剩余的节点中重新做负载均衡。 这里存在一种情况, 就是在过滤不可用或已经调用过的节点时, 节点全部被过滤, 没有剩下任何节点, 此时进入第4步。
(4) 遍历所有已经调用过的节点, 选出所有可用的节点, 再通过负载均衡选出一个节点并返回。 如果还找不到可调用的节点, 则返回null。从上述逻辑中, 我们可以得知, 框架会优先处理粘滞连接。 否则会根据可用性检测或重复调用检测过滤一些节点, 并在剩余的节点中做负载均衡。 如果可用性检测或重复调用检测把节点都过滤了, 则兜底的策略是: 在己经调用过的节点中通过负载均衡选择出一个可用的节点。
5.2负载均衡的总体结构
Dubbo现在内置了 4种负载均衡算法, 用户也可以自行扩展, 因为LoadBalance接口上有@SPI注解, 如代码清单7.12所示。
代码清单7.12负载均衡接口
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance (
@Adaptive("loadbalance")
<T> Invoker<T> select(List<Invoker<T>> invokers, URL url. Invocation invocation)
throws RpcException;
}
4种负载均衡算法都继承自同一个抽象类, 使用的也是模板模式, 抽象父类中己经把通用的逻辑完成, 留了一个抽象的doSelect方法给子类实现。 负载均衡的接口关系如图7-10所示
5.3 Random负载均衡
Random负载均衡是按照权重设置随机概率做负载均衡的。 这种负载均衡算法并不能精确地平均请求, 但是随着请求数量的增加, 最终结果是大致平均的。
(1) 计算总权重并判断每个Invoker的权重是否一样。 遍历整个Invoker列表, 求和总权重。 在遍历过程中, 会对比每个Invoker的权重, 判断所有Invoker的权重是否相同。
(2) 如果权重相同, 则说明每个Invoker的概率都一样, 因此直接用nextlnt随机选一个Invoker返回即可。
(3) 如果权重不同, 则首先得到偏移值, 然后根据偏移值找到对应的Invoker,如代码清单7-14所示
代码清单7-14随机负载均衡源码
int offset = ThreadLocalRandom.current().nextlnt(totalWeight);
for (int i = 0; i < length; i++) {
offset -= weights[i];
if (offset < 0) {
return invokers.get(i);
}
)
5.4 RoundRobin 负载均衡
权重,轮询负载均衡会根据设置的权重来判断轮询的比例 。 普通轮询负载均衡的好处是每个节点获得的请求会很均匀, 如果某些节点的负载能力明显较弱, 则这个节点会堆积比较多的请求。 因此普通的轮询还不能满足需求, 还需要能根据节点权重进行干预。 权重轮询又分为普通权重轮询和平滑权重轮询。 普通权重轮询会造成某个节点会突然被频繁选中, 这样很容易突然让一个节点流量暴增。 Nginx中有一种叫平滑轮询的算法(smooth weighted round-robin balancing),这种算法在轮询时会穿插选择其他节点, 让整个服务器选择的过程比较均匀, 不会“逮住” 一个节点一直调用oDubbo框架中最新的RoundRobin代码已经改为平滑权重轮询算法。
5.5 LeastActive 负载均衡
LeastActive负载均衡称为最少活跃调用数负载均衡, 即框架会记下每个Invoker的活跃数,每次只从活跃数最少的Invoker里选一个节点。 这个负载均衡算法需要配合ActiveLimitFilter过滤器来计算每个接口方法的活跃数。 最少活跃负载均衡可以看作Random负载均衡的“加强版” , 因为最后根据权重做负载均衡的时候, 使用的算法和Random的一样。
5.6 一致性Hash负载均衡
一致性Hash负载均衡可以让参数相同的请求每次都路由到相同的机器上。 这种负载均衡的方式可以让请求相对平均, 相比直接使用Hash而言, 当某些节点下线时, 请求会平摊到其他服务提供者, 不会引起剧烈变动。
普通一致性Hash的简单示例如图7-12所示。
普通一致性Hash会把每个服务节点散列到环形上, 然后把请求的客户端散列到环上, 顺时针往前找到的第一个节点就是要调用的节点。 假设客户端落在区域2,则顺时针找到的服务C就是要调用的节点。 当服务C宕机下线, 则落在区域2部分的客户端会自动迁移到服务D上。这样就避免了全部重新散列的问题。
普通的一致性Hash也有一定的局限性, 它的散列不一定均匀, 容易造成某些节点压力大。因此Dubbo框架使用了优化过的Ketama 一致性Hash。 这种算法会为每个真实节点再创建多个虚拟节点, 让节点在环形上的分布更加均匀, 后续的调用也会随之更加均匀。
6 Merger的实现
当一个接口有多种实现, 消费者又需要同时引用不同的实现时, 可以用group来区分不同的实现, 如下所示。
<dubbo:service group="groupl" interface="com.xxx.testService" />
<dubbo:service group="group2" interface="com.xxx.testservice" />
如果我们需要并行调用不同group的服务, 并且要把结果集合并起来, 贝懦要用到Merger特性。 Merger实现了多个服务调用后结果合并的逻辑。 虽然业务层可以自行实现这个能力, 但Dubbo直接封装到框架中, 作为一种扩展点能力, 简化了业务开发的复杂度。 Merger的工作方式如图7.13所示
6.1总体结构
MergerCluster也是Cluster接口的一种实现, 因此也遵循Cluster的设计模式, 在invoke方法中完成具体逻辑。 整个过程会使用Merger接口的具体实现来合并结果集。 在使用的时候, 通过MergerFactory获得各种具体的Merger实现oMerger的12种默认实现的关系如图7-14所示。
如果开启了 Merger特性, 并且未指定合并器(Merger的具体实现), 则框架会根据接口的返回类型自动匹配合并器。
备注:文章参考《深入理解Apache Dubbo与实战》,作者:林琳,诣极