Dubbo集群容错(1)

22 篇文章 0 订阅
8 篇文章 0 订阅

前言

《深入理解Apche Dubbo与实战》 第七章笔记

一、Cluster层概述

可以将Cluster层看作一个集群容错层,该层中包含Cluster、Directory、Router、LoadBalance等几大核心接口。
其中,Cluster层指的是对外的整个集群容错层;而Cluster是容错接口,提供Failover、Failfast等容错策略。
Cluster层的总体流程如下:
(1) 生成Invoke对象
(2)获得可调用的服务列表
(3)做负载均衡
(4)做RPC调用

二、容错机制的实现

cluster 接口一共有 9 种不同的实现 , 每种实现分别对应不同的 Clusterlnvoker。 本节会介绍继承了 Abstractclusterinvoker 的 7 种 Clusterinvoker 实现 , Merge 和 Mock 属于特殊机制 ,会在后面讲解 。

各种容错机制的特性如下

机制名机制简介
Failover失败自动切换,当出现失败时,重试其他服务器,但重试会导致接口延迟增大
Failfast快速失败,失败后,快速返回异常结果,不做任何重试,通常用在非幂等的接口上
Failsafe出现异常时,直接忽略异常,用在不关心结果的场合下
Forking同时调用多个相同的服务,只要一个返回,立即返回结果,适用于实时性高的场合
Failback请求失败后,放入失败队列,通过定时线程池定时重试,适用于异步或最终一致性的请求
Broadcast广播调用所有可用的服务,任意一个节点报错则报错
Mock广播调用所有可用的服务,任意一个节点报错则报错
Available不做负载均衡,遍历列表,找到第一个可用节点
Mergable可以自动把多个节点请求结果进行合并

1.Failover机制

Failfast 会在失败后直接抛出异常并返回 , 实现非常简单 , 步骤如下 :

