This commit is contained in:
by931
2022-09-06 22:30:37 +08:00
parent 66970f3e38
commit 3d6528675a
796 changed files with 3382 additions and 3382 deletions

View File

@@ -295,11 +295,11 @@ function hide_canvas() {
<h3>为什么要学习 Dubbo</h3>
<p>我们在谈论任何一项技术的时候,都需要强调它所适用的业务场景,因为: <strong>技术之所以有价值,就是因为它解决了一些业务场景难题。</strong></p>
<p>一家公司由小做大,业务会不断发展,随之而来的是 DAU、订单量、数据量的不断增长用来支撑业务的系统复杂度也会不断提高模块之间的依赖关系也会日益复杂。这时候我们一般会从单体架构进入集群架构如下图所示在集群架构中通过负载均衡技术将流量尽可能均摊到集群中的每台机器上以此克服单台机器硬件资源的限制做到横向扩展。</p>
<p><img src="assets/Ciqc1F8eQ_eAbHovAAC1BISTRQE943.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F8eQ_eAbHovAAC1BISTRQE943.png" alt="png" /></p>
<p>单体架构 VS 集群架构</p>
<p>之后,又由于业务系统本身的实现较为复杂、扩展性较差、性能也有上限,代码和功能的复用能力较弱,我们会将一个巨型业务系统拆分成多个微服务,根据不同服务对资源的不同要求,选择更合理的硬件资源。例如,有些流量较小的服务只需要几台机器构成的集群即可,而核心业务则需要成百上千的机器来支持,这样就可以最大化系统资源的利用率。</p>
<p>另外一个好处是,可以在服务维度进行重用,在需要某个服务的时候,直接接入即可,从而提高开发效率。拆分成独立的服务之后(如下图所示),整个服务可以最大化地实现重用,也可以更加灵活地扩展。</p>
<p><img src="assets/Ciqc1F8eRAqAKU1gAAGcFDQgvSc126.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1F8eRAqAKU1gAAGcFDQgvSc126.png" alt="png" /></p>
<p>微服务架构图</p>
<p>但是在微服务架构落地的过程中,我们需要解决的问题有很多,如:</p>
<ul>
@@ -318,15 +318,15 @@ function hide_canvas() {
<p>简单地说, <strong>Dubbo 是一个分布式服务框架,致力于提供高性能、透明化的 RPC 远程服务调用方案以及服务治理方案,以帮助我们解决微服务架构落地时的问题。</strong></p>
<p>Dubbo 是由阿里开源,后来加入了 Apache 基金会,目前已经从孵化器毕业,成为 Apache 的顶级项目。Apache Dubbo 目前已经有接近 32.8 K 的 Star、21.4 K 的 Fork其热度可见一斑 <strong>很多互联网大厂(如阿里、滴滴、去哪儿网等)都是直接使用 Dubbo 作为其 RPC 框架,也有些大厂会基于 Dubbo 进行二次开发实现自己的 RPC 框架</strong> ,如当当网的 DubboX。</p>
<p>作为一名 Java 工程师,深入掌握 Dubbo 的原理和实现已经是大势所趋,并且成为你职场竞争力的关键项。拉勾网显示,研发工程师、架构师等高薪岗位,都要求你熟悉并曾经深入使用某种 RPC 框架,一线大厂更是要求你至少深入了解一款 RPC 框架的原理和核心实现。</p>
<p><img src="assets/CgqCHl8eRBiAdm-9AACm_9hf8ac565.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1F8eRCaAdTaTAACqwi3sVSo733.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CgqCHl8eRC2Acm_pAACV8eY8-C8598.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl8eRBiAdm-9AACm_9hf8ac565.png" alt="png" /></p>
<p><img src="assets/Ciqc1F8eRCaAdTaTAACqwi3sVSo733.png" alt="png" /></p>
<p><img src="assets/CgqCHl8eRC2Acm_pAACV8eY8-C8598.png" alt="png" /></p>
<p>(职位信息来源:拉勾网)</p>
<p>而 Dubbo 就是首选。Dubbo 和 Spring Cloud 是目前主流的微服务框架,阿里、京东、小米、携程、去哪儿网等互联网公司的基础设施早已落成,并且后续的很多项目还是以 Dubbo 为主。Dubbo 重启之后,已经开始规划 3.0 版本,相信后面还会有更加惊艳的表现。</p>
<p>另外RPC 框架的核心原理和设计都是相通的,阅读过 Dubbo 源码之后,你再去了解其他 RPC 框架的代码,就是一件非常简单的事情了。</p>
<h3>阅读 Dubbo 源码的痛点</h3>
<p>学习和掌握一项技能的时候,一般都是按照“是什么”“怎么用”“为什么”(原理)逐层深入的:</p>
<p><img src="assets/CgqCHl8eRDyAaP3fAABjUNh74KM999.png" alt="Drawing 5.png" /></p>
<p><img src="assets/CgqCHl8eRDyAaP3fAABjUNh74KM999.png" alt="png" /></p>
<p>同样,你可以通过阅读官方文档或是几篇介绍性的文章,迅速了解 Dubbo 是什么;接下来,再去上手,用 Dubbo 写几个项目,从而更加全面地熟悉 Dubbo 的使用方式和特性,成为一名“熟练工”,但这也是很多开发者所处的阶段。而“有技术追求”的开发者,一般不会满足于每天只是写写业务代码,而是会开始研究 Dubbo 的源码实现以及底层原理,这就对应了上图中的核心层:“原理”。</p>
<p>而开始阅读源码时,不少开发者会提前去网上查找资料,或者直接埋头钻研源码,并因为这样的学习路径而普遍面临一些痛点问题:</p>
<ul>

View File

@@ -300,7 +300,7 @@ function hide_canvas() {
</ul>
<h3>Dubbo 架构简介</h3>
<p>为便于你更好理解和学习,在开始搭建 Dubbo 源码环境之前,我们先来简单介绍一下 Dubbo 架构中的核心角色,帮助你简单回顾一下 Dubbo 的架构,也帮助不熟悉 Dubbo 的小伙伴快速了解 Dubbo。下图展示了 Dubbo 核心架构:</p>
<p><img src="assets/CgqCHl8eRaCAW4-LAAB7_C-aKWA601.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl8eRaCAW4-LAAB7_C-aKWA601.png" alt="png" /></p>
<p>Dubbo 核心架构图</p>
<ul>
<li><strong>Registry注册中心。</strong> 负责服务地址的注册与查找,服务的 Provider 和 Consumer 只在启动时与注册中心交互。注册中心通过长连接感知 Provider 的存在,在 Provider 出现宕机的时候,注册中心会立即推送相关事件通知 Consumer。</li>
@@ -324,38 +324,38 @@ function hide_canvas() {
<p>然后,在 IDEA 中导入源码,因为这个导入过程中会下载所需的依赖包,所以会耗费点时间。</p>
<h3>Dubbo源码核心模块</h3>
<p>在 IDEA 成功导入 Dubbo 源码之后,你看到的项目结构如下图所示:</p>
<p><img src="assets/Ciqc1F8eRcOAdzNmAADHxcenG7I722.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1F8eRcOAdzNmAADHxcenG7I722.png" alt="png" /></p>
<p>下面我们就来简单介绍一下这些核心模块的功能,至于详细分析,在后面的课时中我们还会继续讲解。</p>
<ul>
<li><strong>dubbo-common 模块:</strong> Dubbo 的一个公共模块,其中有很多工具类以及公共逻辑,例如课程后面紧接着要介绍的 Dubbo SPI 实现、时间轮实现、动态编译器等。</li>
</ul>
<p><img src="assets/CgqCHl8eRfWANQSTAAHowsC6F8s134.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl8eRfWANQSTAAHowsC6F8s134.png" alt="png" /></p>
<ul>
<li><strong>dubbo-remoting 模块:</strong> Dubbo 的远程通信模块,其中的子模块依赖各种开源组件实现远程通信。在 dubbo-remoting-api 子模块中定义该模块的抽象概念在其他子模块中依赖其他开源组件进行实现例如dubbo-remoting-netty4 子模块依赖 Netty 4 实现远程通信dubbo-remoting-zookeeper 通过 Apache Curator 实现与 ZooKeeper 集群的交互。</li>
</ul>
<p><img src="assets/Ciqc1F8eRgCAR30EAABc4PYop3w206.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Ciqc1F8eRgCAR30EAABc4PYop3w206.png" alt="png" /></p>
<ul>
<li><strong>dubbo-rpc 模块:</strong> Dubbo 中对远程调用协议进行抽象的模块,其中抽象了各种协议,依赖于 dubbo-remoting 模块的远程调用功能。dubbo-rpc-api 子模块是核心抽象其他子模块是针对具体协议的实现例如dubbo-rpc-dubbo 子模块是对 Dubbo 协议的实现,依赖了 dubbo-remoting-netty4 等 dubbo-remoting 子模块。 dubbo-rpc 模块的实现中只包含一对一的调用,不关心集群的相关内容。</li>
</ul>
<p><img src="assets/Ciqc1F8eRguAA8jOAABqHomePJk138.png" alt="Drawing 6.png" /></p>
<p><img src="assets/Ciqc1F8eRguAA8jOAABqHomePJk138.png" alt="png" /></p>
<ul>
<li><strong>dubbo-cluster 模块:</strong> Dubbo 中负责管理集群的模块,提供了负载均衡、容错、路由等一系列集群相关的功能,最终的目的是将多个 Provider 伪装为一个 Provider这样 Consumer 就可以像调用一个 Provider 那样调用 Provider 集群了。</li>
<li><strong>dubbo-registry 模块:</strong> Dubbo 中负责与多种开源注册中心进行交互的模块,提供注册中心的能力。其中, dubbo-registry-api 子模块是顶层抽象其他子模块是针对具体开源注册中心组件的具体实现例如dubbo-registry-zookeeper 子模块是 Dubbo 接入 ZooKeeper 的具体实现。</li>
</ul>
<p><img src="assets/CgqCHl8eRhWANEiTAAB2ATuQ2vc619.png" alt="Drawing 7.png" /></p>
<p><img src="assets/CgqCHl8eRhWANEiTAAB2ATuQ2vc619.png" alt="png" /></p>
<ul>
<li><strong>dubbo-monitor 模块:</strong> Dubbo 的监控模块,主要用于统计服务调用次数、调用时间以及实现调用链跟踪的服务。</li>
<li><strong>dubbo-config 模块:</strong> Dubbo 对外暴露的配置都是由该模块进行解析的。例如dubbo-config-api 子模块负责处理 API 方式使用时的相关配置dubbo-config-spring 子模块负责处理与 Spring 集成使用时的相关配置方式。有了 dubbo-config 模块,用户只需要了解 Dubbo 配置的规则即可,无须了解 Dubbo 内部的细节。</li>
</ul>
<p><img src="assets/CgqCHl8eRhyAVJ43AAAaPAwMeQ4525.png" alt="Drawing 8.png" /></p>
<p><img src="assets/CgqCHl8eRhyAVJ43AAAaPAwMeQ4525.png" alt="png" /></p>
<ul>
<li><strong>dubbo-metadata 模块:</strong> Dubbo 的元数据模块本课程后续会详细介绍元数据的内容。dubbo-metadata 模块的实现套路也是有一个 api 子模块进行抽象,然后其他子模块进行具体实现。</li>
</ul>
<p><img src="assets/CgqCHl8eRiSAPFIYAABXCRqgsNA891.png" alt="Drawing 9.png" /></p>
<p><img src="assets/CgqCHl8eRiSAPFIYAABXCRqgsNA891.png" alt="png" /></p>
<ul>
<li><strong>dubbo-configcenter 模块:</strong> Dubbo 的动态配置模块,主要负责外部化配置以及服务治理规则的存储与通知,提供了多个子模块用来接入多种开源的服务发现组件。</li>
</ul>
<p><img src="assets/CgqCHl8eRiuAM7LfAAA9BmMR2zY483.png" alt="Drawing 10.png" /></p>
<p><img src="assets/CgqCHl8eRiuAM7LfAAA9BmMR2zY483.png" alt="png" /></p>
<h3>Dubbo 源码中的 Demo 示例</h3>
<p>在 Dubbo 源码中我们可以看到一个 dubbo-demo 模块,共包括三个非常基础 的 Dubbo 示例项目,分别是: <strong>使用 XML 配置的 Demo 示例、使用注解配置的 Demo 示例</strong> 以及 <strong>直接使用 API 的 Demo 示例</strong> 。下面我们将从这三个示例的角度,简单介绍 Dubbo 的基本使用。同时,这三个项目也将作为后续 Debug Dubbo 源码的入口,我们会根据需要在其之上进行修改 。不过在这儿之前,你需要先启动 ZooKeeper 作为注册中心,然后编写一个业务接口作为 Provider 和 Consumer 的公约。</p>
<h4>启动 ZooKeeper</h4>
@@ -378,7 +378,7 @@ Starting zookeeper ... STARTED # 启动成功
<li>Consumer ,如何使用服务、使用的服务名称是什么、需要传入什么参数、会得到什么响应。</li>
</ul>
<p>dubbo-demo-interface 模块就是定义业务接口的地方,如下图所示:</p>
<p><img src="assets/CgqCHl8eRlWAPwvCAACx42Xn9Dk409.png" alt="Drawing 11.png" /></p>
<p><img src="assets/CgqCHl8eRlWAPwvCAACx42Xn9Dk409.png" alt="png" /></p>
<p>其中DemoService 接口中定义了两个方法:</p>
<pre><code>public interface DemoService {
String sayHello(String name); // 同步调用
@@ -391,7 +391,7 @@ Starting zookeeper ... STARTED # 启动成功
<h4>Demo 1基于 XML 配置</h4>
<p>在 dubbo-demo 模块下的 dubbo-demo-xml 模块,提供了基于 Spring XML 的 Provider 和 Consumer。</p>
<p>我们先来看 dubbo-demo-xml-provider 模块,其结构如下图所示:</p>
<p><img src="assets/CgqCHl8eRmKAT8LjAADV8C5fM8E391.png" alt="Drawing 12.png" /></p>
<p><img src="assets/CgqCHl8eRmKAT8LjAADV8C5fM8E391.png" alt="png" /></p>
<p>在其 pom.xml 中除了一堆 dubbo 的依赖之外,还有依赖了 DemoService 这个公共接口:</p>
<pre><code>&lt;dependency&gt;
&lt;groupId&gt;org.apache.dubbo&lt;/groupId&gt;
@@ -412,7 +412,7 @@ Starting zookeeper ... STARTED # 启动成功
</code></pre>
<p>最后,在 Application 中写个 main() 方法,指定 Spring 配置文件并启动 ClassPathXmlApplicationContext 即可。</p>
<p>接下来再看 dubbo-demo-xml-consumer 模块,结构如下图所示:</p>
<p><img src="assets/Ciqc1F8eRnuAWnTAAAE7eBUfEoA405.png" alt="Drawing 13.png" /></p>
<p><img src="assets/Ciqc1F8eRnuAWnTAAAE7eBUfEoA405.png" alt="png" /></p>
<p>在 pom.xml 中同样依赖了 dubbo-demo-interface 这个公共模块。</p>
<p>在 dubbo-consumer.xml 配置文件中,会指定注册中心地址(就是前面 ZooKeeper 的地址),这样 Dubbo 才能从 ZooKeeper 中拉取到 Provider 暴露的服务列表信息:</p>
<pre><code>&lt;!-- Zookeeper地址 --&gt;

View File

@@ -387,19 +387,19 @@ function hide_canvas() {
}
</code></pre>
<p>我们会看到,在生成的 RegistryFactory$Adaptive 类中会自动实现 getRegistry() 方法,其中会根据 URL 的 Protocol 确定扩展名称,从而确定使用的具体扩展实现类。我们可以找到 RegistryProtocol 这个类,并在其 getRegistry() 方法中打一个断点, Debug 启动上一课时介绍的任意一个 Demo 示例中的 Provider得到如下图所示的内容</p>
<p><img src="assets/Ciqc1F8j2R2AO15wAAGHCEMA4ig361.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F8j2R2AO15wAAGHCEMA4ig361.png" alt="png" /></p>
<p>这里传入的 registryUrl 值为:</p>
<pre><code>zookeeper://127.0.0.1:2181/org.apache.dubbo...
</code></pre>
<p>那么在 RegistryFactory$Adaptive 中得到的扩展名称为 zookeeper此次使用的 Registry 扩展实现类就是 ZookeeperRegistryFactory。至于 Dubbo SPI 的完整内容,我们将在下一课时详细介绍,这里就不再展开了。</p>
<h4>2. URL 在服务暴露中的应用</h4>
<p>我们再来看另一个与 URL 相关的示例。上一课时我们在介绍 Dubbo 的简化架构时提到Provider 在启动时,会将自身暴露的服务注册到 ZooKeeper 上,具体是注册哪些信息到 ZooKeeper 上呢?我们来看 ZookeeperRegistry.doRegister() 方法,在其中打个断点,然后 Debug 启动 Provider会得到下图</p>
<p><img src="assets/Ciqc1F8j2aGAJmTVAAI-2XB7V7o382.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1F8j2aGAJmTVAAI-2XB7V7o382.png" alt="png" /></p>
<p>传入的 URL 中包含了 Provider 的地址172.18.112.15:20880、暴露的接口org.apache.dubbo.demo.DemoService等信息 toUrlPath() 方法会根据传入的 URL 参数确定在 ZooKeeper 上创建的节点路径,还会通过 URL 中的 dynamic 参数值确定创建的 ZNode 是临时节点还是持久节点。</p>
<h4>3. URL 在服务订阅中的应用</h4>
<p>Consumer 启动后会向注册中心进行订阅操作,并监听自己关注的 Provider。那 Consumer 是如何告诉注册中心自己关注哪些 Provider 呢?</p>
<p>我们来看 ZookeeperRegistry 这个实现类,它是由上面的 ZookeeperRegistryFactory 工厂类创建的 Registry 接口实现,其中的 doSubscribe() 方法是订阅操作的核心实现,在第 175 行打一个断点,并 Debug 启动 Demo 中 Consumer会得到下图所示的内容</p>
<p><img src="assets/CgqCHl8j822Aa3VpAAPpUoCBlf4288.png" alt="Lark20200731-183202.png" /></p>
<p><img src="assets/CgqCHl8j822Aa3VpAAPpUoCBlf4288.png" alt="png" /></p>
<p>我们看到传入的 URL 参数如下:</p>
<pre><code>consumer://...?application=dubbo-demo-api-consumer&amp;category=providers,configurators,routers&amp;interface=org.apache.dubbo.demo.DemoService...
</code></pre>

View File

@@ -297,7 +297,7 @@ function hide_canvas() {
<h4>1. JDK SPI 机制</h4>
<p>当服务的提供者提供了一种接口的实现之后,需要在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,此文件记录了该 jar 包提供的服务接口的具体实现类。当某个应用引入了该 jar 包且需要使用该服务时JDK SPI 机制就可以通过查找这个 jar 包的 META-INF/services/ 中的配置文件来获得具体的实现类名,进行实现类的加载和实例化,最终使用该实现类完成业务功能。</p>
<p>下面我们通过一个简单的示例演示下 JDK SPI 的基本使用方式:</p>
<p><img src="assets/CgqCHl8o_UCAI01eAABGsg2cqbw825.png" alt="image" />.png]</p>
<p><img src="assets/CgqCHl8o_UCAI01eAABGsg2cqbw825.png" alt="png" />.png]</p>
<p>首先我们需要创建一个 Log 接口,来模拟日志打印的功能:</p>
<pre><code>public interface Log {
void log(String info);
@@ -340,7 +340,7 @@ com.xxx.impl.Logback
<h4>2. JDK SPI 源码分析</h4>
<p>通过上述示例,我们可以看到 JDK SPI 的入口方法是 ServiceLoader.load() 方法,接下来我们就对其具体实现进行深入分析。</p>
<p>在 ServiceLoader.load() 方法中,首先会尝试获取当前使用的 ClassLoader获取当前线程绑定的 ClassLoader查找失败后使用 SystemClassLoader然后调用 reload() 方法,调用关系如下图所示:</p>
<p><img src="assets/Ciqc1F8o_V6AR93jAABeDIu_Kso211.png" alt="image" /></p>
<p><img src="assets/Ciqc1F8o_V6AR93jAABeDIu_Kso211.png" alt="png" /></p>
<p>在 reload() 方法中,首先会清理 providers 缓存LinkedHashMap 类型的集合),该缓存用来记录 ServiceLoader 创建的实现对象,其中 Key 为实现类的完整类名Value 为实现类的对象。之后创建 LazyIterator 迭代器,用于读取 SPI 配置文件并实例化实现类对象。</p>
<p>ServiceLoader.reload() 方法的具体实现,如下所示:</p>
<pre><code>// 缓存,用来缓存 ServiceLoader创建的实现对象
@@ -351,7 +351,7 @@ public void reload() {
}
</code></pre>
<p>在前面的示例中main() 方法中使用的迭代器底层就是调用了 ServiceLoader.LazyIterator 实现的。Iterator 接口有两个关键方法hasNext() 方法和 next() 方法。这里的 LazyIterator 中的next() 方法最终调用的是其 nextService() 方法hasNext() 方法最终调用的是 hasNextService() 方法,调用关系如下图所示:</p>
<p><img src="assets/Ciqc1F8o_WmAZSkmAABmcc0uM54214.png" alt="image" /></p>
<p><img src="assets/Ciqc1F8o_WmAZSkmAABmcc0uM54214.png" alt="png" /></p>
<p>首先来看 LazyIterator.hasNextService() 方法,该方法主要<strong>负责查找 META-INF/services 目录下的 SPI 配置文件</strong>,并进行遍历,大致实现如下所示:</p>
<pre><code>private static final String PREFIX = &quot;META-INF/services/&quot;;
Enumeration&lt;URL&gt; configs = null;

View File

@@ -313,9 +313,9 @@ function hide_canvas() {
<p>下面我们正式进入 Dubbo SPI 核心实现的介绍。</p>
<h4>1. @SPI 注解</h4>
<p>Dubbo 中某个接口被 @SPI注解修饰时就表示该接口是<strong>扩展接口</strong>,前文示例中的 org.apache.dubbo.rpc.Protocol 接口就是一个扩展接口:</p>
<p><img src="assets/CgqCHl8s936AYuePAABLd6cRz6w646.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl8s936AYuePAABLd6cRz6w646.png" alt="png" /></p>
<p>@SPI 注解的 value 值指定了默认的扩展名称,例如,在通过 Dubbo SPI 加载 Protocol 接口实现时,如果没有明确指定扩展名,则默认会将 @SPI 注解的 value 值作为扩展名,即加载 dubbo 这个扩展名对应的 org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol 这个扩展实现类,相关的 SPI 配置文件在 dubbo-rpc-dubbo 模块中,如下图所示:</p>
<p><img src="assets/CgqCHl8s94mAaj2mAABcaXHNXqc467.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl8s94mAaj2mAABcaXHNXqc467.png" alt="png" /></p>
<p><strong>那 ExtensionLoader 是如何处理 @SPI 注解的呢?</strong></p>
<p>ExtensionLoader 位于 dubbo-common 模块中的 extension 包中,功能类似于 JDK SPI 中的 java.util.ServiceLoader。Dubbo SPI 的核心逻辑几乎都封装在 ExtensionLoader 之中(其中就包括 @SPI 注解的处理逻辑),其使用方式如下所示:</p>
<pre><code>Protocol protocol = ExtensionLoader
@@ -327,7 +327,7 @@ function hide_canvas() {
</ul>
<pre><code> DubboInternalLoadingStrategy &gt; DubboLoadingStrategy &gt; ServicesLoadingStrateg
</code></pre>
<p><img src="assets/Ciqc1F8s95mANXYKAADUVwBlgxs297.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1F8s95mANXYKAADUVwBlgxs297.png" alt="png" /></p>
<ul>
<li><strong>EXTENSION_LOADERSConcurrentMap&lt;Class, ExtensionLoader&gt;类型)</strong>
Dubbo 中一个扩展接口对应一个 ExtensionLoader 实例,该集合缓存了全部 ExtensionLoader 实例,其中的 Key 为扩展接口Value 为加载其扩展实现的 ExtensionLoader 实例。</li>
@@ -407,7 +407,7 @@ function hide_canvas() {
</code></pre>
<h4>2. @Adaptive 注解与适配器</h4>
<p>@Adaptive 注解用来实现 Dubbo 的适配器功能那什么是适配器呢这里我们通过一个示例进行说明。Dubbo 中的 ExtensionFactory 接口有三个实现类如下图所示ExtensionFactory 接口上有 @SPI 注解AdaptiveExtensionFactory 实现类上有 @Adaptive 注解。</p>
<p><img src="assets/Ciqc1F8s-D6AZFtdAAC318rtQ-I710.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Ciqc1F8s-D6AZFtdAAC318rtQ-I710.png" alt="png" /></p>
<p>AdaptiveExtensionFactory 不实现任何具体的功能,而是用来适配 ExtensionFactory 的 SpiExtensionFactory 和 SpringExtensionFactory 这两种实现。AdaptiveExtensionFactory 会根据运行时的一些状态来选择具体调用 ExtensionFactory 的哪个实现。</p>
<p>@Adaptive 注解还可以加到接口方法之上Dubbo 会动态生成适配器类。例如Transporter接口有两个被 @Adaptive 注解修饰的方法:</p>
<pre><code>@SPI(&quot;netty&quot;)
@@ -441,7 +441,7 @@ public interface Transporter {
</code></pre>
<p>生成 Transporter$Adaptive 这个类的逻辑位于 ExtensionLoader.createAdaptiveExtensionClass() 方法,若感兴趣你可以看一下相关代码,其中涉及的 javassist 等方面的知识,在后面的课时中我们会进行介绍。</p>
<p>明确了 @Adaptive 注解的作用之后,我们回到 ExtensionLoader.createExtension() 方法,其中在扫描 SPI 配置文件的时候,会调用 loadClass() 方法加载 SPI 配置文件中指定的类,如下图所示:</p>
<p><img src="assets/CgqCHl8s-H2AJE1LAACILXqbtHY819.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl8s-H2AJE1LAACILXqbtHY819.png" alt="png" /></p>
<p>loadClass() 方法中会识别加载扩展实现类上的 @Adaptive 注解,将该扩展实现的类型缓存到 cachedAdaptiveClass 这个实例字段上volatile修饰</p>
<pre><code>private void loadClass(){
if (clazz.isAnnotationPresent(Adaptive.class)) {
@@ -635,7 +635,7 @@ public &lt;T&gt; T getExtension(Class&lt;T&gt; type, String name) {
}
</code></pre>
<p>最后举个简单的例子说明上述处理流程,假设 cachedActivates 集合缓存的扩展实现如下表所示:</p>
<p><img src="assets/CgqCHl8tNGCAIw8fAACXC_dle_g809.png" alt="11.png" /></p>
<p><img src="assets/CgqCHl8tNGCAIw8fAACXC_dle_g809.png" alt="png" /></p>
<p>在 Provider 端调用 getActivateExtension() 方法时传入的 values 配置为 &quot;demoFilter3、-demoFilter2、default、demoFilter1&quot;,那么根据上面的逻辑:</p>
<ol>
<li>得到默认激活的扩展实实现集合中有 [ demoFilter4, demoFilter6 ]</li>

View File

@@ -293,15 +293,15 @@ function hide_canvas() {
<p>在很多开源框架中,都需要定时任务的管理功能,例如 ZooKeeper、Netty、Quartz、Kafka 以及 Linux 操作系统。</p>
<p>JDK 提供的 java.util.Timer 和 DelayedQueue 等工具类,可以帮助我们实现简单的定时任务管理,其底层实现使用的是<strong></strong>这种数据结构,存取操作的复杂度都是 O(nlog(n)),无法支持大量的定时任务。在定时任务量比较大、性能要求比较高的场景中,为了将定时任务的存取操作以及取消操作的时间复杂度降为 O(1),一般会使用<strong>时间轮</strong>的方式。</p>
<p><strong>时间轮是一种高效的、批量管理定时任务的调度模型</strong>。时间轮一般会实现成一个环形结构,类似一个时钟,分为很多槽,一个槽代表一个时间间隔,每个槽使用双向链表存储定时任务;指针周期性地跳动,跳动到一个槽位,就执行该槽位的定时任务。</p>
<p><img src="assets/CgqCHl8yQfKAEM41AAB8fTu5PCY623.png" alt="1.png" /></p>
<p><img src="assets/CgqCHl8yQfKAEM41AAB8fTu5PCY623.png" alt="png" /></p>
<p>时间轮环形结构示意图</p>
<p>需要注意的是,单层时间轮的容量和精度都是有限的,对于精度要求特别高、时间跨度特别大或是海量定时任务需要调度的场景,通常会使用<strong>多级时间轮</strong>以及<strong>持久化存储与时间轮结合</strong>的方案。</p>
<p>那在 Dubbo 中时间轮的具体实现方式是怎样的呢本课时我们就重点探讨下。Dubbo 的时间轮实现位于 dubbo-common 模块的 org.apache.dubbo.common.timer 包中,下面我们就来分析时间轮涉及的核心接口和实现。</p>
<h3>核心接口</h3>
<p>在 Dubbo 中,所有的定时任务都要继承 <strong>TimerTask 接口</strong>。TimerTask 接口非常简单,只定义了一个 run() 方法,该方法的入参是一个 Timeout 接口的对象。Timeout 对象与 TimerTask 对象一一对应,两者的关系类似于线程池返回的 Future 对象与提交到线程池中的任务对象之间的关系。通过 Timeout 对象我们不仅可以查看定时任务的状态还可以操作定时任务例如取消关联的定时任务。Timeout 接口中的方法如下图所示:</p>
<p><img src="assets/CgqCHl8yQiKAGNLJAABUa6k9juY124.png" alt="image" />.png</p>
<p><img src="assets/CgqCHl8yQiKAGNLJAABUa6k9juY124.png" alt="png" />.png</p>
<p>Timer 接口定义了定时器的基本行为,如下图所示,其核心是 newTimeout() 方法提交一个定时任务TimerTask并返回关联的 Timeout 对象,这有点类似于向线程池提交任务的感觉。</p>
<p><img src="assets/CgqCHl8yQiuAC-1HAABO-eN3cPE094.png" alt="image" /></p>
<p><img src="assets/CgqCHl8yQiuAC-1HAABO-eN3cPE094.png" alt="png" /></p>
<h3>HashedWheelTimeout</h3>
<p>HashedWheelTimeout 是 Timeout 接口的唯一实现,是 HashedWheelTimer 的内部类。HashedWheelTimeout 扮演了两个角色:</p>
<ul>

View File

@@ -292,13 +292,13 @@ function hide_canvas() {
<div><h1>06 ZooKeeper 与 Curator求你别用 ZkClient 了(上)</h1>
<p>在前面我们介绍 Dubbo 简化架构的时候提到过Dubbo Provider 在启动时会将自身的服务信息整理成 URL 注册到注册中心Dubbo Consumer 在启动时会向注册中心订阅感兴趣的 Provider 信息,之后 Provider 和 Consumer 才能建立连接,进行后续的交互。可见,<strong>一个稳定、高效的注册中心对基于 Dubbo 的微服务来说是至关重要的</strong></p>
<p>Dubbo 目前支持 Consul、etcd、Nacos、ZooKeeper、Redis 等多种开源组件作为注册中心,并且在 Dubbo 源码也有相应的接入模块,如下图所示:</p>
<p><img src="assets/Ciqc1F81FQ2ANt6EAADZ01G_QYM489.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F81FQ2ANt6EAADZ01G_QYM489.png" alt="png" /></p>
<p><strong>Dubbo 官方推荐使用 ZooKeeper 作为注册中心</strong>,它是在实际生产中最常用的注册中心实现,这也是我们本课时要介绍 ZooKeeper 核心原理的原因。</p>
<p>要与 ZooKeeper 集群进行交互,我们可以使用 ZooKeeper 原生客户端或是 ZkClient、Apache Curator 等第三方开源客户端。在后面介绍 dubbo-registry-zookeeper 模块的具体实现时你会看到Dubbo 底层使用的是 Apache Curator。<strong>Apache Curator 是实践中最常用的 ZooKeeper 客户端。</strong></p>
<h3>ZooKeeper 核心概念</h3>
<p><strong>Apache ZooKeeper 是一个针对分布式系统的、可靠的、可扩展的协调服务</strong>它通常作为统一命名服务、统一配置管理、注册中心分布式集群管理、分布式锁服务、Leader 选举服务等角色出现。很多分布式系统都依赖与 ZooKeeper 集群实现分布式系统间的协调调度例如Dubbo、HDFS 2.x、HBase、Kafka 等。<strong>ZooKeeper 已经成为现代分布式系统的标配</strong></p>
<p>ZooKeeper 本身也是一个分布式应用程序,下图展示了 ZooKeeper 集群的核心架构。</p>
<p><img src="assets/CgqCHl82OOeARx1GAAEjvCaXdEE505.png" alt="2.png" /></p>
<p><img src="assets/CgqCHl82OOeARx1GAAEjvCaXdEE505.png" alt="png" /></p>
<p>ZooKeeper 集群的核心架构图</p>
<ul>
<li><strong>Client 节点</strong>:从业务角度来看,这是分布式应用中的一个节点,通过 ZkClient 或是其他 ZooKeeper 客户端与 ZooKeeper 集群中的一个 Server 实例维持长连接,并定时发送心跳。从 ZooKeeper 集群的角度来看,它是 ZooKeeper 集群的一个客户端,可以主动查询或操作 ZooKeeper 集群中的数据,也可以在某些 ZooKeeper 节点ZNode上添加监听。当被监听的 ZNode 节点发生变化时,例如,该 ZNode 节点被删除、新增子节点或是其中数据被修改等ZooKeeper 集群都会立即通过长连接通知 Client。</li>
@@ -307,7 +307,7 @@ function hide_canvas() {
<li><strong>Observer 节点</strong>ZooKeeper 集群中特殊的从节点,不会参与 Leader 节点的选举,其他功能与 Follower 节点相同。引入 Observer 角色的目的是增加 ZooKeeper 集群读操作的吞吐量,如果单纯依靠增加 Follower 节点来提高 ZooKeeper 的读吞吐量,那么有一个很严重的副作用,就是 ZooKeeper 集群的写能力会大大降低,因为 ZooKeeper 写数据时需要 Leader 将写操作同步给半数以上的 Follower 节点。引入 Observer 节点使得 ZooKeeper 集群在写能力不降低的情况下,大大提升了读操作的吞吐量。</li>
</ul>
<p>了解了 ZooKeeper 整体的架构之后,我们再来了解一下 ZooKeeper 集群存储数据的逻辑结构。ZooKeeper 逻辑上是按照<strong>树型结构</strong>进行数据存储的(如下图),其中的节点称为 <strong>ZNode</strong>。每个 ZNode 有一个名称标识,即树根到该节点的路径(用 “/” 分隔ZooKeeper 树中的每个节点都可以拥有子节点,这与文件系统的目录树类似。</p>
<p><img src="assets/CgqCHl82OR6AJaDUAAEpNFJyW_0507.png" alt="1.png" /></p>
<p><img src="assets/CgqCHl82OR6AJaDUAAEpNFJyW_0507.png" alt="png" /></p>
<p>ZooKeeper 树型存储结构</p>
<p>ZNode 节点类型有如下四种:</p>
<ul>
@@ -317,7 +317,7 @@ function hide_canvas() {
<li><strong>临时顺序节点。</strong> 基本特性与临时节点一致创建节点的过程中ZooKeeper 会在其名字后自动追加一个单调增长的数字后缀,作为新的节点名。</li>
</ul>
<p>在每个 ZNode 中都维护着一个 stat 结构,记录了该 ZNode 的元数据其中包括版本号、操作控制列表ACL、时间戳和数据长度等信息如下表所示</p>
<p><img src="assets/CgqCHl81FVuAFUp7AARJWw4cOq4421.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CgqCHl81FVuAFUp7AARJWw4cOq4421.png" alt="png" /></p>
<p>我们除了可以通过 ZooKeeper Client 对 ZNode 进行增删改查等基本操作,还可以注册 Watcher 监听 ZNode 节点、其中的数据以及子节点的变化。一旦监听到变化,则相应的 Watcher 即被触发,相应的 ZooKeeper Client 会立即得到通知。Watcher 有如下特点:</p>
<ul>
<li><strong>主动推送。</strong> Watcher 被触发时,由 ZooKeeper 集群主动将更新推送给客户端,而不需要客户端轮询。</li>
@@ -337,7 +337,7 @@ function hide_canvas() {
<li>最后Follower 节点会返回 Client 写请求相应的响应。</li>
</ol>
<p>下图展示了写操作的核心流程:</p>
<p><img src="assets/Ciqc1F81FXOAKEC8AAKkiqE6rHY039.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Ciqc1F81FXOAKEC8AAKkiqE6rHY039.png" alt="png" /></p>
<p>写操作核心流程图</p>
<h3>崩溃恢复</h3>
<p>上面写请求处理流程中,如果发生 Leader 节点宕机,整个 ZooKeeper 集群可能处于如下两种状态:</p>

View File

@@ -305,7 +305,7 @@ function hide_canvas() {
<h3>Apache Curator 基础</h3>
<p><strong>Apache Curator 是 Apache 基金会提供的一款 ZooKeeper 客户端,它提供了一套易用性和可读性非常强的 Fluent 风格的客户端 API ,可以帮助我们快速搭建稳定可靠的 ZooKeeper 客户端程序。</strong></p>
<p>为便于你更全面了解 Curator 的功能,我整理出了如下表格,展示了 Curator 提供的 jar 包:</p>
<p><img src="assets/Ciqc1F87iUKAAAs2AAE2Xaps_KE511.png" alt="1.png" /></p>
<p><img src="assets/Ciqc1F87iUKAAAs2AAE2Xaps_KE511.png" alt="png" /></p>
<p>下面我们从最基础的使用展开,逐一介绍 Apache Curator 在实践中常用的核心功能,开始我们的 Apache Curator 之旅。</p>
<h4>1. 基本操作</h4>
<p>简单了解了 Apache Curator 各个组件的定位之后,下面我们立刻通过一个示例上手使用 Curator。首先我们创建一个 Maven 项目,并添加 Apache Curator 的依赖:</p>
@@ -519,7 +519,7 @@ function hide_canvas() {
}
</code></pre>
<p>接下来,我们打开 ZooKeeper 的命令行客户端,在 /user 节点下先后添加两个子节点,如下所示:</p>
<p><img src="assets/Ciqc1F87iXuAQVanAABhI9RRD8M252.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F87iXuAQVanAABhI9RRD8M252.png" alt="png" /></p>
<p>此时我们只得到一行输出:</p>
<pre><code>NodeChildrenChanged,/user
</code></pre>
@@ -616,13 +616,13 @@ TreeCache,type=NODE_ADDED path=/user/test2
TreeCache,type=INITIALIZED
</code></pre>
<p>接下来,我们在 ZooKeeper 命令行客户端中<strong>更新 /user 节点中的数据</strong></p>
<p><img src="assets/Ciqc1F87iY6ACWnvAAA8jA9QVgM875.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1F87iY6ACWnvAAA8jA9QVgM875.png" alt="png" /></p>
<p>得到如下输出:</p>
<pre><code>TreeCache,type=NODE_UPDATED path=/user
NodeCache节点路径/user节点数据为userData
</code></pre>
<p><strong>创建 /user/test3 节点</strong></p>
<p><img src="assets/CgqCHl87iZqAaG93AABwFnQJA7o497.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl87iZqAaG93AABwFnQJA7o497.png" alt="png" /></p>
<p>得到输出:</p>
<pre><code>TreeCache,type=NODE_ADDED path=/user/test3
2020-06-26T08:35:22.393 CHILD_ADDED
@@ -630,7 +630,7 @@ PathChildrenCache添加子节点:/user/test3
PathChildrenCache子节点数据:xxx3
</code></pre>
<p><strong>更新 /user/test3 节点的数据</strong></p>
<p><img src="assets/Ciqc1F87iaSAFZLpAABDyAm7vuE120.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Ciqc1F87iaSAFZLpAABDyAm7vuE120.png" alt="png" /></p>
<p>得到输出:</p>
<pre><code>TreeCache,type=NODE_UPDATED path=/user/test3
2020-06-26T08:43:54.604 CHILD_UPDATED
@@ -638,7 +638,7 @@ PathChildrenCache修改子节点路径:/user/test3
PathChildrenCache修改子节点数据:xxx33
</code></pre>
<p><strong>删除 /user/test3 节点</strong></p>
<p><img src="assets/CgqCHl87ia6AYvijAABBmFLfzx4213.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl87ia6AYvijAABBmFLfzx4213.png" alt="png" /></p>
<p>得到输出:</p>
<pre><code>TreeCache,type=NODE_REMOVED path=/user/test3
2020-06-26T08:44:06.329 CHILD_REMOVED
@@ -650,7 +650,7 @@ PathChildrenCache删除子节点:/user/test3
<ul>
<li><strong>ServiceInstance。</strong> 这是 curator-x-discovery 扩展包对服务实例的抽象,由 name、id、address、port 以及一个可选的 payload 属性构成。其存储在 ZooKeeper 中的方式如下图展示的这样。</li>
</ul>
<p><img src="assets/CgqCHl87icOABt59AADHccHcE1Q955.png" alt="Drawing 5.png" /></p>
<p><img src="assets/CgqCHl87icOABt59AADHccHcE1Q955.png" alt="png" /></p>
<ul>
<li><strong>ServiceProvider。</strong> 这是 curator-x-discovery 扩展包的核心组件之一,提供了多种不同策略的服务发现方式,具体策略有轮询调度、随机和黏性(总是选择相同的一个)。得到 ServiceProvider 对象之后,我们可以调用其 getInstance() 方法,按照指定策略获取 ServiceInstance 对象(即发现可用服务实例);还可以调用 getAllInstances() 方法,获取所有 ServiceInstance 对象(即获取全部可用服务实例)。</li>
<li><strong>ServiceDiscovery。</strong> 这是 curator-x-discovery 扩展包的入口类。开始必须调用 start() 方法,当使用完成应该调用 close() 方法进行销毁。</li>

View File

@@ -294,7 +294,7 @@ function hide_canvas() {
<p>本课时我们主要从基础知识开始讲起,首先介绍代理模式的基本概念,之后重点介绍 JDK 动态代理的使用以及底层实现原理,同时还会说明 JDK 动态代理的一些局限性,最后再介绍基于字节码生成的动态代理。</p>
<h3>代理模式</h3>
<p>代理模式是 23 种面向对象的设计模式中的一种,它的类图如下所示:</p>
<p><img src="assets/CgqCHl8_hxqAY6vaAAGcUb0A8A4971.png" alt="image" /></p>
<p><img src="assets/CgqCHl8_hxqAY6vaAAGcUb0A8A4971.png" alt="png" /></p>
<p>图中的 Subject 是程序中的<strong>业务逻辑接口</strong>RealSubject 是实现了 Subject 接口的<strong>真正业务类</strong>Proxy 是实现了 Subject 接口的<strong>代理类</strong>,封装了一个 RealSubject 引用。<strong>在程序中不会直接调用 RealSubject 对象的方法,而是使用 Proxy 对象实现相关功能。</strong></p>
<p>Proxy.operation() 方法的实现会调用其中封装的 RealSubject 对象的 operation() 方法执行真正的业务逻辑。代理的作用不仅仅是正常地完成业务逻辑还会在业务逻辑前后添加一些代理逻辑也就是说Proxy.operation() 方法会在 RealSubject.operation() 方法调用前后进行一些预处理以及一些后置处理。这就是我们常说的“<strong>代理模式</strong>”。</p>
<p><strong>使用代理模式可以控制程序对 RealSubject 对象的访问</strong>,如果发现异常的访问,可以直接限流或是返回,也可以在执行业务处理的前后进行相关的预处理和后置处理,帮助上层调用方屏蔽底层的细节。例如,在 RPC 框架中,代理可以完成序列化、网络 I/O 操作、负载均衡、故障恢复以及服务发现等一系列操作,而上层调用方只感知到了一次本地调用。</p>
@@ -443,7 +443,7 @@ function hide_canvas() {
</ul>
<p>这两个组件的使用与 JDK 动态代理中的 Proxy 和 InvocationHandler 相似。</p>
<p>下面我们通过一个示例简单介绍 CGLib 的使用。在使用 CGLib 创建动态代理类时,首先需要定义一个 Callback 接口的实现, CGLib 中也提供了多个Callback接口的子接口如下图所示</p>
<p><img src="assets/CgqCHl8_h1uAcXB-AAKCT9cNDBw713.png" alt="image" /></p>
<p><img src="assets/CgqCHl8_h1uAcXB-AAKCT9cNDBw713.png" alt="png" /></p>
<p>这里以 MethodInterceptor 接口为例进行介绍,首先我们引入 CGLib 的 maven 依赖:</p>
<pre><code>&lt;dependency&gt;
&lt;groupId&gt;cglib&lt;/groupId&gt;

View File

@@ -309,14 +309,14 @@ function hide_canvas() {
<p>在进行网络 I/O 操作的时候,用什么样的方式读写数据将在很大程度上决定了 I/O 的性能。作为一款优秀的网络基础库Netty 就采用了 NIO 的 I/O 模型,这也是其高性能的重要原因之一。</p>
<h4>1. 传统阻塞 I/O 模型</h4>
<p>在传统阻塞型 I/O 模型(即我们常说的 BIO如下图所示每个请求都需要独立的线程完成读数据、业务处理以及写回数据的完整操作。</p>
<p><img src="assets/CgqCHl9EvKaAF18_AACJ4Y62QAY004.png" alt="2.png" /></p>
<p><img src="assets/CgqCHl9EvKaAF18_AACJ4Y62QAY004.png" alt="png" /></p>
<p>一个线程在同一时刻只能与一个连接绑定,如下图所示,当请求的并发量较大时,就需要创建大量线程来处理连接,这就会导致系统浪费大量的资源进行线程切换,降低程序的性能。我们知道,网络数据的传输速度是远远慢于 CPU 的处理速度连接建立后并不总是有数据可读连接也并不总是可写那么线程就只能阻塞等待CPU 的计算能力不能得到充分发挥,同时还会导致大量线程的切换,浪费资源。</p>
<p><img src="assets/CgqCHl9EvLSAQzfFAACIPU0Pqkg586.png" alt="3.png" /></p>
<p><img src="assets/CgqCHl9EvLSAQzfFAACIPU0Pqkg586.png" alt="png" /></p>
<h4>2. I/O 多路复用模型</h4>
<p>针对传统的阻塞 I/O 模型的缺点I/O 复用的模型在性能方面有不小的提升。I/O 复用模型中的多个连接会共用一个 Selector 对象,由 Selector 感知连接的读写事件,而此时的线程数并不需要和连接数一致,只需要很少的线程定期从 Selector 上查询连接的读写状态即可无须大量线程阻塞等待连接。当某个连接有新的数据可以处理时操作系统会通知线程线程从阻塞状态返回开始进行读写操作以及后续的业务逻辑处理。I/O 复用的模型如下图所示:</p>
<p><img src="assets/Ciqc1F9EvNOACOC5AADhkXKnAFg681.png" alt="4.png" /></p>
<p><img src="assets/Ciqc1F9EvNOACOC5AADhkXKnAFg681.png" alt="png" /></p>
<p>Netty 就是采用了上述 I/O 复用的模型。由于多路复用器 Selector 的存在可以同时并发处理成百上千个网络连接大大增加了服务器的处理能力。另外Selector 并不会阻塞线程,也就是说当一个连接不可读或不可写的时候,线程可以去处理其他可读或可写的连接,这就充分提升了 I/O 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程切换。如下图所示:</p>
<p><img src="assets/Ciqc1F9EvOOADRMzAACeQMLGfbs278.png" alt="6.png" /></p>
<p><img src="assets/Ciqc1F9EvOOADRMzAACeQMLGfbs278.png" alt="png" /></p>
<p>从数据处理的角度来看,传统的阻塞 I/O 模型处理的是字节流或字符流,也就是以流式的方式顺序地从一个数据流中读取一个或多个字节,并且不能随意改变读取指针的位置。而在 NIO 中则抛弃了这种传统的 I/O 流概念,引入了 Channel 和 Buffer 的概念,可以从 Channel 中读取数据到 Buffer 中或将数据从 Buffer 中写入到 Channel。Buffer 不像传统 I/O 中的流那样必须顺序操作,在 NIO 中可以读写 Buffer 中任意位置的数据。</p>
<h3>Netty 线程模型设计</h3>
<p>服务器程序在读取到二进制数据之后,首先需要通过编解码,得到程序逻辑可以理解的消息,然后将消息传入业务逻辑进行处理,并产生相应的结果,返回给客户端。编解码逻辑、消息派发逻辑、业务处理逻辑以及返回响应的逻辑,是放到一个线程里面串行执行,还是分配到不同的线程中执行,会对程序的性能产生很大的影响。所以,优秀的线程模型对一个高性能网络库来说是至关重要的。</p>
@@ -324,23 +324,23 @@ function hide_canvas() {
<p>为了帮助你更好地了解 Netty 线程模型的设计理念,我们将从最基础的单 Reactor 单线程模型开始介绍,然后逐步增加模型的复杂度,最终到 Netty 目前使用的非常成熟的线程模型设计。</p>
<h4>1. 单 Reactor 单线程</h4>
<p>Reactor 对象监听客户端请求事件,收到事件后通过 Dispatch 进行分发。如果是连接建立的事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接建立之后的业务请求。如果不是连接建立的事件,而是数据的读写事件,则 Reactor 会将事件分发对应的 Handler 来处理,由这里唯一的线程调用 Handler 对象来完成读取数据、业务处理、发送响应的完整流程。当然,该过程中也可能会出现连接不可读或不可写等情况,该单线程会去执行其他 Handler 的逻辑,而不是阻塞等待。具体情况如下图所示:</p>
<p><img src="assets/CgqCHl9EvVGAPXATAAEj0pK8ONM000.png" alt="7.png" /></p>
<p><img src="assets/CgqCHl9EvVGAPXATAAEj0pK8ONM000.png" alt="png" /></p>
<p>单 Reactor 单线程的优点就是:线程模型简单,没有引入多线程,自然也就没有多线程并发和竞争的问题。</p>
<p>但其缺点也非常明显,那就是<strong>性能瓶颈问题</strong>,一个线程只能跑在一个 CPU 上,能处理的连接数是有限的,无法完全发挥多核 CPU 的优势。一旦某个业务逻辑耗时较长,这唯一的线程就会卡在上面,无法处理其他连接的请求,程序进入假死的状态,可用性也就降低了。正是由于这种限制,一般只会在<strong>客户端</strong>使用这种线程模型。</p>
<h4>2. 单 Reactor 多线程</h4>
<p>在单 Reactor 多线程的架构中Reactor 监控到客户端请求之后如果连接建立的请求则由Acceptor 通过 accept 处理,然后创建一个 Handler 对象处理连接建立之后的业务请求。如果不是连接建立请求,则 Reactor 会将事件分发给调用连接对应的 Handler 来处理。到此为止,该流程与单 Reactor 单线程的模型基本一致,<strong>唯一的区别就是执行 Handler 逻辑的线程隶属于一个线程池</strong></p>
<p><img src="assets/CgqCHl9EvWqAJ5jpAAFbymUVJ8o272.png" alt="8.png" /></p>
<p><img src="assets/CgqCHl9EvWqAJ5jpAAFbymUVJ8o272.png" alt="png" /></p>
<p>单 Reactor 多线程模型</p>
<p>很明显,单 Reactor 多线程的模型可以充分利用多核 CPU 的处理能力,提高整个系统的吞吐量,但引入多线程模型就要考虑线程并发、数据共享、线程调度等问题。在这个模型中,只有一个线程来处理 Reactor 监听到的所有 I/O 事件,其中就包括连接建立事件以及读写事件,当连接数不断增大的时候,这个唯一的 Reactor 线程也会遇到瓶颈。</p>
<h4>3. 主从 Reactor 多线程</h4>
<p>为了解决单 Reactor 多线程模型中的问题,我们可以引入多个 Reactor。其中Reactor 主线程负责通过 Acceptor 对象处理 MainReactor 监听到的连接建立事件当Acceptor 完成网络连接的建立之后MainReactor 会将建立好的连接分配给 SubReactor 进行后续监听。</p>
<p>当一个连接被分配到一个 SubReactor 之上时,会由 SubReactor 负责监听该连接上的读写事件。当有新的读事件OP_READ发生时Reactor 子线程就会调用对应的 Handler 读取数据,然后分发给 Worker 线程池中的线程进行处理并返回结果。待处理结束之后Handler 会根据处理结果调用 send 将响应返回给客户端当然此时连接要有可写事件OP_WRITE才能发送数据。</p>
<p><img src="assets/CgqCHl9EvXuARvm7AAF3raiQza8716.png" alt="9.png" /></p>
<p><img src="assets/CgqCHl9EvXuARvm7AAF3raiQza8716.png" alt="png" /></p>
<p>主从 Reactor 多线程模型</p>
<p>主从 Reactor 多线程的设计模式解决了单一 Reactor 的瓶颈。<strong>主从 Reactor 职责明确,主 Reactor 只负责监听连接建立事件SubReactor只负责监听读写事件</strong>。整个主从 Reactor 多线程架构充分利用了多核 CPU 的优势,可以支持扩展,而且与具体的业务逻辑充分解耦,复用性高。但不足的地方是,在交互上略显复杂,需要一定的编程门槛。</p>
<h4>4. Netty 线程模型</h4>
<p>Netty 同时支持上述几种线程模式Netty 针对服务器端的设计是在主从 Reactor 多线程模型的基础上进行的修改,如下图所示:</p>
<p><img src="assets/Ciqc1F9EvZyAZsQlAAMdGh4CXMI139.png" alt="1.png" /></p>
<p><img src="assets/Ciqc1F9EvZyAZsQlAAMdGh4CXMI139.png" alt="png" /></p>
<p><strong>Netty 抽象出两组线程池BossGroup 专门用于接收客户端的连接WorkerGroup 专门用于网络的读写</strong>。BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup相当于一个事件循环组其中包含多个事件循环 ,每一个事件循环是 NioEventLoop。</p>
<p>NioEventLoop 表示一个不断循环的、执行处理任务的线程,每个 NioEventLoop 都有一个Selector 对象与之对应,用于监听绑定在其上的连接,这些连接上的事件由 Selector 对应的这条线程处理。每个 NioEventLoopGroup 可以含有多个 NioEventLoop也就是多个线程。</p>
<p>每个 Boss NioEventLoop 会监听 Selector 上连接建立的 accept 事件,然后处理 accept 事件与客户端建立网络连接,生成相应的 NioSocketChannel 对象,一个 NioSocketChannel 就表示一条网络连接。之后会将 NioSocketChannel 注册到某个 Worker NioEventLoop 上的 Selector 中。</p>

View File

@@ -307,7 +307,7 @@ function hide_canvas() {
<h3>ChannelPipeline&amp;ChannelHandler</h3>
<p>提到 Pipeline你可能最先想到的是 Linux 命令中的管道它可以实现将一条命令的输出作为另一条命令的输入。Netty 中的 ChannelPipeline 也可以实现类似的功能:<strong>ChannelPipeline 会将一个 ChannelHandler 处理后的数据作为下一个 ChannelHandler 的输入</strong></p>
<p>下图我们引用了 Netty Javadoc 中对 ChannelPipeline 的说明,描述了 ChannelPipeline 中 ChannelHandler 通常是如何处理 I/O 事件的。Netty 中定义了两种事件类型:<strong>入站Inbound事件</strong><strong>出站Outbound事件</strong>。这两种事件就像 Linux 管道中的数据一样,在 ChannelPipeline 中传递事件之中也可能会附加数据。ChannelPipeline 之上可以注册多个 ChannelHandlerChannelInboundHandler 或 ChannelOutboundHandler我们在 ChannelHandler 注册的时候决定处理 I/O 事件的顺序,这就是典型的<strong>责任链模式</strong></p>
<p><img src="assets/Ciqc1F9Ill2AMrCEAAFZL4VXrW8624.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F9Ill2AMrCEAAFZL4VXrW8624.png" alt="png" /></p>
<p>从图中我们还可以看到I/O 事件不会在 ChannelPipeline 中自动传播而是需要调用ChannelHandlerContext 中定义的相应方法进行传播例如fireChannelRead() 方法和 write() 方法等。</p>
<p>这里我们举一个简单的例子,如下所示,在该 ChannelPipeline 上,我们添加了 5 个 ChannelHandler 对象:</p>
<pre><code>ChannelPipeline p = socketChannel.pipeline();
@@ -326,12 +326,12 @@ p.addLast(&quot;5&quot;, new InboundOutboundHandlerX());
<p>在 Netty 中就提供了很多 Encoder 的实现用来解码读取到的数据Encoder 会处理多次 channelRead() 事件,等拿到有意义的数据之后,才会触发一次下一个 ChannelInboundHandler 的 channelRead() 方法。</p>
<p><strong>出站Outbound事件与入站Inbound事件相反一般是由用户触发的。</strong></p>
<p>ChannelHandler 接口中并没有定义方法来处理事件而是由其子类进行处理的如下图所示ChannelInboundHandler 拦截并处理入站事件ChannelOutboundHandler 拦截并处理出站事件。</p>
<p><img src="assets/Ciqc1F9IlmmABbbRAADcN9APiZs099.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1F9IlmmABbbRAADcN9APiZs099.png" alt="png" /></p>
<p>Netty 提供的 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter 主要是帮助完成事件流转功能的,即自动调用传递事件的相应方法。这样,我们在自定义 ChannelHandler 实现类的时候,就可以直接继承相应的 Adapter 类,并覆盖需要的事件处理方法,其他不关心的事件方法直接使用默认实现即可,从而提高开发效率。</p>
<p>ChannelHandler 中的很多方法都需要一个 ChannelHandlerContext 类型的参数ChannelHandlerContext 抽象的是 ChannleHandler 之间的关系以及 ChannelHandler 与ChannelPipeline 之间的关系。<strong>ChannelPipeline 中的事件传播主要依赖于ChannelHandlerContext 实现</strong>,在 ChannelHandlerContext 中维护了 ChannelHandler 之间的关系,所以我们可以从 ChannelHandlerContext 中得到当前 ChannelHandler 的后继节点,从而将事件传播到后续的 ChannelHandler。</p>
<p>ChannelHandlerContext 继承了 AttributeMap所以提供了 attr() 方法设置和删除一些状态属性信息,我们可将业务逻辑中所需使用的状态属性值存入到 ChannelHandlerContext 中然后这些属性就可以随它传播了。Channel 中也维护了一个 AttributeMap与 ChannelHandlerContext 中的 AttributeMap从 Netty 4.1 开始,都是作用于整个 ChannelPipeline。</p>
<p>通过上述分析,我们可以了解到,<strong>一个 Channel 对应一个 ChannelPipeline一个 ChannelHandlerContext 对应一个ChannelHandler。</strong> 如下图所示:</p>
<p><img src="assets/CgqCHl9Ixi-APR5UAADY4pM97IU060.png" alt="1.png" /></p>
<p><img src="assets/CgqCHl9Ixi-APR5UAADY4pM97IU060.png" alt="png" /></p>
<p>最后,需要注意的是,如果要在 ChannelHandler 中执行耗时较长的逻辑,例如,操作 DB 、进行网络或磁盘 I/O 等操作,一般会在注册到 ChannelPipeline 的同时,指定一个线程池异步执行 ChannelHandler 中的操作。</p>
<h3>NioEventLoop</h3>
<p>在前文介绍 Netty 线程模型的时候,我们简单提到了 NioEventLoop 这个组件,当时为了便于理解,只是简单将其描述成了一个线程。</p>
@@ -341,7 +341,7 @@ p.addLast(&quot;5&quot;, new InboundOutboundHandlerX());
<ul>
<li><strong>普通任务队列</strong>。用户产生的普通任务可以提交到该队列中暂存NioEventLoop 发现该队列中的任务后会立即执行。这是一个多生产者、单消费者的队列Netty 使用该队列将外部用户线程产生的任务收集到一起,并在 Reactor 线程内部用单线程的方式串行执行队列中的任务。例如,外部非 I/O 线程调用了 Channel 的 write() 方法Netty 会将其封装成一个任务放入 TaskQueue 队列中,这样,所有的 I/O 操作都会在 I/O 线程中串行执行。</li>
</ul>
<p><img src="assets/Ciqc1F9IxjqAJwMsAAFsUFuiU6A398.png" alt="2.png" /></p>
<p><img src="assets/Ciqc1F9IxjqAJwMsAAFsUFuiU6A398.png" alt="png" /></p>
<ul>
<li><strong>定时任务队列</strong>。当用户在非 I/O 线程产生定时操作时Netty 将用户的定时操作封装成定时任务,并将其放入该定时任务队列中等待相应 NioEventLoop 串行执行。</li>
</ul>
@@ -349,10 +349,10 @@ p.addLast(&quot;5&quot;, new InboundOutboundHandlerX());
<h3>NioEventLoopGroup</h3>
<p><strong>NioEventLoopGroup 表示的是一组 NioEventLoop</strong>。Netty 为了能更充分地利用多核 CPU 资源,一般会有多个 NioEventLoop 同时工作至于多少线程可由用户决定Netty 会根据实际上的处理器核数计算一个默认值具体计算公式是CPU 的核心数 * 2当然我们也可以根据实际情况手动调整。</p>
<p>当一个 Channel 创建之后Netty 会调用 NioEventLoopGroup 提供的 next() 方法,按照一定规则获取其中一个 NioEventLoop 实例,并将 Channel 注册到该 NioEventLoop 实例,之后,就由该 NioEventLoop 来处理 Channel 上的事件。EventLoopGroup、EventLoop 以及 Channel 三者的关联关系,如下图所示:</p>
<p><img src="assets/CgqCHl9IloyAHiBiAALeke8Ln5c195.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl9IloyAHiBiAALeke8Ln5c195.png" alt="png" /></p>
<p>前面我们提到过,在 Netty 服务器端中,会有 BossEventLoopGroup 和 WorkerEventLoopGroup 两个 NioEventLoopGroup。通常一个服务端口只需要一个ServerSocketChannel对应一个 Selector 和一个 NioEventLoop 线程。</p>
<p>BossEventLoop 负责接收客户端的连接事件,即 OP_ACCEPT 事件,然后将创建的 NioSocketChannel 交给 WorkerEventLoopGroup WorkerEventLoopGroup 会由 next() 方法选择其中一个 NioEventLoopGroup并将这个 NioSocketChannel 注册到其维护的 Selector 并对其后续的I/O事件进行处理。</p>
<p><img src="assets/CgqCHl9RrYCAVrHFAAF5lFo-Hgc219.png" alt="image" /></p>
<p><img src="assets/CgqCHl9RrYCAVrHFAAF5lFo-Hgc219.png" alt="png" /></p>
<p>如上图BossEventLoopGroup 通常是一个单线程的 EventLoopEventLoop 维护着一个 Selector 对象,其上注册了一个 ServerSocketChannelBoosEventLoop 会不断轮询 Selector 监听连接事件,在发生连接事件时,通过 accept 操作与客户端创建连接,创建 SocketChannel 对象。然后将 accept 操作得到的 SocketChannel 交给 WorkerEventLoopGroup在Reactor 模式中 WorkerEventLoopGroup 中会维护多个 EventLoop而每个 EventLoop 都会监听分配给它的 SocketChannel 上发生的 I/O 事件,并将这些具体的事件分发给业务线程池处理。</p>
<h3>ByteBuf</h3>
<p>通过前文的介绍,我们了解了 Netty 中数据的流向,这里我们再来介绍一下<strong>数据的容器——ByteBuf</strong></p>
@@ -360,7 +360,7 @@ p.addLast(&quot;5&quot;, new InboundOutboundHandlerX());
<p>ByteBuf 类似于一个字节数组,其中维护了一个读索引和一个写索引,分别用来控制对 ByteBuf 中数据的读写操作,两者符合下面的不等式:</p>
<pre><code>0 &lt;= readerIndex &lt;= writerIndex &lt;= capacity
</code></pre>
<p><img src="assets/CgqCHl9IlsaAJYcYAAA31kf6i_g696.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CgqCHl9IlsaAJYcYAAA31kf6i_g696.png" alt="png" /></p>
<p>ByteBuf 提供的读写操作 API 主要操作底层的字节容器byte[]、ByteBuffer 等)以及读写索引这两指针,你若感兴趣的话,可以查阅相关的 API 说明,这里不再展开介绍。</p>
<p>Netty 中主要分为以下三大类 ByteBuf</p>
<ul>
@@ -377,13 +377,13 @@ p.addLast(&quot;5&quot;, new InboundOutboundHandlerX());
<p>下面我们从如何高效分配和释放内存、如何减少内存碎片以及在多线程环境下如何减少锁竞争这三个方面介绍一下 Netty 提供的 ByteBuf 池化技术。</p>
<p>Netty 首先会向系统申请一整块连续内存,称为 Chunk默认大小为 16 MB这一块连续的内存通过 PoolChunk 对象进行封装。之后Netty 将 Chunk 空间进一步拆分为 Page每个 Chunk 默认包含 2048 个 Page每个 Page 的大小为 8 KB。</p>
<p>在同一个 Chunk 中Netty 将 Page 按照不同粒度进行分层管理。如下图所示,从下数第 1 层中每个分组的大小为 1 * PageSize一共有 2048 个分组;第 2 层中每个分组大小为 2 * PageSize一共有 1024 个组;第 3 层中每个分组大小为 4 * PageSize一共有 512 个组;依次类推,直至最顶层。</p>
<p><img src="assets/CgqCHl9IlteANQ8lAADm9qN5mgE993.png" alt="Drawing 7.png" /></p>
<p><img src="assets/CgqCHl9IlteANQ8lAADm9qN5mgE993.png" alt="png" /></p>
<h4>1. 内存分配&amp;释放</h4>
<p>当服务向内存池请求内存时Netty 会将请求分配的内存数向上取整到最接近的分组大小,然后在该分组的相应层级中从左至右寻找空闲分组。例如,服务请求分配 3 * PageSize 的内存,向上取整得到的分组大小为 4 * PageSize在该层分组中找到完全空闲的一组内存进行分配即可如下图</p>
<p><img src="assets/Ciqc1F9IluuAFgouAADoPKhjuW4842.png" alt="Drawing 8.png" /></p>
<p><img src="assets/Ciqc1F9IluuAFgouAADoPKhjuW4842.png" alt="png" /></p>
<p>当分组大小 4 * PageSize 的内存分配出去后,为了方便下次内存分配,分组被标记为全部已使用(图中红色标记),向上更粗粒度的内存分组被标记为部分已使用(图中黄色标记)。</p>
<p>Netty 使用<strong>完全平衡树的结构</strong>实现了上述算法,这个完全平衡树底层是基于一个 byte 数组构建的,如下图所示:</p>
<p><img src="assets/Ciqc1F9IlvKACdYpAAF2w22m4sQ981.png" alt="Drawing 9.png" /></p>
<p><img src="assets/Ciqc1F9IlvKACdYpAAF2w22m4sQ981.png" alt="png" /></p>
<p>具体的实现逻辑这里就不再展开讲述了,你若感兴趣的话,可以参考 Netty 代码。</p>
<h4>2. 大对象&amp;小对象的处理</h4>
<p>当申请分配的对象是超过 Chunk 容量的大型对象Netty 就不再使用池化管理方式了,在每次请求分配内存时单独创建特殊的非池化 PoolChunk 对象进行管理当对象内存释放时整个PoolChunk 内存释放。</p>
@@ -393,16 +393,16 @@ p.addLast(&quot;5&quot;, new InboundOutboundHandlerX());
<li>小型对象:规整后的大小为 2 的幂,如 512、1024、2048、4096一共 4 种大小。</li>
</ul>
<p>Netty 的实现会先从 PoolChunk 中申请空闲 Page同一个 Page 分为相同大小的小 Buffer 进行存储;这些 Page 用 PoolSubpage 对象进行封装PoolSubpage 内部会记录它自己能分配的小 Buffer 的规格大小、可用内存数量,并通过 bitmap 的方式记录各个小内存的使用情况(如下图所示)。虽然这种方案不能完美消灭内存碎片,但是很大程度上还是减少了内存浪费。</p>
<p><img src="assets/CgqCHl9Ilv-ABhKnAAFcBHa9_3E953.png" alt="Drawing 10.png" /></p>
<p><img src="assets/CgqCHl9Ilv-ABhKnAAFcBHa9_3E953.png" alt="png" /></p>
<p>为了解决单个 PoolChunk 容量有限的问题Netty 将多个 PoolChunk 组成链表一起管理,然后用 PoolChunkList 对象持有链表的 head。</p>
<p><strong>Netty 通过 PoolArena 管理 PoolChunkList 以及 PoolSubpage</strong></p>
<p>PoolArena 内部持有 6 个 PoolChunkList各个 PoolChunkList 持有的 PoolChunk 的使用率区间有所不同,如下图所示:</p>
<p><img src="assets/CgqCHl9IlwaAMIzVAACq5iIjcZk639.png" alt="Drawing 11.png" /></p>
<p><img src="assets/CgqCHl9IlwaAMIzVAACq5iIjcZk639.png" alt="png" /></p>
<p>6 个 PoolChunkList 对象组成双向链表,当 PoolChunk 内存分配、释放,导致使用率变化,需要判断 PoolChunk 是否超过所在 PoolChunkList 的限定使用率范围,如果超出了,需要沿着 6 个 PoolChunkList 的双向链表找到新的合适的 PoolChunkList ,成为新的 head。同样当新建 PoolChunk 分配内存或释放空间时PoolChunk 也需要按照上面逻辑放入合适的PoolChunkList 中。</p>
<p><img src="assets/CgqCHl9Ilw2ATgO0AACWza-dQV4641.png" alt="Drawing 12.png" /></p>
<p><img src="assets/CgqCHl9Ilw2ATgO0AACWza-dQV4641.png" alt="png" /></p>
<p>从上图可以看出,这 6 个 PoolChunkList 额定使用率区间存在交叉,这样设计的原因是:如果使用单个临界值的话,当一个 PoolChunk 被来回申请和释放,内存使用率会在临界值上下徘徊,这就会导致它在两个 PoolChunkList 链表中来回移动。</p>
<p>PoolArena 内部持有 2 个 PoolSubpage 数组,分别存储微型 Buffer 和小型 Buffer 的PoolSubpage。相同大小的 PoolSubpage 组成链表,不同大小的 PoolSubpage 链表的 head 节点保存在 tinySubpagePools 或者 smallSubpagePools 数组中,如下图:</p>
<p><img src="assets/Ciqc1F9IlxSAWAuXAADUtE1ddhw421.png" alt="Drawing 13.png" /></p>
<p><img src="assets/Ciqc1F9IlxSAWAuXAADUtE1ddhw421.png" alt="png" /></p>
<h4>3. 并发处理</h4>
<p>内存分配释放不可避免地会遇到多线程并发场景PoolChunk 的完全平衡树标记以及 PoolSubpage 的 bitmap 标记都是多线程不安全的都是需要加锁同步的。为了减少线程间的竞争Netty 会提前创建多个 PoolArena默认数量为 2 * CPU 核心数),当线程首次请求池化内存分配,会找被最少线程持有的 PoolArena并保存线程局部变量 PoolThreadCache 中,实现线程与 PoolArena 的关联绑定。</p>
<p>Netty 还提供了延迟释放的功能来提升并发性能。当内存释放时PoolArena 并没有马上释放,而是先尝试将该内存关联的 PoolChunk 和 Chunk 中的偏移位置等信息存入 ThreadLocal 的固定大小缓存队列中如果该缓存队列满了则马上释放内存。当有新的分配请求时PoolArena 会优先访问线程本地的缓存队列,查询是否有缓存可用,如果有,则直接分配,提高分配效率。</p>

View File

@@ -292,7 +292,7 @@ function hide_canvas() {
<div><h1>11 简易版 RPC 框架实现(上)</h1>
<p>这是“基础知识”部分的最后一课时,我们将会运用前面介绍的基础知识来做一个实践项目 —— 编写一个简易版本的 RPC 框架,作为“基础知识”部分的总结和回顾。</p>
<p>RPC 是“远程过程调用Remote Procedure Call”的缩写形式比较通俗的解释是<strong>像本地方法调用一样调用远程的服务</strong>。虽然 RPC 的定义非常简单,但是相对完整的、通用的 RPC 框架涉及很多方面的内容例如注册发现、服务治理、负载均衡、集群容错、RPC 协议等,如下图所示:</p>
<p><img src="assets/Ciqc1F9N7ryADlJBAAIlSORjRt8863.png" alt="1.png" /></p>
<p><img src="assets/Ciqc1F9N7ryADlJBAAIlSORjRt8863.png" alt="png" /></p>
<p>简易 RPC 框架的架构图</p>
<p>本课时我们主要实现<strong>RPC 框架的基石部分——远程调用</strong>,简易版 RPC 框架一次远程调用的核心流程是这样的:</p>
<ol>
@@ -306,7 +306,7 @@ function hide_canvas() {
<p>这个远程调用的过程,就是我们简易版本 RPC 框架的核心实现,只有理解了这个流程,才能进行后续的开发。</p>
<h3>项目结构</h3>
<p>了解了简易版 RPC 框架的工作流程和实现目标之后,我们再来看下项目的结构,为了方便起见,这里我们将整个项目放到了一个 Module 中了,如下图所示,你可以按照自己的需求进行模块划分。</p>
<p><img src="assets/Ciqc1F9N7suABhc9AAEN07V6uf8238.png" alt="image" /></p>
<p><img src="assets/Ciqc1F9N7suABhc9AAEN07V6uf8238.png" alt="png" /></p>
<p>那这各个包的功能是怎样的呢?我们就来一一说明。</p>
<ul>
<li>protocol简易版 RPC 框架的自定义协议。</li>
@@ -321,7 +321,7 @@ function hide_canvas() {
<p>从功能角度考虑HTTP 协议在 1.X 时代只支持半双工传输模式虽然支持长连接但是不支持服务端主动推送数据。从效率角度来看在一次简单的远程调用中只需要传递方法名和加个简单的参数此时HTTP 请求中大部分数据都被 HTTP Header 占据,真正的有效负载非常少,效率就比较低。</p>
<p>当然HTTP 协议也有自己的优势,例如,天然穿透防火墙,大量的框架和开源软件支持 HTTP 接口,而且配合 REST 规范使用也是很便捷的,所以有很多 RPC 框架直接使用 HTTP 协议,尤其是在 HTTP 2.0 之后,如 gRPC、Spring Cloud 等。</p>
<p>这里我们自定义一个简易版的 Demo RPC 协议,如下图所示:</p>
<p><img src="assets/Ciqc1F9N7tiAMnZdAAC77BEcyZk305.png" alt="image" /></p>
<p><img src="assets/Ciqc1F9N7tiAMnZdAAC77BEcyZk305.png" alt="png" /></p>
<p>在 Demo RPC 的消息头中,包含了整个 RPC 消息的一些控制信息,例如,版本号、魔数、消息类型、附加信息、消息 ID 以及消息体的长度在附加信息extraInfo按位进行划分分别定义消息的类型、序列化方式、压缩方式以及请求类型。当然你也可以自己扩充 Demo RPC 协议,实现更加复杂的功能。</p>
<p>Demo RPC 消息头对应的实体类是 Header其定义如下</p>
<pre><code>public class Header {
@@ -391,7 +391,7 @@ public class Response implements Serializable {
<h3>编解码实现</h3>
<p>了解了自定义协议的结构之后,我们再来解决协议的编解码问题。</p>
<p>前面课时介绍 Netty 核心概念的时候我们提到过Netty 每个 Channel 绑定一个 ChannelPipeline并依赖 ChannelPipeline 中添加的 ChannelHandler 处理接收到或要发送的数据其中就包括字节到消息以及消息到字节的转换。Netty 中提供了 ByteToMessageDecoder、 MessageToByteEncoder、MessageToMessageEncoder、MessageToMessageDecoder 等抽象类来实现 Message 与 ByteBuf 之间的转换以及 Message 之间的转换,如下图所示:</p>
<p><img src="assets/CgqCHl9N89uAPRjFAAE8pTlyXls424.png" alt="image" /></p>
<p><img src="assets/CgqCHl9N89uAPRjFAAE8pTlyXls424.png" alt="png" /></p>
<p>Netty 提供的 Decoder 和 Encoder 实现</p>
<p>在 Netty 的源码中我们可以看到对很多已有协议的序列化和反序列化都是基于上述抽象类实现的例如HttpServerCodec 中通过依赖 HttpServerRequestDecoder 和 HttpServerResponseEncoder 来实现 HTTP 请求的解码和 HTTP 响应的编码。如下图所示HttpServerRequestDecoder 继承自 ByteToMessageDecoder实现了 ByteBuf 到 HTTP 请求之间的转换HttpServerResponseEncoder 继承自 MessageToMessageEncoder实现 HTTP 响应到其他消息的转换(其中包括转换成 ByteBuf 的能力)。</p>
<p><img src="assets/CgqCHl9OAneAfCv0AADjLyEPSpc098.png" alt="HttpServerCodec结构图.png" /></p>

View File

@@ -295,7 +295,7 @@ function hide_canvas() {
<p>正如前文介绍 Netty 线程模型的时候提到,我们不能在 Netty 的 I/O 线程中执行耗时的业务逻辑。在 Demo RPC 框架的 Server 端接收到请求时,首先会通过上面介绍的 DemoRpcDecoder 反序列化得到请求消息,之后我们会通过一个自定义的 ChannelHandlerDemoRpcServerHandler将请求提交给业务线程池进行处理。</p>
<p>在 Demo RPC 框架的 Client 端接收到响应消息的时候,也是先通过 DemoRpcDecoder 反序列化得到响应消息,之后通过一个自定义的 ChannelHandlerDemoRpcClientHandler将响应返回给上层业务。</p>
<p>DemoRpcServerHandler 和 DemoRpcClientHandler 都继承自 SimpleChannelInboundHandler如下图所示</p>
<p><img src="assets/Ciqc1F9R3QOAbbKRAAD4lAEEjtg767.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F9R3QOAbbKRAAD4lAEEjtg767.png" alt="png" /></p>
<p>DemoRpcClientHandler 和 DemoRpcServerHandler 的继承关系图</p>
<p>下面我们就来看一下这两个自定义的 ChannelHandler 实现:</p>
<pre><code>public class DemoRpcServerHandler extends
@@ -431,7 +431,7 @@ public class DemoRpcClientHandler extends
}
</code></pre>
<p>通过 DemoRpcClient 的代码我们可以看到其 ChannelHandler 的执行顺序如下:</p>
<p><img src="assets/Ciqc1F9R35eARBOdAAEUxDl6DGE227.png" alt="Lark20200904-143159.png" /></p>
<p><img src="assets/Ciqc1F9R35eARBOdAAEUxDl6DGE227.png" alt="png" /></p>
<p>客户端 ChannelHandler 结构图</p>
<p>另外在创建EventLoopGroup时并没有直接使用NioEventLoopGroup而是在 NettyEventLoopFactory 中根据当前操作系统进行选择,对于 Linux 系统,会使用 EpollEventLoopGroup其他系统则使用 NioEventLoopGroup。</p>
<p>接下来我们再看<strong>DemoRpcServer 的具体实现</strong></p>
@@ -475,13 +475,13 @@ public class DemoRpcClientHandler extends
}
</code></pre>
<p>通过对 DemoRpcServer 实现的分析,我们可以知道每个 Channel 上的 ChannelHandler 顺序如下:</p>
<p><img src="assets/CgqCHl9R34eAEosNAAEZMNHAB1c561.png" alt="Lark20200904-143204.png" /></p>
<p><img src="assets/CgqCHl9R34eAEosNAAEZMNHAB1c561.png" alt="png" /></p>
<p>服务端 ChannelHandler 结构图</p>
<h3>registry 相关实现</h3>
<p>介绍完客户端和服务端的通信之后,我们再来看简易 RPC 框架的另一个基础能力——服务注册与服务发现能力,对应 demo-rpc 项目源码中的 registry 包。</p>
<p>registry 包主要是依赖 Apache Curator 实现了一个简易版本的 ZooKeeper 客户端,并基于 ZooKeeper 实现了注册中心最基本的两个功能Provider 注册以及 Consumer 订阅。</p>
<p>这里我们先定义一个 Registry 接口,其中提供了注册以及查询服务实例的方法,如下图所示:</p>
<p><img src="assets/Ciqc1F9R3WuAd1UPAAA82c309GI280.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Ciqc1F9R3WuAd1UPAAA82c309GI280.png" alt="png" /></p>
<p>ZooKeeperRegistry 是基于 curator-x-discovery 对 Registry 接口的实现类型,其中封装了之前课时介绍的 ServiceDiscovery并在其上添加了 ServiceCache 缓存提高查询效率。ZooKeeperRegistry 的具体实现如下:</p>
<pre><code>public class ZookeeperRegistry&lt;T&gt; implements Registry&lt;T&gt; {
private InstanceSerializer serializer =
@@ -628,7 +628,7 @@ public class DemoRpcClientHandler extends
<p>你若感兴趣的话可以尝试进行扩展,以实现一个更加完善的代理层。</p>
<h3>使用方接入</h3>
<p>介绍完 Demo RPC 的核心实现之后下面我们讲解下Demo RPC 框架的使用方式。这里涉及Consumer、DemoServiceImp、Provider三个类以及 DemoService 业务接口。</p>
<p><img src="assets/Ciqc1F9R3YiAXV8hAAAtXArd3J0997.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Ciqc1F9R3YiAXV8hAAAtXArd3J0997.png" alt="png" /></p>
<p>使用接入的相关类</p>
<p>首先,我们定义<strong>DemoService 接口</strong>作为业务 Server 接口,具体定义如下:</p>
<pre><code>public interface DemoService {

View File

@@ -291,7 +291,7 @@ function hide_canvas() {
<p id="tip" align="center"></p>
<div><h1>13 本地缓存:降低 ZooKeeper 压力的一个常用手段</h1>
<p>从这一课时开始我们就进入了第二部分注册中心。注册中心Registry在微服务架构中的作用举足轻重有了它<strong>服务提供者Provider</strong><strong>消费者Consumer</strong> 就能感知彼此。从下面的 Dubbo 架构图中可知:</p>
<p><img src="assets/CgqCHl9W91KABCfoAAB7_C-aKWA893.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl9W91KABCfoAAB7_C-aKWA893.png" alt="png" /></p>
<p>Dubbo 架构图</p>
<ul>
<li>Provider 从容器启动后的初始化阶段便会向注册中心完成注册操作;</li>
@@ -301,27 +301,27 @@ function hide_canvas() {
<p>Registry 只是 Consumer 和 Provider 感知彼此状态变化的一种便捷途径而已,它们彼此的实际通讯交互过程是直接进行的,对于 Registry 来说是透明无感的。Provider 状态发生变化了,会由 Registry 主动推送订阅了该 Provider 的所有 Consumer这保证了 Consumer 感知 Provider 状态变化的及时性,也将和具体业务需求逻辑交互解耦,提升了系统的稳定性。</p>
<p>Dubbo 中存在很多概念,但有些理解起来就特别费劲,如本文的 Registry翻译过来的意思是“注册中心”但它其实是应用本地的注册中心客户端<strong>真正的“注册中心”服务是其他独立部署的进程,或进程组成的集群,比如 ZooKeeper 集群</strong>。本地的 Registry 通过和 ZooKeeper 等进行实时的信息同步,维持这些内容的一致性,从而实现了注册中心这个特性。另外,就 Registry 而言Consumer 和 Provider 只是个用户视角的概念,它们被抽象为了一条 URL 。</p>
<p>从本课时开始,我们就真正开始分析 Dubbo 源码了。首先看一下本课程第二部分内容在 Dubbo 架构中所处的位置(如下图红框所示),可以看到这部分内容在整个 Dubbo 体系中还是相对独立的,没有涉及 Protocol、Invoker 等 Dubbo 内部的概念。等介绍完这些概念之后,我们还会回看图中 Registry 红框之外的内容。</p>
<p><img src="assets/CgqCHl9W92uAEdHNAC1YtFrPHGA595.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl9W92uAEdHNAC1YtFrPHGA595.png" alt="png" /></p>
<p>整个 Dubbo 体系图</p>
<h3>核心接口</h3>
<p>作为“注册中心”部分的第一课时,我们有必要介绍下 dubbo-registry-api 模块中的核心抽象接口,如下图所示:</p>
<p><img src="assets/Ciqc1F9W94aAIB3iAAE7RxqxFDw401.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1F9W94aAIB3iAAE7RxqxFDw401.png" alt="png" /></p>
<p>在 Dubbo 中,一般使用 Node 这个接口来抽象节点的概念。<strong>Node</strong>不仅可以表示 Provider 和 Consumer 节点还可以表示注册中心节点。Node 接口中定义了三个非常基础的方法(如下图所示):</p>
<p><img src="assets/Ciqc1F9W942AJdaYAAAlxcqD4vE542.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Ciqc1F9W942AJdaYAAAlxcqD4vE542.png" alt="png" /></p>
<ul>
<li>getUrl() 方法返回表示当前节点的 URL</li>
<li>isAvailable() 检测当前节点是否可用;</li>
<li>destroy() 方法负责销毁当前节点并释放底层资源。</li>
</ul>
<p><strong>RegistryService 接口</strong>抽象了注册服务的基本行为,如下图所示:</p>
<p><img src="assets/CgqCHl9W95SAEiTBAABRqhrI6ig390.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl9W95SAEiTBAABRqhrI6ig390.png" alt="png" /></p>
<ul>
<li>register() 方法和 unregister() 方法分别表示<strong>注册</strong><strong>取消注册</strong>一个 URL。</li>
<li>subscribe() 方法和 unsubscribe() 方法分别表示<strong>订阅</strong><strong>取消订阅</strong>一个 URL。订阅成功之后当订阅的数据发生变化时注册中心会主动通知第二个参数指定的 NotifyListener 对象NotifyListener 接口中定义的 notify() 方法就是用来接收该通知的。</li>
<li>lookup() 方法能够<strong>查询</strong>符合条件的注册数据,它与 subscribe() 方法有一定的区别subscribe() 方法采用的是 push 模式lookup() 方法采用的是 pull 模式。</li>
</ul>
<p><strong>Registry 接口</strong>继承了 RegistryService 接口和 Node 接口,如下图所示,它表示的就是一个拥有注册中心能力的节点,其中的 reExportRegister() 和 reExportUnregister() 方法都是委托给 RegistryService 中的相应方法。</p>
<p><img src="assets/Ciqc1F9W952Aesi9AAAjKOjjN0I785.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Ciqc1F9W952Aesi9AAAjKOjjN0I785.png" alt="png" /></p>
<p><strong>RegistryFactory 接口</strong>是 Registry 的工厂接口,负责创建 Registry 对象,具体定义如下所示,其中 @SPI 注解指定了默认的扩展名为 dubbo@Adaptive 注解表示会生成适配器类并根据 URL 参数中的 protocol 参数值选择相应的实现。</p>
<pre><code>@SPI(&quot;dubbo&quot;)
public interface RegistryFactory {
@@ -330,9 +330,9 @@ public interface RegistryFactory {
}
</code></pre>
<p>通过下面两张继承关系图可以看出,每个 Registry 实现类都有对应的 RegistryFactory 工厂实现,每个 RegistryFactory 工厂实现只负责创建对应的 Registry 对象。</p>
<p><img src="assets/CgqCHl9W96aAbyVRAAIzHNPLhSM843.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CgqCHl9W96aAbyVRAAIzHNPLhSM843.png" alt="png" /></p>
<p>RegistryFactory 继承关系图</p>
<p><img src="assets/Ciqc1F9W97CAdPcXAAG1fsVxaeI019.png" alt="Drawing 7.png" /></p>
<p><img src="assets/Ciqc1F9W97CAdPcXAAG1fsVxaeI019.png" alt="png" /></p>
<p>Registry 继承关系图</p>
<p>其中RegistryFactoryWrapper 是 RegistryFactory 接口的 Wrapper 类,它在底层 RegistryFactory 创建的 Registry 对象外层封装了一个 ListenerRegistryWrapper ListenerRegistryWrapper 中维护了一个 RegistryServiceListener 集合,会将 register()、subscribe() 等事件通知到 RegistryServiceListener 监听器。</p>
<p>AbstractRegistryFactory 是一个实现了 RegistryFactory 接口的抽象类,提供了规范 URL 的操作以及缓存 Registry 对象的公共能力。其中,缓存 Registry 对象是使用 HashMap&lt;String, Registry&gt; 集合实现的REGISTRIES 静态字段)。在规范 URL 的实现逻辑中AbstractRegistryFactory 会将 RegistryService 的类名设置为 URL path 和 interface 参数,同时删除 export 和 refer 参数。</p>
@@ -404,7 +404,7 @@ protected void notify(URL url, NotifyListener listener,
<p>subscribe() 方法会将当前节点作为 Consumer 的 URL 以及相关的 NotifyListener 记录到 subscribed 集合unsubscribe() 方法会将当前节点的 URL 以及关联的 NotifyListener 从 subscribed 集合删除。</p>
<p>这四个方法都是简单的集合操作,这里我们就不再展示具体代码了。</p>
<p>单看 AbstractRegistry 的实现,上述四个基础的注册、订阅方法都是内存操作,但是 Java 有继承和多态的特性AbstractRegistry 的子类会覆盖上述四个基础的注册、订阅方法进行增强。</p>
<p><img src="assets/Ciqc1F9W9-eAHUVPAACO6kbGAbU855.png" alt="Drawing 8.png" /></p>
<p><img src="assets/Ciqc1F9W9-eAHUVPAACO6kbGAbU855.png" alt="png" /></p>
<h4>3. 恢复/销毁</h4>
<p>AbstractRegistry 中还有另外两个需要关注的方法:<strong>recover() 方法</strong><strong>destroy() 方法</strong></p>
<p>在 Provider 因为网络问题与注册中心断开连接之后,会进行重连,重新连接成功之后,会调用 recover() 方法将 registered 集合中的全部 URL 重新走一遍 register() 方法恢复注册数据。同样recover() 方法也会将 subscribed 集合中的 URL 重新走一遍 subscribe() 方法恢复订阅监听器。recover() 方法的具体实现比较简单,这里就不再展示,你若感兴趣的话,可以参考源码进行学习。</p>

View File

@@ -295,7 +295,7 @@ function hide_canvas() {
<p>Dubbo 本身是一个分布式的 RPC 开源框架,各个依赖于 Dubbo 的服务节点都是单独部署的,为了让 Provider 和 Consumer 能够实时获取彼此的信息,就得依赖于一个<strong>一致性的服务发现组件</strong>实现注册和订阅。Dubbo 可以接入多种服务发现组件例如ZooKeeper、etcd、Consul、Eureka 等。其中Dubbo 特别推荐使用 ZooKeeper。</p>
<p><strong>ZooKeeper 是为分布式应用所设计的高可用且一致性的开源协调服务</strong>。它是一个树型的目录服务,支持变更推送,非常适合应用在生产环境中。</p>
<p>下面是 Dubbo 官方文档中的一张图,展示了 Dubbo 在 Zookeeper 中的节点层级结构:</p>
<p><img src="assets/Ciqc1F9gay-AdrWMAAGjEWP00aQ382.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F9gay-AdrWMAAGjEWP00aQ382.png" alt="png" /></p>
<p>Zookeeper 存储的 Dubbo 数据</p>
<p>图中的“dubbo”节点是 Dubbo 在 Zookeeper 中的根节点“dubbo”是这个根节点的默认名称当然我们也可以通过配置进行修改。</p>
<p>图中 Service 这一层的节点名称是服务接口的全名,例如 demo 示例中该节点的名称为“org.apache.dubbo.demo.DemoService”。</p>
@@ -303,7 +303,7 @@ function hide_canvas() {
<p>根据不同的 Type 节点,图中 URL 这一层中的节点包括Provider URL 、Consumer URL 、Routes URL 和 Configurations URL。</p>
<h3>ZookeeperRegistryFactory</h3>
<p>在前面第 13 课时介绍 Dubbo 注册中心核心概念的时候,我们讲解了 RegistryFactory 这个工厂接口以及其子类 AbstractRegistryFactoryAbstractRegistryFactory 仅仅是提供了缓存 Registry 对象的功能,并未真正实现 Registry 的创建,具体的创建逻辑是由子类完成的。在 dubbo-registry-zookeeper 模块中的 SPI 配置文件(目录位置如下图所示)中,指定了<strong>RegistryFactory 的实现类—— ZookeeperRegistryFactory</strong></p>
<p><img src="assets/CgqCHl9ga02AUesuAABPhgP1Voc406.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl9ga02AUesuAABPhgP1Voc406.png" alt="png" /></p>
<p>RegistryFactory 的 SPI 配置文件位置</p>
<p>ZookeeperRegistryFactory 实现了 AbstractRegistryFactory其中的 createRegistry() 方法会创建 ZookeeperRegistry 实例,后续将由该 ZookeeperRegistry 实例完成与 Zookeeper 的交互。</p>
<p>另外ZookeeperRegistryFactory 中还提供了一个 setZookeeperTransporter() 方法,你可以回顾一下之前我们介绍的 Dubbo SPI 机制,会通过 SPI 或 Spring Ioc 的方式完成自动装载。</p>
@@ -319,7 +319,7 @@ public interface ZookeeperTransporter {
}
</code></pre>
<p>我们从代码中可以看到ZookeeperTransporter 接口被 @SPI 注解修饰,成为一个扩展点,默认选择扩展名 “curator” 的实现,其中的 connect() 方法用于创建 ZookeeperClient 实例(该方法被 @Adaptive 注解修饰,我们可以通过 URL 参数中的 client 或 transporter 参数覆盖 @SPI 注解指定的默认扩展名)。</p>
<p><img src="assets/CgqCHl9ga2CAVhNZAACNo2yx1q4384.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl9ga2CAVhNZAACNo2yx1q4384.png" alt="png" /></p>
<p>按照前面对 Registry 分析的思路作为一个抽象实现AbstractZookeeperTransporter 肯定是实现了创建 ZookeeperClient 之外的其他一些增强功能,然后由子类继承。不然的话,直接由 CuratorZookeeperTransporter 实现 ZookeeperTransporter 接口创建 ZookeeperClient 实例并返回即可,没必要在继承关系中再增加一层抽象类。</p>
<pre><code>public class CuratorZookeeperTransporter extends
AbstractZookeeperTransporter {
@@ -360,15 +360,15 @@ public interface ZookeeperTransporter {
<ul>
<li><strong>StateListener</strong>:主要负责监听 Dubbo 与 Zookeeper 集群的连接状态,包括 SESSION_LOST、CONNECTED、RECONNECTED、SUSPENDED 和 NEW_SESSION_CREATED。</li>
</ul>
<p><img src="assets/CgqCHl9ga4GAQmYSAAAtjyGIDtE504.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CgqCHl9ga4GAQmYSAAAtjyGIDtE504.png" alt="png" /></p>
<ul>
<li><strong>DataListener</strong>:主要监听某个节点存储的数据变化。</li>
</ul>
<p><img src="assets/Ciqc1F9ga4qAVm-6AAAzoshbsio688.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Ciqc1F9ga4qAVm-6AAAzoshbsio688.png" alt="png" /></p>
<ul>
<li>**ChildListener**主要监听某个 ZNode 节点下的子节点变化。</li>
</ul>
<p><img src="assets/CgqCHl9ga4-Aa-4IAABLF9PT8ls256.png" alt="Drawing 5.png" /></p>
<p><img src="assets/CgqCHl9ga4-Aa-4IAABLF9PT8ls256.png" alt="png" /></p>
<p>在 AbstractZookeeperClient 中维护了 stateListeners、listeners 以及 childListeners 三个集合,分别管理上述三种类型的监听器。虽然监听内容不同,但是它们的管理方式是类似的,所以这里我们只分析 listeners 集合的操作:</p>
<pre><code>public void addDataListener(String path,
DataListener listener, Executor executor) {
@@ -456,11 +456,11 @@ public interface ZookeeperTransporter {
<p>在 ZookeeperRegistry 的构造方法中,会通过 ZookeeperTransporter 创建 ZookeeperClient 实例并连接到 Zookeeper 集群同时还会添加一个连接状态的监听器。在该监听器中主要关注RECONNECTED 状态和 NEW_SESSION_CREATED 状态,在当前 Dubbo 节点与 Zookeeper 的连接恢复或是 Session 恢复的时候,会重新进行注册/订阅,防止数据丢失。这段代码比较简单,我们就不展开分析了。</p>
<p>doRegister() 方法和 doUnregister() 方法的实现都是通过 ZookeeperClient 找到合适的路径,然后创建(或删除)相应的 ZNode 节点。这里唯一需要注意的是doRegister() 方法注册 Provider URL 的时候,会根据 dynamic 参数决定创建临时 ZNode 节点还是持久 ZNode 节点(默认创建临时 ZNode 节点),这样当 Provider 端与 Zookeeper 会话关闭时,可以快速将变更推送到 Consumer 端。</p>
<p>这里注意一下 toUrlPath() 这个方法得到的路径,是由下图中展示的方法拼装而成的,其中每个方法对应本课时开始展示的 Zookeeper 节点层级图中的一层。</p>
<p><img src="assets/Ciqc1F9ga6qAOzWsAAGn7w4zPbo192.png" alt="Drawing 6.png" /></p>
<p><img src="assets/Ciqc1F9ga6qAOzWsAAGn7w4zPbo192.png" alt="png" /></p>
<p>doSubscribe() 方法的核心是通过 ZookeeperClient 在指定的 path 上添加 ChildListener 监听器,当订阅的节点发现变化的时候,会通过 ChildListener 监听器触发 notify() 方法,在 notify() 方法中会触发传入的 NotifyListener 监听器。</p>
<p>从 doSubscribe() 方法的代码结构可看出doSubscribe() 方法的逻辑分为了两个大的分支。</p>
<p>一个分支是处理:订阅 URL 中明确指定了 Service 层接口的订阅请求。该分支会从 URL 拿到 Consumer 关注的 category 节点集合,然后在每个 category 节点上添加 ChildListener 监听器。下面是 Demo 示例中 Consumer 订阅的三个 path图中展示了构造 path 各个部分的相关方法:</p>
<p><img src="assets/Ciqc1F9gc_WAYTGzAAEKDnK-16Q791.png" alt="Lark20200915-155646.png" /></p>
<p><img src="assets/Ciqc1F9gc_WAYTGzAAEKDnK-16Q791.png" alt="png" /></p>
<p>下面是这个分支的核心源码分析:</p>
<pre><code>List&lt;URL&gt; urls = new ArrayList&lt;&gt;();
for (String path : toCategoriesPath(url)) { // 要订阅的所有path

View File

@@ -316,7 +316,7 @@ function hide_canvas() {
<p><strong>ProtobufGoogle Protocol Buffers是 Google 公司开发的一套灵活、高效、自动化的、用于对结构化数据进行序列化的协议</strong>。但相比于常用的 JSON 格式Protobuf 有更高的转化效率,时间效率和空间效率都是 JSON 的 5 倍左右。Protobuf 可用于通信协议、数据存储等领域,它本身是语言无关、平台无关、可扩展的序列化结构数据格式。目前 Protobuf提供了 C++、Java、Python、Go 等多种语言的 APIgRPC 底层就是使用 Protobuf 实现的序列化。</p>
<h3>dubbo-serialization</h3>
<p>Dubbo 为了支持多种序列化算法,单独抽象了一层 Serialize 层,在整个 Dubbo 架构中处于最底层,对应的模块是 dubbo-serialization 模块。 dubbo-serialization 模块的结构如下图所示:</p>
<p><img src="assets/Ciqc1F9gbIiAdyaqAAB4bHnToKs832.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F9gbIiAdyaqAAB4bHnToKs832.png" alt="png" /></p>
<p>dubbo-serialization-api 模块中定义了 Dubbo 序列化层的核心接口,其中最核心的是 Serialization 这个接口,它是一个扩展接口,被 @SPI 接口修饰,默认扩展实现是 Hessian2Serialization。Serialization 接口的具体实现如下:</p>
<pre><code>@SPI(&quot;hessian2&quot;) // 被@SPI注解修饰默认是使用hessian2序列化算法
public interface Serialization {
@@ -335,7 +335,7 @@ public interface Serialization {
}
</code></pre>
<p>Dubbo 提供了多个 Serialization 接口实现,用于接入各种各样的序列化算法,如下图所示:</p>
<p><img src="assets/CgqCHl9gbJKAFOslAAFjEeB7nf0890.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl9gbJKAFOslAAFjEeB7nf0890.png" alt="png" /></p>
<p>这里我们<strong>以默认的 hessian2 序列化方式为例</strong>,介绍 Serialization 接口的实现以及其他相关实现。 Hessian2Serialization 实现如下所示:</p>
<pre><code>public class Hessian2Serialization implements Serialization {
public byte getContentTypeId() {
@@ -353,11 +353,11 @@ public interface Serialization {
}
</code></pre>
<p>Hessian2Serialization 中的 serialize() 方法创建的 ObjectOutput 接口实现为 Hessian2ObjectOutput继承关系如下图所示</p>
<p><img src="assets/CgqCHl9gbOiAG_1mAABH4c18z9c011.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl9gbOiAG_1mAABH4c18z9c011.png" alt="png" /></p>
<p>在 DataOutput 接口中定义了序列化 Java 中各种数据类型的相应方法,如下图所示,其中有序列化 boolean、short、int、long 等基础类型的方法,也有序列化 String、byte[] 的方法。</p>
<p><img src="assets/Ciqc1F9gbO6AExKqAAB_Dm_zMt0793.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Ciqc1F9gbO6AExKqAAB_Dm_zMt0793.png" alt="png" /></p>
<p>ObjectOutput 接口继承了 DataOutput 接口,并在其基础之上,添加了序列化对象的功能,具体定义如下图所示,其中的 writeThrowable()、writeEvent() 和 writeAttachments() 方法都是调用 writeObject() 方法实现的。</p>
<p><img src="assets/CgqCHl9gbPOATpsmAABH5ZuVc6E438.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl9gbPOATpsmAABH5ZuVc6E438.png" alt="png" /></p>
<p>Hessian2ObjectOutput 中会封装一个 Hessian2Output 对象,需要注意,这个对象是 ThreadLocal 的,与线程绑定。在 DataOutput 接口以及 ObjectOutput 接口中,序列化各类型数据的方法都会委托给 Hessian2Output 对象的相应方法完成,实现如下:</p>
<pre><code>public class Hessian2ObjectOutput implements ObjectOutput {
private static ThreadLocal&lt;Hessian2Output&gt; OUTPUT_TL = ThreadLocal.withInitial(() -&gt; {
@@ -378,7 +378,7 @@ public interface Serialization {
}
</code></pre>
<p>Hessian2Serialization 中的 deserialize() 方法创建的 ObjectInput 接口实现为 Hessian2ObjectInput继承关系如下所示</p>
<p><img src="assets/CgqCHl9gbQ6AXSDeAABIcO3u8aY906.png" alt="Drawing 5.png" /></p>
<p><img src="assets/CgqCHl9gbQ6AXSDeAABIcO3u8aY906.png" alt="png" /></p>
<p>Hessian2ObjectInput 具体的实现与 Hessian2ObjectOutput 类似:在 DataInput 接口中实现了反序列化各种类型的方法,在 ObjectInput 接口中提供了反序列化 Java 对象的功能,在 Hessian2ObjectInput 中会将所有反序列化的实现委托为 Hessian2Input。</p>
<p>了解了 Dubbo Serialize 层的核心接口以及 Hessian2 序列化算法的接入方式之后,你就可以亲自动手,去阅读其他序列化算法对应模块的代码。</p>
<h3>总结</h3>

View File

@@ -291,17 +291,17 @@ function hide_canvas() {
<p id="tip" align="center"></p>
<div><h1>17 Dubbo Remoting 层核心接口分析:这居然是一套兼容所有 NIO 框架的设计?</h1>
<p>在本专栏的第二部分,我们深入介绍了 Dubbo 注册中心的相关实现,下面我们开始介绍 dubbo-remoting 模块,该模块提供了多种客户端和服务端通信的功能。在 Dubbo 的整体架构设计图中,我们可以看到最底层红色框选中的部分即为 Remoting 层,其中包括了 Exchange、Transport和Serialize 三个子层次。这里我们要介绍的 dubbo-remoting 模块主要对应 Exchange 和 Transport 两层。</p>
<p><img src="assets/CgqCHl9ptP2ADxEXAAuW94W_upc465.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl9ptP2ADxEXAAuW94W_upc465.png" alt="png" /></p>
<p>Dubbo 整体架构设计图</p>
<p>Dubbo 并没有自己实现一套完整的网络库而是使用现有的、相对成熟的第三方网络库例如Netty、Mina 或是 Grizzly 等 NIO 框架。我们可以根据自己的实际场景和需求修改配置,选择底层使用的 NIO 框架。</p>
<p>下图展示了 dubbo-remoting 模块的结构,其中每个子模块对应一个第三方 NIO 框架例如dubbo-remoting-netty4 子模块使用 Netty4 实现 Dubbo 的远程通信dubbo-remoting-grizzly 子模块使用 Grizzly 实现 Dubbo 的远程通信。</p>
<p><img src="assets/Ciqc1F9ptRqAJLQnAABcIxQfCkc811.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1F9ptRqAJLQnAABcIxQfCkc811.png" alt="png" /></p>
<p>其中的 dubbo-remoting-zookeeper我们在前面第 15 课时介绍基于 Zookeeper 的注册中心实现时已经讲解过了,它使用 Apache Curator 实现了与 Zookeeper 的交互。</p>
<h3>dubbo-remoting-api 模块</h3>
<p>需要注意的是,<strong>Dubbo 的 dubbo-remoting-api 是其他 dubbo-remoting-* 模块的顶层抽象,其他 dubbo-remoting 子模块都是依赖第三方 NIO 库实现 dubbo-remoting-api 模块的</strong>,依赖关系如下图所示:</p>
<p><img src="assets/CgqCHl9ptY2ADzl8AAEVDPN3HVo908.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl9ptY2ADzl8AAEVDPN3HVo908.png" alt="png" /></p>
<p>我们先来看一下 dubbo-remoting-api 中对整个 Remoting 层的抽象dubbo-remoting-api 模块的结构如下图所示:</p>
<p><img src="assets/Ciqc1F9ptduASJsQAACrkCpgiGg477.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Ciqc1F9ptduASJsQAACrkCpgiGg477.png" alt="png" /></p>
<p>一般情况下,我们会将功能类似或是相关联的类放到一个包中,所以我们需要先来了解 dubbo-remoting-api 模块中各个包的功能。</p>
<ul>
<li>buffer 包定义了缓冲区相关的接口、抽象类以及实现类。缓冲区在NIO框架中是一个不可或缺的角色在各个 NIO 框架中都有自己的缓冲区实现。这里的 buffer 包在更高的层面,抽象了各个 NIO 框架的缓冲区,同时也提供了一些基础实现。</li>
@@ -313,14 +313,14 @@ function hide_canvas() {
<h3>传输层核心接口</h3>
<p>在 Dubbo 中会抽象出一个“<strong>端点Endpoint</strong>”的概念,我们可以通过一个 ip 和 port 唯一确定一个端点,两个端点之间会创建 TCP 连接可以双向传输数据。Dubbo 将 Endpoint 之间的 TCP 连接抽象为<strong>通道Channel</strong>,将发起请求的 Endpoint 抽象为<strong>客户端Client</strong>,将接收请求的 Endpoint 抽象为<strong>服务端Server</strong>。这些抽象出来的概念,也是整个 dubbo-remoting-api 模块的基础,下面我们会逐个进行介绍。</p>
<p>Dubbo 中<strong>Endpoint 接口</strong>的定义如下:</p>
<p><img src="assets/CgqCHl9pteqACl0cAABxWeZ6ox0288.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl9pteqACl0cAABxWeZ6ox0288.png" alt="png" /></p>
<p>如上图所示,这里的 get*() 方法是获得 Endpoint 本身的一些属性,其中包括获取 Endpoint 的本地地址、关联的 URL 信息以及底层 Channel 关联的 ChannelHandler。send() 方法负责数据发送,两个重载的区别在后面介绍 Endpoint 实现的时候我们再详细说明。最后两个 close() 方法的重载以及 startClose() 方法用于关闭底层 Channel isClosed() 方法用于检测底层 Channel 是否已关闭。</p>
<p>Channel 是对两个 Endpoint 连接的抽象,好比连接两个位置的传送带,两个 Endpoint 传输的消息就好比传送带上的货物,消息发送端会往 Channel 写入消息,而接收端会从 Channel 读取消息。这与第 10 课时介绍的 Netty 中的 Channel 基本一致。</p>
<p><img src="assets/CgqCHl9ptsaAeodMAACTIzdsI8g890.png" alt="Lark20200922-162359.png" /></p>
<p><img src="assets/CgqCHl9ptsaAeodMAACTIzdsI8g890.png" alt="png" /></p>
<p>下面是<strong>Channel 接口</strong>的定义,我们可以看出两点:一个是 Channel 接口继承了 Endpoint 接口,也具备开关状态以及发送数据的能力;另一个是可以在 Channel 上附加 KV 属性。</p>
<p><img src="assets/Ciqc1F9ptfKAeNrwAADvN7mxisw072.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Ciqc1F9ptfKAeNrwAADvN7mxisw072.png" alt="png" /></p>
<p><strong>ChannelHandler 是注册在 Channel 上的消息处理器</strong>,在 Netty 中也有类似的抽象,相信你对此应该不会陌生。下图展示了 ChannelHandler 接口的定义,在 ChannelHandler 中可以处理 Channel 的连接建立以及连接断开事件,还可以处理读取到的数据、发送的数据以及捕获到的异常。从这些方法的命名可以看到,它们都是动词的过去式,说明相应事件已经发生过了。</p>
<p><img src="assets/CgqCHl9ptf-AM7HwAABIy1ahqFw153.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CgqCHl9ptf-AM7HwAABIy1ahqFw153.png" alt="png" /></p>
<p>需要注意的是ChannelHandler 接口被 @SPI 注解修饰,表示该接口是一个扩展点。</p>
<p>在前面课时介绍 Netty 的时候,我们提到过有一类特殊的 ChannelHandler 专门负责实现编解码功能从而实现字节数据与有意义的消息之间的转换或是消息之间的相互转换。在dubbo-remoting-api 中也有相似的抽象,如下所示:</p>
<pre><code>@SPI
@@ -339,9 +339,9 @@ public interface Codec2 {
<p>这里需要关注的是 Codec2 接口被 @SPI 接口修饰了,表示该接口是一个扩展接口,同时其 encode() 方法和 decode() 方法都被 @Adaptive 注解修饰,也就会生成适配器类,其中会根据 URL 中的 codec 值确定具体的扩展实现类。</p>
<p>DecodeResult 这个枚举是在处理 TCP 传输时粘包和拆包使用的,之前简易版本 RPC 也处理过这种问题,例如,当前能读取到的数据不足以构成一个消息时,就会使用 NEED_MORE_INPUT 这个枚举。</p>
<p>接下来看<strong>Client 和 RemotingServer 两个接口</strong>,分别抽象了客户端和服务端,两者都继承了 Channel、Resetable 等接口,也就是说两者都具备了读写数据能力。</p>
<p><img src="assets/CgqCHl9ptgaAPRDbAAA7kgy1X5k082.png" alt="Drawing 7.png" /></p>
<p><img src="assets/CgqCHl9ptgaAPRDbAAA7kgy1X5k082.png" alt="png" /></p>
<p>Client 和 Server 本身都是 Endpoint只不过在语义上区分了请求和响应的职责两者都具备发送的能力所以都继承了 Endpoint 接口。Client 和 Server 的主要区别是 Client 只能关联一个 Channel而 Server 可以接收多个 Client 发起的 Channel 连接。所以在 RemotingServer 接口中定义了查询 Channel 的相关方法,如下图所示:</p>
<p><img src="assets/Ciqc1F9pthSAPWv0AAA0yX1lW-Y033.png" alt="Drawing 8.png" /></p>
<p><img src="assets/Ciqc1F9pthSAPWv0AAA0yX1lW-Y033.png" alt="png" /></p>
<p>Dubbo 在 Client 和 Server 之上又封装了一层<strong>Transporter 接口</strong>,其具体定义如下:</p>
<pre><code>@SPI(&quot;netty&quot;)
public interface Transporter {
@@ -355,10 +355,10 @@ public interface Transporter {
</code></pre>
<p>我们看到 Transporter 接口上有 @SPI 注解它是一个扩展接口默认使用“netty”这个扩展名@Adaptive 注解的出现表示动态生成适配器类会先后根据“server”“transporter”的值确定 RemotingServer 的扩展实现类先后根据“client”“transporter”的值确定 Client 接口的扩展实现。</p>
<p>Transporter 接口的实现有哪些呢?如下图所示,针对每个支持的 NIO 库,都有一个 Transporter 接口实现,散落在各个 dubbo-remoting-* 实现模块中。</p>
<p><img src="assets/CgqCHl9pthuAFNMOAABRJaJXls0493.png" alt="Drawing 9.png" /></p>
<p><img src="assets/CgqCHl9pthuAFNMOAABRJaJXls0493.png" alt="png" /></p>
<p>这些 Transporter 接口实现返回的 Client 和 RemotingServer 具体是什么呢?如下图所示,返回的是 NIO 库对应的 RemotingServer 实现和 Client 实现。</p>
<p><img src="assets/Ciqc1F9ptiCAHkUSAADCSKg5KhY994.png" alt="Drawing 10.png" />
<img src="assets/CgqCHl9pti-AHj3DAACwPfuEgm8435.png" alt="Drawing 11.png" /></p>
<p><img src="assets/Ciqc1F9ptiCAHkUSAADCSKg5KhY994.png" alt="png" />
<img src="assets/CgqCHl9pti-AHj3DAACwPfuEgm8435.png" alt="png" /></p>
<p>相信看到这里,你应该已经发现 Transporter 这一层抽象出来的接口,与 Netty 的核心接口是非常相似的。那为什么要单独抽象出 Transporter层而不是像简易版 RPC 框架那样,直接让上层使用 Netty 呢?</p>
<p>其实这个问题的答案也呼之欲出了Netty、Mina、Grizzly 这个 NIO 库对外接口和使用方式不一样,如果在上层直接依赖了 Netty 或是 Grizzly就依赖了具体的 NIO 库实现,而不是依赖一个有传输能力的抽象,后续要切换实现的话,就需要修改依赖和接入的相关代码,非常容易改出 Bug。这也不符合设计模式中的开放-封闭原则。</p>
<p>有了 Transporter 层之后,我们可以通过 Dubbo SPI 修改使用的具体 Transporter 扩展实现,从而切换到不同的 Client 和 RemotingServer 实现,达到底层 NIO 库切换的目的,而且无须修改任何代码。即使有更先进的 NIO 库出现,我们也只需要开发相应的 dubbo-remoting-* 实现模块提供 Transporter、Client、RemotingServer 等核心接口的实现,即可接入,完全符合开放-封闭原则。</p>
@@ -404,7 +404,7 @@ public interface Transporter {
<li>无论是 Client 还是 RemotingServer都会使用 ChannelHandler 处理 Channel 中传输的数据,其中负责编解码的 ChannelHandler 被抽象出为 Codec2 接口。</li>
</ul>
<p>整个架构如下图所示,与 Netty 的架构非常类似。</p>
<p><img src="assets/CgqCHl9ptlyABsjpAAGGk7pFIzQ293.png" alt="Lark20200922-162354.png" /></p>
<p><img src="assets/CgqCHl9ptlyABsjpAAGGk7pFIzQ293.png" alt="png" /></p>
<p>Transporter 层整体结构图</p>
<h3>总结</h3>
<p>本课时我们首先介绍了 dubbo-remoting 模块在 Dubbo 架构中的位置,以及 dubbo-remoting 模块的结构。接下来分析了 dubbo-remoting 模块中各个子模块之间的依赖关系,并重点介绍了 dubbo-remoting-api 子模块中各个包的核心功能。最后我们还深入分析了整个 Transport 层的核心接口,以及这些接口抽象出来的 Transporter 架构。</p>

View File

@@ -291,7 +291,7 @@ function hide_canvas() {
<p id="tip" align="center"></p>
<div><h1>18 Buffer 缓冲区:我们不生产数据,我们只是数据的搬运工</h1>
<p>Buffer 是一种字节容器,在 Netty 等 NIO 框架中都有类似的设计例如Java NIO 中的ByteBuffer、Netty4 中的 ByteBuf。Dubbo 抽象出了 ChannelBuffer 接口对底层 NIO 框架中的 Buffer 设计进行统一,其子类如下图所示:</p>
<p><img src="assets/CgqCHl9pudyACkPPAABei6G8kSc033.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl9pudyACkPPAABei6G8kSc033.png" alt="png" /></p>
<p>ChannelBuffer 继承关系图</p>
<p>下面我们就按照 ChannelBuffer 的继承结构,从顶层的 ChannelBuffer 接口开始,逐个向下介绍,直至最底层的各个实现类。</p>
<h3>ChannelBuffer 接口</h3>
@@ -303,7 +303,7 @@ function hide_canvas() {
<li>capacity()、clear()、copy() 等辅助方法用来获取 ChannelBuffer 容量以及实现清理、拷贝数据的功能,这里不再赘述。</li>
<li>factory() 方法:该方法返回创建 ChannelBuffer 的工厂对象ChannelBufferFactory 中定义了多个 getBuffer() 方法重载来创建 ChannelBuffer如下图所示这些 ChannelBufferFactory的实现都是单例的。</li>
</ul>
<p><img src="assets/Ciqc1F9pugWAMFoIAABVU01bqiI007.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1F9pugWAMFoIAABVU01bqiI007.png" alt="png" /></p>
<p>ChannelBufferFactory 继承关系图</p>
<p><strong>AbstractChannelBuffer 抽象类</strong>实现了 ChannelBuffer 接口的大部分方法,其核心是维护了以下四个索引。</p>
<ul>
@@ -358,7 +358,7 @@ public ChannelBuffer getBuffer(byte[] array, int offset, int length) {
<li>factoryChannelBufferFactory 类型),用于创建被修饰的 HeapChannelBuffer 对象的 ChannelBufferFactory 工厂,默认为 HeapChannelBufferFactory。</li>
</ul>
<p>DynamicChannelBuffer 需要关注的是 ensureWritableBytes() 方法,该方法实现了动态扩容的功能,在每次写入数据之前,都需要调用该方法确定当前可用空间是否足够,调用位置如下图所示:</p>
<p><img src="assets/CgqCHl9puiWABaDpAACaisslR0Q430.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl9puiWABaDpAACaisslR0Q430.png" alt="png" /></p>
<p>ensureWritableBytes() 方法如果检测到底层 ChannelBuffer 对象的空间不足,则会创建一个新的 ChannelBuffer空间扩大为原来的两倍然后将原来 ChannelBuffer 中的数据拷贝到新 ChannelBuffer 中,最后将 buffer 字段指向新 ChannelBuffer 对象完成整个扩容操作。ensureWritableBytes() 方法的具体实现如下:</p>
<pre><code>public void ensureWritableBytes(int minWritableBytes) {
if (minWritableBytes &lt;= writableBytes()) {
@@ -404,10 +404,10 @@ public void setBytes(int index, byte[] src, int srcIndex, int length) {
<p>NettyBackedChannelBuffer 对 ChannelBuffer 接口的实现都是调用底层封装的 Netty ByteBuf 实现的,这里就不再展开介绍,你若感兴趣的话也可以参考相关代码进行学习。</p>
<h3>相关 Stream 以及门面类</h3>
<p>在 ChannelBuffer 基础上Dubbo 提供了一套输入输出流,如下图所示:</p>
<p><img src="assets/Ciqc1F9puj2AXLalAALcfencKx0331.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Ciqc1F9puj2AXLalAALcfencKx0331.png" alt="png" /></p>
<p>ChannelBufferInputStream 底层封装了一个 ChannelBuffer其实现 InputStream 接口的 read*() 方法全部都是从 ChannelBuffer 中读取数据。ChannelBufferInputStream 中还维护了一个 startIndex 和一个endIndex 索引作为读取数据的起止位置。ChannelBufferOutputStream 与 ChannelBufferInputStream 类似,会向底层的 ChannelBuffer 写入数据,这里就不再展开,你若感兴趣的话可以参考源码进行分析。</p>
<p>最后要介绍 ChannelBuffers 这个<strong>门面类</strong>,下图展示了 ChannelBuffers 这个门面类的所有方法:</p>
<p><img src="assets/CgqCHl9pukOAT_8kAACo0xRQ2po574.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl9pukOAT_8kAACo0xRQ2po574.png" alt="png" /></p>
<p>对这些方法进行分类,可归纳出如下这些方法。</p>
<ul>
<li>dynamicBuffer() 方法:创建 DynamicChannelBuffer 对象,初始化大小由第一个参数指定,默认为 256。</li>

View File

@@ -293,7 +293,7 @@ function hide_canvas() {
<p>在第 17 课时中,我们详细介绍了 dubbo-remoting-api 模块中 Transporter 相关的核心抽象接口,本课时将继续介绍 dubbo-remoting-api 模块的其他内容。这里我们依旧从 Transporter 层的 RemotingServer、Client、Channel、ChannelHandler 等核心接口出发,介绍这些核心接口的实现。</p>
<h3>AbstractPeer 抽象类</h3>
<p>首先,我们来看 AbstractPeer 这个抽象类,它同时实现了 Endpoint 接口和 ChannelHandler 接口,如下图所示,它也是 AbstractChannel、AbstractEndpoint 抽象类的父类。</p>
<p><img src="assets/Ciqc1F9wb8eAHyD_AAFkwn8xp18694.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F9wb8eAHyD_AAFkwn8xp18694.png" alt="png" /></p>
<p>AbstractPeer 继承关系</p>
<blockquote>
<p>Netty 中也有 ChannelHandler、Channel 等接口,但无特殊说明的情况下,这里的接口指的都是 Dubbo 中定义的接口。如果涉及 Netty 中的接口,会进行特殊说明。</p>
@@ -338,7 +338,7 @@ function hide_canvas() {
</code></pre>
<h3>Server 继承路线分析</h3>
<p>AbstractServer 和 AbstractClient 都实现了 AbstractEndpoint 抽象类,我们先来看 AbstractServer 的实现。AbstractServer 在继承了 AbstractEndpoint 的同时,还实现了 RemotingServer 接口,如下图所示:</p>
<p><img src="assets/Ciqc1F9wb-iAMAgtAACJWi59iSc812.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1F9wb-iAMAgtAACJWi59iSc812.png" alt="png" /></p>
<p>AbstractServer 继承关系图</p>
<p><strong>AbstractServer 是对服务端的抽象,实现了服务端的公共逻辑</strong>。AbstractServer 的核心字段有下面几个。</p>
<ul>
@@ -390,7 +390,7 @@ function hide_canvas() {
}
</code></pre>
<p>在 createExecutor() 方法中,会通过 Dubbo SPI 查找 ThreadPool 接口的扩展实现,并调用其 getExecutor() 方法创建线程池。ThreadPool 接口被 @SPI 注解修饰,默认使用 FixedThreadPool 实现,但是 ThreadPool 接口中的 getExecutor() 方法被 @Adaptive 注解修饰,动态生成的适配器类会优先根据 URL 中的 threadpool 参数选择 ThreadPool 的扩展实现。ThreadPool 接口的实现类如下图所示:</p>
<p><img src="assets/CgqCHl9wcBeAYMZ1AABRTGzl5uY627.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl9wcBeAYMZ1AABRTGzl5uY627.png" alt="png" /></p>
<p>ThreadPool 继承关系图</p>
<p>不同实现会根据 URL 参数创建不同特性的线程池,这里以<strong>CacheThreadPool</strong>为例进行分析:</p>
<pre><code>public Executor getExecutor(URL url) {
@@ -514,12 +514,12 @@ protected void afterExecute(Runnable r, Throwable t) {
</code></pre>
<p>看完 NettyServer 实现的 doOpen() 方法之后,你会发现它和简易版 RPC 框架中启动一个 Netty 的 Server 端基本流程类似:初始化 ServerBootstrap、创建 Boss EventLoopGroup 和 Worker EventLoopGroup、创建 ChannelInitializer 指定如何初始化 Channel 上的 ChannelHandler 等一系列 Netty 使用的标准化流程。</p>
<p>其实在 Transporter 这一层看,功能的不同其实就是注册在 Channel 上的 ChannelHandler 不同,通过 doOpen() 方法得到的 Server 端结构如下:</p>
<p><img src="assets/Ciqc1F9y4LaAIHSsAADBytWDQ3U695.png" alt="5.png" /></p>
<p><img src="assets/Ciqc1F9y4LaAIHSsAADBytWDQ3U695.png" alt="png" /></p>
<p>NettyServer 模型</p>
<h4>核心 ChannelHandler</h4>
<p>下面我们来逐个看看这四个 ChannelHandler 的核心功能。</p>
<p>首先是<strong>decoder 和 encoder</strong>,它们都是 NettyCodecAdapter 的内部类,如下图所示,分别继承了 Netty 中的 ByteToMessageDecoder 和 MessageToByteEncoder</p>
<p><img src="assets/CgqCHl9wcESANfPCAABDUdzhtNU066.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl9wcESANfPCAABDUdzhtNU066.png" alt="png" /></p>
<p>还记得 AbstractEndpoint 抽象类中的 codec 字段Codec2 类型InternalDecoder 和 InternalEncoder 会将真正的编解码功能委托给 NettyServer 关联的这个 Codec2 对象去处理,这里以 InternalDecoder 为例进行分析:</p>
<pre><code>private class InternalDecoder extends ByteToMessageDecoder {
protected void decode(ChannelHandlerContext ctx, ByteBuf input, List&lt;Object&gt; out) throws Exception {
@@ -550,17 +550,17 @@ protected void afterExecute(Runnable r, Throwable t) {
<p>InternalEncoder 的具体实现就不再展开讲解了,你若感兴趣可以翻看源码进行研究和分析。</p>
<p>接下来是<strong>IdleStateHandler</strong>,它是 Netty 提供的一个工具型 ChannelHandler用于定时心跳请求的功能或是自动关闭长时间空闲连接的功能。它的原理到底是怎样的呢在 IdleStateHandler 中通过 lastReadTime、lastWriteTime 等几个字段,记录了最近一次读/写事件的时间IdleStateHandler 初始化的时候,会创建一个定时任务,定时检测当前时间与最后一次读/写时间的差值。如果超过我们设置的阈值(也就是上面 NettyServer 中设置的 idleTimeout就会触发 IdleStateEvent 事件,并传递给后续的 ChannelHandler 进行处理。后续 ChannelHandler 的 userEventTriggered() 方法会根据接收到的 IdleStateEvent 事件,决定是关闭长时间空闲的连接,还是发送心跳探活。</p>
<p>最后来看<strong>NettyServerHandler</strong>,它继承了 ChannelDuplexHandler这是 Netty 提供的一个同时处理 Inbound 数据和 Outbound 数据的 ChannelHandler从下面的继承图就能看出来。</p>
<p><img src="assets/Ciqc1F9wcFKAQQZ3AAB282frbWw282.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Ciqc1F9wcFKAQQZ3AAB282frbWw282.png" alt="png" /></p>
<p>NettyServerHandler 继承关系图</p>
<p>在 NettyServerHandler 中有 channels 和 handler 两个核心字段。</p>
<ul>
<li>channelsMap&lt;String,Channel&gt;集合):记录了当前 Server 创建的所有 Channel从下图中可以看到连接创建触发 channelActive() 方法)、连接断开(触发 channelInactive()方法)会操作 channels 集合进行相应的增删。</li>
</ul>
<p><img src="assets/Ciqc1F9wcFuABJWsAAaIoTwCIA0958.png" alt="Drawing 6.png" /></p>
<p><img src="assets/Ciqc1F9wcFuABJWsAAaIoTwCIA0958.png" alt="png" /></p>
<ul>
<li>handlerChannelHandler 类型NettyServerHandler 内几乎所有方法都会触发该 Dubbo ChannelHandler 对象(如下图)。</li>
</ul>
<p><img src="assets/CgqCHl9wcGOAE_ykAAFvy5a4X58367.png" alt="Drawing 7.png" /></p>
<p><img src="assets/CgqCHl9wcGOAE_ykAAFvy5a4X58367.png" alt="png" /></p>
<p>这里以 write() 方法为例进行简单分析:</p>
<pre><code>public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
super.write(ctx, msg, promise); // 将发送的数据继续向下传递
@@ -575,10 +575,10 @@ protected void afterExecute(Runnable r, Throwable t) {
<pre><code>final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);
</code></pre>
<p>其中第二个参数传入的是 NettyServer 这个对象,你可以追溯一下 NettyServer 的继承结构,会发现它的最顶层父类 AbstractPeer 实现了 ChannelHandler并且将所有的方法委托给其中封装的 ChannelHandler 对象,如下图所示:</p>
<p><img src="assets/Ciqc1F9wcGuADQi3AAD6EEURlNU871.png" alt="Drawing 8.png" /></p>
<p><img src="assets/Ciqc1F9wcGuADQi3AAD6EEURlNU871.png" alt="png" /></p>
<p>也就是说NettyServerHandler 会将数据委托给这个 ChannelHandler。</p>
<p>到此为止Server 这条继承线就介绍完了。你可以回顾一下,从 AbstractPeer 开始往下一路继承下来NettyServer 拥有了 Endpoint、ChannelHandler 以及RemotingServer多个接口的能力关联了一个 ChannelHandler 对象以及 Codec2 对象,并最终将数据委托给这两个对象进行处理。所以,上层调用方只需要实现 ChannelHandler 和 Codec2 这两个接口就可以了。</p>
<p><img src="assets/Ciqc1F9y4MyAR8XLAABTLdOZqrc228.png" alt="6.png" /></p>
<p><img src="assets/Ciqc1F9y4MyAR8XLAABTLdOZqrc228.png" alt="png" /></p>
<h3>总结</h3>
<p>本课时重点介绍了 Dubbo Transporter 层中 Server 相关的实现。</p>
<p>首先,我们介绍了 AbstractPeer 这个最顶层的抽象类,了解了 Server、Client 和 Channel 的公共属性。接下来,介绍了 AbstractEndpoint 抽象类,它提供了编解码等 Server 和 Client 所需的公共能力。最后,我们深入分析了 AbstractServer 抽象类以及基于 Netty 4 实现的 NettyServer同时还深入剖析了涉及的各种组件例如ExecutorRepository、NettyServerHandler 等。</p>

View File

@@ -339,7 +339,7 @@ function hide_canvas() {
}
</code></pre>
<p>得到的 NettyClient 结构如下图所示:</p>
<p><img src="assets/Ciqc1F90P1yAYThvAADLV6SJeac973.png" alt="Lark20200930-161759.png" /></p>
<p><img src="assets/Ciqc1F90P1yAYThvAADLV6SJeac973.png" alt="png" /></p>
<p>NettyClient 结构图</p>
<p>NettyClientHandler 的实现方法与上一课时介绍的 NettyServerHandler 类似,同样是实现了 Netty 中的 ChannelDuplexHandler其中会将所有方法委托给 NettyClient 关联的 ChannelHandler 对象进行处理。两者在 userEventTriggered() 方法的实现上有所不同NettyServerHandler 在收到 IdleStateEvent 事件时会断开连接,而 NettyClientHandler 则会发送心跳消息,具体实现如下:</p>
<pre><code>public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
@@ -364,7 +364,7 @@ function hide_canvas() {
<li>activeAtomicBoolean用于标识当前 Channel 是否可用。</li>
</ul>
<p>另外,在 NettyChannel 中还有一个静态的 Map 集合CHANNEL_MAP 字段),用来缓存当前 JVM 中 Netty 框架 Channel 与 Dubbo Channel 之间的映射关系。从下图的调用关系中可以看到NettyChannel 提供了读写 CHANNEL_MAP 集合的方法:</p>
<p><img src="assets/CgqCHl9wcRiAZFTaAADTxIPND7k175.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl9wcRiAZFTaAADTxIPND7k175.png" alt="png" /></p>
<p>NettyChannel 中还有一个要介绍的是 send() 方法,它会通过底层关联的 Netty 框架 Channel将数据发送到对端。其中可以通过第二个参数指定是否等待发送操作结束具体实现如下</p>
<pre><code>public void send(Object message, boolean sent) throws RemotingException {
// 调用AbstractChannel的send()方法检测连接是否可用
@@ -388,7 +388,7 @@ function hide_canvas() {
<h3>ChannelHandler 继承线分析</h3>
<p>前文介绍的 AbstractServer、AbstractClient 以及 Channel 实现,都是通过 AbstractPeer 实现了 ChannelHandler 接口,但只是做了一层简单的委托(也可以说成是装饰器),将全部方法委托给了其底层关联的 ChannelHandler 对象。</p>
<p>这里我们就深入分析 ChannelHandler 的其他实现类,涉及的实现类如下所示:</p>
<p><img src="assets/Ciqc1F9wcSGAXo7JAANZ2BjquOE739.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1F9wcSGAXo7JAANZ2BjquOE739.png" alt="png" /></p>
<p>ChannelHandler 继承关系图</p>
<p>其中<strong>ChannelHandlerDispatcher</strong>在[第 17 课时]已经介绍过了,它负责将多个 ChannelHandler 对象聚合成一个 ChannelHandler 对象。</p>
<p><strong>ChannelHandlerAdapter</strong>是 ChannelHandler 的一个空实现TelnetHandlerAdapter 继承了它并实现了 TelnetHandler 接口。至于Dubbo 对 Telnet 的支持,我们会在后面的课时中单独介绍,这里就先不展开分析了。</p>
@@ -420,7 +420,7 @@ function hide_canvas() {
<p>通过上述介绍,我们发现 AbstractChannelHandlerDelegate 下的三个实现,其实都是在原有 ChannelHandler 的基础上添加了一些增强功能,这是典型的装饰器模式的应用。</p>
<h4>Dispatcher 与 ChannelHandler</h4>
<p>接下来,我们介绍 ChannelHandlerDelegate 接口的另一条继承线——<strong>WrappedChannelHandler</strong>,其子类主要是决定了 Dubbo 以何种线程模型处理收到的事件和消息,就是所谓的“消息派发机制”,与前面介绍的 ThreadPool 有紧密的联系。</p>
<p><img src="assets/CgqCHl9wcTGAdInYAAJOSSxusf4539.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CgqCHl9wcTGAdInYAAJOSSxusf4539.png" alt="png" /></p>
<p>WrappedChannelHandler 继承关系图</p>
<p>从上图中我们可以看到,每个 WrappedChannelHandler 实现类的对象都由一个相应的 Dispatcher 实现类创建,下面是 Dispatcher 接口的定义:</p>
<pre><code>@SPI(AllDispatcher.NAME) // 默认扩展名是all
@@ -520,7 +520,7 @@ public interface Dispatcher {
<p>老版本中没有 ExecutorRepository 的概念,不会根据 URL 复用同一个线程池,而是通过 SPI 找到 ThreadPool 实现创建新线程池。</p>
</blockquote>
<p>此时Dubbo Consumer 同步请求的线程模型如下图所示:</p>
<p><img src="assets/CgqCHl9wcUWAY3b0AAFKI4e5Oa0017.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl9wcUWAY3b0AAFKI4e5Oa0017.png" alt="png" /></p>
<p>Dubbo Consumer 同步请求线程模型</p>
<p>从图中我们可以看到下面的请求-响应流程:</p>
<ol>
@@ -531,7 +531,7 @@ public interface Dispatcher {
</ol>
<p>在这个设计里面Consumer 端会维护一个线程池,而且线程池是按照连接隔离的,即每个连接独享一个线程池。这样,当面临需要消费大量服务且并发数比较大的场景时,例如,典型网关类场景,可能会导致 Consumer 端线程个数不断增加,导致线程调度消耗过多 CPU ,也可能因为线程创建过多而导致 OOM。</p>
<p>为了解决上述问题Dubbo 在 2.7.5 版本之后,<strong>引入了 ThreadlessExecutor</strong>,将线程模型修改成了下图的样子:</p>
<p><img src="assets/CgqCHl9wcVCAQdJjAAFE8eFivcY750.png" alt="Drawing 5.png" /></p>
<p><img src="assets/CgqCHl9wcVCAQdJjAAFE8eFivcY750.png" alt="png" /></p>
<p>引入 ThreadlessExecutor 后的结构图</p>
<ol>
<li>业务线程发出请求之后,拿到一个 Future 对象。</li>
@@ -586,10 +586,10 @@ public interface Dispatcher {
}
</code></pre>
<p>结合前面的分析,我们可以得到下面这张图:</p>
<p><img src="assets/Ciqc1F9wcV-AFAcTAADpElrp-Wc888.png" alt="Drawing 6.png" /></p>
<p><img src="assets/Ciqc1F9wcV-AFAcTAADpElrp-Wc888.png" alt="png" /></p>
<p>Server 端 ChannelHandler 结构图</p>
<p>我们可以在创建 NettyServerHandler 的地方添加断点 Debug 得到下图,也印证了上图的内容:</p>
<p><img src="assets/CgqCHl9wcWaAJVA3AACBSF4eCzg786.png" alt="Drawing 7.png" /></p>
<p><img src="assets/CgqCHl9wcWaAJVA3AACBSF4eCzg786.png" alt="png" /></p>
<h3>总结</h3>
<p>本课时我们重点介绍了 Dubbo Transporter 层中 Client、 Channel、ChannelHandler 相关的实现以及优化。</p>
<p>首先我们介绍了 AbstractClient 抽象接口以及基于 Netty 4 的 NettyClient 实现。接下来,介绍了 AbstractChannel 抽象类以及 NettyChannel 实现。最后,我们深入分析了 ChannelHandler 接口实现,其中详细分析 WrappedChannelHandler 等关键 ChannelHandler 实现,以及 ThreadlessExecutor 优化。</p>

View File

@@ -328,10 +328,10 @@ function hide_canvas() {
</code></pre>
<h3>ExchangeChannel &amp; DefaultFuture</h3>
<p>在前面的课时中,我们介绍了 Channel 接口的功能以及 Transport 层对 Channel 接口的实现。在 Exchange 层中定义了 ExchangeChannel 接口,它在 Channel 接口之上抽象了 Exchange 层的网络连接。ExchangeChannel 接口的定义如下:</p>
<p><img src="assets/Ciqc1F90Q-OAE4K1AADklLgEs0k481.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F90Q-OAE4K1AADklLgEs0k481.png" alt="png" /></p>
<p>ExchangeChannel 接口</p>
<p>其中request() 方法负责发送请求,从图中可以看到这里有两个重载,其中一个重载可以指定请求的超时时间,返回值都是 Future 对象。</p>
<p><img src="assets/CgqCHl90Q_SAIt4sAAAzhH5TZiw571.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl90Q_SAIt4sAAAzhH5TZiw571.png" alt="png" /></p>
<p>HeaderExchangeChannel 继承关系图</p>
<p><strong>从上图中可以看出HeaderExchangeChannel 是 ExchangeChannel 的实现</strong>,它本身是 Channel 的装饰器,封装了一个 Channel 对象,其 send() 方法和 request() 方法的实现都是依赖底层修饰的这个 Channel 对象实现的。</p>
<pre><code>public void send(Object message, boolean sent) throws RemotingException {
@@ -454,10 +454,10 @@ private void notifyTimeout(DefaultFuture future) {
<h3>HeaderExchangeHandler</h3>
<p>在前面介绍 DefaultFuture 时,我们简单说明了请求-响应的流程,其实无论是发送请求还是处理响应,都会涉及 HeaderExchangeHandler所以这里我们就来介绍一下 HeaderExchangeHandler 的内容。</p>
<p><strong>HeaderExchangeHandler 是 ExchangeHandler 的装饰器</strong>,其中维护了一个 ExchangeHandler 对象ExchangeHandler 接口是 Exchange 层与上层交互的接口之一,上层调用方可以实现该接口完成自身的功能;然后再由 HeaderExchangeHandler 修饰,具备 Exchange 层处理 Request-Response 的能力;最后再由 Transport ChannelHandler 修饰,具备 Transport 层的能力。如下图所示:</p>
<p><img src="assets/Ciqc1F-FWUqAVkr0AADiEwO4wK4124.png" alt="Lark20201013-153600.png" /></p>
<p><img src="assets/Ciqc1F-FWUqAVkr0AADiEwO4wK4124.png" alt="png" /></p>
<p>ChannelHandler 继承关系总览图</p>
<p>HeaderExchangeHandler 作为一个装饰器,其 connected()、disconnected()、sent()、received()、caught() 方法最终都会转发给上层提供的 ExchangeHandler 进行处理。这里我们需要聚焦的是 HeaderExchangeHandler 本身对 Request 和 Response 的处理逻辑。</p>
<p><img src="assets/Ciqc1F-FWVeAbsckAAGeD-_NNHc225.png" alt="Lark20201013-153557.png" /></p>
<p><img src="assets/Ciqc1F-FWVeAbsckAAGeD-_NNHc225.png" alt="png" /></p>
<p>received() 方法处理的消息分类</p>
<p>结合上图,我们可以看到在<strong>received() 方法</strong>中,对收到的消息进行了分类处理。</p>
<ul>

View File

@@ -299,7 +299,7 @@ function hide_canvas() {
</ul>
<p>因此HeaderExchangeClient 侧重定时轮资源的分配、定时任务的创建和取消。</p>
<p>HeaderExchangeClient 实现的是 ExchangeClient 接口,如下图所示,间接实现了 ExchangeChannel 和 Client 接口ExchangeClient 接口是个空接口,没有定义任何方法。</p>
<p><img src="assets/Ciqc1F-AF6OAT7YOAAAw9BR-aXg961.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F-AF6OAT7YOAAAw9BR-aXg961.png" alt="png" /></p>
<p>HeaderExchangeClient 继承关系图</p>
<p>HeaderExchangeClient 中有以下两个核心字段。</p>
<ul>
@@ -327,7 +327,7 @@ function hide_canvas() {
<p>其实startReconnectTask() 方法的具体实现与前面展示的 startHeartBeatTask() 方法类似,这里就不再赘述。</p>
<p>下面我们继续回到心跳定时任务进行分析,你可以回顾第 20 课时介绍的 NettyClient 实现,其 canHandleIdle() 方法返回 true表示该实现可以自己发送心跳请求无须 HeaderExchangeClient 再启动一个定时任务。NettyClient 主要依靠 IdleStateHandler 中的定时任务来触发心跳事件,依靠 NettyClientHandler 来发送心跳请求。</p>
<p>对于无法自己发送心跳请求的 Client 实现HeaderExchangeClient 会为其启动 HeartbeatTimerTask 心跳定时任务,其继承关系如下图所示:</p>
<p><img src="assets/CgqCHl-AF7eAHJWXAABGVZRbaEE743.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl-AF7eAHJWXAABGVZRbaEE743.png" alt="png" /></p>
<p>TimerTask 继承关系图</p>
<p>我们先来看 AbstractTimerTask 这个抽象类,它有三个字段。</p>
<ul>
@@ -377,7 +377,7 @@ function hide_canvas() {
<p>在 HeaderExchangeChannel.close(timeout) 方法中首先会将自身的 closed 字段设置为 true这样就不会继续发送请求。如果当前 Channel 上还有请求未收到响应,会循环等待至收到响应,如果超时未收到响应,会自己创建一个状态码将连接关闭的 Response 交给 DefaultFuture 处理,与收到 disconnected 事件相同。然后会关闭 Transport 层的 Channel以 NettyChannel 为例NettyChannel.close() 方法会先将自身的 closed 字段设置为 true清理 CHANNEL_MAP 缓存中的记录,以及 Channel 的附加属性,最后才是关闭 io.netty.channel.Channel。</p>
<h3>HeaderExchangeServer</h3>
<p>下面再来看 HeaderExchangeServer其继承关系如下图所示其中 Endpoint、RemotingServer、Resetable 这三个接口我们在前面已经详细介绍过了,这里不再重复。</p>
<p><img src="assets/CgqCHl-AF8eAUB8nAACKkTpW9nc845.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl-AF8eAUB8nAACKkTpW9nc845.png" alt="png" /></p>
<p>HeaderExchangeServer 的继承关系图</p>
<p>与前面介绍的 HeaderExchangeClient 一样HeaderExchangeServer 是 RemotingServer 的装饰器,实现自 RemotingServer 接口的大部分方法都委托给了所修饰的 RemotingServer 对象。</p>
<p>在 HeaderExchangeServer 的构造方法中,会启动一个 CloseTimerTask 定时任务,定期关闭长时间空闲的连接,具体的实现方式与 HeaderExchangeClient 中的两个定时任务类似,这里不再展开分析。</p>
@@ -423,7 +423,7 @@ public interface Exchanger {
}
</code></pre>
<p>Dubbo 只为 Exchanger 接口提供了 HeaderExchanger 这一个实现,其中 connect() 方法创建的是 HeaderExchangeClient 对象bind() 方法创建的是 HeaderExchangeServer 对象,如下图所示:</p>
<p><img src="assets/CgqCHl-AF9aANkhOAAB5TgtrSDg780.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CgqCHl-AF9aANkhOAAB5TgtrSDg780.png" alt="png" /></p>
<p>HeaderExchanger 门面类</p>
<p>从 HeaderExchanger 的实现可以看到,它会在 Transport 层的 Client 和 Server 实现基础之上,添加前文介绍的 HeaderExchangeClient 和 HeaderExchangeServer 装饰器。同时,为上层实现的 ExchangeHandler 实例添加了 HeaderExchangeHandler 以及 DecodeHandler 两个修饰器:</p>
<pre><code>public class HeaderExchanger implements Exchanger {
@@ -441,7 +441,7 @@ public interface Exchanger {
<h3>再谈 Codec2</h3>
<p>在前面第 17 课时介绍 Dubbo Remoting 核心接口的时候提到Codec2 接口提供了 encode() 和 decode() 两个方法来实现消息与字节流之间的相互转换。需要注意与 DecodeHandler 区分开来,<strong>DecodeHandler 是对请求体和响应结果的解码Codec2 是对整个请求和响应的编解码</strong></p>
<p>这里重点介绍 Transport 层和 Exchange 层对 Codec2 接口的实现,涉及的类如下图所示:</p>
<p><img src="assets/CgqCHl-AF9-AQVPBAAByBFYPxkE786.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl-AF9-AQVPBAAByBFYPxkE786.png" alt="png" /></p>
<p><strong>AbstractCodec</strong>抽象类并没有实现 Codec2 中定义的接口方法,而是提供了几个给子类用的基础方法,下面简单说明这些方法的功能。</p>
<ul>
<li>getSerialization() 方法:通过 SPI 获取当前使用的序列化方式。</li>
@@ -451,7 +451,7 @@ public interface Exchanger {
<p>接下来看<strong>TransportCodec</strong>,我们可以看到这类上被标记了 @Deprecated 注解表示已经废弃。TransportCodec 的实现非常简单,其中根据 getSerialization() 方法选择的序列化方法对传入消息或 ChannelBuffer 进行序列化或反序列化,这里就不再介绍 TransportCodec 实现了。</p>
<p><strong>TelnetCodec</strong>继承了 TransportCodec 序列化和反序列化的基本能力,同时还提供了对 Telnet 命令处理的能力。</p>
<p>最后来看<strong>ExchangeCodec</strong>,它在 TelnetCodec 的基础之上,添加了处理协议头的能力。下面是 Dubbo 协议的格式,能够清晰地看出协议中各个数据所占的位数:</p>
<p><img src="assets/CgqCHl-AF-eAdTmiAADznCJnMrw389.png" alt="Drawing 5.png" /></p>
<p><img src="assets/CgqCHl-AF-eAdTmiAADznCJnMrw389.png" alt="png" /></p>
<p>Dubbo 协议格式</p>
<p>结合上图,我们来深入了解一下 Dubbo 协议中各个部分的含义:</p>
<ul>

View File

@@ -291,15 +291,15 @@ function hide_canvas() {
<p id="tip" align="center"></p>
<div><h1>23 核心接口介绍RPC 层骨架梳理</h1>
<p>在前面的课程中,我们深入介绍了 Dubbo 架构中的 Dubbo Remoting 层的相关内容,了解了 Dubbo 底层的网络模型以及线程模型。从本课时开始,我们就开始介绍 Dubbo Remoting 上面的一层—— Protocol 层(如下图所示),<strong>Protocol 层是 Remoting 层的使用者</strong>,会通过 Exchangers 门面类创建 ExchangeClient 以及 ExchangeServer还会创建相应的 ChannelHandler 实现以及 Codec2 实现并交给 Exchange 层进行装饰。</p>
<p><img src="assets/Ciqc1F-FS2eAdVorABDINpNLpXY061.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F-FS2eAdVorABDINpNLpXY061.png" alt="png" /></p>
<p>Dubbo 架构中 Protocol 层的位置图</p>
<p><strong>Protocol 层在 Dubbo 源码中对应的是 dubbo-rpc 模块</strong>,该模块的结构如下图所示:</p>
<p><img src="assets/Ciqc1F-FS4aAMyvkAABpKhWTC9Q132.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1F-FS4aAMyvkAABpKhWTC9Q132.png" alt="png" /></p>
<p>dubbo-rpc 模块结构图</p>
<p>我们可以看到有很多模块,和 dubbo-remoting 模块类似,其中 <strong>dubbo-rpc-api 是对具体协议、服务暴露、服务引用、代理等的抽象,是整个 Protocol 层的核心</strong>。剩余的模块例如dubbo-rpc-dubbo、dubbo-rpc-grpc、dubbo-rpc-http 等,都是 Dubbo 支持的具体协议可以看作dubbo-rpc-api 模块的具体实现。</p>
<h3>dubbo-rpc-api</h3>
<p>这里我们首先来看 dubbo-rpc-api 模块的包结构,如下图所示:</p>
<p><img src="assets/CgqCHl-FS5CAP7kCAADYKrhf28A273.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl-FS5CAP7kCAADYKrhf28A273.png" alt="png" /></p>
<p>dubbo-rpc-api 模块的包结构图</p>
<p>根据上图展示的 dubbo-rpc-api 模块的结构,我们可以看到 dubbo-rpc-api 模块包括了以下几个核心包。</p>
<ul>
@@ -314,7 +314,7 @@ function hide_canvas() {
<p>在 Dubbo RPC 层中涉及的核心接口有 Invoker、Invocation、Protocol、Result、Exporter、ProtocolServer、Filter 等,这些接口分别抽象了 Dubbo RPC 层的不同概念,看似相互独立,但又相互协同,一起构建出了 DubboRPC 层的骨架。下面我们将逐一介绍这些核心接口的含义。</p>
<p>首先要介绍的是 Dubbo 中非常重要的一个接口——<strong>Invoker 接口</strong>。可以说Invoker 渗透在整个 Dubbo 代码实现里Dubbo 中的很多设计思路都会向 Invoker 这个概念靠拢,但这对于刚接触这部分代码的同学们来说,可能不是很友好。</p>
<p>这里我们借助如下这样一个精简的示意图来对比说明两种最关键的 Invoker服务提供 Invoker 和服务消费 Invoker。</p>
<p><img src="assets/Ciqc1F-FWQuAb1ypAAC0qPg0sWQ701.png" alt="Lark20201013-153553.png" /></p>
<p><img src="assets/Ciqc1F-FWQuAb1ypAAC0qPg0sWQ701.png" alt="png" /></p>
<p>Invoker 核心示意图</p>
<p>以 dubbo-demo-annotation-consumer 这个示例项目中的 Consumer 为例,它会拿到一个 DemoService 对象,如下所示,这其实是一个代理(即上图中的 Proxy这个 Proxy 底层就会通过 Invoker 完成网络调用:</p>
<pre><code>@Component(&quot;demoServiceComponent&quot;)

View File

@@ -291,13 +291,13 @@ function hide_canvas() {
<p id="tip" align="center"></p>
<div><h1>24 从 Protocol 起手,看服务暴露和服务引用的全流程(上)</h1>
<p>在上一课时我们讲解了 Protocol 的核心接口,那本课时我们就以 Protocol 接口为核心,详细介绍整个 Protocol 的核心实现。下图展示了 Protocol 接口的继承关系:</p>
<p><img src="assets/Ciqc1F-FTHGAOVGKAAJe5PD5u9A015.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F-FTHGAOVGKAAJe5PD5u9A015.png" alt="png" /></p>
<p>Protocol 接口继承关系图</p>
<p>其中,<strong>AbstractProtocol</strong>提供了一些 Protocol 实现需要的公共能力以及公共字段,它的核心字段有如下三个。</p>
<ul>
<li>exporterMapMap&lt;String, Exporter&lt;?&gt;&gt;类型):用于存储出去的服务集合,其中的 Key 通过 ProtocolUtils.serviceKey() 方法创建的服务标识,在 ProtocolUtils 中维护了多层的 Map 结构(如下图所示)。首先按照 group 分组,在实践中我们可以根据需求设置 group例如按照机房、地域等进行 group 划分,做到就近调用;在 GroupServiceKeyCache 中,依次按照 serviceName、serviceVersion、port 进行分类,最终缓存的 serviceKey 是前面三者拼接而成的。</li>
</ul>
<p><img src="assets/Ciqc1F-JXfmAJK8RAAHUliqXmBc629.png" alt="Lark20201016-164613.png" /></p>
<p><img src="assets/Ciqc1F-JXfmAJK8RAAHUliqXmBc629.png" alt="png" /></p>
<p>groupServiceKeyCacheMap 结构图</p>
<ul>
<li>serverMapMap&lt;String, ProtocolServer&gt;类型):记录了全部的 ProtocolServer 实例,其中的 Key 是 host 和 port 组成的字符串Value 是监听该地址的 ProtocolServer。ProtocolServer 就是对 RemotingServer 的一层简单封装,表示一个服务端。</li>
@@ -338,13 +338,13 @@ function hide_canvas() {
</code></pre>
<h4>1. DubboExporter</h4>
<p>这里涉及的第一个点是 DubboExporter 对 Invoker 的封装DubboExporter 的继承关系如下图所示:</p>
<p><img src="assets/CgqCHl-FTJSAd9oTAAAm0DgOmVo715.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl-FTJSAd9oTAAAm0DgOmVo715.png" alt="png" /></p>
<p>DubboExporter 继承关系图</p>
<p>AbstractExporter 中维护了一个 Invoker 对象,以及一个 unexported 字段boolean 类型),在 unexport() 方法中会设置 unexported 字段为 true并调用 Invoker 对象的 destory() 方法进行销毁。</p>
<p>DubboExporter 也比较简单,其中会维护底层 Invoker 对应的 ServiceKey 以及 DubboProtocol 中的 exportMap 集合,在其 unexport() 方法中除了会调用父类 AbstractExporter 的 unexport() 方法之外,还会清理该 DubboExporter 实例在 exportMap 中相应的元素。</p>
<h4>2. 服务端初始化</h4>
<p>了解了 Exporter 实现之后,我们继续看 DubboProtocol 中服务发布的流程。从下面这张调用关系图中可以看出openServer() 方法会一路调用前面介绍的 Exchange 层、Transport 层,并最终创建 NettyServer 来接收客户端的请求。</p>
<p><img src="assets/CgqCHl-FTKGAJNO8AAElldtvsRM104.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CgqCHl-FTKGAJNO8AAElldtvsRM104.png" alt="png" /></p>
<p>export() 方法调用栈</p>
<p>下面我们将逐个介绍 export() 方法栈中的每个被调用的方法。</p>
<p>首先,在 openServer() 方法中会根据 URL 判断当前是否为服务端,只有服务端才能创建 ProtocolServer 并对外服务。如果是来自服务端的调用,会依靠 serverMap 集合检查是否已有 ProtocolServer 在监听 URL 指定的地址;如果没有,会调用 createServer() 方法进行创建。openServer() 方法的具体实现如下:</p>
@@ -398,7 +398,7 @@ function hide_canvas() {
}
</code></pre>
<p>在 createServer() 方法中还有几个细节需要展开分析一下。第一个是创建 ExchangeServer 时,使用的 Codec2 接口实现实际上是 DubboCountCodec对应的 SPI 配置文件如下:</p>
<p><img src="assets/CgqCHl-FTK-AUlLCAADTWhhySe8432.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl-FTK-AUlLCAADTWhhySe8432.png" alt="png" /></p>
<p>Codec2 SPI 配置文件</p>
<p>DubboCountCodec 中维护了一个 DubboCodec 对象,编解码的能力都是 DubboCodec 提供的DubboCountCodec 只负责在解码过程中 ChannelBuffer 的 readerIndex 指针控制,具体实现如下:</p>
<pre><code>public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
@@ -429,7 +429,7 @@ function hide_canvas() {
}
</code></pre>
<p>DubboCountCodec、DubboCodec 都实现了第 22 课时介绍的 Codec2 接口,其中 DubboCodec 是 ExchangeCodec 的子类。</p>
<p><img src="assets/Ciqc1F-FTLuAZ-AoAACeZ02hpEg723.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Ciqc1F-FTLuAZ-AoAACeZ02hpEg723.png" alt="png" /></p>
<p>DubboCountCodec 及 DubboCodec 继承关系图</p>
<p>我们知道 ExchangeCodec 只处理了 Dubbo 协议的请求头,而 DubboCodec 则是通过继承的方式,在 ExchangeCodec 基础之上,添加了解析 Dubbo 消息体的功能。在第 22 课时介绍 ExchangeCodec 实现的时候,我们重点分析了 encodeRequest() 方法,即 Request 请求的编码实现,其中会调用 encodeRequestData() 方法完成请求体的编码。</p>
<p>DubboCodec 中就覆盖了 encodeRequestData() 方法,按照 Dubbo 协议的格式编码 Request 请求体,具体实现如下:</p>
@@ -461,7 +461,7 @@ function hide_canvas() {
}
</code></pre>
<p>RpcInvocation 实现了上一课时介绍的 Invocation 接口,如下图所示:</p>
<p><img src="assets/CgqCHl-FTMSAYeP7AAA_pzU2CPA016.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CgqCHl-FTMSAYeP7AAA_pzU2CPA016.png" alt="png" /></p>
<p>RpcInvocation 继承关系图</p>
<p>下面是 RpcInvocation 中的核心字段,通过读写这些字段即可实现 Invocation 接口的全部方法。</p>
<ul>
@@ -478,7 +478,7 @@ function hide_canvas() {
<li>invokeModeInvokeMode类型此次调用的模式分为 SYNC、ASYNC 和 FUTURE 三类。</li>
</ul>
<p>我们在上面的继承图中看到 RpcInvocation 的一个子类—— DecodeableRpcInvocation它是用来支持解码的其实现的 decode() 方法正好是 DubboCodec.encodeRequestData() 方法对应的解码操作,在 DubboCodec.decodeBody() 方法中就调用了这个方法,调用关系如下图所示:</p>
<p><img src="assets/Ciqc1F-FTM2Ae73pAAC0_daI0N4088.png" alt="Drawing 7.png" /></p>
<p><img src="assets/Ciqc1F-FTM2Ae73pAAC0_daI0N4088.png" alt="png" /></p>
<p>decode() 方法调用栈</p>
<p>这个解码过程中有个细节,在 DubboCodec.decodeBody() 方法中有如下代码片段,其中会根据 DECODE_IN_IO_THREAD_KEY 这个参数决定是否在 DubboCodec 中进行解码DubboCodec 是在 IO 线程中调用的)。</p>
<pre><code>// decode request.
@@ -557,7 +557,7 @@ return req;
}
</code></pre>
<p>SerializableClassRegistry 底层维护了一个 static 的 MapREGISTRATIONS 字段registerClass() 方法就是将待优化的类写入该集合中暂存,在使用 Kryo、FST 等序列化算法时,会读取该集合中的类,完成注册操作,相关的调用关系如下图所示:</p>
<p><img src="assets/Ciqc1F-FTOGAEWu7AADOU3xBmjA069.png" alt="Drawing 8.png" /></p>
<p><img src="assets/Ciqc1F-FTOGAEWu7AADOU3xBmjA069.png" alt="png" /></p>
<p>getRegisteredClasses() 方法的调用位置</p>
<p>按照 Dubbo 官方文档的说法即使不注册任何类进行优化Kryo 和 FST 的性能依然普遍优于Hessian2 和 Dubbo 序列化。</p>
<h3>总结</h3>

View File

@@ -332,10 +332,10 @@ function hide_canvas() {
}
</code></pre>
<p>当使用独享连接的时候,对每个 Service 建立固定数量的 Client每个 Client 维护一个底层连接。如下图所示,就是针对每个 Service 都启动了两个独享连接:</p>
<p><img src="assets/CgqCHl-OqnqAD_WFAAGYtk5Nou4688.png" alt="Lark20201020-171207.png" /></p>
<p><img src="assets/CgqCHl-OqnqAD_WFAAGYtk5Nou4688.png" alt="png" /></p>
<p>Service 独享连接示意图</p>
<p>当使用共享连接的时候会区分不同的网络地址host:port一个地址只建立固定数量的共享连接。如下图所示Provider 1 暴露了多个服务Consumer 引用了 Provider 1 中的多个服务,共享连接是说 Consumer 调用 Provider 1 中的多个服务时,是通过固定数量的共享 TCP 长连接进行数据传输,这样就可以达到减少服务端连接数的目的。</p>
<p><img src="assets/Ciqc1F-OqoOAHURKAAF2m0HX5qU972.png" alt="Lark20201020-171159.png" /></p>
<p><img src="assets/Ciqc1F-OqoOAHURKAAF2m0HX5qU972.png" alt="png" /></p>
<p>Service 共享连接示意图</p>
<p>那怎么去创建共享连接呢?<strong>创建共享连接的实现细节是在 getSharedClient() 方法中</strong>,它首先从 referenceClientMap 缓存Map&lt;String, List<code>&lt;ReferenceCountExchangeClient&gt;</code>&gt; 类型)中查询 Keyhost 和 port 拼接成的字符串)对应的共享 Client 集合,如果查找到的 Client 集合全部可用,则直接使用这些缓存的 Client否则要创建新的 Client 来补充替换缓存中不可用的 Client。示例代码如下</p>
<pre><code>private List&lt;ReferenceCountExchangeClient&gt; getSharedClient(URL url, int connectNum) {
@@ -379,7 +379,7 @@ function hide_canvas() {
</code></pre>
<p>这里使用的 ExchangeClient 实现是 ReferenceCountExchangeClient它是 ExchangeClient 的一个装饰器,在原始 ExchangeClient 对象基础上添加了引用计数的功能。</p>
<p>ReferenceCountExchangeClient 中除了持有被修饰的 ExchangeClient 对象外,还有一个 referenceCount 字段AtomicInteger 类型),用于记录该 Client 被应用的次数。从下图中我们可以看到,在 ReferenceCountExchangeClient 的构造方法以及 incrementAndGetCount() 方法中会增加引用次数,在 close() 方法中则会减少引用次数。</p>
<p><img src="assets/Ciqc1F-OqqeAHAStAAF3BXy1LnA608.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1F-OqqeAHAStAAF3BXy1LnA608.png" alt="png" /></p>
<p>referenceCount 修改调用栈</p>
<p>这样,对于同一个地址的共享连接,就可以满足两个基本需求:</p>
<ol>
@@ -417,7 +417,7 @@ private void replaceWithLazyClient() {
}
</code></pre>
<p>LazyConnectExchangeClient 也是 ExchangeClient 的装饰器,它会在原有 ExchangeClient 对象的基础上添加懒加载的功能。LazyConnectExchangeClient 在构造方法中不会创建底层持有连接的 Client而是在需要发送请求的时候才会调用 initClient() 方法进行 Client 的创建,如下图调用关系所示:</p>
<p><img src="assets/CgqCHl-OqrqAHcvUAAC9KpqKEBQ887.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CgqCHl-OqrqAHcvUAAC9KpqKEBQ887.png" alt="png" /></p>
<p>initClient() 方法的调用位置</p>
<p>initClient() 方法的具体实现如下:</p>
<pre><code>private void initClient() throws RemotingException {

View File

@@ -295,7 +295,7 @@ function hide_canvas() {
<p>DubboProtocol.protocolBindingRefer() 方法则会将底层的 ExchangeClient 集合封装成 DubboInvoker然后由上层逻辑封装成代理对象这样业务层就可以像调用本地 Bean 一样,完成远程调用。</p>
<h3>深入 Invoker</h3>
<p>首先,我们来看 AbstractInvoker 这个抽象类,它继承了 Invoker 接口,继承关系如下图所示:</p>
<p><img src="assets/Ciqc1F-Oq-uAdi4nAABRchTw_kQ666.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F-Oq-uAdi4nAABRchTw_kQ666.png" alt="png" /></p>
<p>AbstractInvoker 继承关系示意图</p>
<p>从图中可以看到,最核心的 DubboInvoker 继承自AbstractInvoker 抽象类AbstractInvoker 的核心字段有如下几个。</p>
<ul>
@@ -470,7 +470,7 @@ private static final InternalThreadLocal&lt;RpcContext&gt; SERVER_LOCAL = ...
}
</code></pre>
<p>oneway 指的是客户端发送消息后,不需要得到响应。所以,对于那些不关心服务端响应的请求,就比较适合使用 oneway 通信,如下图所示:</p>
<p><img src="assets/CgqCHl-SkLWAaPzTAACgt5rmWHg530.png" alt="Lark20201023-161312.png" /></p>
<p><img src="assets/CgqCHl-SkLWAaPzTAACgt5rmWHg530.png" alt="png" /></p>
<p>oneway 和 twoway 通信方式对比图</p>
<p>可以看到发送 oneway 请求的方式是send() 方法,而后面发送 twoway 请求的方式是 request() 方法。通过之前的分析我们知道request() 方法会相应地创建 DefaultFuture 对象以及检测超时的定时任务,而 send() 方法则不会创建这些东西,它是直接将 Invocation 包装成 oneway 类型的 Request 发送出去。</p>
<p>在服务端的 HeaderExchangeHandler.receive() 方法中,会针对 oneway 请求和 twoway 请求执行不同的分支处理twoway 请求由 handleRequest() 方法进行处理,其中会关注调用结果并形成 Response 返回给客户端oneway 请求则直接交给上层的 DubboProtocol.requestHandler完成方法调用之后不会返回任何 Response。</p>

View File

@@ -305,7 +305,7 @@ function hide_canvas() {
</code></pre>
<p>InvokeMode 有三个可选值,分别是 SYNC、ASYNC 和 FUTURE。这里对于 SYNC 模式返回的线程池是 ThreadlessExecutor至于其他两种异步模式会根据 URL 选择对应的共享线程池。</p>
<p><strong>SYNC 表示同步模式</strong>,是 Dubbo 的默认调用模式,具体含义如下图所示,客户端发送请求之后,客户端线程会阻塞等待服务端返回响应。</p>
<p><img src="assets/CgqCHl-X8UOAOTRbAACy-uBf52M689.png" alt="Lark20201027-180625.png" /></p>
<p><img src="assets/CgqCHl-X8UOAOTRbAACy-uBf52M689.png" alt="png" /></p>
<p>SYNC 调用模式图</p>
<p>在拿到线程池之后DubboInvoker 就会调用 ExchangeClient.request() 方法,将 Invocation 包装成 Request 请求发送出去,同时会创建相应的 DefaultFuture 返回。注意,这里还加了一个回调,取出其中的 AppResponse 对象。AppResponse 表示的是服务端返回的具体响应,其中有三个字段。</p>
<ul>
@@ -424,7 +424,7 @@ private BiConsumer&lt;Result, Throwable&gt; afterContext = (appResponse, t) -&gt
</code></pre>
<p>ThreadlessExecutor 针对同步请求的优化,我们在前面的第 20 课时已经详细介绍过了,这里不再重复。</p>
<p>最后要说明的是,<strong>AsyncRpcResult 实现了 Result 接口</strong>,如下图所示:</p>
<p><img src="assets/Ciqc1F-WqdmAbppOAABOGWzVljY775.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1F-WqdmAbppOAABOGWzVljY775.png" alt="png" /></p>
<p>AsyncRpcResult 继承关系图</p>
<p>AsyncRpcResult 对 Result 接口的实现例如getValue() 方法、recreate() 方法、getAttachments() 方法等,都会先调用 getAppResponse() 方法从 responseFuture 中拿到 AppResponse 对象,然后再调用其对应的方法。这里我们以 recreate() 方法为例,简单分析一下:</p>
<pre><code>public Result getAppResponse() { // 省略异常处理的逻辑
@@ -468,7 +468,7 @@ public Object recreate() throws Throwable {
<p>另外CompletableFuture 提供了良好的回调方法例如whenComplete()、whenCompleteAsync() 等方法都可以在逻辑完成后,执行该方法中添加的 action 逻辑实现回调的逻辑。同时CompletableFuture 很好地支持了 Future 间的相互协调或组合例如thenApply()、thenApplyAsync() 等方法。</p>
<p>正是由于 CompletableFuture 的增强,我们可以更加流畅地使用回调,不必因为等待一个响应而阻塞着调用线程,而是通过前面介绍的方法告诉 CompletableFuture 完成当前逻辑之后,就去执行某个特定的函数。在 Demo 示例(即 dubbo-demo 模块中的 Demo )中,返回 CompletableFuture 的 sayHelloAsync() 方法就是使用的 FUTURE 模式。</p>
<p>好了DubboInvoker 涉及的同步调用、异步调用的原理和底层实现就介绍到这里了,我们可以通过一张流程图进行简单总结,如下所示:</p>
<p><img src="assets/Ciqc1F-X8WuACaAKAAEb-X6qf4Y710.png" alt="Lark20201027-180621.png" /></p>
<p><img src="assets/Ciqc1F-X8WuACaAKAAEb-X6qf4Y710.png" alt="png" /></p>
<p>DubboInvoker 核心流程图</p>
<p>在 Client 端发送请求时,首先会创建对应的 DefaultFuture其中记录了请求 ID 等信息),然后依赖 Netty 的异步发送特性将请求发送到 Server 端。需要说明的是,这整个发送过程是不会阻塞任何线程的。之后,将 DefaultFuture 返回给上层在这个返回过程中DefaultFuture 会被封装成 AsyncRpcResult同时也可以添加回调函数。</p>
<p>当 Client 端接收到响应结果的时候会交给关联的线程池ExecutorService或是业务线程使用 ThreadlessExecutor 场景)进行处理,得到 Server 返回的真正结果。拿到真正的返回结果后,会将其设置到 DefaultFuture 中,并调用 complete() 方法将其设置为完成状态。此时,就会触发前面注册在 DefaulFuture 上的回调函数,执行回调逻辑。</p>
@@ -477,7 +477,7 @@ public Object recreate() throws Throwable {
<h4>1. ListenerInvokerWrapper</h4>
<p>在前面的第 23 课时中简单提到过 InvokerListener 接口,我们可以提供其实现来监听 refer 事件以及 destroy 事件,相应地要实现 referred() 方法以及 destroyed() 方法。</p>
<p>ProtocolListenerWrapper 是 Protocol 接口的实现之一,如下图所示:</p>
<p><img src="assets/CgqCHl-WqfyAZ0TzAAAbeTUMLT0465.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CgqCHl-WqfyAZ0TzAAAbeTUMLT0465.png" alt="png" /></p>
<p>ProtocolListenerWrapper 继承关系图</p>
<p>ProtocolListenerWrapper 本身是 Protocol 接口的装饰器,在其 export() 方法和 refer() 方法中,会分别在原有 Invoker 基础上封装一层 ListenerExporterWrapper 和 ListenerInvokerWrapper。</p>
<p><strong>ListenerInvokerWrapper 是 Invoker 的装饰器</strong>,其构造方法参数列表中除了被修饰的 Invoker 外,还有 InvokerListener 列表,在构造方法内部会遍历整个 InvokerListener 列表,并调用每个 InvokerListener 的 referred() 方法,通知它们 Invoker 被引用的事件。核心逻辑如下:</p>

View File

@@ -291,13 +291,13 @@ function hide_canvas() {
<p id="tip" align="center"></p>
<div><h1>28 复杂问题简单化,代理帮你隐藏了多少底层细节?</h1>
<p>在前面介绍 DubboProtocol 的相关实现时,我们知道 Protocol 这一层以及后面介绍的 Cluster 层暴露出来的接口都是 Dubbo 内部的一些概念,业务层无法直接使用。为了让业务逻辑能够无缝使用 Dubbo我们就需要将业务逻辑与 Dubbo 内部概念打通这就用到了动态生成代理对象的功能。Proxy 层在 Dubbo 架构中的位置如下所示(虽然在架构图中 Proxy 层与 Protocol 层距离很远,但 Proxy 的具体代码实现就位于 dubbo-rpc-api 模块中):</p>
<p><img src="assets/CgqCHl-WrK6Af2f9ACAFYNhhHPc051.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl-WrK6Af2f9ACAFYNhhHPc051.png" alt="png" /></p>
<p>Dubbo 架构中 Proxy 层的位置图</p>
<p>在 Consumer 进行调用的时候Dubbo 会通过<strong>动态代理</strong>将业务接口实现对象转化为相应的 Invoker 对象,然后在 Cluster 层、Protocol 层都会使用 Invoker。在 Provider 暴露服务的时候,也会有 Invoker 对象与业务接口实现对象之间的转换,这同样也是通过<strong>动态代理</strong>实现的。</p>
<p>实现动态代理的常见方案有JDK 动态代理、CGLib 动态代理和 Javassist 动态代理。这些方案的应用都还是比较广泛的例如Hibernate 底层使用了 Javassist 和 CGLibSpring 使用了 CGLib 和 JDK 动态代理MyBatis 底层使用了 JDK 动态代理和 Javassist。</p>
<p>从性能方面看Javassist 与 CGLib 的实现方式相差无几,两者都比 JDK 动态代理性能要高具体高多少这就要看具体的机器、JDK 版本、测试基准的具体实现等条件了。</p>
<p><strong>Dubbo 提供了两种方式来实现代理,分别是 JDK 动态代理和 Javassist</strong>。我们可以在 proxy 这个包内,看到相应工厂类,如下图所示:</p>
<p><img src="assets/Ciqc1F-WrLqALvCWAACKW4k6bv4319.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1F-WrLqALvCWAACKW4k6bv4319.png" alt="png" /></p>
<p>ProxyFactory 核心实现的位置</p>
<p>了解了 Proxy 存在的必要性以及 Dubbo 提供的两种代理生成方式之后,下面我们就开始对 Proxy 层的实现进行深入分析。</p>
<h3>ProxyFactory</h3>
@@ -316,7 +316,7 @@ public interface ProxyFactory {
</code></pre>
<p>看到 ProxyFactory 上的 @SPI 注解我们知道,其默认实现使用 Javassist 来创建代码对象。</p>
<p><strong>AbstractProxyFactory 是代理工厂的抽象类</strong>,继承关系如下图所示:</p>
<p><img src="assets/Ciqc1F-WrMiAXWheAACKwcyiNxw669.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1F-WrMiAXWheAACKwcyiNxw669.png" alt="png" /></p>
<p>AbstractProxyFactory 继承关系图</p>
<h3>AbstractProxyFactory</h3>
<p>AbstractProxyFactory 主要处理的是需要代理的接口,具体实现在 getProxy() 方法中:</p>
@@ -639,7 +639,7 @@ synchronized (cache) { // 加锁
}
</code></pre>
<p>在前面两个课时中我们已经介绍了 Invoker 接口的一个重要实现分支—— AbstractInvoker 以及它的一个实现 DubboInvoker。AbstractProxyInvoker 是 Invoker 接口的另一个实现分支,继承关系如下图所示,其实现类都是 ProxyFactory 实现中的匿名内部类。</p>
<p><img src="assets/CgqCHl-WrSOAU0FXAABwy4VTTF4402.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CgqCHl-WrSOAU0FXAABwy4VTTF4402.png" alt="png" /></p>
<p>在 AbstractProxyInvoker 实现的 invoke() 方法中,会将 doInvoke() 方法返回的结果封装成 CompletableFuture 对象,然后再封装成 AsyncRpcResult 对象返回,具体实现如下:</p>
<pre><code>public Result invoke(Invocation invocation) throws RpcException {
// 执行doInvoke()方法,调用业务实现

View File

@@ -294,7 +294,7 @@ function hide_canvas() {
<p>Filter 链的组装逻辑设计得非常灵活,其中可以通过“-”配置手动剔除 Dubbo 原生提供的、默认加载的 Filter通过“default”来代替 Dubbo 原生提供的 Filter这样就可以很好地控制哪些 Filter 要加载,以及 Filter 的真正执行顺序。</p>
<p><strong>Filter 是扩展 Dubbo 功能的首选方案</strong>,并且 Dubbo 自身也提供了非常多的 Filter 实现来扩展自身功能。在回顾了 ProtocolFilterWrapper 加载 Filter 的大致逻辑之后,我们本课时就来深入介绍 Dubbo 内置的多种 Filter 实现类,以及自定义 Filter 扩展 Dubbo 的方式。</p>
<p>在开始介绍 Filter 接口实现之前,我们需要了解一下 Filter 在 Dubbo 架构中的位置,这样才能明确 Filter 链处理请求/响应的位置,如下图红框所示:</p>
<p><img src="assets/CgqCHl-lLz2APEb2ABSTPPnfqGQ345.png" alt="Lark20201106-191028.png" /></p>
<p><img src="assets/CgqCHl-lLz2APEb2ABSTPPnfqGQ345.png" alt="png" /></p>
<p>Filter 在 Dubbo 架构中的位置</p>
<h3>ConsumerContextFilter</h3>
<p>ConsumerContextFilter 是一个非常简单的 Consumer 端 Filter 实现,它会在当前的 RpcContext 中记录本地调用的一些状态信息(会记录到 LOCAL 对应的 RpcContext 中),例如,调用相关的 Invoker、Invocation 以及调用的本地地址、远端地址信息,具体实现如下:</p>
@@ -573,7 +573,7 @@ public AccessLogFilter() {
}
</code></pre>
<p>在 LoggerFactory 中维护了一个 LOGGERS 集合Map&lt;String, FailsafeLogger&gt; 类型),其中维护了当前使用的全部 FailsafeLogger 对象FailsafeLogger 对象中封装了一个 Logger 对象,这个 Logger 接口是 Dubbo 自己定义的接口Dubbo 针对每种第三方框架都提供了一个 Logger 接口的实现,如下图所示:</p>
<p><img src="assets/Ciqc1F-lL4eAGvorAAEnucS-mWg399.png" alt="Lark20201106-191032.png" /></p>
<p><img src="assets/Ciqc1F-lL4eAGvorAAEnucS-mWg399.png" alt="png" /></p>
<p>Logger 接口的实现</p>
<p>FailsafeLogger 是 Logger 对象的装饰器,它在每个 Logger 日志写入操作之外,都添加了 try/catch 异常处理。其他的 Dubbo Logger 实现类则是封装了相应第三方的 Logger 对象,并将日志输出操作委托给第三方的 Logger 对象完成。这里我们以 Log4j2Logger 为例进行简单分析:</p>
<pre><code>public class Log4j2Logger implements Logger {
@@ -604,7 +604,7 @@ public AccessLogFilter() {
}
</code></pre>
<p>LoggerAdapter 被 @SPI 注解修饰是一个扩展接口如下图所示LoggerAdapter 对应每个第三方框架的一个相应实现,用于创建相应的 Dubbo Logger 实现对象。</p>
<p><img src="assets/CgqCHl-lL4GAWy4JAAFMZJwzrp8801.png" alt="Lark20201106-191036.png" /></p>
<p><img src="assets/CgqCHl-lL4GAWy4JAAFMZJwzrp8801.png" alt="png" /></p>
<p>LoggerAdapter 接口实现</p>
<p>以 Log4j2LoggerAdapter 为例,其核心在 getLogger() 方法中,主要是创建 Log4j2Logger 对象,具体实现如下:</p>
<pre><code>public class Log4j2LoggerAdapter implements LoggerAdapter {

View File

@@ -302,12 +302,12 @@ function hide_canvas() {
<li>……</li>
</ul>
<p>为了解决上述问题,<strong>Dubbo 独立出了一个实现集群功能的模块—— dubbo-cluster</strong></p>
<p><img src="assets/Ciqc1F-qN92ADHx8AACiY_cvusQ921.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F-qN92ADHx8AACiY_cvusQ921.png" alt="png" /></p>
<p>dubbo-cluster 结构图</p>
<p>作为 dubbo-cluster 模块分析的第一课时,我们就首先来了解一下 dubbo-cluster 模块的架构以及最核心的 Cluster 接口。</p>
<h3>Cluster 架构</h3>
<p>dubbo-cluster 模块的主要功能是将多个 Provider 伪装成一个 Provider 供 Consumer 调用,其中涉及集群的容错处理、路由规则的处理以及负载均衡。下图展示了 dubbo-cluster 的核心组件:</p>
<p><img src="assets/Ciqc1F-qY-CAQ08VAAFAZaC5kyU044.png" alt="Lark20201110-175555.png" /></p>
<p><img src="assets/Ciqc1F-qY-CAQ08VAAFAZaC5kyU044.png" alt="png" /></p>
<p>Cluster 核心接口图</p>
<p>由图我们可以看出dubbo-cluster 主要包括以下四个核心接口:</p>
<ul>
@@ -335,7 +335,7 @@ function hide_canvas() {
<p><strong>AbstractDirectory 是 Directory 接口的抽象实现</strong>,其中除了维护 Consumer 端的 URL 信息,还维护了一个 RouterChain 对象,用于记录当前使用的 Router 对象集合,也就是后面课时要介绍的路由规则。</p>
<p>AbstractDirectory 对 list() 方法的实现也比较简单,就是直接委托给了 doList() 方法doList() 是个抽象方法,由 AbstractDirectory 的子类具体实现。</p>
<p><strong>Directory 接口有 RegistryDirectory 和 StaticDirectory 两个具体实现</strong>,如下图所示:</p>
<p><img src="assets/Ciqc1F-qN_-AMVHmAAA3C6TAxsA315.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Ciqc1F-qN_-AMVHmAAA3C6TAxsA315.png" alt="png" /></p>
<p>Directory 接口继承关系图</p>
<p>其中,<strong>RegistryDirectory 实现</strong>中维护的 Invoker 集合会随着注册中心中维护的注册信息<strong>动态</strong>发生变化,这就依赖了 ZooKeeper 等注册中心的推送能力;<strong>StaticDirectory 实现</strong>中维护的 Invoker 集合则是<strong>静态</strong>的,在 StaticDirectory 对象创建完成之后,不会再发生变化。</p>
<p>下面我们就来分别介绍 Directory 接口的这两个具体实现。</p>
@@ -402,7 +402,7 @@ function hide_canvas() {
}
</code></pre>
<p>我们看到除了作为 NotifyListener 监听器之外RegistryDirectory 内部还有两个 ConfigurationListener 的内部类(继承关系如下图所示),为了保持连贯,这两个监听器的具体原理我们在后面的课时中会详细介绍,这里先不展开讲述。</p>
<p><img src="assets/CgqCHl-qOBmAbzKkAABZPyC5mIA963.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CgqCHl-qOBmAbzKkAABZPyC5mIA963.png" alt="png" /></p>
<p>RegistryDirectory 内部的 ConfigurationListener 实现</p>
<p>通过前面对 Registry 的介绍我们知道,在注册 NotifyListener 的时候,监听的是 providers、configurators 和 routers 三个目录,所以在这三个目录下发生变化的时候,就会触发 RegistryDirectory 的 notify() 方法。</p>
<p>在 RegistryDirectory.notify() 方法中,首先会按照 category 对发生变化的 URL 进行分类,分成 configurators、routers、providers 三类,并分别对不同类型的 URL 进行处理:</p>

View File

@@ -342,10 +342,10 @@ public interface RouterFactory {
}
</code></pre>
<p>RouterFactory 接口有很多实现类,如下图所示:</p>
<p><img src="assets/CgqCHl-qOLWAf_G5AACv9PqZOrc667.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl-qOLWAf_G5AACv9PqZOrc667.png" alt="png" /></p>
<p>RouterFactory 继承关系图</p>
<p>下面我们就来深入介绍下每个 RouterFactory 实现类以及对应的 Router 实现对象。<strong>Router 决定了一次 Dubbo 调用的目标服务Router 接口的每个实现类代表了一个路由规则</strong>,当 Consumer 访问 Provider 时Dubbo 根据路由规则筛选出合适的 Provider 列表之后通过负载均衡算法再次进行筛选。Router 接口的继承关系如下图所示:</p>
<p><img src="assets/Ciqc1F-qOL2AAXYIAACMVPC1qW0732.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1F-qOL2AAXYIAACMVPC1qW0732.png" alt="png" /></p>
<p>Router 继承关系图</p>
<p>接下来我们就开始介绍 RouterFactory 以及 Router 的具体实现。</p>
<h3>ConditionRouterFactory&amp;ConditionRouter</h3>
@@ -453,7 +453,7 @@ public interface RouterFactory {
<pre><code>host = 2.2.2.2,1.1.1.1,3.3.3.3 &amp; method !=get =&gt; host = 1.2.3.4
</code></pre>
<p>经过 ROUTE_PATTERN 正则表达式的分组之后,我们得到如下分组:</p>
<p><img src="assets/CgqCHl-uM9aALLGaAAFMMnXRAPw685.png" alt="2.png" /></p>
<p><img src="assets/CgqCHl-uM9aALLGaAAFMMnXRAPw685.png" alt="png" /></p>
<p>Rule 分组示意图</p>
<p>我们先来看 <code>=&gt;</code> 之前的 Consumer 匹配规则的处理。</p>
<ol>
@@ -464,10 +464,10 @@ public interface RouterFactory {
<li>分组 5 中separator 为 &quot;!=&quot; 字符串content 为 &quot;get&quot; 字符串。处理该分组时,会进入 parseRule() 方法中5处的分支向步骤 4 新建的 MatchPair 对象中的 mismatches 集合添加 &quot;get&quot; 字符串。</li>
</ol>
<p>最后,我们得到的 whenCondition 集合如下图所示:</p>
<p><img src="assets/Ciqc1F-uM-OABiPoAADt1lcbl7U975.png" alt="3.png" /></p>
<p><img src="assets/Ciqc1F-uM-OABiPoAADt1lcbl7U975.png" alt="png" /></p>
<p>whenCondition 集合示意图</p>
<p>同理parseRule() 方法解析上述表达式 <code>=&gt;</code> 之后的规则得到的 thenCondition 集合,如下图所示:</p>
<p><img src="assets/CgqCHl-uM-6AXnrOAAB6hJLFL50095.png" alt="1.png" /></p>
<p><img src="assets/CgqCHl-uM-6AXnrOAAB6hJLFL50095.png" alt="png" /></p>
<p>thenCondition 集合示意图</p>
<p>了解了 ConditionRouter 解析规则的流程以及 MatchPair 内部的匹配原则之后ConditionRouter 中最后一个需要介绍的内容就是它的 route() 方法了。</p>
<p>ConditionRouter.route() 方法首先会尝试前面创建的 whenCondition 集合,判断此次发起调用的 Consumer 是否符合表达式中 <code>=&gt;</code> 之前的 Consumer 过滤条件,若不符合,直接返回整个 invokers 集合;若符合,则通过 thenCondition 集合对 invokers 集合进行过滤,得到符合 Provider 过滤条件的 Invoker 集合然后返回给上层调用方。ConditionRouter.route() 方法的核心实现如下:</p>

View File

@@ -294,7 +294,7 @@ function hide_canvas() {
<h3>FileRouterFactory</h3>
<p><strong>FileRouterFactory 是 ScriptRouterFactory 的装饰器</strong>,其扩展名为 fileFileRouterFactory 在 ScriptRouterFactory 基础上<strong>增加了读取文件的能力</strong>。我们可以将 ScriptRouter 使用的路由规则保存到文件中,然后在 URL 中指定文件路径FileRouterFactory 从中解析到该脚本文件的路径并进行读取,调用 ScriptRouterFactory 去创建相应的 ScriptRouter 对象。</p>
<p>下面我们来看 FileRouterFactory 对 getRouter() 方法的具体实现,其中完成了 file 协议的 URL 到 script 协议 URL 的转换,如下是一个转换示例,首先会将 file:// 协议转换成 script:// 协议,然后会添加 type 参数和 rule 参数,其中 type 参数值根据文件后缀名确定,该示例为 jsrule 参数值为文件内容。</p>
<p><img src="assets/Ciqc1F-zkA2AduheAAGQTzCOwl8784.png" alt="2.png" /></p>
<p><img src="assets/Ciqc1F-zkA2AduheAAGQTzCOwl8784.png" alt="png" /></p>
<p>我们可以再结合接下来这个示例分析 getRouter() 方法的具体实现:</p>
<pre><code>public Router getRouter(URL url) {
// 默认使用script协议
@@ -329,21 +329,21 @@ function hide_canvas() {
<p>目前Dubbo 提供了动态和静态两种方式给 Provider 打标签,其中动态方式就是通过服务治理平台动态下发标签,静态方式就是在 XML 等静态配置中打标签。Consumer 端可以在 RpcContext 的 attachment 中添加 request.tag 附加属性,注意<strong>保存在 attachment 中的值将会在一次完整的远程调用中持续传递</strong>,我们只需要在起始调用时进行设置,就可以达到标签的持续传递。</p>
<p>了解了 Tag 的基本概念和功能之后,我们再简单介绍一个 Tag 的使用示例。</p>
<p>在实际的开发测试中,一个完整的请求会涉及非常多的 Provider分属不同团队进行维护这些团队每天都会处理不同的需求并在其负责的 Provider 服务中进行修改如果所有团队都使用一套测试环境那么测试环境就会变得很不稳定。如下图所示4 个 Provider 分属不同的团队管理Provider 2 和 Provider 4 在测试环境测试,部署了有 Bug 的版本,这样就会导致整个测试环境无法正常处理请求,在这样一个不稳定的测试环境中排查 Bug 是非常困难的,因为可能排查到最后,发现是别人的 Bug。</p>
<p><img src="assets/CgqCHl-zkBuACzVCAABuM5-1_s4317.png" alt="3.png" /></p>
<p><img src="assets/CgqCHl-zkBuACzVCAABuM5-1_s4317.png" alt="png" /></p>
<p>不同状态的 Provider 节点</p>
<p>为了解决上述问题,我们可以针对每个需求分别独立出一套测试环境,但是这个方案会占用大量机器,前期的搭建成本以及后续的维护成本也都非常高。</p>
<p>下面是一个通过 Tag 方式实现环境隔离的架构图,其中,需求 1 对 Provider 2 的请求会全部落到有需求 1 标签的 Provider 上,其他 Provider 使用稳定测试环境中的 Provider需求 2 对 Provider 4 的请求会全部落到有需求 2 标签的 Provider 4 上,其他 Provider 使用稳定测试环境中的 Provider。</p>
<p><img src="assets/CgqCHl-zkCyANtuuAADgH2I1upA475.png" alt="4.png" /></p>
<p><img src="assets/CgqCHl-zkCyANtuuAADgH2I1upA475.png" alt="png" /></p>
<p>依赖 Tag 实现的测试环境隔离方案</p>
<p>在一些特殊场景中,会有 Tag 降级的场景,比如找不到对应 Tag 的 Provider会按照一定的规则进行降级。如果在 Provider 集群中不存在与请求 Tag 对应的 Provider 节点,则默认将降级请求 Tag 为空的 Provider如果希望在找不到匹配 Tag 的 Provider 节点时抛出异常的话,我们需设置 request.tag.force = true。</p>
<p>如果请求中的 request.tag 未设置,只会匹配 Tag 为空的 Provider也就是说即使集群中存在可用的服务若 Tag 不匹配也就无法调用。一句话总结,<strong>携带 Tag 的请求可以降级访问到无 Tag 的 Provider但不携带 Tag 的请求永远无法访问到带有 Tag 的 Provider</strong></p>
<h4>TagRouter</h4>
<p>下面我们再来看 TagRouter 的具体实现。在 TagRouter 中持有一个 TagRouterRule 对象的引用,在 TagRouterRule 中维护了一个 Tag 集合,而在每个 Tag 对象中又都维护了一个 Tag 的名称,以及 Tag 绑定的网络地址集合,如下图所示:</p>
<p><img src="assets/CgqCHl-zkEGALTHPAADFZZM7Y2A139.png" alt="5.png" /></p>
<p><img src="assets/CgqCHl-zkEGALTHPAADFZZM7Y2A139.png" alt="png" /></p>
<p>TagRouter、TagRouterRule、Tag 与 address 映射关系图</p>
<p>另外,在 TagRouterRule 中还维护了 addressToTagnames、tagnameToAddresses 两个集合(都是 Map&lt;String, List<code>&lt;String&gt;</code>&gt; 类型),分别记录了 Tag 名称到各个 address 的映射以及 address 到 Tag 名称的映射。在 TagRouterRule 的 init() 方法中,会根据 tags 集合初始化这两个集合。</p>
<p>了解了 TagRouterRule 的基本构造之后,我们继续来看 TagRouter 构造 TagRouterRule 的过程。TagRouter 除了实现了 Router 接口之外,还实现了 ConfigurationListener 接口,如下图所示:</p>
<p><img src="assets/Ciqc1F-zkEyAMNXQAAF_oit25-o273.png" alt="6.png" /></p>
<p><img src="assets/Ciqc1F-zkEyAMNXQAAF_oit25-o273.png" alt="png" /></p>
<p>TagRouter 继承关系图</p>
<p><strong>ConfigurationListener 用于监听配置的变化,其中就包括 TagRouterRule 配置的变更</strong>。当我们通过动态更新 TagRouterRule 配置的时候,就会触发 ConfigurationListener 接口的 process() 方法TagRouter 对 process() 方法的实现如下:</p>
<pre><code>public synchronized void process(ConfigChangedEvent event) {
@@ -370,10 +370,10 @@ tags:
addresses: []
</code></pre>
<p>经过 TagRuleParser 解析得到的 TagRouterRule 结构,如下所示:</p>
<p><img src="assets/Ciqc1F-zkF6AHgUEAAE3K8dR6QQ826.png" alt="1.png" /></p>
<p><img src="assets/Ciqc1F-zkF6AHgUEAAE3K8dR6QQ826.png" alt="png" /></p>
<p>TagRouterRule 结构图</p>
<p>除了上图展示的几个集合字段TagRouterRule 还从 AbstractRouterRule 抽象类继承了一些控制字段,后面介绍的 ConditionRouterRule 也继承了 AbstractRouterRule。</p>
<p><img src="assets/CgqCHl-zkGmAYDBMAAFODGWwRfo125.png" alt="9.png" /></p>
<p><img src="assets/CgqCHl-zkGmAYDBMAAFODGWwRfo125.png" alt="png" /></p>
<p>AbstractRouterRule继承关系图</p>
<p>AbstractRouterRule 中核心字段的具体含义大致可总结为如下。</p>
<ul>
@@ -455,10 +455,10 @@ tags:
</code></pre>
<h3>ServiceRouter &amp; AppRouter</h3>
<p>除了前文介绍的 TagRouterFactory 继承了 CacheableRouterFactory 之外,<strong>ServiceRouterFactory 也继承 CachabelRouterFactory具有了缓存的能力</strong>,具体继承关系如下图所示:</p>
<p><img src="assets/Ciqc1F-zkHqAH3diAAGWl6aQJy8860.png" alt="8.png" /></p>
<p><img src="assets/Ciqc1F-zkHqAH3diAAGWl6aQJy8860.png" alt="png" /></p>
<p>CacheableRouterFactory 继承关系图</p>
<p>ServiceRouterFactory 创建的 Router 实现是 ServiceRouter与 ServiceRouter 类似的是 AppRouter<strong>两者都继承了 ListenableRouter 抽象类</strong>(虽然 ListenableRouter 是个抽象类,但是没有抽象方法留给子类实现),继承关系如下图所示:</p>
<p><img src="assets/Ciqc1F-zkISAPopjAAH9Njd3pOE049.png" alt="7.png" /></p>
<p><img src="assets/Ciqc1F-zkISAPopjAAH9Njd3pOE049.png" alt="png" /></p>
<p>ListenableRouter 继承关系图</p>
<p><strong>ListenableRouter 在 ConditionRouter 基础上添加了动态配置的能力</strong>ListenableRouter 的 process() 方法与 TagRouter 中的 process() 方法类似,对于 ConfigChangedEvent.DELETE 事件,直接清空 ListenableRouter 中维护的 ConditionRouterRule 和 ConditionRouter 集合的引用;对于 ADDED、UPDATED 事件,则通过 ConditionRuleParser 解析事件内容,得到相应的 ConditionRouterRule 对象和 ConditionRouter 集合。这里的 ConditionRuleParser 同样是以 yaml 文件的格式解析 ConditionRouterRule 的相关配置。ConditionRouterRule 中维护了一个 conditions 集合List<code>&lt;String&gt;</code> 类型),记录了多个 Condition 路由规则,对应生成多个 ConditionRouter 对象。</p>
<p>整个解析 ConditionRouterRule 的过程,与前文介绍的解析 TagRouterRule 的流程类似,这里不再赘述。</p>

View File

@@ -377,11 +377,11 @@ function hide_canvas() {
}
</code></pre>
<p>ConfiguratorFactory 接口是一个扩展接口Dubbo 提供了两个实现类,如下图所示:</p>
<p><img src="assets/CgqCHl-3eLqAL0VKAAGW_mZ-1yE800.png" alt="Lark20201120-160501.png" /></p>
<p><img src="assets/CgqCHl-3eLqAL0VKAAGW_mZ-1yE800.png" alt="png" /></p>
<p>ConfiguratorFactory 继承关系图</p>
<p>其中OverrideConfiguratorFactory 对应的扩展名为 override创建的 Configurator 实现是 OverrideConfiguratorAbsentConfiguratorFactory 对应的扩展名是 absent创建的 Configurator 实现类是 AbsentConfigurator。</p>
<p>Configurator 接口的继承关系如下图所示:</p>
<p><img src="assets/Ciqc1F-3eMCAP6pVAAGo31X0Wd8652.png" alt="Lark20201120-160505.png" /></p>
<p><img src="assets/Ciqc1F-3eMCAP6pVAAGo31X0Wd8652.png" alt="png" /></p>
<p>Configurator 继承关系图</p>
<p>其中AbstractConfigurator 中维护了一个 configuratorUrl 字段,记录了完整的配置 URL。<strong>AbstractConfigurator 是一个模板类,其核心实现是 configure() 方法</strong>,具体实现如下:</p>
<pre><code>public URL configure(URL url) {

View File

@@ -291,7 +291,7 @@ function hide_canvas() {
<p id="tip" align="center"></p>
<div><h1>35 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(上)</h1>
<p>在前面的课时中,我们已经详细介绍了 dubbo-cluster 模块中的 Directory 和 Router 两个核心接口以及核心实现,同时也介绍了这两个接口相关的周边知识。本课时我们继续按照下图的顺序介绍 LoadBalance 的相关内容。</p>
<p><img src="assets/Ciqc1F-81uuAdW51AAH-O1mrOoA018.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F-81uuAdW51AAH-O1mrOoA018.png" alt="png" /></p>
<p>LoadBalance 核心接口图</p>
<p><strong>LoadBalance负载均衡的职责是将网络请求或者其他形式的负载“均摊”到不同的服务节点上从而避免服务集群中部分节点压力过大、资源紧张而另一部分节点比较空闲的情况。</strong></p>
<p>通过合理的负载均衡算法,我们希望可以让每个服务节点获取到适合自己处理能力的负载,<strong>实现处理能力和流量的合理分配</strong>。常用的负载均衡可分为<strong>软件负载均衡</strong>(比如,日常工作中使用的 Nginx<strong>硬件负载均衡</strong>(主要有 F5、Array、NetScaler 等,不过开发工程师在实践中很少直接接触到)。</p>
@@ -306,7 +306,7 @@ function hide_canvas() {
</ul>
<h3>LoadBalance 接口</h3>
<p>上述 Dubbo 提供的负载均衡实现,都是 LoadBalance 接口的实现类,如下图所示:</p>
<p><img src="assets/CgqCHl-81vaAYmqRAAFYpTlQI0s741.png" alt="Lark20201124-174750.png" /></p>
<p><img src="assets/CgqCHl-81vaAYmqRAAFYpTlQI0s741.png" alt="png" /></p>
<p>LoadBalance 继承关系图</p>
<p><strong>LoadBalance 是一个扩展接口,默认使用的扩展实现是 RandomLoadBalance</strong>,其定义如下所示,其中的 @Adaptive 注解参数为 loadbalance即动态生成的适配器会按照 URL 中的 loadbalance 参数值选择扩展实现类。</p>
<pre><code>@SPI(RandomLoadBalance.NAME)
@@ -378,16 +378,16 @@ public interface LoadBalance {
hash(请求参数) % 2^32
</code></pre>
<p>Provider 地址和请求经过对 2^32 取模得到的结果值,都会落到一个 Hash 环上,如下图所示:</p>
<p><img src="assets/CgqCHl-81wSAO1YfAAFfH6Qgse0640.png" alt="Lark20201124-174752.png" /></p>
<p><img src="assets/CgqCHl-81wSAO1YfAAFfH6Qgse0640.png" alt="png" /></p>
<p>一致性 Hash 节点均匀分布图</p>
<p>我们按顺时针的方向,依次将请求分发到对应的 Provider。这样当某台 Provider 节点宕机或增加新的 Provider 节点时,只会影响这个 Provider 节点对应的请求。</p>
<p>在理想情况下,一致性 Hash 算法会将这三个 Provider 节点均匀地分布到 Hash 环上,请求也可以均匀地分发给这三个 Provider 节点。但在实际情况中,这三个 Provider 节点地址取模之后的值,可能差距不大,这样会导致大量的请求落到一个 Provider 节点上,如下图所示:</p>
<p><img src="assets/CgqCHl-81w2ATT5qAAFjvpkgTNM463.png" alt="Lark20201124-174755.png" /></p>
<p><img src="assets/CgqCHl-81w2ATT5qAAFjvpkgTNM463.png" alt="png" /></p>
<p>一致性 Hash 节点非均匀分布图</p>
<p>这就出现了数据倾斜的问题。<strong>所谓数据倾斜是指由于节点不够分散,导致大量请求落到了同一个节点上,而其他节点只会接收到少量请求的情况</strong></p>
<p>为了解决一致性 Hash 算法中出现的数据倾斜问题,又演化出了 Hash 槽的概念。</p>
<p>Hash 槽解决数据倾斜的思路是:既然问题是由 Provider 节点在 Hash 环上分布不均匀造成的,那么可以虚拟出 n 组 P1、P2、P3 的 Provider 节点 ,让多组 Provider 节点相对均匀地分布在 Hash 环上。如下图所示,相同阴影的节点均为同一个 Provider 节点,比如 P1-1、P1-2……P1-99 表示的都是 P1 这个 Provider 节点。引入 Provider 虚拟节点之后,让 Provider 在圆环上分散开来,以避免数据倾斜问题。</p>
<p><img src="assets/CgqCHl-81xaAEUSbAAG0t7C-hcQ544.png" alt="Lark20201124-174743.png" /></p>
<p><img src="assets/CgqCHl-81xaAEUSbAAG0t7C-hcQ544.png" alt="png" /></p>
<p>数据倾斜解决示意图</p>
<h4>2. ConsistentHashSelector 实现分析</h4>
<p>了解了一致性 Hash 算法的基本原理之后,我们再来看一下 ConsistentHashLoadBalance 一致性 Hash 负载均衡的具体实现。首先来看 doSelect() 方法的实现,其中会根据 ServiceKey 和 methodName 选择一个 ConsistentHashSelector 对象,<strong>核心算法都委托给 ConsistentHashSelector 对象完成。</strong></p>
@@ -476,7 +476,7 @@ private Invoker&lt;T&gt; selectForKey(long hash) {
<h3>RandomLoadBalance</h3>
<p>RandomLoadBalance 使用的负载均衡算法是<strong>加权随机算法</strong>。RandomLoadBalance 是一个简单、高效的负载均衡实现,它也是 Dubbo 默认使用的 LoadBalance 实现。</p>
<p>这里我们通过一个示例来说明加权随机算法的核心思想。假设我们有三个 Provider 节点 A、B、C它们对应的权重分别为 5、2、3权重总和为 10。现在把这些权重值放到一维坐标轴上[0, 5) 区间属于节点 A[5, 7) 区间属于节点 B[7, 10) 区间属于节点 C如下图所示</p>
<p><img src="assets/Ciqc1F-81ySAdj_7AAAxc2j-s5k730.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Ciqc1F-81ySAdj_7AAAxc2j-s5k730.png" alt="png" /></p>
<p>权重坐标轴示意图</p>
<p>下面我们通过随机数生成器在 [0, 10) 这个范围内生成一个随机数,然后计算这个随机数会落到哪个区间中。例如,随机生成 4就会落到 Provider A 对应的区间中,此时 RandomLoadBalance 就会返回 Provider A 这个节点。</p>
<p>接下来我们再来看 RandomLoadBalance 中 doSelect() 方法的实现,其核心逻辑分为三个关键点:</p>

View File

@@ -365,7 +365,7 @@ function hide_canvas() {
<p>每个 Provider 节点有两个权重:一个权重是配置的 weight该值在负载均衡的过程中不会变化另一个权重是 currentWeight该值会在负载均衡的过程中动态调整初始值为 0。</p>
<p>当有新的请求进来时RoundRobinLoadBalance 会遍历 Invoker 列表,并用对应的 currentWeight 加上其配置的权重。遍历完成后,再找到最大的 currentWeight将其减去权重总和然后返回相应的 Invoker 对象。</p>
<p>下面我们通过一个示例说明 RoundRobinLoadBalance 的执行流程,这里我们依旧假设 A、B、C 三个节点的权重比例为 5:1:1。</p>
<p><img src="assets/CgqCHl_ArGSAfxA6AAHyWL4Af1o908.png" alt="Lark20201127-153527.png" /></p>
<p><img src="assets/CgqCHl_ArGSAfxA6AAHyWL4Af1o908.png" alt="png" /></p>
<ol>
<li>处理第一个请求currentWeight 数组中的权重与配置的 weight 相加,即从 [0, 0, 0] 变为 [5, 1, 1]。接下来,从中选择权重最大的 Invoker 作为结果,即节点 A。最后将节点 A 的 currentWeight 值减去 totalWeight 值,最终得到 currentWeight 数组为 [-2, 1, 1]。</li>
<li>处理第二个请求currentWeight 数组中的权重与配置的 weight 相加,即从 [-2, 1, 1] 变为 [3, 2, 2]。接下来,从中选择权重最大的 Invoker 作为结果,即节点 A。最后将节点 A 的 currentWeight 值减去 totalWeight 值,最终得到 currentWeight 数组为 [-4, 2, 2]。</li>

View File

@@ -299,7 +299,7 @@ function hide_canvas() {
<p>了解了上述背景知识之后,下面我们就正式开始介绍 Cluster 接口。</p>
<h3>Cluster 接口与容错机制</h3>
<p>Cluster 的工作流程大致可以分为两步(如下图所示):①<strong>创建 Cluster Invoker 实例</strong>(在 Consumer 初始化时Cluster 实现类会创建一个 Cluster Invoker 实例,即下图中的 merge 操作);②<strong>使用 Cluster Invoker 实例</strong>(在 Consumer 服务消费者发起远程调用请求的时候Cluster Invoker 会依赖前面课时介绍的 Directory、Router、LoadBalance 等组件得到最终要调用的 Invoker 对象)。</p>
<p><img src="assets/Ciqc1F_GAzqAFfi0AAEr0jR4WUk707.png" alt="Lark20201201-164714.png" /></p>
<p><img src="assets/Ciqc1F_GAzqAFfi0AAEr0jR4WUk707.png" alt="png" /></p>
<p>Cluster 核心流程图</p>
<p>Cluster Invoker 获取 Invoker 的流程大致可描述为如下:</p>
<ol>
@@ -327,10 +327,10 @@ public interface Cluster {
}
</code></pre>
<p>Cluster 接口的实现类如下图所示,分别对应前面提到的多种容错策略:</p>
<p><img src="assets/Ciqc1F_GA0WADFvhAAFaDfyWGXU780.png" alt="Lark20201201-164718.png" /></p>
<p><img src="assets/Ciqc1F_GA0WADFvhAAFaDfyWGXU780.png" alt="png" /></p>
<p>Cluster 接口继承关系</p>
<p><strong>在每个 Cluster 接口实现中,都会创建对应的 Invoker 对象,这些都继承自 AbstractClusterInvoker 抽象类</strong>,如下图所示:</p>
<p><img src="assets/CgqCHl_GA0-AcVvrAAGLJ3YaO2Q177.png" alt="Lark20201201-164728.png" /></p>
<p><img src="assets/CgqCHl_GA0-AcVvrAAGLJ3YaO2Q177.png" alt="png" /></p>
<p>AbstractClusterInvoker 继承关系图</p>
<p>通过上面两张继承关系图我们可以看出Cluster 接口和 Invoker 接口都会有相应的抽象实现类,这些抽象实现类都实现了一些公共能力。下面我们就来深入介绍 AbstractClusterInvoker 和 AbstractCluster 这两个抽象类。</p>
<h3>AbstractClusterInvoker</h3>
@@ -543,7 +543,7 @@ public &lt;T&gt; Invoker&lt;T&gt; join(Directory&lt;T&gt; directory) throws RpcE
}
</code></pre>
<p>Dubbo 提供了两个 ClusterInterceptor 实现类,分别是 ConsumerContextClusterInterceptor 和 ZoneAwareClusterInterceptor如下图所示</p>
<p><img src="assets/CgqCHl_GA2GAYY4rAAGXJIXwK1k980.png" alt="Lark20201201-164721.png" /></p>
<p><img src="assets/CgqCHl_GA2GAYY4rAAGXJIXwK1k980.png" alt="png" /></p>
<p>ClusterInterceptor 继承关系图</p>
<p>在 ConsumerContextClusterInterceptor 的 before() 方法中,会在 RpcContext 中设置当前 Consumer 地址、此次调用的 Invoker 等信息,同时还会删除之前与当前线程绑定的 Server Context。在 after() 方法中,会删除本地 RpcContext 的信息。ConsumerContextClusterInterceptor 的具体实现如下:</p>
<pre><code>public void before(AbstractClusterInvoker&lt;?&gt; invoker, Invocation invocation) {

View File

@@ -722,10 +722,10 @@ private void rePut(Timeout timeout) {
}
</code></pre>
<p>在 Dubbo 中使用多个注册中心的架构如下图所示:</p>
<p><img src="assets/Ciqc1F_IvtuAXngKAADJgn-frEE576.png" alt="Lark20201203-183149.png" /></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="Lark20201203-183145.png" /></p>
<p><img src="assets/Ciqc1F_IvuOAFBfoAAD_GvyhrZY880.png" alt="png" /></p>
<p>ZoneAwareClusterInvoker 在多注册中心之间进行选择的策略有以下四种。</p>
<ol>
<li>找到<strong>preferred 属性为 true 的注册中心,它是优先级最高的注册中心</strong>,只有该中心无可用 Provider 节点时,才会回落到其他注册中心。</li>

View File

@@ -340,7 +340,7 @@ function hide_canvas() {
</code></pre>
<h3>ArrayMerger</h3>
<p>在 Dubbo 中提供了处理不同类型返回值的 Merger 实现,其中不仅有处理 boolean[]、byte[]、char[]、double[]、float[]、int[]、long[]、short[] 等<strong>基础类型数组</strong>的 Merger 实现,还有处理 List、Set、Map 等<strong>集合类</strong>的 Merger 实现,具体继承关系如下图所示:</p>
<p><img src="assets/CgqCHl_PFWiAbmfPAAPxSnmLN4s499.png" alt="Lark20201208-135542.png" /></p>
<p><img src="assets/CgqCHl_PFWiAbmfPAAPxSnmLN4s499.png" alt="png" /></p>
<p>Merger 继承关系图</p>
<p>我们首先来看 ArrayMerger 实现:<strong>当服务接口的返回值为数组的时候,会使用 ArrayMerger 将多个数组合并成一个数组,也就是将二维数组拍平成一维数组</strong>。ArrayMerger.merge() 方法的具体实现如下:</p>
<pre><code>public Object[] merge(Object[]... items) {

View File

@@ -295,7 +295,7 @@ function hide_canvas() {
<p>在前面第 38 课时中,我们深入介绍了 Dubbo 提供的多种 Cluster 实现以及相关的 Cluster Invoker 实现,其中的 ZoneAwareClusterInvoker 就涉及了 MockClusterInvoker 的相关内容。本课时我们就来介绍 Dubbo 中 Mock 机制的全链路流程,不仅包括与 Cluster 接口相关的 MockClusterWrapper 和 MockClusterInvoker我们还会回顾前面课程的 Router 和 Protocol 接口,分析它们与 Mock 机制相关的实现。</p>
<h3>MockClusterWrapper</h3>
<p>Cluster 接口有两条继承线(如下图所示):一条线是 AbstractCluster 抽象类,这条继承线涉及的全部 Cluster 实现类我们已经在[第 37 课时]中深入分析过了;另一条线是 MockClusterWrapper 这条线。</p>
<p><img src="assets/Ciqc1F_PExSAAkePAABxACxNLzc156.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F_PExSAAkePAABxACxNLzc156.png" alt="png" /></p>
<p>Cluster 继承关系图</p>
<p><strong>MockClusterWrapper 是 Cluster 对象的包装类</strong>,我们在之前[第 4 课时]介绍 Dubbo SPI 机制时已经分析过 Wrapper 的功能MockClusterWrapper 类会对 Cluster 进行包装。下面是 MockClusterWrapper 的具体实现,其中会在 Cluster Invoker 对象的基础上使用 MockClusterInvoker 进行包装:</p>
<pre><code>public class MockClusterWrapper implements Cluster {
@@ -382,7 +382,7 @@ function hide_canvas() {
</code></pre>
<h3>MockInvokersSelector</h3>
<p>在[第 32 课时]和[第 33 课时]中,我们介绍了 Router 接口多个实现类,但当时并没有深入介绍 Mock 相关的 Router 实现类—— MockInvokersSelector它的继承关系如下图所示</p>
<p><img src="assets/CgqCHl_PEyqAeilHAAAnrF4cOr8848.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl_PEyqAeilHAAAnrF4cOr8848.png" alt="png" /></p>
<p>MockInvokersSelector 继承关系图</p>
<p><strong>MockInvokersSelector 是 Dubbo Mock 机制相关的 Router 实现</strong>,在未开启 Mock 机制的时候,会返回正常的 Invoker 对象集合;在开启 Mock 机制之后,会返回 MockInvoker 对象集合。MockInvokersSelector 的具体实现如下:</p>
<pre><code>public &lt;T&gt; List&lt;Invoker&lt;T&gt;&gt; route(final List&lt;Invoker&lt;T&gt;&gt; invokers,

View File

@@ -352,7 +352,7 @@ function hide_canvas() {
}
</code></pre>
<p>这里我们重点关注的是<strong>exportServices() 方法,它是服务发布核心逻辑的入口</strong>,其中每一个服务接口都会转换为对应的 ServiceConfig 实例,然后通过代理的方式转换成 Invoker最终转换成 Exporter 进行发布。服务发布流程中涉及的核心对象转换,如下图所示:</p>
<p><img src="assets/Ciqc1F_YdkGABhTFAACpT-2oDtw867.png" alt="Lark20201215-163844.png" /></p>
<p><img src="assets/Ciqc1F_YdkGABhTFAACpT-2oDtw867.png" alt="png" /></p>
<p>服务发布核心流程图</p>
<p>exportServices() 方法的具体实现如下:</p>
<pre><code>private void exportServices() {
@@ -667,7 +667,7 @@ anyhost=true
<li>触发 RegistryProtocolListener 监听器。</li>
</ol>
<p>远程发布的详细流程如下图所示:</p>
<p><img src="assets/CgpVE1_YNDaATl3fAAFcJTJOw3M699.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgpVE1_YNDaATl3fAAFcJTJOw3M699.png" alt="png" /></p>
<p>服务发布详细流程图</p>
<h3>总结</h3>
<p>本课时我们重点介绍了 Dubbo 服务发布的核心流程。</p>

View File

@@ -294,7 +294,7 @@ function hide_canvas() {
<p>在微服务架构中,服务是基本单位,而 Dubbo 架构中服务的基本单位是 Java 接口,这种架构上的差别就会带来一系列挑战。<strong>从 2.7.5 版本开始Dubbo 引入了服务自省架构,来应对微服务架构带来的挑战</strong>。具体都有哪些挑战呢?下面我们就来详细说明一下。</p>
<h3>注册中心面临的挑战</h3>
<p>在开始介绍注册中心面临的挑战之前,我们先来回顾一下前面课时介绍过的 Dubbo 传统架构以及这个架构中最核心的组件:</p>
<p><img src="assets/Cip5yF_hcH2AVI2PAAB7_C-aKWA247.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Cip5yF_hcH2AVI2PAAB7_C-aKWA247.png" alt="png" /></p>
<p>Dubbo 核心架构图</p>
<p>结合上面这张架构图,我们可以一起回顾一下这些核心组件的功能。</p>
<ul>
@@ -315,7 +315,7 @@ function hide_canvas() {
<h3>Dubbo 的改进方案</h3>
<p>Dubbo 从 2.7.0 版本开始增加了<strong>简化 URL</strong>的特性,从 URL 中抽出的数据会被存放至元数据中心。但是这次优化只是缩短了 URL 的长度,从内存使用量以及降低通知频繁度的角度降低了注册中心的压力,并没有减少注册中心 URL 的数量,所以注册中心所承受的压力还是比较明显的。</p>
<p>Dubbo 2.7.5 版本引入了<strong>服务自省架构</strong>进一步降低了注册中心的压力。在此次优化中Dubbo 修改成应用为粒度的服务注册与发现模型,最大化地减少了 Dubbo 服务元信息注册数量,其核心流程如下图所示:</p>
<p><img src="assets/CgqCHl_hcJqACV_gAAEpu4IHuz4068.png" alt="Lark20201222-120323.png" /></p>
<p><img src="assets/CgqCHl_hcJqACV_gAAEpu4IHuz4068.png" alt="png" /></p>
<p>服务自省架构图</p>
<p>上图展示了引入服务自省之后的 Dubbo 服务注册与发现的核心流程Dubbo 会按照顺序执行这些操作(当其中一个操作失败时,后续操作不会执行)。</p>
<p>我们首先来看 Provider 侧的执行流程:</p>
@@ -338,7 +338,7 @@ function hide_canvas() {
<p>在有的场景中,我们会在线上部署两组不同配置的服务节点,来验证某些配置是否生效。例如,共有 100 个服务节点,平均分成 A、B 两组A 组服务节点超时时间(即 timeout设置为 3000 msB 组的超时时间(即 timeout设置为 2000 ms这样的话该服务就有了两组不同的元数据。</p>
<p>按照前面介绍的优化方案,在订阅服务的时候,会得到 100 个 ServiceInstance因为每个 ServiceInstance 发布的服务元数据都有可能不一样,所以我们需要调用每个 ServiceInstance 的 MetadataService 服务获取元数据。</p>
<p>为了减少 MetadataService 服务的调用次数Dubbo 提出了<strong>服务修订版本的优化方案</strong>,其核心思想是:将每个 ServiceInstance 发布的服务 URL 计算一个 hash 值(也就是 revision 值),并随 ServiceInstance 一起发布到注册中心;在 Consumer 端进行订阅的时候,对于 revision 值相同的 ServiceInstance不再调用 MetadataService 服务,直接共用一份 URL 即可。下图展示了 Dubbo 服务修订的核心逻辑:</p>
<p><img src="assets/Cip5yF_hcMyALC7UAAEPa7NIifA395.png" alt="Lark20201222-120318.png" /></p>
<p><img src="assets/Cip5yF_hcMyALC7UAAEPa7NIifA395.png" alt="png" /></p>
<p>引入 Dubbo 服务修订的 Consumer 端交互图</p>
<p>通过该流程图,我们可以看到 Dubbo Consumer 端实现服务修订的流程如下。</p>
<ol>

View File

@@ -338,12 +338,12 @@ function hide_canvas() {
<li>methodsList 类型):接口中定义的全部方法描述信息。在 MethodDefinition 中记录了方法的名称、参数类型、返回值类型以及方法参数涉及的所有 TypeDefinition。</li>
<li>typesList 类型接口定义中涉及的全部类型描述信息包括方法的参数和字段如果遇到复杂类型TypeDefinition 会递归获取复杂类型内部的字段。在 dubbo-metadata-api 模块中,提供了多种类型对应的 TypeBuilder 用于创建对应的 TypeDefinition对于没有特定 TypeBuilder 实现的类型,会使用 DefaultTypeBuilder。</li>
</ul>
<p><img src="assets/CgqCHl_lrYOAIzjeAAFEk3cUdpg570.png" alt="6.png" /></p>
<p><img src="assets/CgqCHl_lrYOAIzjeAAFEk3cUdpg570.png" alt="png" /></p>
<p>TypeBuilder 接口实现关系图</p>
<p><strong>在服务发布的时候,会将服务的 URL 中的部分数据封装为 FullServiceDefinition 对象,然后作为元数据存储起来</strong>。FullServiceDefinition 继承了 ServiceDefinition并在 ServiceDefinition 基础之上扩展了 params 集合Map&lt;String, String&gt; 类型),用来存储 URL 上的参数。</p>
<h3>MetadataService</h3>
<p>接下来看 MetadataService 接口,在上一讲我们提到<strong>Dubbo 中的每个 ServiceInstance 都会发布 MetadataService 接口供 Consumer 端查询元数据</strong>,下图展示了 MetadataService 接口的继承关系:</p>
<p><img src="assets/CgpVE1_lrZGANC4vAAGdcllZU9o940.png" alt="1.png" /></p>
<p><img src="assets/CgpVE1_lrZGANC4vAAGdcllZU9o940.png" alt="png" /></p>
<p>MetadataService 接口继承关系图</p>
<p>在 MetadataService 接口中定义了查询当前 ServiceInstance 发布的元数据的相关方法,具体如下所示:</p>
<pre><code>public interface MetadataService {
@@ -481,7 +481,7 @@ private boolean doFunction(BiFunction&lt;WritableMetadataService, URL, Boolean&g
<p>元数据中心是 Dubbo 2.7.0 版本之后新增的一项优化,其主要目的是将 URL 中的一部分内容存储到元数据中心,从而减少注册中心的压力。</p>
<p><strong>元数据中心的数据只是给本端自己使用的,改动不需要告知对端</strong>例如Provider 修改了元数据,不需要实时通知 Consumer。这样在注册中心存储的数据量减少的同时还减少了因为配置修改导致的注册中心频繁通知监听者情况的发生很好地减轻了注册中心的压力。</p>
<p><strong>MetadataReport 接口是 Dubbo 节点与元数据中心交互的桥梁</strong>,其继承关系如下图所示:</p>
<p><img src="assets/Cip5yF_lramAYf82AAFkkbA0N2g785.png" alt="2.png" /></p>
<p><img src="assets/Cip5yF_lramAYf82AAFkkbA0N2g785.png" alt="png" /></p>
<p>MetadataReport 继承关系图</p>
<p>我们先来看一下 MetadataReport 接口的核心定义:</p>
<pre><code>public interface MetadataReport {
@@ -675,10 +675,10 @@ private boolean doHandleMetadataCollection(Map&lt;MetadataIdentifier, Object&gt;
</code></pre>
<p>在 AbstractMetadataReport 的构造方法中,会根据 reportServerURL也就是后面的 metadataReportURL参数启动一个“天”级别的定时任务该定时任务会执行 publishAll() 方法,其中会通过 doHandleMetadataCollection() 方法将 allMetadataReports 集合中的全部元数据重新进行上报。该定时任务默认是在凌晨 02:00~06:00 启动,每天执行一次。</p>
<p>到此为止AbstractMetadataReport 为子类实现的公共能力就介绍完了,其他方法都是委托给了相应的 do*() 方法,这些 do*() 方法都是在 AbstractMetadataReport 子类中实现的。</p>
<p><img src="assets/CgqCHl_hcauAR9AQAAG7kMJSlc8827.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CgqCHl_hcauAR9AQAAG7kMJSlc8827.png" alt="png" /></p>
<h4>2. BaseMetadataIdentifier</h4>
<p>在 AbstractMetadataReport 上报元数据的时候,元数据对应的 Key 都是BaseMetadataIdentifier 类型的对象,其继承关系如下图所示:</p>
<p><img src="assets/Ciqc1F_lrb-Ad3a5AAGJ2ySdyBE643.png" alt="3.png" /></p>
<p><img src="assets/Ciqc1F_lrb-Ad3a5AAGJ2ySdyBE643.png" alt="png" /></p>
<p>BaseMetadataIdentifier 继承关系图</p>
<ul>
<li>MetadataIdentifier 中包含了服务接口、version、group、side 和 application 五个核心字段。</li>
@@ -695,7 +695,7 @@ public interface MetadataReportFactory {
</code></pre>
<p>MetadataReportFactory 是个扩展接口,从 @SPI 注解的默认值可以看出<strong>Dubbo 默认使用 Redis 实现元数据中心</strong>
Dubbo 提供了针对 ZooKeeper、Redis、Consul 等作为元数据中心的 MetadataReportFactory 实现,如下图所示:</p>
<p><img src="assets/CgqCHl_lrcmAEIYBAAFSOVpEU1Y779.png" alt="4.png" /></p>
<p><img src="assets/CgqCHl_lrcmAEIYBAAFSOVpEU1Y779.png" alt="png" /></p>
<p>MetadataReportFactory 继承关系图</p>
<p>这些 MetadataReportFactory 实现都继承了 AbstractMetadataReportFactory在 AbstractMetadataReportFactory 提供了缓存 MetadataReport 实现的功能,并定义了一个 createMetadataReport() 抽象方法供子类实现。另外AbstractMetadataReportFactory 实现了 MetadataReportFactory 接口的 getMetadataReport() 方法,下面我们就来简单看一下该方法的实现:</p>
<pre><code>public MetadataReport getMetadataReport(URL url) {
@@ -763,7 +763,7 @@ String getNodePath(BaseMetadataIdentifier metadataIdentifier) {
}
</code></pre>
<p><strong>MetadataServiceExporter 只有 ConfigurableMetadataServiceExporter 这一个实现</strong>,如下图所示:</p>
<p><img src="assets/Cip5yF_hcfmAMtHdAABVR_mzQyg047.png" alt="Drawing 6.png" /></p>
<p><img src="assets/Cip5yF_hcfmAMtHdAABVR_mzQyg047.png" alt="png" /></p>
<p>MetadataServiceExporter 继承关系图</p>
<p>ConfigurableMetadataServiceExporter 的核心实现是 export() 方法,其中会创建一个 ServiceConfig 对象完成 MetadataService 服务的发布:</p>
<pre><code>public ConfigurableMetadataServiceExporter export() {

View File

@@ -368,10 +368,10 @@ public interface ServiceDiscovery extends Prioritized {
}
</code></pre>
<p>ServiceDiscovery 接口被 @SPI 注解修饰,是一个扩展点,针对不同的注册中心,有不同的 ServiceDiscovery 实现,如下图所示:</p>
<p><img src="assets/Ciqc1F_q45aAGn14AAEh58Guyew441.png" alt="Lark20201229-160604.png" /></p>
<p><img src="assets/Ciqc1F_q45aAGn14AAEh58Guyew441.png" alt="png" /></p>
<p>ServiceDiscovery 继承关系图</p>
<p>在 Dubbo 创建 ServiceDiscovery 对象的时候,会通过 ServiceDiscoveryFactory 工厂类进行创建。ServiceDiscoveryFactory 接口也是一个扩展接口Dubbo 只提供了一个默认实现—— DefaultServiceDiscoveryFactory其继承关系如下图所示</p>
<p><img src="assets/CgpVE1_q4_iAZ8ARAAEu4mMS65Y213.png" alt="Lark20201229-160606.png" /></p>
<p><img src="assets/CgpVE1_q4_iAZ8ARAAEu4mMS65Y213.png" alt="png" /></p>
<p>ServiceDiscoveryFactory 继承关系图</p>
<p>在 AbstractServiceDiscoveryFactory 中维护了一个 ConcurrentMap&lt;String, ServiceDiscovery&gt; 类型的集合discoveries 字段)来缓存 ServiceDiscovery 对象,并提供了一个 createDiscovery() 抽象方法来创建 ServiceDiscovery 实例。</p>
<pre><code>public ServiceDiscovery getServiceDiscovery(URL registryURL) {
@@ -427,7 +427,7 @@ public static org.apache.curator.x.discovery.ServiceInstance&lt;ZookeeperInstanc
<p>除了上述服务实例发布的功能之外,在服务实例订阅的时候,还会用到 ZookeeperServiceDiscovery 查询服务实例的信息,这些方法都是直接依赖 Apache Curator 实现的例如getServices() 方法会调用 Curator ServiceDiscovery 的 queryForNames() 方法查询 Service NamegetInstances() 方法会通过 Curator ServiceDiscovery 的 queryForInstances() 方法查询 Service Instance。</p>
<h3>EventListener 接口</h3>
<p>ZookeeperServiceDiscovery 除了实现了 ServiceDiscovery 接口之外,还实现了 EventListener 接口,如下图所示:</p>
<p><img src="assets/Cip5yF_petCAV9sXAAB9u4EYOqk073.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Cip5yF_petCAV9sXAAB9u4EYOqk073.png" alt="png" /></p>
<p>ZookeeperServiceDiscovery 继承关系图</p>
<p>也就是说,<strong>ZookeeperServiceDiscovery 本身也是 EventListener 实现,可以作为 EventListener 监听某些事件</strong>。下面我们先来看 Dubbo 中 EventListener 接口的定义其中关注三个方法onEvent() 方法、getPriority() 方法和 findEventType() 工具方法。</p>
<pre><code>@SPI
@@ -461,7 +461,7 @@ public interface EventListener&lt;E extends Event&gt; extends java.util.EventLis
}
</code></pre>
<p>Dubbo 中有很多 EventListener 接口的实现,如下图所示:</p>
<p><img src="assets/Cip5yF_petmAI2w9AAC5QQrgGjY394.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Cip5yF_petmAI2w9AAC5QQrgGjY394.png" alt="png" /></p>
<p>EventListener 继承关系图</p>
<p>我们先来重点关注 ZookeeperServiceDiscovery 这个实现,在其 onEvent() 方法(以及 addServiceInstancesChangedListener() 方法)中会调用 registerServiceWatcher() 方法重新注册:</p>
<pre><code>public void onEvent(ServiceInstancesChangedEvent event) {
@@ -517,7 +517,7 @@ public interface EventDispatcher extends Listenable&lt;EventListener&lt;?&gt;&gt
}
</code></pre>
<p>EventDispatcher 接口被 @SPI 注解修饰是一个扩展点Dubbo 提供了两个具体实现——ParallelEventDispatcher 和 DirectEventDispatcher如下图所示</p>
<p><img src="assets/Cip5yF_pew-AdtkyAAB-Epfg96E814.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Cip5yF_pew-AdtkyAAB-Epfg96E814.png" alt="png" /></p>
<p>EventDispatcher 继承关系图</p>
<p>在 AbstractEventDispatcher 中维护了两个核心字段。</p>
<ul>

View File

@@ -291,7 +291,7 @@ function hide_canvas() {
<p id="tip" align="center"></p>
<div><h1>46 加餐:深入服务自省方案中的服务发布订阅(下)</h1>
<p>在课程第二部分13~15 课时)中介绍 Dubbo 传统框架中的注册中心部分实现时,我们提到了 Registry、RegistryFactory 等与注册中心交互的接口。<strong>为了将 ServiceDiscovery 接口的功能与 Registry 融合Dubbo 提供了一个 ServiceDiscoveryRegistry 实现</strong>,继承关系如下所示:</p>
<p><img src="assets/Ciqc1F_pe3KAQs8SAAPkHLoWbUM655.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Ciqc1F_pe3KAQs8SAAPkHLoWbUM655.png" alt="png" /></p>
<p>ServiceDiscoveryRegistry 、ServiceDiscoveryRegistryFactory 继承关系图</p>
<p>由图我们可以看到ServiceDiscoveryRegistryFactory扩展名称是 service-discovery-registry是 ServiceDiscoveryRegistry 对应的工厂类,继承了 AbstractRegistryFactory 提供的公共能力。</p>
<p><strong>ServiceDiscoveryRegistry 是一个面向服务实例ServiceInstance的注册中心实现</strong>,其底层依赖前面两个课时介绍的 ServiceDiscovery、WritableMetadataService 等组件。</p>
@@ -476,7 +476,7 @@ public interface ServiceInstanceCustomizer extends Prioritized {
</code></pre>
<p>关于 ServiceInstanceCustomizer 接口,这里需要关注三个点:①该接口被 @SPI 注解修饰,是一个扩展点;②该接口继承了 Prioritized 接口;③该接口中定义的 customize() 方法可以用来自定义 ServiceInstance 信息,其中就包括控制 metadata 集合中的数据。</p>
<p>也就说,<strong>ServiceInstanceCustomizer 的多个实现可以按序调用,实现 ServiceInstance 的自定义</strong>。下图展示了 ServiceInstanceCustomizer 接口的所有实现类:</p>
<p><img src="assets/CgpVE1_pe6SAT90SAAC2xP9_c7c171.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgpVE1_pe6SAT90SAAC2xP9_c7c171.png" alt="png" /></p>
<p>ServiceInstanceCustomizer 继承关系图</p>
<p>我们首先来看 ServiceInstanceMetadataCustomizer 这个抽象类,它主要是对 ServiceInstance 中 metadata 这个 KV 集合进行自定义修改,这部分逻辑在 customize() 方法中,如下所示:</p>
<pre><code>public final void customize(ServiceInstance serviceInstance) {
@@ -624,7 +624,7 @@ public interface ServiceInstanceCustomizer extends Prioritized {
}
</code></pre>
<p>这里涉及一个新的接口——<strong>MetadataServiceProxyFactory它是用来创建 MetadataService 本地代理的工厂类</strong>,继承关系如下所示:</p>
<p><img src="assets/CgpVE1_pe72AFUTPAADh6TOy_Ak061.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgpVE1_pe72AFUTPAADh6TOy_Ak061.png" alt="png" /></p>
<p>MetadataServiceProxyFactory 继承关系图</p>
<p>在 BaseMetadataServiceProxyFactory 中提供了缓存 MetadataService 本地代理的公共功能,其中维护了一个 proxies 集合HashMap&lt;String, MetadataService&gt; 类型Key 是 Service Name 与一个 ServiceInstance 的 revision 值的组合Value 是该 ServiceInstance 对应的 MetadataService 服务的本地代理对象。创建 MetadataService 本地代理的功能是在 createProxy() 抽象方法中实现的,这个方法由 BaseMetadataServiceProxyFactory 的子类具体实现。</p>
<p><strong>下面来看 BaseMetadataServiceProxyFactory 的两个实现——DefaultMetadataServiceProxyFactory 和 RemoteMetadataServiceProxyFactory。</strong></p>
@@ -650,7 +650,7 @@ public interface ServiceInstanceCustomizer extends Prioritized {
}
</code></pre>
<p>这里我们来看 MetadataServiceURLBuilder 接口中创建 MetadataService 服务对应的 URL 的逻辑,下图展示了 MetadataServiceURLBuilder 接口的实现:</p>
<p><img src="assets/Ciqc1F_pe8eARHUJAACyQ5kof-I149.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Ciqc1F_pe8eARHUJAACyQ5kof-I149.png" alt="png" /></p>
<p>MetadataServiceURLBuilder 继承关系图</p>
<p>其中SpringCloudMetadataServiceURLBuilder 是兼容 Spring Cloud 的实现,这里就不深入分析了。我们重点来看 StandardMetadataServiceURLBuilder 的实现,其中会根据 ServiceInstance.metadata 携带的 URL 参数、Service Name、ServiceInstance 的 host 等信息构造 MetadataService 服务对应 URL如下所示</p>
<pre><code>public List&lt;URL&gt; build(ServiceInstance serviceInstance) {
@@ -680,7 +680,7 @@ public interface ServiceInstanceCustomizer extends Prioritized {
}
</code></pre>
<p>接下来我们看 RemoteMetadataServiceProxyFactory 这个实现类,其中的 createProxy() 方法会直接创建一个 RemoteMetadataServiceProxy 对象并返回。在前面第 44 课时介绍 MetadataService 接口的时候,我们重点介绍的是 WritableMetadataService 这个子接口下的实现,并没有提及 RemoteMetadataServiceProxy 这个实现。下图是 RemoteMetadataServiceProxy 在继承体系中的位置:</p>
<p><img src="assets/Ciqc1F_pe9GAEnNjAAA4bEzwZzw039.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Ciqc1F_pe9GAEnNjAAA4bEzwZzw039.png" alt="png" /></p>
<p>RemoteMetadataServiceProxy 继承关系图</p>
<p>RemoteMetadataServiceProxy 作为 RemoteWritableMetadataService 的本地代理,其 getExportedURLs()、getServiceDefinition() 等方法的实现,完全依赖于 MetadataReport 进行实现。这里以 getExportedURLs() 方法为例:</p>
<pre><code>public SortedSet&lt;String&gt; getExportedURLs(String serviceInterface, String group, String version, String protocol) {

View File

@@ -306,11 +306,11 @@ function hide_canvas() {
</ol>
<h3>Configuration</h3>
<p><strong>Configuration 接口是 Dubbo 中所有配置的基础接口</strong>,其中定义了根据指定 Key 获取对应配置值的相关方法,如下图所示:</p>
<p><img src="assets/Cip5yF_zz3yABBYdAACqAETTGm0778.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Cip5yF_zz3yABBYdAACqAETTGm0778.png" alt="png" /></p>
<p>Configuration 接口核心方法</p>
<p>从上图中我们可以看到Configuration 针对不同的 boolean、int、String 返回值都有对应的 get*() 方法,同时还提供了带有默认值的 get*() 方法。<strong>这些 get<p style="text-align:center">*() 方法底层首先调用 getInternalProperty() 方法获取配置值</strong>,然后调用 convert() 方法将获取到的配置值转换成返回值的类型之后返回。getInternalProperty() 是一个抽象方法,由 Configuration 接口的子类具体实现。</p>
<p>下图展示了 Dubbo 中提供的 Configuration 接口实现包括SystemConfiguration、EnvironmentConfiguration、InmemoryConfiguration、PropertiesConfiguration、CompositeConfiguration、ConfigConfigurationAdapter 和 DynamicConfiguration。下面我们将结合具体代码逐个介绍其实现。</p>
<p><img src="assets/Cip5yF_zz6eAMN_oAACEEj9pVjg547.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Cip5yF_zz6eAMN_oAACEEj9pVjg547.png" alt="png" /></p>
<p>Configuration 继承关系图</p>
<h4>SystemConfiguration &amp; EnvironmentConfiguration</h4>
<p>SystemConfiguration 是从 Java Properties 配置(也就是 -D 配置参数中获取相应的配置项EnvironmentConfiguration 是从使用环境变量中获取相应的配置。两者的 getInternalProperty() 方法实现如下:</p>
@@ -435,7 +435,7 @@ public interface OrderedPropertiesProvider {
</code></pre>
<h4>ConfigConfigurationAdapter</h4>
<p>Dubbo 通过 AbstractConfig 类来抽象实例对应的配置,如下图所示:</p>
<p><img src="assets/CgqCHl_zz8WAHdY3AAMJFKW_uQE360.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl_zz8WAHdY3AAMJFKW_uQE360.png" alt="png" /></p>
<p>AbstractConfig 继承关系图</p>
<p>这些 AbstractConfig 实现基本都对应一个固定的配置,也定义了配置对应的字段以及 getter/setter() 方法。例如RegistryConfig 这个实现类就对应了注册中心的相关配置,其中包含了 address、protocol、port、timeout 等一系列与注册中心相关的字段以及对应的 getter/setter() 方法,来接收用户通过 XML、Annotation 或是 API 方式传入的注册中心配置。</p>
<p><strong>ConfigConfigurationAdapter 是 AbstractConfig 与 Configuration 之间的适配器</strong>,它会将 AbstractConfig 对象转换成 Configuration 对象。在 ConfigConfigurationAdapter 的构造方法中会获取 AbstractConfig 对象的全部字段,并转换成一个 Map&lt;String, String&gt; 集合返回,该 Map&lt;String, String&gt; 集合将会被 ConfigConfigurationAdapter 的 metaData 字段引用。相关示例代码如下:</p>
@@ -485,9 +485,9 @@ public interface DynamicConfigurationFactory {
}
</code></pre>
<p>DynamicConfigurationFactory 接口的继承关系以及 DynamicConfiguration 接口对应的继承关系如下:</p>
<p><img src="assets/CgqCHl_0L-GAPVy9AAEqog2bl7U068.png" alt="11.png" /></p>
<p><img src="assets/CgqCHl_0L-GAPVy9AAEqog2bl7U068.png" alt="png" /></p>
<p>DynamicConfigurationFactory 继承关系图</p>
<p><img src="assets/Cip5yF_zz9iAM1YYAAB_QXlLDcU550.png" alt="Drawing 4.png" /></p>
<p><img src="assets/Cip5yF_zz9iAM1YYAAB_QXlLDcU550.png" alt="png" /></p>
<p>DynamicConfiguration 继承关系图</p>
<p>我们先来看 AbstractDynamicConfigurationFactory 的实现,其中会维护一个 dynamicConfigurations 集合Map&lt;String, DynamicConfiguration&gt; 类型),在 getDynamicConfiguration() 方法中会填充该集合,实现<strong>缓存</strong>DynamicConfiguration 对象的效果。同时AbstractDynamicConfigurationFactory 提供了一个 createDynamicConfiguration() 方法给子类实现,来<strong>创建</strong>DynamicConfiguration 对象。</p>
<p>以 ZookeeperDynamicConfigurationFactory 实现为例,其 createDynamicConfiguration() 方法创建的就是 ZookeeperDynamicConfiguration 对象:</p>
@@ -575,7 +575,7 @@ public interface DynamicConfigurationFactory {
}
</code></pre>
<p>CacheListener 中调用的监听器都是 ConfigurationListener 接口实现,如下图所示,这里涉及[第 33 课时]介绍的 TagRouter、AppRouter 和 ServiceRouter它们主要是监听路由配置的变化还涉及 RegistryDirectory 和 RegistryProtocol 中的四个内部类AbstractConfiguratorListener 的子类),它们主要监听 Provider 和 Consumer 的配置变化。</p>
<p><img src="assets/CgqCHl_0L9WAYbfVAAGH_E-l-UU432.png" alt="222.png" /></p>
<p><img src="assets/CgqCHl_0L9WAYbfVAAGH_E-l-UU432.png" alt="png" /></p>
<p>ConfigurationListener 继承关系图</p>
<p>这些 ConfigurationListener 实现在前面的课程中已经详细介绍过了这里就不再重复。ZookeeperDynamicConfiguration 中还提供了 addListener()、removeListener() 两个方法用来增删 ConfigurationListener 监听器,具体实现比较简单,这里就不再展示,你若感兴趣的话可以参考<a href="https://github.com/xxxlxy2008/dubbo">源码</a>进行学习。</p>
<p>介绍完 ZookeeperDynamicConfiguration 的初始化过程之后,我们再来看 ZookeeperDynamicConfiguration 中<strong>读取配置、写入配置</strong>的相关操作。相关方法的实现如下:</p>

View File

@@ -497,7 +497,7 @@ compositeDynamicConfiguration.addConfiguration(prepareEnvironment(configCenter))
<p>随后DubboBootstrap 执行 checkGlobalConfigs() 方法完成 ProviderConfig、ConsumerConfig、MetadataReportConfig 等一系列 AbstractConfig 的检查和初始化,具体实现比较简单,这里就不再展示。</p>
<p>再紧接着DubboBootstrap 会通过 initMetadataService() 方法初始化 MetadataReport、MetadataReportInstance 以及 MetadataService、MetadataServiceExporter这些元数据相关的组件在前面的课时中已经深入分析过了这里的初始化过程并不复杂你若感兴趣的话可以参考<a href="https://github.com/xxxlxy2008/dubbo">源码</a>进行学习。</p>
<p>在 DubboBootstrap 初始化的最后,会调用 initEventListener() 方法将 DubboBootstrap 作为 EventListener 监听器添加到 EventDispatcher 中。DubboBootstrap 继承了 GenericEventListener 抽象类,如下图所示:</p>
<p><img src="assets/CgqCHl_z0G2AfVK7AABzPAVnhNE632.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CgqCHl_z0G2AfVK7AABzPAVnhNE632.png" alt="png" /></p>
<p>EventListener 继承关系图</p>
<p><strong>GenericEventListener 是一个泛型监听器,它可以让子类监听任意关心的 Event 事件,只需定义相关的 onEvent() 方法即可</strong>。在 GenericEventListener 中维护了一个 handleEventMethods 集合,其中 Key 是 Event 的子类即监听器关心的事件Value 是处理该类型 Event 的相应 onEvent() 方法。</p>
<p>在 GenericEventListener 的构造方法中,通过反射将当前 GenericEventListener 实现的全部 onEvent() 方法都查找出来,并记录到 handleEventMethods 字段中。具体查找逻辑在 findHandleEventMethods() 方法中实现:</p>
@@ -530,7 +530,7 @@ compositeDynamicConfiguration.addConfiguration(prepareEnvironment(configCenter))
}
</code></pre>
<p>我们可以查看 DubboBootstrap 的所有方法,目前并没有发现符合 isHandleEventMethod() 条件的方法。但在 GenericEventListener 的另一个实现—— LoggingEventListener 中,可以看到多个符合 isHandleEventMethod() 条件的方法(如下图所示),在这些 onEvent() 方法重载中会输出 INFO 日志。</p>
<p><img src="assets/CgqCHl_z0HeARRBdAAF6NMV2xrI252.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl_z0HeARRBdAAF6NMV2xrI252.png" alt="png" /></p>
<p>LoggingEventListener 中 onEvent 方法重载</p>
<p>至此DubboBootstrap 整个初始化过程,以及该过程中与配置中心相关的逻辑就介绍完了。</p>
<h3>总结</h3>