learn.lianglianglee.com/专栏/Dubbo源码解读与实战-完/38 集群容错:一个好汉三个帮(下).md.html
2022-09-06 22:30:37 +08:00

832 lines
47 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>38 集群容错:一个好汉三个帮(下).md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/Dubbo源码解读与实战-完/00 开篇词 深入掌握 Dubbo 原理与实现,提升你的职场竞争力.md.html">00 开篇词 深入掌握 Dubbo 原理与实现,提升你的职场竞争力</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/01 Dubbo 源码环境搭建:千里之行,始于足下.md.html">01 Dubbo 源码环境搭建:千里之行,始于足下</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/02 Dubbo 的配置总线:抓住 URL就理解了半个 Dubbo.md.html">02 Dubbo 的配置总线:抓住 URL就理解了半个 Dubbo</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/03 Dubbo SPI 精析,接口实现两极反转(上).md.html">03 Dubbo SPI 精析,接口实现两极反转(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/04 Dubbo SPI 精析,接口实现两极反转(下).md.html">04 Dubbo SPI 精析,接口实现两极反转(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/05 海量定时任务,一个时间轮搞定.md.html">05 海量定时任务,一个时间轮搞定</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/06 ZooKeeper 与 Curator求你别用 ZkClient 了(上).md.html">06 ZooKeeper 与 Curator求你别用 ZkClient 了(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/07 ZooKeeper 与 Curator求你别用 ZkClient 了(下).md.html">07 ZooKeeper 与 Curator求你别用 ZkClient 了(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/08 代理模式与常见实现.md.html">08 代理模式与常见实现</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/09 Netty 入门,用它做网络编程都说好(上).md.html">09 Netty 入门,用它做网络编程都说好(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/10 Netty 入门,用它做网络编程都说好(下).md.html">10 Netty 入门,用它做网络编程都说好(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/11 简易版 RPC 框架实现(上).md.html">11 简易版 RPC 框架实现(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/12 简易版 RPC 框架实现(下).md.html">12 简易版 RPC 框架实现(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/13 本地缓存:降低 ZooKeeper 压力的一个常用手段.md.html">13 本地缓存:降低 ZooKeeper 压力的一个常用手段</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/14 重试机制是网络操作的基本保证.md.html">14 重试机制是网络操作的基本保证</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/15 ZooKeeper 注册中心实现,官方推荐注册中心实践.md.html">15 ZooKeeper 注册中心实现,官方推荐注册中心实践</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/16 Dubbo Serialize 层:多种序列化算法,总有一款适合你.md.html">16 Dubbo Serialize 层:多种序列化算法,总有一款适合你</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/17 Dubbo Remoting 层核心接口分析:这居然是一套兼容所有 NIO 框架的设计?.md.html">17 Dubbo Remoting 层核心接口分析:这居然是一套兼容所有 NIO 框架的设计?</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/18 Buffer 缓冲区:我们不生产数据,我们只是数据的搬运工.md.html">18 Buffer 缓冲区:我们不生产数据,我们只是数据的搬运工</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/19 Transporter 层核心实现:编解码与线程模型一文打尽(上).md.html">19 Transporter 层核心实现:编解码与线程模型一文打尽(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/20 Transporter 层核心实现:编解码与线程模型一文打尽(下).md.html">20 Transporter 层核心实现:编解码与线程模型一文打尽(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/21 Exchange 层剖析:彻底搞懂 Request-Response 模型(上).md.html">21 Exchange 层剖析:彻底搞懂 Request-Response 模型(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/22 Exchange 层剖析:彻底搞懂 Request-Response 模型(下).md.html">22 Exchange 层剖析:彻底搞懂 Request-Response 模型(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/23 核心接口介绍RPC 层骨架梳理.md.html">23 核心接口介绍RPC 层骨架梳理</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/24 从 Protocol 起手,看服务暴露和服务引用的全流程(上).md.html">24 从 Protocol 起手,看服务暴露和服务引用的全流程(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/25 从 Protocol 起手,看服务暴露和服务引用的全流程(下).md.html">25 从 Protocol 起手,看服务暴露和服务引用的全流程(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/26 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker.md.html">26 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/27 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker.md.html">27 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/28 复杂问题简单化,代理帮你隐藏了多少底层细节?.md.html">28 复杂问题简单化,代理帮你隐藏了多少底层细节?</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/29 加餐HTTP 协议 + JSON-RPCDubbo 跨语言就是如此简单.md.html">29 加餐HTTP 协议 + JSON-RPCDubbo 跨语言就是如此简单</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/30 Filter 接口,扩展 Dubbo 框架的常用手段指北.md.html">30 Filter 接口,扩展 Dubbo 框架的常用手段指北</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/31 加餐:深潜 Directory 实现,探秘服务目录玄机.md.html">31 加餐:深潜 Directory 实现,探秘服务目录玄机</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/32 路由机制:请求到底怎么走,它说了算(上).md.html">32 路由机制:请求到底怎么走,它说了算(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/33 路由机制:请求到底怎么走,它说了算(下).md.html">33 路由机制:请求到底怎么走,它说了算(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/34 加餐:初探 Dubbo 动态配置的那些事儿.md.html">34 加餐:初探 Dubbo 动态配置的那些事儿</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/35 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(上).md.html">35 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/36 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(下).md.html">36 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/37 集群容错:一个好汉三个帮(上).md.html">37 集群容错:一个好汉三个帮(上)</a>
</li>
<li>
<a class="current-tab" href="/专栏/Dubbo源码解读与实战-完/38 集群容错:一个好汉三个帮(下).md.html">38 集群容错:一个好汉三个帮(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/39 加餐多个返回值不用怕Merger 合并器来帮忙.md.html">39 加餐多个返回值不用怕Merger 合并器来帮忙</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/40 加餐模拟远程调用Mock 机制帮你搞定.md.html">40 加餐模拟远程调用Mock 机制帮你搞定</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/41 加餐:一键通关服务发布全流程.md.html">41 加餐:一键通关服务发布全流程</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/42 加餐:服务引用流程全解析.md.html">42 加餐:服务引用流程全解析</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/43 服务自省设计方案:新版本新方案.md.html">43 服务自省设计方案:新版本新方案</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/44 元数据方案深度剖析,如何避免注册中心数据量膨胀?.md.html">44 元数据方案深度剖析,如何避免注册中心数据量膨胀?</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/45 加餐:深入服务自省方案中的服务发布订阅(上).md.html">45 加餐:深入服务自省方案中的服务发布订阅(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/46 加餐:深入服务自省方案中的服务发布订阅(下).md.html">46 加餐:深入服务自省方案中的服务发布订阅(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/47 配置中心设计与实现:集中化配置 and 本地化配置,我都要(上).md.html">47 配置中心设计与实现:集中化配置 and 本地化配置,我都要(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/48 配置中心设计与实现:集中化配置 and 本地化配置,我都要(下).md.html">48 配置中心设计与实现:集中化配置 and 本地化配置,我都要(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/49 结束语 认真学习,缩小差距.md.html">49 结束语 认真学习,缩小差距</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>38 集群容错:一个好汉三个帮(下)</h1>
<p>你好,我是杨四正,今天我和你分享的主题是集群容错:一个好汉三个帮(下篇)。</p>
<p>在上一课时,我们介绍了 Dubbo Cluster 层中集群容错机制的基础知识,还说明了 Cluster 接口的定义以及其各个实现类的核心功能。同时,我们还分析了 AbstractClusterInvoker 抽象类以及 AbstractCluster 抽象实现类的核心实现。</p>
<p>那接下来在本课时,我们将介绍 Cluster 接口的全部实现类,以及相关的 Cluster Invoker 实现类。</p>
<h3>FailoverClusterInvoker</h3>
<p>通过前面对 Cluster 接口的介绍我们知道Cluster 默认的扩展实现是 FailoverCluster其 doJoin() 方法中会创建一个 FailoverClusterInvoker 对象并返回,具体实现如下:</p>
<pre><code>public &lt;T&gt; AbstractClusterInvoker&lt;T&gt; doJoin(Directory&lt;T&gt; directory) throws RpcException {
return new FailoverClusterInvoker&lt;&gt;(directory);
}
</code></pre>
<p><strong>FailoverClusterInvoker 会在调用失败的时候,自动切换 Invoker 进行重试</strong>。下面来看 FailoverClusterInvoker 的核心实现:</p>
<pre><code>public Result doInvoke(Invocation invocation, final List&lt;Invoker&lt;T&gt;&gt; invokers, LoadBalance loadbalance) throws RpcException {
List&lt;Invoker&lt;T&gt;&gt; copyInvokers = invokers;
// 检查copyInvokers集合是否为空如果为空会抛出异常
checkInvokers(copyInvokers, invocation);
String methodName = RpcUtils.getMethodName(invocation);
// 参数重试次数默认重试2次总共执行3次
int len = getUrl().getMethodParameter(methodName, RETRIES_KEY, DEFAULT_RETRIES) + 1;
if (len &lt;= 0) {
len = 1;
}
RpcException le = null;
// 记录已经尝试调用过的Invoker对象
List&lt;Invoker&lt;T&gt;&gt; invoked = new ArrayList&lt;Invoker&lt;T&gt;&gt;(copyInvokers.size());
Set&lt;String&gt; providers = new HashSet&lt;String&gt;(len);
for (int i = 0; i &lt; len; i++) {
// 第一次传进来的invokers已经check过了第二次则是重试需要重新获取最新的服务列表
if (i &gt; 0) {
checkWhetherDestroyed();
// 这里会重新调用Directory.list()方法获取Invoker列表
copyInvokers = list(invocation);
// 检查copyInvokers集合是否为空如果为空会抛出异常
checkInvokers(copyInvokers, invocation);
}
// 通过LoadBalance选择Invoker对象这里传入的invoked集合
// 就是前面介绍AbstractClusterInvoker.select()方法中的selected集合
Invoker&lt;T&gt; invoker = select(loadbalance, invocation, copyInvokers, invoked);
// 记录此次要尝试调用的Invoker对象下一次重试时就会过滤这个服务
invoked.add(invoker);
RpcContext.getContext().setInvokers((List) invoked);
try {
// 调用目标Invoker对象的invoke()方法,完成远程调用
Result result = invoker.invoke(invocation);
// 经过尝试之后终于成功这里会打印一个警告日志将尝试过来的Provider地址打印出来
if (le != null &amp;&amp; logger.isWarnEnabled()) {
logger.warn(&quot;...&quot;);
}
return result;
} catch (RpcException e) {
if (e.isBiz()) { // biz exception.
throw e;
}
le = e;
} catch (Throwable e) { // 抛出异常,表示此次尝试失败,会进行重试
le = new RpcException(e.getMessage(), e);
} finally {
// 记录尝试过的Provider地址会在上面的警告日志中打印出来
providers.add(invoker.getUrl().getAddress());
}
}
// 达到重试次数上限之后会抛出异常其中会携带调用的方法名、尝试过的Provider节点的地址(providers集合)、全部的Provider个数(copyInvokers集合)以及Directory信息
throw new RpcException(le.getCode(), &quot;...&quot;);
}
</code></pre>
<h3>FailbackClusterInvoker</h3>
<p>FailbackCluster 是 Cluster 接口的另一个扩展实现,扩展名是 failback其 doJoin() 方法中创建的 Invoker 对象是 FailbackClusterInvoker 类型,具体实现如下:</p>
<pre><code>public &lt;T&gt; AbstractClusterInvoker&lt;T&gt; doJoin(Directory&lt;T&gt; directory) throws RpcException {
return new FailbackClusterInvoker&lt;&gt;(directory);
}
</code></pre>
<p><strong>FailbackClusterInvoker 在请求失败之后,返回一个空结果给 Consumer同时还会添加一个定时任务对失败的请求进行重试</strong>。下面来看 FailbackClusterInvoker 的具体实现:</p>
<pre><code>protected Result doInvoke(Invocation invocation, List&lt;Invoker&lt;T&gt;&gt; invokers, LoadBalance loadbalance) throws RpcException {
Invoker&lt;T&gt; invoker = null;
try {
// 检测Invoker集合是否为空
checkInvokers(invokers, invocation);
// 调用select()方法得到此次尝试的Invoker对象
invoker = select(loadbalance, invocation, invokers, null);
// 调用invoke()方法完成远程调用
return invoker.invoke(invocation);
} catch (Throwable e) {
// 请求失败之后,会添加一个定时任务进行重试
addFailed(loadbalance, invocation, invokers, invoker);
return AsyncRpcResult.newDefaultAsyncResult(null, null, invocation); // 请求失败时,会返回一个空结果
}
}
</code></pre>
<p>在 doInvoke() 方法中,请求失败时会调用 addFailed() 方法添加定时任务进行重试,默认每隔 5 秒执行一次,总共重试 3 次,具体实现如下:</p>
<pre><code>private void addFailed(LoadBalance loadbalance, Invocation invocation, List&lt;Invoker&lt;T&gt;&gt; invokers, Invoker&lt;T&gt; lastInvoker) {
if (failTimer == null) {
synchronized (this) {
if (failTimer == null) { // Double Check防止并发问题
// 初始化时间轮这个时间轮有32个槽每个槽代表1秒
failTimer = new HashedWheelTimer(
new NamedThreadFactory(&quot;failback-cluster-timer&quot;, true),
1,
TimeUnit.SECONDS, 32, failbackTasks);
}
}
}
// 创建一个定时任务
RetryTimerTask retryTimerTask = new RetryTimerTask(loadbalance, invocation, invokers, lastInvoker, retries, RETRY_FAILED_PERIOD);
try {
// 将定时任务添加到时间轮中
failTimer.newTimeout(retryTimerTask, RETRY_FAILED_PERIOD, TimeUnit.SECONDS);
} catch (Throwable e) {
logger.error(&quot;...&quot;);
}
}
</code></pre>
<p>在 RetryTimerTask 定时任务中,会重新调用 select() 方法筛选合适的 Invoker 对象,并尝试进行请求。如果请求再次失败且重试次数未达到上限,则调用 rePut() 方法再次添加定时任务等待进行重试如果请求成功也不会返回任何结果。RetryTimerTask 的核心实现如下:</p>
<pre><code>public void run(Timeout timeout) {
try {
// 重新选择Invoker对象注意这里会将上次重试失败的Invoker作为selected集合传入
Invoker&lt;T&gt; retryInvoker = select(loadbalance, invocation, invokers, Collections.singletonList(lastInvoker));
lastInvoker = retryInvoker;
retryInvoker.invoke(invocation); // 请求对应的Provider节点
} catch (Throwable e) {
if ((++retryTimes) &gt;= retries) { // 重试次数达到上限,输出警告日志
logger.error(&quot;...&quot;);
} else {
rePut(timeout); // 重试次数未达到上限,则重新添加定时任务,等待重试
}
}
}
private void rePut(Timeout timeout) {
if (timeout == null) { // 边界检查
return;
}
Timer timer = timeout.timer();
if (timer.isStop() || timeout.isCancelled()) { // 检查时间轮状态、检查定时任务状态
return;
}
// 重新添加定时任务
timer.newTimeout(timeout.task(), tick, TimeUnit.SECONDS);
}
</code></pre>
<h3>FailfastClusterInvoker</h3>
<p>FailfastCluster 的扩展名是 failfast在其 doJoin() 方法中会创建 FailfastClusterInvoker 对象,具体实现如下:</p>
<pre><code>public &lt;T&gt; AbstractClusterInvoker&lt;T&gt; doJoin(Directory&lt;T&gt; directory) throws RpcException {
return new FailfastClusterInvoker&lt;&gt;(directory);
}
</code></pre>
<p><strong>FailfastClusterInvoker 只会进行一次请求,请求失败之后会立即抛出异常,这种策略适合非幂等的操作</strong>,具体实现如下:</p>
<pre><code>public Result doInvoke(Invocation invocation, List&lt;Invoker&lt;T&gt;&gt; invokers, LoadBalance loadbalance) throws RpcException {
checkInvokers(invokers, invocation);
// 调用select()得到此次要调用的Invoker对象
Invoker&lt;T&gt; invoker = select(loadbalance, invocation, invokers, null);
try {
return invoker.invoke(invocation); // 发起请求
} catch (Throwable e) {
// 请求失败,直接抛出异常
if (e instanceof RpcException &amp;&amp; ((RpcException) e).isBiz()) {
throw (RpcException) e;
}
throw new RpcException(&quot;...&quot;);
}
}
</code></pre>
<h3>FailsafeClusterInvoker</h3>
<p>FailsafeCluster 的扩展名是 failsafe在其 doJoin() 方法中会创建 FailsafeClusterInvoker 对象,具体实现如下:</p>
<pre><code>public &lt;T&gt; AbstractClusterInvoker&lt;T&gt; doJoin(Directory&lt;T&gt; directory) throws RpcException {
return new FailsafeClusterInvoker&lt;&gt;(directory);
}
</code></pre>
<p><strong>FailsafeClusterInvoker 只会进行一次请求,请求失败之后会返回一个空结果</strong>,具体实现如下:</p>
<pre><code>public Result doInvoke(Invocation invocation, List&lt;Invoker&lt;T&gt;&gt; invokers, LoadBalance loadbalance) throws RpcException {
try {
// 检测Invoker集合是否为空
checkInvokers(invokers, invocation);
// 调用select()得到此次要调用的Invoker对象
Invoker&lt;T&gt; invoker = select(loadbalance, invocation, invokers, null);
// 发起请求
return invoker.invoke(invocation);
} catch (Throwable e) {
// 请求失败之后,会打印一行日志并返回空结果
logger.error(&quot;...&quot;);
return AsyncRpcResult.newDefaultAsyncResult(null, null, invocation);
}
}
</code></pre>
<h3>ForkingClusterInvoker</h3>
<p>ForkingCluster 的扩展名称为 forking在其 doJoin() 方法中,会创建一个 ForkingClusterInvoker 对象,具体实现如下:</p>
<pre><code>public &lt;T&gt; AbstractClusterInvoker&lt;T&gt; doJoin(Directory&lt;T&gt; directory) throws RpcException {
return new ForkingClusterInvoker&lt;&gt;(directory);
}
</code></pre>
<p>ForkingClusterInvoker 中会<strong>维护一个线程池</strong>executor 字段,通过 Executors.newCachedThreadPool() 方法创建的线程池),<strong>并发调用多个 Provider 节点,只要有一个 Provider 节点成功返回了结果ForkingClusterInvoker 的 doInvoke() 方法就会立即结束运行</strong></p>
<p>ForkingClusterInvoker 主要是为了应对一些实时性要求较高的读操作,因为没有并发控制的多线程写入,可能会导致数据不一致。</p>
<p>ForkingClusterInvoker.doInvoke() 方法首先从 Invoker 集合中选出指定个数forks 参数决定)的 Invoker 对象,然后通过 executor 线程池并发调用这些 Invoker并将请求结果存储在 ref 阻塞队列中,则当前线程会阻塞在 ref 队列上,等待第一个请求结果返回。下面是 ForkingClusterInvoker 的具体实现:</p>
<pre><code>public Result doInvoke(final Invocation invocation, List&lt;Invoker&lt;T&gt;&gt; invokers, LoadBalance loadbalance) throws RpcException {
try {
// 检查Invoker集合是否为空
checkInvokers(invokers, invocation);
final List&lt;Invoker&lt;T&gt;&gt; selected;
// 从URL中获取forks参数作为并发请求的上限默认值为2
final int forks = getUrl().getParameter(FORKS_KEY, DEFAULT_FORKS);
final int timeout = getUrl().getParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT);
if (forks &lt;= 0 || forks &gt;= invokers.size()) {
// 如果forks为负数或是大于Invoker集合的长度会直接并发调用全部Invoker
selected = invokers;
} else {
// 按照forks指定的并发度选择此次并发调用的Invoker对象
selected = new ArrayList&lt;&gt;(forks);
while (selected.size() &lt; forks) {
Invoker&lt;T&gt; invoker = select(loadbalance, invocation, invokers, selected);
if (!selected.contains(invoker)) {
selected.add(invoker); // 避免重复选择
}
}
}
RpcContext.getContext().setInvokers((List) selected);
// 记录失败的请求个数
final AtomicInteger count = new AtomicInteger();
// 用于记录请求结果
final BlockingQueue&lt;Object&gt; ref = new LinkedBlockingQueue&lt;&gt;();
for (final Invoker&lt;T&gt; invoker : selected) { // 遍历 selected 列表
executor.execute(() -&gt; { // 为每个Invoker创建一个任务并提交到线程池中
try {
// 发起请求
Result result = invoker.invoke(invocation);
// 将请求结果写到ref队列中
ref.offer(result);
} catch (Throwable e) {
int value = count.incrementAndGet();
if (value &gt;= selected.size()) {
// 如果失败的请求个数超过了并发请求的个数则向ref队列中写入异常
ref.offer(e);
}
}
});
}
try {
// 当前线程会阻塞等待任意一个请求结果的出现
Object ret = ref.poll(timeout, TimeUnit.MILLISECONDS);
if (ret instanceof Throwable) { // 如果结果类型为Throwable则抛出异常
Throwable e = (Throwable) ret;
throw new RpcException(&quot;...&quot;);
}
return (Result) ret; // 返回结果
} catch (InterruptedException e) {
throw new RpcException(&quot;...&quot;);
}
} finally {
// 清除上下文信息
RpcContext.getContext().clearAttachments();
}
}
</code></pre>
<h3>BroadcastClusterInvoker</h3>
<p>BroadcastCluster 这个 Cluster 实现类的扩展名为 broadcast在其 doJoin() 方法中创建的是 BroadcastClusterInvoker 类型的 Invoker 对象,具体实现如下:</p>
<pre><code>public &lt;T&gt; AbstractClusterInvoker&lt;T&gt; doJoin(Directory&lt;T&gt; directory) throws RpcException {
return new BroadcastClusterInvoker&lt;&gt;(directory);
}
</code></pre>
<p><strong>在 BroadcastClusterInvoker 中,会逐个调用每个 Provider 节点,其中任意一个 Provider 节点报错,都会在全部调用结束之后抛出异常</strong>。BroadcastClusterInvoker<strong>通常用于通知类的操作</strong>,例如通知所有 Provider 节点更新本地缓存。</p>
<p>下面来看 BroadcastClusterInvoker 的具体实现:</p>
<pre><code>public Result doInvoke(final Invocation invocation, List&lt;Invoker&lt;T&gt;&gt; invokers, LoadBalance loadbalance) throws RpcException {
// 检测Invoker集合是否为空
checkInvokers(invokers, invocation);
RpcContext.getContext().setInvokers((List) invokers);
RpcException exception = null; // 用于记录失败请求的相关异常信息
Result result = null;
// 遍历所有Invoker对象
for (Invoker&lt;T&gt; invoker : invokers) {
try {
// 发起请求
result = invoker.invoke(invocation);
} catch (RpcException e) {
exception = e;
logger.warn(e.getMessage(), e);
} catch (Throwable e) {
exception = new RpcException(e.getMessage(), e);
logger.warn(e.getMessage(), e);
}
}
if (exception != null) { // 出现任何异常,都会在这里抛出
throw exception;
}
return result;
}
</code></pre>
<h3>AvailableClusterInvoker</h3>
<p>AvailableCluster 这个 Cluster 实现类的扩展名为 available在其 join() 方法中创建的是 AvailableClusterInvoker 类型的 Invoker 对象,具体实现如下:</p>
<pre><code>public &lt;T&gt; Invoker&lt;T&gt; join(Directory&lt;T&gt; directory) throws RpcException {
return new AvailableClusterInvoker&lt;&gt;(directory);
}
</code></pre>
<p>在 AvailableClusterInvoker 的 doInvoke() 方法中,会遍历整个 Invoker 集合,<strong>逐个调用对应的 Provider 节点,当遇到第一个可用的 Provider 节点时,就尝试访问该 Provider 节点,成功则返回结果;如果访问失败,则抛出异常终止遍历</strong></p>
<p>下面是 AvailableClusterInvoker 的具体实现:</p>
<pre><code>public Result doInvoke(Invocation invocation, List&lt;Invoker&lt;T&gt;&gt; invokers, LoadBalance loadbalance) throws RpcException {
for (Invoker&lt;T&gt; invoker : invokers) { // 遍历整个Invoker集合
if (invoker.isAvailable()) { // 检测该Invoker是否可用
// 发起请求,调用失败时的异常会直接抛出
return invoker.invoke(invocation);
}
}
// 没有找到可用的Invoker也会抛出异常
throw new RpcException(&quot;No provider available in &quot; + invokers);
}
</code></pre>
<h3>MergeableClusterInvoker</h3>
<p>MergeableCluster 这个 Cluster 实现类的扩展名为 mergeable在其 doJoin() 方法中创建的是 MergeableClusterInvoker 类型的 Invoker 对象,具体实现如下:</p>
<pre><code>public &lt;T&gt; AbstractClusterInvoker&lt;T&gt; doJoin(Directory&lt;T&gt; directory) throws RpcException {
return new MergeableClusterInvoker&lt;T&gt;(directory);
}
</code></pre>
<p><strong>MergeableClusterInvoker 会对多个 Provider 节点返回结果合并</strong>。如果请求的方法没有配置 Merger 合并器(即没有指定 merger 参数),则不会进行结果合并,而是直接将第一个可用的 Invoker 结果返回。下面来看 MergeableClusterInvoker 的具体实现:</p>
<pre><code>protected Result doInvoke(Invocation invocation, List&lt;Invoker&lt;T&gt;&gt; invokers, LoadBalance loadbalance) throws RpcException {
checkInvokers(invokers, invocation);
String merger = getUrl().getMethodParameter(invocation.getMethodName(), MERGER_KEY);
// 判断要调用的目标方法是否有合并器,如果没有,则不会进行合并,
// 找到第一个可用的Invoker直接调用并返回结果
if (ConfigUtils.isEmpty(merger)) {
for (final Invoker&lt;T&gt; invoker : invokers) {
if (invoker.isAvailable()) {
try {
return invoker.invoke(invocation);
} catch (RpcException e) {
if (e.isNoInvokerAvailableAfterFilter()) {
log.debug(&quot;No available provider for service&quot; + getUrl().getServiceKey() + &quot; on group &quot; + invoker.getUrl().getParameter(GROUP_KEY) + &quot;, will continue to try another group.&quot;);
} else {
throw e;
}
}
}
}
return invokers.iterator().next().invoke(invocation);
}
// 确定目标方法的返回值类型
Class&lt;?&gt; returnType;
try {
returnType = getInterface().getMethod(
invocation.getMethodName(), invocation.getParameterTypes()).getReturnType();
} catch (NoSuchMethodException e) {
returnType = null;
}
// 调用每个Invoker对象(异步方式)将请求结果记录到results集合中
Map&lt;String, Result&gt; results = new HashMap&lt;&gt;();
for (final Invoker&lt;T&gt; invoker : invokers) {
RpcInvocation subInvocation = new RpcInvocation(invocation, invoker);
subInvocation.setAttachment(ASYNC_KEY, &quot;true&quot;);
results.put(invoker.getUrl().getServiceKey(), invoker.invoke(subInvocation));
}
Object result = null;
List&lt;Result&gt; resultList = new ArrayList&lt;Result&gt;(results.size());
// 等待结果返回
for (Map.Entry&lt;String, Result&gt; entry : results.entrySet()) {
Result asyncResult = entry.getValue();
try {
Result r = asyncResult.get();
if (r.hasException()) {
log.error(&quot;Invoke &quot; + getGroupDescFromServiceKey(entry.getKey()) +
&quot; failed: &quot; + r.getException().getMessage(),
r.getException());
} else {
resultList.add(r);
}
} catch (Exception e) {
throw new RpcException(&quot;Failed to invoke service &quot; + entry.getKey() + &quot;: &quot; + e.getMessage(), e);
}
}
if (resultList.isEmpty()) {
return AsyncRpcResult.newDefaultAsyncResult(invocation);
} else if (resultList.size() == 1) {
return resultList.iterator().next();
}
if (returnType == void.class) {
return AsyncRpcResult.newDefaultAsyncResult(invocation);
}
// merger如果以&quot;.&quot;开头,后面为方法名,这个方法名是远程目标方法的返回类型中的方法
// 得到每个Provider节点返回的结果对象之后会遍历每个返回对象调用merger参数指定的方法
if (merger.startsWith(&quot;.&quot;)) {
merger = merger.substring(1);
Method method;
try {
method = returnType.getMethod(merger, returnType);
} catch (NoSuchMethodException e) {
throw new RpcException(&quot;Can not merge result because missing method [ &quot; + merger + &quot; ] in class [ &quot; +
returnType.getName() + &quot; ]&quot;);
}
if (!Modifier.isPublic(method.getModifiers())) {
method.setAccessible(true);
}
// resultList集合保存了所有的返回对象method是Method对象也就是merger指定的方法
// result是最后返回调用方的结果
result = resultList.remove(0).getValue();
try {
if (method.getReturnType() != void.class
&amp;&amp; method.getReturnType().isAssignableFrom(result.getClass())) {
for (Result r : resultList) { // 反射调用
result = method.invoke(result, r.getValue());
}
} else {
for (Result r : resultList) { // 反射调用
method.invoke(result, r.getValue());
}
}
} catch (Exception e) {
throw new RpcException(&quot;Can not merge result: &quot; + e.getMessage(), e);
}
} else {
Merger resultMerger;
if (ConfigUtils.isDefault(merger)) {
// merger参数为true或者default表示使用默认的Merger扩展实现完成合并
// 在后面课时中会介绍Merger接口
resultMerger = MergerFactory.getMerger(returnType);
} else {
//merger参数指定了Merger的扩展名称则使用SPI查找对应的Merger扩展实现对象
resultMerger = ExtensionLoader.getExtensionLoader(Merger.class).getExtension(merger);
}
if (resultMerger != null) {
List&lt;Object&gt; rets = new ArrayList&lt;Object&gt;(resultList.size());
for (Result r : resultList) {
rets.add(r.getValue());
}
// 执行合并操作
result = resultMerger.merge(
rets.toArray((Object[]) Array.newInstance(returnType, 0)));
} else {
throw new RpcException(&quot;There is no merger to merge result.&quot;);
}
}
return AsyncRpcResult.newDefaultAsyncResult(result, invocation);
}
</code></pre>
<h3>ZoneAwareClusterInvoker</h3>
<p>ZoneAwareCluster 这个 Cluster 实现类的扩展名为 zone-aware在其 doJoin() 方法中创建的是 ZoneAwareClusterInvoker 类型的 Invoker 对象,具体实现如下:</p>
<pre><code>protected &lt;T&gt; AbstractClusterInvoker&lt;T&gt; doJoin(Directory&lt;T&gt; directory) throws RpcException {
return new ZoneAwareClusterInvoker&lt;T&gt;(directory);
}
</code></pre>
<p>在 Dubbo 中使用多个注册中心的架构如下图所示:</p>
<p><img src="assets/Ciqc1F_IvtuAXngKAADJgn-frEE576.png" alt="png" /></p>
<p>双注册中心结构图</p>
<p>Consumer 可以使用 ZoneAwareClusterInvoker 先在多个注册中心之间进行选择,选定注册中心之后,再选择 Provider 节点,如下图所示:</p>
<p><img src="assets/Ciqc1F_IvuOAFBfoAAD_GvyhrZY880.png" alt="png" /></p>
<p>ZoneAwareClusterInvoker 在多注册中心之间进行选择的策略有以下四种。</p>
<ol>
<li>找到<strong>preferred 属性为 true 的注册中心,它是优先级最高的注册中心</strong>,只有该中心无可用 Provider 节点时,才会回落到其他注册中心。</li>
<li>根据<strong>请求中的 zone key 做匹配</strong>,优先派发到相同 zone 的注册中心。</li>
<li>根据<strong>权重</strong>(也就是注册中心配置的 weight 属性)进行轮询。</li>
<li>如果上面的策略都未命中,则选择<strong>第一个可用的 Provider 节点</strong></li>
</ol>
<p>下面来看 ZoneAwareClusterInvoker 的具体实现:</p>
<pre><code>public Result doInvoke(Invocation invocation, final List&lt;Invoker&lt;T&gt;&gt; invokers, LoadBalance loadbalance) throws RpcException {
// 首先找到preferred属性为true的注册中心它是优先级最高的注册中心只有该中心无可用 Provider 节点时,才会回落到其他注册中心
for (Invoker&lt;T&gt; invoker : invokers) {
MockClusterInvoker&lt;T&gt; mockClusterInvoker = (MockClusterInvoker&lt;T&gt;) invoker;
if (mockClusterInvoker.isAvailable() &amp;&amp; mockClusterInvoker.getRegistryUrl()
.getParameter(REGISTRY_KEY + &quot;.&quot; + PREFERRED_KEY, false)) {
return mockClusterInvoker.invoke(invocation);
}
}
// 根据请求中的registry_zone做匹配优先派发到相同zone的注册中心
String zone = (String) invocation.getAttachment(REGISTRY_ZONE);
if (StringUtils.isNotEmpty(zone)) {
for (Invoker&lt;T&gt; invoker : invokers) {
MockClusterInvoker&lt;T&gt; mockClusterInvoker = (MockClusterInvoker&lt;T&gt;) invoker;
if (mockClusterInvoker.isAvailable() &amp;&amp; zone.equals(mockClusterInvoker.getRegistryUrl().getParameter(REGISTRY_KEY + &quot;.&quot; + ZONE_KEY))) {
return mockClusterInvoker.invoke(invocation);
}
}
String force = (String) invocation.getAttachment(REGISTRY_ZONE_FORCE);
if (StringUtils.isNotEmpty(force) &amp;&amp; &quot;true&quot;.equalsIgnoreCase(force)) {
throw new IllegalStateException(&quot;...&quot;);
}
}
// 根据权重也就是注册中心配置的weight属性进行轮询
Invoker&lt;T&gt; balancedInvoker = select(loadbalance, invocation, invokers, null);
if (balancedInvoker.isAvailable()) {
return balancedInvoker.invoke(invocation);
}
// 选择第一个可用的 Provider 节点
for (Invoker&lt;T&gt; invoker : invokers) {
MockClusterInvoker&lt;T&gt; mockClusterInvoker = (MockClusterInvoker&lt;T&gt;) invoker;
if (mockClusterInvoker.isAvailable()) {
return mockClusterInvoker.invoke(invocation);
}
}
throw new RpcException(&quot;No provider available in &quot; + invokers);
}
</code></pre>
<h3>总结</h3>
<p>本课时我们重点介绍了 Dubbo 中 Cluster 接口的各个实现类的原理以及相关 Invoker 的实现原理。这里重点分析的 Cluster 实现有Failover Cluster、Failback Cluster、Failfast Cluster、Failsafe Cluster、Forking Cluster、Broadcast Cluster、Available Cluster 和 Mergeable Cluster。除此之外我们还分析了多注册中心的 ZoneAware Cluster 实现。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Dubbo源码解读与实战-完/37 集群容错:一个好汉三个帮(上).md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Dubbo源码解读与实战-完/39 加餐多个返回值不用怕Merger 合并器来帮忙.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70996f6e9c163d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>