(Cluster 接口上有 SPI 注 W@SPI(FailoverCluster . NAME ), 即默认实现是 Failover 。 该策略的代码逻辑如下 :

(1) 校验 。 校验从 AbstractClusterlnvoker 传入的 Invoker 列表是否为空 。
(2) 获取配置参数 。 从调用 URL 中获取对应的 retries 重试次数 。
(3) 初始化一些集合和对象 。 用于保存调用过程中出现的异常 、 记录调用了哪些节点(这个会在负载均衡中使用 , 在某些配置下 , 尽量不要一直调用同一个服务) 。
(4) 使用 for 循环实现重试 , for 循环的次数就是重试的次数 。 成功则返回 , 否则继续循环 。如果 for 循环完 , 还没有一个成功的返回 , 则抛出异常 , 把 (3) 中记录的信息抛出去 。前 3 步都是做一些校验 、 数据准备的工作 。 第 4 步开始真正的调用逻辑 。 以下步骤是 for循环中的逻辑 :
校验 。 如果 for 循环次数大于 1 , 即有过一次失败 , 则会再次校验节点是否被销毁 、 传入的 Invoker 列表是否为空 。
负载均衡 。 调用 select 方法做负载均衡 , 得到要调用的节点 , 并记录这个节点到步骤 3的集合里 , 再把己经调用的节点信息放进 RPC 上下文中 。
远程调用 。 调用 invoker#invoke 方法做远程调用 , 成功则返回 , 异常则记录异常信息 ,再做下次循环 。

2.Failfast 策略

Failfast 会在失败后直接抛出异常并返回 , 实现非常简单 , 步骤如下 :

(1) 校验 。 校验从 AbstractClusterlnvoker 传入的 Invoker 列表是否为空 。
(2) 负载均衡 。 调用 select 方法做负载均衡 , 得到要调用的节点 。
(3) 进行远程调用 。 在 try 代码块中调用 invoker#invoke 方法做远程调用 。 如果捕获到异常 , 则直接封装成 RpcException 抛出

3.Failsafe

Failsafe 调用时如果出现异常 , 则会直接忽略 。 实现也非常简单 , 步骤如下 :

(1) 校验传入的参数 。 校验从 AbstractClusterlnvoker 传入的 Invoker 列表是否为空 。
(2) 负载均衡 。 调用 select 方法做负载均衡 , 得到要调用的节点
(3) 远程调用 。 在 try 代码块中调用 invoker#invoke 方法做远程调用 , “ catch ” 到任何异常都直接 “ 吞掉 ” , 返回一个空的结果集 。

4.Failback

Fallback 如果调用失败 , 则会定期重试 。 FailbackClusterlnvoker 里面定义了一个ConcurrentHashMap, 专门用来保存失败的调用 。 另外定义了一个定时线程池 , 默认每 5 秒把所有失败的调用拿出来 , 重试一次 。 如果调用重试成功 , 则会从 ConcurrentHashMap 中移除 。

dolnvoke 的调用逻辑如下 :

(1) 校验传入的参数 。 校验从 AbstractClusterlnvoker 传入的 Invoker 列表是否为空 。
(2) 负载均衡 。 调用 select 方法做负载均衡 , 得到要调用的节点 。
(3) 远程调用 。 在 try 代码块中调用 invoker#invoke 方法做远程调用 , “ catch ” 到异常后直接把 invocation 保存到重试的 ConcurrentHashMap 中 , 并返回一个空的结果集 。
(4) 定时线程池会定时把 ConcurrentHashMap 中的失败请求拿出来重新请求 , 请求成功则从 ConcurrentHashMap 中移除 。 如果请求还是失败 , 则异常也会被 “ catch ” 住 , 不会影响ConcurrentHashMap 中后面的重试 。

5.Available

Available 是找到第一个可用的服务直接调用 , 并返回结果 。 步骤如下 :

(1) 遍历从 AbstractClusterlnvoker 传入的 Invoker 列表 , 如果 Invoker 是可用的 , 则直接调用并返回 。
(2) 如果遍历整个列表还没找到可用的 Invoker, 则抛出异常 。

6.Broadcast

Broadcast 会广播给所有可用的节点 , 如果任何一个节点报错 , 则返回异常 。 步骤如下 :

(1) 前置操作 。 校验从 AbstractClusterlnvoker 传入的 Invoker 列表是否为空 ; 在 RPC 上下文中设置 Invoker 列表 ; 初始化一些对象 , 用于保存调用过程中产生的异常和结果信息等 。
(2) 循环遍历所有 Invoker, 直接做 RPC 调用 。 任何一个节点调用出错 , 并不会中断整个广播过程 , 会先记录异常 , 在最后广播完成后再抛出 。 如果多个节点异常 , 则只有最后一个节点的异常会被抛出 , 前面的异常会被覆盖

7.Forking

首先Fork这个词有分支的意思,我们也可能听说过Fork/Join框架,大致就能知道什么意思。Forking 可以同时并行请求多个服务 , 有任何一个返回 , 则直接返回 。
(1) 准备工作 。 校验传入的 Invoker 列表是否可用 ; 初始化一个 Invoker 集合 , 用于保存真正要调用的 Invoker 列表 ; 从 URL 中得到最大并行数 、 超时时间 。
(2) 获取最终要调用的 Invoker 列表 。 假设用户设置最大的并行数为 n, 实际可以调用的最大服务数为 v 。 如果 v < 0 或n < v, 则说明可用的服务数小于用户的设置 , 因此最终要调用的Invoker 只能有 v 个 ; 如果 n > v, 则会循环调用负载均衡方法 , 不断得到可调用的 Invoker, 加入步骤 1 中的 Invoker 集合里 。这里有一点需要注意 : 在 Invoker 加入集合时 , 会做去重操作 。
( 3) 调用前的准备工作 。 设置要调用的 Invoker 列表到 RPC 上下文 ; 初始化一个异常计数器 ; 初始化一个阻塞队列 , 用于记录并行调用的结果 。
(4) 执行调用 。 循环使用线程池并行调用 , 调用成功 , 则把结果加入阻塞队列 ; 调用失败 ,则失败计数 +1 。 如果所有线程的调用都失败了 , 即失败计数 >= 所有可调用的 Invoker 时 , 则把异常信息加入阻塞队列 。
(5) 同步等待结果 。 由于步骤 4 中的步骤是在线程池中执行的 , 因此主线程还会继续往下执行 , 主线程中会使用阻塞队列的 poll(- 超时时间 “ )方法 , 同步等待阻塞队列中的第一个结果 ,如果是正常结果则返回 , 如果是异常则抛出 。

三、Directory的实现

整个容错过程中首先会使用 Directory#list 来获取所有的 Invoker 列表 。 Directory 也有多种实现子类 , 既可以提供静态的 Invoker 列表 , 也可以提供动态的 Invoker 列表 。 静态列表是用户自己设置的 Invoker 列表 ; 动态列表根据注册中心的数据动态变化 , 动态更新 Invoker 列表的数据 , 整个过程对上层透明 。
(1) AbstractDirectoryo 封装了通用逻辑 , 主要实现了四个方法 : 检测 Invoker 是否可用 ,销毁所有 Invoker, list 方法 , 还留了一个抽象的 doList 方法给子类自行实现 。 list 方法是最主要的方法 , 用于返回所有可用的 list, 逻辑分为两步 :

调用抽象方法 doList 获取所有 Invoker 列表 , 不同子类有不同的实现 ;
遍历所有的 router, 进行 Invoker 的过滤 , 最后返回过滤好的 Invoker 列表 。doList 抽象方法则是返回所有的 Invoker 列表 , 由于是抽象方法 , 子类继承后必须要有自己的实现 。

(2) RegistryDirectory 属于 Directory 的动态列表实现 , 会自动从注册中心更新 Invoker列表 、 配置信息 、 路由列表 。
(3) StaticDirectory 是 Directory 的静态列表实现 , 即将传入的 Invoker 列表封装成静态的Directory 对象 , 里面的列表不会改变 。 因为 Cluster# join(Directopy directory) 方法需要传入 Directory 对象 , 因此该实现主要使用在一些上层已经知道自己要调用哪些 Invoker, 只需要包装一个 Directory 对象返回即可的场景 。 在 ReferenceConfig#createProxy 和RegistryDirectory#toMergeMethodInvokerMap 中使用了 Cluster#join 方法 。 StaticDirectory 的逻辑非常简单 , 在构造方法中需要传入 Invoker 列表 , doList 方法则直接返回初始化时传入的列表 。 因此 , 不再详细说明。接下来 , 我们重点关注 RegistryDirectory 的实现 。

四、路由的实现

当 Directory 获取所有 Invoker 列表的时候 , 就会调用到本节的路由接口 。路由接口会根据用户配置的不同路由策略对 Invoker 列表进行过滤 , 只返回符合规则的 Invoker 。 例如 : 如果用户配置了接口 A 的所有调用 , 都使用 IP 为 192.168.1.22 的节点 , 则路由会过滤其他的 Invoker, 只返回 IP 为 192.168.1.22 的 Invoker

路由一般有三种实现方式,条件路由(condition)、脚本路由(script)、文件路由(file)

1.条件路由

条件路由的具体实现类是 ConditionRouter, Dubbo 会根据自定义的规则语法来实现路由规则 。 我们主要需要关注其构造方法和实现父类接口的 route 方法 。

ConditionRouter 构造方法的逻辑

ConditionRouterFactory 在初始化 ConditionRouter 的时候 , 其构造方法中含有规则解析的逻辑 。 步骤如下

(1) 根据 URL 的键 rule 获取对应的规则字符串 。 以 => 为界 , 把规则分成两段 , 前面部分为 whenRule, 即消费者匹配条件 ; 后面部分为 thenRule, 即提供者地址列表的过滤条件 。 我们以上个例子为例 , 其会被解析为 whenRule : method = find* 和 thenRule : host =192.168.1. 22
( 2) 分别解析两个路由规则 。 调用 parseRule 方法 , 通过正则表达式不断循环匹配 whenRule 和 thenRule 字符串 。 解析的时候 , 会根据 key-value 之间的分隔符对 key-value 做分类(如果 A=B,则分隔符为 =) , 支持的分隔符形式有 : A=B 、 A&B 、 A!=B 、 A,B 这 4 种形式 。 最终参数都会被封装成一个个 MatchPair 对象 , 放入 Map 中保存 。 Map 的 key 是参数值 , value 是 MatchPair对象 。 对上面的例子会生成以 method 为 key 的 when Map, 以 host 为 key的 then Map 。 value 则分别是包装了 find* 和 192.168.1.22 的 MatchPair 对象 。
route 方法的实现原理
ConditionRouter 继承了 Router 接口 , 需要实现接口的 route 方法 。 该方法的主要功能是过滤出符合路由规则的 Invoker 列表 , 即做具体的条件匹配判断 , 其步骤如下 :

(1) 校验 。 如果规则没有启用 , 则直接返回 ; 如果传入的 Invoker 列表为空 , 则直接返回空 ; 如果没有任何的 whenRule 匹配 , 即没有规则匹配 , 则直接返回传入的 Invoker 列表 ; 如果whenRule 有匹配的 , 但是 thenRule 为空 , 即没有匹配上规则的 Invoker, 则返回空 。
(2) 遍历 Invoker 列表 , 通过 thenRule 找出所有符合规则的 Invoker 加入集合 。 例如 : 匹配规则中的 method 名称和当前 URL 中的 method 是不是相等 。
(3) 返回结果 。 如果结果集不为空 , 则直接返回 ; 如果结果集为空 , 但是规则配置了force=true, 即强制过滤 , 那么就会返回空结果集 ; 非强制则不过滤 , 即返回所有 Invoker 列表 。具体的逻辑还是比较简单的 , 但代码中的 if 判断会比较多 。

2.文件路由

文件路由是把规则写在文件中 , 文件中写的是自定义的脚本规则 , 可以是 JavaScript 、 Groovy等 , URL 中对应的 key 值填写的是文件的路径 。 文件路由主要做的就是把文件中的路由脚本读出来 , 然后调用路由的工厂去匹配对应的脚本路由做解析 。

3. 脚本路由

脚本路由使用 JDK 自带的脚本解析器解析脚本并运行 , 默认使用 JavaScript 解析器 , 其逻辑分为构造方法和 route 方法两大部分 。 构造方法主要负责一些初始化的工作 , route 方法则是具体的过滤逻辑执行的地方 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值