first commit

This commit is contained in:
张乾
2024-10-16 00:01:16 +08:00
parent ac7d1ed7bc
commit 84ae12296c
322 changed files with 104488 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
00 开篇词 深入掌握 Dubbo 原理与实现,提升你的职场竞争力
你好,我是杨四正,接下来一段时间我们会一起来探究 Dubbo。
我曾在电商、新零售、短视频、直播等领域的多家互联网企业任职,期间我在业务线没日没夜地“搬过砖”,在基础组件部门“造过轮子”,也在架构部门搞过架构设计,目前依旧在从事基础架构的相关工作,主要负责公司的 Framework、RPC 框架、数据库中间件等方向的开发和运维工作。我深入研究过多个开源中间件,平时喜欢以文会友,分享源码分析的经验和心得。
为什么要学习 Dubbo
我们在谈论任何一项技术的时候,都需要强调它所适用的业务场景,因为: 技术之所以有价值,就是因为它解决了一些业务场景难题。
一家公司由小做大,业务会不断发展,随之而来的是 DAU、订单量、数据量的不断增长用来支撑业务的系统复杂度也会不断提高模块之间的依赖关系也会日益复杂。这时候我们一般会从单体架构进入集群架构如下图所示在集群架构中通过负载均衡技术将流量尽可能均摊到集群中的每台机器上以此克服单台机器硬件资源的限制做到横向扩展。
单体架构 VS 集群架构
之后,又由于业务系统本身的实现较为复杂、扩展性较差、性能也有上限,代码和功能的复用能力较弱,我们会将一个巨型业务系统拆分成多个微服务,根据不同服务对资源的不同要求,选择更合理的硬件资源。例如,有些流量较小的服务只需要几台机器构成的集群即可,而核心业务则需要成百上千的机器来支持,这样就可以最大化系统资源的利用率。
另外一个好处是,可以在服务维度进行重用,在需要某个服务的时候,直接接入即可,从而提高开发效率。拆分成独立的服务之后(如下图所示),整个服务可以最大化地实现重用,也可以更加灵活地扩展。
微服务架构图
但是在微服务架构落地的过程中,我们需要解决的问题有很多,如:
服务之间如何高性能地通信?
服务调用如何做到负载均衡、FailOver、限流
如何有效地划清服务边界?
如何进行服务治理?
……
Apache Dubbo是一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力:
面向接口的远程方法调用;
可靠、智能的容错和负载均衡;
服务自动注册和发现能力。
简单地说, Dubbo 是一个分布式服务框架,致力于提供高性能、透明化的 RPC 远程服务调用方案以及服务治理方案,以帮助我们解决微服务架构落地时的问题。
Dubbo 是由阿里开源,后来加入了 Apache 基金会,目前已经从孵化器毕业,成为 Apache 的顶级项目。Apache Dubbo 目前已经有接近 32.8 K 的 Star、21.4 K 的 Fork其热度可见一斑 很多互联网大厂(如阿里、滴滴、去哪儿网等)都是直接使用 Dubbo 作为其 RPC 框架,也有些大厂会基于 Dubbo 进行二次开发实现自己的 RPC 框架 ,如当当网的 DubboX。
作为一名 Java 工程师,深入掌握 Dubbo 的原理和实现已经是大势所趋,并且成为你职场竞争力的关键项。拉勾网显示,研发工程师、架构师等高薪岗位,都要求你熟悉并曾经深入使用某种 RPC 框架,一线大厂更是要求你至少深入了解一款 RPC 框架的原理和核心实现。
(职位信息来源:拉勾网)
而 Dubbo 就是首选。Dubbo 和 Spring Cloud 是目前主流的微服务框架,阿里、京东、小米、携程、去哪儿网等互联网公司的基础设施早已落成,并且后续的很多项目还是以 Dubbo 为主。Dubbo 重启之后,已经开始规划 3.0 版本,相信后面还会有更加惊艳的表现。
另外RPC 框架的核心原理和设计都是相通的,阅读过 Dubbo 源码之后,你再去了解其他 RPC 框架的代码,就是一件非常简单的事情了。
阅读 Dubbo 源码的痛点
学习和掌握一项技能的时候,一般都是按照“是什么”“怎么用”“为什么”(原理)逐层深入的:
同样,你可以通过阅读官方文档或是几篇介绍性的文章,迅速了解 Dubbo 是什么;接下来,再去上手,用 Dubbo 写几个项目,从而更加全面地熟悉 Dubbo 的使用方式和特性,成为一名“熟练工”,但这也是很多开发者所处的阶段。而“有技术追求”的开发者,一般不会满足于每天只是写写业务代码,而是会开始研究 Dubbo 的源码实现以及底层原理,这就对应了上图中的核心层:“原理”。
而开始阅读源码时,不少开发者会提前去网上查找资料,或者直接埋头钻研源码,并因为这样的学习路径而普遍面临一些痛点问题:
网络资料不少,但大多是复制 Dubbo 官方文档,甚至干脆就是粘贴了一堆 Dubbo 源码过来,没有任何自己的个人实践和经验分享,学习花费精力不说,收获却不大。
相关资料讲述的 Dubbo 版本比较陈旧,没有跟上最新的设计和优化,有时候还会误导你。或者切入点很小,只针对 Dubbo 的一个流程进行介绍,看完之后,你只知道这一条调用分支上的相关内容,代码一旦运行到其他地方,还是一脸懵。
若抛开参考资料,自己直接去阅读 Dubbo 源码,你本身又需要具备一定的技术功底,而且要对整个开源项目有比较高的熟练度,这样你才能够循着它的核心逻辑去快速掌握它。而对于一个相对陌生的开源项目来说,这可能就是一个非常痛苦的过程了,并且最致命的是,由于对整个架构的“视野”受限,你很可能会迷失在代码迷宫中,最后虽然也花了很大力气去阅读和 Debug 源码,却在关上 IDEA 之后依然“雾里看花”。
课程设置
我曾经分享过各种开源项目的源码分析资料,并且收到大家的一致好评,所以我决定和拉勾教育合作,开设一个系列课程,根据自己丰富的开源项目分析经验来带你一起阅读 Dubbo 源码,希望帮你做到融会贯通,并在实践中能够举一反三。
具体来说,在这个课程中我会:
从基础知识开始,通过丰富的 Demo 演示,手把手带你分析 Dubbo 涉及的核心知识点。之后再带你使用这些核心技术,通过编写一个简易版本的 RPC 框架串联所有知识点。
带你自底向上剖析 Dubbo 的源码,深入理解 Dubbo 的工作原理及核心实现,让你不再停留在简单使用 Dubbo 的阶段做到知其然也知其所以然。例如Provider 是如何将服务发布到注册中心的、Consumer 是如何从注册中心订阅服务的,等等问题都可以在这里找到解答。
点名 Dubbo 源码中的设计模式,让你了解设计模式的优秀实践方式,帮助你从“纸上谈兵”变成“用兵如神”,这样在你进行架构设计以及代码编写的时候,就可以真正使用这些设计模式,让你的代码扩展性更强、可维护性更好。
带你领略 Dubbo 2.7.5 版本之后的最新优化和设计,让你紧跟时代潮流,更好地反馈到工作实践中。
本课程的每一个知识点都是你深入理解 Dubbo 的进步阶梯,整个分析 Dubbo 实现的过程,就是一步步到达山顶,成为高手的过程。你也可以通过目录,快速了解这个课程的知识体系结构。
讲师寄语
最后,我想和你说的是: 沉迷于代码,但不要只沉迷于代码。
阅读源码的目的是提升自身的技术能力,而提升技术能力的目的是更好地支持业务。阅读源码不是终点,你还需要结合实际业务,更好地体会开源项目的设计理念,并将这种设计应用到实践中。
让我们开启一次紧张刺激的 Dubbo 探秘之旅!我也希望你能在留言区与我分享你的 Dubbo 学习情况,分享你的成长心得和学习痛点,学习不是单向的输出,而是一次交流反馈的过程!加油。
为便于你更好地学习,我将整个 Dubbo 的源码(带注释的)放到 GitHub 上了你可以按需查看https://github.com/xxxlxy2008/dubbo。

View File

@@ -0,0 +1,396 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 Dubbo 源码环境搭建:千里之行,始于足下
好的开始是成功的一半,阅读源码也是一样。 很多同学在下定决心阅读一个开源框架之后,就一头扎进去,迷失在代码“迷宫”中。此时,有同学意识到,需要一边 Debug 一边看;然后又有一批同学在搭建源码环境的时候兜兜转转,走上了放弃之路;最后剩下为数不多的同学,搭建完了源码环境,却又不知道如何模拟请求让源码执行到自己想要 Debug 的地方。
以上这些痛点问题你是不是很熟悉?是不是也曾遇到过?没关系,本课时我就来手把手带领你搭建 Dubbo 源码环境。
在开始搭建源码环境之前,我们会先整体过一下 Dubbo 的架构,这可以帮助你了解 Dubbo 的基本功能以及核心角色。
之后我们再动手搭建 Dubbo 源码环境,构建一个 Demo 示例可运行的最简环境。
完成源码环境搭建之后,我们还会深入介绍 Dubbo 源码中各个核心模块的功能,这会为后续分析各个模块的实现做铺垫。
最后,我们再详细分析下 Dubbo 源码自带的三个 Demo 示例,简单回顾一下 Dubbo 的基本用法,这三个示例也将是我们后续 Debug 源码的入口。
Dubbo 架构简介
为便于你更好理解和学习,在开始搭建 Dubbo 源码环境之前,我们先来简单介绍一下 Dubbo 架构中的核心角色,帮助你简单回顾一下 Dubbo 的架构,也帮助不熟悉 Dubbo 的小伙伴快速了解 Dubbo。下图展示了 Dubbo 核心架构:
Dubbo 核心架构图
Registry注册中心。 负责服务地址的注册与查找,服务的 Provider 和 Consumer 只在启动时与注册中心交互。注册中心通过长连接感知 Provider 的存在,在 Provider 出现宕机的时候,注册中心会立即推送相关事件通知 Consumer。
Provider服务提供者。 在它启动的时候,会向 Registry 进行注册操作,将自己服务的地址和相关配置信息封装成 URL 添加到 ZooKeeper 中。
Consumer服务消费者。 在它启动的时候,会向 Registry 进行订阅操作。订阅操作会从 ZooKeeper 中获取 Provider 注册的 URL并在 ZooKeeper 中添加相应的监听器。获取到 Provider URL 之后Consumer 会根据负载均衡算法从多个 Provider 中选择一个 Provider 并与其建立连接,最后发起对 Provider 的 RPC 调用。 如果 Provider URL 发生变更Consumer 将会通过之前订阅过程中在注册中心添加的监听器,获取到最新的 Provider URL 信息,进行相应的调整,比如断开与宕机 Provider 的连接,并与新的 Provider 建立连接。Consumer 与 Provider 建立的是长连接,且 Consumer 会缓存 Provider 信息,所以一旦连接建立,即使注册中心宕机,也不会影响已运行的 Provider 和 Consumer。
Monitor监控中心。 用于统计服务的调用次数和调用时间。Provider 和 Consumer 在运行过程中,会在内存中统计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。监控中心在上面的架构图中并不是必要角色,监控中心宕机不会影响 Provider、Consumer 以及 Registry 的功能,只会丢失监控数据而已。
搭建Dubbo源码环境
当然要搭建Dubbo 源码环境,你首先需要下载源码。这里你可以直接从官方仓库 https://github.com/apache/dubboFork 到自己的仓库,直接执行下面的命令去下载代码:
git clone [email protected]:xxxxxxxx/dubbo.git
然后切换分支,因为目前最新的是 Dubbo 2.7.7 版本,所以这里我们就用这个新版本:
git checkout -b dubbo-2.7.7 dubbo-2.7.7
接下来,执行 mvn 命令进行编译:
mvn clean install -Dmaven.test.skip=true
最后,执行下面的命令转换成 IDEA 项目:
mvn idea:idea // 要是执行报错,就执行这个 mvn idea:workspace
然后,在 IDEA 中导入源码,因为这个导入过程中会下载所需的依赖包,所以会耗费点时间。
Dubbo源码核心模块
在 IDEA 成功导入 Dubbo 源码之后,你看到的项目结构如下图所示:
下面我们就来简单介绍一下这些核心模块的功能,至于详细分析,在后面的课时中我们还会继续讲解。
dubbo-common 模块: Dubbo 的一个公共模块,其中有很多工具类以及公共逻辑,例如课程后面紧接着要介绍的 Dubbo SPI 实现、时间轮实现、动态编译器等。
dubbo-remoting 模块: Dubbo 的远程通信模块,其中的子模块依赖各种开源组件实现远程通信。在 dubbo-remoting-api 子模块中定义该模块的抽象概念在其他子模块中依赖其他开源组件进行实现例如dubbo-remoting-netty4 子模块依赖 Netty 4 实现远程通信dubbo-remoting-zookeeper 通过 Apache Curator 实现与 ZooKeeper 集群的交互。
dubbo-rpc 模块: Dubbo 中对远程调用协议进行抽象的模块,其中抽象了各种协议,依赖于 dubbo-remoting 模块的远程调用功能。dubbo-rpc-api 子模块是核心抽象其他子模块是针对具体协议的实现例如dubbo-rpc-dubbo 子模块是对 Dubbo 协议的实现,依赖了 dubbo-remoting-netty4 等 dubbo-remoting 子模块。 dubbo-rpc 模块的实现中只包含一对一的调用,不关心集群的相关内容。
dubbo-cluster 模块: Dubbo 中负责管理集群的模块,提供了负载均衡、容错、路由等一系列集群相关的功能,最终的目的是将多个 Provider 伪装为一个 Provider这样 Consumer 就可以像调用一个 Provider 那样调用 Provider 集群了。
dubbo-registry 模块: Dubbo 中负责与多种开源注册中心进行交互的模块,提供注册中心的能力。其中, dubbo-registry-api 子模块是顶层抽象其他子模块是针对具体开源注册中心组件的具体实现例如dubbo-registry-zookeeper 子模块是 Dubbo 接入 ZooKeeper 的具体实现。
dubbo-monitor 模块: Dubbo 的监控模块,主要用于统计服务调用次数、调用时间以及实现调用链跟踪的服务。
dubbo-config 模块: Dubbo 对外暴露的配置都是由该模块进行解析的。例如dubbo-config-api 子模块负责处理 API 方式使用时的相关配置dubbo-config-spring 子模块负责处理与 Spring 集成使用时的相关配置方式。有了 dubbo-config 模块,用户只需要了解 Dubbo 配置的规则即可,无须了解 Dubbo 内部的细节。
dubbo-metadata 模块: Dubbo 的元数据模块本课程后续会详细介绍元数据的内容。dubbo-metadata 模块的实现套路也是有一个 api 子模块进行抽象,然后其他子模块进行具体实现。
dubbo-configcenter 模块: Dubbo 的动态配置模块,主要负责外部化配置以及服务治理规则的存储与通知,提供了多个子模块用来接入多种开源的服务发现组件。
Dubbo 源码中的 Demo 示例
在 Dubbo 源码中我们可以看到一个 dubbo-demo 模块,共包括三个非常基础 的 Dubbo 示例项目,分别是: 使用 XML 配置的 Demo 示例、使用注解配置的 Demo 示例 以及 直接使用 API 的 Demo 示例 。下面我们将从这三个示例的角度,简单介绍 Dubbo 的基本使用。同时,这三个项目也将作为后续 Debug Dubbo 源码的入口,我们会根据需要在其之上进行修改 。不过在这儿之前,你需要先启动 ZooKeeper 作为注册中心,然后编写一个业务接口作为 Provider 和 Consumer 的公约。
启动 ZooKeeper
在前面 Dubbo 的架构图中,你可以看到 Provider 的地址以及配置信息是通过注册中心传递给 Consumer 的。 Dubbo 支持的注册中心尽管有很多, 但在生产环境中, 基本都是用 ZooKeeper 作为注册中心 。因此,在调试 Dubbo 源码时,自然需要在本地启动 ZooKeeper。
那怎么去启动 ZooKeeper 呢?
首先,你得下载 zookeeper-3.4.14.tar.gz 包(下载地址: https://archive.apache.org/dist/zookeeper/zookeeper-3.4.14/)。下载完成之后执行如下命令解压缩:
tar -zxf zookeeper-3.4.14.tar.gz
解压完成之后,进入 zookeeper-3.4.14 目录,复制 conf/zoo_sample.cfg 文件并重命名为 conf/zoo.cfg之后执行如下命令就可以启动 ZooKeeper了。
>./bin/zkServer.sh start
# 下面为输出内容
ZooKeeper JMX enabled by default
Using config: /Users/xxx/zookeeper-3.4.14/bin/../conf/zoo.cfg # 配置文件
Starting zookeeper ... STARTED # 启动成功
业务接口
在使用 Dubbo 之前,你还需要一个业务接口,这个业务接口可以认为是 Dubbo Provider 和 Dubbo Consumer 的公约,反映出很多信息:
Provider ,如何提供服务、提供的服务名称是什么、需要接收什么参数、需要返回什么响应;
Consumer ,如何使用服务、使用的服务名称是什么、需要传入什么参数、会得到什么响应。
dubbo-demo-interface 模块就是定义业务接口的地方,如下图所示:
其中DemoService 接口中定义了两个方法:
public interface DemoService {
String sayHello(String name); // 同步调用
// 异步调用
default CompletableFuture<String> sayHelloAsync(String name) {
return CompletableFuture.completedFuture(sayHello(name));
}
}
Demo 1基于 XML 配置
在 dubbo-demo 模块下的 dubbo-demo-xml 模块,提供了基于 Spring XML 的 Provider 和 Consumer。
我们先来看 dubbo-demo-xml-provider 模块,其结构如下图所示:
在其 pom.xml 中除了一堆 dubbo 的依赖之外,还有依赖了 DemoService 这个公共接口:
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-demo-interface</artifactId>
<version>${project.parent.version}</version>
</dependency>
DemoServiceImpl 实现了 DemoService 接口sayHello() 方法直接返回一个字符串sayHelloAsync() 方法返回一个 CompletableFuture 对象。
在 dubbo-provider.xml 配置文件中,会将 DemoServiceImpl 配置成一个 Spring Bean并作为 DemoService 服务暴露出去:
<!-- 配置为 Spring Bean -->
<bean id="demoService" class="org.apache.dubbo.demo.provider.DemoServiceImpl"/>
<!-- 作为 Dubbo 服务暴露出去 -->
<dubbo:service interface="org.apache.dubbo.demo.DemoService" ref="demoService"/>
还有就是指定注册中心地址(就是前面 ZooKeeper 的地址),这样 Dubbo 才能把暴露的 DemoService 服务注册到 ZooKeeper 中:
<!-- Zookeeper 地址 -->
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
最后,在 Application 中写个 main() 方法,指定 Spring 配置文件并启动 ClassPathXmlApplicationContext 即可。
接下来再看 dubbo-demo-xml-consumer 模块,结构如下图所示:
在 pom.xml 中同样依赖了 dubbo-demo-interface 这个公共模块。
在 dubbo-consumer.xml 配置文件中,会指定注册中心地址(就是前面 ZooKeeper 的地址),这样 Dubbo 才能从 ZooKeeper 中拉取到 Provider 暴露的服务列表信息:
<!-- Zookeeper地址 -->
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
还会使用 dubbo:reference 引入 DemoService 服务,后面可以作为 Spring Bean 使用:
<!--引入DemoService服务并配置成Spring Bean-->
<dubbo:reference id="demoService" check="false"
interface="org.apache.dubbo.demo.DemoService"/>
最后,在 Application 中写个 main() 方法,指定 Spring 配置文件并启动 ClassPathXmlApplicationContext 之后,就可以远程调用 Provider 端的 DemoService 的 sayHello() 方法了。
Demo 2基于注解配置
dubbo-demo-annotation 模块是基于 Spring 注解配置的示例,无非就是将 XML 的那些配置信息转移到了注解上。
我们先来看 dubbo-demo-annotation-provider 这个示例模块:
public class Application {
public static void main(String[] args) throws Exception {
// 使用AnnotationConfigApplicationContext初始化Spring容器
// 从ProviderConfiguration这个类的注解上拿相关配置信息
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(
ProviderConfiguration.class);
context.start();
System.in.read();
}
@Configuration // 配置类
// @EnableDubbo注解指定包下的Bean都会被扫描并做Dubbo服务暴露出去
@EnableDubbo(scanBasePackages = "org.apache.dubbo.demo.provider")
// @PropertySource注解指定了其他配置信息
@PropertySource("classpath:/spring/dubbo-provider.properties")
static class ProviderConfiguration {
@Bean
public RegistryConfig registryConfig() {
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://127.0.0.1:2181");
return registryConfig;
}
}
}
这里,同样会有一个 DemoServiceImpl 实现了 DemoService 接口,并且在 org.apache.dubbo.demo.provider 目录下,能被扫描到,暴露成 Dubbo 服务。
接着再来看 dubbo-demo-annotation-consumer 模块,其中 Application 中也是通过 AnnotationConfigApplicationContext 初始化 Spring 容器,也会扫描指定目录下的 Bean会扫到 DemoServiceComponent 这个 Bean其中就通过 @Reference 注解注入 Dubbo 服务相关的 Bean
@Component("demoServiceComponent")
public class DemoServiceComponent implements DemoService {
@Reference // 注入Dubbo服务
private DemoService demoService;
@Override
public String sayHello(String name) {
return demoService.sayHello(name);
}
// 其他方法
}
Demo 3基于 API 配置
在有的场景中,不能依赖于 Spring 框架,只能使用 API 来构建 Dubbo Provider 和 Consumer比较典型的一种场景就是在写 SDK 的时候。
先来看 dubbo-demo-api-provider 模块,其中 Application.main() 方法是入口:
// 创建一个ServiceConfig的实例泛型参数是业务接口实现类
// 即DemoServiceImpl
ServiceConfig<DemoServiceImpl> service = new ServiceConfig<>();
// 指定业务接口
service.setInterface(DemoService.class);
// 指定业务接口的实现由该对象来处理Consumer的请求
service.setRef(new DemoServiceImpl());
// 获取DubboBootstrap实例这是个单例的对象
DubboBootstrap bootstrap = DubboBootstrap.getInstance();
//生成一个 ApplicationConfig 的实例、指定ZK地址以及ServiceConfig实例
bootstrap.application(new ApplicationConfig("dubbo-demo-api-provider"))
.registry(new RegistryConfig("zookeeper://127.0.0.1:2181"))
.service(service)
.start()
.await();
这里,同样会有一个 DemoServiceImpl 实现了 DemoService 接口,并且在 org.apache.dubbo.demo.provider 目录下,能被扫描到,暴露成 Dubbo 服务。
再来看 dubbo-demo-api-consumer 模块,其中 Application 中包含一个普通的 main() 方法入口:
// 创建ReferenceConfig,其中指定了引用的接口DemoService
ReferenceConfig<DemoService> reference = new ReferenceConfig<>();
reference.setInterface(DemoService.class);
reference.setGeneric("true");
// 创建DubboBootstrap指定ApplicationConfig以及RegistryConfig
DubboBootstrap bootstrap = DubboBootstrap.getInstance();
bootstrap.application(new ApplicationConfig("dubbo-demo-api-consumer"))
.registry(new RegistryConfig("zookeeper://127.0.0.1:2181"))
.reference(reference)
.start();
// 获取DemoService实例并调用其方法
DemoService demoService = ReferenceConfigCache.getCache()
.get(reference);
String message = demoService.sayHello("dubbo");
System.out.println(message);
总结
在本课时,我们首先介绍了 Dubbo 的核心架构以及各核心组件的功能,接下来又搭建了 Dubbo 源码环境,并详细介绍了 Dubbo 核心模块的功能,为后续分析 Dubbo 源码打下了基础。最后我们还深入分析了 Dubbo 源码中自带的三个 Demo 示例,现在你就可以以这三个 Demo 示例为入口 Debug Dubbo 源码了。
在后面的课时中我们将解决几个问题Dubbo 是如何与 ZooKeeper 等注册中心进行交互的Provider 与 Consumer 之间是如何交互的为什么我们在编写业务代码的时候感受不到任何网络交互Dubbo Provider 发布到注册中心的数据是什么Consumer 为何能正确识别?两者的统一契约是什么?这个契约是如何做到可扩展的?这个契约还会用在 Dubbo 的哪些地方?这些问题你也可以提前思考一下,在后面的课程中我会一一为你解答。

View File

@@ -0,0 +1,222 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
02 Dubbo 的配置总线:抓住 URL就理解了半个 Dubbo
你好,我是杨四正,今天我和你分享的主题是 Dubbo 的配置总线:抓住 URL就理解了半个 Dubbo 。
在互联网领域,每个信息资源都有统一的且在网上唯一的地址,该地址就叫 URLUniform Resource Locator统一资源定位符它是互联网的统一资源定位标志也就是指网络地址。
URL 本质上就是一个特殊格式的字符串。一个标准的 URL 格式可以包含如下的几个部分:
protocol://username:password@host:port/path?key=value&key=value
protocolURL 的协议。我们常见的就是 HTTP 协议和 HTTPS 协议,当然,还有其他协议,如 FTP 协议、SMTP 协议等。
username/password用户名/密码。 HTTP Basic Authentication 中多会使用在 URL 的协议之后直接携带用户名和密码的方式。
host/port主机/端口。在实践中一般会使用域名,而不是使用具体的 host 和 port。
path请求的路径。
parameters参数键值对。一般在 GET 请求中会将参数放到 URL 中POST 请求会将参数放到请求体中。
URL 是整个 Dubbo 中非常基础,也是非常核心的一个组件,阅读源码的过程中你会发现很多方法都是以 URL 作为参数的,在方法内部解析传入的 URL 得到有用的参数,所以有人将 URL 称为Dubbo 的配置总线。
例如,在下一课时介绍的 Dubbo SPI 核心实现中,你会看到 URL 参与了扩展实现的确定;在本课程后续介绍注册中心实现的时候,你还会看到 Provider 将自身的信息封装成 URL 注册到 ZooKeeper 中,从而暴露自己的服务, Consumer 也是通过 URL 来确定自己订阅了哪些 Provider 的。
由此可见URL 之于 Dubbo 是非常重要的,所以说“抓住 URL就理解了半个 Dubbo”。那本文我们就来介绍 URL 在 Dubbo 中的应用,以及 URL 作为 Dubbo 统一契约的重要性,最后我们再通过示例说明 URL 在 Dubbo 中的具体应用。
Dubbo 中的 URL
Dubbo 中任意的一个实现都可以抽象为一个 URLDubbo 使用 URL 来统一描述了所有对象和配置信息,并贯穿在整个 Dubbo 框架之中。这里我们来看 Dubbo 中一个典型 URL 的示例,如下:
dubbo://172.17.32.91:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-api-provider&dubbo=2.0.2&interface=org.apache.dubbo.demo.DemoService&methods=sayHello,sayHelloAsync&pid=32508&release=&side=provider&timestamp=1593253404714dubbo://172.17.32.91:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-api-provider&dubbo=2.0.2&interface=org.apache.dubbo.demo.DemoService&methods=sayHello,sayHelloAsync&pid=32508&release=&side=provider&timestamp=1593253404714
这个 Demo Provider 注册到 ZooKeeper 上的 URL 信息,简单解析一下这个 URL 的各个部分:
protocoldubbo 协议。
username/password没有用户名和密码。
host/port172.17.32.91:20880。
pathorg.apache.dubbo.demo.DemoService。
parameters参数键值对这里是问号后面的参数。
下面是 URL 的构造方法,你可以看到其核心字段与前文分析的 URL 基本一致:
public URL(String protocol,
String username,
String password,
String host,
int port,
String path,
Map<String, String> parameters,
Map<String, Map<String, String>> methodParameters) {
if (StringUtils.isEmpty(username)
&& StringUtils.isNotEmpty(password)) {
throw new IllegalArgumentException("Invalid url");
}
this.protocol = protocol;
this.username = username;
this.password = password;
this.host = host;
this.port = Math.max(port, 0);
this.address = getAddress(this.host, this.port);
while (path != null && path.startsWith("/")) {
path = path.substring(1);
}
this.path = path;
if (parameters == null) {
parameters = new HashMap<>();
} else {
parameters = new HashMap<>(parameters);
}
this.parameters = Collections.unmodifiableMap(parameters);
this.methodParameters = Collections.unmodifiableMap(methodParameters);
}
另外,在 dubbo-common 包中还提供了 URL 的辅助类:
URLBuilder 辅助构造 URL
URLStrParser 将字符串解析成 URL 对象。
契约的力量
对于 Dubbo 中的 URL很多人称之为“配置总线”也有人称之为“统一配置模型”。虽然说法不同但都是在表达一个意思URL 在 Dubbo 中被当作是“公共的契约”。一个 URL 可以包含非常多的扩展点参数URL 作为上下文信息贯穿整个扩展点设计体系。
其实,一个优秀的开源产品都有一套灵活清晰的扩展契约,不仅是第三方可以按照这个契约进行扩展,其自身的内核也可以按照这个契约进行搭建。如果没有一个公共的契约,只是针对每个接口或方法进行约定,就会导致不同的接口甚至同一接口中的不同方法,以不同的参数类型进行传参,一会儿传递 Map一会儿传递字符串而且字符串的格式也不确定需要你自己进行解析这就多了一层没有明确表现出来的隐含的约定。
所以说,在 Dubbo 中使用 URL 的好处多多,增加了便捷性:
使用 URL 这种公共契约进行上下文信息传递,最重要的就是代码更加易读、易懂,不用花大量时间去揣测传递数据的格式和含义,进而形成一个统一的规范,使得代码易写、易读。
使用 URL 作为方法的入参(相当于一个 Key/Value 都是 String 的 Map),它所表达的含义比单个参数更丰富,当代码需要扩展的时候,可以将新的参数以 Key/Value 的形式追加到 URL 之中,而不需要改变入参或是返回值的结构。
使用 URL 这种“公共的契约”可以简化沟通,人与人之间的沟通消耗是非常大的,信息传递的效率非常低,使用统一的契约、术语、词汇范围,可以省去很多沟通成本,尽可能地提高沟通效率。
Dubbo 中的 URL 示例
了解了 URL 的结构以及 Dubbo 使用 URL 的原因之后,我们再来看 Dubbo 中的三个真实示例,进一步感受 URL 的重要性。
1. URL 在 SPI 中的应用
Dubbo SPI 中有一个依赖 URL 的重要场景——适配器方法,是被 @Adaptive 注解标注的, URL 一个很重要的作用就是与 @Adaptive 注解一起选择合适的扩展实现类。
例如,在 dubbo-registry-api 模块中我们可以看到 RegistryFactory 这个接口,其中的 getRegistry() 方法上有 @Adaptive({“protocol”}) 注解说明这是一个适配器方法Dubbo 在运行时会为其动态生成相应的 “$Adaptive” 类型,如下所示:
public class RegistryFactory$Adaptive
implements RegistryFactory {
public Registry getRegistry(org.apache.dubbo.common.URL arg0) {
if (arg0 == null) throw new IllegalArgumentException("...");
org.apache.dubbo.common.URL url = arg0;
// 尝试获取URL的Protocol如果Protocol为空则使用默认值"dubbo"
String extName = (url.getProtocol() == null ? "dubbo" :
url.getProtocol());
if (extName == null)
throw new IllegalStateException("...");
// 根据扩展名选择相应的扩展实现Dubbo SPI的核心原理在下一课时深入分析
RegistryFactory extension = (RegistryFactory) ExtensionLoader
.getExtensionLoader(RegistryFactory.class)
.getExtension(extName);
return extension.getRegistry(arg0);
}
}
我们会看到,在生成的 RegistryFactory$Adaptive 类中会自动实现 getRegistry() 方法,其中会根据 URL 的 Protocol 确定扩展名称,从而确定使用的具体扩展实现类。我们可以找到 RegistryProtocol 这个类,并在其 getRegistry() 方法中打一个断点, Debug 启动上一课时介绍的任意一个 Demo 示例中的 Provider得到如下图所示的内容
这里传入的 registryUrl 值为:
zookeeper://127.0.0.1:2181/org.apache.dubbo...
那么在 RegistryFactory$Adaptive 中得到的扩展名称为 zookeeper此次使用的 Registry 扩展实现类就是 ZookeeperRegistryFactory。至于 Dubbo SPI 的完整内容,我们将在下一课时详细介绍,这里就不再展开了。
2. URL 在服务暴露中的应用
我们再来看另一个与 URL 相关的示例。上一课时我们在介绍 Dubbo 的简化架构时提到Provider 在启动时,会将自身暴露的服务注册到 ZooKeeper 上,具体是注册哪些信息到 ZooKeeper 上呢?我们来看 ZookeeperRegistry.doRegister() 方法,在其中打个断点,然后 Debug 启动 Provider会得到下图
传入的 URL 中包含了 Provider 的地址172.18.112.15:20880、暴露的接口org.apache.dubbo.demo.DemoService等信息 toUrlPath() 方法会根据传入的 URL 参数确定在 ZooKeeper 上创建的节点路径,还会通过 URL 中的 dynamic 参数值确定创建的 ZNode 是临时节点还是持久节点。
3. URL 在服务订阅中的应用
Consumer 启动后会向注册中心进行订阅操作,并监听自己关注的 Provider。那 Consumer 是如何告诉注册中心自己关注哪些 Provider 呢?
我们来看 ZookeeperRegistry 这个实现类,它是由上面的 ZookeeperRegistryFactory 工厂类创建的 Registry 接口实现,其中的 doSubscribe() 方法是订阅操作的核心实现,在第 175 行打一个断点,并 Debug 启动 Demo 中 Consumer会得到下图所示的内容
我们看到传入的 URL 参数如下:
consumer://...?application=dubbo-demo-api-consumer&category=providers,configurators,routers&interface=org.apache.dubbo.demo.DemoService...
其中 Protocol 为 consumer ,表示是 Consumer 的订阅协议,其中的 category 参数表示要订阅的分类,这里要订阅 providers、configurators 以及 routers 三个分类interface 参数表示订阅哪个服务接口,这里要订阅的是暴露 org.apache.dubbo.demo.DemoService 实现的 Provider。
通过 URL 中的上述参数ZookeeperRegistry 会在 toCategoriesPath() 方法中将其整理成一个 ZooKeeper 路径,然后调用 zkClient 在其上添加监听。
通过上述示例,相信你已经感觉到 URL 在 Dubbo 体系中称为“总线”或是“契约”的原因了,在后面的源码分析中,我们还将看到更多关于 URL 的实现。
总结
在本课时,我们重点介绍了 Dubbo 对 URL 的封装以及相关的工具类,然后说明了统一契约的好处,当然也是 Dubbo 使用 URL 作为统一配置总线的好处,最后我们还介绍了 Dubbo SPI、Provider 注册、Consumer 订阅等场景中与 URL 相关的实现,这些都可以帮助你更好地感受 URL 在其中发挥的作用。
这里你可以想一下,在其他框架或是实际工作中,有没有类似 Dubbo URL 这种统一的契约?欢迎你在留言区分享你的想法。

View File

@@ -0,0 +1,353 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
03 Dubbo SPI 精析,接口实现两极反转(上)
Dubbo 为了更好地达到 OCP 原则(即“对扩展开放,对修改封闭”的原则),采用了“微内核+插件”的架构。那什么是微内核架构呢微内核架构也被称为插件化架构Plug-in Architecture这是一种面向功能进行拆分的可扩展性架构。内核功能是比较稳定的只负责管理插件的生命周期不会因为系统功能的扩展而不断进行修改。功能上的扩展全部封装到插件之中插件模块是独立存在的模块包含特定的功能能拓展内核系统的功能。
微内核架构中,内核通常采用 Factory、IoC、OSGi 等方式管理插件生命周期Dubbo 最终决定采用 SPI 机制来加载插件Dubbo SPI 参考 JDK 原生的 SPI 机制,进行了性能优化以及功能增强。因此,在讲解 Dubbo SPI 之前,我们有必要先来介绍一下 JDK SPI 的工作原理。
JDK SPI
SPIService Provider Interface主要是被框架开发人员使用的一种技术。例如使用 Java 语言访问数据库时我们会使用到 java.sql.Driver 接口,不同数据库产品底层的协议不同,提供的 java.sql.Driver 实现也不同,在开发 java.sql.Driver 接口时,开发人员并不清楚用户最终会使用哪个数据库,在这种情况下就可以使用 Java SPI 机制在实际运行过程中,为 java.sql.Driver 接口寻找具体的实现。
1. JDK SPI 机制
当服务的提供者提供了一种接口的实现之后,需要在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,此文件记录了该 jar 包提供的服务接口的具体实现类。当某个应用引入了该 jar 包且需要使用该服务时JDK SPI 机制就可以通过查找这个 jar 包的 META-INF/services/ 中的配置文件来获得具体的实现类名,进行实现类的加载和实例化,最终使用该实现类完成业务功能。
下面我们通过一个简单的示例演示下 JDK SPI 的基本使用方式:
.png]
首先我们需要创建一个 Log 接口,来模拟日志打印的功能:
public interface Log {
void log(String info);
}
接下来提供两个实现—— Logback 和 Log4j分别代表两个不同日志框架的实现如下所示
public class Logback implements Log {
@Override
public void log(String info) {
System.out.println("Logback:" + info);
}
}
public class Log4j implements Log {
@Override
public void log(String info) {
System.out.println("Log4j:" + info);
}
}
在项目的 resources/META-INF/services 目录下添加一个名为 com.xxx.Log 的文件,这是 JDK SPI 需要读取的配置文件,具体内容如下:
com.xxx.impl.Log4j
com.xxx.impl.Logback
最后创建 main() 方法,其中会加载上述配置文件,创建全部 Log 接口实现的实例,并执行其 log() 方法,如下所示:
public class Main {
public static void main(String[] args) {
ServiceLoader<Log> serviceLoader =
ServiceLoader.load(Log.class);
Iterator<Log> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
Log log = iterator.next();
log.log("JDK SPI");
}
}
}
// 输出如下:
// Log4j:JDK SPI
// Logback:JDK SPI
2. JDK SPI 源码分析
通过上述示例,我们可以看到 JDK SPI 的入口方法是 ServiceLoader.load() 方法,接下来我们就对其具体实现进行深入分析。
在 ServiceLoader.load() 方法中,首先会尝试获取当前使用的 ClassLoader获取当前线程绑定的 ClassLoader查找失败后使用 SystemClassLoader然后调用 reload() 方法,调用关系如下图所示:
在 reload() 方法中,首先会清理 providers 缓存LinkedHashMap 类型的集合),该缓存用来记录 ServiceLoader 创建的实现对象,其中 Key 为实现类的完整类名Value 为实现类的对象。之后创建 LazyIterator 迭代器,用于读取 SPI 配置文件并实例化实现类对象。
ServiceLoader.reload() 方法的具体实现,如下所示:
// 缓存,用来缓存 ServiceLoader创建的实现对象
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
public void reload() {
providers.clear(); // 清空缓存
lookupIterator = new LazyIterator(service, loader); // 迭代器
}
在前面的示例中main() 方法中使用的迭代器底层就是调用了 ServiceLoader.LazyIterator 实现的。Iterator 接口有两个关键方法hasNext() 方法和 next() 方法。这里的 LazyIterator 中的next() 方法最终调用的是其 nextService() 方法hasNext() 方法最终调用的是 hasNextService() 方法,调用关系如下图所示:
首先来看 LazyIterator.hasNextService() 方法,该方法主要负责查找 META-INF/services 目录下的 SPI 配置文件,并进行遍历,大致实现如下所示:
private static final String PREFIX = "META-INF/services/";
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
// PREFIX前缀与服务接口的名称拼接起来就是META-INF目录下定义的SPI配
// 置文件(即示例中的META-INF/services/com.xxx.Log)
String fullName = PREFIX + service.getName();
// 加载配置文件
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
}
// 按行SPI遍历配置文件的内容
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 解析配置文件
pending = parse(service, configs.nextElement());
}
nextName = pending.next(); // 更新 nextName字段
return true;
}
在 hasNextService() 方法中完成 SPI 配置文件的解析之后,再来看 LazyIterator.nextService() 方法,该方法负责实例化 hasNextService() 方法读取到的实现类,其中会将实例化的对象放到 providers 集合中缓存起来,核心实现如下所示:
private S nextService() {
String cn = nextName;
nextName = null;
// 加载 nextName字段指定的类
Class<?> c = Class.forName(cn, false, loader);
if (!service.isAssignableFrom(c)) { // 检测类型
fail(service, "Provider " + cn + " not a subtype");
}
S p = service.cast(c.newInstance()); // 创建实现类的对象
providers.put(cn, p); // 将实现类名称以及相应实例对象添加到缓存
return p;
}
以上就是在 main() 方法中使用的迭代器的底层实现。最后,我们再来看一下 main() 方法中使用ServiceLoader.iterator() 方法拿到的迭代器是如何实现的,这个迭代器是依赖 LazyIterator 实现的一个匿名内部类,核心实现如下:
public Iterator<S> iterator() {
return new Iterator<S>() {
// knownProviders用来迭代providers缓存
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
// 先走查询缓存缓存查询失败再通过LazyIterator加载
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
// 先走查询缓存,缓存查询失败,再通过 LazyIterator加载
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
// 省略remove()方法
};
}
3. JDK SPI 在 JDBC 中的应用
了解了 JDK SPI 实现的原理之后,我们再来看实践中 JDBC 是如何使用 JDK SPI 机制加载不同数据库厂商的实现类。
JDK 中只定义了一个 java.sql.Driver 接口,具体的实现是由不同数据库厂商来提供的。这里我们就以 MySQL 提供的 JDBC 实现包为例进行分析。
在 mysql-connector-java-*.jar 包中的 META-INF/services 目录下,有一个 java.sql.Driver 文件中只有一行内容,如下所示:
com.mysql.cj.jdbc.Driver
在使用 mysql-connector-java-*.jar 包连接 MySQL 数据库的时候,我们会用到如下语句创建数据库连接:
String url = "jdbc:xxx://xxx:xxx/xxx";
Connection conn = DriverManager.getConnection(url, username, pwd);
DriverManager 是 JDK 提供的数据库驱动管理器,其中的代码片段,如下所示:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
在调用 getConnection() 方法的时候DriverManager 类会被 Java 虚拟机加载、解析并触发 static 代码块的执行;在 loadInitialDrivers() 方法中通过 JDK SPI 扫描 Classpath 下 java.sql.Driver 接口实现类并实例化,核心实现如下所示:
private static void loadInitialDrivers() {
String drivers = System.getProperty("jdbc.drivers")
// 使用 JDK SPI机制加载所有 java.sql.Driver实现类
ServiceLoader<Driver> loadedDrivers =
ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while(driversIterator.hasNext()) {
driversIterator.next();
}
String[] driversList = drivers.split(":");
for (String aDriver : driversList) { // 初始化Driver实现类
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
}
}
在 MySQL 提供的 com.mysql.cj.jdbc.Driver 实现类中,同样有一段 static 静态代码块,这段代码会创建一个 com.mysql.cj.jdbc.Driver 对象并注册到 DriverManager.registeredDrivers 集合中CopyOnWriteArrayList 类型),如下所示:
static {
java.sql.DriverManager.registerDriver(new Driver());
}
在 getConnection() 方法中DriverManager 从该 registeredDrivers 集合中获取对应的 Driver 对象创建 Connection核心实现如下所示
private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
// 省略 try/catch代码块以及权限处理逻辑
for(DriverInfo aDriver : registeredDrivers) {
Connection con = aDriver.driver.connect(url, info);
return con;
}
}
总结
本文我们通过一个示例入手,介绍了 JDK 提供的 SPI 机制的基本使用,然后深入分析了 JDK SPI 的核心原理和底层实现,对其源码进行了深入剖析,最后我们以 MySQL 提供的 JDBC 实现为例,分析了 JDK SPI 在实践中的使用方式。
JDK SPI 机制虽然简单易用,但是也存在一些小瑕疵,你可以先思考一下,在下一课时剖析 Dubbo SPI 机制的时候,我会为你解答该问题。

View File

@@ -0,0 +1,659 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04 Dubbo SPI 精析,接口实现两极反转(下)
在上一课时,我们一起学习了 JDK SPI 的基础使用以及核心原理,不过 Dubbo 并没有直接使用 JDK SPI 机制,而是借鉴其思想,实现了自身的一套 SPI 机制,这就是本课时将重点介绍的内容。
Dubbo SPI
在开始介绍 Dubbo SPI 实现之前,我们先来统一下面两个概念。
扩展点:通过 SPI 机制查找并加载实现的接口(又称“扩展接口”)。前文示例中介绍的 Log 接口、com.mysql.cj.jdbc.Driver 接口,都是扩展点。
扩展点实现:实现了扩展接口的实现类。
通过前面的分析可以发现JDK SPI 在查找扩展实现类的过程中,需要遍历 SPI 配置文件中定义的所有实现类,该过程中会将这些实现类全部实例化。如果 SPI 配置文件中定义了多个实现类而我们只需要使用其中一个实现类时就会生成不必要的对象。例如org.apache.dubbo.rpc.Protocol 接口有 InjvmProtocol、DubboProtocol、RmiProtocol、HttpProtocol、HessianProtocol、ThriftProtocol 等多个实现,如果使用 JDK SPI就会加载全部实现类导致资源的浪费。
Dubbo SPI 不仅解决了上述资源浪费的问题,还对 SPI 配置文件扩展和修改。
首先Dubbo 按照 SPI 配置文件的用途,将其分成了三类目录。
META-INF/services/ 目录:该目录下的 SPI 配置文件用来兼容 JDK SPI 。
META-INF/dubbo/ 目录:该目录用于存放用户自定义 SPI 配置文件。
META-INF/dubbo/internal/ 目录:该目录用于存放 Dubbo 内部使用的 SPI 配置文件。
然后Dubbo 将 SPI 配置文件改成了 KV 格式,例如:
dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
其中 key 被称为扩展名(也就是 ExtensionName当我们在为一个接口查找具体实现类时可以指定扩展名来选择相应的扩展实现。例如这里指定扩展名为 dubboDubbo SPI 就知道我们要使用org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol 这个扩展实现类,只实例化这一个扩展实现即可,无须实例化 SPI 配置文件中的其他扩展实现类。
使用 KV 格式的 SPI 配置文件的另一个好处是:让我们更容易定位到问题。假设我们使用的一个扩展实现类所在的 jar 包没有引入到项目中,那么 Dubbo SPI 在抛出异常的时候,会携带该扩展名信息,而不是简单地提示扩展实现类无法加载。这些更加准确的异常信息降低了排查问题的难度,提高了排查问题的效率。
下面我们正式进入 Dubbo SPI 核心实现的介绍。
1. @SPI 注解
Dubbo 中某个接口被 @SPI注解修饰时,就表示该接口是扩展接口,前文示例中的 org.apache.dubbo.rpc.Protocol 接口就是一个扩展接口:
@SPI 注解的 value 值指定了默认的扩展名称,例如,在通过 Dubbo SPI 加载 Protocol 接口实现时,如果没有明确指定扩展名,则默认会将 @SPI 注解的 value 值作为扩展名,即加载 dubbo 这个扩展名对应的 org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol 这个扩展实现类,相关的 SPI 配置文件在 dubbo-rpc-dubbo 模块中,如下图所示:
那 ExtensionLoader 是如何处理 @SPI 注解的呢?
ExtensionLoader 位于 dubbo-common 模块中的 extension 包中,功能类似于 JDK SPI 中的 java.util.ServiceLoader。Dubbo SPI 的核心逻辑几乎都封装在 ExtensionLoader 之中(其中就包括 @SPI 注解的处理逻辑),其使用方式如下所示:
Protocol protocol = ExtensionLoader
.getExtensionLoader(Protocol.class).getExtension("dubbo");
这里首先来了解一下 ExtensionLoader 中三个核心的静态字段。
strategiesLoadingStrategy[]类型): LoadingStrategy 接口有三个实现(通过 JDK SPI 方式加载的),如下图所示,分别对应前面介绍的三个 Dubbo SPI 配置文件所在的目录,且都继承了 Prioritized 这个优先级接口,默认优先级是
DubboInternalLoadingStrategy > DubboLoadingStrategy > ServicesLoadingStrateg
EXTENSION_LOADERSConcurrentMap类型
Dubbo 中一个扩展接口对应一个 ExtensionLoader 实例,该集合缓存了全部 ExtensionLoader 实例,其中的 Key 为扩展接口Value 为加载其扩展实现的 ExtensionLoader 实例。
EXTENSION_INSTANCESConcurrentMap, Object>类型该集合缓存了扩展实现类与其实例对象的映射关系。在前文示例中Key 为 ClassValue 为 DubboProtocol 对象。
下面我们再来关注一下 ExtensionLoader 的实例字段。
typeClass<?>类型):当前 ExtensionLoader 实例负责加载扩展接口。
cachedDefaultNameString类型记录了 type 这个扩展接口上 @SPI 注解的 value 值,也就是默认扩展名。
cachedNamesConcurrentMap, String>类型):缓存了该 ExtensionLoader 加载的扩展实现类与扩展名之间的映射关系。
cachedClassesHolder>>类型):缓存了该 ExtensionLoader 加载的扩展名与扩展实现类之间的映射关系。cachedNames 集合的反向关系缓存。
cachedInstancesConcurrentMap>类型):缓存了该 ExtensionLoader 加载的扩展名与扩展实现对象之间的映射关系。
ExtensionLoader.getExtensionLoader() 方法会根据扩展接口从 EXTENSION_LOADERS 缓存中查找相应的 ExtensionLoader 实例,核心实现如下:
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
ExtensionLoader<T> loader =
(ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type,
new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
得到接口对应的 ExtensionLoader 对象之后会调用其 getExtension() 方法,根据传入的扩展名称从 cachedInstances 缓存中查找扩展实现的实例,最终将其实例化后返回:
public T getExtension(String name) {
// getOrCreateHolder()方法中封装了查找cachedInstances缓存的逻辑
Holder<Object> holder = getOrCreateHolder(name);
Object instance = holder.get();
if (instance == null) { // double-check防止并发问题
synchronized (holder) {
instance = holder.get();
if (instance == null) {
// 根据扩展名从SPI配置文件中查找对应的扩展实现类
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
在 createExtension() 方法中完成了 SPI 配置文件的查找以及相应扩展实现类的实例化,同时还实现了自动装配以及自动 Wrapper 包装等功能。其核心流程是这样的:
获取 cachedClasses 缓存,根据扩展名从 cachedClasses 缓存中获取扩展实现类。如果 cachedClasses 未初始化,则会扫描前面介绍的三个 SPI 目录获取查找相应的 SPI 配置文件,然后加载其中的扩展实现类,最后将扩展名和扩展实现类的映射关系记录到 cachedClasses 缓存中。这部分逻辑在 loadExtensionClasses() 和 loadDirectory() 方法中。
根据扩展实现类从 EXTENSION_INSTANCES 缓存中查找相应的实例。如果查找失败,会通过反射创建扩展实现对象。
自动装配扩展实现对象中的属性(即调用其 setter。这里涉及 ExtensionFactory 以及自动装配的相关内容,本课时后面会进行详细介绍。
自动包装扩展实现对象。这里涉及 Wrapper 类以及自动包装特性的相关内容,本课时后面会进行详细介绍。
如果扩展实现类实现了 Lifecycle 接口,在 initExtension() 方法中会调用 initialize() 方法进行初始化。
private T createExtension(String name) {
Class<?> clazz = getExtensionClasses().get(name); // --- 1
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz); // --- 2
if (instance == null) {
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
injectExtension(instance); // --- 3
Set<Class<?>> wrapperClasses = cachedWrapperClasses; // --- 4
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
for (Class<?> wrapperClass : wrapperClasses) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
initExtension(instance); // ---5
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
type + ") couldn't be instantiated: " + t.getMessage(), t);
}
}
2. @Adaptive 注解与适配器
@Adaptive 注解用来实现 Dubbo 的适配器功能那什么是适配器呢这里我们通过一个示例进行说明。Dubbo 中的 ExtensionFactory 接口有三个实现类如下图所示ExtensionFactory 接口上有 @SPI 注解AdaptiveExtensionFactory 实现类上有 @Adaptive 注解。
AdaptiveExtensionFactory 不实现任何具体的功能,而是用来适配 ExtensionFactory 的 SpiExtensionFactory 和 SpringExtensionFactory 这两种实现。AdaptiveExtensionFactory 会根据运行时的一些状态来选择具体调用 ExtensionFactory 的哪个实现。
@Adaptive 注解还可以加到接口方法之上Dubbo 会动态生成适配器类。例如Transporter接口有两个被 @Adaptive 注解修饰的方法:
@SPI("netty")
public interface Transporter {
@Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
RemotingServer bind(URL url, ChannelHandler handler) throws RemotingException;
@Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
Client connect(URL url, ChannelHandler handler) throws RemotingException;
}
Dubbo 会生成一个 Transporter$Adaptive 适配器类,该类继承了 Transporter 接口:
public class Transporter$Adaptive implements Transporter {
public org.apache.dubbo.remoting.Client connect(URL arg0, ChannelHandler arg1) throws RemotingException {
// 必须传递URL参数
if (arg0 == null) throw new IllegalArgumentException("url == null");
URL url = arg0;
// 确定扩展名优先从URL中的client参数获取其次是transporter参数
// 这两个参数名称由@Adaptive注解指定,最后是@SPI注解中的默认值
String extName = url.getParameter("client",
url.getParameter("transporter", "netty"));
if (extName == null)
throw new IllegalStateException("...");
// 通过ExtensionLoader加载Transporter接口的指定扩展实现
Transporter extension = (Transporter) ExtensionLoader
.getExtensionLoader(Transporter.class)
.getExtension(extName);
return extension.connect(arg0, arg1);
}
... // 省略bind()方法
}
生成 Transporter$Adaptive 这个类的逻辑位于 ExtensionLoader.createAdaptiveExtensionClass() 方法,若感兴趣你可以看一下相关代码,其中涉及的 javassist 等方面的知识,在后面的课时中我们会进行介绍。
明确了 @Adaptive 注解的作用之后,我们回到 ExtensionLoader.createExtension() 方法,其中在扫描 SPI 配置文件的时候,会调用 loadClass() 方法加载 SPI 配置文件中指定的类,如下图所示:
loadClass() 方法中会识别加载扩展实现类上的 @Adaptive 注解,将该扩展实现的类型缓存到 cachedAdaptiveClass 这个实例字段上volatile修饰
private void loadClass(){
if (clazz.isAnnotationPresent(Adaptive.class)) {
// 缓存到cachedAdaptiveClass字段
cacheAdaptiveClass(clazz, overridden);
} else ... // 省略其他分支
}
我们可以通过 ExtensionLoader.getAdaptiveExtension() 方法获取适配器实例,并将该实例缓存到 cachedAdaptiveInstance 字段Holder类型核心流程如下
首先,检查 cachedAdaptiveInstance 字段中是否已缓存了适配器实例,如果已缓存,则直接返回该实例即可。
然后,调用 getExtensionClasses() 方法,其中就会触发前文介绍的 loadClass() 方法,完成 cachedAdaptiveClass 字段的填充。
如果存在 @Adaptive 注解修饰的扩展实现类,该类就是适配器类,通过 newInstance() 将其实例化即可。如果不存在 @Adaptive 注解修饰的扩展实现类,就需要通过 createAdaptiveExtensionClass() 方法扫描扩展接口中方法上的 @Adaptive 注解,动态生成适配器类,然后实例化。
接下来,调用 injectExtension() 方法进行自动装配,就能得到一个完整的适配器实例。
最后,将适配器实例缓存到 cachedAdaptiveInstance 字段,然后返回适配器实例。
getAdaptiveExtension() 方法的流程涉及多个方法,这里不再粘贴代码,感兴趣的同学可以参考上述流程分析相应源码。
此外,我们还可以通过 API 方式addExtension() 方法)设置 cachedAdaptiveClass 这个字段,指定适配器类型(这个方法你知道即可)。
总之,适配器什么实际工作都不用做,就是根据参数和状态选择其他实现来完成工作。 。
3. 自动包装特性
Dubbo 中的一个扩展接口可能有多个扩展实现类这些扩展实现类可能会包含一些相同的逻辑如果在每个实现类中都写一遍那么这些重复代码就会变得很难维护。Dubbo 提供的自动包装特性,就可以解决这个问题。 Dubbo 将多个扩展实现类的公共逻辑,抽象到 Wrapper 类中Wrapper 类与普通的扩展实现类一样,也实现了扩展接口,在获取真正的扩展实现对象时,在其外面包装一层 Wrapper 对象,你可以理解成一层装饰器。
了解了 Wrapper 类的基本功能,我们回到 ExtensionLoader.loadClass() 方法中,可以看到:
private void loadClass(){
... // 省略前面对@Adaptive注解的处理
} else if (isWrapperClass(clazz)) { // ---1
cacheWrapperClass(clazz); // ---2
} else ... // 省略其他分支
}
在 isWrapperClass() 方法中,会判断该扩展实现类是否包含拷贝构造函数(即构造函数只有一个参数且为扩展接口类型),如果包含,则为 Wrapper 类,这就是判断 Wrapper 类的标准。
将 Wrapper 类记录到 cachedWrapperClassesSet>类型)这个实例字段中进行缓存。
前面在介绍 createExtension() 方法时的 4 处,有下面这段代码,其中会遍历全部 Wrapper 类并一层层包装到真正的扩展实例对象外层:
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
for (Class<?> wrapperClass : wrapperClasses) {
instance = injectExtension((T) wrapperClass
.getConstructor(type).newInstance(instance));
}
}
4. 自动装配特性
在 createExtension() 方法中我们看到Dubbo SPI 在拿到扩展实现类的对象(以及 Wrapper 类的对象)之后,还会调用 injectExtension() 方法扫描其全部 setter 方法,并根据 setter 方法的名称以及参数的类型,加载相应的扩展实现,然后调用相应的 setter 方法填充属性,这就实现了 Dubbo SPI 的自动装配特性。简单来说,自动装配属性就是在加载一个扩展点的时候,将其依赖的扩展点一并加载,并进行装配。
下面简单看一下 injectExtension() 方法的具体实现:
private T injectExtension(T instance) {
if (objectFactory == null) { // 检测objectFactory字段
return instance;
}
for (Method method : instance.getClass().getMethods()) {
... // 如果不是setter方法忽略该方法(略)
if (method.getAnnotation(DisableInject.class) != null) {
continue; // 如果方法上明确标注了@DisableInject注解,忽略该方法
}
// 根据setter方法的参数确定扩展接口
Class<?> pt = method.getParameterTypes()[0];
... // 如果参数为简单类型忽略该setter方法(略)
// 根据setter方法的名称确定属性名称
String property = getSetterProperty(method);
// 加载并实例化扩展实现类
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
method.invoke(instance, object); // 调用setter方法进行装配
}
}
return instance;
}
injectExtension() 方法实现的自动装配依赖了 ExtensionFactory即 objectFactory 字段),前面我们提到过 ExtensionFactory 有 SpringExtensionFactory 和 SpiExtensionFactory 两个真正的实现(还有一个实现是 AdaptiveExtensionFactory 是适配器)。下面我们分别介绍下这两个真正的实现。
第一个SpiExtensionFactory。 根据扩展接口获取相应的适配器,没有到属性名称:
@Override
public <T> T getExtension(Class<T> type, String name) {
if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
// 查找type对应的ExtensionLoader实例
ExtensionLoader<T> loader = ExtensionLoader
.getExtensionLoader(type);
if (!loader.getSupportedExtensions().isEmpty()) {
return loader.getAdaptiveExtension(); // 获取适配器实现
}
}
return null;
}
第二个SpringExtensionFactory。 将属性名称作为 Spring Bean 的名称,从 Spring 容器中获取 Bean
public <T> T getExtension(Class<T> type, String name) {
... // 检查:type必须为接口且必须包含@SPI注解(略)
for (ApplicationContext context : CONTEXTS) {
// 从Spring容器中查找Bean
T bean = BeanFactoryUtils.getOptionalBean(context,name,type);
if (bean != null) {
return bean;
}
}
return null;
}
5. @Activate注解与自动激活特性
这里以 Dubbo 中的 Filter 为例说明自动激活特性的含义org.apache.dubbo.rpc.Filter 接口有非常多的扩展实现类,在一个场景中可能需要某几个 Filter 扩展实现类协同工作,而另一个场景中可能需要另外几个实现类一起工作。这样,就需要一套配置来指定当前场景中哪些 Filter 实现是可用的,这就是 @Activate 注解要做的事情。
@Activate 注解标注在扩展实现类上,有 group、value 以及 order 三个属性。
group 属性:修饰的实现类是在 Provider 端被激活还是在 Consumer 端被激活。
value 属性:修饰的实现类只在 URL 参数中出现指定的 key 时才会被激活。
order 属性:用来确定扩展实现类的排序。
我们先来看 loadClass() 方法对 @Activate 的扫描,其中会将包含 @Activate 注解的实现类缓存到 cachedActivates 这个实例字段Map类型Key为扩展名Value为 @Activate 注解):
private void loadClass(){
if (clazz.isAnnotationPresent(Adaptive.class)) {
// 处理@Adaptive注解
cacheAdaptiveClass(clazz, overridden);
} else if (isWrapperClass(clazz)) { // 处理Wrapper类
cacheWrapperClass(clazz);
} else { // 处理真正的扩展实现类
clazz.getConstructor(); // 扩展实现类必须有无参构造函数
...// 兜底:SPI配置文件中未指定扩展名称则用类的简单名称作为扩展名(略)
String[] names = NAME_SEPARATOR.split(name);
if (ArrayUtils.isNotEmpty(names)) {
// 将包含@Activate注解的实现类缓存到cachedActivates集合中
cacheActivateClass(clazz, names[0]);
for (String n : names) {
// 在cachedNames集合中缓存实现类->扩展名的映射
cacheName(clazz, n);
// 在cachedClasses集合中缓存扩展名->实现类的映射
saveInExtensionClass(extensionClasses, clazz, n,
overridden);
}
}
}
}
使用 cachedActivates 这个集合的地方是 getActivateExtension() 方法。首先来关注 getActivateExtension() 方法的参数url 中包含了配置信息values 是配置中指定的扩展名group 为 Provider 或 Consumer。下面是 getActivateExtension() 方法的核心逻辑:
首先,获取默认激活的扩展集合。默认激活的扩展实现类有几个条件:①在 cachedActivates 集合中存在;②@Activate 注解指定的 group 属性与当前 group 匹配;③扩展名没有出现在 values 中即未在配置中明确指定也未在配置中明确指定删除④URL 中出现了 @Activate 注解中指定的 Key。
然后,按照 @Activate 注解中的 order 属性对默认激活的扩展集合进行排序。
最后,按序添加自定义扩展实现类的对象。
public List<T> getActivateExtension(URL url, String[] values,
String group) {
List<T> activateExtensions = new ArrayList<>();
// values配置就是扩展名
List<String> names = values == null ?
new ArrayList<>(0) : asList(values);
if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {// ---1
getExtensionClasses(); // 触发cachedActivates等缓存字段的加载
for (Map.Entry<String, Object> entry :
cachedActivates.entrySet()) {
String name = entry.getKey(); // 扩展名
Object activate = entry.getValue(); // @Activate注解
String[] activateGroup, activateValue;
if (activate instanceof Activate) { // @Activate注解中的配置
activateGroup = ((Activate) activate).group();
activateValue = ((Activate) activate).value();
} else {
continue;
}
if (isMatchGroup(group, activateGroup) // 匹配group
// 没有出现在values配置中的即为默认激活的扩展实现
&& !names.contains(name)
// 通过"-"明确指定不激活该扩展实现
&& !names.contains(REMOVE_VALUE_PREFIX + name)
// 检测URL中是否出现了指定的Key
&& isActive(activateValue, url)) {
// 加载扩展实现的实例对象,这些都是激活的
activateExtensions.add(getExtension(name));
}
}
// 排序 --- 2
activateExtensions.sort(ActivateComparator.COMPARATOR);
}
List<T> loadedExtensions = new ArrayList<>();
for (int i = 0; i < names.size(); i++) { // ---3
String name = names.get(i);
// 通过"-"开头的配置明确指定不激活的扩展实现直接就忽略了
if (!name.startsWith(REMOVE_VALUE_PREFIX)
&& !names.contains(REMOVE_VALUE_PREFIX + name)) {
if (DEFAULT_KEY.equals(name)) {
if (!loadedExtensions.isEmpty()) {
// 按照顺序将自定义的扩展添加到默认扩展集合前面
activateExtensions.addAll(0, loadedExtensions);
loadedExtensions.clear();
}
} else {
loadedExtensions.add(getExtension(name));
}
}
}
if (!loadedExtensions.isEmpty()) {
// 按照顺序将自定义的扩展添加到默认扩展集合后面
activateExtensions.addAll(loadedExtensions);
}
return activateExtensions;
}
最后举个简单的例子说明上述处理流程假设 cachedActivates 集合缓存的扩展实现如下表所示
Provider 端调用 getActivateExtension() 方法时传入的 values 配置为 demoFilter3-demoFilter2defaultdemoFilter1”,那么根据上面的逻辑
得到默认激活的扩展实实现集合中有 [ demoFilter4, demoFilter6 ]
排序后为 [ demoFilter6, demoFilter4 ]
按序添加自定义扩展实例之后得到 [ demoFilter3, demoFilter6, demoFilter4, demoFilter1 ]。
总结
本课时我们深入全面地讲解了 Dubbo SPI 的核心实现首先介绍了 @SPI 注解的底层实现这是 Dubbo SPI 最核心的基础然后介绍了 @Adaptive 注解与动态生成适配器类的核心原理和实现最后分析了 Dubbo SPI 中的自动包装和自动装配特性以及 @Activate 注解的原理
Dubbo SPI Dubbo 框架实现扩展机制的核心希望你仔细研究其实现为后续源码分析过程打下基础
也欢迎你在留言区分享你的学习心得和实践经验

View File

@@ -0,0 +1,141 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
05 海量定时任务,一个时间轮搞定
在很多开源框架中,都需要定时任务的管理功能,例如 ZooKeeper、Netty、Quartz、Kafka 以及 Linux 操作系统。
JDK 提供的 java.util.Timer 和 DelayedQueue 等工具类,可以帮助我们实现简单的定时任务管理,其底层实现使用的是堆这种数据结构,存取操作的复杂度都是 O(nlog(n)),无法支持大量的定时任务。在定时任务量比较大、性能要求比较高的场景中,为了将定时任务的存取操作以及取消操作的时间复杂度降为 O(1),一般会使用时间轮的方式。
时间轮是一种高效的、批量管理定时任务的调度模型。时间轮一般会实现成一个环形结构,类似一个时钟,分为很多槽,一个槽代表一个时间间隔,每个槽使用双向链表存储定时任务;指针周期性地跳动,跳动到一个槽位,就执行该槽位的定时任务。
时间轮环形结构示意图
需要注意的是,单层时间轮的容量和精度都是有限的,对于精度要求特别高、时间跨度特别大或是海量定时任务需要调度的场景,通常会使用多级时间轮以及持久化存储与时间轮结合的方案。
那在 Dubbo 中时间轮的具体实现方式是怎样的呢本课时我们就重点探讨下。Dubbo 的时间轮实现位于 dubbo-common 模块的 org.apache.dubbo.common.timer 包中,下面我们就来分析时间轮涉及的核心接口和实现。
核心接口
在 Dubbo 中,所有的定时任务都要继承 TimerTask 接口。TimerTask 接口非常简单,只定义了一个 run() 方法,该方法的入参是一个 Timeout 接口的对象。Timeout 对象与 TimerTask 对象一一对应,两者的关系类似于线程池返回的 Future 对象与提交到线程池中的任务对象之间的关系。通过 Timeout 对象我们不仅可以查看定时任务的状态还可以操作定时任务例如取消关联的定时任务。Timeout 接口中的方法如下图所示:
.png
Timer 接口定义了定时器的基本行为,如下图所示,其核心是 newTimeout() 方法提交一个定时任务TimerTask并返回关联的 Timeout 对象,这有点类似于向线程池提交任务的感觉。
HashedWheelTimeout
HashedWheelTimeout 是 Timeout 接口的唯一实现,是 HashedWheelTimer 的内部类。HashedWheelTimeout 扮演了两个角色:
第一个,时间轮中双向链表的节点,即定时任务 TimerTask 在 HashedWheelTimer 中的容器。
第二个,定时任务 TimerTask 提交到 HashedWheelTimer 之后返回的句柄Handle用于在时间轮外部查看和控制定时任务。
HashedWheelTimeout 中的核心字段如下:
prev、nextHashedWheelTimeout类型分别对应当前定时任务在链表中的前驱节点和后继节点。
taskTimerTask类型指实际被调度的任务。
deadlinelong类型指定时任务执行的时间。这个时间是在创建 HashedWheelTimeout 时指定的计算公式是currentTime创建 HashedWheelTimeout 的时间) + delay任务延迟时间 - startTimeHashedWheelTimer 的启动时间),时间单位为纳秒。
statevolatile int类型指定时任务当前所处状态可选的有三个分别是 INIT0、CANCELLED1和 EXPIRED2。另外还有一个 STATE_UPDATER 字段AtomicIntegerFieldUpdater类型实现 state 状态变更的原子性。
remainingRoundslong类型指当前任务剩余的时钟周期数。时间轮所能表示的时间长度是有限的在任务到期时间与当前时刻的时间差超过时间轮单圈能表示的时长就出现了套圈的情况需要该字段值表示剩余的时钟周期。
HashedWheelTimeout 中的核心方法有:
isCancelled()、isExpired() 、state() 方法, 主要用于检查当前 HashedWheelTimeout 状态。
cancel() 方法, 将当前 HashedWheelTimeout 的状态设置为 CANCELLED并将当前 HashedWheelTimeout 添加到 cancelledTimeouts 队列中等待销毁。
expire() 方法, 当任务到期时,会调用该方法将当前 HashedWheelTimeout 设置为 EXPIRED 状态,然后调用其中的 TimerTask 的 run() 方法执行定时任务。
remove() 方法, 将当前 HashedWheelTimeout 从时间轮中删除。
HashedWheelBucket
HashedWheelBucket 是时间轮中的一个槽,时间轮中的槽实际上就是一个用于缓存和管理双向链表的容器,双向链表中的每一个节点就是一个 HashedWheelTimeout 对象,也就关联了一个 TimerTask 定时任务。
HashedWheelBucket 持有双向链表的首尾两个节点,分别是 head 和 tail 两个字段,再加上每个 HashedWheelTimeout 节点均持有前驱和后继的引用,这样就可以正向或是逆向遍历整个双向链表了。
下面我们来看 HashedWheelBucket 中的核心方法。
addTimeout() 方法:新增 HashedWheelTimeout 到双向链表的尾部。
pollTimeout() 方法:移除双向链表中的头结点,并将其返回。
remove() 方法:从双向链表中移除指定的 HashedWheelTimeout 节点。
clearTimeouts() 方法:循环调用 pollTimeout() 方法处理整个双向链表,并返回所有未超时或者未被取消的任务。
expireTimeouts() 方法:遍历双向链表中的全部 HashedWheelTimeout 节点。 在处理到期的定时任务时,会通过 remove() 方法取出,并调用其 expire() 方法执行;对于已取消的任务,通过 remove() 方法取出后直接丢弃;对于未到期的任务,会将 remainingRounds 字段(剩余时钟周期数)减一。
HashedWheelTimer
HashedWheelTimer 是 Timer 接口的实现它通过时间轮算法实现了一个定时器。HashedWheelTimer 会根据当前时间轮指针选定对应的槽HashedWheelBucket从双向链表的头部开始迭代对每个定时任务HashedWheelTimeout进行计算属于当前时钟周期则取出运行不属于则将其剩余的时钟周期数减一操作。
下面我们来看 HashedWheelTimer 的核心属性。
workerStatevolatile int类型时间轮当前所处状态可选值有 init、started、shutdown。同时有相应的 AtomicIntegerFieldUpdater 实现 workerState 的原子修改。
startTimelong类型当前时间轮的启动时间提交到该时间轮的定时任务的 deadline 字段值均以该时间戳为起点进行计算。
wheelHashedWheelBucket[]类型):该数组就是时间轮的环形队列,每一个元素都是一个槽。当指定时间轮槽数为 n 时,实际上会取大于且最靠近 n 的 2 的幂次方值。
timeouts、cancelledTimeoutsLinkedBlockingQueue类型timeouts 队列用于缓冲外部提交时间轮中的定时任务cancelledTimeouts 队列用于暂存取消的定时任务。HashedWheelTimer 会在处理 HashedWheelBucket 的双向链表之前,先处理这两个队列中的数据。
ticklong类型该字段在 HashedWheelTimer$Worker 中,是时间轮的指针,是一个步长为 1 的单调递增计数器。
maskint类型掩码 mask = wheel.length - 1执行 ticks & mask 便能定位到对应的时钟槽。
ticksDurationlong类型时间指针每次加 1 所代表的实际时间,单位为纳秒。
pendingTimeoutsAtomicLong类型当前时间轮剩余的定时任务总数。
workerThreadThread类型时间轮内部真正执行定时任务的线程。
workerWorker类型真正执行定时任务的逻辑封装这个 Runnable 对象中。
时间轮对外提供了一个 newTimeout() 接口用于提交定时任务,在定时任务进入到 timeouts 队列之前会先调用 start() 方法启动时间轮,其中会完成下面两个关键步骤:
确定时间轮的 startTime 字段;
启动 workerThread 线程,开始执行 worker 任务。
之后根据 startTime 计算该定时任务的 deadline 字段,最后才能将定时任务封装成 HashedWheelTimeout 并添加到 timeouts 队列。
下面我们来分析时间轮指针一次转动的全流程。
时间轮指针转动,时间轮周期开始。
清理用户主动取消的定时任务,这些定时任务在用户取消时,会记录到 cancelledTimeouts 队列中。在每次指针转动的时候,时间轮都会清理该队列。
将缓存在 timeouts 队列中的定时任务转移到时间轮中对应的槽中。
根据当前指针定位对应槽,处理该槽位的双向链表中的定时任务。
检测时间轮的状态。如果时间轮处于运行状态,则循环执行上述步骤,不断执行定时任务。如果时间轮处于停止状态,则执行下面的步骤获取到未被执行的定时任务并加入 unprocessedTimeouts 队列:遍历时间轮中每个槽位,并调用 clearTimeouts() 方法;对 timeouts 队列中未被加入槽中循环调用 poll()。
最后再次清理 cancelledTimeouts 队列中用户主动取消的定时任务。
上述核心逻辑在 HashedWheelTimer$Worker.run() 方法中,若你感兴趣的话,可以翻看一下源码进行分析。
Dubbo 中如何使用定时任务
在 Dubbo 中,时间轮并不直接用于周期性操作,而是只向时间轮提交执行单次的定时任务,在上一次任务执行完成的时候,调用 newTimeout() 方法再次提交当前任务,这样就会在下个周期执行该任务。即使在任务执行过程中出现了 GC、I/O 阻塞等情况,导致任务延迟或卡住,也不会有同样的任务源源不断地提交进来,导致任务堆积。
Dubbo 中对时间轮的应用主要体现在如下两个方面:
失败重试, 例如Provider 向注册中心进行注册失败时的重试操作,或是 Consumer 向注册中心订阅时的失败重试等。
周期性定时任务, 例如,定期发送心跳请求,请求超时的处理,或是网络连接断开后的重连机制。
总结
本课时我们重点介绍了 Dubbo 中时间轮相关的内容:
首先介绍了 JDK 提供的 Timer 定时器以及 DelayedQueue 等工具类的问题,并说明了时间轮的解决方案;
然后深入讲解了 Dubbo 对时间轮的抽象,以及具体实现细节;
最后还说明了 Dubbo 中时间轮的应用场景,在我们后面介绍 Dubbo 其他模块的时候,你还会看到时间轮的身影。
这里再给你留个课后思考题:如果存在海量定时任务,并且这些任务的开始时间跨度非常长,例如,有的是 1 分钟之后执行,有的是 1 小时之后执行,有的是 1 年之后执行,那你该如何对时间轮进行扩展,处理这些定时任务呢?欢迎你在留言区分享你的想法,期待看到你的答案。

View File

@@ -0,0 +1,133 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06 ZooKeeper 与 Curator求你别用 ZkClient 了(上)
在前面我们介绍 Dubbo 简化架构的时候提到过Dubbo Provider 在启动时会将自身的服务信息整理成 URL 注册到注册中心Dubbo Consumer 在启动时会向注册中心订阅感兴趣的 Provider 信息,之后 Provider 和 Consumer 才能建立连接,进行后续的交互。可见,一个稳定、高效的注册中心对基于 Dubbo 的微服务来说是至关重要的。
Dubbo 目前支持 Consul、etcd、Nacos、ZooKeeper、Redis 等多种开源组件作为注册中心,并且在 Dubbo 源码也有相应的接入模块,如下图所示:
Dubbo 官方推荐使用 ZooKeeper 作为注册中心,它是在实际生产中最常用的注册中心实现,这也是我们本课时要介绍 ZooKeeper 核心原理的原因。
要与 ZooKeeper 集群进行交互,我们可以使用 ZooKeeper 原生客户端或是 ZkClient、Apache Curator 等第三方开源客户端。在后面介绍 dubbo-registry-zookeeper 模块的具体实现时你会看到Dubbo 底层使用的是 Apache Curator。Apache Curator 是实践中最常用的 ZooKeeper 客户端。
ZooKeeper 核心概念
Apache ZooKeeper 是一个针对分布式系统的、可靠的、可扩展的协调服务它通常作为统一命名服务、统一配置管理、注册中心分布式集群管理、分布式锁服务、Leader 选举服务等角色出现。很多分布式系统都依赖与 ZooKeeper 集群实现分布式系统间的协调调度例如Dubbo、HDFS 2.x、HBase、Kafka 等。ZooKeeper 已经成为现代分布式系统的标配。
ZooKeeper 本身也是一个分布式应用程序,下图展示了 ZooKeeper 集群的核心架构。
ZooKeeper 集群的核心架构图
Client 节点:从业务角度来看,这是分布式应用中的一个节点,通过 ZkClient 或是其他 ZooKeeper 客户端与 ZooKeeper 集群中的一个 Server 实例维持长连接,并定时发送心跳。从 ZooKeeper 集群的角度来看,它是 ZooKeeper 集群的一个客户端,可以主动查询或操作 ZooKeeper 集群中的数据,也可以在某些 ZooKeeper 节点ZNode上添加监听。当被监听的 ZNode 节点发生变化时,例如,该 ZNode 节点被删除、新增子节点或是其中数据被修改等ZooKeeper 集群都会立即通过长连接通知 Client。
Leader 节点ZooKeeper 集群的主节点,负责整个 ZooKeeper 集群的写操作,保证集群内事务处理的顺序性。同时,还要负责整个集群中所有 Follower 节点与 Observer 节点的数据同步。
Follower 节点ZooKeeper 集群中的从节点,可以接收 Client 读请求并向 Client 返回结果,并不处理写请求,而是转发到 Leader 节点完成写入操作。另外Follower 节点还会参与 Leader 节点的选举。
Observer 节点ZooKeeper 集群中特殊的从节点,不会参与 Leader 节点的选举,其他功能与 Follower 节点相同。引入 Observer 角色的目的是增加 ZooKeeper 集群读操作的吞吐量,如果单纯依靠增加 Follower 节点来提高 ZooKeeper 的读吞吐量,那么有一个很严重的副作用,就是 ZooKeeper 集群的写能力会大大降低,因为 ZooKeeper 写数据时需要 Leader 将写操作同步给半数以上的 Follower 节点。引入 Observer 节点使得 ZooKeeper 集群在写能力不降低的情况下,大大提升了读操作的吞吐量。
了解了 ZooKeeper 整体的架构之后,我们再来了解一下 ZooKeeper 集群存储数据的逻辑结构。ZooKeeper 逻辑上是按照树型结构进行数据存储的(如下图),其中的节点称为 ZNode。每个 ZNode 有一个名称标识,即树根到该节点的路径(用 “/” 分隔ZooKeeper 树中的每个节点都可以拥有子节点,这与文件系统的目录树类似。
ZooKeeper 树型存储结构
ZNode 节点类型有如下四种:
持久节点。 持久节点创建后,会一直存在,不会因创建该节点的 Client 会话失效而删除。
持久顺序节点。 持久顺序节点的基本特性与持久节点一致创建节点的过程中ZooKeeper 会在其名字后自动追加一个单调增长的数字后缀,作为新的节点名。
临时节点。 创建临时节点的 ZooKeeper Client 会话失效之后,其创建的临时节点会被 ZooKeeper 集群自动删除。与持久节点的另一点区别是,临时节点下面不能再创建子节点。
临时顺序节点。 基本特性与临时节点一致创建节点的过程中ZooKeeper 会在其名字后自动追加一个单调增长的数字后缀,作为新的节点名。
在每个 ZNode 中都维护着一个 stat 结构,记录了该 ZNode 的元数据其中包括版本号、操作控制列表ACL、时间戳和数据长度等信息如下表所示
我们除了可以通过 ZooKeeper Client 对 ZNode 进行增删改查等基本操作,还可以注册 Watcher 监听 ZNode 节点、其中的数据以及子节点的变化。一旦监听到变化,则相应的 Watcher 即被触发,相应的 ZooKeeper Client 会立即得到通知。Watcher 有如下特点:
主动推送。 Watcher 被触发时,由 ZooKeeper 集群主动将更新推送给客户端,而不需要客户端轮询。
一次性。 数据变化时Watcher 只会被触发一次。如果客户端想得到后续更新的通知,必须要在 Watcher 被触发后重新注册一个 Watcher。
可见性。 如果一个客户端在读请求中附带 WatcherWatcher 被触发的同时再次读取数据,客户端在得到 Watcher 消息之前肯定不可能看到更新后的数据。换句话说,更新通知先于更新结果。
顺序性。 如果多个更新触发了多个 Watcher ,那 Watcher 被触发的顺序与更新顺序一致。
消息广播流程概述
ZooKeeper 集群中三种角色的节点Leader、Follower 和 Observer都可以处理 Client 的读请求,因为每个节点都保存了相同的数据副本,直接进行读取即可返回给 Client。
对于写请求,如果 Client 连接的是 Follower 节点(或 Observer 节点),则在 Follower 节点(或 Observer 节点)收到写请求将会被转发到 Leader 节点。下面是 Leader 处理写请求的核心流程:
Leader 节点接收写请求后,会为写请求赋予一个全局唯一的 zxid64 位自增 id通过 zxid 的大小比较就可以实现写操作的顺序一致性。
Leader 通过先进先出队列(会给每个 Follower 节点都创建一个队列,保证发送的顺序性),将带有 zxid 的消息作为一个 proposal提案分发给所有 Follower 节点。
当 Follower 节点接收到 proposal 之后,会先将 proposal 写到本地事务日志,写事务成功后再向 Leader 节点回一个 ACK 响应。
当 Leader 节点接收到过半 Follower 的 ACK 响应之后Leader 节点就向所有 Follower 节点发送 COMMIT 命令,并在本地执行提交。
当 Follower 收到消息的 COMMIT 命令之后也会提交操作,写操作到此完成。
最后Follower 节点会返回 Client 写请求相应的响应。
下图展示了写操作的核心流程:
写操作核心流程图
崩溃恢复
上面写请求处理流程中,如果发生 Leader 节点宕机,整个 ZooKeeper 集群可能处于如下两种状态:
当 Leader 节点收到半数以上 Follower 节点的 ACK 响应之后,会向各个 Follower 节点广播 COMMIT 命令,同时也会在本地执行 COMMIT 并向连接的客户端进行响应。如果在各个 Follower 收到 COMMIT 命令前 Leader 就宕机了,就会导致剩下的服务器没法执行这条消息。
当 Leader 节点生成 proposal 之后就宕机了,而其他 Follower 并没有收到此 proposal或者只有一小部分 Follower 节点收到了这条 proposal那么此次写操作就是执行失败的。
在 Leader 宕机后ZooKeeper 会进入崩溃恢复模式,重新进行 Leader 节点的选举。
ZooKeeper 对新 Leader 有如下两个要求:
对于原 Leader 已经提交了的 proposal新 Leader 必须能够广播并提交,这样就需要选择拥有最大 zxid 值的节点作为 Leader。
对于原 Leader 还未广播或只部分广播成功的 proposal新 Leader 能够通知原 Leader 和已经同步了的 Follower 删除,从而保证集群数据的一致性。
ZooKeeper 选主使用的是 ZAB 协议,如果展开介绍的话内容会非常多,这里我们就通过一个示例简单介绍 ZooKeeper 选主的大致流程。
比如,当前集群中有 5 个 ZooKeeper 节点构成sid 分别为 1、2、3、4 和 5zxid 分别为 10、10、9、9 和 8此时sid 为 1 的节点是 Leader 节点。实际上zxid 包含了 epoch高 32 位)和自增计数器(低 32 位) 两部分。其中epoch 是“纪元”的意思,标识当前 Leader 周期,每次选举时 epoch 部分都会递增,这就防止了网络隔离之后,上一周期的旧 Leader 重新连入集群造成不必要的重新选举。该示例中我们假设各个节点的 epoch 都相同。
某一时刻,节点 1 的服务器宕机了ZooKeeper 集群开始进行选主。由于无法检测到集群中其他节点的状态信息(处于 Looking 状态),因此每个节点都将自己作为被选举的对象来进行投票。于是 sid 为 2、3、4、5 的节点投票情况分别为2,103,94,95,8同时各个节点也会接收到来自其他节点的投票这里以sid, zxid的形式来标识一次投票信息
对于节点 2 来说接收到3,94,95,8的投票对比后发现自己的 zxid 最大,因此不需要做任何投票变更。
对于节点 3 来说接收到2,104,95,8的投票对比后由于 2 的 zxid 比自己的 zxid 要大因此需要更改投票改投2,10并将改投后的票发给其他节点。
对于节点 4 来说接收到2,103,95,8的投票对比后由于 2 的 zxid 比自己的 zxid 要大因此需要更改投票改投2,10并将改投后的票发给其他节点。
对于节点 5 来说也是一样最终改投2,10
经过第二轮投票后,集群中的每个节点都会再次收到其他机器的投票,然后开始统计投票,如果有过半的节点投了同一个节点,则该节点成为新的 Leader这里显然节点 2 成了新 Leader节点。
Leader 节点此时会将 epoch 值加 1并将新生成的 epoch 分发给各个 Follower 节点。各个 Follower 节点收到全新的 epoch 后,返回 ACK 给 Leader 节点,并带上各自最大的 zxid 和历史事务日志信息。Leader 选出最大的 zxid并更新自身历史事务日志示例中的节点 2 无须更新。Leader 节点紧接着会将最新的事务日志同步给集群中所有的 Follower 节点,只有当半数 Follower 同步成功,这个准 Leader 节点才能成为正式的 Leader 节点并开始工作。
总结
本课时我们重点介绍了 ZooKeeper 的核心概念以及 ZooKeeper 集群的基本工作原理:
首先介绍了 ZooKeeper 集群中各个节点的角色以及职能;
然后介绍了 ZooKeeper 中存储数据的逻辑结构以及 ZNode 节点的相关特性;
紧接着又讲解了 ZooKeeper 集群读写数据的核心流程;
最后我们通过示例分析了 ZooKeeper 集群的崩溃恢复流程。
在下一课时,我们将介绍 Apache Curator 的相关内容。

View File

@@ -0,0 +1,856 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
07 ZooKeeper 与 Curator求你别用 ZkClient 了(下)
在上一课时我们介绍了 ZooKeeper 的核心概念以及工作原理,这里我们再简单了解一下 ZooKeeper 客户端的相关内容,毕竟在实际工作中,直接使用客户端与 ZooKeeper 进行交互的次数比深入 ZooKeeper 底层进行扩展和二次开发的次数要多得多。从 ZooKeeper 架构的角度看,使用 Dubbo 的业务节点也只是一个 ZooKeeper 客户端罢了。
ZooKeeper 官方提供的客户端支持了一些基本操作例如创建会话、创建节点、读取节点、更新数据、删除节点和检查节点是否存在等但在实际开发中只有这些简单功能是根本不够的。而且ZooKeeper 本身的一些 API 也存在不足,例如:
ZooKeeper 的 Watcher 是一次性的,每次触发之后都需要重新进行注册。
会话超时之后,没有实现自动重连的机制。
ZooKeeper 提供了非常详细的异常,异常处理显得非常烦琐,对开发新手来说,非常不友好。
只提供了简单的 byte[] 数组的接口,没有提供基本类型以及对象级别的序列化。
创建节点时,如果节点存在抛出异常,需要自行检查节点是否存在。
删除节点就无法实现级联删除。
常见的第三方开源 ZooKeeper 客户端有 ZkClient 和 Apache Curator。
ZkClient 是在 ZooKeeper 原生 API 接口的基础上进行了包装,虽然 ZkClient 解决了 ZooKeeper 原生 API 接口的很多问题,提供了非常简洁的 API 接口,实现了会话超时自动重连的机制,解决了 Watcher 反复注册等问题,但其缺陷也非常明显。例如,文档不全、重试机制难用、异常全部转换成了 RuntimeException、没有足够的参考示例等。可见一个简单易用、高效可靠的 ZooKeeper 客户端是多么重要。
Apache Curator 基础
Apache Curator 是 Apache 基金会提供的一款 ZooKeeper 客户端,它提供了一套易用性和可读性非常强的 Fluent 风格的客户端 API ,可以帮助我们快速搭建稳定可靠的 ZooKeeper 客户端程序。
为便于你更全面了解 Curator 的功能,我整理出了如下表格,展示了 Curator 提供的 jar 包:
下面我们从最基础的使用展开,逐一介绍 Apache Curator 在实践中常用的核心功能,开始我们的 Apache Curator 之旅。
1. 基本操作
简单了解了 Apache Curator 各个组件的定位之后,下面我们立刻通过一个示例上手使用 Curator。首先我们创建一个 Maven 项目,并添加 Apache Curator 的依赖:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.1</version>
</dependency>
然后写一个 main 方法,其中会说明 Curator 提供的基础 API 的使用:
public class Main {
public static void main(String[] args) throws Exception {
// Zookeeper集群地址多个节点地址可以用逗号分隔
String zkAddress = "127.0.0.1:2181";
// 重试策略如果连接不上ZooKeeper集群会重试三次重试间隔会递增
RetryPolicy retryPolicy =
new ExponentialBackoffRetry(1000, 3);
// 创建Curator Client并启动启动成功之后就可以与Zookeeper进行交互了
CuratorFramework client =
CuratorFrameworkFactory.newClient(zkAddress, retryPolicy);
client.start();
// 下面简单说明Curator中常用的API
// create()方法创建ZNode可以调用额外方法来设置节点类型、添加Watcher
// 下面是创建一个名为"user"的持久节点其中会存储一个test字符串
String path = client.create().withMode(CreateMode.PERSISTENT)
.forPath("/user", "test".getBytes());
System.out.println(path);
// 输出:/user
// checkExists()方法可以检查一个节点是否存在
Stat stat = client.checkExists().forPath("/user");
System.out.println(stat!=null);
// 输出:true返回的Stat不为null即表示节点存在
// getData()方法可以获取一个节点中的数据
byte[] data = client.getData().forPath("/user");
System.out.println(new String(data));
// 输出:test
// setData()方法可以设置一个节点中的数据
stat = client.setData().forPath("/user","data".getBytes());
data = client.getData().forPath("/user");
System.out.println(new String(data));
// 输出:data
// 在/user节点下创建多个临时顺序节点
for (int i = 0; i < 3; i++) {
client.create().withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath("/user/child-";
}
// 获取所有子节点
List<String> children = client.getChildren().forPath("/user");
System.out.println(children);
// 输出:[child-0000000002, child-0000000001, child-0000000000]
// delete()方法可以删除指定节点deletingChildrenIfNeeded()方法
// 会级联删除子节点
client.delete().deletingChildrenIfNeeded().forPath("/user");
}
}
2. Background
上面介绍的创建、删除、更新、读取等方法都是同步的Curator 提供异步接口引入了BackgroundCallback 这个回调接口以及 CuratorListener 这个监听器,用于处理 Background 调用之后服务端返回的结果信息。BackgroundCallback 接口和 CuratorListener 监听器中接收一个 CuratorEvent 的参数,里面包含事件类型、响应码、节点路径等详细信息。
下面我们通过一个示例说明 BackgroundCallback 接口以及 CuratorListener 监听器的基本使用:
public class Main2 {
public static void main(String[] args) throws Exception {
// Zookeeper集群地址多个节点地址可以用逗号分隔
String zkAddress = "127.0.0.1:2181";
// 重试策略如果连接不上ZooKeeper集群会重试三次重试间隔会递增
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
// 创建Curator Client并启动启动成功之后就可以与Zookeeper进行交互了
CuratorFramework client = CuratorFrameworkFactory
.newClient(zkAddress, retryPolicy);
client.start();
// 添加CuratorListener监听器针对不同的事件进行处理
client.getCuratorListenable().addListener(
new CuratorListener() {
public void eventReceived(CuratorFramework client,
CuratorEvent event) throws Exception {
switch (event.getType()) {
case CREATE:
System.out.println("CREATE:" +
event.getPath());
break;
case DELETE:
System.out.println("DELETE:" +
event.getPath());
break;
case EXISTS:
System.out.println("EXISTS:" +
event.getPath());
break;
case GET_DATA:
System.out.println("GET_DATA:" +
event.getPath() + ","
+ new String(event.getData()));
break;
case SET_DATA:
System.out.println("SET_DATA:" +
new String(event.getData()));
break;
case CHILDREN:
System.out.println("CHILDREN:" +
event.getPath());
break;
default:
}
}
});
// 注意:下面所有的操作都添加了inBackground()方法,转换为后台操作
client.create().withMode(CreateMode.PERSISTENT)
.inBackground().forPath("/user", "test".getBytes());
client.checkExists().inBackground().forPath("/user");
client.setData().inBackground().forPath("/user",
"setData-Test".getBytes());
client.getData().inBackground().forPath("/user");
for (int i = 0; i < 3; i++) {
client.create().withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.inBackground().forPath("/user/child-");
}
client.getChildren().inBackground().forPath("/user");
// 添加BackgroundCallback
client.getChildren().inBackground(new BackgroundCallback() {
public void processResult(CuratorFramework client,
CuratorEvent event) throws Exception {
System.out.println("in background:"
+ event.getType() + "," + event.getPath());
}
}).forPath("/user");
client.delete().deletingChildrenIfNeeded().inBackground()
.forPath("/user");
System.in.read();
}
}
// 输出
// CREATE:/user
// EXISTS:/user
// GET_DATA:/user,setData-Test
// CREATE:/user/child-
// CREATE:/user/child-
// CREATE:/user/child-
// CHILDREN:/user
// DELETE:/user
3. 连接状态监听
除了基础的数据操作Curator 还提供了监听连接状态的监听器——ConnectionStateListener它主要是处理 Curator 客户端和 ZooKeeper 服务器间连接的异常情况例如 短暂或者长时间断开连接
短暂断开连接时ZooKeeper 客户端会检测到与服务端的连接已经断开但是服务端维护的客户端 Session 尚未过期之后客户端和服务端重新建立了连接当客户端重新连接后由于 Session 没有过期ZooKeeper 能够保证连接恢复后保持正常服务
而长时间断开连接时Session 已过期与先前 Session 相关的 Watcher 和临时节点都会丢失 Curator 重新创建了与 ZooKeeper 的连接时会获取到 Session 过期的相关异常Curator 会销毁老 Session并且创建一个新的 Session由于老 Session 关联的数据不存在了 ConnectionStateListener 监听到 LOST 事件时就可以依靠本地存储的数据恢复 Session
这里 Session 指的是 ZooKeeper 服务器与客户端的会话客户端启动的时候会与服务器建立一个 TCP 连接从第一次连接建立开始客户端会话的生命周期也开始了客户端能够通过心跳检测与服务器保持有效的会话也能够向 ZooKeeper 服务器发送请求并接受响应同时还能够通过该连接接收来自服务器的 Watch 事件通知
我们可以设置客户端会话的超时时间sessionTimeout当服务器压力太大网络故障或是客户端主动断开连接等原因导致连接断开时只要客户端在 sessionTimeout 规定的时间内能够重新连接到 ZooKeeper 集群中任意一个实例那么之前创建的会话仍然有效ZooKeeper 通过 sessionID 唯一标识 Session所以在 ZooKeeper 集群中sessionID 需要保证全局唯一 由于 ZooKeeper 会将 Session 信息存放到硬盘中即使节点重启之前未过期的 Session 仍然会存在
public class Main3 {
public static void main(String[] args) throws Exception {
// Zookeeper集群地址多个节点地址可以用逗号分隔
String zkAddress = "127.0.0.1:2181";
// 重试策略如果连接不上ZooKeeper集群会重试三次重试间隔会递增
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
// 创建Curator Client并启动启动成功之后就可以与Zookeeper进行交互了
CuratorFramework client = CuratorFrameworkFactory
.newClient(zkAddress, retryPolicy);
client.start();
// 添加ConnectionStateListener监听器
client.getConnectionStateListenable().addListener(
new ConnectionStateListener() {
public void stateChanged(CuratorFramework client,
ConnectionState newState) {
// 这里我们可以针对不同的连接状态进行特殊的处理
switch (newState) {
case CONNECTED:
// 第一次成功连接到ZooKeeper之后会进入该状态
// 对于每个CuratorFramework对象此状态仅出现一次
break;
case SUSPENDED: // ZooKeeper的连接丢失
break;
case RECONNECTED: // 丢失的连接被重新建立
break;
case LOST:
// 当Curator认为会话已经过期时则进入此状态
break;
case READ_ONLY: // 连接进入只读模式
break;
}
}
});
}
}
4. Watcher
Watcher 监听机制是 ZooKeeper 中非常重要的特性可以监听某个节点上发生的特定事件例如监听节点数据变更节点删除子节点状态变更等事件当相应事件发生时ZooKeeper 会产生一个 Watcher 事件并且发送到客户端通过 Watcher 机制就可以使用 ZooKeeper 实现分布式锁集群管理等功能
Curator 客户端中我们可以使用 usingWatcher() 方法添加 Watcher前面示例中能够添加 Watcher 的有 checkExists()、getData()以及 getChildren() 三个方法下面我们来看一个具体的示例
public class Main4 {
public static void main(String[] args) throws Exception {
// Zookeeper集群地址多个节点地址可以用逗号分隔
String zkAddress = "127.0.0.1:2181";
// 重试策略如果连接不上ZooKeeper集群会重试三次重试间隔会递增
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
// 创建Curator Client并启动启动成功之后就可以与Zookeeper进行交互了
CuratorFramework client = CuratorFrameworkFactory
.newClient(zkAddress, retryPolicy);
client.start();
try {
client.create().withMode(CreateMode.PERSISTENT)
.forPath("/user", "test".getBytes());
} catch (Exception e) {
}
// 这里通过usingWatcher()方法添加一个Watcher
List<String> children = client.getChildren().usingWatcher(
new CuratorWatcher() {
public void process(WatchedEvent event) throws Exception {
System.out.println(event.getType() + "," +
event.getPath());
}
}).forPath("/user");
System.out.println(children);
System.in.read();
}
}
接下来,我们打开 ZooKeeper 的命令行客户端,在 /user 节点下先后添加两个子节点,如下所示:
此时我们只得到一行输出:
NodeChildrenChanged,/user
之所以这样,是因为通过 usingWatcher() 方法添加的 CuratorWatcher 只会触发一次触发完毕后就会销毁。checkExists() 方法、getData() 方法通过 usingWatcher() 方法添加的 Watcher 也是一样的原理,只不过监听的事件不同,你若感兴趣的话,可以自行尝试一下。
相信你已经感受到,直接通过注册 Watcher 进行事件监听不是特别方便,需要我们自己反复注册 Watcher。Apache Curator 引入了 Cache 来实现对 ZooKeeper 服务端事件的监听。Cache 是 Curator 中对事件监听的包装其对事件的监听其实可以近似看作是一个本地缓存视图和远程ZooKeeper 视图的对比过程。同时Curator 能够自动为开发人员处理反复注册监听,从而大大简化了代码的复杂程度。
实践中常用的 Cache 有三大类:
NodeCache。 对一个节点进行监听监听事件包括指定节点的增删改操作。注意哦NodeCache 不仅可以监听数据节点的内容变更,也能监听指定节点是否存在,如果原本节点不存在,那么 Cache 就会在节点被创建后触发 NodeCacheListener删除操作亦然。
PathChildrenCache。 对指定节点的一级子节点进行监听,监听事件包括子节点的增删改操作,但是不对该节点的操作监听。
TreeCache。 综合 NodeCache 和 PathChildrenCache 的功能,是对指定节点以及其子节点进行监听,同时还可以设置监听的深度。
下面通过示例介绍上述三种 Cache 的基本使用:
public class Main5 {
public static void main(String[] args) throws Exception {
// Zookeeper集群地址多个节点地址可以用逗号分隔
String zkAddress = "127.0.0.1:2181";
// 重试策略如果连接不上ZooKeeper集群会重试三次重试间隔会递增
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
// 创建Curator Client并启动启动成功之后就可以与Zookeeper进行交互了
CuratorFramework client = CuratorFrameworkFactory
.newClient(zkAddress, retryPolicy);
client.start();
// 创建NodeCache监听的是"/user"这个节点
NodeCache nodeCache = new NodeCache(client, "/user");
// start()方法有个boolean类型的参数默认是false。如果设置为true
// 那么NodeCache在第一次启动的时候就会立刻从ZooKeeper上读取对应节点的
// 数据内容并保存在Cache中。
nodeCache.start(true);
if (nodeCache.getCurrentData() != null) {
System.out.println("NodeCache节点初始化数据为"
+ new String(nodeCache.getCurrentData().getData()));
} else {
System.out.println("NodeCache节点数据为空");
}
// 添加监听器
nodeCache.getListenable().addListener(() -> {
String data = new String(nodeCache.getCurrentData().getData());
System.out.println("NodeCache节点路径" + nodeCache.getCurrentData().getPath()
+ ",节点数据为:" + data);
});
// 创建PathChildrenCache实例监听的是"user"这个节点
PathChildrenCache childrenCache = new PathChildrenCache(client, "/user", true);
// StartMode指定的初始化的模式
// NORMAL:普通异步初始化
// BUILD_INITIAL_CACHE:同步初始化
// POST_INITIALIZED_EVENT:异步初始化,初始化之后会触发事件
childrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE);
// childrenCache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
// childrenCache.start(PathChildrenCache.StartMode.NORMAL);
List<ChildData> children = childrenCache.getCurrentData();
System.out.println("获取子节点列表:");
// 如果是BUILD_INITIAL_CACHE可以获取这个数据如果不是就不行
children.forEach(childData -> {
System.out.println(new String(childData.getData()));
});
childrenCache.getListenable().addListener(((client1, event) -> {
System.out.println(LocalDateTime.now() + " " + event.getType());
if (event.getType().equals(PathChildrenCacheEvent.Type.INITIALIZED)) {
System.out.println("PathChildrenCache:子节点初始化成功...");
} else if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_ADDED)) {
String path = event.getData().getPath();
System.out.println("PathChildrenCache添加子节点:" + event.getData().getPath());
System.out.println("PathChildrenCache子节点数据:" + new String(event.getData().getData()));
} else if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_REMOVED)) {
System.out.println("PathChildrenCache删除子节点:" + event.getData().getPath());
} else if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)) {
System.out.println("PathChildrenCache修改子节点路径:" + event.getData().getPath());
System.out.println("PathChildrenCache修改子节点数据:" + new String(event.getData().getData()));
}
}));
// 创建TreeCache实例监听"user"节点
TreeCache cache = TreeCache.newBuilder(client, "/user").setCacheData(false).build();
cache.getListenable().addListener((c, event) -> {
if (event.getData() != null) {
System.out.println("TreeCache,type=" + event.getType() + " path=" + event.getData().getPath());
} else {
System.out.println("TreeCache,type=" + event.getType());
}
});
cache.start();
System.in.read();
}
}
此时ZooKeeper 集群中存在 /user/test1 和 /user/test2 两个节点,启动上述测试代码,得到的输出如下:
NodeCache节点初始化数据为test //NodeCache的相关输出
获取子节点列表:// PathChildrenCache的相关输出
xxx
xxx2
// TreeCache监听到的事件
TreeCache,type=NODE_ADDED path=/user
TreeCache,type=NODE_ADDED path=/user/test1
TreeCache,type=NODE_ADDED path=/user/test2
TreeCache,type=INITIALIZED
接下来,我们在 ZooKeeper 命令行客户端中更新 /user 节点中的数据:
得到如下输出:
TreeCache,type=NODE_UPDATED path=/user
NodeCache节点路径/user节点数据为userData
创建 /user/test3 节点:
得到输出:
TreeCache,type=NODE_ADDED path=/user/test3
2020-06-26T08:35:22.393 CHILD_ADDED
PathChildrenCache添加子节点:/user/test3
PathChildrenCache子节点数据:xxx3
更新 /user/test3 节点的数据:
得到输出:
TreeCache,type=NODE_UPDATED path=/user/test3
2020-06-26T08:43:54.604 CHILD_UPDATED
PathChildrenCache修改子节点路径:/user/test3
PathChildrenCache修改子节点数据:xxx33
删除 /user/test3 节点:
得到输出:
TreeCache,type=NODE_REMOVED path=/user/test3
2020-06-26T08:44:06.329 CHILD_REMOVED
PathChildrenCache删除子节点:/user/test3
curator-x-discovery 扩展库
为了避免 curator-framework 包过于膨胀Curator 将很多其他解决方案都拆出来了作为单独的一个包例如curator-recipes、curator-x-discovery、curator-x-rpc 等。
在后面我们会使用到 curator-x-discovery 来完成一个简易 RPC 框架的注册中心模块。curator-x-discovery 扩展包是一个服务发现的解决方案。在 ZooKeeper 中,我们可以使用临时节点实现一个服务注册机制。当服务启动后在 ZooKeeper 的指定 Path 下创建临时节点,服务断掉与 ZooKeeper 的会话之后,其相应的临时节点就会被删除。这个 curator-x-discovery 扩展包抽象了这种功能,并提供了一套简单的 API 来实现服务发现机制。curator-x-discovery 扩展包的核心概念如下:
ServiceInstance。 这是 curator-x-discovery 扩展包对服务实例的抽象,由 name、id、address、port 以及一个可选的 payload 属性构成。其存储在 ZooKeeper 中的方式如下图展示的这样。
ServiceProvider。 这是 curator-x-discovery 扩展包的核心组件之一,提供了多种不同策略的服务发现方式,具体策略有轮询调度、随机和黏性(总是选择相同的一个)。得到 ServiceProvider 对象之后,我们可以调用其 getInstance() 方法,按照指定策略获取 ServiceInstance 对象(即发现可用服务实例);还可以调用 getAllInstances() 方法,获取所有 ServiceInstance 对象(即获取全部可用服务实例)。
ServiceDiscovery。 这是 curator-x-discovery 扩展包的入口类。开始必须调用 start() 方法,当使用完成应该调用 close() 方法进行销毁。
ServiceCache。 如果程序中会频繁地查询 ServiceInstance 对象,我们可以添加 ServiceCache 缓存ServiceCache 会在内存中缓存 ServiceInstance 实例的列表,并且添加相应的 Watcher 来同步更新缓存。查询 ServiceCache 的方式也是 getInstances() 方法。另外ServiceCache 上还可以添加 Listener 来监听缓存变化。
下面通过一个简单示例来说明一下 curator-x-discovery 包的使用,该示例中的 ServerInfo 记录了一个服务的 host、port 以及描述信息。
public class ZookeeperCoordinator {
private ServiceDiscovery<ServerInfo> serviceDiscovery;
private ServiceCache<ServerInfo> serviceCache;
private CuratorFramework client;
private String root;
// 这里的JsonInstanceSerializer是将ServerInfo序列化成Json
private InstanceSerializer serializer =
new JsonInstanceSerializer<>(ServerInfo.class);
ZookeeperCoordinator(Config config) throws Exception {
this.root = config.getPath();
// 创建Curator客户端
client = CuratorFrameworkFactory.newClient(
config.getHostPort(), new ExponentialBackoffRetry(...));
client.start(); // 启动Curator客户端
client.blockUntilConnected(); // 阻塞当前线程,等待连接成功
// 创建ServiceDiscovery
serviceDiscovery = ServiceDiscoveryBuilder
.builder(ServerInfo.class)
.client(client) // 依赖Curator客户端
.basePath(root) // 管理的Zk路径
.watchInstances(true) // 当ServiceInstance加载
.serializer(serializer)
.build();
serviceDiscovery.start(); // 启动ServiceDiscovery
// 创建ServiceCache监Zookeeper相应节点的变化也方便后续的读取
serviceCache = serviceDiscovery.serviceCacheBuilder()
.name(root)
.build();
serviceCache.start(); // 启动ServiceCache
}
public void registerRemote(ServerInfo serverInfo)throws Exception{
// 将ServerInfo对象转换成ServiceInstance对象
ServiceInstance<ServerInfo> thisInstance =
ServiceInstance.<ServerInfo>builder()
.name(root)
.id(UUID.randomUUID().toString()) // 随机生成的UUID
.address(serverInfo.getHost()) // host
.port(serverInfo.getPort()) // port
.payload(serverInfo) // payload
.build();
// 将ServiceInstance写入到Zookeeper中
serviceDiscovery.registerService(thisInstance);
}
public List<ServerInfo> queryRemoteNodes() {
List<ServerInfo> ServerInfoDetails = new ArrayList<>();
// 查询 ServiceCache 获取全部的 ServiceInstance 对象
List<ServiceInstance<ServerInfo>> serviceInstances =
serviceCache.getInstances();
serviceInstances.forEach(serviceInstance -> {
// 从每个ServiceInstance对象的playload字段中反序列化得
// 到ServerInfo实例
ServerInfo instance = serviceInstance.getPayload();
ServerInfoDetails.add(instance);
});
return ServerInfoDetails;
}
}
curator-recipes 简介
Recipes 是 Curator 对常见分布式场景的解决方案,这里我们只是简单介绍一下,具体的使用和原理,就先不做深入分析了。
Queues。提供了多种的分布式队列解决方法比如权重队列、延迟队列等。在生产环境中很少将 ZooKeeper 用作分布式队列,只适合在压力非常小的情况下,才使用该解决方案,所以建议你要适度使用。
Counters。全局计数器是分布式系统中很常用的工具curator-recipes 提供了 SharedCount、DistributedAtomicLong 等组件,帮助开发人员实现分布式计数器功能。
Locks。java.util.concurrent.locks 中提供的各种锁相信你已经有所了解了在微服务架构中分布式锁也是一项非常基础的服务组件curator-recipes 提供了多种基于 ZooKeeper 实现的分布式锁,满足日常工作中对分布式锁的需求。
Barries。curator-recipes 提供的分布式栅栏可以实现多个服务之间协同工作,具体实现有 DistributedBarrier 和 DistributedDoubleBarrier。
Elections。实现的主要功能是在多个参与者中选举出 Leader然后由 Leader 节点作为操作调度、任务监控或是队列消费的执行者。curator-recipes 给出的实现是 LeaderLatch。
总结
本课时我们重点介绍了 Apache Curator 相关的内容:
首先将 Apache Curator 与其他 ZooKeeper 客户端进行了对比Apache Curator 的易用性是选择 Apache Curator 的重要原因。
接下来,我们通过示例介绍了 Apache Curator 的基本使用方式以及实际使用过程中的一些注意点。
然后,介绍了 curator-x-discovery 扩展库的基本概念和使用。
最后,简单介绍了 curator-recipes 提供的强大功能。
关于 Apache Curator你有什么其他的见解欢迎你在评论区给我留言与我分享。
zk-demo 链接https://github.com/xxxlxy2008/zk-demo 。

View File

@@ -0,0 +1,630 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08 代理模式与常见实现
动态代理机制在 Java 中有着广泛的应用例如Spring AOP、MyBatis、Hibernate 等常用的开源框架都使用到了动态代理机制。当然Dubbo 中也使用到了动态代理,在后面开发简易版 RPC 框架的时候,我们还会参考 Dubbo 使用动态代理机制来屏蔽底层的网络传输以及服务发现的相关实现。
本课时我们主要从基础知识开始讲起,首先介绍代理模式的基本概念,之后重点介绍 JDK 动态代理的使用以及底层实现原理,同时还会说明 JDK 动态代理的一些局限性,最后再介绍基于字节码生成的动态代理。
代理模式
代理模式是 23 种面向对象的设计模式中的一种,它的类图如下所示:
图中的 Subject 是程序中的业务逻辑接口RealSubject 是实现了 Subject 接口的真正业务类Proxy 是实现了 Subject 接口的代理类,封装了一个 RealSubject 引用。在程序中不会直接调用 RealSubject 对象的方法,而是使用 Proxy 对象实现相关功能。
Proxy.operation() 方法的实现会调用其中封装的 RealSubject 对象的 operation() 方法执行真正的业务逻辑。代理的作用不仅仅是正常地完成业务逻辑还会在业务逻辑前后添加一些代理逻辑也就是说Proxy.operation() 方法会在 RealSubject.operation() 方法调用前后进行一些预处理以及一些后置处理。这就是我们常说的“代理模式”。
使用代理模式可以控制程序对 RealSubject 对象的访问,如果发现异常的访问,可以直接限流或是返回,也可以在执行业务处理的前后进行相关的预处理和后置处理,帮助上层调用方屏蔽底层的细节。例如,在 RPC 框架中,代理可以完成序列化、网络 I/O 操作、负载均衡、故障恢复以及服务发现等一系列操作,而上层调用方只感知到了一次本地调用。
代理模式还可以用于实现延迟加载的功能。我们知道查询数据库是一个耗时的操作,而有些时候查询到的数据也并没有真正被程序使用。延迟加载功能就可以有效地避免这种浪费,系统访问数据库时,首先可以得到一个代理对象,此时并没有执行任何数据库查询操作,代理对象中自然也没有真正的数据;当系统真正需要使用数据时,再调用代理对象完成数据库查询并返回数据。常见 ORM 框架例如MyBatis、 Hibernate中的延迟加载的原理大致也是如此。
另外代理对象可以协调真正RealSubject 对象与调用者之间的关系,在一定程度上实现了解耦的效果。
JDK 动态代理
上面介绍的这种代理模式实现也被称为“静态代理模式”这是因为在编译阶段就要为每个RealSubject 类创建一个 Proxy 类,当需要代理的类很多时,就会出现大量的 Proxy 类。
这种场景下,我们可以使用 JDK 动态代理解决这个问题。JDK 动态代理的核心是InvocationHandler 接口。这里提供一个 InvocationHandler 的Demo 实现,代码如下:
public class DemoInvokerHandler implements InvocationHandler {
private Object target; // 真正的业务对象也就是RealSubject对象
public DemoInvokerHandler(Object target) { // 构造方法
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// ...在执行业务方法之前的预处理...
Object result = method.invoke(target, args);
// ...在执行业务方法之后的后置处理...
return result;
}
public Object getProxy() {
// 创建代理对象
return Proxy.newProxyInstance(Thread.currentThread()
.getContextClassLoader(),
target.getClass().getInterfaces(), this);
}
}
接下来,我们可以创建一个 main() 方法来模拟上层调用者,创建并使用动态代理:
public class Main {
public static void main(String[] args) {
Subject subject = new RealSubject();
DemoInvokerHandler invokerHandler =
new DemoInvokerHandler(subject);
// 获取代理对象
Subject proxy = (Subject) invokerHandler.getProxy();
// 调用代理对象的方法它会调用DemoInvokerHandler.invoke()方法
proxy.operation();
}
}
对于需要相同代理逻辑的业务类,只需要提供一个 InvocationHandler 接口实现类即可。在 Java 运行的过程中JDK会为每个 RealSubject 类动态生成相应的代理类并加载到 JVM 中,然后创建对应的代理实例对象,返回给上层调用者。
了解了 JDK 动态代理的基本使用之后,下面我们就来分析 JDK动态代理创建代理类的底层实现原理。不同JDK版本的 Proxy 类实现可能有细微差别,但核心思路不变,这里使用 1.8.0 版本的 JDK。
JDK 动态代理相关实现的入口是 Proxy.newProxyInstance() 这个静态方法它的三个参数分别是加载动态生成的代理类的类加载器、业务类实现的接口和上面介绍的InvocationHandler对象。Proxy.newProxyInstance()方法的具体实现如下:
public static Object newProxyInstance(ClassLoader loader,
Class[] interfaces, InvocationHandler h)
throws IllegalArgumentException {
final Class<?>[] intfs = interfaces.clone();
// ...省略权限检查等代码
Class<?> cl = getProxyClass0(loader, intfs); // 获取代理类
// ...省略try/catch代码块和相关异常处理
// 获取代理类的构造方法
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
return cons.newInstance(new Object[]{h}); // 创建代理对象
}
通过 newProxyInstance()方法的实现可以看到JDK 动态代理是在 getProxyClass0() 方法中完成代理类的生成和加载。getProxyClass0() 方法的具体实现如下:
private static Class getProxyClass0 (ClassLoader loader,
Class... interfaces) {
// 边界检查,限制接口数量(略)
// 如果指定的类加载器中已经创建了实现指定接口的代理类,则查找缓存;
// 否则通过ProxyClassFactory创建实现指定接口的代理类
return proxyClassCache.get(loader, interfaces);
}
proxyClassCache 是定义在 Proxy 类中的静态字段,主要用于缓存已经创建过的代理类,定义如下:
private static final WeakCache[], Class> proxyClassCache
= new WeakCache<>(new KeyFactory(),
new ProxyClassFactory());
WeakCache.get() 方法会首先尝试从缓存中查找代理类,如果查找不到,则会创建 Factory 对象并调用其 get() 方法获取代理类。Factory 是 WeakCache 中的内部类Factory.get() 方法会调用 ProxyClassFactory.apply() 方法创建并加载代理类。
ProxyClassFactory.apply() 方法首先会检测代理类需要实现的接口集合,然后确定代理类的名称,之后创建代理类并将其写入文件中,最后加载代理类,返回对应的 Class 对象用于后续的实例化代理类对象。该方法的具体实现如下:
public Class apply(ClassLoader loader, Class[] interfaces) {
// ... 对interfaces集合进行一系列检测
// ... 选择定义代理类的包名(略)
// 代理类的名称是通过包名、代理类名称前缀以及编号这三项组成的
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
// 生成代理类,并写入文件
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
// 加载代理类并返回Class对象
return defineClass0(loader, proxyName, proxyClassFile, 0,
proxyClassFile.length);
}
ProxyGenerator.generateProxyClass() 方法会按照指定的名称和接口集合生成代理类的字节码,并根据条件决定是否保存到磁盘上。该方法的具体代码如下:
public static byte[] generateProxyClass(final String name,
Class[] interfaces) {
ProxyGenerator gen = new ProxyGenerator(name, interfaces);
// 动态生成代理类的字节码,具体生成过程不再详细介绍,感兴趣的读者可以继续分析
final byte[] classFile = gen.generateClassFile();
// 如果saveGeneratedFiles值为true会将生成的代理类的字节码保存到文件中
if (saveGeneratedFiles) {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction() {
public Void run() {
// 省略try/catch代码块
FileOutputStream file = new FileOutputStream(
dotToSlash(name) + ".class");
file.write(classFile);
file.close();
return null;
}
}
);
}
return classFile; // 返回上面生成的代理类的字节码
}
最后为了清晰地看到JDK动态生成的代理类的真正定义我们需要将上述生成的代理类的字节码进行反编译。上述示例为RealSubject生成的代理类反编译后得到的代码如下
public final class $Proxy37
extends Proxy implements Subject { // 实现了Subject接口
// 这里省略了从Object类继承下来的相关方法和属性
private static Method m3;
static {
// 省略了try/catch代码块
// 记录了operation()方法对应的Method对象
m3 = Class.forName("com.xxx.Subject")
.getMethod("operation", new Class[0]);
}
// 构造方法的参数就是我们在示例中使用的DemoInvokerHandler对象
public $Proxy11(InvocationHandler var1) throws {
super(var1);
}
public final void operation() throws {
// 省略了try/catch代码块
// 调用DemoInvokerHandler对象的invoke()方法
// 最终调用RealSubject对象的对应方法
super.h.invoke(this, m3, (Object[]) null);
}
}
至此JDK 动态代理的基本使用以及核心原理就介绍完了。简单总结一下JDK 动态代理的实现原理是动态创建代理类并通过指定类加载器进行加载在创建代理对象时将InvocationHandler对象作为构造参数传入。当调用代理对象时会调用 InvocationHandler.invoke() 方法,从而执行代理逻辑,并最终调用真正业务对象的相应方法。
CGLib
JDK 动态代理是 Java 原生支持的不需要任何外部依赖但是正如上面分析的那样它只能基于接口进行代理对于没有继承任何接口的类JDK 动态代理就没有用武之地了。
如果想对没有实现任何接口的类进行代理,可以考虑使用 CGLib。
CGLibCode Generation Library是一个基于 ASM 的字节码生成库它允许我们在运行时对字节码进行修改和动态生成。CGLib 采用字节码技术实现动态代理功能,其底层原理是通过字节码技术为目标类生成一个子类,并在该子类中采用方法拦截的方式拦截所有父类方法的调用,从而实现代理的功能。
因为 CGLib 使用生成子类的方式实现动态代理,所以无法代理 final 关键字修饰的方法因为final 方法是不能够被重写的。这样的话CGLib 与 JDK 动态代理之间可以相互补充:在目标类实现接口时,使用 JDK 动态代理创建代理对象;当目标类没有实现接口时,使用 CGLib 实现动态代理的功能。在 Spring、MyBatis 等多种开源框架中都可以看到JDK动态代理与 CGLib 结合使用的场景。
CGLib 的实现有两个重要的成员组成。
Enhancer指定要代理的目标对象以及实际处理代理逻辑的对象最终通过调用 create() 方法得到代理对象,对这个对象所有的非 final 方法的调用都会转发给 MethodInterceptor 进行处理。
MethodInterceptor动态代理对象的方法调用都会转发到intercept方法进行增强。
这两个组件的使用与 JDK 动态代理中的 Proxy 和 InvocationHandler 相似。
下面我们通过一个示例简单介绍 CGLib 的使用。在使用 CGLib 创建动态代理类时,首先需要定义一个 Callback 接口的实现, CGLib 中也提供了多个Callback接口的子接口如下图所示
这里以 MethodInterceptor 接口为例进行介绍,首先我们引入 CGLib 的 maven 依赖:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
下面是 CglibProxy 类的具体代码,它实现了 MethodInterceptor 接口:
public class CglibProxy implements MethodInterceptor {
// 初始化Enhancer对象
private Enhancer enhancer = new Enhancer();
public Object getProxy(Class clazz) {
enhancer.setSuperclass(clazz); // 指定生成的代理类的父类
enhancer.setCallback(this); // 设置Callback对象
return enhancer.create(); // 通过ASM字节码技术动态创建子类实例
}
// 实现MethodInterceptor接口的intercept()方法
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("前置处理");
Object result = proxy.invokeSuper(obj, args); // 调用父类中的方法
System.out.println("后置处理");
return result;
}
}
下面我们再编写一个要代理的目标类以及 main 方法进行测试,具体如下:
public class CGLibTest { // 目标类
public String method(String str) { // 目标方法
System.out.println(str);
return "CGLibTest.method():" + str;
}
public static void main(String[] args) {
CglibProxy proxy = new CglibProxy();
// 生成CBLibTest的代理对象
CGLibTest proxyImp = (CGLibTest)
proxy.getProxy(CGLibTest.class);
// 调用代理对象的method()方法
String result = proxyImp.method("test");
System.out.println(result);
// ----------------
// 输出如下:
// 前置代理
// test
// 后置代理
// CGLibTest.method():test
}
}
到此CGLib 基础使用的内容就介绍完了,在后面介绍 Dubbo 源码时我们还会继续介绍涉及的 CGLib 内容。
Javassist
Javassist 是一个开源的生成 Java 字节码的类库其主要优点在于简单、快速直接使用Javassist 提供的 Java API 就能动态修改类的结构,或是动态生成类。
Javassist 的使用比较简单,首先来看如何使用 Javassist 提供的 Java API 动态创建类。示例代码如下:
public class JavassistMain {
public static void main(String[] args) throws Exception {
ClassPool cp = ClassPool.getDefault(); // 创建ClassPool
// 要生成的类名称为com.test.JavassistDemo
CtClass clazz = cp.makeClass("com.test.JavassistDemo");
StringBuffer body = null;
// 创建字段,指定了字段类型、字段名称、字段所属的类
CtField field = new CtField(cp.get("java.lang.String"),
"prop", clazz);
// 指定该字段使用private修饰
field.setModifiers(Modifier.PRIVATE);
// 设置prop字段的getter/setter方法
clazz.addMethod(CtNewMethod.setter("getProp", field));
clazz.addMethod(CtNewMethod.getter("setProp", field));
// 设置prop字段的初始化值并将prop字段添加到clazz中
clazz.addField(field, CtField.Initializer.constant("MyName"));
// 创建构造方法,指定了构造方法的参数类型和构造方法所属的类
CtConstructor ctConstructor = new CtConstructor(
new CtClass[]{}, clazz);
// 设置方法体
body = new StringBuffer();
body.append("{\n prop=\"MyName\";\n}");
ctConstructor.setBody(body.toString());
clazz.addConstructor(ctConstructor); // 将构造方法添加到clazz中
// 创建execute()方法,指定了方法返回值、方法名称、方法参数列表以及
// 方法所属的类
CtMethod ctMethod = new CtMethod(CtClass.voidType, "execute",
new CtClass[]{}, clazz);
// 指定该方法使用public修饰
ctMethod.setModifiers(Modifier.PUBLIC);
// 设置方法体
body = new StringBuffer();
body.append("{\n System.out.println(\"execute():\" " +
"+ this.prop);");
body.append("\n}");
ctMethod.setBody(body.toString());
clazz.addMethod(ctMethod); // 将execute()方法添加到clazz中
// 将上面定义的JavassistDemo类保存到指定的目录
clazz.writeFile("/Users/xxx/");
// 加载clazz类并创建对象
Class<?> c = clazz.toClass();
Object o = c.newInstance();
// 调用execute()方法
Method method = o.getClass().getMethod("execute",
new Class[]{});
method.invoke(o, new Object[]{});
}
}
执行上述代码之后,在指定的目录下可以找到生成的 JavassistDemo.class 文件,将其反编译,得到 JavassistDemo 的代码如下:
public class JavassistDemo {
private String prop = "MyName";
public JavassistDemo() {
prop = "MyName";
}
public void setProp(String paramString) {
this.prop = paramString;
}
public String getProp() {
return this.prop;
}
public void execute() {
System.out.println("execute():" + this.prop);
}
}
Javassist 也可以实现动态代理功能,底层的原理也是通过创建目标类的子类的方式实现的。这里使用 Javassist 为上面生成的 JavassitDemo 创建一个代理对象,具体实现如下:
public class JavassitMain2 {
public static void main(String[] args) throws Exception {
ProxyFactory factory = new ProxyFactory();
// 指定父类ProxyFactory会动态生成继承该父类的子类
factory.setSuperclass(JavassistDemo.class);
// 设置过滤器,判断哪些方法调用需要被拦截
factory.setFilter(new MethodFilter() {
public boolean isHandled(Method m) {
if (m.getName().equals("execute")) {
return true;
}
return false;
}
});
// 设置拦截处理
factory.setHandler(new MethodHandler() {
@Override
public Object invoke(Object self, Method thisMethod,
Method proceed, Object[] args) throws Throwable {
System.out.println("前置处理");
Object result = proceed.invoke(self, args);
System.out.println("执行结果:" + result);
System.out.println("后置处理");
return result;
}
});
// 创建JavassistDemo的代理类并创建代理对象
Class<?> c = factory.createClass();
JavassistDemo JavassistDemo = (JavassistDemo) c.newInstance();
JavassistDemo.execute(); // 执行execute()方法,会被拦截
System.out.println(JavassistDemo.getProp());
}
}
Javassist 的基础知识就介绍到这里。Javassist可以直接使用 Java 语言的字符串生成类还是比较好用的。Javassist 的性能也比较好,是 Dubbo 默认的代理生成方式。
总结
本课时我们首先介绍了代理模式的核心概念和用途,让你对代理模式有初步的了解;然后介绍了 JDK 动态代理使用,并深入到 JDK 源码中分析了 JDK 动态代理的实现原理,以及 JDK 动态代理的局限;最后我们介绍了 CGLib和Javassist这两款代码生成工具的基本使用简述了两者生成代理的原理。
那你还知道哪些实现动态代理的方式呢?欢迎你在评论区留言讨论。

View File

@@ -0,0 +1,122 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
09 Netty 入门,用它做网络编程都说好(上)
了解 Java 的同学应该知道JDK 本身提供了一套 NIO 的 API但是这一套原生的 API 存在一系列的问题。
Java NIO 的 API 非常复杂。 要写出成熟可用的 Java NIO 代码,需要熟练掌握 JDK 中的 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等组件,还要理解其中一些反人类的设计以及底层原理,这对新手来说是非常不友好的。
如果直接使用 Java NIO 进行开发,难度和开发量会非常大。我们需要自己补齐很多可靠性方面的实现,例如,网络波动导致的连接重连、半包读写等。这就会导致一些本末倒置的情况出现:核心业务逻辑比较简单,但补齐其他公共能力的代码非常多,开发耗时比较长。这时就需要一个统一的 NIO 框架来封装这些公共能力了。
JDK 自身的 Bug。其中比较出名的就要属 Epoll Bug 了,这个 Bug 会导致 Selector 空轮询CPU 使用率达到 100%,这样就会导致业务逻辑无法执行,降低服务性能。
Netty 在 JDK 自带的 NIO API 基础之上进行了封装,解决了 JDK 自身的一些问题,具备如下优点:
入门简单,使用方便,文档齐全,无其他依赖,只依赖 JDK 就够了。
高性能,高吞吐,低延迟,资源消耗少。
灵活的线程模型支持阻塞和非阻塞的I/O 模型。
代码质量高,目前主流版本基本没有 Bug。
正因为 Netty 有以上优点,所以很多互联网公司以及开源的 RPC 框架都将其作为网络通信的基础库例如Apache Spark、Apache Flink、 Elastic Search 以及我们本课程分析的 Dubbo 等。
下面我们将从 I/O 模型和线程模型的角度详细为你介绍 Netty 的核心设计,进而帮助你全面掌握 Netty 原理。
Netty I/O 模型设计
在进行网络 I/O 操作的时候,用什么样的方式读写数据将在很大程度上决定了 I/O 的性能。作为一款优秀的网络基础库Netty 就采用了 NIO 的 I/O 模型,这也是其高性能的重要原因之一。
1. 传统阻塞 I/O 模型
在传统阻塞型 I/O 模型(即我们常说的 BIO如下图所示每个请求都需要独立的线程完成读数据、业务处理以及写回数据的完整操作。
一个线程在同一时刻只能与一个连接绑定,如下图所示,当请求的并发量较大时,就需要创建大量线程来处理连接,这就会导致系统浪费大量的资源进行线程切换,降低程序的性能。我们知道,网络数据的传输速度是远远慢于 CPU 的处理速度连接建立后并不总是有数据可读连接也并不总是可写那么线程就只能阻塞等待CPU 的计算能力不能得到充分发挥,同时还会导致大量线程的切换,浪费资源。
2. I/O 多路复用模型
针对传统的阻塞 I/O 模型的缺点I/O 复用的模型在性能方面有不小的提升。I/O 复用模型中的多个连接会共用一个 Selector 对象,由 Selector 感知连接的读写事件,而此时的线程数并不需要和连接数一致,只需要很少的线程定期从 Selector 上查询连接的读写状态即可无须大量线程阻塞等待连接。当某个连接有新的数据可以处理时操作系统会通知线程线程从阻塞状态返回开始进行读写操作以及后续的业务逻辑处理。I/O 复用的模型如下图所示:
Netty 就是采用了上述 I/O 复用的模型。由于多路复用器 Selector 的存在可以同时并发处理成百上千个网络连接大大增加了服务器的处理能力。另外Selector 并不会阻塞线程,也就是说当一个连接不可读或不可写的时候,线程可以去处理其他可读或可写的连接,这就充分提升了 I/O 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程切换。如下图所示:
从数据处理的角度来看,传统的阻塞 I/O 模型处理的是字节流或字符流,也就是以流式的方式顺序地从一个数据流中读取一个或多个字节,并且不能随意改变读取指针的位置。而在 NIO 中则抛弃了这种传统的 I/O 流概念,引入了 Channel 和 Buffer 的概念,可以从 Channel 中读取数据到 Buffer 中或将数据从 Buffer 中写入到 Channel。Buffer 不像传统 I/O 中的流那样必须顺序操作,在 NIO 中可以读写 Buffer 中任意位置的数据。
Netty 线程模型设计
服务器程序在读取到二进制数据之后,首先需要通过编解码,得到程序逻辑可以理解的消息,然后将消息传入业务逻辑进行处理,并产生相应的结果,返回给客户端。编解码逻辑、消息派发逻辑、业务处理逻辑以及返回响应的逻辑,是放到一个线程里面串行执行,还是分配到不同的线程中执行,会对程序的性能产生很大的影响。所以,优秀的线程模型对一个高性能网络库来说是至关重要的。
Netty 采用了 Reactor 线程模型的设计。 Reactor 模式,也被称为 Dispatcher 模式,核心原理是 Selector 负责监听 I/O 事件,在监听到 I/O 事件之后分发Dispatch给相关线程进行处理。
为了帮助你更好地了解 Netty 线程模型的设计理念,我们将从最基础的单 Reactor 单线程模型开始介绍,然后逐步增加模型的复杂度,最终到 Netty 目前使用的非常成熟的线程模型设计。
1. 单 Reactor 单线程
Reactor 对象监听客户端请求事件,收到事件后通过 Dispatch 进行分发。如果是连接建立的事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接建立之后的业务请求。如果不是连接建立的事件,而是数据的读写事件,则 Reactor 会将事件分发对应的 Handler 来处理,由这里唯一的线程调用 Handler 对象来完成读取数据、业务处理、发送响应的完整流程。当然,该过程中也可能会出现连接不可读或不可写等情况,该单线程会去执行其他 Handler 的逻辑,而不是阻塞等待。具体情况如下图所示:
单 Reactor 单线程的优点就是:线程模型简单,没有引入多线程,自然也就没有多线程并发和竞争的问题。
但其缺点也非常明显,那就是性能瓶颈问题,一个线程只能跑在一个 CPU 上,能处理的连接数是有限的,无法完全发挥多核 CPU 的优势。一旦某个业务逻辑耗时较长,这唯一的线程就会卡在上面,无法处理其他连接的请求,程序进入假死的状态,可用性也就降低了。正是由于这种限制,一般只会在客户端使用这种线程模型。
2. 单 Reactor 多线程
在单 Reactor 多线程的架构中Reactor 监控到客户端请求之后如果连接建立的请求则由Acceptor 通过 accept 处理,然后创建一个 Handler 对象处理连接建立之后的业务请求。如果不是连接建立请求,则 Reactor 会将事件分发给调用连接对应的 Handler 来处理。到此为止,该流程与单 Reactor 单线程的模型基本一致,唯一的区别就是执行 Handler 逻辑的线程隶属于一个线程池。
单 Reactor 多线程模型
很明显,单 Reactor 多线程的模型可以充分利用多核 CPU 的处理能力,提高整个系统的吞吐量,但引入多线程模型就要考虑线程并发、数据共享、线程调度等问题。在这个模型中,只有一个线程来处理 Reactor 监听到的所有 I/O 事件,其中就包括连接建立事件以及读写事件,当连接数不断增大的时候,这个唯一的 Reactor 线程也会遇到瓶颈。
3. 主从 Reactor 多线程
为了解决单 Reactor 多线程模型中的问题,我们可以引入多个 Reactor。其中Reactor 主线程负责通过 Acceptor 对象处理 MainReactor 监听到的连接建立事件当Acceptor 完成网络连接的建立之后MainReactor 会将建立好的连接分配给 SubReactor 进行后续监听。
当一个连接被分配到一个 SubReactor 之上时,会由 SubReactor 负责监听该连接上的读写事件。当有新的读事件OP_READ发生时Reactor 子线程就会调用对应的 Handler 读取数据,然后分发给 Worker 线程池中的线程进行处理并返回结果。待处理结束之后Handler 会根据处理结果调用 send 将响应返回给客户端当然此时连接要有可写事件OP_WRITE才能发送数据。
主从 Reactor 多线程模型
主从 Reactor 多线程的设计模式解决了单一 Reactor 的瓶颈。主从 Reactor 职责明确,主 Reactor 只负责监听连接建立事件SubReactor只负责监听读写事件。整个主从 Reactor 多线程架构充分利用了多核 CPU 的优势,可以支持扩展,而且与具体的业务逻辑充分解耦,复用性高。但不足的地方是,在交互上略显复杂,需要一定的编程门槛。
4. Netty 线程模型
Netty 同时支持上述几种线程模式Netty 针对服务器端的设计是在主从 Reactor 多线程模型的基础上进行的修改,如下图所示:
Netty 抽象出两组线程池BossGroup 专门用于接收客户端的连接WorkerGroup 专门用于网络的读写。BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup相当于一个事件循环组其中包含多个事件循环 ,每一个事件循环是 NioEventLoop。
NioEventLoop 表示一个不断循环的、执行处理任务的线程,每个 NioEventLoop 都有一个Selector 对象与之对应,用于监听绑定在其上的连接,这些连接上的事件由 Selector 对应的这条线程处理。每个 NioEventLoopGroup 可以含有多个 NioEventLoop也就是多个线程。
每个 Boss NioEventLoop 会监听 Selector 上连接建立的 accept 事件,然后处理 accept 事件与客户端建立网络连接,生成相应的 NioSocketChannel 对象,一个 NioSocketChannel 就表示一条网络连接。之后会将 NioSocketChannel 注册到某个 Worker NioEventLoop 上的 Selector 中。
每个 Worker NioEventLoop 会监听对应 Selector 上的 read/write 事件,当监听到 read/write 事件的时候,会通过 Pipeline 进行处理。一个 Pipeline 与一个 Channel 绑定,在 Pipeline 上可以添加多个 ChannelHandler每个 ChannelHandler 中都可以包含一定的逻辑例如编解码等。Pipeline 在处理请求的时候,会按照我们指定的顺序调用 ChannelHandler。
总结
在本课时我们重点介绍了网络 I/O 的一些背景知识,以及 Netty 的一些宏观设计模型。
首先,我们介绍了 Java NIO 的一些缺陷和不足,这也是 Netty 等网络库出现的重要原因之一。
接下来,我们介绍了 Netty 在 I/O 模型上的设计,阐述了 I/O 多路复用的优势。
最后,我们从基础的单 Reactor 单线程模型开始,一步步深入,介绍了常见的网络 I/O 线程模型,并介绍了 Netty 目前使用的线程模型。
当然,关于 Netty 的相关内容,也欢迎你在留言区与我分享和交流。

View File

@@ -0,0 +1,230 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
10 Netty 入门,用它做网络编程都说好(下)
在上一课时,我们从 I/O 模型以及线程模型两个角度,宏观介绍了 Netty 的设计。在本课时,我们就深入到 Netty 内部,介绍一下 Netty 框架核心组件的功能,并概述它们的实现原理,进一步帮助你了解 Netty 的内核。
这里我们依旧采用之前的思路来介绍 Netty 的核心组件:首先是 Netty 对 I/O 模型设计中概念的抽象,如 Selector 等组件;接下来是线程模型的相关组件介绍,主要是 NioEventLoop、NioEventLoopGroup 等;最后再深入剖析 Netty 处理数据的相关组件,例如 ByteBuf、内存管理的相关知识。
Channel
Channel 是 Netty 对网络连接的抽象,核心功能是执行网络 I/O 操作。不同协议、不同阻塞类型的连接对应不同的 Channel 类型。我们一般用的都是 NIO 的 Channel下面是一些常用的 NIO Channel 类型。
NioSocketChannel对应异步的 TCP Socket 连接。
NioServerSocketChannel对应异步的服务器端 TCP Socket 连接。
NioDatagramChannel对应异步的 UDP 连接。
上述异步 Channel 主要提供了异步的网络 I/O 操作,例如:建立连接、读写操作等。异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用返回时所请求的 I/O 操作已完成。I/O 操作返回的是一个 ChannelFuture 对象,无论 I/O 操作是否成功Channel 都可以通过监听器通知调用方,我们通过向 ChannelFuture 上注册监听器来监听 I/O 操作的结果。
Netty 也支持同步 I/O 操作,但在实践中几乎不使用。绝大多数情况下,我们使用的是 Netty 中异步 I/O 操作。虽然立即返回一个 ChannelFuture 对象,但不能立刻知晓 I/O 操作是否成功,这时我们就需要向 ChannelFuture 中注册一个监听器,当操作执行成功或失败时,监听器会自动触发注册的监听事件。
另外Channel 还提供了检测当前网络连接状态等功能,这些可以帮助我们实现网络异常断开后自动重连的功能。
Selector
Selector 是对多路复用器的抽象,也是 Java NIO 的核心基础组件之一。Netty 就是基于 Selector 对象实现 I/O 多路复用的,在 Selector 内部,会通过系统调用不断地查询这些注册在其上的 Channel 是否有已就绪的 I/O 事件例如可读事件OP_READ、可写事件OP_WRITE或是网络连接事件OP_ACCEPT而无须使用用户线程进行轮询。这样我们就可以用一个线程监听多个 Channel 上发生的事件。
ChannelPipeline&ChannelHandler
提到 Pipeline你可能最先想到的是 Linux 命令中的管道它可以实现将一条命令的输出作为另一条命令的输入。Netty 中的 ChannelPipeline 也可以实现类似的功能ChannelPipeline 会将一个 ChannelHandler 处理后的数据作为下一个 ChannelHandler 的输入。
下图我们引用了 Netty Javadoc 中对 ChannelPipeline 的说明,描述了 ChannelPipeline 中 ChannelHandler 通常是如何处理 I/O 事件的。Netty 中定义了两种事件类型入站Inbound事件和出站Outbound事件。这两种事件就像 Linux 管道中的数据一样,在 ChannelPipeline 中传递事件之中也可能会附加数据。ChannelPipeline 之上可以注册多个 ChannelHandlerChannelInboundHandler 或 ChannelOutboundHandler我们在 ChannelHandler 注册的时候决定处理 I/O 事件的顺序,这就是典型的责任链模式。
从图中我们还可以看到I/O 事件不会在 ChannelPipeline 中自动传播而是需要调用ChannelHandlerContext 中定义的相应方法进行传播例如fireChannelRead() 方法和 write() 方法等。
这里我们举一个简单的例子,如下所示,在该 ChannelPipeline 上,我们添加了 5 个 ChannelHandler 对象:
ChannelPipeline p = socketChannel.pipeline();
p.addLast("1", new InboundHandlerA());
p.addLast("2", new InboundHandlerB());
p.addLast("3", new OutboundHandlerA());
p.addLast("4", new OutboundHandlerB());
p.addLast("5", new InboundOutboundHandlerX());
对于入站Inbound事件处理序列为1 → 2 → 5
对于出站Outbound事件处理序列为5 → 4 → 3。
可见入站Inbound与出站Outbound事件处理顺序正好相反。
入站Inbound事件一般由 I/O 线程触发。举个例子,我们自定义了一种消息协议,一条完整的消息是由消息头和消息体两部分组成,其中消息头会含有消息类型、控制位、数据长度等元数据,消息体则包含了真正传输的数据。在面对一块较大的数据时,客户端一般会将数据切分成多条消息发送,服务端接收到数据后,一般会先进行解码和缓存,待收集到长度足够的字节数据,组装成有固定含义的消息之后,才会传递给下一个 ChannelInboudHandler 进行后续处理。
在 Netty 中就提供了很多 Encoder 的实现用来解码读取到的数据Encoder 会处理多次 channelRead() 事件,等拿到有意义的数据之后,才会触发一次下一个 ChannelInboundHandler 的 channelRead() 方法。
出站Outbound事件与入站Inbound事件相反一般是由用户触发的。
ChannelHandler 接口中并没有定义方法来处理事件而是由其子类进行处理的如下图所示ChannelInboundHandler 拦截并处理入站事件ChannelOutboundHandler 拦截并处理出站事件。
Netty 提供的 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter 主要是帮助完成事件流转功能的,即自动调用传递事件的相应方法。这样,我们在自定义 ChannelHandler 实现类的时候,就可以直接继承相应的 Adapter 类,并覆盖需要的事件处理方法,其他不关心的事件方法直接使用默认实现即可,从而提高开发效率。
ChannelHandler 中的很多方法都需要一个 ChannelHandlerContext 类型的参数ChannelHandlerContext 抽象的是 ChannleHandler 之间的关系以及 ChannelHandler 与ChannelPipeline 之间的关系。ChannelPipeline 中的事件传播主要依赖于ChannelHandlerContext 实现,在 ChannelHandlerContext 中维护了 ChannelHandler 之间的关系,所以我们可以从 ChannelHandlerContext 中得到当前 ChannelHandler 的后继节点,从而将事件传播到后续的 ChannelHandler。
ChannelHandlerContext 继承了 AttributeMap所以提供了 attr() 方法设置和删除一些状态属性信息,我们可将业务逻辑中所需使用的状态属性值存入到 ChannelHandlerContext 中然后这些属性就可以随它传播了。Channel 中也维护了一个 AttributeMap与 ChannelHandlerContext 中的 AttributeMap从 Netty 4.1 开始,都是作用于整个 ChannelPipeline。
通过上述分析,我们可以了解到,一个 Channel 对应一个 ChannelPipeline一个 ChannelHandlerContext 对应一个ChannelHandler。 如下图所示:
最后,需要注意的是,如果要在 ChannelHandler 中执行耗时较长的逻辑,例如,操作 DB 、进行网络或磁盘 I/O 等操作,一般会在注册到 ChannelPipeline 的同时,指定一个线程池异步执行 ChannelHandler 中的操作。
NioEventLoop
在前文介绍 Netty 线程模型的时候,我们简单提到了 NioEventLoop 这个组件,当时为了便于理解,只是简单将其描述成了一个线程。
一个 EventLoop 对象由一个永远都不会改变的线程驱动,同时一个 NioEventLoop 包含了一个 Selector 对象,可以支持多个 Channel 注册在其上,该 NioEventLoop 可以同时服务多个 Channel每个 Channel 只能与一个 NioEventLoop 绑定,这样就实现了线程与 Channel 之间的关联。
我们知道Channel 中的 I/O 操作是由 ChannelPipeline 中注册的 ChannelHandler 进行处理的,而 ChannelHandler 的逻辑都是由相应 NioEventLoop 关联的那个线程执行的。
除了与一个线程绑定之外NioEvenLoop 中还维护了两个任务队列:
普通任务队列。用户产生的普通任务可以提交到该队列中暂存NioEventLoop 发现该队列中的任务后会立即执行。这是一个多生产者、单消费者的队列Netty 使用该队列将外部用户线程产生的任务收集到一起,并在 Reactor 线程内部用单线程的方式串行执行队列中的任务。例如,外部非 I/O 线程调用了 Channel 的 write() 方法Netty 会将其封装成一个任务放入 TaskQueue 队列中,这样,所有的 I/O 操作都会在 I/O 线程中串行执行。
定时任务队列。当用户在非 I/O 线程产生定时操作时Netty 将用户的定时操作封装成定时任务,并将其放入该定时任务队列中等待相应 NioEventLoop 串行执行。
到这里我们可以看出NioEventLoop 主要做三件事:监听 I/O 事件、执行普通任务以及执行定时任务。NioEventLoop 到底分配多少时间在不同类型的任务上,是可以配置的。另外,为了防止 NioEventLoop 长时间阻塞在一个任务上,一般会将耗时的操作提交到其他业务线程池处理。
NioEventLoopGroup
NioEventLoopGroup 表示的是一组 NioEventLoop。Netty 为了能更充分地利用多核 CPU 资源,一般会有多个 NioEventLoop 同时工作至于多少线程可由用户决定Netty 会根据实际上的处理器核数计算一个默认值具体计算公式是CPU 的核心数 * 2当然我们也可以根据实际情况手动调整。
当一个 Channel 创建之后Netty 会调用 NioEventLoopGroup 提供的 next() 方法,按照一定规则获取其中一个 NioEventLoop 实例,并将 Channel 注册到该 NioEventLoop 实例,之后,就由该 NioEventLoop 来处理 Channel 上的事件。EventLoopGroup、EventLoop 以及 Channel 三者的关联关系,如下图所示:
前面我们提到过,在 Netty 服务器端中,会有 BossEventLoopGroup 和 WorkerEventLoopGroup 两个 NioEventLoopGroup。通常一个服务端口只需要一个ServerSocketChannel对应一个 Selector 和一个 NioEventLoop 线程。
BossEventLoop 负责接收客户端的连接事件,即 OP_ACCEPT 事件,然后将创建的 NioSocketChannel 交给 WorkerEventLoopGroup WorkerEventLoopGroup 会由 next() 方法选择其中一个 NioEventLoopGroup并将这个 NioSocketChannel 注册到其维护的 Selector 并对其后续的I/O事件进行处理。
如上图BossEventLoopGroup 通常是一个单线程的 EventLoopEventLoop 维护着一个 Selector 对象,其上注册了一个 ServerSocketChannelBoosEventLoop 会不断轮询 Selector 监听连接事件,在发生连接事件时,通过 accept 操作与客户端创建连接,创建 SocketChannel 对象。然后将 accept 操作得到的 SocketChannel 交给 WorkerEventLoopGroup在Reactor 模式中 WorkerEventLoopGroup 中会维护多个 EventLoop而每个 EventLoop 都会监听分配给它的 SocketChannel 上发生的 I/O 事件,并将这些具体的事件分发给业务线程池处理。
ByteBuf
通过前文的介绍,我们了解了 Netty 中数据的流向这里我们再来介绍一下数据的容器——ByteBuf。
在进行跨进程远程交互的时候我们需要以字节的形式发送和接收数据发送端和接收端都需要一个高效的数据容器来缓存字节数据ByteBuf 就扮演了这样一个数据容器的角色。
ByteBuf 类似于一个字节数组,其中维护了一个读索引和一个写索引,分别用来控制对 ByteBuf 中数据的读写操作,两者符合下面的不等式:
0 <= readerIndex <= writerIndex <= capacity
ByteBuf 提供的读写操作 API 主要操作底层的字节容器byte[]、ByteBuffer 等)以及读写索引这两指针,你若感兴趣的话,可以查阅相关的 API 说明,这里不再展开介绍。
Netty 中主要分为以下三大类 ByteBuf
Heap Buffer堆缓冲区。这是最常用的一种 ByteBuf它将数据存储在 JVM 的堆空间,其底层实现是在 JVM 堆内分配一个数组,实现数据的存储。堆缓冲区可以快速分配,当不使用时也可以由 GC 轻松释放。它还提供了直接访问底层数组的方法,通过 ByteBuf.array() 来获取底层存储数据的 byte[] 。
Direct Buffer直接缓冲区。直接缓冲区会使用堆外内存存储数据不会占用 JVM 堆的空间,使用时应该考虑应用程序要使用的最大内存容量以及如何及时释放。直接缓冲区在使用 Socket 传递数据时性能很好,当然,它也是有缺点的,因为没有了 JVM GC 的管理在分配内存空间和释放内存时比堆缓冲区更复杂Netty 主要使用内存池来解决这样的问题,这也是 Netty 使用内存池的原因之一。
Composite Buffer复合缓冲区。我们可以创建多个不同的 ByteBuf然后提供一个这些 ByteBuf 组合的视图,也就是 CompositeByteBuf。它就像一个列表可以动态添加和删除其中的 ByteBuf。
内存管理
Netty 使用 ByteBuf 对象作为数据容器,进行 I/O 读写操作,其实 Netty 的内存管理也是围绕着ByteBuf 对象高效地分配和释放。从内存管理角度来看ByteBuf 可分为 Unpooled 和 Pooled 两类。
Unpooled是指非池化的内存管理方式。每次分配时直接调用系统 API 向操作系统申请 ByteBuf在使用完成之后通过系统调用进行释放。Unpooled 将内存管理完全交给系统,不做任何特殊处理,使用起来比较方便,对于申请和释放操作不频繁、操作成本比较低的 ByteBuf 来说,是比较好的选择。
Pooled是指池化的内存管理方式。该方式会预先申请一块大内存形成内存池在需要申请 ByteBuf 空间的时候,会将内存池中一部分合理的空间封装成 ByteBuf 给服务使用,使用完成后回收到内存池中。前面提到 DirectByteBuf 底层使用的堆外内存管理比较复杂,池化技术很好地解决了这一问题。
下面我们从如何高效分配和释放内存、如何减少内存碎片以及在多线程环境下如何减少锁竞争这三个方面介绍一下 Netty 提供的 ByteBuf 池化技术。
Netty 首先会向系统申请一整块连续内存,称为 Chunk默认大小为 16 MB这一块连续的内存通过 PoolChunk 对象进行封装。之后Netty 将 Chunk 空间进一步拆分为 Page每个 Chunk 默认包含 2048 个 Page每个 Page 的大小为 8 KB。
在同一个 Chunk 中Netty 将 Page 按照不同粒度进行分层管理。如下图所示,从下数第 1 层中每个分组的大小为 1 * PageSize一共有 2048 个分组;第 2 层中每个分组大小为 2 * PageSize一共有 1024 个组;第 3 层中每个分组大小为 4 * PageSize一共有 512 个组;依次类推,直至最顶层。
1. 内存分配&释放
当服务向内存池请求内存时Netty 会将请求分配的内存数向上取整到最接近的分组大小,然后在该分组的相应层级中从左至右寻找空闲分组。例如,服务请求分配 3 * PageSize 的内存,向上取整得到的分组大小为 4 * PageSize在该层分组中找到完全空闲的一组内存进行分配即可如下图
当分组大小 4 * PageSize 的内存分配出去后,为了方便下次内存分配,分组被标记为全部已使用(图中红色标记),向上更粗粒度的内存分组被标记为部分已使用(图中黄色标记)。
Netty 使用完全平衡树的结构实现了上述算法,这个完全平衡树底层是基于一个 byte 数组构建的,如下图所示:
具体的实现逻辑这里就不再展开讲述了,你若感兴趣的话,可以参考 Netty 代码。
2. 大对象&小对象的处理
当申请分配的对象是超过 Chunk 容量的大型对象Netty 就不再使用池化管理方式了,在每次请求分配内存时单独创建特殊的非池化 PoolChunk 对象进行管理当对象内存释放时整个PoolChunk 内存释放。
如果需要一定数量空间远小于 PageSize 的 ByteBuf 对象,例如,创建 256 Byte 的 ByteBuf按照上述算法就需要为每个小 ByteBuf 对象分配一个 Page这就出现了很多内存碎片。Netty 通过再将 Page 细分的方式解决这个问题。Netty 将请求的空间大小向上取最近的 16 的倍数(或 2 的幂),规整后小于 PageSize 的小 Buffer 可分为两类。
微型对象:规整后的大小为 16 的整倍数,如 16、32、48、……、496一共 31 种大小。
小型对象:规整后的大小为 2 的幂,如 512、1024、2048、4096一共 4 种大小。
Netty 的实现会先从 PoolChunk 中申请空闲 Page同一个 Page 分为相同大小的小 Buffer 进行存储;这些 Page 用 PoolSubpage 对象进行封装PoolSubpage 内部会记录它自己能分配的小 Buffer 的规格大小、可用内存数量,并通过 bitmap 的方式记录各个小内存的使用情况(如下图所示)。虽然这种方案不能完美消灭内存碎片,但是很大程度上还是减少了内存浪费。
为了解决单个 PoolChunk 容量有限的问题Netty 将多个 PoolChunk 组成链表一起管理,然后用 PoolChunkList 对象持有链表的 head。
Netty 通过 PoolArena 管理 PoolChunkList 以及 PoolSubpage。
PoolArena 内部持有 6 个 PoolChunkList各个 PoolChunkList 持有的 PoolChunk 的使用率区间有所不同,如下图所示:
6 个 PoolChunkList 对象组成双向链表,当 PoolChunk 内存分配、释放,导致使用率变化,需要判断 PoolChunk 是否超过所在 PoolChunkList 的限定使用率范围,如果超出了,需要沿着 6 个 PoolChunkList 的双向链表找到新的合适的 PoolChunkList ,成为新的 head。同样当新建 PoolChunk 分配内存或释放空间时PoolChunk 也需要按照上面逻辑放入合适的PoolChunkList 中。
从上图可以看出,这 6 个 PoolChunkList 额定使用率区间存在交叉,这样设计的原因是:如果使用单个临界值的话,当一个 PoolChunk 被来回申请和释放,内存使用率会在临界值上下徘徊,这就会导致它在两个 PoolChunkList 链表中来回移动。
PoolArena 内部持有 2 个 PoolSubpage 数组,分别存储微型 Buffer 和小型 Buffer 的PoolSubpage。相同大小的 PoolSubpage 组成链表,不同大小的 PoolSubpage 链表的 head 节点保存在 tinySubpagePools 或者 smallSubpagePools 数组中,如下图:
3. 并发处理
内存分配释放不可避免地会遇到多线程并发场景PoolChunk 的完全平衡树标记以及 PoolSubpage 的 bitmap 标记都是多线程不安全的都是需要加锁同步的。为了减少线程间的竞争Netty 会提前创建多个 PoolArena默认数量为 2 * CPU 核心数),当线程首次请求池化内存分配,会找被最少线程持有的 PoolArena并保存线程局部变量 PoolThreadCache 中,实现线程与 PoolArena 的关联绑定。
Netty 还提供了延迟释放的功能来提升并发性能。当内存释放时PoolArena 并没有马上释放,而是先尝试将该内存关联的 PoolChunk 和 Chunk 中的偏移位置等信息存入 ThreadLocal 的固定大小缓存队列中如果该缓存队列满了则马上释放内存。当有新的分配请求时PoolArena 会优先访问线程本地的缓存队列,查询是否有缓存可用,如果有,则直接分配,提高分配效率。
总结
在本课时,我们主要介绍了 Netty 核心组件的功能和原理:
首先介绍了 Channel、ChannelFuture、Selector 等组件,它们是构成 I/O 多路复用的核心。
之后介绍了 EventLoop、EventLoopGroup 等组件,它们与 Netty 使用的主从 Reactor 线程模型息息相关。
最后深入介绍了 Netty 的内存管理,主要从内存分配管理、内存碎片优化以及并发分配内存等角度进行了介绍。
那你还知道哪些优秀的网络库或网络层设计呢?欢迎你留言讨论。

View File

@@ -0,0 +1,359 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
11 简易版 RPC 框架实现(上)
这是“基础知识”部分的最后一课时,我们将会运用前面介绍的基础知识来做一个实践项目 —— 编写一个简易版本的 RPC 框架,作为“基础知识”部分的总结和回顾。
RPC 是“远程过程调用Remote Procedure Call”的缩写形式比较通俗的解释是像本地方法调用一样调用远程的服务。虽然 RPC 的定义非常简单,但是相对完整的、通用的 RPC 框架涉及很多方面的内容例如注册发现、服务治理、负载均衡、集群容错、RPC 协议等,如下图所示:
简易 RPC 框架的架构图
本课时我们主要实现RPC 框架的基石部分——远程调用,简易版 RPC 框架一次远程调用的核心流程是这样的:
Client 首先会调用本地的代理,也就是图中的 Proxy。
Client 端 Proxy 会按照协议Protocol将调用中传入的数据序列化成字节流。
之后 Client 会通过网络,将字节数据发送到 Server 端。
Server 端接收到字节数据之后,会按照协议进行反序列化,得到相应的请求信息。
Server 端 Proxy 会根据序列化后的请求信息,调用相应的业务逻辑。
Server 端业务逻辑的返回值,也会按照上述逻辑返回给 Client 端。
这个远程调用的过程,就是我们简易版本 RPC 框架的核心实现,只有理解了这个流程,才能进行后续的开发。
项目结构
了解了简易版 RPC 框架的工作流程和实现目标之后,我们再来看下项目的结构,为了方便起见,这里我们将整个项目放到了一个 Module 中了,如下图所示,你可以按照自己的需求进行模块划分。
那这各个包的功能是怎样的呢?我们就来一一说明。
protocol简易版 RPC 框架的自定义协议。
serialization提供了自定义协议对应的序列化、反序列化的相关工具类。
codec提供了自定义协议对应的编码器和解码器。
transport基于 Netty 提供了底层网络通信的功能,其中会使用到 codec 包中定义编码器和解码器,以及 serialization 包中的序列化器和反序列化器。
registry基于 ZooKeeper 和 Curator 实现了简易版本的注册中心功能。
proxy使用 JDK 动态代理实现了一层代理。
自定义协议
当前已经有很多成熟的协议了,例如 HTTP、HTTPS 等,那为什么我们还要自定义 RPC 协议呢?
从功能角度考虑HTTP 协议在 1.X 时代只支持半双工传输模式虽然支持长连接但是不支持服务端主动推送数据。从效率角度来看在一次简单的远程调用中只需要传递方法名和加个简单的参数此时HTTP 请求中大部分数据都被 HTTP Header 占据,真正的有效负载非常少,效率就比较低。
当然HTTP 协议也有自己的优势,例如,天然穿透防火墙,大量的框架和开源软件支持 HTTP 接口,而且配合 REST 规范使用也是很便捷的,所以有很多 RPC 框架直接使用 HTTP 协议,尤其是在 HTTP 2.0 之后,如 gRPC、Spring Cloud 等。
这里我们自定义一个简易版的 Demo RPC 协议,如下图所示:
在 Demo RPC 的消息头中,包含了整个 RPC 消息的一些控制信息,例如,版本号、魔数、消息类型、附加信息、消息 ID 以及消息体的长度在附加信息extraInfo按位进行划分分别定义消息的类型、序列化方式、压缩方式以及请求类型。当然你也可以自己扩充 Demo RPC 协议,实现更加复杂的功能。
Demo RPC 消息头对应的实体类是 Header其定义如下
public class Header {
private short magic; // 魔数
private byte version; // 协议版本
private byte extraInfo; // 附加信息
private Long messageId; // 消息ID
private Integer size; // 消息体长度
... // 省略getter/setter方法
}
确定了 Demo RPC 协议消息头的结构之后,我们再来看 Demo RPC 协议消息体由哪些字段构成,这里我们通过 Request 和 Response 两个实体类来表示请求消息和响应消息的消息体:
public class Request implements Serializable {
private String serviceName; // 请求的Service类名
private String methodName; // 请求的方法名称
private Class[] argTypes; // 请求方法的参数类型
private Object[] args; // 请求方法的参数
... // 省略getter/setter方法
}
public class Response implements Serializable {
private int code = 0; // 响应的错误码正常响应为0非0表示异常响应
private String errMsg; // 异常信息
private Object result; // 响应结果
... // 省略getter/setter方法
}
注意Request 和 Response 对象是要进行序列化的,需要实现 Serializable 接口。为了让这两个类的对象能够在 Client 和 Server 之间跨进程传输,需要进行序列化和反序列化操作,这里定义一个 Serialization 接口,统一完成序列化相关的操作:
public interface Serialization {
<T> byte[] serialize(T obj)throws IOException;
<T> T deSerialize(byte[] data, Class<T> clz)throws IOException;
}
在 Demo RPC 中默认使用 Hessian 序列化方式,下面的 HessianSerialization 就是基于 Hessian 序列化方式对 Serialization 接口的实现:
public class HessianSerialization implements Serialization {
public <T> byte[] serialize(T obj) throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
HessianOutput hessianOutput = new HessianOutput(os);
hessianOutput.writeObject(obj);
return os.toByteArray();
}
public <T> T deSerialize(byte[] data, Class<T> clazz)
throws IOException {
ByteArrayInputStream is = new ByteArrayInputStream(data);
HessianInput hessianInput = new HessianInput(is);
return (T) hessianInput.readObject(clazz);
}
}
在有的场景中,请求或响应传输的数据比较大,直接传输比较消耗带宽,所以一般会采用压缩后再发送的方式。在前面介绍的 Demo RPC 消息头中的 extraInfo 字段中,就包含了标识消息体压缩方式的 bit 位。这里我们定义一个 Compressor 接口抽象所有压缩算法:
public interface Compressor {
byte[] compress(byte[] array) throws IOException;
byte[] unCompress(byte[] array) throws IOException;
}
同时提供了一个基于 Snappy 压缩算法的实现,作为 Demo RPC 的默认压缩算法:
public class SnappyCompressor implements Compressor {
public byte[] compress(byte[] array) throws IOException {
if (array == null) { return null; }
return Snappy.compress(array);
}
public byte[] unCompress(byte[] array) throws IOException {
if (array == null) { return null; }
return Snappy.uncompress(array);
}
}
编解码实现
了解了自定义协议的结构之后,我们再来解决协议的编解码问题。
前面课时介绍 Netty 核心概念的时候我们提到过Netty 每个 Channel 绑定一个 ChannelPipeline并依赖 ChannelPipeline 中添加的 ChannelHandler 处理接收到或要发送的数据其中就包括字节到消息以及消息到字节的转换。Netty 中提供了 ByteToMessageDecoder、 MessageToByteEncoder、MessageToMessageEncoder、MessageToMessageDecoder 等抽象类来实现 Message 与 ByteBuf 之间的转换以及 Message 之间的转换,如下图所示:
Netty 提供的 Decoder 和 Encoder 实现
在 Netty 的源码中我们可以看到对很多已有协议的序列化和反序列化都是基于上述抽象类实现的例如HttpServerCodec 中通过依赖 HttpServerRequestDecoder 和 HttpServerResponseEncoder 来实现 HTTP 请求的解码和 HTTP 响应的编码。如下图所示HttpServerRequestDecoder 继承自 ByteToMessageDecoder实现了 ByteBuf 到 HTTP 请求之间的转换HttpServerResponseEncoder 继承自 MessageToMessageEncoder实现 HTTP 响应到其他消息的转换(其中包括转换成 ByteBuf 的能力)。
Netty 中 HTTP 协议的 Decoder 和 Encoder 实现
在简易版 RPC 框架中,我们的自定义请求暂时没有 HTTP 协议那么复杂,只要简单继承 ByteToMessageDecoder 和 MessageToMessageEncoder 即可。
首先来看 DemoRpcDecoder它实现了 ByteBuf 到 Demo RPC Message 的转换,具体实现如下:
public class DemoRpcDecoder extends ByteToMessageDecoder {
protected void decode(ChannelHandlerContext ctx,
ByteBuf byteBuf, List<Object> out) throws Exception {
if (byteBuf.readableBytes() < Constants.HEADER_SIZE) {
return; // 不到16字节的话无法解析消息头暂不读取
}
// 记录当前readIndex指针的位置方便重置
byteBuf.markReaderIndex();
// 尝试读取消息头的魔数部分
short magic = byteBuf.readShort();
if (magic != Constants.MAGIC) { // 魔数不匹配会抛出异常
byteBuf.resetReaderIndex(); // 重置readIndex指针
throw new RuntimeException("magic number error:" + magic);
}
// 依次读取消息版本附加信息消息ID以及消息体长度四部分
byte version = byteBuf.readByte();
byte extraInfo = byteBuf.readByte();
long messageId = byteBuf.readLong();
int size = byteBuf.readInt();
Object request = null;
// 心跳消息是没有消息体的无须读取
if (!Constants.isHeartBeat(extraInfo)) {
// 对于非心跳消息没有积累到足够的数据是无法进行反序列化的
if (byteBuf.readableBytes() < size) {
byteBuf.resetReaderIndex();
return;
}
// 读取消息体并进行反序列化
byte[] payload = new byte[size];
byteBuf.readBytes(payload);
// 这里根据消息头中的extraInfo部分选择相应的序列化和压缩方式
Serialization serialization =
SerializationFactory.get(extraInfo);
Compressor compressor = CompressorFactory.get(extraInfo);
// 经过解压缩和反序列化得到消息体
request = serialization.deserialize(
compressor.unCompress(payload), Request.class);
}
// 将上面读取到的消息头和消息体拼装成完整的Message并向后传递
Header header = new Header(magic, version, extraInfo,
messageId, size);
Message message = new Message(header, request);
out.add(message);
}
}
接下来看 DemoRpcEncoder它实现了 Demo RPC Message ByteBuf 的转换具体实现如下
class DemoRpcEncoder extends MessageToByteEncoder<Message>{
@Override
protected void encode(ChannelHandlerContext channelHandlerContext,
Message message, ByteBuf byteBuf) throws Exception {
Header header = message.getHeader();
// 依次序列化消息头中的魔数、版本、附加信息以及消息ID
byteBuf.writeShort(header.getMagic());
byteBuf.writeByte(header.getVersion());
byteBuf.writeByte(header.getExtraInfo());
byteBuf.writeLong(header.getMessageId());
Object content = message.getContent();
if (Constants.isHeartBeat(header.getExtraInfo())) {
byteBuf.writeInt(0); // 心跳消息没有消息体这里写入0
return;
}
// 按照extraInfo部分指定的序列化方式和压缩方式进行处理
Serialization serialization =
SerializationFactory.get(header.getExtraInfo());
Compressor compressor =
CompressorFactory.get(header.getExtraInfo());
byte[] payload = compressor.compress(
serialization.serialize(content));
byteBuf.writeInt(payload.length); // 写入消息体长度
byteBuf.writeBytes(payload); // 写入消息体
}
}
总结
本课时我们首先介绍了简易 RPC 框架的基础架构以及其处理一次远程调用的基本流程,并对整个简易 RPC 框架项目的结构进行了简单介绍。接下来,我们讲解了简易 RPC 框架使用的自定义协议格式、序列化/反序列化方式以及压缩方式,这些都是远程数据传输不可或缺的基础。然后,我们又介绍了 Netty 中的编解码体系,以及 HTTP 协议相关的编解码器实现。最后,我们还分析了简易 RPC 协议对应的编解码器,即 DemoRpcEncoder 和 DemoRpcDecoder。
在下一课时,我们将自底向上,继续介绍简易 RPC 框架的剩余部分实现。
简易版 RPC 框架 Demo 的链接https://github.com/xxxlxy2008/demo-prc 。

View File

@@ -0,0 +1,773 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
12 简易版 RPC 框架实现(下)
在上一课时中,我们介绍了整个简易 RPC 框架项目的结构和工作原理,并且介绍了简易 RPC 框架底层的协议结构、序列化/反序列化实现、压缩实现以及编解码器的具体实现。本课时我们将继续自底向上,介绍简易 RPC 框架的剩余部分实现。
transport 相关实现
正如前文介绍 Netty 线程模型的时候提到,我们不能在 Netty 的 I/O 线程中执行耗时的业务逻辑。在 Demo RPC 框架的 Server 端接收到请求时,首先会通过上面介绍的 DemoRpcDecoder 反序列化得到请求消息,之后我们会通过一个自定义的 ChannelHandlerDemoRpcServerHandler将请求提交给业务线程池进行处理。
在 Demo RPC 框架的 Client 端接收到响应消息的时候,也是先通过 DemoRpcDecoder 反序列化得到响应消息,之后通过一个自定义的 ChannelHandlerDemoRpcClientHandler将响应返回给上层业务。
DemoRpcServerHandler 和 DemoRpcClientHandler 都继承自 SimpleChannelInboundHandler如下图所示
DemoRpcClientHandler 和 DemoRpcServerHandler 的继承关系图
下面我们就来看一下这两个自定义的 ChannelHandler 实现:
public class DemoRpcServerHandler extends
SimpleChannelInboundHandler<Message<Request>> {
// 业务线程池
static Executor executor = Executors.newCachedThreadPool();
protected void channelRead0(final ChannelHandlerContext ctx,
Message<Request> message) throws Exception {
byte extraInfo = message.getHeader().getExtraInfo();
if (Constants.isHeartBeat(extraInfo)) { // 心跳消息,直接返回即可
channelHandlerContext.writeAndFlush(message);
return;
}
// 非心跳消息直接封装成Runnable提交到业务线程
executor.execute(new InvokeRunnable(message, cxt));
}
}
public class DemoRpcClientHandler extends
SimpleChannelInboundHandler<Message<Response>> {
protected void channelRead0(ChannelHandlerContext ctx,
Message<Response> message) throws Exception {
NettyResponseFuture responseFuture =
Connection.IN_FLIGHT_REQUEST_MAP
.remove(message.getHeader().getMessageId());
Response response = message.getContent();
// 心跳消息特殊处理
if (response == null && Constants.isHeartBeat(
message.getHeader().getExtraInfo())) {
response = new Response();
response.setCode(Constants.HEARTBEAT_CODE);
}
responseFuture.getPromise().setSuccess(response);
}
}
注意,这里有两个点需要特别说明一下。一个点是 Server 端的 InvokeRunnable在这个 Runnable 任务中会根据请求的 serviceName、methodName 以及参数信息,调用相应的方法:
class InvokeRunnable implements Runnable {
private ChannelHandlerContext ctx;
private Message<Request> message;
public void run() {
Response response = new Response();
Object result = null;
try {
Request request = message.getContent();
String serviceName = request.getServiceName();
// 这里提供BeanManager对所有业务Bean进行管理其底层在内存中维护了
// 一个业务Bean实例的集合。感兴趣的同学可以尝试接入Spring等容器管
// 理业务Bean
Object bean = BeanManager.getBean(serviceName);
// 下面通过反射调用Bean中的相应方法
Method method = bean.getClass().getMethod(
request.getMethodName(), request.getArgTypes());
result = method.invoke(bean, request.getArgs());
} catch (Exception e) { // 省略异常处理
} finally {
}
response.setResult(result); // 设置响应结果
// 将响应消息返回给客户端
ctx.writeAndFlush(new Message(message.getHeader(), response));
}
}
另一个点是 Client 端的 Connection它是用来暂存已发送出去但未得到响应的请求这样在响应返回时就可以查找到相应的请求以及 Future从而将响应结果返回给上层业务逻辑具体实现如下
public class Connection implements Closeable {
private static AtomicLong ID_GENERATOR = new AtomicLong(0);
public static Map<Long, NettyResponseFuture<Response>>
IN_FLIGHT_REQUEST_MAP = new ConcurrentHashMap<>();
private ChannelFuture future;
private AtomicBoolean isConnected = new AtomicBoolean();
public Connection(ChannelFuture future, boolean isConnected) {
this.future = future;
this.isConnected.set(isConnected);
}
public NettyResponseFuture<Response> request(Message<Request> message, long timeOut) {
// 生成并设置消息ID
long messageId = ID_GENERATOR.incrementAndGet();
message.getHeader().setMessageId(messageId);
// 创建消息关联的Future
NettyResponseFuture responseFuture = new NettyResponseFuture(System.currentTimeMillis(),
timeOut, message, future.channel(), new DefaultPromise(new DefaultEventLoop()));
// 将消息ID和关联的Future记录到IN_FLIGHT_REQUEST_MAP集合中
IN_FLIGHT_REQUEST_MAP.put(messageId, responseFuture);
try {
future.channel().writeAndFlush(message); // 发送请求
} catch (Exception e) {
// 发送请求异常时删除对应的Future
IN_FLIGHT_REQUEST_MAP.remove(messageId);
throw e;
}
return responseFuture;
}
// 省略getter/setter以及close()方法
}
我们可以看到Connection 中没有定时清理 IN_FLIGHT_REQUEST_MAP 集合的操作,在无法正常获取响应的时候,就会导致 IN_FLIGHT_REQUEST_MAP 不断膨胀,最终 OOM。你也可以添加一个时间轮定时器定时清理过期的请求消息这里我们就不再展开讲述了。
完成自定义 ChannelHandler 的编写之后,我们需要再定义两个类—— DemoRpcClient 和 DemoRpcServer分别作为 Client 和 Server 的启动入口。DemoRpcClient 的实现如下:
public class DemoRpcClient implements Closeable {
protected Bootstrap clientBootstrap;
protected EventLoopGroup group;
private String host;
private int port;
public DemoRpcClient(String host, int port) throws Exception {
this.host = host;
this.port = port;
clientBootstrap = new Bootstrap();
// 创建并配置客户端Bootstrap
group = NettyEventLoopFactory.eventLoopGroup(
Constants.DEFAULT_IO_THREADS, "NettyClientWorker");
clientBootstrap.group(group)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.SO_KEEPALIVE, true)
.channel(NioSocketChannel.class)
// 指定ChannelHandler的顺序
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast("demo-rpc-encoder",
new DemoRpcEncoder());
ch.pipeline().addLast("demo-rpc-decoder",
new DemoRpcDecoder());
ch.pipeline().addLast("client-handler",
new DemoRpcClientHandler());
}
});
}
public ChannelFuture connect() { // 连接指定的地址和端口
ChannelFuture connect = clientBootstrap.connect(host, port);
connect.awaitUninterruptibly();
return connect;
}
public void close() {
group.shutdownGracefully();
}
}
通过 DemoRpcClient 的代码我们可以看到其 ChannelHandler 的执行顺序如下:
客户端 ChannelHandler 结构图
另外在创建EventLoopGroup时并没有直接使用NioEventLoopGroup而是在 NettyEventLoopFactory 中根据当前操作系统进行选择,对于 Linux 系统,会使用 EpollEventLoopGroup其他系统则使用 NioEventLoopGroup。
接下来我们再看DemoRpcServer 的具体实现:
public class DemoRpcServer {
private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
private ServerBootstrap serverBootstrap;
private Channel channel;
protected int port;
public DemoRpcServer(int port) throws InterruptedException {
this.port = port;
// 创建boss和worker两个EventLoopGroup注意一些小细节
// workerGroup 是按照中的线程数是按照 CPU 核数计算得到的,
bossGroup = NettyEventLoopFactory.eventLoopGroup(1, "boos");
workerGroup = NettyEventLoopFactory.eventLoopGroup(
Math.min(Runtime.getRuntime().availableProcessors() + 1,
32), "worker");
serverBootstrap = new ServerBootstrap().group(bossGroup,
workerGroup).channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
.childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>()
{ // 指定每个Channel上注册的ChannelHandler以及顺序
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast("demp-rpc-decoder",
new DemoRpcDecoder());
ch.pipeline().addLast("demo-rpc-encoder",
new DemoRpcEncoder());
ch.pipeline().addLast("server-handler",
new DemoRpcServerHandler());
}
});
}
public ChannelFuture start() throws InterruptedException {
ChannelFuture channelFuture = serverBootstrap.bind(port);
channel = channelFuture.channel();
channel.closeFuture();
return channelFuture;
}
}
通过对 DemoRpcServer 实现的分析,我们可以知道每个 Channel 上的 ChannelHandler 顺序如下:
服务端 ChannelHandler 结构图
registry 相关实现
介绍完客户端和服务端的通信之后,我们再来看简易 RPC 框架的另一个基础能力——服务注册与服务发现能力,对应 demo-rpc 项目源码中的 registry 包。
registry 包主要是依赖 Apache Curator 实现了一个简易版本的 ZooKeeper 客户端,并基于 ZooKeeper 实现了注册中心最基本的两个功能Provider 注册以及 Consumer 订阅。
这里我们先定义一个 Registry 接口,其中提供了注册以及查询服务实例的方法,如下图所示:
ZooKeeperRegistry 是基于 curator-x-discovery 对 Registry 接口的实现类型,其中封装了之前课时介绍的 ServiceDiscovery并在其上添加了 ServiceCache 缓存提高查询效率。ZooKeeperRegistry 的具体实现如下:
public class ZookeeperRegistry<T> implements Registry<T> {
private InstanceSerializer serializer =
new JsonInstanceSerializer<>(ServerInfo.class);
private ServiceDiscovery<T> serviceDiscovery;
private ServiceCache<T> serviceCache;
private String address = "localhost:2181";
public void start() throws Exception {
String root = "/demo/rpc";
// 初始化CuratorFramework
CuratorFramework client = CuratorFrameworkFactory
.newClient(address, new ExponentialBackoffRetry(1000, 3));
client.start(); // 启动Curator客户端
client.blockUntilConnected(); // 阻塞当前线程,等待连接成
client.createContainers(root);
// 初始化ServiceDiscovery
serviceDiscovery = ServiceDiscoveryBuilder
.builder(ServerInfo.class)
.client(client).basePath(root)
.serializer(serializer)
.build();
serviceDiscovery.start(); // 启动ServiceDiscovery
// 创建ServiceCache监Zookeeper相应节点的变化也方便后续的读取
serviceCache = serviceDiscovery.serviceCacheBuilder()
.name(root)
.build();
serviceCache.start(); // 启动ServiceCache
}
@Override
public void registerService(ServiceInstance<T> service)
throws Exception {
serviceDiscovery.registerService(service);
}
@Override
public void unregisterService(ServiceInstance service)
throws Exception {
serviceDiscovery.unregisterService(service);
}
@Override
public List<ServiceInstance<T>> queryForInstances(
String name) throws Exception {
// 直接根据name进行过滤ServiceCache中的缓存数据
return serviceCache.getInstances().stream()
.filter(s -> s.getName().equals(name))
.collect(Collectors.toList());
}
}
通过对 ZooKeeperRegistry的分析可以得知它是基于 Curator 中的 ServiceDiscovery 组件与 ZooKeeper 进行交互的,并且对 Registry 接口的实现也是通过直接调用 ServiceDiscovery 的相关方法实现的。在查询时,直接读取 ServiceCache 中的缓存数据ServiceCache 底层在本地维护了一个 ConcurrentHashMap 缓存,通过 PathChildrenCache 监听 ZooKeeper 中各个子节点的变化,同步更新本地缓存。这里我们简单看一下 ServiceCache 的核心实现:
public class ServiceCacheImpl<T> implements ServiceCache<T>,
PathChildrenCacheListener{//实现PathChildrenCacheListener接口
// 关联的ServiceDiscovery实例
private final ServiceDiscoveryImpl<T> discovery;
// 底层的PathChildrenCache用于监听子节点的变化
private final PathChildrenCache cache;
// 本地缓存
private final ConcurrentMap<String, ServiceInstance<T>> instances
= Maps.newConcurrentMap();
public List<ServiceInstance<T>> getInstances(){ // 返回本地缓存内容
return Lists.newArrayList(instances.values());
}
public void childEvent(CuratorFramework client,
PathChildrenCacheEvent event) throws Exception{
switch(event.getType()){
case CHILD_ADDED:
case CHILD_UPDATED:{
addInstance(event.getData(), false); // 更新本地缓存
notifyListeners = true;
break;
}
case CHILD_REMOVED:{ // 更新本地缓存
instances.remove(instanceIdFromData(event.getData()));
notifyListeners = true;
break;
}
}
... // 通知ServiceCache上注册的监听器
}
}
proxy 相关实现
在简易版 Demo RPC 框架中Proxy 主要是为 Client 端创建一个代理,帮助客户端程序屏蔽底层的网络操作以及与注册中心之间的交互。
简易版 Demo RPC 使用 JDK 动态代理的方式生成代理,这里需要编写一个 InvocationHandler 接口的实现,即下面的 DemoRpcProxy。其中有两个核心方法一个是 newInstance() 方法,用于生成代理对象;另一个是 invoke() 方法,当调用目标对象的时候,会执行 invoke() 方法中的代理逻辑。
下面是 DemoRpcProxy 的具体实现:
public class DemoRpcProxy implements InvocationHandler {
// 需要代理的服务(接口)名称
private String serviceName;
// 用于与Zookeeper交互其中自带缓存
private Registry<ServerInfo> registry;
public DemoRpcProxy(String serviceName, Registry<ServerInfo>
registry) throws Exception { // 初始化上述两个字段
this.serviceName = serviceName;
this.registry = registry;
}
public static <T> T newInstance(Class<T> clazz,
Registry<ServerInfo> registry) throws Exception {
// 创建代理对象
return (T) Proxy.newProxyInstance(Thread.currentThread()
.getContextClassLoader(), new Class[]{clazz},
new DemoRpcProxy(clazz.getName(), registry));
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// 从Zookeeper缓存中获取可用的Server地址,并随机从中选择一个
List<ServiceInstance<ServerInfo>> serviceInstances =
registry.queryForInstances(serviceName);
ServiceInstance<ServerInfo> serviceInstance = serviceInstances
.get(ThreadLocalRandom.current()
.nextInt(serviceInstances.size()));
// 创建请求消息然后调用remoteCall()方法请求上面选定的Server端
String methodName = method.getName();
Header header =new Header(MAGIC, VERSION_1...);
Message<Request> message = new Message(header,
new Request(serviceName, methodName, args));
return remoteCall(serviceInstance.getPayload(), message);
}
protected Object remoteCall(ServerInfo serverInfo,
Message message) throws Exception {
if (serverInfo == null) {
throw new RuntimeException("get available server error");
}
// 创建DemoRpcClient连接指定的Server端
DemoRpcClient demoRpcClient = new DemoRpcClient(
serverInfo.getHost(), serverInfo.getPort());
ChannelFuture channelFuture = demoRpcClient.connect()
.awaitUninterruptibly();
// 创建对应的Connection对象并发送请求
Connection connection = new Connection(channelFuture, true);
NettyResponseFuture responseFuture =
connection.request(message, Constants.DEFAULT_TIMEOUT);
// 等待请求对应的响应
return responseFuture.getPromise().get(
Constants.DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
}
}
从 DemoRpcProxy 的实现中我们可以看到,它依赖了 ServiceInstanceCache 获取ZooKeeper 中注册的 Server 端地址,同时依赖了 DemoRpcClient 与Server 端进行通信,上层调用方拿到这个代理对象后,就可以像调用本地方法一样进行调用,而不再关心底层网络通信和服务发现的细节。当然,这个简易版 DemoRpcProxy 的实现还有很多可以优化的地方,例如:
缓存 DemoRpcClient 客户端对象以及相应的 Connection 对象,不必每次进行创建。
可以添加失败重试机制,在请求出现超时的时候,进行重试。
可以添加更加复杂和灵活的负载均衡机制,例如,根据 Hash 值散列进行负载均衡、根据节点 load 情况进行负载均衡等。
你若感兴趣的话可以尝试进行扩展,以实现一个更加完善的代理层。
使用方接入
介绍完 Demo RPC 的核心实现之后下面我们讲解下Demo RPC 框架的使用方式。这里涉及Consumer、DemoServiceImp、Provider三个类以及 DemoService 业务接口。
使用接入的相关类
首先我们定义DemoService 接口作为业务 Server 接口,具体定义如下:
public interface DemoService {
String sayHello(String param);
}
DemoServiceImpl对 DemoService 接口的实现也非常简单,如下所示,将参数做简单修改后返回:
public class DemoServiceImpl implements DemoService {
public String sayHello(String param) {
return "hello:" + param;
}
}
了解完相应的业务接口和实现之后我们再来看Provider的实现它的角色类似于 Dubbo 中的 Provider其会创建 DemoServiceImpl 这个业务 Bean 并将自身的地址信息暴露出去,如下所示:
public class Provider {
public static void main(String[] args) throws Exception {
// 创建DemoServiceImpl并注册到BeanManager中
BeanManager.registerBean("demoService",
new DemoServiceImpl());
// 创建ZookeeperRegistry并将Provider的地址信息封装成ServerInfo
// 对象注册到Zookeeper
ZookeeperRegistry<ServerInfo> discovery =
new ZookeeperRegistry<>();
discovery.start();
ServerInfo serverInfo = new ServerInfo("127.0.0.1", 20880);
discovery.registerService(
ServiceInstance.<ServerInfo>builder().name("demoService")
.payload(serverInfo).build());
// 启动DemoRpcServer等待Client的请求
DemoRpcServer rpcServer = new DemoRpcServer(20880);
rpcServer.start();
}
}
最后是Consumer它类似于 Dubbo 中的 Consumer其会订阅 Provider 地址信息,然后根据这些信息选择一个 Provider 建立连接,发送请求并得到响应,这些过程在 Proxy 中都予以了封装那Consumer 的实现就很简单了,可参考如下示例代码:
public class Consumer {
public static void main(String[] args) throws Exception {
// 创建ZookeeperRegistr对象
ZookeeperRegistry<ServerInfo> discovery = new ZookeeperRegistry<>();
// 创建代理对象通过代理调用远端Server
DemoService demoService = DemoRpcProxy.newInstance(DemoService.class, discovery);
// 调用sayHello()方法,并输出结果
String result = demoService.sayHello("hello");
System.out.println(result);
}
}
总结
本课时我们首先介绍了简易 RPC 框架中的transport 包它在上一课时介绍的编解码器基础之上实现了服务端和客户端的通信能力。之后讲解了registry 包如何实现与 ZooKeeper 的交互,完善了简易 RPC 框架的服务注册与服务发现的能力。接下来又分析了proxy 包的实现,其中通过 JDK 动态代理的方式,帮接入方屏蔽了底层网络通信的复杂性。最后,我们编写了一个简单的 DemoService 业务接口,以及相应的 Provider 和 Consumer 接入简易 RPC 框架。
在本课时最后,留给你一个小问题:在 transport 中创建 EventLoopGroup 的时候,为什么针对 Linux 系统使用的 EventLoopGroup会有所不同呢期待你的留言。
简易版 RPC 框架 Demo 的链接https://github.com/xxxlxy2008/demo-prc 。

View File

@@ -0,0 +1,221 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
13 本地缓存:降低 ZooKeeper 压力的一个常用手段
从这一课时开始我们就进入了第二部分注册中心。注册中心Registry在微服务架构中的作用举足轻重有了它服务提供者Provider 和消费者Consumer 就能感知彼此。从下面的 Dubbo 架构图中可知:
Dubbo 架构图
Provider 从容器启动后的初始化阶段便会向注册中心完成注册操作;
Consumer 启动初始化阶段会完成对所需 Prov·ider 的订阅操作;
另外,在 Provider 发生变化时,需要通知监听的 Consumer。
Registry 只是 Consumer 和 Provider 感知彼此状态变化的一种便捷途径而已,它们彼此的实际通讯交互过程是直接进行的,对于 Registry 来说是透明无感的。Provider 状态发生变化了,会由 Registry 主动推送订阅了该 Provider 的所有 Consumer这保证了 Consumer 感知 Provider 状态变化的及时性,也将和具体业务需求逻辑交互解耦,提升了系统的稳定性。
Dubbo 中存在很多概念,但有些理解起来就特别费劲,如本文的 Registry翻译过来的意思是“注册中心”但它其实是应用本地的注册中心客户端真正的“注册中心”服务是其他独立部署的进程或进程组成的集群比如 ZooKeeper 集群。本地的 Registry 通过和 ZooKeeper 等进行实时的信息同步,维持这些内容的一致性,从而实现了注册中心这个特性。另外,就 Registry 而言Consumer 和 Provider 只是个用户视角的概念,它们被抽象为了一条 URL 。
从本课时开始,我们就真正开始分析 Dubbo 源码了。首先看一下本课程第二部分内容在 Dubbo 架构中所处的位置(如下图红框所示),可以看到这部分内容在整个 Dubbo 体系中还是相对独立的,没有涉及 Protocol、Invoker 等 Dubbo 内部的概念。等介绍完这些概念之后,我们还会回看图中 Registry 红框之外的内容。
整个 Dubbo 体系图
核心接口
作为“注册中心”部分的第一课时,我们有必要介绍下 dubbo-registry-api 模块中的核心抽象接口,如下图所示:
在 Dubbo 中,一般使用 Node 这个接口来抽象节点的概念。Node不仅可以表示 Provider 和 Consumer 节点还可以表示注册中心节点。Node 接口中定义了三个非常基础的方法(如下图所示):
getUrl() 方法返回表示当前节点的 URL
isAvailable() 检测当前节点是否可用;
destroy() 方法负责销毁当前节点并释放底层资源。
RegistryService 接口抽象了注册服务的基本行为,如下图所示:
register() 方法和 unregister() 方法分别表示注册和取消注册一个 URL。
subscribe() 方法和 unsubscribe() 方法分别表示订阅和取消订阅一个 URL。订阅成功之后当订阅的数据发生变化时注册中心会主动通知第二个参数指定的 NotifyListener 对象NotifyListener 接口中定义的 notify() 方法就是用来接收该通知的。
lookup() 方法能够查询符合条件的注册数据,它与 subscribe() 方法有一定的区别subscribe() 方法采用的是 push 模式lookup() 方法采用的是 pull 模式。
Registry 接口继承了 RegistryService 接口和 Node 接口,如下图所示,它表示的就是一个拥有注册中心能力的节点,其中的 reExportRegister() 和 reExportUnregister() 方法都是委托给 RegistryService 中的相应方法。
RegistryFactory 接口是 Registry 的工厂接口,负责创建 Registry 对象,具体定义如下所示,其中 @SPI 注解指定了默认的扩展名为 dubbo@Adaptive 注解表示会生成适配器类并根据 URL 参数中的 protocol 参数值选择相应的实现。
@SPI("dubbo")
public interface RegistryFactory {
@Adaptive({"protocol"})
Registry getRegistry(URL url);
}
通过下面两张继承关系图可以看出,每个 Registry 实现类都有对应的 RegistryFactory 工厂实现,每个 RegistryFactory 工厂实现只负责创建对应的 Registry 对象。
RegistryFactory 继承关系图
Registry 继承关系图
其中RegistryFactoryWrapper 是 RegistryFactory 接口的 Wrapper 类,它在底层 RegistryFactory 创建的 Registry 对象外层封装了一个 ListenerRegistryWrapper ListenerRegistryWrapper 中维护了一个 RegistryServiceListener 集合,会将 register()、subscribe() 等事件通知到 RegistryServiceListener 监听器。
AbstractRegistryFactory 是一个实现了 RegistryFactory 接口的抽象类,提供了规范 URL 的操作以及缓存 Registry 对象的公共能力。其中,缓存 Registry 对象是使用 HashMap 集合实现的REGISTRIES 静态字段)。在规范 URL 的实现逻辑中AbstractRegistryFactory 会将 RegistryService 的类名设置为 URL path 和 interface 参数,同时删除 export 和 refer 参数。
AbstractRegistry
AbstractRegistry 实现了 Registry 接口,虽然 AbstractRegistry 本身在内存中实现了注册数据的读写功能也没有什么抽象方法但它依然被标记成了抽象类从前面的Registry 继承关系图中可以看出Registry 接口的所有实现类都继承了 AbstractRegistry。
为了减轻注册中心组件的压力AbstractRegistry 会把当前节点订阅的 URL 信息缓存到本地的 Properties 文件中,其核心字段如下:
registryUrlURL类型。 该 URL 包含了创建该 Registry 对象的全部配置信息,是 AbstractRegistryFactory 修改后的产物。
propertiesProperties 类型、fileFile 类型)。 本地的 Properties 文件缓存properties 是加载到内存的 Properties 对象file 是磁盘上对应的文件,两者的数据是同步的。在 AbstractRegistry 初始化时,会根据 registryUrl 中的 file.cache 参数值决定是否开启文件缓存。如果开启文件缓存功能,就会立即将 file 文件中的 KV 缓存加载到 properties 字段中。当 properties 中的注册数据发生变化时,会写入本地的 file 文件进行同步。properties 是一个 KV 结构,其中 Key 是当前节点作为 Consumer 的一个 URLValue 是对应的 Provider 列表,包含了所有 Category例如providers、routes、configurators 等) 下的 URL。properties 中有一个特殊的 Key 值为 registies对应的 Value 是注册中心列表,其他记录的都是 Provider 列表。
syncSaveFileboolean 类型)。 是否同步保存文件的配置,对应的是 registryUrl 中的 save.file 参数。
registryCacheExecutorExecutorService 类型)。 这是一个单线程的线程池,在一个 Provider 的注册数据发生变化的时候,会将该 Provider 的全量数据同步到 properties 字段和缓存文件中,如果 syncSaveFile 配置为 false就由该线程池异步完成文件写入。
lastCacheChangedAtomicLong 类型)。 注册数据的版本号,每次写入 file 文件时,都是全覆盖写入,而不是修改文件,所以需要版本控制,防止旧数据覆盖新数据。
registeredSet 类型)。 这个比较简单,它是注册的 URL 集合。
subscribedConcurrentMap 类型)。 表示订阅 URL 的监听器集合,其中 Key 是被监听的 URL Value 是相应的监听器集合。
notifiedConcurrentMap>类型)。 该集合第一层 Key 是当前节点作为 Consumer 的一个 URL表示的是该节点的某个 Consumer 角色(一个节点可以同时消费多个 Provider 节点Value 是一个 Map 集合,该 Map 集合的 Key 是 Provider URL 的分类Category例如 providers、routes、configurators 等Value 就是相应分类下的 URL 集合。
介绍完 AbstractRegistry 的核心字段之后,我们接下来就再看看 AbstractRegistry 依赖这些字段都提供了哪些公共能力。
1. 本地缓存
作为一个 RPC 框架Dubbo 在微服务架构中解决了各个服务间协作的难题;作为 Provider 和 Consumer 的底层依赖它会与服务一起打包部署。dubbo-registry 也仅仅是其中一个依赖包,负责完成与 ZooKeeper、etcd、Consul 等服务发现组件的交互。
当 Provider 端暴露的 URL 发生变化时ZooKeeper 等服务发现组件会通知 Consumer 端的 Registry 组件Registry 组件会调用 notify() 方法,被通知的 Consumer 能匹配到所有 Provider 的 URL 列表并写入 properties 集合中。
下面我们来看 notify() 方法的核心实现:
// 注意入参第一个URL参数表示的是Consumer第二个NotifyListener是第一个参数对应的监听器第三个参数是Provider端暴露的URL的全量数据
protected void notify(URL url, NotifyListener listener,
List<URL> urls) {
... // 省略一系列边界条件的检查
Map<String, List<URL>> result = new HashMap<>();
for (URL u : urls) {
// 需要Consumer URL与Provider URL匹配具体匹配规则后面详述
if (UrlUtils.isMatch(url, u)) {
// 根据Provider URL中的category参数进行分类
String category = u.getParameter("category", "providers");
List<URL> categoryList = result.computeIfAbsent(category,
k -> new ArrayList<>());
categoryList.add(u);
}
}
if (result.size() == 0) {
return;
}
Map<String, List<URL>> categoryNotified =
notified.computeIfAbsent(url, u -> new ConcurrentHashMap<>());
for (Map.Entry<String, List<URL>> entry : result.entrySet()) {
String category = entry.getKey();
List<URL> categoryList = entry.getValue();
categoryNotified.put(category, categoryList); // 更新notified
listener.notify(categoryList); // 调用NotifyListener
// 更新properties集合以及底层的文件缓存
saveProperties(url);
}
}
在 saveProperties() 方法中会取出 Consumer 订阅的各个分类的 URL 连接起来(中间以空格分隔),然后以 Consumer 的 ServiceKey 为键值写到 properties 中,同时 lastCacheChanged 版本号会自增。完成 properties 字段的更新之后,会根据 syncSaveFile 字段值来决定是在当前线程同步更新 file 文件,还是向 registryCacheExecutor 线程池提交任务,异步完成 file 文件的同步。本地缓存文件的具体路径是:
/.dubbo/dubbo-registry-[当前应用名]-[当前Registry所在的IP地址].cache
这里首先关注第一个细节UrlUtils.isMatch() 方法。该方法会完成 Consumer URL 与 Provider URL 的匹配,依次匹配的部分如下所示:
匹配 Consumer 和 Provider 的接口(优先取 interface 参数,其次再取 path。双方接口相同或者其中一方为“*”,则匹配成功,执行下一步。
匹配 Consumer 和 Provider 的 category。
检测 Consumer URL 和 Provider URL 中的 enable 参数是否符合条件。
检测 Consumer 和 Provider 端的 group、version 以及 classifier 是否符合条件。
第二个细节是URL.getServiceKey() 方法。该方法返回的 ServiceKey 是 properties 集合以及相应缓存文件中的 Key。ServiceKey 的格式如下:
[group]/{interface(或path)}[:version]
AbstractRegistry 的核心是本地文件缓存的功能。 在 AbstractRegistry 的构造方法中,会调用 loadProperties() 方法将上面写入的本地缓存文件,加载到 properties 对象中。
在网络抖动等原因而导致订阅失败时Consumer 端的 Registry 就可以调用 getCacheUrls() 方法获取本地缓存,从而得到最近注册的 Provider URL。可见AbstractRegistry 通过本地缓存提供了一种容错机制,保证了服务的可靠性。
2. 注册/订阅
AbstractRegistry 实现了 Registry 接口,它实现的 registry() 方法会将当前节点要注册的 URL 缓存到 registered 集合,而 unregistry() 方法会从 registered 集合删除指定的 URL例如当前节点下线的时候。
subscribe() 方法会将当前节点作为 Consumer 的 URL 以及相关的 NotifyListener 记录到 subscribed 集合unsubscribe() 方法会将当前节点的 URL 以及关联的 NotifyListener 从 subscribed 集合删除。
这四个方法都是简单的集合操作,这里我们就不再展示具体代码了。
单看 AbstractRegistry 的实现,上述四个基础的注册、订阅方法都是内存操作,但是 Java 有继承和多态的特性AbstractRegistry 的子类会覆盖上述四个基础的注册、订阅方法进行增强。
3. 恢复/销毁
AbstractRegistry 中还有另外两个需要关注的方法recover() 方法和destroy() 方法。
在 Provider 因为网络问题与注册中心断开连接之后,会进行重连,重新连接成功之后,会调用 recover() 方法将 registered 集合中的全部 URL 重新走一遍 register() 方法恢复注册数据。同样recover() 方法也会将 subscribed 集合中的 URL 重新走一遍 subscribe() 方法恢复订阅监听器。recover() 方法的具体实现比较简单,这里就不再展示,你若感兴趣的话,可以参考源码进行学习。
在当前节点下线的时候,会调用 Node.destroy() 方法释放底层资源。AbstractRegistry 实现的 destroy() 方法会调用 unregister() 方法和 unsubscribe() 方法将当前节点注册的 URL 以及订阅的监听全部清理掉,其中不会清理非动态注册的 URL即 dynamic 参数明确指定为 false。AbstractRegistry 中 destroy() 方法的实现比较简单,这里我们也不再展示,如果你感兴趣话,同样可以参考源码进行学习。
总结
本课时是 Dubbo 注册中心分析的第一个课时,我们首先介绍了注册中心在整个 Dubbo 架构中的位置,以及 Registry、 RegistryService、 RegistryFactory 等核心接口的功能。接下来我们还详细讲解了 AbstractRegistry 这个抽象类提供的公共能力,主要是从本地缓存、注册/订阅、恢复/销毁这三方面进行了分析。

View File

@@ -0,0 +1,356 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
14 重试机制是网络操作的基本保证
在真实的微服务系统中, ZooKeeper、etcd 等服务发现组件一般会独立部署成一个集群,业务服务通过网络连接这些服务发现节点,完成注册和订阅操作。但即使是机房内部的稳定网络,也无法保证两个节点之间的请求一定成功,因此 Dubbo 这类 RPC 框架在稳定性和容错性方面,就受到了比较大的挑战。为了保证服务的可靠性,重试机制就变得必不可少了。
所谓的 “重试机制”就是在请求失败时,客户端重新发起一个一模一样的请求,尝试调用相同或不同的服务端,完成相应的业务操作。能够使用重试机制的业务接口得是“幂等”的,也就是无论请求发送多少次,得到的结果都是一样的,例如查询操作。
核心设计
在上一课时中,我们介绍了 AbstractRegistry 中的 register()/unregister()、subscribe()/unsubscribe() 以及 notify() 等核心操作,详细分析了通过本地缓存实现的容错功能。其实,这几个核心方法同样也是重试机制的关注点。
dubbo-registry 将重试机制的相关实现放到了 AbstractRegistry 的子类—— FailbackRegistry 中。如下图所示,接入 ZooKeeper、etcd 等开源服务发现组件的 Registry 实现,都继承了 FailbackRegistry也就都拥有了失败重试的能力。
FailbackRegistry 设计核心是:覆盖了 AbstractRegistry 中 register()/unregister()、subscribe()/unsubscribe() 以及 notify() 这五个核心方法,结合前面介绍的时间轮,实现失败重试的能力;真正与服务发现组件的交互能力则是放到了 doRegister()/doUnregister()、doSubscribe()/doUnsubscribe() 以及 doNotify() 这五个抽象方法中,由具体子类实现。这是典型的模板方法模式的应用。
核心字段介绍
分析一个实现类的第一步就是了解其核心字段,那 FailbackRegistry 的核心字段有哪些呢?
retryTimerHashedWheelTimer 类型):用于定时执行失败重试操作的时间轮。
retryPeriodint 类型):重试操作的时间间隔。
failedRegisteredConcurrentMap类型注册失败的 URL 集合,其中 Key 是注册失败的 URLValue 是对应的重试任务。
failedUnregisteredConcurrentMap类型取消注册失败的 URL 集合,其中 Key 是取消注册失败的 URLValue 是对应的重试任务。
failedSubscribedConcurrentMap类型订阅失败 URL 集合,其中 Key 是订阅失败的 URL + Listener 集合Value 是相应的重试任务。
failedUnsubscribedConcurrentMap类型取消订阅失败的 URL 集合,其中 Key 是取消订阅失败的 URL + Listener 集合Value 是相应的重试任务。
failedNotifiedConcurrentMap类型通知失败的 URL 集合,其中 Key 是通知失败的 URL + Listener 集合Value 是相应的重试任务。
在 FailbackRegistry 的构造方法中,首先会调用父类 AbstractRegistry 的构造方法完成本地缓存相关的初始化操作,然后从传入的 URL 参数中获取重试操作的时间间隔即retry.period 参数)来初始化 retryPeriod 字段,最后初始化 retryTimer****时间轮。整个代码比较简单,这里就不展示了。
核心方法实现分析
FailbackRegistry 对 register()/unregister() 方法和 subscribe()/unsubscribe() 方法的具体实现非常类似所以这里我们就只介绍其中register() 方法的具体实现流程。
根据 registryUrl 中 accepts 参数指定的匹配模式,决定是否接受当前要注册的 Provider URL。
调用父类 AbstractRegistry 的 register() 方法,将 Provider URL 写入 registered 集合中。
调用 removeFailedRegistered() 方法和 removeFailedUnregistered() 方法,将该 Provider URL 从 failedRegistered 集合和 failedUnregistered 集合中删除,并停止相关的重试任务。
调用 doRegister() 方法,与服务发现组件进行交互。该方法由子类实现,每个子类只负责接入一个特定的服务发现组件。
在 doRegister() 方法出现异常的时候,会根据 URL 参数以及异常的类型,进行分类处理:待注册 URL 的 check 参数为 true默认值为 true待注册的 URL 不是 consumer 协议registryUrl 的 check 参数也为 true默认值为 true。若满足这三个条件或者抛出的异常为 SkipFailbackWrapperException则直接抛出异常。否则就会创建重试任务并添加到 failedRegistered 集合中。
明确 register() 方法的核心流程之后,我们再来看 register() 方法的具体代码实现:
public void register(URL url) {
if (!acceptable(url)) {
logger.info("..."); // 打印相关的提示日志
return;
}
super.register(url); // 完成本地文件缓存的初始化
// 清理failedRegistered集合和failedUnregistered集合并取消相关任务
removeFailedRegistered(url);
removeFailedUnregistered(url);
try {
doRegister(url); // 与服务发现组件进行交互,具体由子类实现
} catch (Exception e) {
Throwable t = e;
// 检测check参数决定是否直接抛出异常
boolean check = getUrl().getParameter(Constants.CHECK_KEY,
true) && url.getParameter(Constants.CHECK_KEY, true)
&& !CONSUMER_PROTOCOL.equals(url.getProtocol());
boolean skipFailback = t instanceof
SkipFailbackWrapperException;
if (check || skipFailback) {
if (skipFailback) {
t = t.getCause();
}
throw new IllegalStateException("Failed to register");
}
// 如果不抛出异常则创建失败重试的任务并添加到failedRegistered集合中
addFailedRegistered(url);
}
}
从以上代码可以看出,当 Provider 向 Registry 注册 URL 的时候,如果注册失败,且未设置 check 属性,则创建一个定时任务,添加到时间轮中。
下面我们再来看看创建并添加这个重试任务的相关方法——addFailedRegistered() 方法,具体实现如下:
private void addFailedRegistered(URL url) {
FailedRegisteredTask oldOne = failedRegistered.get(url);
if (oldOne != null) { // 已经存在重试任务,则无须创建,直接返回
return;
}
FailedRegisteredTask newTask = new FailedRegisteredTask(url,
this);
oldOne = failedRegistered.putIfAbsent(url, newTask);
if (oldOne == null) {
// 如果是新建的重试任务则提交到时间轮中等待retryPeriod毫秒后执行
retryTimer.newTimeout(newTask, retryPeriod,
TimeUnit.MILLISECONDS);
}
}
重试任务
FailbackRegistry.addFailedRegistered() 方法中创建的 FailedRegisteredTask 任务以及其他的重试任务,都继承了 AbstractRetryTask 抽象类,如下图所示:
在 AbstractRetryTask 中维护了当前任务关联的 URL、当前重试的次数等信息在其 run() 方法中,会根据重试 URL 中指定的重试次数retry.times 参数,默认值为 3、任务是否被取消以及时间轮的状态决定此次任务的 doRetry() 方法是否正常执行。
public void run(Timeout timeout) throws Exception {
if (timeout.isCancelled() || timeout.timer().isStop() || isCancel()) { // 检测定时任务状态和时间轮状态
return;
}
if (times > retryTimes) { // 检查重试次数
logger.warn("...");
return;
}
try {
doRetry(url, registry, timeout); // 执行重试
} catch (Throwable t) {
reput(timeout, retryPeriod); // 重新添加定时任务,等待重试
}
}
如果任务的 doRetry() 方法执行出现异常AbstractRetryTask 会通过 reput() 方法将当前任务重新放入时间轮中,并递增当前任务的执行次数。
protected void reput(Timeout timeout, long tick) {
if (timeout == null) { // 边界检查
throw new IllegalArgumentException();
}
Timer timer = timeout.timer(); // 检查定时任务
if (timer.isStop() || timeout.isCancelled() || isCancel()) {
return;
}
times++; // 递增times
// 添加定时任务
timer.newTimeout(timeout.task(), tick, TimeUnit.MILLISECONDS);
}
AbstractRetryTask 将 doRetry() 方法作为抽象方法,留给子类实现具体的重试逻辑,这也是模板方法的使用。
在子类 FailedRegisteredTask 的 doRetry() 方法实现中,会再次执行关联 Registry 的 doRegister() 方法,完成与服务发现组件交互。如果注册成功,则会调用 removeFailedRegisteredTask() 方法将当前关联的 URL 以及当前重试任务从 failedRegistered 集合中删除。如果注册失败,则会抛出异常,执行上文介绍的 reput ()方法重试。
protected void doRetry(URL url, FailbackRegistry registry, Timeout timeout) {
registry.doRegister(url); // 重新注册
registry.removeFailedRegisteredTask(url); // 删除重试任务
}
public void removeFailedRegisteredTask(URL url) {
failedRegistered.remove(url);
}
另外,在 register() 方法入口处,会主动调用 removeFailedRegistered() 方法和 removeFailedUnregistered() 方法来清理指定 URL 关联的定时任务:
public void register(URL url) {
super.register(url);
removeFailedRegistered(url); // 清理FailedRegisteredTask定时任务
removeFailedUnregistered(url); // 清理FailedUnregisteredTask定时任务
try {
doRegister(url);
} catch (Exception e) {
addFailedRegistered(url);
}
}
其他核心方法
unregister() 方法以及 unsubscribe() 方法的实现方式与 register() 方法类似,只是调用的 do*() 抽象方法、依赖的 AbstractRetryTask 有所不同而已,这里就不再展开细讲。
你还记得上一课时我们介绍的 AbstractRegistry 通过本地文件缓存实现的容错机制吗FailbackRegistry.subscribe() 方法在处理异常的时候,会先获取缓存的订阅数据并调用 notify() 方法,如果没有缓存相应的订阅数据,才会检查 check 参数决定是否抛出异常。
通过上一课时对 AbstractRegistry.notify() 方法的介绍,我们知道其核心逻辑之一就是回调 NotifyListener。下面我们就来看一下 FailbackRegistry 对 notify() 方法的覆盖:
protected void notify(URL url, NotifyListener listener,
List<URL> urls) {
... // 检查url和listener不为空(略)
try {
// FailbackRegistry.doNotify()方法实际上就是调用父类
// AbstractRegistry.notify()方法,没有其他逻辑
doNotify(url, listener, urls);
} catch (Exception t) {
// doNotify()方法出现异常,则会添加一个定时任务
addFailedNotified(url, listener, urls);
}
}
addFailedNotified() 方法会创建相应的 FailedNotifiedTask 任务,添加到 failedNotified 集合中,同时也会添加到时间轮中等待执行。如果已存在相应的 FailedNotifiedTask 重试任务,则会更新任务需要处理的 URL 集合。
在 FailedNotifiedTask 中维护了一个 URL 集合,用来记录当前任务一次运行需要通知的 URL每执行完一次任务就会清空该集合具体实现如下
protected void doRetry(URL url, FailbackRegistry registry,
Timeout timeout) {
// 如果urls集合为空则会通知所有Listener该任务也就啥都不做了
if (CollectionUtils.isNotEmpty(urls)) {
listener.notify(urls);
urls.clear();
}
reput(timeout, retryPeriod); // 将任务重新添加到时间轮中等待执行
}
从上面的代码可以看出FailedNotifiedTask 重试任务一旦被添加,就会一直运行下去,但真的是这样吗?在 FailbackRegistry 的 subscribe()、unsubscribe() 方法中,可以看到 removeFailedNotified() 方法的调用,这里就是清理 FailedNotifiedTask 任务的地方。我们以 FailbackRegistry.subscribe() 方法为例进行介绍:
public void subscribe(URL url, NotifyListener listener) {
super.subscribe(url, listener);
removeFailedSubscribed(url, listener); // 关注这个方法
try {
doSubscribe(url, listener);
} catch (Exception e) {
addFailedSubscribed(url, listener);
}
}
// removeFailedSubscribed()方法中会清理FailedSubscribedTask、FailedUnsubscribedTask、FailedNotifiedTask三类定时任务
private void removeFailedSubscribed(URL url, NotifyListener listener) {
Holder h = new Holder(url, listener); // 清理FailedSubscribedTask
FailedSubscribedTask f = failedSubscribed.remove(h);
if (f != null) {
f.cancel();
}
removeFailedUnsubscribed(url, listener);// 清理FailedUnsubscribedTask
removeFailedNotified(url, listener); // 清理FailedNotifiedTask
}
介绍完 FailbackRegistry 中最核心的注册/订阅实现之后,我们再来关注其实现的恢复功能,也就是 recover() 方法。该方法会直接通过 FailedRegisteredTask 任务处理 registered 集合中的全部 URL通过 FailedSubscribedTask 任务处理 subscribed 集合中的 URL 以及关联的 NotifyListener。
FailbackRegistry 在生命周期结束时,会调用自身的 destroy() 方法,其中除了调用父类的 destroy() 方法之外,还会调用时间轮(即 retryTimer 字段)的 stop() 方法,释放时间轮相关的资源。
总结
本课时重点介绍了 AbstractRegistry 的实现类——FailbackRegistry 的核心实现,它主要是在 AbstractRegistry 的基础上,提供了重试机制。具体方法就是通过之前课时介绍的时间轮,在 register()/ unregister()、subscribe()/ unsubscribe() 等核心方法失败时,添加重试定时任务,实现重试机制,同时也添加了相应的定时任务清理逻辑。

View File

@@ -0,0 +1,447 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
15 ZooKeeper 注册中心实现,官方推荐注册中心实践
Dubbo 支持 ZooKeeper 作为注册中心服务,这也是 Dubbo 推荐使用的注册中心。为了让你能更好地理解 ZooKeeper 在 Dubbo 中的应用,接下来我们就先简单回顾下 ZooKeeper。
Dubbo 本身是一个分布式的 RPC 开源框架,各个依赖于 Dubbo 的服务节点都是单独部署的,为了让 Provider 和 Consumer 能够实时获取彼此的信息就得依赖于一个一致性的服务发现组件实现注册和订阅。Dubbo 可以接入多种服务发现组件例如ZooKeeper、etcd、Consul、Eureka 等。其中Dubbo 特别推荐使用 ZooKeeper。
ZooKeeper 是为分布式应用所设计的高可用且一致性的开源协调服务。它是一个树型的目录服务,支持变更推送,非常适合应用在生产环境中。
下面是 Dubbo 官方文档中的一张图,展示了 Dubbo 在 Zookeeper 中的节点层级结构:
Zookeeper 存储的 Dubbo 数据
图中的“dubbo”节点是 Dubbo 在 Zookeeper 中的根节点“dubbo”是这个根节点的默认名称当然我们也可以通过配置进行修改。
图中 Service 这一层的节点名称是服务接口的全名,例如 demo 示例中该节点的名称为“org.apache.dubbo.demo.DemoService”。
图中 Type 这一层的节点是 URL 的分类一共有四种分类分别是providers服务提供者列表、consumers服务消费者列表、routes路由规则列表和 configurations配置规则列表
根据不同的 Type 节点,图中 URL 这一层中的节点包括Provider URL 、Consumer URL 、Routes URL 和 Configurations URL。
ZookeeperRegistryFactory
在前面第 13 课时介绍 Dubbo 注册中心核心概念的时候,我们讲解了 RegistryFactory 这个工厂接口以及其子类 AbstractRegistryFactoryAbstractRegistryFactory 仅仅是提供了缓存 Registry 对象的功能,并未真正实现 Registry 的创建,具体的创建逻辑是由子类完成的。在 dubbo-registry-zookeeper 模块中的 SPI 配置文件目录位置如下图所示指定了RegistryFactory 的实现类—— ZookeeperRegistryFactory。
RegistryFactory 的 SPI 配置文件位置
ZookeeperRegistryFactory 实现了 AbstractRegistryFactory其中的 createRegistry() 方法会创建 ZookeeperRegistry 实例,后续将由该 ZookeeperRegistry 实例完成与 Zookeeper 的交互。
另外ZookeeperRegistryFactory 中还提供了一个 setZookeeperTransporter() 方法,你可以回顾一下之前我们介绍的 Dubbo SPI 机制,会通过 SPI 或 Spring Ioc 的方式完成自动装载。
ZookeeperTransporter
dubbo-remoting-zookeeper 模块是 dubbo-remoting 模块的子模块,但它并不依赖 dubbo-remoting 中的其他模块,是相对独立的,所以这里我们可以直接介绍该模块。
简单来说dubbo-remoting-zookeeper 模块是在 Apache Curator 的基础上封装了一套 Zookeeper 客户端,将与 Zookeeper 的交互融合到 Dubbo 的体系之中。
dubbo-remoting-zookeeper 模块中有两个核心接口ZookeeperTransporter 接口和 ZookeeperClient 接口。
ZookeeperTransporter 只负责一件事情,那就是创建 ZookeeperClient 对象。
@SPI("curator")
public interface ZookeeperTransporter {
@Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
ZookeeperClient connect(URL url);
}
我们从代码中可以看到ZookeeperTransporter 接口被 @SPI 注解修饰,成为一个扩展点,默认选择扩展名 “curator” 的实现,其中的 connect() 方法用于创建 ZookeeperClient 实例(该方法被 @Adaptive 注解修饰,我们可以通过 URL 参数中的 client 或 transporter 参数覆盖 @SPI 注解指定的默认扩展名)。
按照前面对 Registry 分析的思路作为一个抽象实现AbstractZookeeperTransporter 肯定是实现了创建 ZookeeperClient 之外的其他一些增强功能,然后由子类继承。不然的话,直接由 CuratorZookeeperTransporter 实现 ZookeeperTransporter 接口创建 ZookeeperClient 实例并返回即可,没必要在继承关系中再增加一层抽象类。
public class CuratorZookeeperTransporter extends
AbstractZookeeperTransporter {
// 创建ZookeeperClient实例
public ZookeeperClient createZookeeperClient(URL url) {
return new CuratorZookeeperClient(url);
}
}
AbstractZookeeperTransporter 的核心功能有如下:
缓存 ZookeeperClient 实例;
在某个 Zookeeper 节点无法连接时,切换到备用 Zookeeper 地址。
在配置 Zookeeper 地址的时候,我们可以配置多个 Zookeeper 节点的地址,这样的话,当一个 Zookeeper 节点宕机之后Dubbo 就可以主动切换到其他 Zookeeper 节点。例如,我们提供了如下的 URL 配置:
zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?backup=127.0.0.1:8989,127.0.0.1:9999
AbstractZookeeperTransporter 的 connect() 方法首先会得到上述 URL 中配置的 127.0.0.1:2181、127.0.0.1:8989 和 127.0.0.1:9999 这三个 Zookeeper 节点地址,然后从 ZookeeperClientMap 缓存(这是一个 MapKey 为 Zookeeper 节点地址Value 是相应的 ZookeeperClient 实例)中查找一个可用 ZookeeperClient 实例。如果查找成功,则复用 ZookeeperClient 实例;如果查找失败,则创建一个新的 ZookeeperClient 实例返回并更新 ZookeeperClientMap 缓存。
ZookeeperClient 实例连接到 Zookeeper 集群之后,就可以了解整个 Zookeeper 集群的拓扑,后续再出现 Zookeeper 节点宕机的情况,就是由 Zookeeper 集群本身以及 Apache Curator 共同完成故障转移。
ZookeeperClient
从名字就可以看出ZookeeperClient 接口是 Dubbo 封装的 Zookeeper 客户端,该接口定义了大量的方法,都是用来与 Zookeeper 进行交互的。
create() 方法:创建 ZNode 节点,还提供了创建临时 ZNode 节点的重载方法。
getChildren() 方法:获取指定节点的子节点集合。
getContent() 方法:获取某个节点存储的内容。
delete() 方法:删除节点。
add*Listener() / remove*Listener() 方法:添加/删除监听器。
close() 方法:关闭当前 ZookeeperClient 实例。
AbstractZookeeperClient 作为 ZookeeperClient 接口的抽象实现,主要提供了如下几项能力:
缓存当前 ZookeeperClient 实例创建的持久 ZNode 节点;
管理当前 ZookeeperClient 实例添加的各类监听器;
管理当前 ZookeeperClient 的运行状态。
我们来看 AbstractZookeeperClient 的核心字段,首先是 persistentExistNodePathConcurrentHashSet<String>类型)字段,它缓存了当前 ZookeeperClient 创建的持久 ZNode 节点路径,在创建 ZNode 节点之前,会先查这个缓存,而不是与 Zookeeper 交互来判断持久 ZNode 节点是否存在,这就减少了一次与 Zookeeper 的交互。
dubbo-remoting-zookeeper 对外提供了 StateListener、DataListener 和 ChildListener 三种类型的监听器。
StateListener主要负责监听 Dubbo 与 Zookeeper 集群的连接状态,包括 SESSION_LOST、CONNECTED、RECONNECTED、SUSPENDED 和 NEW_SESSION_CREATED。
DataListener主要监听某个节点存储的数据变化。
ChildListener主要监听某个 ZNode 节点下的子节点变化。
在 AbstractZookeeperClient 中维护了 stateListeners、listeners 以及 childListeners 三个集合,分别管理上述三种类型的监听器。虽然监听内容不同,但是它们的管理方式是类似的,所以这里我们只分析 listeners 集合的操作:
public void addDataListener(String path,
DataListener listener, Executor executor) {
// 获取指定path上的DataListener集合
ConcurrentMap<DataListener, TargetDataListener> dataListenerMap =
listeners.computeIfAbsent(path, k -> new ConcurrentHashMap<>());
// 查询该DataListener关联的TargetDataListener
TargetDataListener targetListener =
dataListenerMap.computeIfAbsent(listener,
k -> createTargetDataListener(path, k));
// 通过TargetDataListener在指定的path上添加监听
addTargetDataListener(path, targetListener, executor);
}
这里的 createTargetDataListener() 方法和 addTargetDataListener() 方法都是抽象方法,由 AbstractZookeeperClient 的子类实现TargetDataListener 是 AbstractZookeeperClient 中标记的一个泛型。
为什么 AbstractZookeeperClient 要使用泛型定义?这是因为不同的 ZookeeperClient 实现可能依赖不同的 Zookeeper 客户端组件,不同 Zookeeper 客户端组件的监听器实现也有所不同,而整个 dubbo-remoting-zookeeper 模块对外暴露的监听器是统一的,就是上面介绍的那三种。因此,这时就需要一层转换进行解耦,这层解耦就是通过 TargetDataListener 完成的。
虽然在 Dubbo 2.7.7 版本中只支持 Curator但是在 Dubbo 2.6.5 版本的源码中可以看到ZookeeperClient 还有使用 ZkClient 的实现。
在最新的 Dubbo 版本中CuratorZookeeperClient 是 AbstractZookeeperClient 的唯一实现类,在其构造方法中会初始化 Curator 客户端并阻塞等待连接成功:
public CuratorZookeeperClient(URL url) {
super(url);
int timeout = url.getParameter("timeout", 5000);
int sessionExpireMs = url.getParameter("zk.session.expire",
60000);
CuratorFrameworkFactory.Builder builder =
CuratorFrameworkFactory.builder()
.connectString(url.getBackupAddress())//zk地址(包括备用地址)
.retryPolicy(new RetryNTimes(1, 1000)) // 重试配置
.connectionTimeoutMs(timeout) // 连接超时时长
.sessionTimeoutMs(sessionExpireMs); // session过期时间
... // 省略处理身份验证的逻辑
client = builder.build();
// 添加连接状态的监听
client.getConnectionStateListenable().addListener(
new CuratorConnectionStateListener(url));
client.start();
boolean connected = client.blockUntilConnected(timeout,
TimeUnit.MILLISECONDS);
... // 检测connected这个返回值连接失败抛出异常
}
CuratorZookeeperClient 与 Zookeeper 交互的全部操作,都是围绕着这个 Apache Curator 客户端展开的, Apache Curator 的具体使用方式在前面的第 6 和 7 课时已经介绍过了,这里就不再赘述。
内部类 CuratorWatcherImpl 就是 CuratorZookeeperClient 实现 AbstractZookeeperClient 时指定的泛型类,它实现了 TreeCacheListener 接口,可以添加到 TreeCache 上监听自身节点以及子节点的变化。在 childEvent() 方法的实现中我们可以看到,当 TreeCache 关注的树型结构发生变化时,会将触发事件的路径、节点内容以及事件类型传递给关联的 DataListener 实例进行回调:
public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception {
if (dataListener != null) {
TreeCacheEvent.Type type = event.getType();
EventType eventType = null;
String content = null;
String path = null;
switch (type) {
case NODE_ADDED:
eventType = EventType.NodeCreated;
path = event.getData().getPath();
content = event.getData().getData() == null ? "" : new String(event.getData().getData(), CHARSET);
break;
case NODE_UPDATED:
...
case NODE_REMOVED:
...
// 省略其他时间的处理
}
// 回调DataListener传递触发事件的path、节点内容以及事件类型
dataListener.dataChanged(path, content, eventType);
}
}
在 CuratorZookeeperClient 的 addTargetDataListener() 方法实现中,我们可以看到 TreeCache 的创建、启动逻辑以及添加 CuratorWatcherImpl 监听的逻辑:
protected void addTargetDataListener(String path, CuratorZookeeperClient.CuratorWatcherImpl treeCacheListener, Executor executor) {
// 创建TreeCache
TreeCache treeCache = TreeCache.newBuilder(client, path).setCacheData(false).build();
treeCacheMap.putIfAbsent(path, treeCache); // 缓存TreeCache
if (executor == null) { // 添加监听
treeCache.getListenable().addListener(treeCacheListener);
} else {
treeCache.getListenable().addListener(treeCacheListener, executor);
}
treeCache.start(); // 启动
}
如果需要在回调中获取全部 Child 节点,那么 dubbo-remoting-zookeeper 调用方需要使用 ChildListener在下面即将介绍的 ZookeeperRegistry 中可以看到 ChildListener 相关使用方式。CuratorWatcherImpl 也是 ChildListener 与 CuratorWatcher 的桥梁,具体实现方式与上述逻辑类似,这里不再展开。
到此为止dubbo-remoting-zookeeper 模块的核心实现就介绍完了,该模块作为 Dubbo 与 Zookeeper 交互的基础,不仅支撑了基于 Zookeeper 的注册中心的实现,还支撑了基于 Zookeeper 的服务发现的实现。这里我们重点关注基于 Zookeeper 的注册中心实现。
ZookeeperRegistry
下面我们回到 dubbo-registry-zookeeper 模块,继续分析基于 Zookeeper 的注册中心实现。
在 ZookeeperRegistry 的构造方法中,会通过 ZookeeperTransporter 创建 ZookeeperClient 实例并连接到 Zookeeper 集群同时还会添加一个连接状态的监听器。在该监听器中主要关注RECONNECTED 状态和 NEW_SESSION_CREATED 状态,在当前 Dubbo 节点与 Zookeeper 的连接恢复或是 Session 恢复的时候,会重新进行注册/订阅,防止数据丢失。这段代码比较简单,我们就不展开分析了。
doRegister() 方法和 doUnregister() 方法的实现都是通过 ZookeeperClient 找到合适的路径,然后创建(或删除)相应的 ZNode 节点。这里唯一需要注意的是doRegister() 方法注册 Provider URL 的时候,会根据 dynamic 参数决定创建临时 ZNode 节点还是持久 ZNode 节点(默认创建临时 ZNode 节点),这样当 Provider 端与 Zookeeper 会话关闭时,可以快速将变更推送到 Consumer 端。
这里注意一下 toUrlPath() 这个方法得到的路径,是由下图中展示的方法拼装而成的,其中每个方法对应本课时开始展示的 Zookeeper 节点层级图中的一层。
doSubscribe() 方法的核心是通过 ZookeeperClient 在指定的 path 上添加 ChildListener 监听器,当订阅的节点发现变化的时候,会通过 ChildListener 监听器触发 notify() 方法,在 notify() 方法中会触发传入的 NotifyListener 监听器。
从 doSubscribe() 方法的代码结构可看出doSubscribe() 方法的逻辑分为了两个大的分支。
一个分支是处理:订阅 URL 中明确指定了 Service 层接口的订阅请求。该分支会从 URL 拿到 Consumer 关注的 category 节点集合,然后在每个 category 节点上添加 ChildListener 监听器。下面是 Demo 示例中 Consumer 订阅的三个 path图中展示了构造 path 各个部分的相关方法:
下面是这个分支的核心源码分析:
List<URL> urls = new ArrayList<>();
for (String path : toCategoriesPath(url)) { // 要订阅的所有path
// 订阅URL对应的Listener集合
ConcurrentMap<NotifyListener, ChildListener> listeners =
zkListeners.computeIfAbsent(url,
k -> new ConcurrentHashMap<>());
// 一个NotifyListener关联一个ChildListener这个ChildListener会回调
// ZookeeperRegistry.notify()方法其中会回调当前NotifyListener
ChildListener zkListener = listeners.computeIfAbsent(listener,
k -> (parentPath, currentChilds) ->
ZookeeperRegistry.this.notify(url, k,
toUrlsWithEmpty(url, parentPath, currentChilds)));
// 尝试创建持久节点主要是为了确保当前path在Zookeeper上存在
zkClient.create(path, false);
// 这一个ChildListener会添加到多个path上
List<String> children = zkClient.addChildListener(path,
zkListener);
if (children != null) {
// 如果没有Provider注册toUrlsWithEmpty()方法会返回empty协议的URL
urls.addAll(toUrlsWithEmpty(url, path, children));
}
}
// 初次订阅的时候会主动调用一次notify()方法通知NotifyListener处理当前已有的
// URL等注册数据
notify(url, listener, urls);
doSubscribe() 方法的另一个分支是处理:监听所有 Service 层节点的订阅请求例如Monitor 就会发出这种订阅请求,因为它需要监控所有 Service 节点的变化。这个分支的处理逻辑是在根节点上添加一个 ChildListener 监听器,当有 Service 层的节点出现的时候,会触发这个 ChildListener其中会重新触发 doSubscribe() 方法执行上一个分支的逻辑(即前面分析的针对确定的 Service 层接口订阅分支)。
下面是针对这个分支核心代码的分析:
String root = toRootPath(); // 获取根节点
// 获取NotifyListener对应的ChildListener
ConcurrentMap<NotifyListener, ChildListener> listeners =
zkListeners.computeIfAbsent(url, k -> new ConcurrentHashMap<>());
ChildListener zkListener = listeners.computeIfAbsent(listener, k ->
(parentPath, currentChilds) -> {
for (String child : currentChilds) {
child = URL.decode(child);
if (!anyServices.contains(child)) {
anyServices.add(child); // 记录该节点已经订阅过
// 该ChildListener要做的就是触发对具体Service节点的订阅
subscribe(url.setPath(child).addParameters("interface",
child, "check", String.valueOf(false)), k);
}
}
});
zkClient.create(root, false); // 保证根节点存在
// 第一次订阅的时候要处理当前已有的Service层节点
List<String> services = zkClient.addChildListener(root, zkListener);
if (CollectionUtils.isNotEmpty(services)) {
for (String service : services) {
service = URL.decode(service);
anyServices.add(service);
subscribe(url.setPath(service).addParameters(INTERFACE_KEY,
service, "check", String.valueOf(false)), listener);
}
}
ZookeeperRegistry 提供的 doUnsubscribe() 方法实现会将 URL 和 NotifyListener 对应的 ChildListener 从相关的 path 上删除,从而达到不再监听该 path 的效果。
总结
本课时我们重点介绍了 Dubbo 接入 Zookeeper 作为注册中心的核心实现。
首先我们快速回顾了 Zookeeper 的基础内容,以及作为 Dubbo 注册中心时 Zookeeper 存储的具体内容,之后介绍了针对 Zookeeper 的 RegistryFactory 实现—— ZookeeperRegistryFactory。
接下来我们讲解了 Dubbo 接入 Zookeeper 时使用的组件实现,重点分析了 ZookeeperTransporter 和 ZookeeperClient 实现,它们底层依赖 Apache Curator 与 Zookeeper 完成交互。
最后,我们还说明了 ZookeeperRegistry 是如何通过 ZookeeperClient 接入 Zookeeper实现 Registry 的相关功能。
关于本课时,你若还有什么疑问或想法,欢迎你留言跟我分享。

View File

@@ -0,0 +1,193 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
16 Dubbo Serialize 层:多种序列化算法,总有一款适合你
通过前面课时的介绍,我们知道一个 RPC 框架需要通过网络通信实现跨 JVM 的调用。既然需要网络通信那就必然会使用到序列化与反序列化的相关技术Dubbo 也不例外。下面我们从 Java 序列化的基础内容开始,介绍一下常见的序列化算法,最后再分析一下 Dubbo 是如何支持这些序列化算法的。
Java 序列化基础
Java 中的序列化操作一般有如下四个步骤。
第一步,被序列化的对象需要实现 Serializable 接口,示例代码如下:
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private transient StudentUtil studentUtil;
}
在这个示例中我们可以看到transient 关键字,它的作用就是:在对象序列化过程中忽略被其修饰的成员属性变量。一般情况下,它可以用来修饰一些非数据型的字段以及一些可以通过其他字段计算得到的值。通过合理地使用 transient 关键字,可以降低序列化后的数据量,提高网络传输效率。
第二步,生成一个序列号 serialVersionUID这个序列号不是必需的但还是建议你生成。serialVersionUID 的字面含义是序列化的版本号,只有序列化和反序列化的 serialVersionUID 都相同的情况下,才能够成功地反序列化。如果类中没有定义 serialVersionUID那么 JDK 也会随机生成一个 serialVersionUID。如果在某些场景中你希望不同版本的类序列化和反序列化相互兼容那就需要定义相同的 serialVersionUID。
第三步,根据需求决定是否要重写 writeObject()/readObject() 方法,实现自定义序列化。
最后一步,调用 java.io.ObjectOutputStream 的 writeObject()/readObject() 进行序列化与反序列化。
既然 Java 本身的序列化操作如此简单,那为什么市面上还依旧出现了各种各样的序列化框架呢?因为这些第三方序列化框架的速度更快、序列化的效率更高,而且支持跨语言操作。
常见序列化算法
为了帮助你快速了解 Dubbo 支持的序列化算法,我们这里就对其中常见的序列化算法进行简单介绍。
Apache Avro 是一种与编程语言无关的序列化格式。Avro 依赖于用户自定义的 Schema在进行序列化数据的时候无须多余的开销就可以快速完成序列化并且生成的序列化数据也较小。当进行反序列化的时候需要获取到写入数据时用到的 Schema。在 Kafka、Hadoop 以及 Dubbo 中都可以使用 Avro 作为序列化方案。
FastJson 是阿里开源的 JSON 解析库,可以解析 JSON 格式的字符串。它支持将 Java 对象序列化为 JSON 字符串,反过来从 JSON 字符串也可以反序列化为 Java 对象。FastJson 是 Java 程序员常用到的类库之一正如其名“快”是其主要卖点。从官方的测试结果来看FastJson 确实是最快的,比 Jackson 快 20% 左右,但是近几年 FastJson 的安全漏洞比较多,所以你在选择版本的时候,还是需要谨慎一些。
Fst全称是 fast-serialization是一款高性能 Java 对象序列化工具包100% 兼容 JDK 原生环境序列化速度大概是JDK 原生序列化的 4~10 倍,序列化后的数据大小是 JDK 原生序列化大小的 13 左右。目前Fst 已经更新到 3.x 版本,支持 JDK 14。
Kryo 是一个高效的 Java 序列化/反序列化库,目前 Twitter、Yahoo、Apache 等都在使用该序列化技术,特别是 Spark、Hive 等大数据领域用得较多。Kryo 提供了一套快速、高效和易用的序列化 API。无论是数据库存储还是网络传输都可以使用 Kryo 完成 Java 对象的序列化。Kryo 还可以执行自动深拷贝和浅拷贝支持环形引用。Kryo 的特点是 API 代码简单序列化速度快并且序列化之后得到的数据比较小。另外Kryo 还提供了 NIO 的网络通信库——KryoNet你若感兴趣的话可以自行查询和了解一下。
Hessian2 序列化是一种支持动态类型、跨语言的序列化协议Java 对象序列化的二进制流可以被其他语言使用。Hessian2 序列化之后的数据可以进行自描述,不会像 Avro 那样依赖外部的 Schema 描述文件或者接口定义。Hessian2 可以用一个字节表示常用的基础类型,这极大缩短了序列化之后的二进制流。需要注意的是,在 Dubbo 中使用的 Hessian2 序列化并不是原生的 Hessian2 序列化,而是阿里修改过的 Hessian Lite它是 Dubbo 默认使用的序列化方式。其序列化之后的二进制流大小大约是 Java 序列化的 50%,序列化耗时大约是 Java 序列化的 30%,反序列化耗时大约是 Java 序列化的 20%。
ProtobufGoogle Protocol Buffers是 Google 公司开发的一套灵活、高效、自动化的、用于对结构化数据进行序列化的协议。但相比于常用的 JSON 格式Protobuf 有更高的转化效率,时间效率和空间效率都是 JSON 的 5 倍左右。Protobuf 可用于通信协议、数据存储等领域,它本身是语言无关、平台无关、可扩展的序列化结构数据格式。目前 Protobuf提供了 C++、Java、Python、Go 等多种语言的 APIgRPC 底层就是使用 Protobuf 实现的序列化。
dubbo-serialization
Dubbo 为了支持多种序列化算法,单独抽象了一层 Serialize 层,在整个 Dubbo 架构中处于最底层,对应的模块是 dubbo-serialization 模块。 dubbo-serialization 模块的结构如下图所示:
dubbo-serialization-api 模块中定义了 Dubbo 序列化层的核心接口,其中最核心的是 Serialization 这个接口,它是一个扩展接口,被 @SPI 接口修饰,默认扩展实现是 Hessian2Serialization。Serialization 接口的具体实现如下:
@SPI("hessian2") // 被@SPI注解修饰默认是使用hessian2序列化算法
public interface Serialization {
// 每一种序列化算法都对应一个ContentType该方法用于获取ContentType
String getContentType();
// 获取ContentType的ID值是一个byte类型的值唯一确定一个算法
byte getContentTypeId();
// 创建一个ObjectOutput对象ObjectOutput负责实现序列化的功能即将Java
// 对象转化为字节序列
@Adaptive
ObjectOutput serialize(URL url, OutputStream output) throws IOException;
// 创建一个ObjectInput对象ObjectInput负责实现反序列化的功能即将
// 字节序列转换成Java对象
@Adaptive
ObjectInput deserialize(URL url, InputStream input) throws IOException;
}
Dubbo 提供了多个 Serialization 接口实现,用于接入各种各样的序列化算法,如下图所示:
这里我们以默认的 hessian2 序列化方式为例,介绍 Serialization 接口的实现以及其他相关实现。 Hessian2Serialization 实现如下所示:
public class Hessian2Serialization implements Serialization {
public byte getContentTypeId() {
return HESSIAN2_SERIALIZATION_ID; // hessian2的ContentType ID
}
public String getContentType() { // hessian2的ContentType
return "x-application/hessian2";
}
public ObjectOutput serialize(URL url, OutputStream out) throws IOException { // 创建ObjectOutput对象
return new Hessian2ObjectOutput(out);
}
public ObjectInput deserialize(URL url, InputStream is) throws IOException { // 创建ObjectInput对象
return new Hessian2ObjectInput(is);
}
}
Hessian2Serialization 中的 serialize() 方法创建的 ObjectOutput 接口实现为 Hessian2ObjectOutput继承关系如下图所示
在 DataOutput 接口中定义了序列化 Java 中各种数据类型的相应方法,如下图所示,其中有序列化 boolean、short、int、long 等基础类型的方法,也有序列化 String、byte[] 的方法。
ObjectOutput 接口继承了 DataOutput 接口,并在其基础之上,添加了序列化对象的功能,具体定义如下图所示,其中的 writeThrowable()、writeEvent() 和 writeAttachments() 方法都是调用 writeObject() 方法实现的。
Hessian2ObjectOutput 中会封装一个 Hessian2Output 对象,需要注意,这个对象是 ThreadLocal 的,与线程绑定。在 DataOutput 接口以及 ObjectOutput 接口中,序列化各类型数据的方法都会委托给 Hessian2Output 对象的相应方法完成,实现如下:
public class Hessian2ObjectOutput implements ObjectOutput {
private static ThreadLocal<Hessian2Output> OUTPUT_TL = ThreadLocal.withInitial(() -> {
// 初始化Hessian2Output对象
Hessian2Output h2o = new Hessian2Output(null); h2o.setSerializerFactory(Hessian2SerializerFactory.SERIALIZER_FACTORY);
h2o.setCloseStreamOnClose(true);
return h2o;
});
private final Hessian2Output mH2o;
public Hessian2ObjectOutput(OutputStream os) {
mH2o = OUTPUT_TL.get(); // 触发OUTPUT_TL的初始化
mH2o.init(os);
}
public void writeObject(Object obj) throws IOException {
mH2o.writeObject(obj);
}
... // 省略序列化其他类型数据的方法
}
Hessian2Serialization 中的 deserialize() 方法创建的 ObjectInput 接口实现为 Hessian2ObjectInput继承关系如下所示
Hessian2ObjectInput 具体的实现与 Hessian2ObjectOutput 类似:在 DataInput 接口中实现了反序列化各种类型的方法,在 ObjectInput 接口中提供了反序列化 Java 对象的功能,在 Hessian2ObjectInput 中会将所有反序列化的实现委托为 Hessian2Input。
了解了 Dubbo Serialize 层的核心接口以及 Hessian2 序列化算法的接入方式之后,你就可以亲自动手,去阅读其他序列化算法对应模块的代码。
总结
在本课时,我们首先介绍了 Java 序列化的基础知识帮助你快速了解序列化和反序列化的基本概念。然后介绍了常见的序列化算法例如Arvo、Fastjson、Fst、Kryo、Hessian、Protobuf 等。最后,深入分析了 dubbo-serialization 模块对各个序列化算法的接入方式,其中重点说明了 Hessian2 序列化方式。
关于本课时,你若还有什么疑问或想法,欢迎你留言跟我分享。

View File

@@ -0,0 +1,234 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
17 Dubbo Remoting 层核心接口分析:这居然是一套兼容所有 NIO 框架的设计?
在本专栏的第二部分,我们深入介绍了 Dubbo 注册中心的相关实现,下面我们开始介绍 dubbo-remoting 模块,该模块提供了多种客户端和服务端通信的功能。在 Dubbo 的整体架构设计图中,我们可以看到最底层红色框选中的部分即为 Remoting 层,其中包括了 Exchange、Transport和Serialize 三个子层次。这里我们要介绍的 dubbo-remoting 模块主要对应 Exchange 和 Transport 两层。
Dubbo 整体架构设计图
Dubbo 并没有自己实现一套完整的网络库而是使用现有的、相对成熟的第三方网络库例如Netty、Mina 或是 Grizzly 等 NIO 框架。我们可以根据自己的实际场景和需求修改配置,选择底层使用的 NIO 框架。
下图展示了 dubbo-remoting 模块的结构,其中每个子模块对应一个第三方 NIO 框架例如dubbo-remoting-netty4 子模块使用 Netty4 实现 Dubbo 的远程通信dubbo-remoting-grizzly 子模块使用 Grizzly 实现 Dubbo 的远程通信。
其中的 dubbo-remoting-zookeeper我们在前面第 15 课时介绍基于 Zookeeper 的注册中心实现时已经讲解过了,它使用 Apache Curator 实现了与 Zookeeper 的交互。
dubbo-remoting-api 模块
需要注意的是Dubbo 的 dubbo-remoting-api 是其他 dubbo-remoting-* 模块的顶层抽象,其他 dubbo-remoting 子模块都是依赖第三方 NIO 库实现 dubbo-remoting-api 模块的,依赖关系如下图所示:
我们先来看一下 dubbo-remoting-api 中对整个 Remoting 层的抽象dubbo-remoting-api 模块的结构如下图所示:
一般情况下,我们会将功能类似或是相关联的类放到一个包中,所以我们需要先来了解 dubbo-remoting-api 模块中各个包的功能。
buffer 包定义了缓冲区相关的接口、抽象类以及实现类。缓冲区在NIO框架中是一个不可或缺的角色在各个 NIO 框架中都有自己的缓冲区实现。这里的 buffer 包在更高的层面,抽象了各个 NIO 框架的缓冲区,同时也提供了一些基础实现。
exchange 包:抽象了 Request 和 Response 两个概念,并为其添加很多特性。这是整个远程调用非常核心的部分。
transport 包:对网络传输层的抽象,但它只负责抽象单向消息的传输,即请求消息由 Client 端发出Server 端接收;响应消息由 Server 端发出Client端接收。有很多网络库可以实现网络传输的功能例如 Netty、Grizzly 等, transport 包是在这些网络库上层的一层抽象。
其他接口Endpoint、Channel、Transporter、Dispatcher 等顶层接口放到了org.apache.dubbo.remoting 这个包,这些接口是 Dubbo Remoting 的核心接口。
下面我们就来介绍 Dubbo 是如何抽象这些核心接口的。
传输层核心接口
在 Dubbo 中会抽象出一个“端点Endpoint”的概念我们可以通过一个 ip 和 port 唯一确定一个端点,两个端点之间会创建 TCP 连接可以双向传输数据。Dubbo 将 Endpoint 之间的 TCP 连接抽象为通道Channel将发起请求的 Endpoint 抽象为客户端Client将接收请求的 Endpoint 抽象为服务端Server。这些抽象出来的概念也是整个 dubbo-remoting-api 模块的基础,下面我们会逐个进行介绍。
Dubbo 中Endpoint 接口的定义如下:
如上图所示,这里的 get*() 方法是获得 Endpoint 本身的一些属性,其中包括获取 Endpoint 的本地地址、关联的 URL 信息以及底层 Channel 关联的 ChannelHandler。send() 方法负责数据发送,两个重载的区别在后面介绍 Endpoint 实现的时候我们再详细说明。最后两个 close() 方法的重载以及 startClose() 方法用于关闭底层 Channel isClosed() 方法用于检测底层 Channel 是否已关闭。
Channel 是对两个 Endpoint 连接的抽象,好比连接两个位置的传送带,两个 Endpoint 传输的消息就好比传送带上的货物,消息发送端会往 Channel 写入消息,而接收端会从 Channel 读取消息。这与第 10 课时介绍的 Netty 中的 Channel 基本一致。
下面是Channel 接口的定义,我们可以看出两点:一个是 Channel 接口继承了 Endpoint 接口,也具备开关状态以及发送数据的能力;另一个是可以在 Channel 上附加 KV 属性。
ChannelHandler 是注册在 Channel 上的消息处理器,在 Netty 中也有类似的抽象,相信你对此应该不会陌生。下图展示了 ChannelHandler 接口的定义,在 ChannelHandler 中可以处理 Channel 的连接建立以及连接断开事件,还可以处理读取到的数据、发送的数据以及捕获到的异常。从这些方法的命名可以看到,它们都是动词的过去式,说明相应事件已经发生过了。
需要注意的是ChannelHandler 接口被 @SPI 注解修饰,表示该接口是一个扩展点。
在前面课时介绍 Netty 的时候,我们提到过有一类特殊的 ChannelHandler 专门负责实现编解码功能从而实现字节数据与有意义的消息之间的转换或是消息之间的相互转换。在dubbo-remoting-api 中也有相似的抽象,如下所示:
@SPI
public interface Codec2 {
@Adaptive({Constants.CODEC_KEY})
void encode(Channel channel, ChannelBuffer buffer, Object message)
throws IOException;
@Adaptive({Constants.CODEC_KEY})
Object decode(Channel channel, ChannelBuffer buffer)
throws IOException;
enum DecodeResult {
NEED_MORE_INPUT, SKIP_SOME_INPUT
}
}
这里需要关注的是 Codec2 接口被 @SPI 接口修饰了,表示该接口是一个扩展接口,同时其 encode() 方法和 decode() 方法都被 @Adaptive 注解修饰,也就会生成适配器类,其中会根据 URL 中的 codec 值确定具体的扩展实现类。
DecodeResult 这个枚举是在处理 TCP 传输时粘包和拆包使用的,之前简易版本 RPC 也处理过这种问题,例如,当前能读取到的数据不足以构成一个消息时,就会使用 NEED_MORE_INPUT 这个枚举。
接下来看Client 和 RemotingServer 两个接口,分别抽象了客户端和服务端,两者都继承了 Channel、Resetable 等接口,也就是说两者都具备了读写数据能力。
Client 和 Server 本身都是 Endpoint只不过在语义上区分了请求和响应的职责两者都具备发送的能力所以都继承了 Endpoint 接口。Client 和 Server 的主要区别是 Client 只能关联一个 Channel而 Server 可以接收多个 Client 发起的 Channel 连接。所以在 RemotingServer 接口中定义了查询 Channel 的相关方法,如下图所示:
Dubbo 在 Client 和 Server 之上又封装了一层Transporter 接口,其具体定义如下:
@SPI("netty")
public interface Transporter {
@Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
RemotingServer bind(URL url, ChannelHandler handler)
throws RemotingException;
@Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
Client connect(URL url, ChannelHandler handler)
throws RemotingException;
}
我们看到 Transporter 接口上有 @SPI 注解它是一个扩展接口默认使用“netty”这个扩展名@Adaptive 注解的出现表示动态生成适配器类会先后根据“server”“transporter”的值确定 RemotingServer 的扩展实现类先后根据“client”“transporter”的值确定 Client 接口的扩展实现。
Transporter 接口的实现有哪些呢?如下图所示,针对每个支持的 NIO 库,都有一个 Transporter 接口实现,散落在各个 dubbo-remoting-* 实现模块中。
这些 Transporter 接口实现返回的 Client 和 RemotingServer 具体是什么呢?如下图所示,返回的是 NIO 库对应的 RemotingServer 实现和 Client 实现。
相信看到这里,你应该已经发现 Transporter 这一层抽象出来的接口,与 Netty 的核心接口是非常相似的。那为什么要单独抽象出 Transporter层而不是像简易版 RPC 框架那样,直接让上层使用 Netty 呢?
其实这个问题的答案也呼之欲出了Netty、Mina、Grizzly 这个 NIO 库对外接口和使用方式不一样,如果在上层直接依赖了 Netty 或是 Grizzly就依赖了具体的 NIO 库实现,而不是依赖一个有传输能力的抽象,后续要切换实现的话,就需要修改依赖和接入的相关代码,非常容易改出 Bug。这也不符合设计模式中的开放-封闭原则。
有了 Transporter 层之后,我们可以通过 Dubbo SPI 修改使用的具体 Transporter 扩展实现,从而切换到不同的 Client 和 RemotingServer 实现,达到底层 NIO 库切换的目的,而且无须修改任何代码。即使有更先进的 NIO 库出现,我们也只需要开发相应的 dubbo-remoting-* 实现模块提供 Transporter、Client、RemotingServer 等核心接口的实现,即可接入,完全符合开放-封闭原则。
在最后我们还要看一个类——Transporters它不是一个接口而是门面类其中封装了 Transporter 对象的创建(通过 Dubbo SPI以及 ChannelHandler 的处理,如下所示:
public class Transporters {
private Transporters() {
// 省略bind()和connect()方法的重载
public static RemotingServer bind(URL url,
ChannelHandler... handlers) throws RemotingException {
ChannelHandler handler;
if (handlers.length == 1) {
handler = handlers[0];
} else {
handler = new ChannelHandlerDispatcher(handlers);
}
return getTransporter().bind(url, handler);
}
public static Client connect(URL url, ChannelHandler... handlers)
throws RemotingException {
ChannelHandler handler;
if (handlers == null || handlers.length == 0) {
handler = new ChannelHandlerAdapter();
} else if (handlers.length == 1) {
handler = handlers[0];
} else { // ChannelHandlerDispatcher
handler = new ChannelHandlerDispatcher(handlers);
}
return getTransporter().connect(url, handler);
}
public static Transporter getTransporter() {
// 自动生成Transporter适配器并加载
return ExtensionLoader.getExtensionLoader(Transporter.class)
.getAdaptiveExtension();
}
}
在创建 Client 和 RemotingServer 的时候,可以指定多个 ChannelHandler 绑定到 Channel 来处理其中传输的数据。Transporters.connect() 方法和 bind() 方法中,会将多个 ChannelHandler 封装成一个 ChannelHandlerDispatcher 对象。
ChannelHandlerDispatcher 也是 ChannelHandler 接口的实现类之一,维护了一个 CopyOnWriteArraySet 集合,它所有的 ChannelHandler 接口实现都会调用其中每个 ChannelHandler 元素的相应方法。另外ChannelHandlerDispatcher 还提供了增删该 ChannelHandler 集合的相关方法。
到此为止Dubbo Transport 层的核心接口就介绍完了,这里简单总结一下:
Endpoint 接口抽象了“端点”的概念,这是所有抽象接口的基础。
上层使用方会通过 Transporters 门面类获取到 Transporter 的具体扩展实现,然后通过 Transporter 拿到相应的 Client 和 RemotingServer 实现就可以建立或接收Channel 与远端进行交互了。
无论是 Client 还是 RemotingServer都会使用 ChannelHandler 处理 Channel 中传输的数据,其中负责编解码的 ChannelHandler 被抽象出为 Codec2 接口。
整个架构如下图所示,与 Netty 的架构非常类似。
Transporter 层整体结构图
总结
本课时我们首先介绍了 dubbo-remoting 模块在 Dubbo 架构中的位置,以及 dubbo-remoting 模块的结构。接下来分析了 dubbo-remoting 模块中各个子模块之间的依赖关系,并重点介绍了 dubbo-remoting-api 子模块中各个包的核心功能。最后我们还深入分析了整个 Transport 层的核心接口,以及这些接口抽象出来的 Transporter 架构。
关于本课时,你若还有什么疑问或想法,欢迎你留言跟我分享。

View File

@@ -0,0 +1,282 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
18 Buffer 缓冲区:我们不生产数据,我们只是数据的搬运工
Buffer 是一种字节容器,在 Netty 等 NIO 框架中都有类似的设计例如Java NIO 中的ByteBuffer、Netty4 中的 ByteBuf。Dubbo 抽象出了 ChannelBuffer 接口对底层 NIO 框架中的 Buffer 设计进行统一,其子类如下图所示:
ChannelBuffer 继承关系图
下面我们就按照 ChannelBuffer 的继承结构,从顶层的 ChannelBuffer 接口开始,逐个向下介绍,直至最底层的各个实现类。
ChannelBuffer 接口
ChannelBuffer 接口的设计与 Netty4 中 ByteBuf 抽象类的设计基本一致,也有 readerIndex 和 writerIndex 指针的概念,如下所示,它们的核心方法也是如出一辙。
getBytes()、setBytes() 方法:从参数指定的位置读、写当前 ChannelBuffer不会修改 readerIndex 和 writerIndex 指针的位置。
readBytes() 、writeBytes() 方法:也是读、写当前 ChannelBuffer但是 readBytes() 方法会从 readerIndex 指针开始读取数据,并移动 readerIndex 指针writeBytes() 方法会从 writerIndex 指针位置开始写入数据,并移动 writerIndex 指针。
markReaderIndex()、markWriterIndex() 方法:记录当前 readerIndex 指针和 writerIndex 指针的位置,一般会和 resetReaderIndex()、resetWriterIndex() 方法配套使用。resetReaderIndex() 方法会将 readerIndex 指针重置到 markReaderIndex() 方法标记的位置resetwriterIndex() 方法同理。
capacity()、clear()、copy() 等辅助方法用来获取 ChannelBuffer 容量以及实现清理、拷贝数据的功能,这里不再赘述。
factory() 方法:该方法返回创建 ChannelBuffer 的工厂对象ChannelBufferFactory 中定义了多个 getBuffer() 方法重载来创建 ChannelBuffer如下图所示这些 ChannelBufferFactory的实现都是单例的。
ChannelBufferFactory 继承关系图
AbstractChannelBuffer 抽象类实现了 ChannelBuffer 接口的大部分方法,其核心是维护了以下四个索引。
readerIndex、writerIndexint 类型):通过 readBytes() 方法及其重载读取数据时,会后移 readerIndex 索引;通过 writeBytes() 方法及其重载写入数据的时候,会后移 writerIndex 索引。
markedReaderIndex、markedWriterIndexint 类型):实现记录 readerIndexwriterIndex以及回滚 readerIndexwriterIndex的功能前面我们已经介绍过markReaderIndex() 方法、resetReaderIndex() 方法以及 markWriterIndex() 方法、resetWriterIndex() 方法,你可以对比学习。
AbstractChannelBuffer 中 readBytes() 和 writeBytes() 方法的各个重载最终会通过 getBytes() 方法和 setBytes() 方法实现数据的读写,这些方法在 AbstractChannelBuffer 子类中实现。下面以读写一个 byte 数组为例,进行介绍:
public void readBytes(byte[] dst, int dstIndex, int length) {
// 检测可读字节数是否足够
checkReadableBytes(length);
// 将readerIndex之后的length个字节数读取到dst数组中dstIndex~
// dstIndex+length的位置
getBytes(readerIndex, dst, dstIndex, length);
// 将readerIndex后移length个字节
readerIndex += length;
}
public void writeBytes(byte[] src, int srcIndex, int length) {
// 将src数组中srcIndex~srcIndex+length的数据写入当前buffer中
// writerIndex~writerIndex+length的位置
setBytes(writerIndex, src, srcIndex, length);
// 将writeIndex后移length个字节
writerIndex += length;
}
Buffer 各实现类解析
了解了 ChannelBuffer 接口的核心方法以及 AbstractChannelBuffer 的公共实现之后,我们再来看 ChannelBuffer 的具体实现。
HeapChannelBuffer 是基于字节数组的 ChannelBuffer 实现,我们可以看到其中有一个 arraybyte[]数组)字段,它就是 HeapChannelBuffer 存储数据的地方。HeapChannelBuffer 的 setBytes() 以及 getBytes() 方法实现是调用 System.arraycopy() 方法完成数组操作的,具体实现如下:
public void setBytes(int index, byte[] src, int srcIndex, int length) {
System.arraycopy(src, srcIndex, array, index, length);
}
public void getBytes(int index, byte[] dst, int dstIndex, int length) {
System.arraycopy(array, index, dst, dstIndex, length);
}
HeapChannelBuffer 对应的 ChannelBufferFactory 实现是 HeapChannelBufferFactory其 getBuffer() 方法会通过 ChannelBuffers 这个工具类创建一个指定大小 HeapChannelBuffer 对象,下面简单介绍两个 getBuffer() 方法重载:
@Override
public ChannelBuffer getBuffer(int capacity) {
// 新建一个HeapChannelBuffer底层的会新建一个长度为capacity的byte数组
return ChannelBuffers.buffer(capacity);
}
@Override
public ChannelBuffer getBuffer(byte[] array, int offset, int length) {
// 新建一个HeapChannelBuffer并且会拷贝array数组中offset~offset+lenght
// 的数据到新HeapChannelBuffer中
return ChannelBuffers.wrappedBuffer(array, offset, length);
}
其他 getBuffer() 方法重载这里就不再展示,你若感兴趣的话可以参考源码进行学习。
DynamicChannelBuffer 可以认为是其他 ChannelBuffer 的装饰器,它可以为其他 ChannelBuffer 添加动态扩展容量的功能。DynamicChannelBuffer 中有两个核心字段:
bufferChannelBuffer 类型),是被修饰的 ChannelBuffer默认为 HeapChannelBuffer。
factoryChannelBufferFactory 类型),用于创建被修饰的 HeapChannelBuffer 对象的 ChannelBufferFactory 工厂,默认为 HeapChannelBufferFactory。
DynamicChannelBuffer 需要关注的是 ensureWritableBytes() 方法,该方法实现了动态扩容的功能,在每次写入数据之前,都需要调用该方法确定当前可用空间是否足够,调用位置如下图所示:
ensureWritableBytes() 方法如果检测到底层 ChannelBuffer 对象的空间不足,则会创建一个新的 ChannelBuffer空间扩大为原来的两倍然后将原来 ChannelBuffer 中的数据拷贝到新 ChannelBuffer 中,最后将 buffer 字段指向新 ChannelBuffer 对象完成整个扩容操作。ensureWritableBytes() 方法的具体实现如下:
public void ensureWritableBytes(int minWritableBytes) {
if (minWritableBytes <= writableBytes()) {
return;
}
int newCapacity;
if (capacity() == 0) {
newCapacity = 1;
} else {
newCapacity = capacity();
}
int minNewCapacity = writerIndex() + minWritableBytes;
while (newCapacity < minNewCapacity) {
newCapacity <<= 1;
}
ChannelBuffer newBuffer = factory().getBuffer(newCapacity);
newBuffer.writeBytes(buffer, 0, writerIndex());
buffer = newBuffer;
}
ByteBufferBackedChannelBuffer 是基于 Java NIO ByteBuffer ChannelBuffer 实现其中的方法基本都是通过组合 ByteBuffer API 实现的下面以 getBytes() 方法和 setBytes() 方法的一个重载为例进行分析
public void getBytes(int index, byte[] dst, int dstIndex, int length) {
ByteBuffer data = buffer.duplicate();
try {
// 移动ByteBuffer中的指针
data.limit(index + length).position(index);
} catch (IllegalArgumentException e) {
throw new IndexOutOfBoundsException();
}
// 通过ByteBuffer的get()方法实现读取
data.get(dst, dstIndex, length);
}
public void setBytes(int index, byte[] src, int srcIndex, int length) {
ByteBuffer data = buffer.duplicate();
// 移动ByteBuffer中的指针
data.limit(index + length).position(index);
// 将数据写入底层的ByteBuffer中
data.put(src, srcIndex, length);
}
ByteBufferBackedChannelBuffer 的其他方法实现比较简单这里就不再展示你若感兴趣的话可以参考源码进行学习
NettyBackedChannelBuffer 是基于 Netty ByteBuf ChannelBuffer 实现Netty 中的 ByteBuf 内部维护了 readerIndex writerIndex 以及 markedReaderIndexmarkedWriterIndex 这四个索引所以 NettyBackedChannelBuffer 没有再继承 AbstractChannelBuffer 抽象类而是直接实现了 ChannelBuffer 接口
NettyBackedChannelBuffer ChannelBuffer 接口的实现都是调用底层封装的 Netty ByteBuf 实现的这里就不再展开介绍你若感兴趣的话也可以参考相关代码进行学习
相关 Stream 以及门面类
ChannelBuffer 基础上Dubbo 提供了一套输入输出流如下图所示
ChannelBufferInputStream 底层封装了一个 ChannelBuffer其实现 InputStream 接口的 read*() 方法全部都是从 ChannelBuffer 中读取数据ChannelBufferInputStream 中还维护了一个 startIndex 和一个endIndex 索引作为读取数据的起止位置ChannelBufferOutputStream ChannelBufferInputStream 类似会向底层的 ChannelBuffer 写入数据这里就不再展开你若感兴趣的话可以参考源码进行分析
最后要介绍 ChannelBuffers 这个门面类下图展示了 ChannelBuffers 这个门面类的所有方法
对这些方法进行分类可归纳出如下这些方法
dynamicBuffer() 方法创建 DynamicChannelBuffer 对象初始化大小由第一个参数指定默认为 256
buffer() 方法创建指定大小的 HeapChannelBuffer 对象
wrappedBuffer() 方法将传入的 byte[] 数字封装成 HeapChannelBuffer 对象
directBuffer() 方法创建 ByteBufferBackedChannelBuffer 对象需要注意的是底层的 ByteBuffer 使用的堆外内存需要特别关注堆外内存的管理
equals() 方法用于比较两个 ChannelBuffer 是否相同其中会逐个比较两个 ChannelBuffer 中的前 7 个可读字节只有两者完全一致才算两个 ChannelBuffer 相同其核心实现如下示例代码
public static boolean equals(ChannelBuffer bufferA, ChannelBuffer bufferB) {
final int aLen = bufferA.readableBytes();
if (aLen != bufferB.readableBytes()) {
return false; // 比较两个ChannelBuffer的可读字节数
}
final int byteCount = aLen & 7; // 只比较前7个字节
int aIndex = bufferA.readerIndex();
int bIndex = bufferB.readerIndex();
for (int i = byteCount; i > 0; i--) {
if (bufferA.getByte(aIndex) != bufferB.getByte(bIndex)) {
return false; // 前7个字节发现不同则返回false
}
aIndex++;
bIndex++;
}
return true;
}
compare() 方法:用于比较两个 ChannelBuffer 的大小,会逐个比较两个 ChannelBuffer 中的全部可读字节,具体实现与 equals() 方法类似,这里就不再重复讲述。
总结
本课时重点介绍了 dubbo-remoting 模块 buffers 包中的核心实现。我们首先介绍了 ChannelBuffer 接口这一个顶层接口,了解了 ChannelBuffer 提供的核心功能和运作原理;接下来介绍了 ChannelBuffer 的多种实现,其中包括 HeapChannelBuffer、DynamicChannelBuffer、ByteBufferBackedChannelBuffer 等具体实现类,以及 AbstractChannelBuffer 这个抽象类;最后分析了 ChannelBufferFactory 使用到的 ChannelBuffers 工具类以及在 ChannelBuffer 之上封装的 InputStream 和 OutputStream 实现。
关于本课时,你若还有什么疑问或想法,欢迎你留言跟我分享。

View File

@@ -0,0 +1,567 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
19 Transporter 层核心实现:编解码与线程模型一文打尽(上)
在第 17 课时中,我们详细介绍了 dubbo-remoting-api 模块中 Transporter 相关的核心抽象接口,本课时将继续介绍 dubbo-remoting-api 模块的其他内容。这里我们依旧从 Transporter 层的 RemotingServer、Client、Channel、ChannelHandler 等核心接口出发,介绍这些核心接口的实现。
AbstractPeer 抽象类
首先,我们来看 AbstractPeer 这个抽象类,它同时实现了 Endpoint 接口和 ChannelHandler 接口,如下图所示,它也是 AbstractChannel、AbstractEndpoint 抽象类的父类。
AbstractPeer 继承关系
Netty 中也有 ChannelHandler、Channel 等接口,但无特殊说明的情况下,这里的接口指的都是 Dubbo 中定义的接口。如果涉及 Netty 中的接口,会进行特殊说明。
AbstractPeer 中有四个字段:一个是表示该端点自身的 URL 类型的字段,还有两个 Boolean 类型的字段closing 和 closed用来记录当前端点的状态这三个字段都与 Endpoint 接口相关;第四个字段指向了一个 ChannelHandler 对象AbstractPeer 对 ChannelHandler 接口的所有实现,都是委托给了这个 ChannelHandler 对象。从上面的继承关系图中我们可以得出这样一个结论AbstractChannel、AbstractServer、AbstractClient 都是要关联一个 ChannelHandler 对象的。
AbstractEndpoint 抽象类
我们顺着上图的继承关系继续向下看AbstractEndpoint 继承了 AbstractPeer 这个抽象类。AbstractEndpoint 中维护了一个 Codec2 对象codec 字段和两个超时时间timeout 字段和 connectTimeout 字段),在 AbstractEndpoint 的构造方法中会根据传入的 URL 初始化这三个字段:
public AbstractEndpoint(URL url, ChannelHandler handler) {
super(url, handler); // 调用父类AbstractPeer的构造方法
// 根据URL中的codec参数值确定此处具体的Codec2实现类
this.codec = getChannelCodec(url);
// 根据URL中的timeout参数确定timeout字段的值默认1000
this.timeout = url.getPositiveParameter(TIMEOUT_KEY,
DEFAULT_TIMEOUT);
// 根据URL中的connect.timeout参数确定connectTimeout字段的值默认3000
this.connectTimeout = url.getPositiveParameter(
Constants.CONNECT_TIMEOUT_KEY, Constants.DEFAULT_CONNECT_TIMEOUT);
}
在[第 17 课时]介绍 Codec2 接口的时候提到它是一个 SPI 扩展点,这里的 AbstractEndpoint.getChannelCodec() 方法就是基于 Dubbo SPI 选择其扩展实现的,具体实现如下:
protected static Codec2 getChannelCodec(URL url) {
// 根据URL的codec参数获取扩展名
String codecName = url.getParameter(Constants.CODEC_KEY, "telnet");
if (ExtensionLoader.getExtensionLoader(Codec2.class).hasExtension(codecName)) { // 通过ExtensionLoader加载并实例化Codec2的具体扩展实现
return ExtensionLoader.getExtensionLoader(Codec2.class).getExtension(codecName);
} else { // Codec2接口不存在相应的扩展名就尝试从Codec这个老接口的扩展名中查找目前Codec接口已经废弃了所以省略这部分逻辑
}
}
另外AbstractEndpoint 还实现了 Resetable 接口(只有一个 reset() 方法需要实现),虽然 AbstractEndpoint 中的 reset() 方法比较长,但是逻辑非常简单,就是根据传入的 URL 参数重置 AbstractEndpoint 的三个字段。下面是重置 codec 字段的代码片段,还是调用 getChannelCodec() 方法实现的:
public void reset(URL url) {
// 检测当前AbstractEndpoint是否已经关闭(略)
// 省略重置timeout、connectTimeout两个字段的逻辑
try {
if (url.hasParameter(Constants.CODEC_KEY)) {
this.codec = getChannelCodec(url);
}
} catch (Throwable t) {
logger.error(t.getMessage(), t);
}
}
Server 继承路线分析
AbstractServer 和 AbstractClient 都实现了 AbstractEndpoint 抽象类,我们先来看 AbstractServer 的实现。AbstractServer 在继承了 AbstractEndpoint 的同时,还实现了 RemotingServer 接口,如下图所示:
AbstractServer 继承关系图
AbstractServer 是对服务端的抽象实现了服务端的公共逻辑。AbstractServer 的核心字段有下面几个。
localAddress、bindAddressInetSocketAddress 类型):分别对应该 Server 的本地地址和绑定的地址,都是从 URL 中的参数中获取。bindAddress 默认值与 localAddress 一致。
acceptsint 类型):该 Server 能接收的最大连接数,从 URL 的 accepts 参数中获取,默认值为 0表示没有限制。
executorRepositoryExecutorRepository 类型):负责管理线程池,后面我们会深入介绍 ExecutorRepository 的具体实现。
executorExecutorService 类型):当前 Server 关联的线程池,由上面的 ExecutorRepository 创建并管理。
在 AbstractServer 的构造方法中会根据传入的 URL初始化上述字段并调用 doOpen() 这个抽象方法完成该 Server 的启动,具体实现如下:
public AbstractServer(URL url, ChannelHandler handler) {
super(url, handler); // 调用父类的构造方法
// 根据传入的URL初始化localAddress和bindAddress
localAddress = getUrl().toInetSocketAddress();
String bindIp = getUrl().getParameter(Constants.BIND_IP_KEY, getUrl().getHost());
int bindPort = getUrl().getParameter(Constants.BIND_PORT_KEY, getUrl().getPort());
if (url.getParameter(ANYHOST_KEY, false) || NetUtils.isInvalidLocalHost(bindIp)) {
bindIp = ANYHOST_VALUE;
}
bindAddress = new InetSocketAddress(bindIp, bindPort);
// 初始化accepts等字段
this.accepts = url.getParameter(ACCEPTS_KEY, DEFAULT_ACCEPTS);
this.idleTimeout = url.getParameter(IDLE_TIMEOUT_KEY, DEFAULT_IDLE_TIMEOUT);
try {
doOpen(); // 调用doOpen()这个抽象方法启动该Server
} catch (Throwable t) {
throw new RemotingException("...");
}
// 获取该Server关联的线程池
executor = executorRepository.createExecutorIfAbsent(url);
}
ExecutorRepository
在继续分析 AbstractServer 的具体实现类之前,我们先来了解一下 ExecutorRepository 这个接口。
ExecutorRepository 负责创建并管理 Dubbo 中的线程池,该接口虽然是个 SPI 扩展点,但是只有一个默认实现—— DefaultExecutorRepository。在该默认实现中维护了一个 ConcurrentMap> 集合data 字段)缓存已有的线程池,第一层 Key 值表示线程池属于 Provider 端还是 Consumer 端,第二层 Key 值表示线程池关联服务的端口。
DefaultExecutorRepository.createExecutorIfAbsent() 方法会根据 URL 参数创建相应的线程池并缓存在合适的位置,具体实现如下:
public synchronized ExecutorService createExecutorIfAbsent(URL url) {
// 根据URL中的side参数值决定第一层key
String componentKey = EXECUTOR_SERVICE_COMPONENT_KEY;
if (CONSUMER_SIDE.equalsIgnoreCase(url.getParameter(SIDE_KEY))) {
componentKey = CONSUMER_SIDE;
}
Map<Integer, ExecutorService> executors = data.computeIfAbsent(componentKey, k -> new ConcurrentHashMap<>());
// 根据URL中的port值确定第二层key
Integer portKey = url.getPort();
ExecutorService executor = executors.computeIfAbsent(portKey, k -> createExecutor(url));
// 如果缓存中相应的线程池已关闭则同样需要调用createExecutor()方法
// 创建新的线程池,并替换掉缓存中已关闭的线程持,这里省略这段逻辑
return executor;
}
在 createExecutor() 方法中,会通过 Dubbo SPI 查找 ThreadPool 接口的扩展实现,并调用其 getExecutor() 方法创建线程池。ThreadPool 接口被 @SPI 注解修饰,默认使用 FixedThreadPool 实现,但是 ThreadPool 接口中的 getExecutor() 方法被 @Adaptive 注解修饰,动态生成的适配器类会优先根据 URL 中的 threadpool 参数选择 ThreadPool 的扩展实现。ThreadPool 接口的实现类如下图所示:
ThreadPool 继承关系图
不同实现会根据 URL 参数创建不同特性的线程池这里以CacheThreadPool为例进行分析
public Executor getExecutor(URL url) {
String name = url.getParameter(THREAD_NAME_KEY, DEFAULT_THREAD_NAME);
// 核心线程数量
int cores = url.getParameter(CORE_THREADS_KEY, DEFAULT_CORE_THREADS);
// 最大线程数量
int threads = url.getParameter(THREADS_KEY, Integer.MAX_VALUE);
// 缓冲队列的最大长度
int queues = url.getParameter(QUEUES_KEY, DEFAULT_QUEUES);
// 非核心线程的最大空闲时长,当非核心线程空闲时间超过该值时,会被回收
int alive = url.getParameter(ALIVE_KEY, DEFAULT_ALIVE);
// 下面就是依赖JDK的ThreadPoolExecutor创建指定特性的线程池并返回
return new ThreadPoolExecutor(cores, threads, alive, TimeUnit.MILLISECONDS,
queues == 0 ? new SynchronousQueue<Runnable>() :
(queues < 0 ? new LinkedBlockingQueue<Runnable>()
: new LinkedBlockingQueue<Runnable>(queues)),
new NamedInternalThreadFactory(name, true), new AbortPolicyWithReport(name, url));
}
再简单说一下其他 ThreadPool 实现创建的线程池。
LimitedThreadPool与 CacheThreadPool 一样可以指定核心线程数、最大线程数以及缓冲队列长度。区别在于LimitedThreadPool 创建的线程池的非核心线程不会被回收。
FixedThreadPool核心线程数和最大线程数一致且不会被回收。
上述三种类型的线程池都是基于 JDK ThreadPoolExecutor 线程池,在核心线程全部被占用的时候,会优先将任务放到缓冲队列中缓存,在缓冲队列满了之后,才会尝试创建新线程来处理任务。
EagerThreadPool 创建的线程池是 EagerThreadPoolExecutor继承了 JDK 提供的 ThreadPoolExecutor使用的队列是 TaskQueue继承了LinkedBlockingQueue。该线程池与 ThreadPoolExecutor 不同的是在线程数没有达到最大线程数的前提下EagerThreadPoolExecutor 会优先创建线程来执行任务而不是放到缓冲队列中当线程数达到最大值时EagerThreadPoolExecutor 会将任务放入缓冲队列,等待空闲线程。
EagerThreadPoolExecutor 覆盖了 ThreadPoolExecutor 中的两个方法execute() 方法和 afterExecute() 方法,具体实现如下,我们可以看到其中维护了一个 submittedTaskCount 字段AtomicInteger 类型),用来记录当前在线程池中的任务总数(正在线程中执行的任务数+队列中等待的任务数)。
public void execute(Runnable command) {
// 任务提交之前递增submittedTaskCount
submittedTaskCount.incrementAndGet();
try {
super.execute(command); // 提交任务
} catch (RejectedExecutionException rx) {
final TaskQueue queue = (TaskQueue) super.getQueue();
try {
// 任务被拒绝之后,会尝试再次放入队列中缓存,等待空闲线程执行
if (!queue.retryOffer(command, 0, TimeUnit.MILLISECONDS)) {
// 再次入队被拒绝,则队列已满,无法执行任务
// 递减submittedTaskCount
submittedTaskCount.decrementAndGet();
throw new RejectedExecutionException("Queue capacity is full.", rx);
}
} catch (InterruptedException x) {
// 再次入队列异常递减submittedTaskCount
submittedTaskCount.decrementAndGet();
throw new RejectedExecutionException(x);
}
} catch (Throwable t) { // 任务提交异常递减submittedTaskCount
submittedTaskCount.decrementAndGet();
throw t;
}
}
protected void afterExecute(Runnable r, Throwable t) {
// 任务指定结束递减submittedTaskCount
submittedTaskCount.decrementAndGet();
}
看到这里,你可能会有些疑惑:没有看到优先创建线程执行任务的逻辑啊。其实重点在关联的 TaskQueue 实现中,它覆盖了 LinkedBlockingQueue.offer() 方法,会判断线程池的 submittedTaskCount 值是否已经达到最大线程数,如果未超过,则会返回 false迫使线程池创建新线程来执行任务。示例代码如下
public boolean offer(Runnable runnable) {
// 获取当前线程池中的活跃线程数
int currentPoolThreadSize = executor.getPoolSize();
// 当前有线程空闲,直接将任务提交到队列中,空闲线程会直接从中获取任务执行
if (executor.getSubmittedTaskCount() < currentPoolThreadSize) {
return super.offer(runnable);
}
// 当前没有空闲线程但是还可以创建新线程则返回false迫使线程池创建
// 新线程来执行任务
if (currentPoolThreadSize < executor.getMaximumPoolSize()) {
return false;
}
// 当前线程数已经达到上限只能放到队列中缓存了
return super.offer(runnable);
}
线程池最后一个相关的小细节是 AbortPolicyWithReport 它继承了 ThreadPoolExecutor.AbortPolicy覆盖的 rejectedExecution 方法中会输出包含线程池相关信息的 WARN 级别日志然后进行 dumpJStack() 方法最后才会抛出RejectedExecutionException 异常
我们回到 Server 的继承线上下面来看基于 Netty 4 实现的 NettyServer它继承了前文介绍的 AbstractServer实现了 doOpen() 方法和 doClose() 方法这里重点看 doOpen() 方法如下所示
protected void doOpen() throws Throwable {
// 创建ServerBootstrap
bootstrap = new ServerBootstrap();
// 创建boss EventLoopGroup
bossGroup = NettyEventLoopFactory.eventLoopGroup(1, "NettyServerBoss");
// 创建worker EventLoopGroup
workerGroup = NettyEventLoopFactory.eventLoopGroup(
getUrl().getPositiveParameter(IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS),
"NettyServerWorker");
// 创建NettyServerHandler它是一个Netty中的ChannelHandler实现
// 不是Dubbo Remoting层的ChannelHandler接口的实现
final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);
// 获取当前NettyServer创建的所有Channel这里的channels集合中的
// Channel不是Netty中的Channel对象而是Dubbo Remoting层的Channel对象
channels = nettyServerHandler.getChannels();
// 初始化ServerBootstrap指定boss和worker EventLoopGroup
bootstrap.group(bossGroup, workerGroup)
.channel(NettyEventLoopFactory.serverSocketChannelClass())
.option(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
.childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 连接空闲超时时间
int idleTimeout = UrlUtils.getIdleTimeout(getUrl());
// NettyCodecAdapter中会创建Decoder和Encoder
NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
ch.pipeline()
// 注册Decoder和Encoder
.addLast("decoder", adapter.getDecoder())
.addLast("encoder", adapter.getEncoder())
// 注册IdleStateHandler
.addLast("server-idle-handler", new IdleStateHandler(0, 0, idleTimeout, MILLISECONDS))
// 注册NettyServerHandler
.addLast("handler", nettyServerHandler);
}
});
// 绑定指定的地址和端口
ChannelFuture channelFuture = bootstrap.bind(getBindAddress());
channelFuture.syncUninterruptibly(); // 等待bind操作完成
channel = channelFuture.channel();
}
看完 NettyServer 实现的 doOpen() 方法之后,你会发现它和简易版 RPC 框架中启动一个 Netty 的 Server 端基本流程类似:初始化 ServerBootstrap、创建 Boss EventLoopGroup 和 Worker EventLoopGroup、创建 ChannelInitializer 指定如何初始化 Channel 上的 ChannelHandler 等一系列 Netty 使用的标准化流程。
其实在 Transporter 这一层看,功能的不同其实就是注册在 Channel 上的 ChannelHandler 不同,通过 doOpen() 方法得到的 Server 端结构如下:
NettyServer 模型
核心 ChannelHandler
下面我们来逐个看看这四个 ChannelHandler 的核心功能。
首先是decoder 和 encoder它们都是 NettyCodecAdapter 的内部类,如下图所示,分别继承了 Netty 中的 ByteToMessageDecoder 和 MessageToByteEncoder
还记得 AbstractEndpoint 抽象类中的 codec 字段Codec2 类型InternalDecoder 和 InternalEncoder 会将真正的编解码功能委托给 NettyServer 关联的这个 Codec2 对象去处理,这里以 InternalDecoder 为例进行分析:
private class InternalDecoder extends ByteToMessageDecoder {
protected void decode(ChannelHandlerContext ctx, ByteBuf input, List<Object> out) throws Exception {
// 将ByteBuf封装成统一的ChannelBuffer
ChannelBuffer message = new NettyBackedChannelBuffer(input);
// 拿到关联的Channel
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
do {
// 记录当前readerIndex的位置
int saveReaderIndex = message.readerIndex();
// 委托给Codec2进行解码
Object msg = codec.decode(channel, message);
// 当前接收到的数据不足一个消息的长度会返回NEED_MORE_INPUT
// 这里会重置readerIndex继续等待接收更多的数据
if (msg == Codec2.DecodeResult.NEED_MORE_INPUT) {
message.readerIndex(saveReaderIndex);
break;
} else {
if (msg != null) { // 将读取到的消息传递给后面的Handler处理
out.add(msg);
}
}
} while (message.readable());
}
}
你是不是发现 InternalDecoder 的实现与我们简易版 RPC 的 Decoder 实现非常相似呢?
InternalEncoder 的具体实现就不再展开讲解了,你若感兴趣可以翻看源码进行研究和分析。
接下来是IdleStateHandler它是 Netty 提供的一个工具型 ChannelHandler用于定时心跳请求的功能或是自动关闭长时间空闲连接的功能。它的原理到底是怎样的呢在 IdleStateHandler 中通过 lastReadTime、lastWriteTime 等几个字段,记录了最近一次读/写事件的时间IdleStateHandler 初始化的时候,会创建一个定时任务,定时检测当前时间与最后一次读/写时间的差值。如果超过我们设置的阈值(也就是上面 NettyServer 中设置的 idleTimeout就会触发 IdleStateEvent 事件,并传递给后续的 ChannelHandler 进行处理。后续 ChannelHandler 的 userEventTriggered() 方法会根据接收到的 IdleStateEvent 事件,决定是关闭长时间空闲的连接,还是发送心跳探活。
最后来看NettyServerHandler它继承了 ChannelDuplexHandler这是 Netty 提供的一个同时处理 Inbound 数据和 Outbound 数据的 ChannelHandler从下面的继承图就能看出来。
NettyServerHandler 继承关系图
在 NettyServerHandler 中有 channels 和 handler 两个核心字段。
channelsMap集合记录了当前 Server 创建的所有 Channel从下图中可以看到连接创建触发 channelActive() 方法)、连接断开(触发 channelInactive()方法)会操作 channels 集合进行相应的增删。
handlerChannelHandler 类型NettyServerHandler 内几乎所有方法都会触发该 Dubbo ChannelHandler 对象(如下图)。
这里以 write() 方法为例进行简单分析:
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
super.write(ctx, msg, promise); // 将发送的数据继续向下传递
// 并不影响消息的继续发送只是触发sent()方法进行相关的处理,这也是方法
// 名称是动词过去式的原因,可以仔细体会一下。其他方法可能没有那么明显,
// 这里以write()方法为例进行说明
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
handler.sent(channel, msg);
}
在 NettyServer 创建 NettyServerHandler 的时候,可以看到下面的这行代码:
final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);
其中第二个参数传入的是 NettyServer 这个对象,你可以追溯一下 NettyServer 的继承结构,会发现它的最顶层父类 AbstractPeer 实现了 ChannelHandler并且将所有的方法委托给其中封装的 ChannelHandler 对象,如下图所示:
也就是说NettyServerHandler 会将数据委托给这个 ChannelHandler。
到此为止Server 这条继承线就介绍完了。你可以回顾一下,从 AbstractPeer 开始往下一路继承下来NettyServer 拥有了 Endpoint、ChannelHandler 以及RemotingServer多个接口的能力关联了一个 ChannelHandler 对象以及 Codec2 对象,并最终将数据委托给这两个对象进行处理。所以,上层调用方只需要实现 ChannelHandler 和 Codec2 这两个接口就可以了。
总结
本课时重点介绍了 Dubbo Transporter 层中 Server 相关的实现。
首先,我们介绍了 AbstractPeer 这个最顶层的抽象类,了解了 Server、Client 和 Channel 的公共属性。接下来,介绍了 AbstractEndpoint 抽象类,它提供了编解码等 Server 和 Client 所需的公共能力。最后,我们深入分析了 AbstractServer 抽象类以及基于 Netty 4 实现的 NettyServer同时还深入剖析了涉及的各种组件例如ExecutorRepository、NettyServerHandler 等。

View File

@@ -0,0 +1,571 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
20 Transporter 层核心实现:编解码与线程模型一文打尽(下)
在上一课时中,我们深入分析了 Transporter 层中 Server 相关的核心抽象类以及基于 Netty 4 的实现类。本课时我们继续分析 Transporter 层中剩余的核心接口实现,主要涉及 Client 接口、Channel 接口、ChannelHandler 接口,以及相关的关键组件。
Client 继承路线分析
在上一课时分析 AbstractEndpoint 的时候可以看到,除了 AbstractServer 这一条继承线之外,还有 AbstractClient 这条继承线它是对客户端的抽象。AbstractClient 中的核心字段有如下几个。
connectLockLock 类型):在 Client 底层进行连接、断开、重连等操作时,需要获取该锁进行同步。
needReconnectBoolean 类型):在发送数据之前,会检查 Client 底层的连接是否断开,如果断开了,则会根据 needReconnect 字段,决定是否重连。
executorExecutorService 类型):当前 Client 关联的线程池,线程池的具体内容在上一课时已经详细介绍过了,这里不再赘述。
在 AbstractClient 的构造方法中,会解析 URL 初始化 needReconnect 字段和 executor字段如下示例代码
public AbstractClient(URL url, ChannelHandler handler) throws RemotingException {
super(url, handler); // 调用父类的构造方法
// 解析URL初始化needReconnect值
needReconnect = url.getParameter("send.reconnect", false);
initExecutor(url); // 解析URL初始化executor
doOpen(); // 初始化底层的NIO库的相关组件
// 创建底层连接
connect(); // 省略异常处理的逻辑
}
与 AbstractServer 类似AbstractClient 定义了 doOpen()、doClose()、doConnect()和doDisConnect() 四个抽象方法给子类实现。
下面来看基于 Netty 4 实现的 NettyClient它继承了 AbstractClient 抽象类,实现了上述四个 do*() 抽象方法,我们这里重点关注 doOpen() 方法和 doConnect() 方法。在 NettyClient 的 doOpen() 方法中会通过 Bootstrap 构建客户端其中会完成连接超时时间、keepalive 等参数的设置,以及 ChannelHandler 的创建和注册,具体实现如下所示:
protected void doOpen() throws Throwable {
// 创建NettyClientHandler
final NettyClientHandler nettyClientHandler = new NettyClientHandler(getUrl(), this);
bootstrap = new Bootstrap(); // 创建Bootstrap
bootstrap.group(NIO_EVENT_LOOP_GROUP)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.channel(socketChannelClass());
// 设置连接超时时间这里使用到AbstractEndpoint中的connectTimeout字段
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, Math.max(3000, getConnectTimeout()));
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
// 心跳请求的时间间隔
int heartbeatInterval = UrlUtils.getHeartbeat(getUrl());
// 通过NettyCodecAdapter创建Netty中的编解码器这里不再重复介绍
NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyClient.this);
// 注册ChannelHandler
ch.pipeline().addLast("decoder", adapter.getDecoder())
.addLast("encoder", adapter.getEncoder())
.addLast("client-idle-handler", new IdleStateHandler(heartbeatInterval, 0, 0, MILLISECONDS))
.addLast("handler", nettyClientHandler);
// 如果需要Socks5Proxy需要添加Socks5ProxyHandler(略)
}
});
}
得到的 NettyClient 结构如下图所示:
NettyClient 结构图
NettyClientHandler 的实现方法与上一课时介绍的 NettyServerHandler 类似,同样是实现了 Netty 中的 ChannelDuplexHandler其中会将所有方法委托给 NettyClient 关联的 ChannelHandler 对象进行处理。两者在 userEventTriggered() 方法的实现上有所不同NettyServerHandler 在收到 IdleStateEvent 事件时会断开连接,而 NettyClientHandler 则会发送心跳消息,具体实现如下:
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
Request req = new Request();
req.setVersion(Version.getProtocolVersion());
req.setTwoWay(true);
req.setEvent(HEARTBEAT_EVENT); // 发送心跳请求
channel.send(req);
} else {
super.userEventTriggered(ctx, evt);
}
}
Channel 继承线分析
除了上一课时介绍的 AbstractEndpoint 之外AbstractChannel 也继承了 AbstractPeer 这个抽象类,同时还继承了 Channel 接口。AbstractChannel 实现非常简单,只是在 send() 方法中检测了底层连接的状态,没有实现具体的发送消息的逻辑。
这里我们依然以基于 Netty 4 的实现—— NettyChannel 为例,分析它对 AbstractChannel 的实现。NettyChannel 中的核心字段有如下几个。
channelChannel类型Netty 框架中的 Channel与当前的 Dubbo Channel 对象一一对应。
attributesMap类型当前 Channel 中附加属性,都会记录到该 Map 中。NettyChannel 中提供的 getAttribute()、hasAttribute()、setAttribute() 等方法,都是操作该集合。
activeAtomicBoolean用于标识当前 Channel 是否可用。
另外,在 NettyChannel 中还有一个静态的 Map 集合CHANNEL_MAP 字段),用来缓存当前 JVM 中 Netty 框架 Channel 与 Dubbo Channel 之间的映射关系。从下图的调用关系中可以看到NettyChannel 提供了读写 CHANNEL_MAP 集合的方法:
NettyChannel 中还有一个要介绍的是 send() 方法,它会通过底层关联的 Netty 框架 Channel将数据发送到对端。其中可以通过第二个参数指定是否等待发送操作结束具体实现如下
public void send(Object message, boolean sent) throws RemotingException {
// 调用AbstractChannel的send()方法检测连接是否可用
super.send(message, sent);
boolean success = true;
int timeout = 0;
// 依赖Netty框架的Channel发送数据
ChannelFuture future = channel.writeAndFlush(message);
if (sent) { // 等待发送结束,有超时时间
timeout = getUrl().getPositiveParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT);
success = future.await(timeout);
}
Throwable cause = future.cause();
if (cause != null) {
throw cause;
}
// 出现异常会调用removeChannelIfDisconnected()方法,在底层连接断开时,
// 会清理CHANNEL_MAP缓存(略)
}
ChannelHandler 继承线分析
前文介绍的 AbstractServer、AbstractClient 以及 Channel 实现,都是通过 AbstractPeer 实现了 ChannelHandler 接口,但只是做了一层简单的委托(也可以说成是装饰器),将全部方法委托给了其底层关联的 ChannelHandler 对象。
这里我们就深入分析 ChannelHandler 的其他实现类,涉及的实现类如下所示:
ChannelHandler 继承关系图
其中ChannelHandlerDispatcher在[第 17 课时]已经介绍过了,它负责将多个 ChannelHandler 对象聚合成一个 ChannelHandler 对象。
ChannelHandlerAdapter是 ChannelHandler 的一个空实现TelnetHandlerAdapter 继承了它并实现了 TelnetHandler 接口。至于Dubbo 对 Telnet 的支持,我们会在后面的课时中单独介绍,这里就先不展开分析了。
从名字上看ChannelHandlerDelegate接口是对另一个 ChannelHandler 对象的封装,它的两个实现类 AbstractChannelHandlerDelegate 和 WrappedChannelHandler 中也仅仅是封装了另一个 ChannelHandler 对象。
其中AbstractChannelHandlerDelegate有三个实现类都比较简单我们来逐个讲解。
MultiMessageHandler专门处理 MultiMessage 的 ChannelHandler 实现。MultiMessage 是 Exchange 层的一种消息类型,它其中封装了多个消息。在 MultiMessageHandler 收到 MultiMessage 消息的时候received() 方法会遍历其中的所有消息,并交给底层的 ChannelHandler 对象进行处理。
DecodeHandler专门处理 Decodeable 的 ChannelHandler 实现。实现了 Decodeable 接口的类都会提供了一个 decode() 方法实现对自身的解码DecodeHandler.received() 方法就是通过该方法得到解码后的消息,然后传递给底层的 ChannelHandler 对象继续处理。
HeartbeatHandler专门处理心跳消息的 ChannelHandler 实现。在 HeartbeatHandler.received() 方法接收心跳请求的时候,会生成相应的心跳响应并返回;在收到心跳响应的时候,会打印相应的日志;在收到其他类型的消息时,会传递给底层的 ChannelHandler 对象进行处理。下面是其核心实现:
public void received(Channel channel, Object message) throws RemotingException {
setReadTimestamp(channel); // 记录最近的读写事件时间戳
if (isHeartbeatRequest(message)) { // 收到心跳请求
Request req = (Request) message;
if (req.isTwoWay()) { // 返回心跳响应注意携带请求的ID
Response res = new Response(req.getId(), req.getVersion());
res.setEvent(HEARTBEAT_EVENT);
channel.send(res);
return;
}
if (isHeartbeatResponse(message)) { // 收到心跳响应
// 打印日志(略)
return;
}
handler.received(channel, message);
}
另外,我们可以看到,在 received() 和 send() 方法中HeartbeatHandler 会将最近一次的读写时间作为附加属性记录到 Channel 中。
通过上述介绍,我们发现 AbstractChannelHandlerDelegate 下的三个实现,其实都是在原有 ChannelHandler 的基础上添加了一些增强功能,这是典型的装饰器模式的应用。
Dispatcher 与 ChannelHandler
接下来,我们介绍 ChannelHandlerDelegate 接口的另一条继承线——WrappedChannelHandler其子类主要是决定了 Dubbo 以何种线程模型处理收到的事件和消息,就是所谓的“消息派发机制”,与前面介绍的 ThreadPool 有紧密的联系。
WrappedChannelHandler 继承关系图
从上图中我们可以看到,每个 WrappedChannelHandler 实现类的对象都由一个相应的 Dispatcher 实现类创建,下面是 Dispatcher 接口的定义:
@SPI(AllDispatcher.NAME) // 默认扩展名是all
public interface Dispatcher {
// 通过URL中的参数可以指定扩展名覆盖默认扩展名
@Adaptive({"dispatcher", "dispather", "channel.handler"})
ChannelHandler dispatch(ChannelHandler handler, URL url);
}
AllDispatcher 创建的是 AllChannelHandler 对象它会将所有网络事件以及消息交给关联的线程池进行处理。AllChannelHandler覆盖了 WrappedChannelHandler 中除了 sent() 方法之外的其他网络事件处理方法,将调用其底层的 ChannelHandler 的逻辑放到关联的线程池中执行。
我们先来看 connect() 方法其中会将CONNECTED 事件的处理封装成ChannelEventRunnable提交到线程池中执行具体实现如下
public void connected(Channel channel) throws RemotingException {
ExecutorService executor = getExecutorService(); // 获取公共线程池
// 将CONNECTED事件的处理封装成ChannelEventRunnable提交到线程池中执行
executor.execute(new ChannelEventRunnable(channel, handler, ChannelState.CONNECTED));
// 省略异常处理的逻辑
}
这里的 getExecutorService() 方法会按照当前端点Server/Client的 URL 从 ExecutorRepository 中获取相应的公共线程池。
disconnected()方法处理连接断开事件caught() 方法处理异常事件,它们也是按照上述方式实现的,这里不再展开赘述。
received() 方法会在当前端点收到数据的时候被调用,具体执行流程是先由 IO 线程(也就是 Netty 中的 EventLoopGroup从二进制流中解码出请求然后调用 AllChannelHandler 的 received() 方法,其中会将请求提交给线程池执行,执行完后调用 sent()方法向对端写回响应结果。received() 方法的具体实现如下:
public void received(Channel channel, Object message) throws RemotingException {
// 获取线程池
ExecutorService executor = getPreferredExecutorService(message);
try {
// 将消息封装成ChannelEventRunnable任务提交到线程池中执行
executor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));
} catch (Throwable t) {
// 如果线程池满了,请求会被拒绝,这里会根据请求配置决定是否返回一个说明性的响应
if(message instanceof Request && t instanceof RejectedExecutionException){
sendFeedback(channel, (Request) message, t);
return;
}
throw new ExecutionException("...");
}
}
getPreferredExecutorService() 方法对响应做了特殊处理:如果请求在发送的时候指定了关联的线程池,在收到对应的响应消息的时候,会优先根据请求的 ID 查找请求关联的线程池处理响应。
public ExecutorService getPreferredExecutorService(Object msg) {
if (msg instanceof Response) {
Response response = (Response) msg;
DefaultFuture responseFuture = DefaultFuture.getFuture(response.getId()); // 获取请求关联的DefaultFuture
if (responseFuture == null) {
return getSharedExecutorService();
} else { // 如果请求关联了线程池,则会获取相关的线程来处理响应
ExecutorService executor = responseFuture.getExecutor();
if (executor == null || executor.isShutdown()) {
executor = getSharedExecutorService();
}
return executor;
}
} else { // 如果是请求消息,则直接使用公共的线程池处理
return getSharedExecutorService();
}
}
这里涉及了 Request 和 Response 的概念,是 Exchange 层的概念,在后面会展开介绍,这里你只需要知道它们是不同的消息类型即可。
注意AllChannelHandler 并没有覆盖父类的 sent() 方法,也就是说,发送消息是直接在当前线程调用 sent() 方法完成的。
下面我们来看剩余的 WrappedChannelHandler 的实现。ExecutionChannelHandler由 ExecutionDispatcher 创建)只会将请求消息派发到线程池进行处理,也就是只重写了 received() 方法。对于响应消息以及其他网络事件例如连接建立事件、连接断开事件、心跳消息等ExecutionChannelHandler 会直接在 IO 线程中进行处理。
DirectChannelHandler 实现(由 DirectDispatcher 创建)会在 IO 线程中处理所有的消息和网络事件。
MessageOnlyChannelHandler 实现(由 MessageOnlyDispatcher 创建)会将所有收到的消息提交到线程池处理,其他网络事件则是由 IO 线程直接处理。
ConnectionOrderedChannelHandler 实现(由 ConnectionOrderedDispatcher 创建)会将收到的消息交给线程池进行处理,对于连接建立以及断开事件,会提交到一个独立的线程池并排队进行处理。在 ConnectionOrderedChannelHandler 的构造方法中,会初始化一个线程池,该线程池的队列长度是固定的:
public ConnectionOrderedChannelHandler(ChannelHandler handler, URL url) {
super(handler, url);
String threadName = url.getParameter(THREAD_NAME_KEY, DEFAULT_THREAD_NAME);
// 注意,该线程池只有一个线程,队列的长度也是固定的,
// 由URL中的connect.queue.capacity参数指定
connectionExecutor = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(url.getPositiveParameter(CONNECT_QUEUE_CAPACITY, Integer.MAX_VALUE)),
new NamedThreadFactory(threadName, true),
new AbortPolicyWithReport(threadName, url)
);
queuewarninglimit = url.getParameter(CONNECT_QUEUE_WARNING_SIZE, DEFAULT_CONNECT_QUEUE_WARNING_SIZE);
}
在 ConnectionOrderedChannelHandler 的 connected() 方法和 disconnected() 方法实现中,会将连接建立和断开事件交给上述 connectionExecutor 线程池排队处理。
在上面介绍 WrappedChannelHandler 各个实现的时候,我们会看到其中有针对 ThreadlessExecutor 这种线程池类型的特殊处理例如ExecutionChannelHandler.received() 方法中就有如下的分支逻辑:
public void received(Channel channel, Object message) throws RemotingException {
// 获取线程池(请求绑定的线程池或是公共线程池)
ExecutorService executor = getPreferredExecutorService(message);
if (message instanceof Request) { // 请求消息直接提交给线程池处理
executor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));
} else if (executor instanceof ThreadlessExecutor) {
// 针对ThreadlessExecutor这种线程池类型的特殊处理
executor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));
} else {
handler.received(channel, message);
}
}
ThreadlessExecutor 优化
ThreadlessExecutor 是一种特殊类型的线程池与其他正常的线程池最主要的区别是ThreadlessExecutor 内部不管理任何线程。
我们可以调用 ThreadlessExecutor 的execute() 方法,将任务提交给这个线程池,但是这些提交的任务不会被调度到任何线程执行,而是存储在阻塞队列中,只有当其他线程调用 ThreadlessExecutor.waitAndDrain() 方法时才会真正执行。也说就是,执行任务的与调用 waitAndDrain() 方法的是同一个线程。
那为什么会有 ThreadlessExecutor 这个实现呢?这主要是因为在 Dubbo 2.7.5 版本之前,在 WrappedChannelHandler 中会为每个连接启动一个线程池。
老版本中没有 ExecutorRepository 的概念,不会根据 URL 复用同一个线程池,而是通过 SPI 找到 ThreadPool 实现创建新线程池。
此时Dubbo Consumer 同步请求的线程模型如下图所示:
Dubbo Consumer 同步请求线程模型
从图中我们可以看到下面的请求-响应流程:
业务线程发出请求之后,拿到一个 Future 实例。
业务线程紧接着调用 Future.get() 阻塞等待请求结果返回。
当响应返回之后,交由连接关联的独立线程池进行反序列化等解析处理。
待处理完成之后,将业务结果通过 Future.set() 方法返回给业务线程。
在这个设计里面Consumer 端会维护一个线程池,而且线程池是按照连接隔离的,即每个连接独享一个线程池。这样,当面临需要消费大量服务且并发数比较大的场景时,例如,典型网关类场景,可能会导致 Consumer 端线程个数不断增加,导致线程调度消耗过多 CPU ,也可能因为线程创建过多而导致 OOM。
为了解决上述问题Dubbo 在 2.7.5 版本之后,引入了 ThreadlessExecutor将线程模型修改成了下图的样子
引入 ThreadlessExecutor 后的结构图
业务线程发出请求之后,拿到一个 Future 对象。
业务线程会调用 ThreadlessExecutor.waitAndDrain() 方法waitAndDrain() 方法会在阻塞队列上等待。
当收到响应时IO 线程会生成一个任务,填充到 ThreadlessExecutor 队列中,
业务线程会将上面添加的任务取出,并在本线程中执行。得到业务结果之后,调用 Future.set() 方法进行设置,此时 waitAndDrain() 方法返回。
业务线程从 Future 中拿到结果值。
了解了 ThreadlessExecutor 出现的缘由之后,接下来我们再深入了解一下 ThreadlessExecutor 的核心实现。首先是 ThreadlessExecutor 的核心字段,有如下几个。
queueLinkedBlockingQueue类型阻塞队列用来在 IO 线程和业务线程之间传递任务。
waiting、finishedBoolean类型ThreadlessExecutor 中的 waitAndDrain() 方法一般与一次 RPC 调用绑定,只会执行一次。当后续再次调用 waitAndDrain() 方法时,会检查 finished 字段若为true则此次调用直接返回。当后续再次调用 execute() 方法提交任务时,会根据 waiting 字段决定任务是放入 queue 队列等待业务线程执行,还是直接由 sharedExecutor 线程池执行。
sharedExecutorExecutorService类型ThreadlessExecutor 底层关联的共享线程池,当业务线程已经不再等待响应时,会由该共享线程执行提交的任务。
waitingFutureCompletableFuture类型指向请求对应的 DefaultFuture 对象,其具体实现我们会在后面的课时详细展开介绍。
ThreadlessExecutor 的核心逻辑在 execute() 方法和 waitAndDrain() 方法。execute() 方法相对简单,它会根据 waiting 状态决定任务提交到哪里,相关示例代码如下:
public void execute(Runnable runnable) {
synchronized (lock) {
if (!waiting) { // 判断业务线程是否还在等待响应结果
// 不等待,则直接交给共享线程池处理任务
sharedExecutor.execute(runnable);
} else {// 业务线程还在等待,则将任务写入队列,然后由业务线程自己执行
queue.add(runnable);
}
}
}
waitAndDrain() 方法中首先会检测 finished 字段值然后获取阻塞队列中的全部任务并执行执行完成之后会修改finished和 waiting 字段,标识当前 ThreadlessExecutor 已使用完毕,无业务线程等待。
public void waitAndDrain() throws InterruptedException {
if (finished) { // 检测当前ThreadlessExecutor状态
return;
}
// 获取阻塞队列中获取任务
Runnable runnable = queue.take();
synchronized (lock) {
waiting = false; // 修改waiting状态
runnable.run(); // 执行任务
}
runnable = queue.poll(); // 如果阻塞队列中还有其他任务,也需要一并执行
while (runnable != null) {
runnable.run(); // 省略异常处理逻辑
runnable = queue.poll();
}
finished = true; // 修改finished状态
}
到此为止Transporter 层对 ChannelHandler 的实现就介绍完了,其中涉及了多个 ChannelHandler 的装饰器,为了帮助你更好地理解,这里我们回到 NettyServer 中,看看它是如何对上层 ChannelHandler 进行封装的。
在 NettyServer 的构造方法中会调用 ChannelHandlers.wrap() 方法对传入的 ChannelHandler 对象进行修饰:
protected ChannelHandler wrapInternal(ChannelHandler handler, URL url) {
return new MultiMessageHandler(new HeartbeatHandler(ExtensionLoader.getExtensionLoader(Dispatcher.class)
.getAdaptiveExtension().dispatch(handler, url)));
}
结合前面的分析,我们可以得到下面这张图:
Server 端 ChannelHandler 结构图
我们可以在创建 NettyServerHandler 的地方添加断点 Debug 得到下图,也印证了上图的内容:
总结
本课时我们重点介绍了 Dubbo Transporter 层中 Client、 Channel、ChannelHandler 相关的实现以及优化。
首先我们介绍了 AbstractClient 抽象接口以及基于 Netty 4 的 NettyClient 实现。接下来,介绍了 AbstractChannel 抽象类以及 NettyChannel 实现。最后,我们深入分析了 ChannelHandler 接口实现,其中详细分析 WrappedChannelHandler 等关键 ChannelHandler 实现,以及 ThreadlessExecutor 优化。

View File

@@ -0,0 +1,417 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
21 Exchange 层剖析:彻底搞懂 Request-Response 模型(上)
在前面的课程中,我们深入介绍了 Dubbo Remoting 中的 Transport 层,了解了 Dubbo 抽象出来的端到端的统一传输层接口,并分析了以 Netty 为基础的相关实现。当然,其他 NIO 框架的接入也是类似的,本课程就不再展开赘述了。
在本课时中,我们将介绍 Transport 层的上一层,也是 Dubbo Remoting 层中的最顶层—— Exchange 层。Dubbo 将信息交换行为抽象成 Exchange 层,官方文档对这一层的说明是:封装了请求-响应的语义,即关注一问一答的交互模式,实现了同步转异步。在 Exchange 这一层,以 Request 和 Response 为中心,针对 Channel、ChannelHandler、Client、RemotingServer 等接口进行实现。
下面我们从 Request 和 Response 这一对基础类开始,依次介绍 Exchange 层中 ExchangeChannel、HeaderExchangeHandler 的核心实现。
Request 和 Response
Exchange 层的 Request 和 Response 这两个类是 Exchange 层的核心对象是对请求和响应的抽象。我们先来看Request 类的核心字段:
public class Request {
// 用于生成请求的自增ID当递增到Long.MAX_VALUE之后会溢出到Long.MIN_VALUE我们可以继续使用该负数作为消息ID
private static final AtomicLong INVOKE_ID = new AtomicLong(0);
private final long mId; // 请求的ID
private String mVersion; // 请求版本号
// 请求的双向标识如果该字段设置为true则Server端在收到请求后
// 需要给Client返回一个响应
private boolean mTwoWay = true;
// 事件标识,例如心跳请求、只读请求等,都会带有这个标识
private boolean mEvent = false;
// 请求发送到Server之后由Decoder将二进制数据解码成Request对象
// 如果解码环节遇到异常则会设置该标识然后交由其他ChannelHandler根据
// 该标识做进一步处理
private boolean mBroken = false;
// 请求体可以是任何Java类型的对象,也可以是null
private Object mData;
}
接下来是 Response 的核心字段:
public class Response {
// 响应ID与相应请求的ID一致
private long mId = 0;
// 当前协议的版本号,与请求消息的版本号一致
private String mVersion;
// 响应状态码有OK、CLIENT_TIMEOUT、SERVER_TIMEOUT等10多个可选值
private byte mStatus = OK;
private boolean mEvent = false;
private String mErrorMsg; // 可读的错误响应消息
private Object mResult; // 响应体
}
ExchangeChannel & DefaultFuture
在前面的课时中,我们介绍了 Channel 接口的功能以及 Transport 层对 Channel 接口的实现。在 Exchange 层中定义了 ExchangeChannel 接口,它在 Channel 接口之上抽象了 Exchange 层的网络连接。ExchangeChannel 接口的定义如下:
ExchangeChannel 接口
其中request() 方法负责发送请求,从图中可以看到这里有两个重载,其中一个重载可以指定请求的超时时间,返回值都是 Future 对象。
HeaderExchangeChannel 继承关系图
从上图中可以看出HeaderExchangeChannel 是 ExchangeChannel 的实现,它本身是 Channel 的装饰器,封装了一个 Channel 对象,其 send() 方法和 request() 方法的实现都是依赖底层修饰的这个 Channel 对象实现的。
public void send(Object message, boolean sent) throws RemotingException {
if (message instanceof Request || message instanceof Response
|| message instanceof String) {
channel.send(message, sent);
} else {
Request request = new Request();
request.setVersion(Version.getProtocolVersion());
request.setTwoWay(false);
request.setData(message);
channel.send(request, sent);
}
}
public CompletableFuture<Object> request(Object request, int timeout, ExecutorService executor) throws RemotingException {
Request req = new Request(); // 创建Request对象
req.setVersion(Version.getProtocolVersion());
req.setTwoWay(true);
req.setData(request);
DefaultFuture future = DefaultFuture.newFuture(channel,
req, timeout, executor); // 创建DefaultFuture
channel.send(req);
return future;
}
注意这里的 request() 方法,它返回的是一个 DefaultFuture 对象。通过前面课时的介绍我们知道io.netty.channel.Channel 的 send() 方法会返回一个 ChannelFuture 方法表示此次发送操作是否完成而这里的DefaultFuture 就表示此次请求-响应是否完成,也就是说,要收到响应为 Future 才算完成。
下面我们就来深入介绍一下请求发送过程中涉及的 DefaultFuture 以及HeaderExchangeChannel的内容。
首先来了解一下 DefaultFuture 的具体实现,它继承了 JDK 中的 CompletableFuture其中维护了两个 static 集合。
CHANNELSMap集合管理请求与 Channel 之间的关联关系,其中 Key 为请求 IDValue 为发送请求的 Channel。
FUTURESMap集合管理请求与 DefaultFuture 之间的关联关系,其中 Key 为请求 IDValue 为请求对应的 Future。
DefaultFuture 中核心的实例字段包括如下几个。
requestRequest 类型)和 idLong 类型):对应请求以及请求的 ID。
channelChannel 类型):发送请求的 Channel。
timeoutint 类型):整个请求-响应交互完成的超时时间。
startlong 类型):该 DefaultFuture 的创建时间。
sentvolatile long 类型):请求发送的时间。
timeoutCheckTaskTimeout 类型):该定时任务到期时,表示对端响应超时。
executorExecutorService 类型):请求关联的线程池。
DefaultFuture.newFuture() 方法创建 DefaultFuture 对象时,需要先初始化上述字段,并创建请求相应的超时定时任务:
public static DefaultFuture newFuture(Channel channel, Request request, int timeout, ExecutorService executor) {
// 创建DefaultFuture对象并初始化其中的核心字段
final DefaultFuture future = new DefaultFuture(channel, request, timeout);
future.setExecutor(executor);
// 对于ThreadlessExecutor的特殊处理ThreadlessExecutor可以关联一个waitingFuture就是这里创建DefaultFuture对象
if (executor instanceof ThreadlessExecutor) {
((ThreadlessExecutor) executor).setWaitingFuture(future);
}
// 创建一个定时任务,用处理响应超时的情况
timeoutCheck(future);
return future;
}
在 HeaderExchangeChannel.request() 方法中完成 DefaultFuture 对象的创建之后,会将请求通过底层的 Dubbo Channel 发送出去,发送过程中会触发沿途 ChannelHandler 的 sent() 方法,其中的 HeaderExchangeHandler 会调用 DefaultFuture.sent() 方法更新 sent 字段,记录请求发送的时间戳。后续如果响应超时,则会将该发送时间戳添加到提示信息中。
过一段时间之后Consumer 会收到对端返回的响应,在读取到完整响应之后,会触发 Dubbo Channel 中各个 ChannelHandler 的 received() 方法,其中就包括上一课时介绍的 WrappedChannelHandler。例如AllChannelHandler 子类会将后续 ChannelHandler.received() 方法的调用封装成任务提交到线程池中,响应会提交到 DefaultFuture 关联的线程池中,如上一课时介绍的 ThreadlessExecutor然后由业务线程继续后续的 ChannelHandler 调用。(你也可以回顾一下上一课时对 Transport 层 Dispatcher 以及 ThreadlessExecutor 的介绍。)
当响应传递到 HeaderExchangeHandler 的时候,会通过调用 handleResponse() 方法进行处理,其中调用了 DefaultFuture.received() 方法,该方法会找到响应关联的 DefaultFuture 对象(根据请求 ID 从 FUTURES 集合查找)并调用 doReceived() 方法,将 DefaultFuture 设置为完成状态。
public static void received(Channel channel, Response response, boolean timeout) { // 省略try/finally代码块
// 清理FUTURES中记录的请求ID与DefaultFuture之间的映射关系
DefaultFuture future = FUTURES.remove(response.getId());
if (future != null) {
Timeout t = future.timeoutCheckTask;
if (!timeout) { // 未超时,取消定时任务
t.cancel();
}
future.doReceived(response); // 调用doReceived()方法
}else{ // 查找不到关联的DefaultFuture会打印日志(略)}
// 清理CHANNELS中记录的请求ID与Channel之间的映射关系
CHANNELS.remove(response.getId());
}
// DefaultFuture.doReceived()方法的代码片段
private void doReceived(Response res) {
if (res == null) {
throw new IllegalStateException("response cannot be null");
}
if (res.getStatus() == Response.OK) { // 正常响应
this.complete(res.getResult());
} else if (res.getStatus() == Response.CLIENT_TIMEOUT || res.getStatus() == Response.SERVER_TIMEOUT) { // 超时
this.completeExceptionally(new TimeoutException(res.getStatus() == Response.SERVER_TIMEOUT, channel, res.getErrorMessage()));
} else { // 其他异常
this.completeExceptionally(new RemotingException(channel, res.getErrorMessage()));
}
// 下面是针对ThreadlessExecutor的兜底处理主要是防止业务线程一直阻塞在ThreadlessExecutor上
if (executor != null && executor instanceof ThreadlessExecutor) {
ThreadlessExecutor threadlessExecutor = (ThreadlessExecutor) executor;
if (threadlessExecutor.isWaiting()) {
// notifyReturn()方法会向ThreadlessExecutor提交一个任务这样业务线程就不会阻塞了提交的任务会尝试将DefaultFuture设置为异常结束
threadlessExecutor.notifyReturn(new IllegalStateException("The result has returned..."));
}
}
}
下面我们再来看看响应超时的场景。在创建 DefaultFuture 时调用的 timeoutCheck() 方法中,会创建 TimeoutCheckTask 定时任务,并添加到时间轮中,具体实现如下:
private static void timeoutCheck(DefaultFuture future) {
TimeoutCheckTask task = new TimeoutCheckTask(future.getId());
future.timeoutCheckTask = TIME_OUT_TIMER.newTimeout(task, future.getTimeout(), TimeUnit.MILLISECONDS);
}
TIME_OUT_TIMER 是一个 HashedWheelTimer 对象,即 Dubbo 中对时间轮的实现,这是一个 static 字段,所有 DefaultFuture 对象共用一个。
TimeoutCheckTask 是 DefaultFuture 中的内部类,实现了 TimerTask 接口可以提交到时间轮中等待执行。当响应超时的时候TimeoutCheckTask 会创建一个 Response并调用前面介绍的 DefaultFuture.received() 方法。示例代码如下:
public void run(Timeout timeout) {
// 检查该任务关联的DefaultFuture对象是否已经完成
if (future.getExecutor() != null) { // 提交到线程池执行注意ThreadlessExecutor的情况
future.getExecutor().execute(() -> notifyTimeout(future));
} else {
notifyTimeout(future);
}
}
private void notifyTimeout(DefaultFuture future) {
// 没有收到对端的响应这里会创建一个Response表示超时的响应
Response timeoutResponse = new Response(future.getId());
timeoutResponse.setStatus(future.isSent() ? Response.SERVER_TIMEOUT : Response.CLIENT_TIMEOUT);
timeoutResponse.setErrorMessage(future.getTimeoutMessage(true));
// 将关联的DefaultFuture标记为超时异常完成
DefaultFuture.received(future.getChannel(), timeoutResponse, true);
}
HeaderExchangeHandler
在前面介绍 DefaultFuture 时,我们简单说明了请求-响应的流程,其实无论是发送请求还是处理响应,都会涉及 HeaderExchangeHandler所以这里我们就来介绍一下 HeaderExchangeHandler 的内容。
HeaderExchangeHandler 是 ExchangeHandler 的装饰器,其中维护了一个 ExchangeHandler 对象ExchangeHandler 接口是 Exchange 层与上层交互的接口之一,上层调用方可以实现该接口完成自身的功能;然后再由 HeaderExchangeHandler 修饰,具备 Exchange 层处理 Request-Response 的能力;最后再由 Transport ChannelHandler 修饰,具备 Transport 层的能力。如下图所示:
ChannelHandler 继承关系总览图
HeaderExchangeHandler 作为一个装饰器,其 connected()、disconnected()、sent()、received()、caught() 方法最终都会转发给上层提供的 ExchangeHandler 进行处理。这里我们需要聚焦的是 HeaderExchangeHandler 本身对 Request 和 Response 的处理逻辑。
received() 方法处理的消息分类
结合上图我们可以看到在received() 方法中,对收到的消息进行了分类处理。
只读请求会由handlerEvent() 方法进行处理,它会在 Channel 上设置 channel.readonly 标志,后续介绍的上层调用中会读取该值。
void handlerEvent(Channel channel, Request req) throws RemotingException {
if (req.getData() != null && req.getData().equals(READONLY_EVENT)) {
channel.setAttribute(Constants.CHANNEL_ATTRIBUTE_READONLY_KEY, Boolean.TRUE);
}
}
双向请求由handleRequest() 方法进行处理,会先对解码失败的请求进行处理,返回异常响应;然后将正常解码的请求交给上层实现的 ExchangeHandler 进行处理,并添加回调。上层 ExchangeHandler 处理完请求后,会触发回调,根据处理结果填充响应结果和响应码,并向对端发送。
void handleRequest(final ExchangeChannel channel, Request req) throws RemotingException {
Response res = new Response(req.getId(), req.getVersion());
if (req.isBroken()) { // 请求解码失败
Object data = req.getData();
// 设置异常信息和响应码
res.setErrorMessage("Fail to decode request due to: " + msg);
res.setStatus(Response.BAD_REQUEST);
channel.send(res); // 将异常响应返回给对端
return;
}
Object msg = req.getData();
// 交给上层实现的ExchangeHandler进行处理
CompletionStage<Object> future = handler.reply(channel, msg);
future.whenComplete((appResult, t) -> { // 处理结束后的回调
if (t == null) { // 返回正常响应
res.setStatus(Response.OK);
res.setResult(appResult);
} else { // 处理过程发生异常,设置异常信息和错误码
res.setStatus(Response.SERVICE_ERROR);
res.setErrorMessage(StringUtils.toString(t));
}
channel.send(res); // 发送响应
});
}
单向请求直接委托给上层 ExchangeHandler 实现的 received() 方法进行处理由于不需要响应HeaderExchangeHandler 不会关注处理结果。
对于 Response 的处理前文已提到了HeaderExchangeHandler 会通过handleResponse() 方法将关联的 DefaultFuture 设置为完成状态(或是异常完成状态),具体内容这里不再展开讲述。
对于 String 类型的消息HeaderExchangeHandler 会根据当前服务的角色进行分类,具体与 Dubbo 对 telnet 的支持相关,后面的课时会详细介绍,这里就不展开分析了。
接下来我们再来看sent() 方法,该方法会通知上层 ExchangeHandler 实现的 sent() 方法,同时还会针对 Request 请求调用 DefaultFuture.sent() 方法记录请求的具体发送时间,该逻辑在前文也已经介绍过了,这里不再重复。
在connected() 方法中,会为 Dubbo Channel 创建相应的 HeaderExchangeChannel并将两者绑定然后通知上层 ExchangeHandler 处理 connect 事件。
在disconnected() 方法中,首先通知上层 ExchangeHandler 进行处理,之后在 DefaultFuture.closeChannel() 通知 DefaultFuture 连接断开(其实就是创建并传递一个 Response该 Response 的状态码为 CHANNEL_INACTIVE这样就不会继续阻塞业务线程了最后再将 HeaderExchangeChannel 与底层的 Dubbo Channel 解绑。
总结
本课时我们重点介绍了 Dubbo Exchange 层中对 Channel 和 ChannelHandler 接口的实现。
我们首先介绍了 Exchange 层中请求-响应模型的基本抽象,即 Request 类和 Response 类。然后又介绍了 ExchangeChannel 对 Channel 接口的实现,同时还说明了发送请求之后得到的 DefaultFuture 对象,这也是上一课时遗留的小问题。最后,讲解了 HeaderExchangeHandler 是如何将 Transporter 层的 ChannelHandler 对象与上层的 ExchangeHandler 对象相关联的。

View File

@@ -0,0 +1,432 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
22 Exchange 层剖析:彻底搞懂 Request-Response 模型(下)
在上一课时中,我们重点分析了 Exchange 层中 Channel 接口以及 ChannelHandler 接口的核心实现,同时还介绍 Request、Response 两个基础类,以及 DefaultFuture 这个 Future 实现。本课时,我们将继续讲解 Exchange 层其他接口的实现逻辑。
HeaderExchangeClient
HeaderExchangeClient 是 Client 装饰器,主要为其装饰的 Client 添加两个功能:
维持与 Server 的长连状态,这是通过定时发送心跳消息实现的;
在因故障掉线之后,进行重连,这是通过定时检查连接状态实现的。
因此HeaderExchangeClient 侧重定时轮资源的分配、定时任务的创建和取消。
HeaderExchangeClient 实现的是 ExchangeClient 接口,如下图所示,间接实现了 ExchangeChannel 和 Client 接口ExchangeClient 接口是个空接口,没有定义任何方法。
HeaderExchangeClient 继承关系图
HeaderExchangeClient 中有以下两个核心字段。
clientClient 类型):被修饰的 Client 对象。HeaderExchangeClient 中对 Client 接口的实现,都会委托给该对象进行处理。
channelExchangeChannel 类型Client 与服务端建立的连接HeaderExchangeChannel 也是一个装饰器在前面我们已经详细介绍过了这里就不再展开介绍。HeaderExchangeClient 中对 ExchangeChannel 接口的实现,都会委托给该对象进行处理。
HeaderExchangeClient 构造方法的第一个参数封装 Transport 层的 Client 对象,第二个参数 startTimer参与控制是否开启心跳定时任务和重连定时任务如果为 true才会进一步根据其他条件最终决定是否启动定时任务。这里我们以心跳定时任务为例
private void startHeartBeatTask(URL url) {
if (!client.canHandleIdle()) { // Client的具体实现决定是否启动该心跳任务
AbstractTimerTask.ChannelProvider cp = () -> Collections.singletonList(HeaderExchangeClient.this);
// 计算心跳间隔最小间隔不能低于1s
int heartbeat = getHeartbeat(url);
long heartbeatTick = calculateLeastDuration(heartbeat);
// 创建心跳任务
this.heartBeatTimerTask = new HeartbeatTimerTask(cp, heartbeatTick, heartbeat);
// 提交到IDLE_CHECK_TIMER这个时间轮中等待执行
IDLE_CHECK_TIMER.newTimeout(heartBeatTimerTask, heartbeatTick, TimeUnit.MILLISECONDS);
}
}
重连定时任务是在 startReconnectTask() 方法中启动的,其中会根据 URL 中的参数决定是否启动任务。重连定时任务最终也是提交到 IDLE_CHECK_TIMER 这个时间轮中,时间轮定义如下:
private static final HashedWheelTimer IDLE_CHECK_TIMER = new HashedWheelTimer(
new NamedThreadFactory("dubbo-client-idleCheck", true), 1, TimeUnit.SECONDS, TICKS_PER_WHEEL);
其实startReconnectTask() 方法的具体实现与前面展示的 startHeartBeatTask() 方法类似,这里就不再赘述。
下面我们继续回到心跳定时任务进行分析,你可以回顾第 20 课时介绍的 NettyClient 实现,其 canHandleIdle() 方法返回 true表示该实现可以自己发送心跳请求无须 HeaderExchangeClient 再启动一个定时任务。NettyClient 主要依靠 IdleStateHandler 中的定时任务来触发心跳事件,依靠 NettyClientHandler 来发送心跳请求。
对于无法自己发送心跳请求的 Client 实现HeaderExchangeClient 会为其启动 HeartbeatTimerTask 心跳定时任务,其继承关系如下图所示:
TimerTask 继承关系图
我们先来看 AbstractTimerTask 这个抽象类,它有三个字段。
channelProviderChannelProvider类型ChannelProvider 是 AbstractTimerTask 抽象类中定义的内部接口,定时任务会从该对象中获取 Channel。
tickLong类型任务的过期时间。
cancelboolean类型任务是否已取消。
AbstractTimerTask 抽象类实现了 TimerTask 接口的 run() 方法,首先会从 ChannelProvider 中获取此次任务相关的 Channel 集合(在 Client 端只有一个 Channel在 Server 端有多个 Channel然后检查 Channel 的状态,针对未关闭的 Channel 执行 doTask() 方法处理,最后通过 reput() 方法将当前任务重新加入时间轮中,等待再次到期执行。
AbstractTimerTask.run() 方法的具体实现如下:
public void run(Timeout timeout) throws Exception {
// 从ChannelProvider中获取任务要操作的Channel集合
Collection<Channel> c = channelProvider.getChannels();
for (Channel channel : c) {
if (channel.isClosed()) { // 检测Channel状态
continue;
}
doTask(channel); // 执行任务
}
reput(timeout, tick); // 将当前任务重新加入时间轮中,等待执行
}
doTask() 是一个 AbstractTimerTask 留给子类实现的抽象方法不同的定时任务执行不同的操作。例如HeartbeatTimerTask.doTask() 方法中会读取最后一次读写时间,然后计算距离当前的时间,如果大于心跳间隔,就会发送一个心跳请求,核心实现如下:
protected void doTask(Channel channel) {
// 获取最后一次读写时间
Long lastRead = lastRead(channel);
Long lastWrite = lastWrite(channel);
if ((lastRead != null && now() - lastRead > heartbeat)
|| (lastWrite != null && now() - lastWrite > heartbeat)) {
// 最后一次读写时间超过心跳时间,就会发送心跳请求
Request req = new Request();
req.setVersion(Version.getProtocolVersion());
req.setTwoWay(true);
req.setEvent(HEARTBEAT_EVENT);
channel.send(req);
}
}
这里 lastRead 和 lastWrite 时间戳,都是从要待处理 Channel 的附加属性中获取的,对应的 Key 分别是KEY_READ_TIMESTAMP、KEY_WRITE_TIMESTAMP。你可以回顾前面课程中介绍的 HeartbeatHandler它属于 Transport 层,是一个 ChannelHandler 的装饰器,在其 connected() 、sent() 方法中会记录最后一次写操作时间,在其 connected()、received() 方法中会记录最后一次读操作时间,在其 disconnected() 方法中会清理这两个时间戳。
在 ReconnectTimerTask 中会检测待处理 Channel 的连接状态,以及读操作的空闲时间,对于断开或是空闲时间较长的 Channel 进行重连,具体逻辑这里就不再展开了。
HeaderExchangeClient 最后要关注的是它的关闭流程,具体实现在 close() 方法中,如下所示:
public void close(int timeout) {
startClose(); // 将closing字段设置为true
doClose(); // 关闭心跳定时任务和重连定时任务
channel.close(timeout); // 关闭HeaderExchangeChannel
}
在 HeaderExchangeChannel.close(timeout) 方法中首先会将自身的 closed 字段设置为 true这样就不会继续发送请求。如果当前 Channel 上还有请求未收到响应,会循环等待至收到响应,如果超时未收到响应,会自己创建一个状态码将连接关闭的 Response 交给 DefaultFuture 处理,与收到 disconnected 事件相同。然后会关闭 Transport 层的 Channel以 NettyChannel 为例NettyChannel.close() 方法会先将自身的 closed 字段设置为 true清理 CHANNEL_MAP 缓存中的记录,以及 Channel 的附加属性,最后才是关闭 io.netty.channel.Channel。
HeaderExchangeServer
下面再来看 HeaderExchangeServer其继承关系如下图所示其中 Endpoint、RemotingServer、Resetable 这三个接口我们在前面已经详细介绍过了,这里不再重复。
HeaderExchangeServer 的继承关系图
与前面介绍的 HeaderExchangeClient 一样HeaderExchangeServer 是 RemotingServer 的装饰器,实现自 RemotingServer 接口的大部分方法都委托给了所修饰的 RemotingServer 对象。
在 HeaderExchangeServer 的构造方法中,会启动一个 CloseTimerTask 定时任务,定期关闭长时间空闲的连接,具体的实现方式与 HeaderExchangeClient 中的两个定时任务类似,这里不再展开分析。
需要注意的是,前面课时介绍的 NettyServer 并没有启动该定时任务,而是靠 NettyServerHandler 和 IdleStateHandler 实现的,原理与 NettyClient 类似,这里不再展开,你若感兴趣的话,可以回顾第 20课时或是查看 CloseTimerTask 的具体实现。
在 19 课时介绍 Transport Server 的时候,我们并没有过多介绍其关闭流程,这里我们就通过 HeaderExchangeServer 自顶向下梳理整个 Server 端关闭流程。先来看 HeaderExchangeServer.close() 方法的关闭流程:
将被修饰的 RemotingServer 的 closing 字段设置为 true表示这个 Server 端正在关闭,不再接受新 Client 的连接。你可以回顾第 19 课时中介绍的 AbstractServer.connected() 方法,会发现 Server 正在关闭或是已经关闭时,则直接关闭新建的 Client 连接。
向 Client 发送一个携带 ReadOnly 事件的请求(根据 URL 中的配置决定是否发送默认为发送。在接收到该请求之后Client 端的 HeaderExchangeHandler 会在 Channel 上添加 Key 为 “channel.readonly” 的附加信息,上层调用方会根据该附加信息,判断该连接是否可写。
循环去检测是否还存在 Client 与当前 Server 维持着长连接,直至全部 Client 断开连接或超时。
更新 closed 字段为 true之后 Client 不会再发送任何请求或是回复响应了。
取消 CloseTimerTask 定时任务。
调用底层 RemotingServer 对象的 close() 方法。以 NettyServer 为例,其 close() 方法会先调用 AbstractPeer 的 close() 方法将自身的 closed 字段设置为 true然后调用 doClose() 方法关闭 boss Channel即用来接收客户端连接的 Channel关闭 channels 集合中记录的 Channel这些 Channel 是与 Client 之间的连接),清理 channels 集合;最后,关闭 bossGroup 和 workerGroup 两个线程池。
HeaderExchangeServer.close() 方法的核心逻辑如下:
public void close(final int timeout) {
startClose(); // 将底层RemotingServer的closing字段设置为true表示当前Server正在关闭不再接收连接
if (timeout > 0) {
final long max = (long) timeout;
final long start = System.currentTimeMillis();
if (getUrl().getParameter(Constants.CHANNEL_SEND_READONLYEVENT_KEY, true)) {
// 发送ReadOnly事件请求通知客户端
sendChannelReadOnlyEvent();
}
while (HeaderExchangeServer.this.isRunning()
&& System.currentTimeMillis() - start < max) {
Thread.sleep(10); // 循环等待客户端断开连接
}
}
doClose(); // 将自身closed字段设置为true取消CloseTimerTask定时任务
server.close(timeout); // 关闭Transport层的Server
}
通过对上述关闭流程的分析你就可以清晰地知道 HeaderExchangeServer 优雅关闭的原理
HeaderExchanger
对于上层来说Exchange 层的入口是 Exchangers 这个门面类其中提供了多个 bind() 以及 connect() 方法的重载这些重载方法最终会通过 SPI 机制获取 Exchanger 接口的扩展实现这个流程与第 17 课时介绍的 Transport 层的入口—— Transporters 门面类相同
我们可以看到 Exchanger 接口的定义与前面介绍的 Transporter 接口非常类似同样是被 @SPI 接口修饰默认扩展名为header”,对应的是 HeaderExchanger 这个实现bind() 方法和 connect() 方法也同样是被 @Adaptive 注解修饰可以通过 URL 参数中的 exchanger 参数值指定扩展名称来覆盖默认值
@SPI(HeaderExchanger.NAME)
public interface Exchanger {
@Adaptive({Constants.EXCHANGER_KEY})
ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException;
@Adaptive({Constants.EXCHANGER_KEY})
ExchangeClient connect(URL url, ExchangeHandler handler) throws RemotingException;
}
Dubbo 只为 Exchanger 接口提供了 HeaderExchanger 这一个实现其中 connect() 方法创建的是 HeaderExchangeClient 对象bind() 方法创建的是 HeaderExchangeServer 对象如下图所示
HeaderExchanger 门面类
HeaderExchanger 的实现可以看到它会在 Transport 层的 Client Server 实现基础之上添加前文介绍的 HeaderExchangeClient HeaderExchangeServer 装饰器同时为上层实现的 ExchangeHandler 实例添加了 HeaderExchangeHandler 以及 DecodeHandler 两个修饰器
public class HeaderExchanger implements Exchanger {
public static final String NAME = "header";
@Override
public ExchangeClient connect(URL url, ExchangeHandler handler) throws RemotingException {
return new HeaderExchangeClient(Transporters.connect(url, new DecodeHandler(new HeaderExchangeHandler(handler))), true);
}
@Override
public ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
return new HeaderExchangeServer(Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler))));
}
}
再谈 Codec2
在前面第 17 课时介绍 Dubbo Remoting 核心接口的时候提到Codec2 接口提供了 encode() decode() 两个方法来实现消息与字节流之间的相互转换需要注意与 DecodeHandler 区分开来DecodeHandler 是对请求体和响应结果的解码Codec2 是对整个请求和响应的编解码
这里重点介绍 Transport 层和 Exchange 层对 Codec2 接口的实现涉及的类如下图所示
AbstractCodec抽象类并没有实现 Codec2 中定义的接口方法而是提供了几个给子类用的基础方法下面简单说明这些方法的功能
getSerialization() 方法通过 SPI 获取当前使用的序列化方式
checkPayload() 方法检查编解码数据的长度如果数据超长会抛出异常
isClientSide()、isServerSide() 方法判断当前是 Client 端还是 Server
接下来看TransportCodec我们可以看到这类上被标记了 @Deprecated 注解表示已经废弃TransportCodec 的实现非常简单其中根据 getSerialization() 方法选择的序列化方法对传入消息或 ChannelBuffer 进行序列化或反序列化这里就不再介绍 TransportCodec 实现了
TelnetCodec继承了 TransportCodec 序列化和反序列化的基本能力同时还提供了对 Telnet 命令处理的能力
最后来看ExchangeCodec它在 TelnetCodec 的基础之上添加了处理协议头的能力下面是 Dubbo 协议的格式能够清晰地看出协议中各个数据所占的位数
Dubbo 协议格式
结合上图我们来深入了解一下 Dubbo 协议中各个部分的含义
0~7 位和 8~15 位分别是 Magic High Magic Low是固定魔数值0xdabb我们可以通过这两个 Byte快速判断一个数据包是否为 Dubbo 协议这也类似 Java 字节码文件里的魔数
16 位是 Req/Res 标识用于标识当前消息是请求还是响应
17 位是 2Way 标识用于标识当前消息是单向还是双向
18 位是 Event 标识用于标识当前消息是否为事件消息
19~23 位是序列化类型的标志用于标识当前消息使用哪一种序列化算法
24~31 位是 Status 状态用于记录响应的状态仅在 Req/Res 0响应时有用
32~95 位是 Request ID用于记录请求的唯一标识类型为 long
96~127 位是序列化后的内容长度该值是按字节计数int 类型
128 位之后是可变的数据被特定的序列化算法由序列化类型标志确定序列化后每个部分都是一个 byte [] 或者 byte如果是请求包Req/Res = 1则每个部分依次为Dubbo versionService nameService versionMethod nameMethod parameter typesMethod arguments Attachments如果是响应包Req/Res = 0则每个部分依次为①返回值类型byte标识从服务器端返回的值类型包括返回空值RESPONSE_NULL_VALUE 2)、正常响应值RESPONSE_VALUE 1和异常RESPONSE_WITH_EXCEPTION 0三种;②返回值从服务端返回的响应 bytes
可以看到 Dubbo 协议中前 128 位是协议头之后的内容是具体的负载数据协议头就是通过 ExchangeCodec 实现编解码的
ExchangeCodec 的核心字段有如下几个
HEADER_LENGTHint 类型值为 16协议头的字节数16 字节 128
MAGICshort 类型值为 0xdabb协议头的前 16 分为 MAGIC_HIGH MAGIC_LOW 两个字节
FLAG_REQUESTbyte 类型值为 0x80用于设置 Req/Res 标志位
FLAG_TWOWAYbyte 类型值为 0x40用于设置 2Way 标志位
FLAG_EVENTbyte 类型值为 0x20用于设置 Event 标志位
SERIALIZATION_MASKint 类型值为 0x1f用于获取序列化类型的标志位的掩码
ExchangeCodec encode() 方法中会根据需要编码的消息类型进行分类其中 encodeRequest() 方法专门对 Request 对象进行编码具体实现如下
protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
Serialization serialization = getSerialization(channel);
byte[] header = new byte[HEADER_LENGTH]; // 该数组用来暂存协议头
// 在header数组的前两个字节中写入魔数
Bytes.short2bytes(MAGIC, header);
// 根据当前使用的序列化设置协议头中的序列化标志位
header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId());
if (req.isTwoWay()) { // 设置协议头中的2Way标志位
header[2] |= FLAG_TWOWAY;
}
if (req.isEvent()) { // 设置协议头中的Event标志位
header[2] |= FLAG_EVENT;
}
// 将请求ID记录到请求头中
Bytes.long2bytes(req.getId(), header, 4);
// 下面开始序列化请求并统计序列化后的字节数
// 首先使用savedWriteIndex记录ChannelBuffer当前的写入位置
int savedWriteIndex = buffer.writerIndex();
// 将写入位置后移16字节
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
// 根据选定的序列化方式对请求进行序列化
ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
if (req.isEvent()) { // 对事件进行序列化
encodeEventData(channel, out, req.getData());
} else { // 对Dubbo请求进行序列化具体在DubboCodec中实现
encodeRequestData(channel, out, req.getData(), req.getVersion());
}
out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable) out).cleanup();
}
bos.flush();
bos.close(); // 完成序列化
int len = bos.writtenBytes(); // 统计请求序列化之后得到的字节数
checkPayload(channel, len); // 限制一下请求的字节长度
Bytes.int2bytes(len, header, 12); // 将字节数写入header数组中
// 下面调整ChannelBuffer当前的写入位置并将协议头写入Buffer中
buffer.writerIndex(savedWriteIndex);
buffer.writeBytes(header);
// 最后将ChannelBuffer的写入位置移动到正确的位置
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
}
encodeResponse() 方法编码响应的方式与 encodeRequest() 方法编码请求的方式类似这里就不再展开介绍了感兴趣的同学可以参考源码进行学习对于既不是 Request也不是 Response 的消息ExchangeCodec 会使用从父类继承下来的能力来编码例如对 telnet 命令的编码
ExchangeCodec decode() 方法是 encode() 方法的逆过程会先检查魔数然后读取协议头和后续消息的长度最后根据协议头中的各个标志位构造相应的对象以及反序列化数据在了解协议头结构的前提下再去阅读这段逻辑就十分轻松了这就留给你自己尝试分析一下
总结
本课时我们重点介绍了 Dubbo Exchange 层中对 Client Server 接口的实现
我们首先介绍了 HeaderExchangeClient ExchangeClient 接口的实现以及 HeaderExchangeServer ExchangeServer 接口的实现这两者是在 Transport Client Server 的基础上添加了新的功能接下来又讲解了 HeaderExchanger 这个用来创建 HeaderExchangeClient HeaderExchangeServer 的门面类最后分析了 Dubbo 协议的格式以及处理 Dubbo 协议的 ExchangeCodec 实现

View File

@@ -0,0 +1,412 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
23 核心接口介绍RPC 层骨架梳理
在前面的课程中,我们深入介绍了 Dubbo 架构中的 Dubbo Remoting 层的相关内容,了解了 Dubbo 底层的网络模型以及线程模型。从本课时开始,我们就开始介绍 Dubbo Remoting 上面的一层—— Protocol 层如下图所示Protocol 层是 Remoting 层的使用者,会通过 Exchangers 门面类创建 ExchangeClient 以及 ExchangeServer还会创建相应的 ChannelHandler 实现以及 Codec2 实现并交给 Exchange 层进行装饰。
Dubbo 架构中 Protocol 层的位置图
Protocol 层在 Dubbo 源码中对应的是 dubbo-rpc 模块,该模块的结构如下图所示:
dubbo-rpc 模块结构图
我们可以看到有很多模块,和 dubbo-remoting 模块类似,其中 dubbo-rpc-api 是对具体协议、服务暴露、服务引用、代理等的抽象,是整个 Protocol 层的核心。剩余的模块例如dubbo-rpc-dubbo、dubbo-rpc-grpc、dubbo-rpc-http 等,都是 Dubbo 支持的具体协议可以看作dubbo-rpc-api 模块的具体实现。
dubbo-rpc-api
这里我们首先来看 dubbo-rpc-api 模块的包结构,如下图所示:
dubbo-rpc-api 模块的包结构图
根据上图展示的 dubbo-rpc-api 模块的结构,我们可以看到 dubbo-rpc-api 模块包括了以下几个核心包。
filter 包:在进行服务引用时会进行一系列的过滤,其中包括了很多过滤器。
listener 包:在服务发布和服务引用的过程中,我们可以添加一些 Listener 来监听相应的事件,与 Listener 相关的接口 Adapter、Wrapper 实现就在这个包内。
protocol 包:一些实现了 Protocol 接口以及 Invoker 接口的抽象类位于该包之中,它们主要是为 Protocol 接口的具体实现以及 Invoker 接口的具体实现提供一些公共逻辑。
proxy 包:提供了创建代理的能力,在这个包中支持 JDK 动态代理以及 Javassist 字节码两种方式生成本地代理类。
support 包:包括了 RpcUtils 工具类、Mock 相关的 Protocol 实现以及 Invoker 实现。
没有在上述 package 中的接口和类,是更为核心的抽象接口,上述 package 内的类更多的是这些接口的实现类。下面我们就来介绍这些在 org.apache.dubbo.rpc 包下的核心接口。
核心接口
在 Dubbo RPC 层中涉及的核心接口有 Invoker、Invocation、Protocol、Result、Exporter、ProtocolServer、Filter 等,这些接口分别抽象了 Dubbo RPC 层的不同概念,看似相互独立,但又相互协同,一起构建出了 DubboRPC 层的骨架。下面我们将逐一介绍这些核心接口的含义。
首先要介绍的是 Dubbo 中非常重要的一个接口——Invoker 接口。可以说Invoker 渗透在整个 Dubbo 代码实现里Dubbo 中的很多设计思路都会向 Invoker 这个概念靠拢,但这对于刚接触这部分代码的同学们来说,可能不是很友好。
这里我们借助如下这样一个精简的示意图来对比说明两种最关键的 Invoker服务提供 Invoker 和服务消费 Invoker。
Invoker 核心示意图
以 dubbo-demo-annotation-consumer 这个示例项目中的 Consumer 为例,它会拿到一个 DemoService 对象,如下所示,这其实是一个代理(即上图中的 Proxy这个 Proxy 底层就会通过 Invoker 完成网络调用:
@Component("demoServiceComponent")
public class DemoServiceComponent implements DemoService {
@Reference
private DemoService demoService;
@Override
public String sayHello(String name) {
return demoService.sayHello(name);
}
}
紧接着我们再来看一个 dubbo-demo-annotation-provider 示例中的 Provider 实现:
@Service
public class DemoServiceImpl implements DemoService {
@Override
public String sayHello(String name) {
return "Hello " + name + ", response from provider: " + RpcContext.getContext().getLocalAddress();
}
}
这里的 DemoServiceImpl 类会被封装成为一个 AbstractProxyInvoker 实例,并新生成对应的 Exporter 实例。当 Dubbo Protocol 层收到一个请求之后,会找到这个 Exporter 实例,并调用其对应的 AbstractProxyInvoker 实例,从而完成 Provider 逻辑的调用。这里我先帮你找出了最重要的两类 Invoker ,简单介绍了它们工作场景,当然 Dubbo 中还有其他类型的 Invoker后面我们再一一介绍。
下面来看 Invoker 这个接口的具体定义,如下所示:
public interface Invoker<T> extends Node {
// 服务接口
Class<T> getInterface();
// 进行一次调用,也有人称之为一次"会话",你可以理解为一次调用
Result invoke(Invocation invocation) throws RpcException;
}
Invocation 接口是 Invoker.invoke() 方法的参数,抽象了一次 RPC 调用的目标服务和方法信息、相关参数信息、具体的参数值以及一些附加信息,具体定义如下:
public interface Invocation {
// 调用Service的唯一标识
String getTargetServiceUniqueName();
// 调用的方法名称
String getMethodName();
// 调用的服务名称
String getServiceName();
// 参数类型集合
Class<?>[] getParameterTypes();
// 参数签名集合
default String[] getCompatibleParamSignatures() {
return Stream.of(getParameterTypes())
.map(Class::getName)
.toArray(String[]::new);
}
// 此次调用具体的参数值
Object[] getArguments();
// 此次调用关联的Invoker对象
Invoker<?> getInvoker();
// Invoker对象可以设置一些KV属性这些属性并不会传递给Provider
Object put(Object key, Object value);
Object get(Object key);
Map<Object, Object> getAttributes();
// Invocation可以携带一个KV信息作为附加信息一并传递给Provider
// 注意与 attribute 的区分
Map<String, String> getAttachments();
Map<String, Object> getObjectAttachments();
void setAttachment(String key, String value);
void setAttachment(String key, Object value);
void setObjectAttachment(String key, Object value);
void setAttachmentIfAbsent(String key, String value);
void setAttachmentIfAbsent(String key, Object value);
void setObjectAttachmentIfAbsent(String key, Object value);
String getAttachment(String key);
Object getObjectAttachment(String key);
String getAttachment(String key, String defaultValue);
Object getObjectAttachment(String key, Object defaultValue);
}
Result 接口是 Invoker.invoke() 方法的返回值,抽象了一次调用的返回值,其中包含了被调用方返回值(或是异常)以及附加信息,我们也可以添加回调方法,在 RPC 调用方法结束时会触发这些回调。Result 接口的具体定义如下:
public interface Result extends Serializable {
// 获取/设置此次调用的返回值
Object getValue();
void setValue(Object value);
// 如果此次调用发生异常,则可以通过下面三个方法获取
Throwable getException();
void setException(Throwable t);
boolean hasException();
// recreate()方法是一个复合操作,如果此次调用发生异常,则直接抛出异常,
// 如果没有异常,则返回结果
Object recreate() throws Throwable;
// 添加一个回调当RPC调用完成时会触发这里添加的回调
Result whenCompleteWithContext(BiConsumer<Result, Throwable> fn);
<U> CompletableFuture<U> thenApply(Function<Result, ? extends U> fn);
// 阻塞线程等待此次RPC调用完成(或是超时)
Result get() throws InterruptedException, ExecutionException;
Result get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
// Result中同样可以携带附加信息
Map<String, String> getAttachments();
Map<String, Object> getObjectAttachments();
void addAttachments(Map<String, String> map);
void addObjectAttachments(Map<String, Object> map);
void setAttachments(Map<String, String> map);
void setObjectAttachments(Map<String, Object> map);
String getAttachment(String key);
Object getObjectAttachment(String key);
String getAttachment(String key, String defaultValue);
Object getObjectAttachment(String key, Object defaultValue);
void setAttachment(String key, String value);
void setAttachment(String key, Object value);
void setObjectAttachment(String key, Object valu
}
在上面介绍 Provider 端的 Invoker 时提到,我们的业务接口实现会被包装成一个 AbstractProxyInvoker 对象,然后由 Exporter 暴露出去,让 Consumer 可以调用到该服务。Exporter 暴露 Invoker 的实现,说白了,就是让 Provider 能够根据请求的各种信息,找到对应的 Invoker。我们可以维护一个 Map其中 Key 可以根据请求中的信息构建Value 为封装相应服务 Bean 的 Exporter 对象,这样就可以实现上述服务发布的要求了。
我们先来看 Exporter 接口的定义:
public interface Exporter<T> {
// 获取底层封装的Invoker对象
Invoker<T> getInvoker();
// 取消发布底层的Invoker对象
void unexport();
}
为了监听服务发布事件以及取消暴露事件Dubbo 定义了一个 SPI 扩展接口——ExporterListener 接口,其定义如下:
@SPI
public interface ExporterListener {
// 当有服务发布的时候,会触发该方法
void exported(Exporter<?> exporter) throws RpcException;
// 当有服务取消发布的时候,会触发该方法
void unexported(Exporter<?> exporter);
}
虽然 ExporterListener 是个扩展接口,但是 Dubbo 本身并没有提供什么有用的扩展实现,我们需要自己提供具体实现监听感兴趣的事情。
相应地,我们可以添加 InvokerListener 监听器,监听 Consumer 引用服务时触发的事件InvokerListener 接口的定义如下:
@SPI
public interface InvokerListener {
// 当服务引用的时候,会触发该方法
void referred(Invoker<?> invoker) throws RpcException;
// 当销毁引用的服务时,会触发该方法
void destroyed(Invoker<?> invoker);
}
Protocol 接口是整个 Dubbo Protocol 层的核心接口之一,其中定义了 export() 和 refer() 两个核心方法,具体定义如下:
@SPI("dubbo") // 默认使用DubboProtocol实现
public interface Protocol {
// 默认端口
int getDefaultPort();
// 将一个Invoker暴露出去export()方法实现需要是幂等的,
// 即同一个服务暴露多次和暴露一次的效果是相同的
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
// 引用一个Invokerrefer()方法会根据参数返回一个Invoker对象
// Consumer端可以通过这个Invoker请求到Provider端的服务
@Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
// 销毁export()方法以及refer()方法使用到的Invoker对象释放
// 当前Protocol对象底层占用的资源
void destroy();
// 返回当前Protocol底层的全部ProtocolServer
default List<ProtocolServer> getServers() {
return Collections.emptyList();
}
}
在 Protocol 接口的实现中export() 方法并不是简单地将 Invoker 对象包装成 Exporter 对象返回,其中还涉及代理对象的创建、底层 Server 的启动等操作refer() 方法除了根据传入的 type 类型以及 URL 参数查询 Invoker 之外,还涉及相关 Client 的创建等操作。
Dubbo 在 Protocol 层专门定义了一个 ProxyFactory 接口作为创建代理对象的工厂。ProxyFactory 接口是一个扩展接口,其中定义了 getProxy() 方法为 Invoker 创建代理对象,还定义了 getInvoker() 方法将代理对象反向封装成 Invoker 对象。
@SPI("javassist")
public interface ProxyFactory {
// 为传入的Invoker对象创建代理对象
@Adaptive({PROXY_KEY})
<T> T getProxy(Invoker<T> invoker) throws RpcException;
@Adaptive({PROXY_KEY})
<T> T getProxy(Invoker<T> invoker, boolean generic) throws RpcException;
// 将传入的代理对象封装成Invoker对象可以暂时理解为getProxy()的逆操作
@Adaptive({PROXY_KEY})
<T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) throws RpcException;
}
看到 ProxyFactory 上的 @SPI 注解,我们知道其默认实现使用 javassist 来创建代码对象当然Dubbo 还提供了其他方式来创建代码,例如 JDK 动态代理。
ProtocolServer 接口是对前文介绍的 RemotingServer 的一层简单封装,其实现也都非常简单,这里就不再展开。
最后一个要介绍的核心接口是 Filter 接口。关于 Filter相信做过 Java Web 编程的同学们会非常熟悉这个基础概念Java Web 开发中的 Filter 是用来拦截 HTTP 请求的Dubbo 中的 Filter 接口功能与之类似,是用来拦截 Dubbo 请求的。
在 Dubbo 的 Filter 接口中,定义了一个 invoke() 方法将请求传递给后续的 Invoker 进行处理(后续的这个 Invoker 对象可能是一个 Filter 封装而成的。Filter 接口的具体定义如下:
@SPI
public interface Filter {
// 将请求传给后续的Invoker进行处理
Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;
interface Listener { // 用于监听响应以及异常
void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation);
void onError(Throwable t, Invoker<?> invoker, Invocation invocation);
}
}
Filter 也是一个扩展接口Dubbo 提供了丰富的 Filter 实现来进行功能扩展,当然我们也可以提供自己的 Filter 实现来扩展 Dubbo 的功能。
总结
本课时我们首先介绍了 Dubbo RPC 层在整个 Dubbo 框架中所处的位置,然后说明了 dubbo-rpc-api 层的结构以及其中各个包提供的基本功能。接下来,我们还详细介绍了 Dubbo RPC 层中涉及的核心接口,包括 Invoker、Invocation、Protocol、Result、ProxyFactory、ProtocolServer 等核心接口,以及 ExporterListener、Filter 等扩展类的接口。

View File

@@ -0,0 +1,516 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
24 从 Protocol 起手,看服务暴露和服务引用的全流程(上)
在上一课时我们讲解了 Protocol 的核心接口,那本课时我们就以 Protocol 接口为核心,详细介绍整个 Protocol 的核心实现。下图展示了 Protocol 接口的继承关系:
Protocol 接口继承关系图
其中AbstractProtocol提供了一些 Protocol 实现需要的公共能力以及公共字段,它的核心字段有如下三个。
exporterMapMap>类型):用于存储出去的服务集合,其中的 Key 通过 ProtocolUtils.serviceKey() 方法创建的服务标识,在 ProtocolUtils 中维护了多层的 Map 结构(如下图所示)。首先按照 group 分组,在实践中我们可以根据需求设置 group例如按照机房、地域等进行 group 划分,做到就近调用;在 GroupServiceKeyCache 中,依次按照 serviceName、serviceVersion、port 进行分类,最终缓存的 serviceKey 是前面三者拼接而成的。
groupServiceKeyCacheMap 结构图
serverMapMap类型记录了全部的 ProtocolServer 实例,其中的 Key 是 host 和 port 组成的字符串Value 是监听该地址的 ProtocolServer。ProtocolServer 就是对 RemotingServer 的一层简单封装,表示一个服务端。
invokersSet>类型):服务引用的集合。
AbstractProtocol 没有对 Protocol 的 export() 方法进行实现,对 refer() 方法的实现也是委托给了 protocolBindingRefer() 这个抽象方法然后由子类实现。AbstractProtocol 唯一实现的方法就是 destory() 方法,其首先会遍历 Invokers 集合,销毁全部的服务引用,然后遍历全部的 exporterMap 集合,销毁发布出去的服务,具体实现如下:
public void destroy() {
for (Invoker<?> invoker : invokers) {
if (invoker != null) {
invokers.remove(invoker);
invoker.destroy(); // 关闭全部的服务引用
}
}
for (String key : new ArrayList<String>(exporterMap.keySet())) {
Exporter<?> exporter = exporterMap.remove(key);
if (exporter != null) {
exporter.unexport(); // 关闭暴露出去的服务
}
}
}
export 流程简析
了解了 AbstractProtocol 提供的公共能力之后我们再来分析Dubbo 默认使用的 Protocol 实现类—— DubboProtocol 实现。这里我们首先关注 DubboProtocol 的 export() 方法,也就是服务发布的相关实现,如下所示:
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
URL url = invoker.getUrl();
// 创建ServiceKey其核心实现在前文已经详细分析过了这里不再重复
String key = serviceKey(url);
// 将上层传入的Invoker对象封装成DubboExporter对象然后记录到exporterMap集合中
DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
exporterMap.put(key, exporter);
... // 省略一些日志操作
// 启动ProtocolServer
openServer(url);
// 进行序列化的优化处理
optimizeSerialization(url);
return exporter;
}
1. DubboExporter
这里涉及的第一个点是 DubboExporter 对 Invoker 的封装DubboExporter 的继承关系如下图所示:
DubboExporter 继承关系图
AbstractExporter 中维护了一个 Invoker 对象,以及一个 unexported 字段boolean 类型),在 unexport() 方法中会设置 unexported 字段为 true并调用 Invoker 对象的 destory() 方法进行销毁。
DubboExporter 也比较简单,其中会维护底层 Invoker 对应的 ServiceKey 以及 DubboProtocol 中的 exportMap 集合,在其 unexport() 方法中除了会调用父类 AbstractExporter 的 unexport() 方法之外,还会清理该 DubboExporter 实例在 exportMap 中相应的元素。
2. 服务端初始化
了解了 Exporter 实现之后,我们继续看 DubboProtocol 中服务发布的流程。从下面这张调用关系图中可以看出openServer() 方法会一路调用前面介绍的 Exchange 层、Transport 层,并最终创建 NettyServer 来接收客户端的请求。
export() 方法调用栈
下面我们将逐个介绍 export() 方法栈中的每个被调用的方法。
首先,在 openServer() 方法中会根据 URL 判断当前是否为服务端,只有服务端才能创建 ProtocolServer 并对外服务。如果是来自服务端的调用,会依靠 serverMap 集合检查是否已有 ProtocolServer 在监听 URL 指定的地址;如果没有,会调用 createServer() 方法进行创建。openServer() 方法的具体实现如下:
private void openServer(URL url) {
String key = url.getAddress(); // 获取host:port这个地址
boolean isServer = url.getParameter(IS_SERVER_KEY, true);
if (isServer) { // 只有Server端才能启动Server对象
ProtocolServer server = serverMap.get(key);
if (server == null) { // 无ProtocolServer监听该地址
synchronized (this) { // DoubleCheck防止并发问题
server = serverMap.get(key);
if (server == null) {
// 调用createServer()方法创建ProtocolServer对象
serverMap.put(key, createServer(url));
}
}
} else {
// 如果已有ProtocolServer实例则尝试根据URL信息重置ProtocolServer
server.reset(url);
}
}
}
createServer() 方法首先会为 URL 添加一些默认值,同时会进行一些参数值的检测,主要有五个。
HEARTBEAT_KEY 参数值,默认值为 60000表示默认的心跳时间间隔为 60 秒。
CHANNEL_READONLYEVENT_SENT_KEY 参数值,默认值为 true表示 ReadOnly 请求需要阻塞等待响应返回。在 Server 关闭的时候,只能发送 ReadOnly 请求,这些 ReadOnly 请求由这里设置的 CHANNEL_READONLYEVENT_SENT_KEY 参数值决定是否需要等待响应返回。
CODEC_KEY 参数值,默认值为 dubbo。你可以回顾 Codec2 接口中 @Adaptive 注解的参数,都是获取该 URL 中的 CODEC_KEY 参数值。
检测 SERVER_KEY 参数指定的扩展实现名称是否合法,默认值为 netty。你可以回顾 Transporter 接口中 @Adaptive 注解的参数,它决定了 Transport 层使用的网络库实现,默认使用 Netty 4 实现。
检测 CLIENT_KEY 参数指定的扩展实现名称是否合法。同 SERVER_KEY 参数的检查流程。
完成上述默认参数值的设置之后,我们就可以通过 Exchangers 门面类创建 ExchangeServer并封装成 DubboProtocolServer 返回。
private ProtocolServer createServer(URL url) {
url = URLBuilder.from(url)
// ReadOnly请求是否阻塞等待
.addParameterIfAbsent(CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString())
// 心跳间隔
.addParameterIfAbsent(HEARTBEAT_KEY, String.valueOf(DEFAULT_HEARTBEAT))
.addParameter(CODEC_KEY, DubboCodec.NAME) // Codec2扩展实现
.build();
// 检测SERVER_KEY参数指定的Transporter扩展实现是否合法
String str = url.getParameter(SERVER_KEY, DEFAULT_REMOTING_SERVER);
if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) {
throw new RpcException("...");
}
// 通过Exchangers门面类创建ExchangeServer对象
ExchangeServer server = Exchangers.bind(url, requestHandler);
... // 检测CLIENT_KEY参数指定的Transporter扩展实现是否合法(略)
// 将ExchangeServer封装成DubboProtocolServer返回
return new DubboProtocolServer(server);
}
在 createServer() 方法中还有几个细节需要展开分析一下。第一个是创建 ExchangeServer 时,使用的 Codec2 接口实现实际上是 DubboCountCodec对应的 SPI 配置文件如下:
Codec2 SPI 配置文件
DubboCountCodec 中维护了一个 DubboCodec 对象,编解码的能力都是 DubboCodec 提供的DubboCountCodec 只负责在解码过程中 ChannelBuffer 的 readerIndex 指针控制,具体实现如下:
public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
int save = buffer.readerIndex(); // 首先保存readerIndex指针位置
// 创建MultiMessage对象其中可以存储多条消息
MultiMessage result = MultiMessage.create();
do {
// 通过DubboCodec提供的解码能力解码一条消息
Object obj = codec.decode(channel, buffer);
// 如果可读字节数不足一条消息则会重置readerIndex指针
if (Codec2.DecodeResult.NEED_MORE_INPUT == obj) {
buffer.readerIndex(save);
break;
} else { // 将成功解码的消息添加到MultiMessage中暂存
result.addMessage(obj);
logMessageLength(obj, buffer.readerIndex() - save);
save = buffer.readerIndex();
}
} while (true);
if (result.isEmpty()) { // 一条消息也未解码出来则返回NEED_MORE_INPUT错误码
return Codec2.DecodeResult.NEED_MORE_INPUT;
}
if (result.size() == 1) { // 只解码出来一条消息,则直接返回该条消息
return result.get(0);
}
// 解码出多条消息的话会将MultiMessage返回
return result;
}
DubboCountCodec、DubboCodec 都实现了第 22 课时介绍的 Codec2 接口,其中 DubboCodec 是 ExchangeCodec 的子类。
DubboCountCodec 及 DubboCodec 继承关系图
我们知道 ExchangeCodec 只处理了 Dubbo 协议的请求头,而 DubboCodec 则是通过继承的方式,在 ExchangeCodec 基础之上,添加了解析 Dubbo 消息体的功能。在第 22 课时介绍 ExchangeCodec 实现的时候,我们重点分析了 encodeRequest() 方法,即 Request 请求的编码实现,其中会调用 encodeRequestData() 方法完成请求体的编码。
DubboCodec 中就覆盖了 encodeRequestData() 方法,按照 Dubbo 协议的格式编码 Request 请求体,具体实现如下:
protected void encodeRequestData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
// 请求体相关的内容都封装在了RpcInvocation
RpcInvocation inv = (RpcInvocation) data;
out.writeUTF(version); // 写入版本号
String serviceName = inv.getAttachment(INTERFACE_KEY);
if (serviceName == null) {
serviceName = inv.getAttachment(PATH_KEY);
}
// 写入服务名称
out.writeUTF(serviceName);
// 写入Service版本号
out.writeUTF(inv.getAttachment(VERSION_KEY));
// 写入方法名称
out.writeUTF(inv.getMethodName());
// 写入参数类型列表
out.writeUTF(inv.getParameterTypesDesc());
// 依次写入全部参数
Object[] args = inv.getArguments();
if (args != null) {
for (int i = 0; i < args.length; i++) {
out.writeObject(encodeInvocationArgument(channel, inv, i));
}
}
// 依次写入全部的附加信息
out.writeAttachments(inv.getObjectAttachments());
}
RpcInvocation 实现了上一课时介绍的 Invocation 接口如下图所示
RpcInvocation 继承关系图
下面是 RpcInvocation 中的核心字段通过读写这些字段即可实现 Invocation 接口的全部方法
targetServiceUniqueNameString类型要调用的唯一服务名称其实就是 ServiceKey interface/group:version 三部分构成的字符串
methodNameString类型调用的目标方法名称
serviceNameString类型调用的目标服务名称示例中就是org.apache.dubbo.demo.DemoService
parameterTypesClass<?>[]类型):记录了目标方法的全部参数类型。
parameterTypesDescString类型参数列表签名。
argumentsObject[]类型):具体参数值。
attachmentsMap类型此次调用的附加信息可以被序列化到请求中。
attributesMap类型此次调用的属性信息这些信息不能被发送出去。
invokerInvoker<?>类型):此次调用关联的 Invoker 对象。
returnTypeClass<?>类型):返回值的类型。
invokeModeInvokeMode类型此次调用的模式分为 SYNC、ASYNC 和 FUTURE 三类。
我们在上面的继承图中看到 RpcInvocation 的一个子类—— DecodeableRpcInvocation它是用来支持解码的其实现的 decode() 方法正好是 DubboCodec.encodeRequestData() 方法对应的解码操作,在 DubboCodec.decodeBody() 方法中就调用了这个方法,调用关系如下图所示:
decode() 方法调用栈
这个解码过程中有个细节,在 DubboCodec.decodeBody() 方法中有如下代码片段,其中会根据 DECODE_IN_IO_THREAD_KEY 这个参数决定是否在 DubboCodec 中进行解码DubboCodec 是在 IO 线程中调用的)。
// decode request.
Request req = new Request(id);
... // 省略Request中其他字段的设置
Object data;
DecodeableRpcInvocation inv;
// 这里会检查DECODE_IN_IO_THREAD_KEY参数
if (channel.getUrl().getParameter(DECODE_IN_IO_THREAD_KEY, DEFAULT_DECODE_IN_IO_THREAD)) {
inv = new DecodeableRpcInvocation(channel, req, is, proto);
inv.decode(); // 直接调用decode()方法在当前IO线程中解码
} else { // 这里只是读取数据不会调用decode()方法在当前IO线程中进行解码
inv = new DecodeableRpcInvocation(channel, req,
new UnsafeByteArrayInputStream(readMessageData(is)), proto);
}
data = inv;
req.setData(data); // 设置到Request请求的data字段
return req;
如果不在 DubboCodec 中解码,那会在哪里解码呢?你可以回顾第 20 课时介绍的 DecodeHandlerTransport 层),它的 received() 方法也是可以进行解码的另外DecodeableRpcInvocation 中有一个 hasDecoded 字段来判断当前是否已经完成解码,这样,三者配合就可以根据 DECODE_IN_IO_THREAD_KEY 参数决定执行解码操作的线程了。
如果你对线程模型不清楚,可以依次回顾一下 Exchangers、HeaderExchanger、Transporters 三个门面类的 bind() 方法,以及 Dispatcher 各实现提供的线程模型,搞清楚各个 ChannelHandler 是由哪个线程执行的,这些知识点在前面课时都介绍过了,不再重复。这里我们就直接以 AllDispatcher 实现为例给出结论。
IO 线程内执行的 ChannelHandler 实现依次有InternalEncoder、InternalDecoder两者底层都是调用 DubboCodec、IdleStateHandler、MultiMessageHandler、HeartbeatHandler 和 NettyServerHandler。
在非 IO 线程内执行的 ChannelHandler 实现依次有DecodeHandler、HeaderExchangeHandler 和 DubboProtocol$requestHandler。
在 DubboProtocol 中有一个 requestHandler 字段,它是一个实现了 ExchangeHandlerAdapter 抽象类的匿名内部类的实例,间接实现了 ExchangeHandler 接口,其核心是 reply() 方法,具体实现如下:
public CompletableFuture<Object> reply(ExchangeChannel channel, Object message) throws RemotingException {
... // 这里省略了检查message类型的逻辑通过前面Handler的处理这里收到的message必须是Invocation类型的对象
Invocation inv = (Invocation) message;
// 获取此次调用Invoker对象
Invoker<?> invoker = getInvoker(channel, inv);
... // 针对客户端回调的内容,在后面详细介绍,这里不再展开分析
// 将客户端的地址记录到RpcContext中
RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
// 执行真正的调用
Result result = invoker.invoke(inv);
// 返回结果
return result.thenApply(Function.identity());
}
其中 getInvoker() 方法会先根据 Invocation 携带的信息构造 ServiceKey然后从 exporterMap 集合中查找对应的 DubboExporter 对象,并从中获取底层的 Invoker 对象返回,具体实现如下:
Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException {
... // 省略对客户端Callback以及stub的处理逻辑后面单独介绍
String serviceKey = serviceKey(port, path, (String) inv.getObjectAttachments().get(VERSION_KEY),
(String) inv.getObjectAttachments().get(GROUP_KEY));
DubboExporter<?> exporter = (DubboExporter<?>) exporterMap.get(serviceKey);
... // 查找不到相应的DubboExporter对象时会直接抛出异常这里省略了这个检测
return exporter.getInvoker(); // 获取exporter中获取Invoker对象
}
到这里,我们终于见到了对 Invoker 对象的调用,对 Invoker 实现的介绍和分析,在后面课时我们会深入介绍,这里就先专注于 DubboProtocol 的相关内容。
3. 序列化优化处理
下面我们回到 DubboProtocol.export() 方法继续分析,在完成 ProtocolServer 的启动之后export() 方法最后会调用 optimizeSerialization() 方法对指定的序列化算法进行优化。
这里先介绍一个基础知识,在使用某些序列化算法(例如, Kryo、FST 等)时,为了让其能发挥出最佳的性能,最好将那些需要被序列化的类提前注册到 Dubbo 系统中。例如,我们可以通过一个实现了 SerializationOptimizer 接口的优化器,并在配置中指定该优化器,如下示例代码:
public class SerializationOptimizerImpl implements SerializationOptimizer {
public Collection<Class> getSerializableClasses() {
List<Class> classes = new ArrayList<>();
classes.add(xxxx.class); // 添加需要被序列化的类
return classes;
}
}
在 DubboProtocol.optimizeSerialization() 方法中,就会获取该优化器中注册的类,通知底层的序列化算法进行优化,序列化的性能将会被大大提升。当然,在进行序列化的时候,难免会级联到很多 Java 内部的类例如数组、各种集合类型等Kryo、FST 等序列化算法已经自动将JDK 中的常用类进行了注册,所以无须重复注册它们。
下面我们回头来看 optimizeSerialization() 方法,分析序列化优化操作的具体实现细节:
private void optimizeSerialization(URL url) throws RpcException {
// 根据URL中的optimizer参数值确定SerializationOptimizer接口的实现类
String className = url.getParameter(OPTIMIZER_KEY, "");
Class clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
// 创建SerializationOptimizer实现类的对象
SerializationOptimizer optimizer = (SerializationOptimizer) clazz.newInstance();
// 调用getSerializableClasses()方法获取需要注册的类
for (Class c : optimizer.getSerializableClasses()) {
SerializableClassRegistry.registerClass(c);
}
optimizers.add(className);
}
SerializableClassRegistry 底层维护了一个 static 的 MapREGISTRATIONS 字段registerClass() 方法就是将待优化的类写入该集合中暂存,在使用 Kryo、FST 等序列化算法时,会读取该集合中的类,完成注册操作,相关的调用关系如下图所示:
getRegisteredClasses() 方法的调用位置
按照 Dubbo 官方文档的说法即使不注册任何类进行优化Kryo 和 FST 的性能依然普遍优于Hessian2 和 Dubbo 序列化。
总结
本课时我们重点介绍了 DubboProtocol 发布一个 Dubbo 服务的核心流程。首先,我们介绍了 AbstractProtocol 这个抽象类为 Protocol 实现类提供的公共能力和字段,然后我们结合 Dubbo 协议对应的 DubboProtocol 实现,讲解了发布一个 Dubbo 服务的核心流程其中涉及整个服务端核心启动流程、RpcInvocation 实现、DubboProtocol.requestHandler 字段调用 Invoker 对象以及序列化相关的优化处理等内容。

View File

@@ -0,0 +1,370 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
25 从 Protocol 起手,看服务暴露和服务引用的全流程(下)
在上一课时,我们以 DubboProtocol 实现为基础,详细介绍了 Dubbo 服务发布的核心流程。在本课时,我们继续介绍 DubboProtocol 中服务引用相关的实现。
refer 流程
下面我们开始介绍 DubboProtocol 中引用服务的相关实现,其核心实现在 protocolBindingRefer() 方法中:
public <T> Invoker<T> protocolBindingRefer(Class<T> serviceType, URL url) throws RpcException {
optimizeSerialization(url); // 进行序列化优化,注册需要优化的类
// 创建DubboInvoker对象
DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
// 将上面创建DubboInvoker对象添加到invoker集合之中
invokers.add(invoker);
return invoker;
}
关于 DubboInvoker 的具体实现我们先暂时不做深入分析。这里我们需要先关注的是getClients() 方法,它创建了底层发送请求和接收响应的 Client 集合,其核心分为了两个部分,一个是针对共享连接的处理,另一个是针对独享连接的处理,具体实现如下:
private ExchangeClient[] getClients(URL url) {
// 是否使用共享连接
boolean useShareConnect = false;
// CONNECTIONS_KEY参数值决定了后续建立连接的数量
int connections = url.getParameter(CONNECTIONS_KEY, 0);
List<ReferenceCountExchangeClient> shareClients = null;
if (connections == 0) { // 如果没有连接数的相关配置,默认使用共享连接的方式
useShareConnect = true;
// 确定建立共享连接的条数,默认只建立一条共享连接
String shareConnectionsStr = url.getParameter(SHARE_CONNECTIONS_KEY, (String) null);
connections = Integer.parseInt(StringUtils.isBlank(shareConnectionsStr) ? ConfigUtils.getProperty(SHARE_CONNECTIONS_KEY,
DEFAULT_SHARE_CONNECTIONS) : shareConnectionsStr);
// 创建公共ExchangeClient集合
shareClients = getSharedClient(url, connections);
}
// 整理要返回的ExchangeClient集合
ExchangeClient[] clients = new ExchangeClient[connections];
for (int i = 0; i < clients.length; i++) {
if (useShareConnect) {
clients[i] = shareClients.get(i);
} else {
// 不使用公共连接的情况下会创建单独的ExchangeClient实例
clients[i] = initClient(url);
}
}
return clients;
}
当使用独享连接的时候对每个 Service 建立固定数量的 Client每个 Client 维护一个底层连接如下图所示就是针对每个 Service 都启动了两个独享连接
Service 独享连接示意图
当使用共享连接的时候会区分不同的网络地址host:port一个地址只建立固定数量的共享连接如下图所示Provider 1 暴露了多个服务Consumer 引用了 Provider 1 中的多个服务共享连接是说 Consumer 调用 Provider 1 中的多个服务时是通过固定数量的共享 TCP 长连接进行数据传输这样就可以达到减少服务端连接数的目的
Service 共享连接示意图
那怎么去创建共享连接呢创建共享连接的实现细节是在 getSharedClient() 方法中它首先从 referenceClientMap 缓存Map`> 类型)中查询 Keyhost 和 port 拼接成的字符串)对应的共享 Client 集合,如果查找到的 Client 集合全部可用,则直接使用这些缓存的 Client否则要创建新的 Client 来补充替换缓存中不可用的 Client。示例代码如下
private List<ReferenceCountExchangeClient> getSharedClient(URL url, int connectNum) {
String key = url.getAddress(); // 获取对端的地址(host:port)
// 从referenceClientMap集合中获取与该地址连接的ReferenceCountExchangeClient集合
List<ReferenceCountExchangeClient> clients = referenceClientMap.get(key);
// checkClientCanUse()方法中会检测clients集合中的客户端是否全部可用
if (checkClientCanUse(clients)) {
batchClientRefIncr(clients); // 客户端全部可用时
return clients;
}
locks.putIfAbsent(key, new Object());
synchronized (locks.get(key)) { // 针对指定地址的客户端进行加锁,分区加锁可以提高并发度
clients = referenceClientMap.get(key);
if (checkClientCanUse(clients)) { // double check再次检测客户端是否全部可用
batchClientRefIncr(clients); // 增加应用Client的次数
return clients;
}
connectNum = Math.max(connectNum, 1); // 至少一个共享连接
// 如果当前Clients集合为空则直接通过initClient()方法初始化所有共享客户端
if (CollectionUtils.isEmpty(clients)) {
clients = buildReferenceCountExchangeClientList(url, connectNum);
referenceClientMap.put(key, clients);
} else { // 如果只有部分共享客户端不可用,则只需要处理这些不可用的客户端
for (int i = 0; i < clients.size(); i++) {
ReferenceCountExchangeClient referenceCountExchangeClient = clients.get(i);
if (referenceCountExchangeClient == null || referenceCountExchangeClient.isClosed()) {
clients.set(i, buildReferenceCountExchangeClient(url));
continue;
}
// 增加引用
referenceCountExchangeClient.incrementAndGetCount();
}
}
// 清理locks集合中的锁对象防止内存泄漏如果key对应的服务宕机或是下线
// 这里不进行清理的话这个用于加锁的Object对象是无法被GC的从而出现内存泄漏
locks.remove(key);
return clients;
}
}
这里使用的 ExchangeClient 实现是 ReferenceCountExchangeClient它是 ExchangeClient 的一个装饰器在原始 ExchangeClient 对象基础上添加了引用计数的功能
ReferenceCountExchangeClient 中除了持有被修饰的 ExchangeClient 对象外还有一个 referenceCount 字段AtomicInteger 类型用于记录该 Client 被应用的次数从下图中我们可以看到 ReferenceCountExchangeClient 的构造方法以及 incrementAndGetCount() 方法中会增加引用次数 close() 方法中则会减少引用次数
referenceCount 修改调用栈
这样对于同一个地址的共享连接就可以满足两个基本需求
当引用次数减到 0 的时候ExchangeClient 连接关闭
当引用次数未减到 0 的时候底层的 ExchangeClient 不能关闭
还有一个需要注意的细节是 ReferenceCountExchangeClient.close() 方法在关闭底层 ExchangeClient 对象之后会立即创建一个 LazyConnectExchangeClient 也有人称其为幽灵连接具体逻辑如下所示这里的 LazyConnectExchangeClient 主要用于异常情况的兜底
public void close(int timeout) {
// 引用次数减到0关闭底层的ExchangeClient具体操作有停掉心跳任务重连任务以及关闭底层Channel这些在前文介绍HeaderExchangeClient的时候已经详细分析过了这里不再赘述
if (referenceCount.decrementAndGet() <= 0) {
if (timeout == 0) {
client.close();
} else {
client.close(timeout);
}
// 创建LazyConnectExchangeClient并将client字段指向该对象
replaceWithLazyClient();
}
}
private void replaceWithLazyClient() {
// 在原有的URL之上添加一些LazyConnectExchangeClient特有的参数
URL lazyUrl = URLBuilder.from(url)
.addParameter(LAZY_CONNECT_INITIAL_STATE_KEY, Boolean.TRUE)
.addParameter(RECONNECT_KEY, Boolean.FALSE)
.addParameter(SEND_RECONNECT_KEY, Boolean.TRUE.toString())
.addParameter("warning", Boolean.TRUE.toString())
.addParameter(LazyConnectExchangeClient.REQUEST_WITH_WARNING_KEY, true)
.addParameter("_client_memo", "referencecounthandler.replacewithlazyclient")
.build();
// 如果当前client字段已经指向了LazyConnectExchangeClient则不需要再次创建LazyConnectExchangeClient兜底了
if (!(client instanceof LazyConnectExchangeClient) || client.isClosed()) {
// ChannelHandler依旧使用原始ExchangeClient使用的Handler即DubboProtocol中的requestHandler字段
client = new LazyConnectExchangeClient(lazyUrl, client.getExchangeHandler());
}
}
LazyConnectExchangeClient 也是 ExchangeClient 的装饰器它会在原有 ExchangeClient 对象的基础上添加懒加载的功能LazyConnectExchangeClient 在构造方法中不会创建底层持有连接的 Client而是在需要发送请求的时候才会调用 initClient() 方法进行 Client 的创建如下图调用关系所示
initClient() 方法的调用位置
initClient() 方法的具体实现如下
private void initClient() throws RemotingException {
if (client != null) { // 底层Client已经初始化过了这里不再初始化
return;
}
connectLock.lock();
try {
if (client != null) { return; } // double check
// 通过Exchangers门面类创建ExchangeClient对象
this.client = Exchangers.connect(url, requestHandler);
} finally {
connectLock.unlock();
}
}
在这些发送请求的方法中除了通过 initClient() 方法初始化底层 ExchangeClient 还会调用warning() 方法其会根据当前 URL 携带的参数决定是否打印 WARN 级别日志为了防止瞬间打印大量日志的情况发生这里有打印的频率限制默认每发送 5000 次请求打印 1 条日志你可以看到在前面展示的兜底场景中我们就开启了打印日志的选项
分析完 getSharedClient() 方法创建共享 Client 的核心流程之后我们回到 DubboProtocol 继续介绍创建独享 Client 的流程
创建独享 Client 的入口在DubboProtocol.initClient() 方法它首先会在 URL 中设置一些默认的参数然后根据 LAZY_CONNECT_KEY 参数决定是否使用 LazyConnectExchangeClient 进行封装实现懒加载功能如下代码所示
private ExchangeClient initClient(URL url) {
// 获取客户端扩展名并进行检查省略检测的逻辑
String str = url.getParameter(CLIENT_KEY, url.getParameter(SERVER_KEY, DEFAULT_REMOTING_CLIENT));
// 设置Codec2的扩展名
url = url.addParameter(CODEC_KEY, DubboCodec.NAME);
// 设置默认的心跳间隔
url = url.addParameterIfAbsent(HEARTBEAT_KEY, String.valueOf(DEFAULT_HEARTBEAT));
ExchangeClient client;
// 如果配置了延迟创建连接的特性则创建LazyConnectExchangeClient
if (url.getParameter(LAZY_CONNECT_KEY, false)) {
client = new LazyConnectExchangeClient(url, requestHandler);
} else { // 未使用延迟连接功能则直接创建HeaderExchangeClient
client = Exchangers.connect(url, requestHandler);
}
return client;
}
这里涉及的 LazyConnectExchangeClient 装饰器以及 Exchangers 门面类在前面已经深入分析过了就不再赘述了
DubboProtocol 中还剩下几个方法没有介绍这里你只需要简单了解一下它们的实现即可
batchClientRefIncr() 方法会遍历传入的集合将其中的每个 ReferenceCountExchangeClient 对象的引用加一
buildReferenceCountExchangeClient() 方法会调用上面介绍的 initClient() 创建 Client 对象然后再包装一层 ReferenceCountExchangeClient 进行修饰最后返回该方法主要用于创建共享 Client
destroy方法
DubboProtocol 销毁的时候会调用 destroy() 方法释放底层资源其中就涉及 export 流程中创建的 ProtocolServer 对象以及 refer 流程中创建的 Client
DubboProtocol.destroy() 方法首先会逐个关闭 serverMap 集合中的 ProtocolServer 对象相关代码片段如下
for (String key : new ArrayList<>(serverMap.keySet())) {
ProtocolServer protocolServer = serverMap.remove(key);
if (protocolServer == null) { continue;}
RemotingServer server = protocolServer.getRemotingServer();
// 在close()方法中发送ReadOnly请求、阻塞指定时间、关闭底层的定时任务、关闭相关线程池最终会断开所有连接关闭Server。这些逻辑在前文介绍HeaderExchangeServer、NettyServer等实现的时候已经详细分析过了这里不再展开
server.close(ConfigurationUtils.getServerShutdownTimeout());
}
ConfigurationUtils.getServerShutdownTimeout() 方法返回的阻塞时长默认是 10 秒,我们可以通过 dubbo.service.shutdown.wait 或是 dubbo.service.shutdown.wait.seconds 进行配置。
之后DubboProtocol.destroy() 方法会逐个关闭 referenceClientMap 集合中的 Client逻辑与上述关闭ProtocolServer的逻辑相同这里不再重复。只不过需要注意前面我们提到的 ReferenceCountExchangeClient 的存在,只有引用减到 0底层的 Client 才会真正销毁。
最后DubboProtocol.destroy() 方法会调用父类 AbstractProtocol 的 destroy() 方法,销毁全部 Invoker 对象,前面已经介绍过 AbstractProtocol.destroy() 方法的实现,这里也不再重复。
总结
本课时我们继续上一课时的话题,以 DubboProtocol 为例,介绍了 Dubbo 在 Protocol 层实现服务引用的核心流程。我们首先介绍了 DubboProtocol 初始化 Client 的核心逻辑分析了共享连接和独立连接的模型后续还讲解了ReferenceCountExchangeClient、LazyConnectExchangeClient 等装饰器的功能和实现,最后说明了 destroy() 方法释放底层资源的相关实现。
关于 DubboProtocol你若还有什么疑问或想法欢迎你留言跟我分享。下一课时我们将开始深入介绍 Dubbo 的“心脏”—— Invoker 接口的相关实现,这是我们的一篇加餐文章,记得按时来听课。

View File

@@ -0,0 +1,381 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
26 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker
在前面课时介绍 DubboProtocol 的时候我们看到,上层业务 Bean 会被封装成 Invoker 对象,然后传入 DubboProtocol.export() 方法中,该 Invoker 被封装成 DubboExporter并保存到 exporterMap 集合中缓存。
在 DubboProtocol 暴露的 ProtocolServer 收到请求时,经过一系列解码处理,最终会到达 DubboProtocol.requestHandler 这个 ExchangeHandler 对象中,该 ExchangeHandler 对象会从 exporterMap 集合中取出请求的 Invoker并调用其 invoke() 方法处理请求。
DubboProtocol.protocolBindingRefer() 方法则会将底层的 ExchangeClient 集合封装成 DubboInvoker然后由上层逻辑封装成代理对象这样业务层就可以像调用本地 Bean 一样,完成远程调用。
深入 Invoker
首先,我们来看 AbstractInvoker 这个抽象类,它继承了 Invoker 接口,继承关系如下图所示:
AbstractInvoker 继承关系示意图
从图中可以看到,最核心的 DubboInvoker 继承自AbstractInvoker 抽象类AbstractInvoker 的核心字段有如下几个。
typeClass<T> 类型):该 Invoker 对象封装的业务接口类型,例如 Demo 示例中的 DemoService 接口。
urlURL 类型):与当前 Invoker 关联的 URL 对象,其中包含了全部的配置信息。
attachmentMap 类型):当前 Invoker 关联的一些附加信息,这些附加信息可以来自关联的 URL。在 AbstractInvoker 的构造函数的某个重载中,会调用 convertAttachment() 方法,其中就会从关联的 URL 对象获取指定的 KV 值记录到 attachment 集合中。
availablevolatile boolean类型、destroyedAtomicBoolean 类型):这两个字段用来控制当前 Invoker 的状态。available 默认值为 truedestroyed 默认值为 false。在 destroy() 方法中会将 available 设置为 false将 destroyed 字段设置为 true。
在 AbstractInvoker 中实现了 Invoker 接口中的 invoke() 方法,这里有点模板方法模式的感觉,其中先对 URL 中的配置信息以及 RpcContext 中携带的附加信息进行处理,添加到 Invocation 中作为附加信息,然后调用 doInvoke() 方法发起远程调用(该方法由 AbstractInvoker 的子类具体实现),最后得到 AsyncRpcResult 对象返回。
public Result invoke(Invocation inv) throws RpcException {
// 首先将传入的Invocation转换为RpcInvocation
RpcInvocation invocation = (RpcInvocation) inv;
invocation.setInvoker(this);
// 将前文介绍的attachment集合添加为Invocation的附加信息
if (CollectionUtils.isNotEmptyMap(attachment)) {
invocation.addObjectAttachmentsIfAbsent(attachment);
}
// 将RpcContext的附加信息添加为Invocation的附加信息
Map<String, Object> contextAttachments = RpcContext.getContext().getObjectAttachments();
if (CollectionUtils.isNotEmptyMap(contextAttachments)) {
invocation.addObjectAttachments(contextAttachments);
}
// 设置此次调用的模式,异步还是同步
invocation.setInvokeMode(RpcUtils.getInvokeMode(url, invocation));
// 如果是异步调用给这次调用添加一个唯一ID
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
AsyncRpcResult asyncResult;
try { // 调用子类实现的doInvoke()方法
asyncResult = (AsyncRpcResult) doInvoke(invocation);
} catch (InvocationTargetException e) {// 省略异常处理的逻辑
} catch (RpcException e) { // 省略异常处理的逻辑
} catch (Throwable e) {
asyncResult = AsyncRpcResult.newDefaultAsyncResult(null, e, invocation);
}
RpcContext.getContext().setFuture(new FutureAdapter(asyncResult.getResponseFuture()));
return asyncResult;
}
接下来,需要深入介绍的第一个类是 RpcContext。
RpcContext
RpcContext 是线程级别的上下文信息,每个线程绑定一个 RpcContext 对象,底层依赖 ThreadLocal 实现。RpcContext 主要用于存储一个线程中一次请求的临时状态当线程处理新的请求Provider 端或是线程发起新的请求Consumer 端RpcContext 中存储的内容就会更新。
下面来看 RpcContext 中两个InternalThreadLocal的核心字段这两个字段的定义如下所示
// 在发起请求时会使用该RpcContext来存储上下文信息
private static final InternalThreadLocal<RpcContext> LOCAL = new InternalThreadLocal<RpcContext>() {
@Override
protected RpcContext initialValue() {
return new RpcContext();
}
};
// 在接收到响应的时候会使用该RpcContext来存储上下文信息
private static final InternalThreadLocal<RpcContext> SERVER_LOCAL = ...
JDK 提供的 ThreadLocal 底层实现大致如下:对于不同线程创建对应的 ThreadLocalMap用于存放线程绑定信息当用户调用ThreadLocal.get() 方法获取变量时,底层会先获取当前线程 Thread然后获取绑定到当前线程 Thread 的 ThreadLocalMap最后将当前 ThreadLocal 对象作为 Key 去 ThreadLocalMap 表中获取线程绑定的数据。ThreadLocal.set() 方法的逻辑与之类似,首先会获取绑定到当前线程的 ThreadLocalMap然后将 ThreadLocal 实例作为 Key、待存储的数据作为 Value 存储到 ThreadLocalMap 中。
Dubbo 的 InternalThreadLocal 与 JDK 提供的 ThreadLocal 功能类似,只是底层实现略有不同,其底层的 InternalThreadLocalMap 采用数组结构存储数据,直接通过 index 获取变量,相较于 Map 方式计算 hash 值的性能更好。
这里我们来介绍一下 dubbo-common 模块中的 InternalThread 这个类,它继承了 Thread 类Dubbo 的线程工厂 NamedInternalThreadFactory 创建的线程类其实都是 InternalThread 实例对象,你可以回顾前面第 19 课时介绍的 ThreadPool 接口实现,它们都是通过 NamedInternalThreadFactory 这个工厂类来创建线程的。
InternalThread 中主要提供了 setThreadLocalMap() 和 threadLocalMap() 两个方法,用于设置和获取 InternalThreadLocalMap。InternalThreadLocalMap 中的核心字段有如下四个。
indexedVariablesObject[] 类型):用于存储绑定到当前线程的数据。
NEXT_INDEXAtomicInteger 类型):自增索引,用于计算下次存储到 indexedVariables 数组中的位置,这是一个静态字段。
slowThreadLocalMapThreadLocal<InternalThreadLocalMap> 类型):当使用原生 Thread 的时候,会使用该 ThreadLocal 存储 InternalThreadLocalMap这是一个降级策略。
UNSETObject 类型):当一个与线程绑定的值被删除之后,会被设置为 UNSET 值。
在 InternalThreadLocalMap 中获取当前线程绑定的InternalThreadLocaMap的静态方法都会与 slowThreadLocalMap 字段配合实现降级,也就是说,如果当前线程为原生 Thread 类型,则根据 slowThreadLocalMap 获取InternalThreadLocalMap。这里我们以 getIfSet() 方法为例:
public static InternalThreadLocalMap getIfSet() {
Thread thread = Thread.currentThread(); // 获取当前线程
if (thread instanceof InternalThread) { // 判断当前线程的类型
// 如果是InternalThread类型直接获取InternalThreadLocalMap返回
return ((InternalThread) thread).threadLocalMap();
}
// 原生Thread则需要通过ThreadLocal获取InternalThreadLocalMap
return slowThreadLocalMap.get();
}
InternalThreadLocalMap 中的 get()、remove()、set() 等方法都有类似的降级操作,这里不再一一重复。
在拿到 InternalThreadLocalMap 对象之后,我们就可以调用其 setIndexedVariable() 方法和 indexedVariable() 方法读写这里我们得结合InternalThreadLocal进行讲解。在 InternalThreadLocal 的构造方法中,会使用 InternalThreadLocalMap.NEXT_INDEX 初始化其 index 字段int 类型),在 InternalThreadLocal.set() 方法中就会将传入的数据存储到 InternalThreadLocalMap.indexedVariables 集合中,具体的下标位置就是这里的 index 字段值:
public final void set(V value) {
if (value == null|| value == InternalThreadLocalMap.UNSET{
remove(); // 如果要存储的值为null或是UNSERT则直接清除
} else {
// 获取当前线程绑定的InternalThreadLocalMap
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
// 将value存储到InternalThreadLocalMap.indexedVariables集合中
if (threadLocalMap.setIndexedVariable(index, value)) {
// 将当前InternalThreadLocal记录到待删除集合中
addToVariablesToRemove(threadLocalMap, this);
}
}
}
InternalThreadLocal 的静态变量 VARIABLES_TO_REMOVE_INDEX 是调用InternalThreadLocalMap 的 nextVariableIndex 方法得到的一个索引值,在 InternalThreadLocalMap 数组的对应位置保存的是 Set<InternalThreadLocal> 类型的集合,也就是上面提到的“待删除集合”,即绑定到当前线程所有的 InternalThreadLocal这样就可以方便管理对象及内存的释放。
接下来我们继续看 InternalThreadLocalMap.setIndexedVariable() 方法的实现:
public boolean setIndexedVariable(int index, Object value) {
Object[] lookup = indexedVariables;
if (index < lookup.length) { // 将value存储到index指定的位置
Object oldValue = lookup[index];
lookup[index] = value;
return oldValue == UNSET;
} else {
// 当index超过indexedVariables数组的长度时需要对indexedVariables数组进行扩容
expandIndexedVariableTableAndSet(index, value);
return true;
}
}
明确了设置 InternalThreadLocal 变量的流程之后我们再来分析读取 InternalThreadLocal 变量的流程入口在 InternalThreadLocal get() 方法
public final V get() {
// 获取当前线程绑定的InternalThreadLocalMap
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
// 根据当前InternalThreadLocal对象的index字段从InternalThreadLocalMap中读取相应的数据
Object v = threadLocalMap.indexedVariable(index);
if (v != InternalThreadLocalMap.UNSET) {
return (V) v; // 如果非UNSET则表示读取到了有效数据直接返回
}
// 读取到UNSET值则会调用initialize()方法进行初始化其中首先会调用initialValue()方法进行初始化然后会调用前面介绍的setIndexedVariable()方法和addToVariablesToRemove()方法存储初始化得到的值
return initialize(threadLocalMap);
}
我们可以看到 RpcContext LOCAL SERVER_LOCAL 两个 InternalThreadLocal 类型的字段都实现了 initialValue() 方法它们的实现都是创建并返回 RpcContext 对象
理解了 InternalThreadLocal 的底层原理之后我们回到 RpcContext 继续分析RpcContext 作为调用的上下文信息可以记录非常多的信息下面介绍其中的一些核心字段
attachmentsMap 类型可用于记录调用上下文的附加信息这些信息会被添加到 Invocation 并传递到远端节点
valuesMap 类型用来记录上下文的键值对信息但是不会被传递到远端节点
methodNameparameterTypesarguments分别用来记录调用的方法名参数类型列表以及具体的参数列表与相关 Invocation 对象中的信息一致
localAddressremoteAddressInetSocketAddress 类型记录了自己和远端的地址
requestresponseObject 类型可用于记录底层关联的请求和响应
asyncContextAsyncContext 类型异步Context其中可以存储异步调用相关的 RpcContext 以及异步请求相关的 Future
DubboInvoker
通过前面对 DubboProtocol 的分析我们知道protocolBindingRefer() 方法会根据调用的业务接口类型以及 URL 创建底层的 ExchangeClient 集合然后封装成 DubboInvoker 对象返回DubboInvoker AbstractInvoker 的实现类在其 doInvoke() 方法中首先会选择此次调用使用 ExchangeClient 对象然后确定此次调用是否需要返回值最后调用 ExchangeClient.request() 方法发送请求对返回的 Future 进行简单封装并返回
protected Result doInvoke(final Invocation invocation) throws Throwable {
RpcInvocation inv = (RpcInvocation) invocation;
// 此次调用的方法名称
final String methodName = RpcUtils.getMethodName(invocation);
// 向Invocation中添加附加信息这里将URL的path和version添加到附加信息中
inv.setAttachment(PATH_KEY, getUrl().getPath());
inv.setAttachment(VERSION_KEY, version);
ExchangeClient currentClient; // 选择一个ExchangeClient实例
if (clients.length == 1) {
currentClient = clients[0];
} else {
currentClient = clients[index.getAndIncrement() % clients.length];
}
boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
// 根据调用的方法名称和配置计算此次调用的超时时间
int timeout = calculateTimeout(invocation, methodName);
if (isOneway) { // 不需要关注返回值的请求
boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
currentClient.send(inv, isSent);
return AsyncRpcResult.newDefaultAsyncResult(invocation);
} else { // 需要关注返回值的请求
// 获取处理响应的线程池对于同步请求会使用ThreadlessExecutorThreadlessExecutor的原理前面已经分析过了这里不再赘述对于异步请求则会使用共享的线程池ExecutorRepository接口的相关设计和实现在前面已经详细分析过了这里不再重复
ExecutorService executor = getCallbackExecutor(getUrl(), inv);
// 使用上面选出的ExchangeClient执行request()方法将请求发送出去
CompletableFuture<AppResponse> appResponseFuture =
currentClient.request(inv, timeout, executor).thenApply(obj -> (AppResponse) obj);
// 这里将AppResponse封装成AsyncRpcResult返回
AsyncRpcResult result = new AsyncRpcResult(appResponseFuture, inv);
result.setExecutor(executor);
return result;
}
}
在 DubboInvoker.invoke() 方法中有一些细节需要关注一下。首先是根据 URL 以及 Invocation 中的配置决定此次调用是否为oneway 调用方式。
public static boolean isOneway(URL url, Invocation inv) {
boolean isOneway;
if (Boolean.FALSE.toString().equals(inv.getAttachment(RETURN_KEY))) {
isOneway = true; // 首先关注的是Invocation中"return"这个附加属性
} else {
isOneway = !url.getMethodParameter(getMethodName(inv), RETURN_KEY, true); // 之后关注URL中调用方法对应的"return"配置
}
return isOneway;
}
oneway 指的是客户端发送消息后,不需要得到响应。所以,对于那些不关心服务端响应的请求,就比较适合使用 oneway 通信,如下图所示:
oneway 和 twoway 通信方式对比图
可以看到发送 oneway 请求的方式是send() 方法,而后面发送 twoway 请求的方式是 request() 方法。通过之前的分析我们知道request() 方法会相应地创建 DefaultFuture 对象以及检测超时的定时任务,而 send() 方法则不会创建这些东西,它是直接将 Invocation 包装成 oneway 类型的 Request 发送出去。
在服务端的 HeaderExchangeHandler.receive() 方法中,会针对 oneway 请求和 twoway 请求执行不同的分支处理twoway 请求由 handleRequest() 方法进行处理,其中会关注调用结果并形成 Response 返回给客户端oneway 请求则直接交给上层的 DubboProtocol.requestHandler完成方法调用之后不会返回任何 Response。
我们就结合如下示例代码来简单说明一下 HeaderExchangeHandler.request() 方法中的相关片段。
public void received(Channel channel, Object message) throws RemotingException {
final ExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel);
if (message instanceof Request) {
if (request.isTwoWay()) {
handleRequest(exchangeChannel, request);
} else {
handler.received(exchangeChannel, request.getData());
}
} else ... // 省略其他分支的展示
}
总结
本课时我们重点介绍了 Dubbo 最核心的接口—— Invoker。首先我们介绍了 AbstractInvoker 抽象类提供的公共能力;然后分析了 RpcContext 的功能和涉及的组件例如InternalThreadLocal、InternalThreadLocalMap 等;最后我们说明了 DubboInvoker 对 doinvoke() 方法的实现,并区分了 oneway 和 twoway 两种类型的请求。
下一课时,我们将继续介绍 DubboInvoker 的实现。

View File

@@ -0,0 +1,505 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
27 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker
关于 DubboInvoker在发送完oneway 请求之后,会立即创建一个已完成状态的 AsyncRpcResult 对象(主要是其中的 responseFuture 是已完成状态)。这在上一课时我们已经讲解过了。
本课时我们将继续介绍 DubboInvoker 处理 twoway 请求和响应的相关实现,其中会涉及响应解码、同步/异步响应等相关内容;完成对 DubboInvoker 的分析之后,我们还会介绍 Dubbo 中与 Listener、Filter 相关的 Invoker 装饰器。
再探 DubboInvoker
那 DubboInvoker 对twoway 请求的处理又是怎样的呢接下来我们就来重点介绍下。首先DubboInvoker 会调用 getCallbackExecutor() 方法,根据不同的 InvokeMode 返回不同的线程池实现,代码如下:
protected ExecutorService getCallbackExecutor(URL url, Invocation inv) {
ExecutorService sharedExecutor = ExtensionLoader.getExtensionLoader(ExecutorRepository.class).getDefaultExtension().getExecutor(url);
if (InvokeMode.SYNC == RpcUtils.getInvokeMode(getUrl(), inv)) {
return new ThreadlessExecutor(sharedExecutor);
} else {
return sharedExecutor;
}
}
InvokeMode 有三个可选值,分别是 SYNC、ASYNC 和 FUTURE。这里对于 SYNC 模式返回的线程池是 ThreadlessExecutor至于其他两种异步模式会根据 URL 选择对应的共享线程池。
SYNC 表示同步模式,是 Dubbo 的默认调用模式,具体含义如下图所示,客户端发送请求之后,客户端线程会阻塞等待服务端返回响应。
SYNC 调用模式图
在拿到线程池之后DubboInvoker 就会调用 ExchangeClient.request() 方法,将 Invocation 包装成 Request 请求发送出去,同时会创建相应的 DefaultFuture 返回。注意,这里还加了一个回调,取出其中的 AppResponse 对象。AppResponse 表示的是服务端返回的具体响应,其中有三个字段。
resultObject 类型):响应结果,也就是服务端返回的结果值,注意,这是一个业务上的结果值。例如,在我们前面第 01 课时的 Demo 示例(即 dubbo-demo 模块中的 DemoProvider 端 DemoServiceImpl 返回的 “Hello Dubbo xxx” 这一串字符串。
exceptionThrowable 类型):服务端返回的异常信息。
attachmentsMap 类型):服务端返回的附加信息。
这里请求返回的 AppResponse 你可能不太熟悉,但是其子类 DecodeableRpcResult 你可能就有点眼熟了DecodeableRpcResult 表示的是一个响应,与其对应的是 DecodeableRpcInvocation它表示的是请求。在第 24 课时介绍 DubboCodec 对 Dubbo 请求体的编码流程中,我们已经详细介绍过 DecodeableRpcInvocation 了,你可以回顾一下 DubboCodec 的 decodeBody() 方法,就会发现 DecodeableRpcResult 的“身影”。
1. DecodeableRpcResult
DecodeableRpcResult 解码核心流程大致如下:
首先,确定当前使用的序列化方式,并对字节流进行解码。
然后,读取一个 byte 的标志位,其可选值有六种枚举,下面我们就以其中的 RESPONSE_VALUE_WITH_ATTACHMENTS 为例进行分析。
标志位为 RESPONSE_VALUE_WITH_ATTACHMENTS 时,会先通过 handleValue() 方法处理返回值,其中会根据 RpcInvocation 中记录的返回值类型读取返回值,并设置到 result 字段。
最后,再通过 handleAttachment() 方法读取返回的附加信息,并设置到 DecodeableRpcResult 的 attachments 字段中。
public Object decode(Channel channel, InputStream input) throws IOException {
// 反序列化
ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType)
.deserialize(channel.getUrl(), input);
byte flag = in.readByte(); // 读取一个byte的标志位
// 根据标志位判断当前结果中包含的信息,并调用不同的方法进行处理
switch (flag) {
case DubboCodec.RESPONSE_NULL_VALUE:
break;
case DubboCodec.RESPONSE_VALUE:
handleValue(in);
break;
case DubboCodec.RESPONSE_WITH_EXCEPTION:
handleException(in);
break;
case DubboCodec.RESPONSE_NULL_VALUE_WITH_ATTACHMENTS:
handleAttachment(in);
break;
case DubboCodec.RESPONSE_VALUE_WITH_ATTACHMENTS:
handleValue(in);
handleAttachment(in);
break;
case DubboCodec.RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS:
default:
throw new IOException("..." );
}
if (in instanceof Cleanable) {
((Cleanable) in).cleanup();
}
return this;
}
decode() 方法中其他分支的代码这里就不再展示了,你若感兴趣的话可以参考 DecodeableRpcResult 源码进行分析。
2. AsyncRpcResult
在 DubboInvoker 中还有一个 AsyncRpcResult 类,它表示的是一个异步的、未完成的 RPC 调用,其中会记录对应 RPC 调用的信息(例如,关联的 RpcContext 和 Invocation 对象),包括以下几个核心字段。
responseFutureCompletableFuture<AppResponse> 类型):这个 responseFuture 字段与前文提到的 DefaultFuture 有紧密的联系,是 DefaultFuture 回调链上的一个 Future。后面 AsyncRpcResult 之上添加的回调,实际上都是添加到这个 Future 之上。
storedContext、storedServerContextRpcContext 类型):用于存储相关的 RpcContext 对象。我们知道 RpcContext 是与线程绑定的,而真正执行 AsyncRpcResult 上添加的回调方法的线程可能先后处理过多个不同的 AsyncRpcResult所以我们需要传递并保存当前的 RpcContext。
executorExecutor 类型):此次 RPC 调用关联的线程池。
invocationInvocation 类型):此次 RPC 调用关联的 Invocation 对象。
在 AsyncRpcResult 构造方法中,除了接收发送请求返回的 CompletableFuture<AppResponse> 对象,还会将当前的 RpcContext 保存到 storedContext 和 storedServerContext 中,具体实现如下:
public AsyncRpcResult(CompletableFuture<AppResponse> future, Invocation invocation) {
this.responseFuture = future;
this.invocation = invocation;
this.storedContext = RpcContext.getContext();
this.storedServerContext = RpcContext.getServerContext();
}
通过 whenCompleteWithContext() 方法,我们可以为 AsyncRpcResult 添加回调方法,而这个回调方法会被包装一层并注册到 responseFuture 上,具体实现如下:
public Result whenCompleteWithContext(BiConsumer<Result, Throwable> fn) {
// 在responseFuture之上注册回调
this.responseFuture = this.responseFuture.whenComplete((v, t) -> {
beforeContext.accept(v, t);
fn.accept(v, t);
afterContext.accept(v, t);
});
return this;
}
这里的 beforeContext 首先会将当前线程的 RpcContext 记录到 tmpContext 中,然后将构造函数中存储的 RpcContext 设置到当前线程中,为后面的回调执行做准备;而 afterContext 则会恢复线程原有的 RpcContext。具体实现如下
private RpcContext tmpContext;
private RpcContext tmpServerContext;
private BiConsumer<Result, Throwable> beforeContext = (appResponse, t) -> {
// 将当前线程的 RpcContext 记录到 tmpContext 中
tmpContext = RpcContext.getContext();
tmpServerContext = RpcContext.getServerContext();
// 将构造函数中存储的 RpcContext 设置到当前线程中
RpcContext.restoreContext(storedContext);
RpcContext.restoreServerContext(storedServerContext);
};
private BiConsumer<Result, Throwable> afterContext = (appResponse, t) -> {
// 将tmpContext中存储的RpcContext恢复到当前线程绑定的RpcContext
RpcContext.restoreContext(tmpContext);
RpcContext.restoreServerContext(tmpServerContext);
};
这样AsyncRpcResult 就可以处于不断地添加回调而不丢失 RpcContext 的状态。总之AsyncRpcResult 整个就是为异步请求设计的。
在前面的分析中我们看到RpcInvocation.InvokeMode 字段中可以指定调用为 SYNC 模式,也就是同步调用模式,那 AsyncRpcResult 这种异步设计是如何支持同步调用的呢? 在 AbstractProtocol.refer() 方法中Dubbo 会将 DubboProtocol.protocolBindingRefer() 方法返回的 Invoker 对象(即 DubboInvoker 对象)用 AsyncToSyncInvoker 封装一层。
AsyncToSyncInvoker 是 Invoker 的装饰器,负责将异步调用转换成同步调用,其 invoke() 方法的核心实现如下:
public Result invoke(Invocation invocation) throws RpcException {
Result asyncResult = invoker.invoke(invocation);
if (InvokeMode.SYNC == ((RpcInvocation) invocation).getInvokeMode()) {
// 调用get()方法,阻塞等待响应返回
asyncResult.get(Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
}
return asyncResult;
}
其实 AsyncRpcResult.get() 方法底层调用的就是 responseFuture 字段的 get() 方法,对于同步请求来说,会先调用 ThreadlessExecutor.waitAndDrain() 方法阻塞等待响应返回,具体实现如下所示:
public Result get() throws InterruptedException, ExecutionException {
if (executor != null && executor instanceof ThreadlessExecutor) {
// 针对ThreadlessExecutor的特殊处理这里调用waitAndDrain()等待响应
ThreadlessExecutor threadlessExecutor = (ThreadlessExecutor) executor;
threadlessExecutor.waitAndDrain();
}
// 非ThreadlessExecutor线程池的场景中则直接调用Future(最底层是DefaultFuture)的get()方法阻塞
return responseFuture.get();
}
ThreadlessExecutor 针对同步请求的优化,我们在前面的第 20 课时已经详细介绍过了,这里不再重复。
最后要说明的是AsyncRpcResult 实现了 Result 接口,如下图所示:
AsyncRpcResult 继承关系图
AsyncRpcResult 对 Result 接口的实现例如getValue() 方法、recreate() 方法、getAttachments() 方法等,都会先调用 getAppResponse() 方法从 responseFuture 中拿到 AppResponse 对象,然后再调用其对应的方法。这里我们以 recreate() 方法为例,简单分析一下:
public Result getAppResponse() { // 省略异常处理的逻辑
if (responseFuture.isDone()) { // 检测responseFuture是否已完成
return responseFuture.get(); // 获取AppResponse
}
// 根据调用方法的返回值,生成默认值
return createDefaultValue(invocation);
}
public Object recreate() throws Throwable {
RpcInvocation rpcInvocation = (RpcInvocation) invocation;
if (InvokeMode.FUTURE == rpcInvocation.getInvokeMode()) {
return RpcContext.getContext().getFuture();
}
// 调用AppResponse.recreate()方法
return getAppResponse().recreate();
}
AppResponse.recreate() 方法实现比较简单,如下所示:
public Object recreate() throws Throwable {
if (exception != null) { // 存在异常则直接抛出异常
// 省略处理堆栈信息的逻辑
throw exception;
}
return result; // 正常返回无异常时直接返回result
}
这里我们注意到,在 recreate() 方法中AsyncRpcResult 会对 FUTURE 特殊处理。如果服务接口定义的返回参数是 CompletableFuture则属于 FUTURE 模式FUTURE 模式也属于 Dubbo 提供的一种异步调用方式只不过是服务端异步。FUTURE 模式下拿到的 CompletableFuture 对象其实是在 AbstractInvoker 中塞到 RpcContext 中的,在 AbstractInvoker.invoke() 方法中有这么一段代码:
RpcContext.getContext().setFuture(
new FutureAdapter(asyncResult.getResponseFuture()));
这里拿到的其实就是 AsyncRpcResult 中 responseFuture即前面介绍的 DefaultFuture。可见无论是 SYNC 模式、ASYNC 模式还是 FUTURE 模式,都是围绕 DefaultFuture 展开的。
其实,在 Dubbo 2.6.x 及之前的版本提供了一定的异步编程能力,但其异步方式存在如下一些问题:
Future 获取方式不够直接,业务需要从 RpcContext 中手动获取。
Future 接口无法实现自动回调,而自定义 ResponseFuture这是 Dubbo 2.6.x 中类)虽支持回调,但支持的异步场景有限,并且还不支持 Future 间的相互协调或组合等。
不支持 Provider 端异步。
Dubbo 2.6.x 及之前版本中使用的 Future 是在 Java 5 中引入的,所以存在以上一些功能设计上的问题;而在 Java 8 中引入的 CompletableFuture 进一步丰富了 Future 接口很好地解决了这些问题。Dubbo 在 2.7.0 版本已经升级了对 Java 8 的支持,同时基于 CompletableFuture 对当前的异步功能进行了增强,弥补了上述不足。
因为 CompletableFuture 实现了 CompletionStage 和 Future 接口,所以它还是可以像以前一样通过 get() 阻塞或者 isDone() 方法轮询的方式获得结果,这就保证了同步调用依旧可用。当然,在实际工作中,不是很建议用 get() 这样阻塞的方式来获取结果,因为这样就丢失了异步操作带来的性能提升。
另外CompletableFuture 提供了良好的回调方法例如whenComplete()、whenCompleteAsync() 等方法都可以在逻辑完成后,执行该方法中添加的 action 逻辑实现回调的逻辑。同时CompletableFuture 很好地支持了 Future 间的相互协调或组合例如thenApply()、thenApplyAsync() 等方法。
正是由于 CompletableFuture 的增强,我们可以更加流畅地使用回调,不必因为等待一个响应而阻塞着调用线程,而是通过前面介绍的方法告诉 CompletableFuture 完成当前逻辑之后,就去执行某个特定的函数。在 Demo 示例(即 dubbo-demo 模块中的 Demo )中,返回 CompletableFuture 的 sayHelloAsync() 方法就是使用的 FUTURE 模式。
好了DubboInvoker 涉及的同步调用、异步调用的原理和底层实现就介绍到这里了,我们可以通过一张流程图进行简单总结,如下所示:
DubboInvoker 核心流程图
在 Client 端发送请求时,首先会创建对应的 DefaultFuture其中记录了请求 ID 等信息),然后依赖 Netty 的异步发送特性将请求发送到 Server 端。需要说明的是,这整个发送过程是不会阻塞任何线程的。之后,将 DefaultFuture 返回给上层在这个返回过程中DefaultFuture 会被封装成 AsyncRpcResult同时也可以添加回调函数。
当 Client 端接收到响应结果的时候会交给关联的线程池ExecutorService或是业务线程使用 ThreadlessExecutor 场景)进行处理,得到 Server 返回的真正结果。拿到真正的返回结果后,会将其设置到 DefaultFuture 中,并调用 complete() 方法将其设置为完成状态。此时,就会触发前面注册在 DefaulFuture 上的回调函数,执行回调逻辑。
Invoker 装饰器
除了上面介绍的 DubboInvoker 实现之外Invoker 接口还有很多装饰器实现,这里重点介绍 Listener、Filter 相关的 Invoker 实现。
1. ListenerInvokerWrapper
在前面的第 23 课时中简单提到过 InvokerListener 接口,我们可以提供其实现来监听 refer 事件以及 destroy 事件,相应地要实现 referred() 方法以及 destroyed() 方法。
ProtocolListenerWrapper 是 Protocol 接口的实现之一,如下图所示:
ProtocolListenerWrapper 继承关系图
ProtocolListenerWrapper 本身是 Protocol 接口的装饰器,在其 export() 方法和 refer() 方法中,会分别在原有 Invoker 基础上封装一层 ListenerExporterWrapper 和 ListenerInvokerWrapper。
ListenerInvokerWrapper 是 Invoker 的装饰器,其构造方法参数列表中除了被修饰的 Invoker 外,还有 InvokerListener 列表,在构造方法内部会遍历整个 InvokerListener 列表,并调用每个 InvokerListener 的 referred() 方法,通知它们 Invoker 被引用的事件。核心逻辑如下:
public ListenerInvokerWrapper(Invoker<T> invoker, List<InvokerListener> listeners) {
this.invoker = invoker; // 底层被修饰的Invoker对象
this.listeners = listeners; // 监听器集合
if (CollectionUtils.isNotEmpty(listeners)) {
for (InvokerListener listener : listeners) {
if (listener != null) {// 在服务引用过程中触发全部InvokerListener监听器
listener.referred(invoker);
}
}
}
}
在 ListenerInvokerWrapper.destroy() 方法中,首先会调用被修饰 Invoker 对象的 destroy() 方法,之后循环调用全部 InvokerListener 的 destroyed() 方法,通知它们该 Invoker 被销毁的事件,具体实现比较简单,这里就不再展示,你若感兴趣的话可以参考源码进行学习。
与 InvokerListener 对应的是 ExporterListener 监听器,其实现类可以通过实现 exported() 方法和 unexported() 方法监听服务暴露事件以及取消暴露事件。
相应地,在 ProtocolListenerWrapper 的 export() 方法中也会在原有 Invoker 之上用 ListenerExporterWrapper 进行一层封装ListenerExporterWrapper 的构造方法中会循环调用全部 ExporterListener 的 exported() 方法,通知其服务暴露的事件,核心逻辑如下所示:
public ListenerExporterWrapper(Exporter<T> exporter, List<ExporterListener> listeners) {
this.exporter = exporter;
this.listeners = listeners;
if (CollectionUtils.isNotEmpty(listeners)) {
RuntimeException exception = null;
for (ExporterListener listener : listeners) {
if (listener != null) {
listener.exported(this);
}
}
}
}
ListenerExporterWrapper.unexported() 方法的逻辑与上述 exported() 方法的实现基本类似,这里不再赘述。
这里介绍的 ListenerInvokerWrapper 和 ListenerExporterWrapper 都是被 @SPI 注解修饰的,我们可以提供相应的扩展实现,然后配置 SPI 文件监听这些事件。
2. Filter 相关的 Invoker 装饰器
Filter 接口是 Dubbo 为用户提供的一个非常重要的扩展接口,将各个 Filter 串联成 Filter 链并与 Invoker 实例相关。构造 Filter 链的核心逻辑位于 ProtocolFilterWrapper.buildInvokerChain() 方法中ProtocolFilterWrapper 的 refer() 方法和 export() 方法都会调用该方法。
buildInvokerChain() 方法的核心逻辑如下:
首先会根据 URL 中携带的配置信息,确定当前激活的 Filter 扩展实现有哪些,形成 Filter 集合。
遍历 Filter 集合,将每个 Filter 实现封装成一个匿名 Invoker在这个匿名 Invoker 中,会调用 Filter 的 invoke() 方法执行 Filter 的逻辑,然后由 Filter 内部的逻辑决定是否将调用传递到下一个 Filter 执行。
buildInvokerChain() 方法的具体实现如下:
private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
Invoker<T> last = invoker;
// 根据 URL 中携带的配置信息,确定当前激活的 Filter 扩展实现有哪些,形成 Filter 集合
List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
if (!filters.isEmpty()) {
for (int i = filters.size() - 1; i >= 0; i--) {
final Filter filter = filters.get(i);
final Invoker<T> next = last;
// 遍历 Filter 集合,将每个 Filter 实现封装成一个匿名 Invoker
last = new Invoker<T>() {
@Override
public Result invoke(Invocation invocation) throws RpcException {
Result asyncResult;
try {
// 调用 Filter 的 invoke() 方法执行 Filter 的逻辑,然后由 Filter 内部的逻辑决定是否将调用传递到下一个 Filter 执行
asyncResult = filter.invoke(next, invocation);
} catch (Exception e) {
... // 省略异常时监听器的逻辑
} finally {
}
return asyncResult.whenCompleteWithContext((r, t) -> {
... // 省略监听器的处理逻辑
});
}
};
}
}
return last;
}
在 Filter 接口内部还定义了一个 Listener 接口,有一些 Filter 实现会同时实现这个内部 Listener 接口,当 invoke() 方法执行正常结束时,会调用该 Listener 的 onResponse() 方法进行通知;当 invoke() 方法执行出现异常时,会调用该 Listener 的 onError() 方法进行通知。
另外,还有一个 ListenableFilter 抽象类,它继承了 Filter 接口,在原有 Filter 的基础上添加了一个 listeners 集合ConcurrentMap 集合)用来记录一次请求需要触发的监听器。需要注意的是,在执行 invoke() 调用之前,我们可以调用 addListener() 方法添加 Filter.Listener 实例进行监听,完成一次 invoke() 方法之后,这些添加的 Filter.Listener 实例就会立即从 listeners 集合中删除,也就是说,这些 Filter.Listener 实例不会在调用之间共享。
总结
本课时主要介绍的是 Dubbo 中 Invoker 接口的核心实现,这也是 Dubbo 最核心的实现之一。
紧接上一课时,我们分析了 DubboInvoker 对 twoway 请求的处理逻辑,其中展开介绍了涉及的 DecodeableRpcResult 以及 AsyncRpcResult 等核心类,深入讲解了 Dubbo 的同步、异步调用实现原理,说明了 Dubbo 在 2.7.x 版本之后的相关改进。最后,我们还介绍了 Invoker 接口的几个装饰器,其中涉及用于注册监听器的 ListenerInvokerWrapper 以及 Filter 相关的 Invoker 装饰器。
下一课时,我们将深入介绍 Dubbo RPC 层中代理的相关实现。

View File

@@ -0,0 +1,873 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
28 复杂问题简单化,代理帮你隐藏了多少底层细节?
在前面介绍 DubboProtocol 的相关实现时,我们知道 Protocol 这一层以及后面介绍的 Cluster 层暴露出来的接口都是 Dubbo 内部的一些概念,业务层无法直接使用。为了让业务逻辑能够无缝使用 Dubbo我们就需要将业务逻辑与 Dubbo 内部概念打通这就用到了动态生成代理对象的功能。Proxy 层在 Dubbo 架构中的位置如下所示(虽然在架构图中 Proxy 层与 Protocol 层距离很远,但 Proxy 的具体代码实现就位于 dubbo-rpc-api 模块中):
Dubbo 架构中 Proxy 层的位置图
在 Consumer 进行调用的时候Dubbo 会通过动态代理将业务接口实现对象转化为相应的 Invoker 对象,然后在 Cluster 层、Protocol 层都会使用 Invoker。在 Provider 暴露服务的时候,也会有 Invoker 对象与业务接口实现对象之间的转换,这同样也是通过动态代理实现的。
实现动态代理的常见方案有JDK 动态代理、CGLib 动态代理和 Javassist 动态代理。这些方案的应用都还是比较广泛的例如Hibernate 底层使用了 Javassist 和 CGLibSpring 使用了 CGLib 和 JDK 动态代理MyBatis 底层使用了 JDK 动态代理和 Javassist。
从性能方面看Javassist 与 CGLib 的实现方式相差无几,两者都比 JDK 动态代理性能要高具体高多少这就要看具体的机器、JDK 版本、测试基准的具体实现等条件了。
Dubbo 提供了两种方式来实现代理,分别是 JDK 动态代理和 Javassist。我们可以在 proxy 这个包内,看到相应工厂类,如下图所示:
ProxyFactory 核心实现的位置
了解了 Proxy 存在的必要性以及 Dubbo 提供的两种代理生成方式之后,下面我们就开始对 Proxy 层的实现进行深入分析。
ProxyFactory
关于 ProxyFactory 接口,我们在前面的第 23 课时中已经介绍过了这里做一下简单回顾。ProxyFactory 是一个扩展接口,其中定义了两个核心方法:一个是 getProxy() 方法,为 Invoker 对象创建代理对象;另一个是 getInvoker() 方法,将代理对象反向封装成 Invoker 对象。
@SPI("javassist")
public interface ProxyFactory {
// 为传入的Invoker对象创建代理对象
@Adaptive({PROXY_KEY})
<T> T getProxy(Invoker<T> invoker) throws RpcException;
@Adaptive({PROXY_KEY})
<T> T getProxy(Invoker<T> invoker, boolean generic) throws RpcException;
// 将传入的代理对象封装成Invoker对象
@Adaptive({PROXY_KEY})
<T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) throws RpcException;
}
看到 ProxyFactory 上的 @SPI 注解我们知道,其默认实现使用 Javassist 来创建代码对象。
AbstractProxyFactory 是代理工厂的抽象类,继承关系如下图所示:
AbstractProxyFactory 继承关系图
AbstractProxyFactory
AbstractProxyFactory 主要处理的是需要代理的接口,具体实现在 getProxy() 方法中:
public <T> T getProxy(Invoker<T> invoker, boolean generic) throws RpcException {
Set<Class<?>> interfaces = new HashSet<>();// 记录要代理的接口
// 获取URL中interfaces参数指定的接口
String config = invoker.getUrl().getParameter(INTERFACES);
if (config != null && config.length() > 0) {
// 按照逗号切分interfaces参数得到接口集合
String[] types = COMMA_SPLIT_PATTERN.split(config);
for (String type : types) { // 记录这些接口信息
interfaces.add(ReflectUtils.forName(type));
}
}
if (generic) { // 针对泛化接口的处理
if (!GenericService.class.isAssignableFrom(invoker.getInterface())) {
interfaces.add(GenericService.class);
}
// 从URL中获取interface参数指定的接口
String realInterface = invoker.getUrl().getParameter(Constants.INTERFACE);
interfaces.add(ReflectUtils.forName(realInterface));
}
// 获取Invoker中type字段指定的接口
interfaces.add(invoker.getInterface());
// 添加EchoService、Destroyable两个默认接口
interfaces.addAll(Arrays.asList(INTERNAL_INTERFACES));
// 调用抽象的getProxy()重载方法
return getProxy(invoker, interfaces.toArray(new Class<?>[0]));
}
AbstractProxyFactory 从多个地方获取需要代理的接口之后,会调用子类实现的 getProxy() 方法创建代理对象。
JavassistProxyFactory 对 getProxy() 方法的实现比较简单,直接委托给了 dubbo-common 模块中的 Proxy 工具类进行代理类的生成。下面我们就来深入分析 Proxy 生成代理类的全流程。
Proxy
在 dubbo-common 模块Proxy 中的 getProxy() 方法提供了动态创建代理类的核心实现。这个创建代理类的流程比较长,为了便于你更好地理解,这里我们将其拆开,一步步进行分析。
首先是查找 PROXY_CACHE_MAP 这个代理类缓存new WeakHashMap>() 类型),其中第一层 Key 是 ClassLoader 对象,第二层 Key 是上面整理得到的接口拼接而成的Value 是被缓存的代理类的 WeakReference弱引用
WeakReference弱引用的特性是WeakReference 引用的对象生命周期是两次 GC 之间,也就是说当垃圾收集器扫描到只具有弱引用的对象时,无论当前内存空间是否足够,都会回收该对象。(由于垃圾收集器是一个优先级很低的线程,不一定会很快发现那些只具有弱引用的对象。)
WeakReference 的特性决定了它特别适合用于数据可恢复的内存型缓存。查找缓存的结果有下面三个:
如果缓存中查找不到任务信息,则会在缓存中添加一个 PENDING_GENERATION_MARKER 占位符,当前线程后续创建生成代理类并最终替换占位符。
如果在缓存中查找到了 PENDING_GENERATION_MARKER 占位符,说明其他线程已经在生成相应的代理类了,当前线程会阻塞等待。
如果缓存中查找到完整代理类,则会直接返回,不会再执行后续动态代理类的生成。
下面是 Proxy.getProxy() 方法中对 PROXY_CACHE_MAP 缓存进行查询的相关代码片段:
public static Proxy getProxy(ClassLoader cl, Class<?>... ics) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < ics.length; i++) { // 循环处理每个接口类
String itf = ics[i].getName();
if (!ics[i].isInterface()) { // 传入的必须是接口类否则直接报错
throw new RuntimeException(itf + " is not a interface.");
}
// 加载接口类加载失败则直接报错
Class<?> tmp = Class.forName(itf, false, cl);
if (tmp != ics[i]) {
throw new IllegalArgumentException("...");
}
sb.append(itf).append(';'); // 将接口类的完整名称用分号连接起来
}
// 接口列表将会作为第二层集合的Key
String key = sb.toString();
final Map<String, Object> cache;
synchronized (PROXY_CACHE_MAP) { // 加锁同步
cache = PROXY_CACHE_MAP.computeIfAbsent(cl, k -> new HashMap<>());
}
Proxy proxy = null;
synchronized (cache) { // 加锁
do {
Object value = cache.get(key);
if (value instanceof Reference<?>) { // 获取到WeakReference
proxy = (Proxy) ((Reference<?>) value).get();
if (proxy != null) { // 查找到缓存的代理类
return proxy;
}
}
if (value == PENDING_GENERATION_MARKER) { // 获取到占位符
cache.wait(); // 阻塞等待其他线程生成好代理类,并添加到缓存中
} else { // 设置占位符,由当前线程生成代理类
cache.put(key, PENDING_GENERATION_MARKER);
break; // 退出当前循环
}
}
while (true);
}
... ... // 后续动态生成代理类的逻辑
}
完成缓存的查找之后,下面我们再来看代理类的生成过程。
第一步,调用 ClassGenerator.newInstance() 方法创建 ClassLoader 对应的 ClassPool。ClassGenerator 中封装了 Javassist 的基本操作,还定义了很多字段用来暂存代理类的信息,在其 toClass() 方法中会用这些暂存的信息来动态生成代理类。下面就来简单说明一下这些字段。
mClassNameString 类型):代理类的类名。
mSuperClassString 类型):代理类父类的名称。
mInterfacesSet<String> 类型):代理类实现的接口。
mFieldsList类型代理类中的字段。
mConstructorsList<String>类型):代理类中全部构造方法的信息,其中包括构造方法的具体实现。
mMethodsList<String>类型):代理类中全部方法的信息,其中包括方法的具体实现。
mDefaultConstructorboolean 类型):标识是否为代理类生成的默认构造方法。
在 ClassGenerator 的 toClass() 方法中,会根据上述字段用 Javassist 生成代理类,具体实现如下:
public Class<?> toClass(ClassLoader loader, ProtectionDomain pd) {
if (mCtc != null) {
mCtc.detach();
}
// 在代理类继承父类的时候会将该id作为后缀编号防止代理类重名
long id = CLASS_NAME_COUNTER.getAndIncrement();
CtClass ctcs = mSuperClass == null ? null : mPool.get(mSuperClass);
if (mClassName == null) { // 确定代理类的名称
mClassName = (mSuperClass == null || javassist.Modifier.isPublic(ctcs.getModifiers())
? ClassGenerator.class.getName() : mSuperClass + "$sc") + id;
}
mCtc = mPool.makeClass(mClassName); // 创建CtClass用来生成代理类
if (mSuperClass != null) { // 设置代理类的父类
mCtc.setSuperclass(ctcs);
}
// 设置代理类实现的接口默认会添加DC这个接口
mCtc.addInterface(mPool.get(DC.class.getName()));
if (mInterfaces != null) {
for (String cl : mInterfaces) {
mCtc.addInterface(mPool.get(cl));
}
}
if (mFields != null) { // 设置代理类的字段
for (String code : mFields) {
mCtc.addField(CtField.make(code, mCtc));
}
}
if (mMethods != null) { // 生成代理类的方法
for (String code : mMethods) {
if (code.charAt(0) == ':') {
mCtc.addMethod(CtNewMethod.copy(getCtMethod(mCopyMethods.get(code.substring(1))),
code.substring(1, code.indexOf('(')), mCtc, null));
} else {
mCtc.addMethod(CtNewMethod.make(code, mCtc));
}
}
}
if (mDefaultConstructor) { // 生成默认的构造方法
mCtc.addConstructor(CtNewConstructor.defaultConstructor(mCtc));
}
if (mConstructors != null) { // 生成构造方法
for (String code : mConstructors) {
if (code.charAt(0) == ':') {
mCtc.addConstructor(CtNewConstructor
.copy(getCtConstructor(mCopyConstructors.get(code.substring(1))), mCtc, null));
} else {
String[] sn = mCtc.getSimpleName().split("\\$+"); // inner class name include $.
mCtc.addConstructor(
CtNewConstructor.make(code.replaceFirst(SIMPLE_NAME_TAG, sn[sn.length - 1]), mCtc));
}
}
}
return mCtc.toClass(loader, pd);
}
第二步,从 PROXY_CLASS_COUNTER 字段AtomicLong类型中获取一个 id 值,作为代理类的后缀,这主要是为了避免类名重复发生冲突。
第三步,遍历全部接口,获取每个接口中定义的方法,对每个方法进行如下处理:
加入 worked 集合Set<String> 类型)中,用来判重。
将方法对应的 Method 对象添加到 methods 集合List<Method> 类型)中。
获取方法的参数类型以及返回类型,构建方法体以及 return 语句。
将构造好的方法添加到 ClassGenerator 中的 mMethods 集合中进行缓存。
相关代码片段如下所示:
long id = PROXY_CLASS_COUNTER.getAndIncrement();
String pkg = null;
ClassGenerator ccp = null, ccm = null;
ccp = ClassGenerator.newInstance(cl);
Set<String> worked = new HashSet<>()
List<Method> methods = new ArrayList>();
for (int i = 0; i < ics.length; i++) {
if (!Modifier.isPublic(ics[i].getModifiers())) {
String npkg = ics[i].getPackage().getName();
if (pkg == null) { // 如果接口不是public的则需要保证所有接口在一个包下
pkg = npkg;
} else {
if (!pkg.equals(npkg)) {
throw new IllegalArgumentException("non-public interfaces from different packages");
}
}
}
ccp.addInterface(ics[i]); // 向ClassGenerator中添加接口
for (Method method : ics[i].getMethods()) { // 遍历接口中的每个方法
String desc = ReflectUtils.getDesc(method);
// 跳过已经重复方法以及static方法
if (worked.contains(desc) || Modifier.isStatic(method.getModifiers())) {
continue;
}
if (ics[i].isInterface() && Modifier.isStatic(method.getModifiers())) {
continue;
}
worked.add(desc); // 将方法描述添加到worked这个Set集合中进行去重
int ix = methods.size();
Class<?> rt = method.getReturnType(); // 获取方法的返回值
Class<?>[] pts = method.getParameterTypes(); // 获取方法的参数列表
// 创建方法体
StringBuilder code = new StringBuilder("Object[] args = new Object[").append(pts.length).append("];");
for (int j = 0; j < pts.length; j++) {
code.append(" args[").append(j).append("] = ($w)$").append(j + 1).append(";");
}
code.append(" Object ret = handler.invoke(this, methods[").append(ix).append("], args);");
if (!Void.TYPE.equals(rt)) { // 生成return语句
code.append(" return ").append(asArgument(rt, "ret")).append(";");
}
// 将生成好的方法添加到ClassGenerator中缓存
methods.add(method);
ccp.addMethod(method.getName(), method.getModifiers(), rt, pts, method.getExceptionTypes(), code.toString());
}
}
这里我们以 Demo 示例(即 dubbo-demo 模块中的 Demo中的 sayHello() 方法为例,生成的方法如下所示:
public java.lang.String sayHello(java.lang.String arg0){
Object[] args = new Object[1];
args[0] = ($w)$1;
// 这里通过InvocationHandler.invoke()方法调用目标方法
Object ret = handler.invoke(this, methods[3], args);
return (java.lang.String)ret;
}
这里的方法调用其实是:委托 InvocationHandler 对象的 invoke() 方法去调用真正的实例方法。
第四步开始创建代理实例类ProxyInstance和代理类。这里我们先创建代理实例类需要向 ClassGenerator 中添加相应的信息,例如,类名、默认构造方法、字段、父类以及一个 newInstance() 方法,具体实现如下:
String pcn = pkg + ".proxy" + id; // 生成并设置代理类类名
ccp.setClassName(pcn);
// 添加字段一个是前面生成的methods集合另一个是InvocationHandler对象
ccp.addField("public static java.lang.reflect.Method[] methods;");
ccp.addField("private " + InvocationHandler.class.getName() + " handler;");
// 添加构造方法
ccp.addConstructor(Modifier.PUBLIC, new Class<?>[]{InvocationHandler.class}, new Class<?>[0], "handler=$1;");
ccp.addDefaultConstructor(); // 默认构造方法
Class<?> clazz = ccp.toClass();
clazz.getField("methods").set(null, methods.toArray(new Method[0]));
这里得到的代理实例类中每个方法的实现,都类似于上面提到的 sayHello() 方法的实现,即通过 InvocationHandler.invoke()方法调用目标方法。
接下来创建代理类,它实现了 Proxy 接口,并实现了 newInstance() 方法,该方法会直接返回上面代理实例类的对象,相关代码片段如下:
String fcn = Proxy.class.getName() + id;
ccm = ClassGenerator.newInstance(cl);
ccm.setClassName(fcn);
ccm.addDefaultConstructor(); // 默认构造方法
ccm.setSuperClass(Proxy.class); // 实现Proxy接口
// 实现newInstance()方法,返回上面创建的代理实例类的对象
ccm.addMethod("public Object newInstance(" + InvocationHandler.class.getName() + " h){ return new " + pcn + "($1); }");
Class<?> pc = ccm.toClass();
proxy = (Proxy) pc.newInstance();
生成的代理类如下所示:
package com.apache.dubbo.common.bytecode;
public class Proxy0 implements Proxy {
public void Proxy0() {}
public Object newInstance(InvocationHandler h){
return new proxy0(h);
}
}
第五步,也就是最后一步,在 finally 代码块中,会释放 ClassGenerator 的相关资源,将生成的代理类添加到 PROXY_CACHE_MAP 缓存中保存,同时会唤醒所有阻塞在 PROXY_CACHE_MAP 缓存上的线程,重新检测需要的代理类是否已经生成完毕。相关代码片段如下:
if (ccp != null) { // 释放ClassGenerator的相关资源
ccp.release();
}
if (ccm != null) {
ccm.release();
}
synchronized (cache) { // 加锁
if (proxy == null) {
cache.remove(key);
} else { // 填充PROXY_CACHE_MAP缓存
cache.put(key, new WeakReference<Proxy>(proxy));
}
cache.notifyAll(); // 唤醒所有阻塞在PROXY_CACHE_MAP上的线程
}
getProxy() 方法实现
分析完 Proxy 使用 Javassist 生成代理类的完整流程之后,我们再回头看一下 JavassistProxyFactory 工厂的 getProxy() 方法实现。这里首先通过前面分析的 getProxy() 方法获取 Proxy 对象,然后调用 newInstance() 方法获取目标类的代理对象,具体如下所示:
public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
}
相比之下JdkProxyFactory 对 getProxy() 方法的实现就简单很多,直接使用 JDK 自带的 java.lang.reflect.Proxy 生成代理对象,你可以参考前面第 8 课时中 JDK 动态代理的基本使用方式以及原理:
public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), interfaces, new InvokerInvocationHandler(invoker));
}
InvokerInvocationHandler
无论是 Javassist 还是 JDK 生成的代理类,都会将方法委托给 InvokerInvocationHandler 进行处理。InvokerInvocationHandler 中维护了一个 Invoker 对象,也是前面 getProxy() 方法传入的第一个参数,这个 Invoker 不是一个简单的 DubboInvoker 对象,而是在 DubboInvoker 之上经过一系列装饰器修饰的 Invoker 对象。
在 InvokerInvocationHandler 的 invoke() 方法中,首先会针对特殊的方法进行处理,比如 toString()、$destroy() 等方法。之后,对于业务方法,会创建相应的 RpcInvocation 对象调用 Invoker.invoke() 方法发起 RPC 调用,具体实现如下:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 对于Object中定义的方法直接调用Invoker对象的相应方法即可
if (method.getDeclaringClass() == Object.class) {
return method.invoke(invoker, args);
}
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 0) { // 对$destroy等方法的特殊处理
if ("$destroy".equals(methodName)) {
invoker.destroy();
return null;
}
}
... // 省略其他特殊处理的方法
// 创建RpcInvocation对象后面会作为远程RPC调用的参数
RpcInvocation rpcInvocation = new RpcInvocation(method, invoker.getInterface().getName(), args);
String serviceKey = invoker.getUrl().getServiceKey();
rpcInvocation.setTargetServiceUniqueName(serviceKey);
if (consumerModel != null) {
rpcInvocation.put(Constants.CONSUMER_MODEL, consumerModel);
rpcInvocation.put(Constants.METHOD_MODEL, consumerModel.getMethodModel(method));
}
// 调用invoke()方法发起远程调用拿到AsyncRpcResult之后调用recreate()方法获取响应结果(或是Future)
return invoker.invoke(rpcInvocation).recreate();
}
Wrapper
Invoker 是 Dubbo 的核心模型。在 Dubbo 中Provider 的业务层实现会被包装成一个 ProxyInvoker然后这个 ProxyInvoker 还会被 Filter、Listener 以及其他装饰器包装。ProxyFactory 的 getInvoker 方法就是将业务接口实现封装成 ProxyInvoker 入口。
我们先来看 JdkProxyFactory 中的实现。JdkProxyFactory 会创建一个匿名 AbstractProxyInvoker 的实现,其中的 doInvoke() 方法是通过 Java 原生的反射技术实现的,具体实现如下:
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
return new AbstractProxyInvoker<T>(proxy, type, url) {
@Override
protected Object doInvoke(T proxy, String methodName,
Class<?>[] parameterTypes, Object[] arguments) throws Throwable {
// 使用反射方式查找methodName对应的方法并进行调用
Method method = proxy.getClass().getMethod(methodName, parameterTypes);
return method.invoke(proxy, arguments);
}
};
}
在前面两个课时中我们已经介绍了 Invoker 接口的一个重要实现分支—— AbstractInvoker 以及它的一个实现 DubboInvoker。AbstractProxyInvoker 是 Invoker 接口的另一个实现分支,继承关系如下图所示,其实现类都是 ProxyFactory 实现中的匿名内部类。
在 AbstractProxyInvoker 实现的 invoke() 方法中,会将 doInvoke() 方法返回的结果封装成 CompletableFuture 对象,然后再封装成 AsyncRpcResult 对象返回,具体实现如下:
public Result invoke(Invocation invocation) throws RpcException {
// 执行doInvoke()方法,调用业务实现
Object value = doInvoke(proxy, invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());
// 将value值封装成CompletableFuture对象
CompletableFuture<Object> future = wrapWithFuture(value);
// 再次转换转换为CompletableFuture<AppResponse>类型
CompletableFuture<AppResponse> appResponseFuture = future.handle((obj, t) -> {
AppResponse result = new AppResponse();
if (t != null) {
if (t instanceof CompletionException) {
result.setException(t.getCause());
} else {
result.setException(t);
}
} else {
result.setValue(obj);
}
return result;
});
// 将CompletableFuture封装成AsyncRpcResult返回
return new AsyncRpcResult(appResponseFuture, invocation);
}
了解了 AbstractProxyInvoker 以及 JdkProxyFactory 返回的实现之后,我们再来看 JavassistProxyFactory.getInvoker() 方法返回的实现。首先该方法会通过 Wrapper 创建一个包装类,然后创建一个实现了 AbstractProxyInvoker 的匿名内部类,其 doInvoker() 方法会直接委托给 Wrapper 对象的 InvokeMethod() 方法,具体实现如下:
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
// 通过Wrapper创建一个包装类对象
final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
// 创建一个实现了AbstractProxyInvoker的匿名内部类其doInvoker()方法会直接委托给Wrapper对象的InvokeMethod()方法
return new AbstractProxyInvoker<T>(proxy, type, url) {
@Override
protected Object doInvoke(T proxy, String methodName,
Class<?>[] parameterTypes, Object[] arguments) throws Throwable {
return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
}
};
}
Wrapper 类本身是抽象类,是对 Java 类的一种包装。Wrapper 会从 Java 类中的字段和方法抽象出相应 propertyName 和 methodName在需要调用一个字段或方法的时候会根据传入的方法名和参数进行匹配找到对应的字段和方法进行调用。
Wrapper.getWrapper() 方法会根据不同的 Java 对象,使用 Javassist 生成一个相应的 Wrapper 实现对象。下面我们就来一起分析下 getWrapper() 方法实现:
首先检测该 Java 类是否实现了 DC 这个标识接口,在前面介绍 Proxy 抽象类的时候,我们提到过这个接口;
检测 WRAPPER_MAP 集合Map, Wrapper> 类型)中是否缓存了对应的 Wrapper 对象,如果已缓存则直接返回,如果未缓存则调用 makeWrapper() 方法动态生成 Wrapper 实现类,以及相应的实例对象,并写入缓存中。
makeWrapper() 方法的实现非常长,但是逻辑并不复杂,该方法会遍历传入的 Class 对象的所有 public 字段和 public 方法,构建组装 Wrapper 实现类需要的 Java 代码。具体实现有如下三个步骤。
第一步public 字段会构造相应的 getPropertyValue() 方法和 setPropertyValue() 方法。例如有一个名为“name”的 public 字段,则会生成如下的代码:
// 生成的getPropertyValue()方法
public Object getPropertyValue(Object o, String n){
DemoServiceImpl w;
try{
w = ((DemoServiceImpl)$1);
}catch(Throwable e){
throw new IllegalArgumentException(e);
}
if( $2.equals(" if( $2.equals("name") ){
return ($w)w.name;
}
}
// 生成的setPropertyValue()方法
public void setPropertyValue(Object o, String n, Object v){
DemoServiceImpl w;
try{
w = ((DemoServiceImpl)$1);
}catch(Throwable e){
throw new IllegalArgumentException(e);
}
if( $2.equals("name") ){
w.name=(java.lang.String)$3; return;
}
}
第二步,处理 public 方法,这些 public 方法会添加到 invokeMethod 方法中。以 Demo 示例(即 dubbo-demo 模块中的 demo )中的 DemoServiceImpl 为例,生成的 invokeMethod() 方法实现如下:
public Object invokeMethod(Object o, String n, Class[] p, Object[] v) throws java.lang.reflect.InvocationTargetException {
org.apache.dubbo.demo.provider.DemoServiceImpl w;
try {
w = ((org.apache.dubbo.demo.provider.DemoServiceImpl) $1);
} catch (Throwable e) {
throw new IllegalArgumentException(e);
}
try {
// 省略getter/setter方法
if ("sayHello".equals($2) && $3.length == 1) {
return ($w) w.sayHello((java.lang.String) $4[0]);
}
if ("sayHelloAsync".equals($2) && $3.length == 1) {
return ($w) w.sayHelloAsync((java.lang.String) $4[0]);
}
} catch (Throwable e) {
throw new java.lang.reflect.InvocationTargetException(e);
}
throw new NoSuchMethodException("Not found method");
}
第三步,完成了上述 Wrapper 实现类相关信息的填充之后makeWrapper() 方法会通过 ClassGenerator 创建 Wrapper 实现类,具体原理与前面 Proxy 创建代理类的流程类似,这里就不再赘述。
总结
本课时主要介绍了 dubbo-rpc-api 模块中“代理”相关的内容。首先我们从 ProxyFactory.getProxy() 方法入手,详细介绍了 JDK 方式和 Javassist 方式创建动态代理类的底层原理,以及其中使用的 InvokerInvocationHandler 的实现。接下来我们又通过 ProxyFactory.getInvoker() 方法入手,重点讲解了 Wrapper 的生成过程和核心原理。
下面这张简图很好地展示了 Dubbo 中 Proxy 和 Wrapper 的重要性:
Proxy 和 Wrapper 远程调用简图
Consumer 端的 Proxy 底层屏蔽了复杂的网络交互、集群策略以及 Dubbo 内部的 Invoker 等概念提供给上层使用的是业务接口。Provider 端的 Wrapper 是将个性化的业务接口实现,统一转换成 Dubbo 内部的 Invoker 接口实现。正是由于 Proxy 和 Wrapper 这两个组件的存在Dubbo 才能实现内部接口和业务接口的无缝转换。
关于“代理”相关的内容,你若还有什么想法,欢迎你留言跟我分享。下一课时,我们会再做一个加餐,介绍 Dubbo 中支持的 HTTP 协议的相关内容。

View File

@@ -0,0 +1,598 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
31 加餐:深潜 Directory 实现,探秘服务目录玄机
从这一课时我们就进入“集群”模块了,今天我们分享的是一篇加餐文章,主题是:深潜 Directory 实现,探秘服务目录玄机。
在生产环境中,为了保证服务的可靠性、吞吐量以及容错能力,我们通常会在多个服务器上运行相同的服务端程序,然后以集群的形式对外提供服务。根据各项性能指标的要求不同,各个服务端集群中服务实例的个数也不尽相同,从几个实例到几百个实例不等。
对于客户端程序来说,就会出现几个问题:
客户端程序是否要感知每个服务端地址?
客户端程序的一次请求,到底调用哪个服务端程序呢?
请求失败之后的处理是重试,还会是抛出异常?
如果是重试,是再次请求该服务实例,还是尝试请求其他服务实例?
服务端集群如何做到负载均衡,负载均衡的标准是什么呢?
……
为了解决上述问题Dubbo 独立出了一个实现集群功能的模块—— dubbo-cluster。
dubbo-cluster 结构图
作为 dubbo-cluster 模块分析的第一课时,我们就首先来了解一下 dubbo-cluster 模块的架构以及最核心的 Cluster 接口。
Cluster 架构
dubbo-cluster 模块的主要功能是将多个 Provider 伪装成一个 Provider 供 Consumer 调用,其中涉及集群的容错处理、路由规则的处理以及负载均衡。下图展示了 dubbo-cluster 的核心组件:
Cluster 核心接口图
由图我们可以看出dubbo-cluster 主要包括以下四个核心接口:
Cluster 接口,是集群容错的接口,主要是在某些 Provider 节点发生故障时,让 Consumer 的调用请求能够发送到正常的 Provider 节点,从而保证整个系统的可用性。
Directory 接口,表示多个 Invoker 的集合,是后续路由规则、负载均衡策略以及集群容错的基础。
Router 接口,抽象的是路由器,请求经过 Router 的时候,会按照用户指定的规则匹配出符合条件的 Provider。
LoadBalance 接口是负载均衡接口Consumer 会按照指定的负载均衡策略,从 Provider 集合中选出一个最合适的 Provider 节点来处理请求。
Cluster 层的核心流程是这样的:当调用进入 Cluster 的时候Cluster 会创建一个 AbstractClusterInvoker 对象,在这个 AbstractClusterInvoker 中,首先会从 Directory 中获取当前 Invoker 集合;然后按照 Router 集合进行路由,得到符合条件的 Invoker 集合;接下来按照 LoadBalance 指定的负载均衡策略得到最终要调用的 Invoker 对象。
了解了 dubbo-cluster 模块的核心架构和基础组件之后,我们后续将会按照上面架构图的顺序介绍每个接口的定义以及相关实现。
Directory 接口详解
Directory 接口表示的是一个集合,该集合由多个 Invoker 构成,后续的路由处理、负载均衡、集群容错等一系列操作都是在 Directory 基础上实现的。
下面我们深入分析一下 Directory 的相关内容,首先是 Directory 接口中定义的方法:
public interface Directory<T> extends Node {
// 服务接口类型
Class<T> getInterface();
// list()方法会根据传入的Invocation请求过滤自身维护的Invoker集合返回符合条件的Invoker集合
List<Invoker<T>> list(Invocation invocation) throws RpcException;
// getAllInvokers()方法返回当前Directory对象维护的全部Invoker对象
List<Invoker<T>> getAllInvokers();
// Consumer端的URL
URL getConsumerUrl();
}
AbstractDirectory 是 Directory 接口的抽象实现,其中除了维护 Consumer 端的 URL 信息,还维护了一个 RouterChain 对象,用于记录当前使用的 Router 对象集合,也就是后面课时要介绍的路由规则。
AbstractDirectory 对 list() 方法的实现也比较简单,就是直接委托给了 doList() 方法doList() 是个抽象方法,由 AbstractDirectory 的子类具体实现。
Directory 接口有 RegistryDirectory 和 StaticDirectory 两个具体实现,如下图所示:
Directory 接口继承关系图
其中RegistryDirectory 实现中维护的 Invoker 集合会随着注册中心中维护的注册信息动态发生变化,这就依赖了 ZooKeeper 等注册中心的推送能力StaticDirectory 实现中维护的 Invoker 集合则是静态的,在 StaticDirectory 对象创建完成之后,不会再发生变化。
下面我们就来分别介绍 Directory 接口的这两个具体实现。
1. StaticDirectory
StaticDirectory 这个 Directory 实现比较简单在构造方法中StaticDirectory 会接收一个 Invoker 集合,并赋值到自身的 invokers 字段中,作为底层的 Invoker 集合。在 doList() 方法中StaticDirectory 会使用 RouterChain 中的 Router 从 invokers 集合中过滤出符合路由规则的 Invoker 对象集合,具体实现如下:
protected List<Invoker<T>> doList(Invocation invocation) throws RpcException {
List<Invoker<T>> finalInvokers = invokers;
if (routerChain != null) { // 通过RouterChain过滤出符合条件的Invoker集合
finalInvokers = routerChain.route(getConsumerUrl(), invocation);
}
return finalInvokers == null ? Collections.emptyList() : finalInvokers;
}
在创建 StaticDirectory 对象的时候,如果没有传入 RouterChain 对象,则会根据 URL 构造一个包含内置 Router 的 RouterChain 对象:
public void buildRouterChain() {
RouterChain<T> routerChain = RouterChain.buildChain(getUrl()); // 创建内置Router集合
// 将invokers与RouterChain关联
routerChain.setInvokers(invokers);
this.setRouterChain(routerChain); // 设置routerChain字段
}
2. RegistryDirectory
RegistryDirectory 是一个动态的 Directory 实现,实现了 NotifyListener 接口当注册中心的服务配置发生变化时RegistryDirectory 会收到变更通知然后RegistryDirectory 会根据注册中心推送的通知,动态增删底层 Invoker 集合。
下面我们先来看一下 RegistryDirectory 中的核心字段。
clusterCluster 类型):集群策略适配器,这里通过 Dubbo SPI 方式(即 ExtensionLoader.getAdaptiveExtension() 方法)动态创建适配器实例。
routerFactoryRouterFactory 类型):路由工厂适配器,也是通过 Dubbo SPI 动态创建的适配器实例。routerFactory 字段和 cluster 字段都是静态字段,多个 RegistryDirectory 对象通用。
serviceKeyString 类型):服务对应的 ServiceKey默认是 {interface}:[group]:[version] 三部分构成。
serviceTypeClass 类型服务接口类型例如org.apache.dubbo.demo.DemoService。
queryMapMap 类型Consumer URL 中 refer 参数解析后得到的全部 KV。
directoryUrlURL 类型):只保留 Consumer 属性的 URL也就是由 queryMap 集合重新生成的 URL。
multiGroupboolean类型是否引用多个服务组。
protocolProtocol 类型):使用的 Protocol 实现。
registryRegistry 类型):使用的注册中心实现。
invokersvolatile List 类型):动态更新的 Invoker 集合。
urlInvokerMapvolatile Map< String, Invoker> 类型Provider URL 与对应 Invoker 之间的映射,该集合会与 invokers 字段同时动态更新。
cachedInvokerUrlsvolatile Set类型当前缓存的所有 Provider 的 URL该集合会与 invokers 字段同时动态更新。
configuratorsvolatile List< Configurator>类型):动态更新的配置信息,配置的具体内容在后面的分析中会介绍到。
在 RegistryDirectory 的构造方法中,会根据传入的注册中心 URL 初始化上述核心字段,具体实现如下:
public RegistryDirectory(Class<T> serviceType, URL url) {
// 传入的url参数是注册中心的URL例如zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?...其中refer参数包含了Consumer信息例如refer=application=dubbo-demo-api-consumer&dubbo=2.0.2&interface=org.apache.dubbo.demo.DemoService&pid=13423&register.ip=192.168.124.3&side=consumer(URLDecode之后的值)
super(url);
shouldRegister = !ANY_VALUE.equals(url.getServiceInterface()) && url.getParameter(REGISTER_KEY, true);
shouldSimplified = url.getParameter(SIMPLIFIED_KEY, false);
this.serviceType = serviceType;
this.serviceKey = url.getServiceKey();
// 解析refer参数值得到其中Consumer的属性信息
this.queryMap = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));
// 将queryMap中的KV作为参数重新构造URL其中的protocol和path部分不变
this.overrideDirectoryUrl = this.directoryUrl = turnRegistryUrlToConsumerUrl(url);
String group = directoryUrl.getParameter(GROUP_KEY, "");
this.multiGroup = group != null && (ANY_VALUE.equals(group) || group.contains(","));
}
在完成初始化之后,我们来看 subscribe() 方法,该方法会在 Consumer 进行订阅的时候被调用,其中调用 Registry 的 subscribe() 完成订阅操作,同时还会将当前 RegistryDirectory 对象作为 NotifyListener 监听器添加到 Registry 中,具体实现如下:
public void subscribe(URL url) {
setConsumerUrl(url);
// 将当前RegistryDirectory对象作为ConfigurationListener记录到CONSUMER_CONFIGURATION_LISTENER中
CONSUMER_CONFIGURATION_LISTENER.addNotifyListener(this);
serviceConfigurationListener = new ReferenceConfigurationListener(this, url);
// 完成订阅操作,注册中心的相关操作在前文已经介绍过了,这里不再重复
registry.subscribe(url, this);
}
我们看到除了作为 NotifyListener 监听器之外RegistryDirectory 内部还有两个 ConfigurationListener 的内部类(继承关系如下图所示),为了保持连贯,这两个监听器的具体原理我们在后面的课时中会详细介绍,这里先不展开讲述。
RegistryDirectory 内部的 ConfigurationListener 实现
通过前面对 Registry 的介绍我们知道,在注册 NotifyListener 的时候,监听的是 providers、configurators 和 routers 三个目录,所以在这三个目录下发生变化的时候,就会触发 RegistryDirectory 的 notify() 方法。
在 RegistryDirectory.notify() 方法中,首先会按照 category 对发生变化的 URL 进行分类,分成 configurators、routers、providers 三类,并分别对不同类型的 URL 进行处理:
将 configurators 类型的 URL 转化为 Configurator保存到 configurators 字段中;
将 router 类型的 URL 转化为 Router并通过 routerChain.addRouters() 方法添加 routerChain 中保存;
将 provider 类型的 URL 转化为 Invoker 对象,并记录到 invokers 集合和 urlInvokerMap 集合中。
notify() 方法的具体实现如下:
public synchronized void notify(List<URL> urls) {
// 按照category进行分类分成configurators、routers、providers三类
Map<String, List<URL>> categoryUrls = urls.stream()
.filter(Objects::nonNull)
.filter(this::isValidCategory)
.filter(this::isNotCompatibleFor26x)
.collect(Collectors.groupingBy(this::judgeCategory));
// 获取configurators类型的URL并转换成Configurator对象
List<URL> configuratorURLs = categoryUrls.getOrDefault(CONFIGURATORS_CATEGORY, Collections.emptyList());
this.configurators = Configurator.toConfigurators(configuratorURLs).orElse(this.configurators);
// 获取routers类型的URL并转成Router对象添加到RouterChain中
List<URL> routerURLs = categoryUrls.getOrDefault(ROUTERS_CATEGORY, Collections.emptyList());
toRouters(routerURLs).ifPresent(this::addRouters);
// 获取providers类型的URL调用refreshOverrideAndInvoker()方法进行处理
List<URL> providerURLs = categoryUrls.getOrDefault(PROVIDERS_CATEGORY, Collections.emptyList());
... // 在Dubbo3.0中会触发AddressListener监听器但是现在AddressListener接口还没有实现所以省略这段代码
refreshOverrideAndInvoker(providerURLs);
}
我们这里首先来专注providers 类型 URL 的处理,具体实现位置在 refreshInvoker() 方法中,具体实现如下:
private void refreshInvoker(List<URL> invokerUrls) {
// 如果invokerUrls集合不为空长度为1并且协议为empty则表示该服务的所有Provider都下线了会销毁当前所有Provider对应的Invoker。
if (invokerUrls.size() == 1 && invokerUrls.get(0) != null
&& EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
this.forbidden = true; // forbidden标记设置为true后续请求将直接抛出异常
this.invokers = Collections.emptyList();
routerChain.setInvokers(this.invokers); // 清空RouterChain中的Invoker集合
destroyAllInvokers(); // 关闭所有Invoker对象
} else {
this.forbidden = false; // forbidden标记设置为falseRegistryDirectory可以正常处理后续请求
Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; // 保存本地引用
if (invokerUrls == Collections.<URL>emptyList()) {
invokerUrls = new ArrayList<>();
}
if (invokerUrls.isEmpty() && this.cachedInvokerUrls != null) {
// 如果invokerUrls集合为空并且cachedInvokerUrls不为空则将使用cachedInvokerUrls缓存的数据
// 也就是说注册中心中的providers目录未发生变化invokerUrls则为空表示cachedInvokerUrls集合中缓存的URL为最新的值
invokerUrls.addAll(this.cachedInvokerUrls);
} else {
// 如果invokerUrls集合不为空则用invokerUrls集合更新cachedInvokerUrls集合
// 也就是说providers发生变化invokerUrls集合中会包含此时注册中心所有的服务提供者
this.cachedInvokerUrls = new HashSet<>();
this.cachedInvokerUrls.addAll(invokerUrls);//Cached invoker urls, convenient for comparison
}
if (invokerUrls.isEmpty()) {
return; // 如果invokerUrls集合为空即providers目录未发生变更则无须处理结束本次更新服务提供者Invoker操作。
}
// 将invokerUrls转换为对应的Invoker映射关系
Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);
if (CollectionUtils.isEmptyMap(newUrlInvokerMap)) {
return;
}
// 更新invokers字段和urlInvokerMap集合
List<Invoker<T>> newInvokers = Collections.unmodifiableList(new ArrayList<>(newUrlInvokerMap.values()));
routerChain.setInvokers(newInvokers);
// 针对multiGroup的特殊处理合并多个group的Invoker
this.invokers = multiGroup ? toMergeInvokerList(newInvokers) : newInvokers;
this.urlInvokerMap = newUrlInvokerMap;
// 比较新旧两组Invoker集合销毁掉已经下线的Invoker
destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap);
}
}
通过对 refreshInvoker() 方法的介绍,我们可以看出,其最核心的逻辑是 Provider URL 转换成 Invoker 对象,也就是 toInvokers() 方法。下面我们就来深入 toInvokers() 方法内部,看看其具体的转换逻辑:
private Map<String, Invoker<T>> toInvokers(List<URL> urls) {
... // urls集合为空时直接返回空Map
Set<String> keys = new HashSet<>();
String queryProtocols = this.queryMap.get(PROTOCOL_KEY); // 获取Consumer端支持的协议即protocol参数指定的协议
for (URL providerUrl : urls) {
if (queryProtocols != null && queryProtocols.length() > 0) {
boolean accept = false;
String[] acceptProtocols = queryProtocols.split(",");
for (String acceptProtocol : acceptProtocols) { // 遍历所有Consumer端支持的协议
if (providerUrl.getProtocol().equals(acceptProtocol)) {
accept = true;
break;
}
}
if (!accept) {
continue; // 如果当前URL不支持Consumer端的协议也就无法执行后续转换成Invoker的逻辑
}
}
if (EMPTY_PROTOCOL.equals(providerUrl.getProtocol())) {
continue; // 跳过empty协议的URL
}
// 如果Consumer端不支持该URL的协议这里通过SPI方式检测是否有对应的Protocol扩展实现也会跳过该URL
if (!ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(providerUrl.getProtocol())) {
logger.error("...");
continue;
}
// 合并URL参数这个合并过程在本课时后面展开介绍
URL url = mergeUrl(providerUrl);
// 获取完整URL对应的字符串也就是在urlInvokerMap集合中的key
String key = url.toFullString();
if (keys.contains(key)) { // 跳过重复的URL
continue;
}
keys.add(key); // 记录key
// 匹配urlInvokerMap缓存中的Invoker对象如果命中缓存直接将Invoker添加到newUrlInvokerMap这个新集合中即可
// 如果未命中缓存则创建新的Invoker对象然后添加到newUrlInvokerMap这个新集合中
Map<String, Invoker<T>> localUrlInvokerMap = this.urlInvokerMap;
Invoker<T> invoker = localUrlInvokerMap == null ? null : localUrlInvokerMap.get(key);
if (invoker == null) {
try {
boolean enabled = true;
if (url.hasParameter(DISABLED_KEY)) { // 检测URL中的disable和enable参数决定是否能够创建Invoker对象
enabled = !url.getParameter(DISABLED_KEY, false);
} else {
enabled = url.getParameter(ENABLED_KEY, true);
}
if (enabled) { // 这里通过Protocol.refer()方法创建对应的Invoker对象
invoker = new InvokerDelegate<>(protocol.refer(serviceType, url), url, providerUrl);
}
} catch (Throwable t) {
logger.error("Failed to refer invoker for interface:" + serviceType + ",url:(" + url + ")" + t.getMessage(), t);
}
if (invoker != null) { // 将key和Invoker对象之间的映射关系记录到newUrlInvokerMap中
newUrlInvokerMap.put(key, invoker);
}
} else {// 缓存命中直接将urlInvokerMap中的Invoker转移到newUrlInvokerMap即可
newUrlInvokerMap.put(key, invoker);
}
}
keys.clear();
return newUrlInvokerMap;
}
toInvokers() 方法的代码虽然有点长,但核心逻辑就是调用 Protocol.refer() 方法创建 Invoker 对象,其他的逻辑都是在判断是否调用该方法。
在 toInvokers() 方法内部,我们可以看到调用了 mergeUrl() 方法对 URL 参数进行合并。在 mergeUrl() 方法中,会将注册中心中 configurators 目录下的 URLoverride 协议),以及服务治理控制台动态添加的配置与 Provider URL 进行合并,即覆盖 Provider URL 原有的一些信息,具体实现如下:
private URL mergeUrl(URL providerUrl) {
// 首先移除Provider URL中只在Provider端生效的属性例如threadname、threadpool、corethreads、threads、queues等参数。
// 然后用Consumer端的配置覆盖Provider URL的相应配置其中version、group、methods、timestamp等参数以Provider端的配置优先
// 最后合并Provider端和Consumer端配置的Filter以及Listener
providerUrl = ClusterUtils.mergeUrl(providerUrl, queryMap);
// 合并configurators类型的URLconfigurators类型的URL又分为三类
// 第一类是注册中心Configurators目录下新增的URL(override协议)
// 第二类是通过ConsumerConfigurationListener监听器(监听应用级别的配置)得到的动态配置
// 第三类是通过ReferenceConfigurationListener监听器(监听服务级别的配置)得到的动态配置
// 这里只需要先了解除了注册中心的configurators目录下有配置信息之外还有可以在服务治理控制台动态添加配置
// ConsumerConfigurationListener、ReferenceConfigurationListener监听器就是用来监听服务治理控制台的动态配置的
// 至于服务治理控制台的具体使用,在后面详细介绍
providerUrl = overrideWithConfigurator(providerUrl);
// 增加check=false即只有在调用时才检查Provider是否可用
providerUrl = providerUrl.addParameter(Constants.CHECK_KEY, String.valueOf(false));
// 重新复制overrideDirectoryUrlproviderUrl在经过第一步参数合并后包含override协议覆盖后的属性赋值给overrideDirectoryUrl。
this.overrideDirectoryUrl = this.overrideDirectoryUrl.addParametersIfAbsent(providerUrl.getParameters());
... // 省略对Dubbo低版本的兼容处理逻辑
return providerUrl;
}
完成 URL 到 Invoker 对象的转换toInvokers() 方法)之后,其实在 refreshInvoker() 方法的最后,还会根据 multiGroup 的配置决定是否调用 toMergeInvokerList() 方法将每个 group 中的 Invoker 合并成一个 Invoker。下面我们一起来看 toMergeInvokerList() 方法的具体实现:
private List<Invoker<T>> toMergeInvokerList(List<Invoker<T>> invokers) {
List<Invoker<T>> mergedInvokers = new ArrayList<>();
Map<String, List<Invoker<T>>> groupMap = new HashMap<>();
for (Invoker<T> invoker : invokers) { // 按照group将Invoker分组
String group = invoker.getUrl().getParameter(GROUP_KEY, "");
groupMap.computeIfAbsent(group, k -> new ArrayList<>());
groupMap.get(group).add(invoker);
}
if (groupMap.size() == 1) { // 如果只有一个group则直接使用该group分组对应的Invoker集合作为mergedInvokers
mergedInvokers.addAll(groupMap.values().iterator().next());
} else if (groupMap.size() > 1) { // 将每个group对应的Invoker集合合并成一个Invoker
for (List<Invoker<T>> groupList : groupMap.values()) {
// 这里使用到StaticDirectory以及Cluster合并每个group中的Invoker
StaticDirectory<T> staticDirectory = new StaticDirectory<>(groupList);
staticDirectory.buildRouterChain();
mergedInvokers.add(CLUSTER.join(staticDirectory));
}
} else {
mergedInvokers = invokers;
}
return mergedInvokers;
}
这里使用到了 Cluster 接口的相关功能,我们在后面课时还会继续深入分析 Cluster 接口及其实现,你现在可以将 Cluster 理解为一个黑盒,知道其 join() 方法会将多个 Invoker 对象转换成一个 Invoker 对象即可。
到此为止RegistryDirectory 处理一次完整的动态 Provider 发现流程就介绍完了。
最后我们再分析下RegistryDirectory 中另外一个核心方法—— doList() 方法,该方法是 AbstractDirectory 留给其子类实现的一个方法,也是通过 Directory 接口获取 Invoker 集合的核心所在,具体实现如下:
public List<Invoker<T>> doList(Invocation invocation) {
if (forbidden) { // 检测forbidden字段当该字段在refreshInvoker()过程中设置为true时表示无Provider可用直接抛出异常
throw new RpcException("...");
}
if (multiGroup) {
// multiGroup为true时的特殊处理在refreshInvoker()方法中针对multiGroup为true的场景已经使用Router进行了筛选所以这里直接返回接口
return this.invokers == null ? Collections.emptyList() : this.invokers;
}
List<Invoker<T>> invokers = null;
// 通过RouterChain.route()方法筛选Invoker集合最终得到符合路由条件的Invoker集合
invokers = routerChain.route(getConsumerUrl(), invocation);
return invokers == null ? Collections.emptyList() : invokers;
}
总结
在本课时,我们首先介绍了 dubbo-cluster 模块的整体架构,简单说明了 Cluster、Directory、Router、LoadBalance 四个核心接口的功能。接下来我们就深入介绍了 Directory 接口的定义以及 StaticDirectory、RegistryDirectory 两个类的核心实现,其中 RegistryDirectory 涉及动态查找 Provider URL 以及处理动态配置的相关逻辑,显得略微复杂了一点,希望你能耐心学习和理解。关于这部分内容,你若有不懂或不理解的地方,也欢迎你留言和我交流。

View File

@@ -0,0 +1,495 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
32 路由机制:请求到底怎么走,它说了算(上)
作为 dubbo-cluster 模块分析的第二课时,本课时我们就来介绍一下 dubbo-cluster 模块中涉及的另一个核心概念—— Router。
Router 的主要功能就是根据用户配置的路由规则以及请求携带的信息,过滤出符合条件的 Invoker 集合,供后续负载均衡逻辑使用。在上一课时介绍 RegistryDirectory 实现的时候,我们就已经看到了 RouterChain 这个 Router 链的存在,但是没有深入分析,下面我们就来深入 Router 进行分析。
RouterChain、RouterFactory 与 Router
首先我们来看 RouterChain 的核心字段。
invokersList`> 类型):当前 RouterChain 对象要过滤的 Invoker 集合。我们可以看到,在 StaticDirectory 中是通过 RouterChain.setInvokers() 方法进行设置的。
builtinRoutersList<Router> 类型):当前 RouterChain 激活的内置 Router 集合。
routersList<Router> 类型):当前 RouterChain 中真正要使用的 Router 集合,其中不仅包括了上面 builtinRouters 集合中全部的 Router 对象,还包括通过 addRouters() 方法添加的 Router 对象。
在 RouterChain 的构造函数中,会在传入的 URL 参数中查找 router 参数值,并根据该值获取确定激活的 RouterFactory之后通过 Dubbo SPI 机制加载这些激活的 RouterFactory 对象,由 RouterFactory 创建当前激活的内置 Router 实例,具体实现如下:
private RouterChain(URL url) {
// 通过ExtensionLoader加载激活的RouterFactory
List<RouterFactory> extensionFactories = ExtensionLoader.getExtensionLoader(RouterFactory.class)
.getActivateExtension(url, "router");
// 遍历所有RouterFactory调用其getRouter()方法创建相应的Router对象
List<Router> routers = extensionFactories.stream()
.map(factory -> factory.getRouter(url))
.collect(Collectors.toList());
initWithRouters(routers); // 初始化buildinRouters字段以及routers字段
}
public void initWithRouters(List<Router> builtinRouters) {
this.builtinRouters = builtinRouters;
this.routers = new ArrayList<>(builtinRouters);
this.sort(); // 这里会对routers集合进行排序
}
完成内置 Router 的初始化之后,在 Directory 实现中还可以通过 addRouter() 方法添加新的 Router 实例到 routers 字段中,具体实现如下:
public void addRouters(List<Router> routers) {
List<Router> newRouters = new ArrayList<>();
newRouters.addAll(builtinRouters); // 添加builtinRouters集合
newRouters.addAll(routers); // 添加传入的Router集合
CollectionUtils.sort(newRouters); // 重新排序
this.routers = newRouters;
}
RouterChain.route() 方法会遍历 routers 字段,逐个调用 Router 对象的 route() 方法,对 invokers 集合进行过滤,具体实现如下:
public List<Invoker<T>> route(URL url, Invocation invocation) {
List<Invoker<T>> finalInvokers = invokers;
for (Router router : routers) { // 遍历全部的Router对象
finalInvokers = router.route(finalInvokers, url, invocation);
}
return finalInvokers;
}
了解了 RouterChain 的大致逻辑之后,我们知道真正进行路由的是 routers 集合中的 Router 对象。接下来我们再来看 RouterFactory 这个工厂接口RouterFactory 接口是一个扩展接口,具体定义如下:
@SPI
public interface RouterFactory {
@Adaptive("protocol") // 动态生成的适配器会根据protocol参数选择扩展实现
Router getRouter(URL url);
}
RouterFactory 接口有很多实现类,如下图所示:
RouterFactory 继承关系图
下面我们就来深入介绍下每个 RouterFactory 实现类以及对应的 Router 实现对象。Router 决定了一次 Dubbo 调用的目标服务Router 接口的每个实现类代表了一个路由规则,当 Consumer 访问 Provider 时Dubbo 根据路由规则筛选出合适的 Provider 列表之后通过负载均衡算法再次进行筛选。Router 接口的继承关系如下图所示:
Router 继承关系图
接下来我们就开始介绍 RouterFactory 以及 Router 的具体实现。
ConditionRouterFactory&ConditionRouter
首先来看 ConditionRouterFactory 实现,其扩展名为 condition在其 getRouter() 方法中会创建 ConditionRouter 对象,如下所示:
public Router getRouter(URL url) {
return new ConditionRouter(url);
}
ConditionRouter 是基于条件表达式的路由实现类,下面就是一条基于条件表达式的路由规则:
host = 192.168.0.100 => host = 192.168.0.150
在上述规则中,=>之前的为 Consumer 匹配的条件,该条件中的所有参数会与 Consumer 的 URL 进行对比,当 Consumer 满足匹配条件时,会对该 Consumer 的此次调用执行 => 后面的过滤规则。
=> 之后为 Provider 地址列表的过滤条件,该条件中的所有参数会和 Provider 的 URL 进行对比Consumer 最终只拿到过滤后的地址列表。
如果 Consumer 匹配条件为空,表示 => 之后的过滤条件对所有 Consumer 生效,例如:=> host != 192.168.0.150,含义是所有 Consumer 都不能请求 192.168.0.150 这个 Provider 节点。
如果 Provider 过滤条件为空,表示禁止访问所有 Provider例如host = 192.168.0.100 =>,含义是 192.168.0.100 这个 Consumer 不能访问任何 Provider 节点。
ConditionRouter 的核心字段有如下几个。
urlURL 类型):路由规则的 URL可以从 rule 参数中获取具体的路由规则。
ROUTE_PATTERNPattern 类型):用于切分路由规则的正则表达式。
priorityint 类型):路由规则的优先级,用于排序,该字段值越大,优先级越高,默认值为 0。
forceboolean 类型):当路由结果为空时,是否强制执行。如果不强制执行,则路由结果为空的路由规则将会自动失效;如果强制执行,则直接返回空的路由结果。
whenConditionMap 类型Consumer 匹配的条件集合,通过解析条件表达式 rule 的 => 之前半部分,可以得到该集合中的内容。
thenConditionMap 类型Provider 匹配的条件集合,通过解析条件表达式 rule 的 => 之后半部分,可以得到该集合中的内容。
在 ConditionRouter 的构造方法中,会根据 URL 中携带的相应参数初始化 priority、force、enable 等字段,然后从 URL 的 rule 参数中获取路由规则进行解析,具体的解析逻辑是在 init() 方法中实现的,如下所示:
public void init(String rule) {
// 将路由规则中的"consumer."和"provider."字符串清理掉
rule = rule.replace("consumer.", "").replace("provider.", "");
// 按照"=>"字符串进行分割得到whenRule和thenRule两部分
int i = rule.indexOf("=>");
String whenRule = i < 0 ? null : rule.substring(0, i).trim();
String thenRule = i < 0 ? rule.trim() : rule.substring(i + 2).trim();
// 解析whenRule和thenRule得到whenCondition和thenCondition两个条件集合
Map<String, MatchPair> when = StringUtils.isBlank(whenRule) || "true".equals(whenRule) ? new HashMap<String, MatchPair>() : parseRule(whenRule);
Map<String, MatchPair> then = StringUtils.isBlank(thenRule) || "false".equals(thenRule) ? null : parseRule(thenRule);
this.whenCondition = when;
this.thenCondition = then;
}
whenCondition 和 thenCondition 两个集合中Key 是条件表达式中指定的参数名称(例如 host = 192.168.0.150 这个表达式中的 host。ConditionRouter 支持三类参数:
服务调用信息例如method、argument 等;
URL 本身的字段例如protocol、host、port 等;
URL 上的所有参数例如application 等。
Value 是 MatchPair 对象,包含两个 Set 类型的集合—— matches 和 mismatches。在使用 MatchPair 进行过滤的时候,会按照下面四条规则执行。
当 mismatches 集合为空的时候,会逐个遍历 matches 集合中的匹配条件,匹配成功任意一条即会返回 true。这里具体的匹配逻辑以及后续 mismatches 集合中条件的匹配逻辑,都是在 UrlUtils.isMatchGlobPattern() 方法中实现,其中完成了如下操作:如果匹配条件以 “$” 符号开头,则从 URL 中获取相应的参数值进行匹配;当遇到 “” 通配符的时候,会处理”“通配符在匹配条件开头、中间以及末尾三种情况。
当 matches 集合为空的时候,会逐个遍历 mismatches 集合中的匹配条件,匹配成功任意一条即会返回 false。
当 matches 集合和 mismatches 集合同时不为空时,会优先匹配 mismatches 集合中的条件,成功匹配任意一条规则,就会返回 false若 mismatches 中的条件全部匹配失败,才会开始匹配 matches 集合,成功匹配任意一条规则,就会返回 true。
当上述三个步骤都没有成功匹配时,直接返回 false。
上述流程具体实现在 MatchPair 的 isMatch() 方法中,比较简单,这里就不再展示。
了解了每个 MatchPair 的匹配流程之后我们来看parseRule() 方法是如何解析一条完整的条件表达式,生成对应 MatchPair 的,具体实现如下:
private static Map<String, MatchPair> parseRule(String rule) throws ParseException {
Map<String, MatchPair> condition = new HashMap<String, MatchPair>();
MatchPair pair = null;
Set<String> values = null;
// 首先按照ROUTE_PATTERN指定的正则表达式匹配整个条件表达式
final Matcher matcher = ROUTE_PATTERN.matcher(rule);
while (matcher.find()) { // 遍历匹配的结果
// 每个匹配结果有两部分(分组),第一部分是分隔符,第二部分是内容
String separator = matcher.group(1);
String content = matcher.group(2);
if (StringUtils.isEmpty(separator)) { // ---(1) 没有分隔符content即为参数名称
pair = new MatchPair();
// 初始化MatchPair对象并将其与对应的Key(即content)记录到condition集合中
condition.put(content, pair);
}
else if ("&".equals(separator)) { // ---(4)
// &分隔符表示多个表达式,会创建多个MatchPair对象
if (condition.get(content) == null) {
pair = new MatchPair();
condition.put(content, pair);
} else {
pair = condition.get(content);
}
}else if ("=".equals(separator)) { // ---(2)
// =以及!=两个分隔符表示KV的分界线
if (pair == null) {
throw new ParseException("..."");
}
values = pair.matches;
values.add(content);
}else if ("!=".equals(separator)) { // ---(5)
if (pair == null) {
throw new ParseException("...");
}
values = pair.mismatches;
values.add(content);
}else if (",".equals(separator)) { // ---(3)
// 逗号分隔符表示有多个Value值
if (values == null || values.isEmpty()) {
throw new ParseException("...");
}
values.add(content);
} else {
throw new ParseException("...");
}
}
return condition;
}
介绍完 parseRule() 方法的实现之后,我们可以再通过下面这个条件表达式示例的解析流程,更深入地体会 parseRule() 方法的工作原理:
host = 2.2.2.2,1.1.1.1,3.3.3.3 & method !=get => host = 1.2.3.4
经过 ROUTE_PATTERN 正则表达式的分组之后,我们得到如下分组:
Rule 分组示意图
我们先来看 => 之前的 Consumer 匹配规则的处理。
分组 1 中separator 为空字符串content 为 host 字符串。此时会进入上面示例代码展示的 parseRule() 方法中1处的分支创建 MatchPair 对象,并以 host 为 Key 记录到 condition 集合中。
分组 2 中separator 为 “=” 空字符串content 为 “2.2.2.2” 字符串。处理该分组时,会进入 parseRule() 方法中2 处的分支,在 MatchPair 的 matches 集合中添加 “2.2.2.2” 字符串。
分组 3 中separator 为 “,” 字符串content 为 “3.3.3.3” 字符串。处理该分组时,会进入 parseRule() 方法中3处的分支继续向 MatchPair 的 matches 集合中添加 “3.3.3.3” 字符串。
分组 4 中separator 为 “&” 字符串content 为 “method” 字符串。处理该分组时,会进入 parseRule() 方法中4处的分支创建新的 MatchPair 对象,并以 method 为 Key 记录到 condition 集合中。
分组 5 中separator 为 “!=” 字符串content 为 “get” 字符串。处理该分组时,会进入 parseRule() 方法中5处的分支向步骤 4 新建的 MatchPair 对象中的 mismatches 集合添加 “get” 字符串。
最后,我们得到的 whenCondition 集合如下图所示:
whenCondition 集合示意图
同理parseRule() 方法解析上述表达式 => 之后的规则得到的 thenCondition 集合,如下图所示:
thenCondition 集合示意图
了解了 ConditionRouter 解析规则的流程以及 MatchPair 内部的匹配原则之后ConditionRouter 中最后一个需要介绍的内容就是它的 route() 方法了。
ConditionRouter.route() 方法首先会尝试前面创建的 whenCondition 集合,判断此次发起调用的 Consumer 是否符合表达式中 => 之前的 Consumer 过滤条件,若不符合,直接返回整个 invokers 集合;若符合,则通过 thenCondition 集合对 invokers 集合进行过滤,得到符合 Provider 过滤条件的 Invoker 集合然后返回给上层调用方。ConditionRouter.route() 方法的核心实现如下:
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
throws RpcException {
... // 通过enable字段判断当前ConditionRouter对象是否可用
... // 当前invokers集合为空则直接返回
if (!matchWhen(url, invocation)) { // 匹配发起请求的Consumer是否符合表达式中=>之前的过滤条件
return invokers;
}
List<Invoker<T>> result = new ArrayList<Invoker<T>>();
if (thenCondition == null) { // 判断=>之后是否存在Provider过滤条件若不存在则直接返回空集合表示无Provider可用
return result;
}
for (Invoker<T> invoker : invokers) { // 逐个判断Invoker是否符合表达式中=>之后的过滤条件
if (matchThen(invoker.getUrl(), url)) {
result.add(invoker); // 记录符合条件的Invoker
}
}
if (!result.isEmpty()) {
return result;
} else if (force) { // 在无Invoker符合条件时根据force决定是返回空集合还是返回全部Invoker
return result;
}
return invokers;
}
ScriptRouterFactory&ScriptRouter
ScriptRouterFactory 的扩展名为 script其 getRouter() 方法中会创建一个 ScriptRouter 对象并返回。
ScriptRouter 支持 JDK 脚本引擎的所有脚本例如JavaScript、JRuby、Groovy 等,通过 type=javascript 参数设置脚本类型,缺省为 javascript。下面我们就定义一个 route() 函数进行 host 过滤:
function route(invokers, invocation, context){
var result = new java.util.ArrayList(invokers.size());
var targetHost = new java.util.ArrayList();
targetHost.add("10.134.108.2");
for (var i = 0; i < invokers.length; i) { // 遍历Invoker集合
// 判断Invoker的host是否符合条件
if(targetHost.contains(invokers[i].getUrl().getHost())){
result.add(invokers[i]);
}
}
return result;
}
route(invokers, invocation, context) // 立即执行route()函数
我们可以将上面这段代码进行编码并作为 rule 参数的值添加到 URL 在这个 URL 传入 ScriptRouter 的构造函数时即可被 ScriptRouter 解析
ScriptRouter 的核心字段有如下几个
urlURL 类型路由规则的 URL可以从 rule 参数中获取具体的路由规则
priorityint 类型路由规则的优先级用于排序该字段值越大优先级越高默认值为 0
ENGINESConcurrentHashMap 类型这是一个 static 集合其中的 Key 是脚本语言的名称Value 是对应的 ScriptEngine 对象这里会按照脚本语言的类型复用 ScriptEngine 对象
engineScriptEngine 类型当前 ScriptRouter 使用的 ScriptEngine 对象
ruleString 类型当前 ScriptRouter 使用的具体脚本内容
functionCompiledScript 类型根据 rule 这个具体脚本内容编译得到
ScriptRouter 的构造函数中首先会初始化 url 字段以及 priority 字段用于排序然后根据 URL 中的 type 参数初始化 enginerule function 三个核心字段 具体实现如下
public ScriptRouter(URL url) {
this.url = url;
this.priority = url.getParameter(PRIORITY_KEY, SCRIPT_ROUTER_DEFAULT_PRIORITY);
// 根据URL中的type参数值从ENGINES集合中获取对应的ScriptEngine对象
engine = getEngine(url);
// 获取URL中的rule参数值即为具体的脚本
rule = getRule(url);
Compilable compilable = (Compilable) engine;
// 编译rule字段中的脚本得到function字段
function = compilable.compile(rule);
}
接下来看 ScriptRouter route() 方法的实现其中首先会创建调用 function 函数所需的入参也就是 Bindings 对象然后调用 function 函数得到过滤后的 Invoker 集合最后通过 getRoutedInvokers() 方法整理 Invoker 集合得到最终的返回值
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
// 创建Bindings对象作为function函数的入参
Bindings bindings = createBindings(invokers, invocation);
if (function == null) {
return invokers;
}
// 调用function函数并在getRoutedInvokers()方法中整理得到的Invoker集合
return getRoutedInvokers(function.eval(bindings));
}
private <T> Bindings createBindings(List<Invoker<T>> invokers, Invocation invocation) {
Bindings bindings = engine.createBindings();
// 与前面的javascript的示例脚本结合我们可以看到这里在Bindings中为脚本中的route()函数提供了invokers、Invocation、context三个参数
bindings.put("invokers", new ArrayList<>(invokers));
bindings.put("invocation", invocation);
bindings.put("context", RpcContext.getContext());
return bindings;
}
总结
本课时重点介绍了 Router 接口的相关内容。首先我们介绍了 RouterChain 的核心实现以及构建过程,然后讲解了 RouterFactory 接口和 Router 接口中核心方法的功能。接下来我们还深入分析了ConditionRouter 对条件路由功能的实现以及ScriptRouter 对脚本路由功能的实现。

View File

@@ -0,0 +1,371 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
34 加餐:初探 Dubbo 动态配置的那些事儿
在前面第 31 课时中我们详细讲解了 RegistryDirectory 相关的内容,作为一个 NotifyListener 监听器RegistryDirectory 会同时监听注册中心的 providers、routers 和 configurators 三个目录。通过 RegistryDirectory 处理 configurators 目录的逻辑,我们了解到 configurators 目录中动态添加的 URL 会覆盖 providers 目录下注册的 Provider URLDubbo 还会按照 configurators 目录下的最新配置,重新创建 Invoker 对象(同时会销毁原来的 Invoker 对象)。
在老版本的 Dubbo 中,我们可以通过服务治理控制台向注册中心的 configurators 目录写入动态配置的 URL。在 Dubbo 2.7.x 版本中,动态配置信息除了可以写入注册中心的 configurators 目录之外,还可以写入外部的配置中心,这部分内容我们将在后面的课时详细介绍,今天这一课时我们重点来看写入注册中心的动态配置。
首先,我们需要了解一下 configurators 目录中 URL 都有哪些协议以及这些协议的含义,然后还要知道 Dubbo 是如何解析这些 URL 得到 Configurator 对象的,以及 Configurator 是如何与已有的 Provider URL 共同作用得到实现动态更新配置的效果。
基础协议
首先,我们需要了解写入注册中心 configurators 中的动态配置有 override 和 absent 两种协议。下面是一个 override 协议的示例:
override://0.0.0.0/org.apache.dubbo.demo.DemoService?category=configurators&dynamic=false&enabled=true&application=dubbo-demo-api-consumer&timeout=1000
那这个 URL 中各个部分的含义是怎样的呢?下面我们就一个一个来分析下。
override表示采用覆盖方式。Dubbo 支持 override 和 absent 两种协议,我们也可以通过 SPI 的方式进行扩展。
0.0.0.0,表示对所有 IP 生效。如果只想覆盖某个特定 IP 的 Provider 配置,可以使用该 Provider 的具体 IP。
org.apache.dubbo.demo.DemoService表示只对指定服务生效。
category=configurators表示该 URL 为动态配置类型。
dynamic=false表示该 URL 为持久数据,即使注册该 URL 的节点退出,该 URL 依旧会保存在注册中心。
enabled=true表示该 URL 的覆盖规则已生效。
application=dubbo-demo-api-consumer表示只对指定应用生效。如果不指定则默认表示对所有应用都生效。
timeout=1000表示将满足以上条件 Provider URL 中的 timeout 参数值覆盖为 1000。如果想覆盖其他配置可以直接以参数的形式添加到 override URL 之上。
在 Dubbo 的官网中,还提供了一些简单示例,我们这里也简单解读一下。
禁用某个 Provider通常用于临时剔除某个 Provider 节点:
override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&disabled=true
调整某个 Provider 的权重为 200
override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&weight=200
调整负载均衡策略为 LeastActiveLoadBalance负载均衡的内容会在下一课时详细介绍
override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&loadbalance=leastactive
服务降级通常用于临时屏蔽某个出错的非关键服务mock 机制的具体实现我们会在后面的课时详细介绍):
override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null
Configurator
当我们在注册中心的 configurators 目录中添加 override或 absent协议的 URL 时Registry 会收到注册中心的通知,回调注册在其上的 NotifyListener其中就包括 RegistryDirectory。我们在第 31 课时中已经详细分析了 RegistryDirectory.notify() 处理 providers、configurators 和 routers 目录变更的流程,其中 configurators 目录下 URL 会被解析成 Configurator 对象。
Configurator 接口抽象了一条配置信息,同时提供了将配置 URL 解析成 Configurator 对象的工具方法。Configurator 接口具体定义如下:
public interface Configurator extends Comparable<Configurator> {
// 获取该Configurator对象对应的配置URL例如前文介绍的override协议URL
URL getUrl();
// configure()方法接收的参数是原始URL返回经过Configurator修改后的URL
URL configure(URL url);
// toConfigurators()工具方法可以将多个配置URL对象解析成相应的Configurator对象
static Optional<List<Configurator>> toConfigurators(List<URL> urls) {
// 创建ConfiguratorFactory适配器
ConfiguratorFactory configuratorFactory = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.getAdaptiveExtension();
List<Configurator> configurators = new ArrayList<>(urls.size()); // 记录解析的结果
for (URL url : urls) {
// 遇到empty协议直接清空configurators集合结束解析返回空集合
if (EMPTY_PROTOCOL.equals(url.getProtocol())) {
configurators.clear();
break;
}
Map<String, String> override = new HashMap<>(url.getParameters());
override.remove(ANYHOST_KEY);
if (override.size() == 0) { // 如果该配置URL没有携带任何参数则跳过该URL
configurators.clear();
continue;
}
// 通过ConfiguratorFactory适配器选择合适ConfiguratorFactory扩展并创建Configurator对象
configurators.add(configuratorFactory.getConfigurator(url));
}
Collections.sort(configurators); // 排序
return Optional.of(configurators);
}
// 排序首先按照ip进行排序所有ip的优先级都高于0.0.0.0当ip相同时会按照priority参数值进行排序
default int compareTo(Configurator o) {
if (o == null) {
return -1;
}
int ipCompare = getUrl().getHost().compareTo(o.getUrl().getHost());
if (ipCompare == 0) {
int i = getUrl().getParameter(PRIORITY_KEY, 0);
int j = o.getUrl().getParameter(PRIORITY_KEY, 0);
return Integer.compare(i, j);
} else {
return ipCompare;
}
}
ConfiguratorFactory 接口是一个扩展接口Dubbo 提供了两个实现类,如下图所示:
ConfiguratorFactory 继承关系图
其中OverrideConfiguratorFactory 对应的扩展名为 override创建的 Configurator 实现是 OverrideConfiguratorAbsentConfiguratorFactory 对应的扩展名是 absent创建的 Configurator 实现类是 AbsentConfigurator。
Configurator 接口的继承关系如下图所示:
Configurator 继承关系图
其中AbstractConfigurator 中维护了一个 configuratorUrl 字段,记录了完整的配置 URL。AbstractConfigurator 是一个模板类,其核心实现是 configure() 方法,具体实现如下:
public URL configure(URL url) {
// 这里会根据配置URL的enabled参数以及host决定该URL是否可用同时还会根据原始URL是否为空以及原始URL的host是否为空决定当前是否执行后续覆盖逻辑
if (!configuratorUrl.getParameter(ENABLED_KEY, true) || configuratorUrl.getHost() == null || url == null || url.getHost() == null) {
return url;
}
// 针对2.7.0之后版本这里添加了一个configVersion参数作为区分
String apiVersion = configuratorUrl.getParameter(CONFIG_VERSION_KEY);
if (StringUtils.isNotEmpty(apiVersion)) { // 对2.7.0之后版本的配置处理
String currentSide = url.getParameter(SIDE_KEY);
String configuratorSide = configuratorUrl.getParameter(SIDE_KEY);
// 根据配置URL中的side参数以及原始URL中的side参数值进行匹配
if (currentSide.equals(configuratorSide) && CONSUMER.equals(configuratorSide) && 0 == configuratorUrl.getPort()) {
url = configureIfMatch(NetUtils.getLocalHost(), url);
} else if (currentSide.equals(configuratorSide) && PROVIDER.equals(configuratorSide) && url.getPort() == configuratorUrl.getPort()) {
url = configureIfMatch(url.getHost(), url);
}
} else { // 2.7.0版本之前对配置的处理
url = configureDeprecated(url);
}
return url;
}
这里我们需要关注下configureDeprecated() 方法对历史版本的兼容,其实这也是对注册中心 configurators 目录下配置 URL 的处理,具体实现如下:
private URL configureDeprecated(URL url) {
// 如果配置URL中的端口不为空则是针对Provider的需要判断原始URL的端口两者端口相同才能执行configureIfMatch()方法中的配置方法
if (configuratorUrl.getPort() != 0) {
if (url.getPort() == configuratorUrl.getPort()) {
return configureIfMatch(url.getHost(), url);
}
} else {
// 如果没有指定端口则该配置URL要么是针对Consumer的要么是针对任意URL的即host为0.0.0.0
// 如果原始URL属于Consumer则使用Consumer的host进行匹配
if (url.getParameter(SIDE_KEY, PROVIDER).equals(CONSUMER)) {
return configureIfMatch(NetUtils.getLocalHost(), url);
} else if (url.getParameter(SIDE_KEY, CONSUMER).equals(PROVIDER)) {
// 如果是Provider URL则用0.0.0.0来配置
return configureIfMatch(ANYHOST_VALUE, url);
}
}
return url;
}
configureIfMatch() 方法会排除匹配 URL 中不可动态修改的参数,并调用 Configurator 子类的 doConfigurator() 方法重写原始 URL具体实现如下
private URL configureIfMatch(String host, URL url) {
if (ANYHOST_VALUE.equals(configuratorUrl.getHost()) || host.equals(configuratorUrl.getHost())) { // 匹配host
String providers = configuratorUrl.getParameter(OVERRIDE_PROVIDERS_KEY);
if (StringUtils.isEmpty(providers) || providers.contains(url.getAddress()) || providers.contains(ANYHOST_VALUE)) {
String configApplication = configuratorUrl.getParameter(APPLICATION_KEY,
configuratorUrl.getUsername());
String currentApplication = url.getParameter(APPLICATION_KEY, url.getUsername());
if (configApplication == null || ANY_VALUE.equals(configApplication)
|| configApplication.equals(currentApplication)) { // 匹配application
// 排除不能动态修改的属性其中包括category、check、dynamic、enabled还有以~开头的属性
Set<String> conditionKeys = new HashSet<String>();
conditionKeys.add(CATEGORY_KEY);
conditionKeys.add(Constants.CHECK_KEY);
conditionKeys.add(DYNAMIC_KEY);
conditionKeys.add(ENABLED_KEY);
conditionKeys.add(GROUP_KEY);
conditionKeys.add(VERSION_KEY);
conditionKeys.add(APPLICATION_KEY);
conditionKeys.add(SIDE_KEY);
conditionKeys.add(CONFIG_VERSION_KEY);
conditionKeys.add(COMPATIBLE_CONFIG_KEY);
conditionKeys.add(INTERFACES);
for (Map.Entry<String, String> entry : configuratorUrl.getParameters().entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (key.startsWith("~") || APPLICATION_KEY.equals(key) || SIDE_KEY.equals(key)) {
conditionKeys.add(key);
// 如果配置URL与原URL中以~开头的参数值不相同则不使用该配置URL重写原URL
if (value != null && !ANY_VALUE.equals(value)
&& !value.equals(url.getParameter(key.startsWith("~") ? key.substring(1) : key))) {
return url;
}
}
}
// 移除配置URL不支持动态配置的参数之后调用Configurator子类的doConfigure方法重新生成URL
return doConfigure(url, configuratorUrl.removeParameters(conditionKeys));
}
}
}
return url;
}
我们再反过来仔细审视一下 AbstractConfigurator.configure() 方法中针对 2.7.0 版本之后动态配置的处理,其中会根据 side 参数明确判断配置 URL 和原始 URL 属于 Consumer 端还是 Provider 端,判断逻辑也更加清晰。匹配之后的具体替换过程同样是调用 configureIfMatch() 方法实现的,这里不再重复。
Configurator 的两个子类实现非常简单。在 OverrideConfigurator 的 doConfigure() 方法中,会直接用配置 URL 中剩余的全部参数,覆盖原始 URL 中的相应参数,具体实现如下:
public URL doConfigure(URL currentUrl, URL configUrl) {
// 直接调用addParameters()方法,进行覆盖
return currentUrl.addParameters(configUrl.getParameters());
}
在 AbsentConfigurator 的 doConfigure() 方法中,会尝试用配置 URL 中的参数添加到原始 URL 中,如果原始 URL 中已经有了该参数是不会被覆盖的,具体实现如下:
public URL doConfigure(URL currentUrl, URL configUrl) {
// 直接调用addParametersIfAbsent()方法尝试添加参数
return currentUrl.addParametersIfAbsent(configUrl.getParameters());
}
到这里Dubbo 2.7.0 版本之前的动态配置核心实现就介绍完了,其中我们也简单涉及了 Dubbo 2.7.0 版本之后一些逻辑,只不过没有全面介绍 Dubbo 2.7.0 之后的配置格式以及核心处理逻辑,不用担心,这些内容我们将会在后面的“配置中心”章节继续深入分析。
总结
本课时我们主要介绍了 Dubbo 中配置相关的实现。我们首先通过示例分析了 configurators 目录中涉及的 override 协议 URL、absent 协议 URL 的格式以及各个参数的含义,然后还详细讲解了 Dubbo 解析 configurator URL 得到的 Configurator 对象,以及 Configurator 覆盖 Provider URL 各个参数的具体实现。

View File

@@ -0,0 +1,452 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
35 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(上)
在前面的课时中,我们已经详细介绍了 dubbo-cluster 模块中的 Directory 和 Router 两个核心接口以及核心实现,同时也介绍了这两个接口相关的周边知识。本课时我们继续按照下图的顺序介绍 LoadBalance 的相关内容。
LoadBalance 核心接口图
LoadBalance负载均衡的职责是将网络请求或者其他形式的负载“均摊”到不同的服务节点上从而避免服务集群中部分节点压力过大、资源紧张而另一部分节点比较空闲的情况。
通过合理的负载均衡算法,我们希望可以让每个服务节点获取到适合自己处理能力的负载,实现处理能力和流量的合理分配。常用的负载均衡可分为软件负载均衡(比如,日常工作中使用的 Nginx和硬件负载均衡主要有 F5、Array、NetScaler 等,不过开发工程师在实践中很少直接接触到)。
常见的 RPC 框架中都有负载均衡的概念和相应的实现Dubbo 也不例外。Dubbo 需要对 Consumer 的调用请求进行分配,避免少数 Provider 节点负载过大,而剩余的其他 Provider 节点处于空闲的状态。因为当 Provider 负载过大时,就会导致一部分请求超时、丢失等一系列问题发生,造成线上故障。
Dubbo 提供了 5 种负载均衡实现,分别是:
基于 Hash 一致性的 ConsistentHashLoadBalance
基于权重随机算法的 RandomLoadBalance
基于最少活跃调用数算法的 LeastActiveLoadBalance
基于加权轮询算法的 RoundRobinLoadBalance
基于最短响应时间的 ShortestResponseLoadBalance 。
LoadBalance 接口
上述 Dubbo 提供的负载均衡实现,都是 LoadBalance 接口的实现类,如下图所示:
LoadBalance 继承关系图
LoadBalance 是一个扩展接口,默认使用的扩展实现是 RandomLoadBalance其定义如下所示其中的 @Adaptive 注解参数为 loadbalance即动态生成的适配器会按照 URL 中的 loadbalance 参数值选择扩展实现类。
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
@Adaptive("loadbalance")
<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}
LoadBalance 接口中 select() 方法的核心功能是根据传入的 URL 和 Invocation以及自身的负载均衡算法从 Invoker 集合中选择一个 Invoker 返回。
AbstractLoadBalance 抽象类并没有真正实现 select() 方法,只是对 Invoker 集合为空或是只包含一个 Invoker 对象的特殊情况进行了处理,具体实现如下:
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
if (CollectionUtils.isEmpty(invokers)) {
return null; // Invoker集合为空直接返回null
}
if (invokers.size() == 1) { // Invoker集合只包含一个Invoker则直接返回该Invoker对象
return invokers.get(0);
}
// Invoker集合包含多个Invoker对象时交给doSelect()方法处理,这是个抽象方法,留给子类具体实现
return doSelect(invokers, url, invocation);
}
另外AbstractLoadBalance 还提供了一个 getWeight() 方法,该方法用于计算 Provider 权重,具体实现如下:
int getWeight(Invoker<?> invoker, Invocation invocation) {
int weight;
URL url = invoker.getUrl();
if (REGISTRY_SERVICE_REFERENCE_PATH.equals(url.getServiceInterface())) {
// 如果是RegistryService接口的话直接获取权重即可
weight = url.getParameter(REGISTRY_KEY + "." + WEIGHT_KEY, DEFAULT_WEIGHT);
} else {
weight = url.getMethodParameter(invocation.getMethodName(), WEIGHT_KEY, DEFAULT_WEIGHT);
if (weight > 0) {
// 获取服务提供者的启动时间戳
long timestamp = invoker.getUrl().getParameter(TIMESTAMP_KEY, 0L);
if (timestamp > 0L) {
// 计算Provider运行时长
long uptime = System.currentTimeMillis() - timestamp;
if (uptime < 0) {
return 1;
}
// 计算Provider预热时长
int warmup = invoker.getUrl().getParameter(WARMUP_KEY, DEFAULT_WARMUP);
// 如果Provider运行时间小于预热时间则该Provider节点可能还在预热阶段需要重新计算服务权重(降低其权重)
if (uptime > 0 && uptime < warmup) {
weight = calculateWarmupWeight((int)uptime, warmup, weight);
}
}
}
}
return Math.max(weight, 0);
}
calculateWarmupWeight() 方法的目的是对还在预热状态的 Provider 节点进行降权避免 Provider 一启动就有大量请求涌进来服务预热是一个优化手段这是由 JVM 本身的一些特性决定的例如JIT 等方面的优化我们一般会在服务启动之后让其在小流量状态下运行一段时间然后再逐步放大流量
static int calculateWarmupWeight(int uptime, int warmup, int weight) {
// 计算权重随着服务运行时间uptime增大权重ww的值会慢慢接近配置值weight
int ww = (int) ( uptime / ((float) warmup / weight));
return ww < 1 ? 1 : (Math.min(ww, weight));
}
了解了 LoadBalance 接口的定义以及 AbstractLoadBalance 提供的公共能力之后下面我们开始逐个介绍 LoadBalance 接口的具体实现
ConsistentHashLoadBalance
ConsistentHashLoadBalance 底层使用一致性 Hash 算法实现负载均衡为了让你更好地理解这部分内容我们先来简单介绍一下一致性 Hash 算法相关的知识点
1. 一致性 Hash 简析
一致性 Hash 负载均衡可以让参数相同的请求每次都路由到相同的服务节点上这种负载均衡策略可以在某些 Provider 节点下线的时候让这些节点上的流量平摊到其他 Provider 不会引起流量的剧烈波动
下面我们通过一个示例简单介绍一致性 Hash 算法的原理
假设现在有 123 三个 Provider 节点对外提供服务 100 个请求同时到达如果想让请求尽可能均匀地分布到这三个 Provider 节点上我们可能想到的最简单的方法就是 Hash 取模 hash(请求参数) % 3如果参与 Hash 计算的是请求的全部参数那么参数相同的请求将会落到同一个 Provider 节点上不过此时如果突然有一个 Provider 节点出现宕机的情况那我们就需要对 2 取模即请求会重新分配到相应的 Provider 之上在极端情况下甚至会出现所有请求的处理节点都发生了变化这就会造成比较大的波动
为了避免因一个 Provider 节点宕机而导致大量请求的处理节点发生变化的情况我们可以考虑使用一致性 Hash 算法一致性 Hash 算法的原理也是取模算法 Hash 取模的不同之处在于Hash 取模是对 Provider 节点数量取模而一致性 Hash 算法是对 2^32 取模
一致性 Hash 算法需要同时对 Provider 地址以及请求参数进行取模
hash(Provider地址) % 2^32
hash(请求参数) % 2^32
Provider 地址和请求经过对 2^32 取模得到的结果值都会落到一个 Hash 环上如下图所示
一致性 Hash 节点均匀分布图
我们按顺时针的方向依次将请求分发到对应的 Provider这样当某台 Provider 节点宕机或增加新的 Provider 节点时只会影响这个 Provider 节点对应的请求
在理想情况下一致性 Hash 算法会将这三个 Provider 节点均匀地分布到 Hash 环上请求也可以均匀地分发给这三个 Provider 节点但在实际情况中这三个 Provider 节点地址取模之后的值可能差距不大这样会导致大量的请求落到一个 Provider 节点上如下图所示
一致性 Hash 节点非均匀分布图
这就出现了数据倾斜的问题所谓数据倾斜是指由于节点不够分散导致大量请求落到了同一个节点上而其他节点只会接收到少量请求的情况
为了解决一致性 Hash 算法中出现的数据倾斜问题又演化出了 Hash 槽的概念
Hash 槽解决数据倾斜的思路是既然问题是由 Provider 节点在 Hash 环上分布不均匀造成的那么可以虚拟出 n P1P2P3 Provider 节点 让多组 Provider 节点相对均匀地分布在 Hash 环上如下图所示相同阴影的节点均为同一个 Provider 节点比如 P1-1P1-2……P1-99 表示的都是 P1 这个 Provider 节点引入 Provider 虚拟节点之后 Provider 在圆环上分散开来以避免数据倾斜问题
数据倾斜解决示意图
2. ConsistentHashSelector 实现分析
了解了一致性 Hash 算法的基本原理之后我们再来看一下 ConsistentHashLoadBalance 一致性 Hash 负载均衡的具体实现首先来看 doSelect() 方法的实现其中会根据 ServiceKey methodName 选择一个 ConsistentHashSelector 对象核心算法都委托给 ConsistentHashSelector 对象完成
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 获取调用的方法名称
String methodName = RpcUtils.getMethodName(invocation);
// 将ServiceKey和方法拼接起来构成一个key
String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
// 注意这是为了在invokers列表发生变化时都会重新生成ConsistentHashSelector对象
int invokersHashCode = invokers.hashCode();
// 根据key获取对应的ConsistentHashSelector对象selectors是一个ConcurrentMap<String, ConsistentHashSelector>集合
ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
if (selector == null || selector.identityHashCode != invokersHashCode) { // 未查找到ConsistentHashSelector对象则进行创建
selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, invokersHashCode));
selector = (ConsistentHashSelector<T>) selectors.get(key);
}
// 通过ConsistentHashSelector对象选择一个Invoker对象
return selector.select(invocation);
}
下面我们来看 ConsistentHashSelector其核心字段如下所示。
virtualInvokersTreeMap`> 类型):用于记录虚拟 Invoker 对象的 Hash 环。这里使用 TreeMap 实现 Hash 环,并将虚拟的 Invoker 对象分布在 Hash 环上。
replicaNumberint 类型):虚拟 Invoker 个数。
identityHashCodeint 类型Invoker 集合的 HashCode 值。
argumentIndexint[] 类型):需要参与 Hash 计算的参数索引。例如argumentIndex = [0, 1, 2] 时,表示调用的目标方法的前三个参数要参与 Hash 计算。
接下来看 ConsistentHashSelector 的构造方法,其中的主要任务是:
构建 Hash 槽;
确认参与一致性 Hash 计算的参数,默认是第一个参数。
这些操作的目的就是为了让 Invoker 尽可能均匀地分布在 Hash 环上,具体实现如下:
ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
// 初始化virtualInvokers字段也就是虚拟Hash槽
this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
// 记录Invoker集合的hashCode用该hashCode值来判断Provider列表是否发生了变化
this.identityHashCode = identityHashCode;
URL url = invokers.get(0).getUrl();
// 从hash.nodes参数中获取虚拟节点的个数
this.replicaNumber = url.getMethodParameter(methodName, HASH_NODES, 160);
// 获取参与Hash计算的参数下标值默认对第一个参数进行Hash运算
String[] index = COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, HASH_ARGUMENTS, "0"));
argumentIndex = new int[index.length];
for (int i = 0; i < index.length; i++) {
argumentIndex[i] = Integer.parseInt(index[i]);
}
// 构建虚拟Hash槽默认replicaNumber=160相当于在Hash槽上放160个槽位
// 外层轮询40次内层轮询4次共40*4=160次也就是同一节点虚拟出160个槽位
for (Invoker<T> invoker : invokers) {
String address = invoker.getUrl().getAddress();
for (int i = 0; i < replicaNumber / 4; i++) {
// 对address + i进行md5运算得到一个长度为16的字节数组
byte[] digest = md5(address + i);
// 对digest部分字节进行4次Hash运算得到4个不同的long型正整数
for (int h = 0; h < 4; h++) {
// h = 0 digest 中下标为 0~3 4 个字节进行位运算
// h = 1 digest 中下标为 4~7 4 个字节进行位运算
// h = 2 h = 3时过程同上
long m = hash(digest, h);
virtualInvokers.put(m, invoker);
}
}
}
}
最后请求会通过 ConsistentHashSelector.select() 方法选择合适的 Invoker 对象其中会先对请求参数进行 md5 以及 Hash 运算得到一个 Hash 然后再通过这个 Hash 值到 TreeMap 中查找目标 Invoker具体实现如下
public Invoker<T> select(Invocation invocation) {
// 将参与一致性Hash的参数拼接到一起
String key = toKey(invocation.getArguments());
// 计算key的Hash值
byte[] digest = md5(key);
// 匹配Invoker对象
return selectForKey(hash(digest, 0));
}
private Invoker<T> selectForKey(long hash) {
// 从virtualInvokers集合TreeMap是按照Key排序的中查找第一个节点值大于或等于传入Hash值的Invoker对象
Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
// 如果Hash值大于Hash环中的所有Invoker则回到Hash环的开头返回第一个Invoker对象
if (entry == null) {
entry = virtualInvokers.firstEntry();
}
return entry.getValue();
}
RandomLoadBalance
RandomLoadBalance 使用的负载均衡算法是加权随机算法。RandomLoadBalance 是一个简单、高效的负载均衡实现,它也是 Dubbo 默认使用的 LoadBalance 实现。
这里我们通过一个示例来说明加权随机算法的核心思想。假设我们有三个 Provider 节点 A、B、C它们对应的权重分别为 5、2、3权重总和为 10。现在把这些权重值放到一维坐标轴上[0, 5) 区间属于节点 A[5, 7) 区间属于节点 B[7, 10) 区间属于节点 C如下图所示
权重坐标轴示意图
下面我们通过随机数生成器在 [0, 10) 这个范围内生成一个随机数,然后计算这个随机数会落到哪个区间中。例如,随机生成 4就会落到 Provider A 对应的区间中,此时 RandomLoadBalance 就会返回 Provider A 这个节点。
接下来我们再来看 RandomLoadBalance 中 doSelect() 方法的实现,其核心逻辑分为三个关键点:
计算每个 Invoker 对应的权重值以及总权重值;
当各个 Invoker 权重值不相等时,计算随机数应该落在哪个 Invoker 区间中,返回对应的 Invoker 对象;
当各个 Invoker 权重值相同时,随机返回一个 Invoker 即可。
RandomLoadBalance 经过多次请求后,能够将调用请求按照权重值均匀地分配到各个 Provider 节点上。下面是 RandomLoadBalance 的核心实现:
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size();
boolean sameWeight = true;
// 计算每个Invoker对象对应的权重并填充到weights[]数组中
int[] weights = new int[length];
// 计算第一个Invoker的权重
int firstWeight = getWeight(invokers.get(0), invocation);
weights[0] = firstWeight;
// totalWeight用于记录总权重值
int totalWeight = firstWeight;
for (int i = 1; i < length; i++) {
// 计算每个Invoker的权重以及总权重totalWeight
int weight = getWeight(invokers.get(i), invocation);
weights[i] = weight;
// Sum
totalWeight += weight;
// 检测每个Provider的权重是否相同
if (sameWeight && weight != firstWeight) {
sameWeight = false;
}
}
// 各个Invoker权重值不相等时计算随机数落在哪个区间上
if (totalWeight > 0 && !sameWeight) {
// 随机获取一个[0, totalWeight) 区间内的数字
int offset = ThreadLocalRandom.current().nextInt(totalWeight);
// 循环让offset数减去Invoker的权重值当offset小于0时返回相应的Invoker
for (int i = 0; i < length; i++) {
offset -= weights[i];
if (offset < 0) {
return invokers.get(i);
}
}
}
// 各个Invoker权重值相同时随机返回一个Invoker即可
return invokers.get(ThreadLocalRandom.current().nextInt(length));
}
总结
本课时我们重点介绍了 Dubbo Cluster 层中负载均衡相关的内容首先我们介绍了 LoadBalance 接口的定义以及 AbstractLoadBalance 抽象类提供的公共能力然后我们还详细讲解了 ConsistentHashLoadBalance 的核心实现其中还简单说明了一致性 Hash 算法的基础知识点最后我们又一块儿分析了 RandomLoadBalance 的基本原理和核心实现

View File

@@ -0,0 +1,412 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
36 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(下)
在上一课时我们了解了 LoadBalance 接口定义以及 AbstractLoadBalance 抽象类的内容,还详细介绍了 ConsistentHashLoadBalance 以及 RandomLoadBalance 这两个实现类的核心原理和大致实现。本课时我们将继续介绍 LoadBalance 的剩余三个实现。
LeastActiveLoadBalance
LeastActiveLoadBalance 使用的是最小活跃数负载均衡算法。它认为当前活跃请求数越小的 Provider 节点,剩余的处理能力越多,处理请求的效率也就越高,那么该 Provider 在单位时间内就可以处理更多的请求,所以我们应该优先将请求分配给该 Provider 节点。
LeastActiveLoadBalance 需要配合 ActiveLimitFilter 使用ActiveLimitFilter 会记录每个接口方法的活跃请求数,在 LeastActiveLoadBalance 进行负载均衡时,只会从活跃请求数最少的 Invoker 集合里挑选 Invoker。
在 LeastActiveLoadBalance 的实现中,首先会选出所有活跃请求数最小的 Invoker 对象,之后的逻辑与 RandomLoadBalance 完全一样,即按照这些 Invoker 对象的权重挑选最终的 Invoker 对象。下面是 LeastActiveLoadBalance.doSelect() 方法的具体实现:
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 初始化Invoker数量
int length = invokers.size();
// 记录最小的活跃请求数
int leastActive = -1;
// 记录活跃请求数最小的Invoker集合的个数
int leastCount = 0;
// 记录活跃请求数最小的Invoker在invokers数组中的下标位置
int[] leastIndexes = new int[length];
// 记录活跃请求数最小的Invoker集合中每个Invoker的权重值
int[] weights = new int[length];
// 记录活跃请求数最小的Invoker集合中所有Invoker的权重值之和
int totalWeight = 0;
// 记录活跃请求数最小的Invoker集合中第一个Invoker的权重值
int firstWeight = 0;
// 活跃请求数最小的集合中所有Invoker的权重值是否相同
boolean sameWeight = true;
for (int i = 0; i < length; i++) { // 遍历所有Invoker获取活跃请求数最小的Invoker集合
Invoker<T> invoker = invokers.get(i);
// 获取该Invoker的活跃请求数
int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
// 获取该Invoker的权重
int afterWarmup = getWeight(invoker, invocation);
weights[i] = afterWarmup;
// 比较活跃请求数
if (leastActive == -1 || active < leastActive) {
// 当前的Invoker是第一个活跃请求数最小的Invoker则记录如下信息
leastActive = active; // 重新记录最小的活跃请求数
leastCount = 1; // 重新记录活跃请求数最小的Invoker集合个数
leastIndexes[0] = i; // 重新记录Invoker
totalWeight = afterWarmup; // 重新记录总权重值
firstWeight = afterWarmup; // 该Invoker作为第一个Invoker记录其权重值
sameWeight = true; // 重新记录是否权重值相等
} else if (active == leastActive) {
// 当前Invoker属于活跃请求数最小的Invoker集合
leastIndexes[leastCount++] = i; // 记录该Invoker的下标
totalWeight += afterWarmup; // 更新总权重
if (sameWeight && afterWarmup != firstWeight) {
sameWeight = false; // 更新权重值是否相等
}
}
}
// 如果只有一个活跃请求数最小的Invoker对象直接返回即可
if (leastCount == 1) {
return invokers.get(leastIndexes[0]);
}
// 下面按照RandomLoadBalance的逻辑从活跃请求数最小的Invoker集合中随机选择一个Invoker对象返回
if (!sameWeight && totalWeight > 0) {
int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);
for (int i = 0; i < leastCount; i++) {
int leastIndex = leastIndexes[i];
offsetWeight -= weights[leastIndex];
if (offsetWeight < 0) {
return invokers.get(leastIndex);
}
}
}
return invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]);
}
ActiveLimitFilter 以及底层的 RpcStatus 记录活跃请求数的具体原理在前面的[ 30 课时]中我们已经详细分析过了这里不再重复如果有不清楚的地方你可以回顾之前课时相关的内容
RoundRobinLoadBalance
RoundRobinLoadBalance 实现的是加权轮询负载均衡算法
轮询指的是将请求轮流分配给每个 Provider例如 ABC 三个 Provider 节点按照普通轮询的方式我们会将第一个请求分配给 Provider A将第二个请求分配给 Provider B第三个请求分配给 Provider C第四个请求再次分配给 Provider A如此循环往复
轮询是一种无状态负载均衡算法实现简单适用于集群中所有 Provider 节点性能相近的场景 但现实情况中就很难保证这一点了因为很容易出现集群中性能最好和最差的 Provider 节点处理同样流量的情况这就可能导致性能差的 Provider 节点各方面资源非常紧张甚至无法及时响应了但是性能好的 Provider 节点的各方面资源使用还较为空闲这时我们可以通过加权轮询的方式降低分配到性能较差的 Provider 节点的流量
加权之后分配给每个 Provider 节点的流量比会接近或等于它们的权重比例如Provider 节点 ABC 权重比为 5:1:1那么在 7 次请求中节点 A 将收到 5 次请求节点 B 会收到 1 次请求节点 C 则会收到 1 次请求
Dubbo 2.6.4 版本及之前RoundRobinLoadBalance 的实现存在一些问题例如选择 Invoker 的性能问题负载均衡时不够平滑等 Dubbo 2.6.5 版本之后这些问题都得到了修复所以这里我们就来介绍最新的 RoundRobinLoadBalance 实现
每个 Provider 节点有两个权重一个权重是配置的 weight该值在负载均衡的过程中不会变化另一个权重是 currentWeight该值会在负载均衡的过程中动态调整初始值为 0
当有新的请求进来时RoundRobinLoadBalance 会遍历 Invoker 列表并用对应的 currentWeight 加上其配置的权重遍历完成后再找到最大的 currentWeight将其减去权重总和然后返回相应的 Invoker 对象
下面我们通过一个示例说明 RoundRobinLoadBalance 的执行流程这里我们依旧假设 ABC 三个节点的权重比例为 5:1:1
处理第一个请求currentWeight 数组中的权重与配置的 weight 相加即从 [0, 0, 0] 变为 [5, 1, 1]接下来从中选择权重最大的 Invoker 作为结果即节点 A最后将节点 A currentWeight 值减去 totalWeight 最终得到 currentWeight 数组为 [-2, 1, 1]
处理第二个请求currentWeight 数组中的权重与配置的 weight 相加即从 [-2, 1, 1] 变为 [3, 2, 2]接下来从中选择权重最大的 Invoker 作为结果即节点 A最后将节点 A currentWeight 值减去 totalWeight 最终得到 currentWeight 数组为 [-4, 2, 2]
处理第三个请求currentWeight 数组中的权重与配置的 weight 相加即从 [-4, 2, 2] 变为 [1, 3, 3]接下来从中选择权重最大的 Invoker 作为结果即节点 B最后将节点 B currentWeight 值减去 totalWeight 最终得到 currentWeight 数组为 [1, -4, 3]
处理第四个请求currentWeight 数组中的权重与配置的 weight 相加即从 [1, -4, 3] 变为 [6, -3, 4]接下来从中选择权重最大的 Invoker 作为结果即节点 A最后将节点 A currentWeight 值减去 totalWeight 最终得到 currentWeight 数组为 [-1, -3, 4]
处理第五个请求currentWeight 数组中的权重与配置的 weight 相加即从 [-1, -3, 4] 变为 [4, -2, 5]接下来从中选择权重最大的 Invoker 作为结果即节点 C最后将节点 C currentWeight 值减去 totalWeight 最终得到 currentWeight 数组为 [4, -2, -2]
处理第六个请求currentWeight 数组中的权重与配置的 weight 相加即从 [4, -2, -2] 变为 [9, -1, -1]接下来从中选择权重最大的 Invoker 作为结果即节点 A最后将节点 A currentWeight 值减去 totalWeight 最终得到 currentWeight 数组为 [2, -1, -1]
处理第七个请求currentWeight 数组中的权重与配置的 weight 相加即从 [2, -1, -1] 变为 [7, 0, 0]接下来从中选择权重最大的 Invoker 作为结果即节点 A最后将节点 A currentWeight 值减去 totalWeight 最终得到 currentWeight 数组为 [0, 0, 0]
到此为止一个轮询的周期就结束了
而在 Dubbo 2.6.4 版本中上面示例的一次轮询结果是 [A, A, A, A, A, B, C]也就是说前 5 个请求会全部都落到 A 这个节点上这将会使节点 A 在短时间内接收大量的请求压力陡增而节点 B 和节点 C 此时没有收到任何请求处于完全空闲的状态这种瞬间分配不平衡的情况也就是前面提到的不平滑问题
RoundRobinLoadBalance 我们为每个 Invoker 对象创建了一个对应的 WeightedRoundRobin 对象用来记录配置的权重weight 字段以及随每次负载均衡算法执行变化的 current 权重current 字段
了解了 WeightedRoundRobin 这个内部类后我们再来看 RoundRobinLoadBalance.doSelect() 方法的具体实现
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
// 获取整个Invoker列表对应的WeightedRoundRobin映射表如果为空则创建一个新的WeightedRoundRobin映射表
ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.computeIfAbsent(key, k -> new ConcurrentHashMap<>());
int totalWeight = 0;
long maxCurrent = Long.MIN_VALUE;
long now = System.currentTimeMillis(); // 获取当前时间
Invoker<T> selectedInvoker = null;
WeightedRoundRobin selectedWRR = null;
for (Invoker<T> invoker : invokers) {
String identifyString = invoker.getUrl().toIdentityString();
int weight = getWeight(invoker, invocation);
// 检测当前Invoker是否有相应的WeightedRoundRobin对象没有则进行创建
WeightedRoundRobin weightedRoundRobin = map.computeIfAbsent(identifyString, k -> {
WeightedRoundRobin wrr = new WeightedRoundRobin();
wrr.setWeight(weight);
return wrr;
});
// 检测Invoker权重是否发生了变化若发生变化则更新WeightedRoundRobin的weight字段
if (weight != weightedRoundRobin.getWeight()) {
weightedRoundRobin.setWeight(weight);
}
// 让currentWeight加上配置的Weight
long cur = weightedRoundRobin.increaseCurrent();
// 设置lastUpdate字段
weightedRoundRobin.setLastUpdate(now);
// 寻找具有最大currentWeight的Invoker以及Invoker对应的WeightedRoundRobin
if (cur > maxCurrent) {
maxCurrent = cur;
selectedInvoker = invoker;
selectedWRR = weightedRoundRobin;
}
totalWeight += weight; // 计算权重总和
}
if (invokers.size() != map.size()) {
map.entrySet().removeIf(item -> now - item.getValue().getLastUpdate() > RECYCLE_PERIOD);
}
if (selectedInvoker != null) {
// 用currentWeight减去totalWeight
selectedWRR.sel(totalWeight);
// 返回选中的Invoker对象
return selectedInvoker;
}
return invokers.get(0);
}
ShortestResponseLoadBalance
ShortestResponseLoadBalance 是Dubbo 2.7 版本之后新增加的一个 LoadBalance 实现类。它实现了最短响应时间的负载均衡算法,也就是从多个 Provider 节点中选出调用成功的且响应时间最短的 Provider 节点,不过满足该条件的 Provider 节点可能有多个,所以还要再使用随机算法进行一次选择,得到最终要调用的 Provider 节点。
了解了 ShortestResponseLoadBalance 的核心原理之后,我们一起来看 ShortestResponseLoadBalance.doSelect() 方法的核心实现,如下所示:
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 记录Invoker集合的数量
int length = invokers.size();
// 用于记录所有Invoker集合中最短响应时间
long shortestResponse = Long.MAX_VALUE;
// 具有相同最短响应时间的Invoker个数
int shortestCount = 0;
// 存放所有最短响应时间的Invoker的下标
int[] shortestIndexes = new int[length];
// 存储每个Invoker的权重
int[] weights = new int[length];
// 存储权重总和
int totalWeight = 0;
// 记录第一个Invoker对象的权重
int firstWeight = 0;
// 最短响应时间Invoker集合中的Invoker权重是否相同
boolean sameWeight = true;
for (int i = 0; i < length; i++) {
Invoker<T> invoker = invokers.get(i);
RpcStatus rpcStatus = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName());
// 获取调用成功的平均时间,具体计算方式是:调用成功的请求数总数对应的总耗时 / 调用成功的请求数总数 = 成功调用的平均时间
// RpcStatus 的内容在前面课时已经介绍过了,这里不再重复
long succeededAverageElapsed = rpcStatus.getSucceededAverageElapsed();
// 获取的是该Provider当前的活跃请求数也就是当前正在处理的请求数
int active = rpcStatus.getActive();
// 计算一个处理新请求的预估值也就是如果当前请求发给这个Provider大概耗时多久处理完成
long estimateResponse = succeededAverageElapsed * active;
// 计算该Invoker的权重主要是处理预热
int afterWarmup = getWeight(invoker, invocation);
weights[i] = afterWarmup;
if (estimateResponse < shortestResponse) {
// 第一次找到Invoker集合中最短响应耗时的Invoker对象记录其相关信息
shortestResponse = estimateResponse;
shortestCount = 1;
shortestIndexes[0] = i;
totalWeight = afterWarmup;
firstWeight = afterWarmup;
sameWeight = true;
} else if (estimateResponse == shortestResponse) {
// 出现多个耗时最短的Invoker对象
shortestIndexes[shortestCount++] = i;
totalWeight += afterWarmup;
if (sameWeight && i > 0
&& afterWarmup != firstWeight) {
sameWeight = false;
}
}
}
if (shortestCount == 1) {
return invokers.get(shortestIndexes[0]);
}
// 如果耗时最短的所有Invoker对象的权重不相同则通过加权随机负载均衡的方式选择一个Invoker返回
if (!sameWeight && totalWeight > 0) {
int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);
for (int i = 0; i < shortestCount; i++) {
int shortestIndex = shortestIndexes[i];
offsetWeight -= weights[shortestIndex];
if (offsetWeight < 0) {
return invokers.get(shortestIndex);
}
}
}
// 如果耗时最短的所有Invoker对象的权重相同则随机返回一个
return invokers.get(shortestIndexes[ThreadLocalRandom.current().nextInt(shortestCount)]);
}
总结
今天我们紧接上一课时介绍了 LoadBalance 接口的剩余三个实现
我们首先介绍了 LeastActiveLoadBalance 实现它使用最小活跃数负载均衡算法选择当前请求最少的 Provider 节点处理最新的请求接下来介绍了 RoundRobinLoadBalance 实现它使用加权轮询负载均衡算法弥补了单纯的轮询负载均衡算法导致的问题同时随着 Dubbo 版本的升级也将其自身不够平滑的问题优化掉了最后介绍了 ShortestResponseLoadBalance 实现它会从响应时间最短的 Provider 节点中选择一个 Provider 节点来处理新请求

View File

@@ -0,0 +1,581 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
37 集群容错:一个好汉三个帮(上)
你好,我是杨四正,今天我和你分享的主题是集群容错:一个好汉三个帮(上篇)。
在前面的课时中,我们已经对 Directory、Router、LoadBalance 等概念进行了深入的剖析,本课时将重点分析 Cluster 接口的相关内容。
Cluster 接口提供了我们常说的集群容错功能。
集群中的单个节点有一定概率出现一些问题,例如,磁盘损坏、系统崩溃等,导致节点无法对外提供服务,因此在分布式 RPC 框架中,必须要重视这种情况。为了避免单点故障,我们的 Provider 通常至少会部署在两台服务器上,以集群的形式对外提供服务,对于一些负载比较高的服务,则需要部署更多 Provider 来抗住流量。
在 Dubbo 中,通过 Cluster 这个接口把一组可供调用的 Provider 信息组合成为一个统一的 Invoker 供调用方进行调用。经过 Router 过滤、LoadBalance 选址之后,选中一个具体 Provider 进行调用,如果调用失败,则会按照集群的容错策略进行容错处理。
Dubbo 默认内置了若干容错策略,并且每种容错策略都有自己独特的应用场景,我们可以通过配置选择不同的容错策略。如果这些内置容错策略不能满足需求,我们还可以通过自定义容错策略进行配置。
了解了上述背景知识之后,下面我们就正式开始介绍 Cluster 接口。
Cluster 接口与容错机制
Cluster 的工作流程大致可以分为两步(如下图所示):①创建 Cluster Invoker 实例(在 Consumer 初始化时Cluster 实现类会创建一个 Cluster Invoker 实例,即下图中的 merge 操作);②使用 Cluster Invoker 实例(在 Consumer 服务消费者发起远程调用请求的时候Cluster Invoker 会依赖前面课时介绍的 Directory、Router、LoadBalance 等组件得到最终要调用的 Invoker 对象)。
Cluster 核心流程图
Cluster Invoker 获取 Invoker 的流程大致可描述为如下:
通过 Directory 获取 Invoker 列表,以 RegistryDirectory 为例,会感知注册中心的动态变化,实时获取当前 Provider 对应的 Invoker 集合;
调用 Router 的 route() 方法进行路由,过滤掉不符合路由规则的 Invoker 对象;
通过 LoadBalance 从 Invoker 列表中选择一个 Invoker
ClusterInvoker 会将参数传给 LoadBalance 选择出的 Invoker 实例的 invoke 方法,进行真正的远程调用。
这个过程是一个正常流程没有涉及容错处理。Dubbo 中常见的容错方式有如下几个。
Failover Cluster失败自动切换。它是 Dubbo 的默认容错机制,在请求一个 Provider 节点失败的时候,自动切换其他 Provider 节点,默认执行 3 次,适合幂等操作。当然,重试次数越多,在故障容错的时候带给 Provider 的压力就越大,在极端情况下甚至可能造成雪崩式的问题。
Failback Cluster失败自动恢复。失败后记录到队列中通过定时器重试。
Failfast Cluster快速失败。请求失败后返回异常不进行任何重试。
Failsafe Cluster失败安全。请求失败后忽略异常不进行任何重试。
Forking Cluster并行调用多个 Provider 节点,只要有一个成功就返回。
Broadcast Cluster广播多个 Provider 节点,只要有一个节点失败就失败。
Available Cluster遍历所有的 Provider 节点,找到每一个可用的节点,就直接调用。如果没有可用的 Provider 节点,则直接抛出异常。
Mergeable Cluster请求多个 Provider 节点并将得到的结果进行合并。
下面我们再来看 Cluster 接口。Cluster 接口是一个扩展接口,通过 @SPI 注解的参数我们知道其使用的默认实现是 FailoverCluster它只定义了一个 join() 方法,在其上添加了 @Adaptive 注解,会动态生成适配器类,其中会优先根据 Directory.getUrl() 方法返回的 URL 中的 cluster 参数值选择扩展实现,若无 cluster 参数则使用默认的 FailoverCluster 实现。Cluster 接口的具体定义如下所示:
@SPI(FailoverCluster.NAME)
public interface Cluster {
@Adaptive
<T> Invoker<T> join(Directory<T> directory) throws RpcException;
}
Cluster 接口的实现类如下图所示,分别对应前面提到的多种容错策略:
Cluster 接口继承关系
在每个 Cluster 接口实现中,都会创建对应的 Invoker 对象,这些都继承自 AbstractClusterInvoker 抽象类,如下图所示:
AbstractClusterInvoker 继承关系图
通过上面两张继承关系图我们可以看出Cluster 接口和 Invoker 接口都会有相应的抽象实现类,这些抽象实现类都实现了一些公共能力。下面我们就来深入介绍 AbstractClusterInvoker 和 AbstractCluster 这两个抽象类。
AbstractClusterInvoker
了解了 Cluster Invoker 的继承关系之后,我们首先来看 AbstractClusterInvoker它有两点核心功能一个是实现的 Invoker 接口,对 Invoker.invoke() 方法进行通用的抽象实现;另一个是实现通用的负载均衡算法。
在 AbstractClusterInvoker.invoke() 方法中,会通过 Directory 获取 Invoker 列表,然后通过 SPI 初始化 LoadBalance最后调用 doInvoke() 方法执行子类的逻辑。在 Directory.list() 方法返回 Invoker 集合之前,已经使用 Router 进行了一次筛选,你可以回顾前面[第 31 课时]对 RegistryDirectory 的分析。
public Result invoke(final Invocation invocation) throws RpcException {
// 检测当前Invoker是否已销毁
checkWhetherDestroyed();
// 将RpcContext中的attachment添加到Invocation中
Map<String, Object> contextAttachments = RpcContext.getContext().getObjectAttachments();
if (contextAttachments != null && contextAttachments.size() != 0) {
((RpcInvocation) invocation).addObjectAttachments(contextAttachments);
}
// 通过Directory获取Invoker对象列表通过对RegistryDirectory的介绍我们知道其中已经调用了Router进行过滤
List<Invoker<T>> invokers = list(invocation);
// 通过SPI加载LoadBalance
LoadBalance loadbalance = initLoadBalance(invokers, invocation);
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
// 调用doInvoke()方法,该方法是个抽象方法
return doInvoke(invocation, invokers, loadbalance);
}
protected List<Invoker<T>> list(Invocation invocation) throws RpcException {
return directory.list(invocation); // 调用Directory.list()方法
}
下面我们来看一下 AbstractClusterInvoker 是如何按照不同的 LoadBalance 算法从 Invoker 集合中选取最终 Invoker 对象的。
AbstractClusterInvoker 并没有简单粗暴地使用 LoadBalance.select() 方法完成负载均衡,而是做了进一步的封装,具体实现在 select() 方法中。在 select() 方法中会根据配置决定是否开启粘滞连接特性,如果开启了,则需要将上次使用的 Invoker 缓存起来,只要 Provider 节点可用就直接调用,不会再进行负载均衡。如果调用失败,才会重新进行负载均衡,并且排除已经重试过的 Provider 节点。
// 第一个参数是此次使用的LoadBalance实现第二个参数Invocation是此次服务调用的上下文信息
// 第三个参数是待选择的Invoker集合第四个参数用来记录负载均衡已经选出来、尝试过的Invoker集合
protected Invoker<T> select(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
if (CollectionUtils.isEmpty(invokers)) {
return null;
}
// 获取调用方法名
String methodName = invocation == null ? StringUtils.EMPTY_STRING : invocation.getMethodName();
// 获取sticky配置sticky表示粘滞连接所谓粘滞连接是指Consumer会尽可能地
// 调用同一个Provider节点除非这个Provider无法提供服务
boolean sticky = invokers.get(0).getUrl()
.getMethodParameter(methodName, CLUSTER_STICKY_KEY, DEFAULT_CLUSTER_STICKY);
// 检测invokers列表是否包含sticky Invoker如果不包含
// 说明stickyInvoker代表的服务提供者挂了此时需要将其置空
if (stickyInvoker != null && !invokers.contains(stickyInvoker)) {
stickyInvoker = null;
}
// 如果开启了粘滞连接特性需要先判断这个Provider节点是否已经重试过了
if (sticky && stickyInvoker != null // 表示粘滞连接
&& (selected == null || !selected.contains(stickyInvoker)) // 表示stickyInvoker未重试过
) {
// 检测当前stickyInvoker是否可用如果可用直接返回stickyInvoker
if (availablecheck && stickyInvoker.isAvailable()) {
return stickyInvoker;
}
}
// 执行到这里说明前面的stickyInvoker为空或者不可用
// 这里会继续调用doSelect选择新的Invoker对象
Invoker<T> invoker = doSelect(loadbalance, invocation, invokers, selected);
if (sticky) { // 是否开启粘滞更新stickyInvoker字段
stickyInvoker = invoker;
}
return invoker;
}
doSelect() 方法主要做了两件事:
一是通过 LoadBalance 选择 Invoker 对象;
二是如果选出来的 Invoker 不稳定或不可用,会调用 reselect() 方法进行重选。
private Invoker<T> doSelect(LoadBalance loadbalance, Invocation invocation,
List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
// 判断是否需要进行负载均衡Invoker集合为空直接返回null
if (CollectionUtils.isEmpty(invokers)) {
return null;
}
if (invokers.size() == 1) { // 只有一个Invoker对象直接返回即可
return invokers.get(0);
}
// 通过LoadBalance实现选择Invoker对象
Invoker<T> invoker = loadbalance.select(invokers, getUrl(), invocation);
// 如果LoadBalance选出的Invoker对象已经尝试过请求了或不可用则需要调用reselect()方法重选
if ((selected != null && selected.contains(invoker)) // Invoker已经尝试调用过了但是失败了
|| (!invoker.isAvailable() && getUrl() != null && availablecheck) // Invoker不可用
) {
try {
// 调用reselect()方法重选
Invoker<T> rInvoker = reselect(loadbalance, invocation, invokers, selected, availablecheck);
// 如果重选的Invoker对象不为空则直接返回这个 rInvoker
if (rInvoker != null) {
invoker = rInvoker;
} else {
int index = invokers.indexOf(invoker);
try {
// 如果重选的Invoker对象为空则返回该Invoker的下一个Invoker对象
invoker = invokers.get((index + 1) % invokers.size());
} catch (Exception e) {
logger.warn("...");
}
}
} catch (Throwable t) {
logger.error("...");
}
}
return invoker;
}
reselect() 方法会重新进行一次负载均衡,首先对未尝试过的可用 Invokers 进行负载均衡,如果已经全部重试过了,则将尝试过的 Provider 节点过滤掉,然后在可用的 Provider 节点中重新进行负载均衡。
private Invoker<T> reselect(LoadBalance loadbalance, Invocation invocation,
List<Invoker<T>> invokers, List<Invoker<T>> selected, boolean availablecheck) throws RpcException {
// 用于记录要重新进行负载均衡的Invoker集合
List<Invoker<T>> reselectInvokers = new ArrayList<>(
invokers.size() > 1 ? (invokers.size() - 1) : invokers.size());
// 将不在selected集合中的Invoker过滤出来进行负载均衡
for (Invoker<T> invoker : invokers) {
if (availablecheck && !invoker.isAvailable()) {
continue;
}
if (selected == null || !selected.contains(invoker)) {
reselectInvokers.add(invoker);
}
}
// reselectInvokers不为空时才需要通过负载均衡组件进行选择
if (!reselectInvokers.isEmpty()) {
return loadbalance.select(reselectInvokers, getUrl(), invocation);
}
// 只能对selected集合中可用的Invoker再次进行负载均衡
if (selected != null) {
for (Invoker<T> invoker : selected) {
if ((invoker.isAvailable()) // available first
&& !reselectInvokers.contains(invoker)) {
reselectInvokers.add(invoker);
}
}
}
if (!reselectInvokers.isEmpty()) {
return loadbalance.select(reselectInvokers, getUrl(), invocation);
}
return null;
}
AbstractCluster
常用的 ClusterInvoker 实现都继承了 AbstractClusterInvoker 类型,对应的 Cluster 扩展实现都继承了 AbstractCluster 抽象类。AbstractCluster 抽象类的核心逻辑是在 ClusterInvoker 外层包装一层 ClusterInterceptor从而实现类似切面的效果。
下面是 ClusterInterceptor 接口的定义:
@SPI
public interface ClusterInterceptor {
// 前置拦截方法
void before(AbstractClusterInvoker<?> clusterInvoker, Invocation invocation);
// 后置拦截方法
void after(AbstractClusterInvoker<?> clusterInvoker, Invocation invocation);
// 调用ClusterInvoker的invoke()方法完成请求
default Result intercept(AbstractClusterInvoker<?> clusterInvoker, Invocation invocation) throws RpcException {
return clusterInvoker.invoke(invocation);
}
// 这个Listener用来监听请求的正常结果以及异常
interface Listener {
void onMessage(Result appResponse, AbstractClusterInvoker<?> clusterInvoker, Invocation invocation);
void onError(Throwable t, AbstractClusterInvoker<?> clusterInvoker, Invocation invocation);
}
}
在 AbstractCluster 抽象类的 join() 方法中,首先会调用 doJoin() 方法获取最终要调用的 Invoker 对象doJoin() 是个抽象方法,由 AbstractCluster 子类根据具体的策略进行实现。之后AbstractCluster.join() 方法会调用 buildClusterInterceptors() 方法加载 ClusterInterceptor 扩展实现类,对 Invoker 对象进行包装。具体实现如下:
private <T> Invoker<T> buildClusterInterceptors(AbstractClusterInvoker<T> clusterInvoker, String key) {
AbstractClusterInvoker<T> last = clusterInvoker;
// 通过SPI方式加载ClusterInterceptor扩展实现
List<ClusterInterceptor> interceptors = ExtensionLoader.getExtensionLoader(ClusterInterceptor.class).getActivateExtension(clusterInvoker.getUrl(), key);
if (!interceptors.isEmpty()) {
for (int i = interceptors.size() - 1; i >= 0; i--) {
// 将InterceptorInvokerNode收尾连接到一起形成调用链
final ClusterInterceptor interceptor = interceptors.get(i);
final AbstractClusterInvoker<T> next = last;
last = new InterceptorInvokerNode<>(clusterInvoker, interceptor, next);
}
}
return last;
}
@Override
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
// 扩展名称由reference.interceptor参数确定
return buildClusterInterceptors(doJoin(directory), directory.getUrl().getParameter(REFERENCE_INTERCEPTOR_KEY));
}
InterceptorInvokerNode 会将底层的 AbstractClusterInvoker 对象以及关联的 ClusterInterceptor 对象封装到一起,还会维护一个 next 引用,指向下一个 InterceptorInvokerNode 对象。
在 InterceptorInvokerNode.invoke() 方法中,会先调用 ClusterInterceptor 的前置逻辑,然后执行 intercept() 方法调用 AbstractClusterInvoker 的 invoke() 方法完成远程调用,最后执行 ClusterInterceptor 的后置逻辑。具体实现如下:
public Result invoke(Invocation invocation) throws RpcException {
Result asyncResult;
try {
interceptor.before(next, invocation); // 前置逻辑
// 执行invoke()方法完成远程调用
asyncResult = interceptor.intercept(next, invocation);
} catch (Exception e) {
if (interceptor instanceof ClusterInterceptor.Listener) {
// 出现异常时会触发监听器的onError()方法
ClusterInterceptor.Listener listener = (ClusterInterceptor.Listener) interceptor;
listener.onError(e, clusterInvoker, invocation);
}
throw e;
} finally {
// 执行后置逻辑
interceptor.after(next, invocation);
}
return asyncResult.whenCompleteWithContext((r, t) -> {
if (interceptor instanceof ClusterInterceptor.Listener) {
ClusterInterceptor.Listener listener = (ClusterInterceptor.Listener) interceptor;
if (t == null) {
// 正常返回时会调用onMessage()方法触发监听器
listener.onMessage(r, clusterInvoker, invocation);
} else {
listener.onError(t, clusterInvoker, invocation);
}
}
});
}
Dubbo 提供了两个 ClusterInterceptor 实现类,分别是 ConsumerContextClusterInterceptor 和 ZoneAwareClusterInterceptor如下图所示
ClusterInterceptor 继承关系图
在 ConsumerContextClusterInterceptor 的 before() 方法中,会在 RpcContext 中设置当前 Consumer 地址、此次调用的 Invoker 等信息,同时还会删除之前与当前线程绑定的 Server Context。在 after() 方法中,会删除本地 RpcContext 的信息。ConsumerContextClusterInterceptor 的具体实现如下:
public void before(AbstractClusterInvoker<?> invoker, Invocation invocation) {
// 获取当前线程绑定的RpcContext
RpcContext context = RpcContext.getContext();
// 设置Invoker、Consumer地址等信息 context.setInvocation(invocation).setLocalAddress(NetUtils.getLocalHost(), 0);
if (invocation instanceof RpcInvocation) {
((RpcInvocation) invocation).setInvoker(invoker);
}
RpcContext.removeServerContext();
}
public void after(AbstractClusterInvoker<?> clusterInvoker, Invocation invocation) {
RpcContext.removeContext(true); // 删除本地RpcContext的信息
}
ConsumerContextClusterInterceptor 同时继承了 ClusterInterceptor.Listener 接口,在其 onMessage() 方法中,会获取响应中的 attachments 并设置到 RpcContext 中的 SERVER_LOCAL 之中,具体实现如下:
public void onMessage(Result appResponse, AbstractClusterInvoker<?> invoker, Invocation invocation) {
// 从AppResponse中获取attachment并设置到SERVER_LOCAL这个RpcContext中 RpcContext.getServerContext().setObjectAttachments(appResponse.getObjectAttachments());
}
介绍完 ConsumerContextClusterInterceptor我们再来看 ZoneAwareClusterInterceptor。
在 ZoneAwareClusterInterceptor 的 before() 方法中,会从 RpcContext 中获取多注册中心相关的参数并设置到 Invocation 中(主要是 registry_zone 参数和 registry_zone_force 参数,这两个参数的具体含义,在后面分析 ZoneAwareClusterInvoker 时详细介绍ZoneAwareClusterInterceptor 的 after() 方法为空实现。ZoneAwareClusterInterceptor 的具体实现如下:
public void before(AbstractClusterInvoker<?> clusterInvoker, Invocation invocation) {
RpcContext rpcContext = RpcContext.getContext();
// 从RpcContext中获取registry_zone参数和registry_zone_force参数
String zone = (String) rpcContext.getAttachment(REGISTRY_ZONE);
String force = (String) rpcContext.getAttachment(REGISTRY_ZONE_FORCE);
// 检测用户是否提供了ZoneDetector接口的扩展实现
ExtensionLoader<ZoneDetector> loader = ExtensionLoader.getExtensionLoader(ZoneDetector.class);
if (StringUtils.isEmpty(zone) && loader.hasExtension("default")) {
ZoneDetector detector = loader.getExtension("default");
zone = detector.getZoneOfCurrentRequest(invocation);
force = detector.isZoneForcingEnabled(invocation, zone);
}
// 将registry_zone参数和registry_zone_force参数设置到Invocation中
if (StringUtils.isNotEmpty(zone)) {
invocation.setAttachment(REGISTRY_ZONE, zone);
}
if (StringUtils.isNotEmpty(force)) {
invocation.setAttachment(REGISTRY_ZONE_FORCE, force);
}
}
需要注意的是ZoneAwareClusterInterceptor 没有实现 ClusterInterceptor.Listener 接口,也就是不提供监听响应的功能。
总结
本课时我们主要介绍的是 Dubbo Cluster 层中容错机制相关的内容。首先,我们了解了集群容错机制的作用。然后,我们介绍了 Cluster 接口的定义以及其各个实现类的核心功能。之后,我们深入讲解了 AbstractClusterInvoker 的实现,其核心是实现了一套通用的负载均衡算法。最后,我们还分析了 AbstractCluster 抽象实现类以及其中涉及的 ClusterInterceptor 接口的内容。

View File

@@ -0,0 +1,952 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
38 集群容错:一个好汉三个帮(下)
你好,我是杨四正,今天我和你分享的主题是集群容错:一个好汉三个帮(下篇)。
在上一课时,我们介绍了 Dubbo Cluster 层中集群容错机制的基础知识,还说明了 Cluster 接口的定义以及其各个实现类的核心功能。同时,我们还分析了 AbstractClusterInvoker 抽象类以及 AbstractCluster 抽象实现类的核心实现。
那接下来在本课时,我们将介绍 Cluster 接口的全部实现类,以及相关的 Cluster Invoker 实现类。
FailoverClusterInvoker
通过前面对 Cluster 接口的介绍我们知道Cluster 默认的扩展实现是 FailoverCluster其 doJoin() 方法中会创建一个 FailoverClusterInvoker 对象并返回,具体实现如下:
public <T> AbstractClusterInvoker<T> doJoin(Directory<T> directory) throws RpcException {
return new FailoverClusterInvoker<>(directory);
}
FailoverClusterInvoker 会在调用失败的时候,自动切换 Invoker 进行重试。下面来看 FailoverClusterInvoker 的核心实现:
public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
List<Invoker<T>> copyInvokers = invokers;
// 检查copyInvokers集合是否为空如果为空会抛出异常
checkInvokers(copyInvokers, invocation);
String methodName = RpcUtils.getMethodName(invocation);
// 参数重试次数默认重试2次总共执行3次
int len = getUrl().getMethodParameter(methodName, RETRIES_KEY, DEFAULT_RETRIES) + 1;
if (len <= 0) {
len = 1;
}
RpcException le = null;
// 记录已经尝试调用过的Invoker对象
List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyInvokers.size());
Set<String> providers = new HashSet<String>(len);
for (int i = 0; i < len; i++) {
// 第一次传进来的invokers已经check过了第二次则是重试需要重新获取最新的服务列表
if (i > 0) {
checkWhetherDestroyed();
// 这里会重新调用Directory.list()方法获取Invoker列表
copyInvokers = list(invocation);
// 检查copyInvokers集合是否为空如果为空会抛出异常
checkInvokers(copyInvokers, invocation);
}
// 通过LoadBalance选择Invoker对象这里传入的invoked集合
// 就是前面介绍AbstractClusterInvoker.select()方法中的selected集合
Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
// 记录此次要尝试调用的Invoker对象下一次重试时就会过滤这个服务
invoked.add(invoker);
RpcContext.getContext().setInvokers((List) invoked);
try {
// 调用目标Invoker对象的invoke()方法,完成远程调用
Result result = invoker.invoke(invocation);
// 经过尝试之后终于成功这里会打印一个警告日志将尝试过来的Provider地址打印出来
if (le != null && logger.isWarnEnabled()) {
logger.warn("...");
}
return result;
} catch (RpcException e) {
if (e.isBiz()) { // biz exception.
throw e;
}
le = e;
} catch (Throwable e) { // 抛出异常,表示此次尝试失败,会进行重试
le = new RpcException(e.getMessage(), e);
} finally {
// 记录尝试过的Provider地址会在上面的警告日志中打印出来
providers.add(invoker.getUrl().getAddress());
}
}
// 达到重试次数上限之后会抛出异常其中会携带调用的方法名、尝试过的Provider节点的地址(providers集合)、全部的Provider个数(copyInvokers集合)以及Directory信息
throw new RpcException(le.getCode(), "...");
}
FailbackClusterInvoker
FailbackCluster 是 Cluster 接口的另一个扩展实现,扩展名是 failback其 doJoin() 方法中创建的 Invoker 对象是 FailbackClusterInvoker 类型,具体实现如下:
public <T> AbstractClusterInvoker<T> doJoin(Directory<T> directory) throws RpcException {
return new FailbackClusterInvoker<>(directory);
}
FailbackClusterInvoker 在请求失败之后,返回一个空结果给 Consumer同时还会添加一个定时任务对失败的请求进行重试。下面来看 FailbackClusterInvoker 的具体实现:
protected Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
Invoker<T> invoker = null;
try {
// 检测Invoker集合是否为空
checkInvokers(invokers, invocation);
// 调用select()方法得到此次尝试的Invoker对象
invoker = select(loadbalance, invocation, invokers, null);
// 调用invoke()方法完成远程调用
return invoker.invoke(invocation);
} catch (Throwable e) {
// 请求失败之后,会添加一个定时任务进行重试
addFailed(loadbalance, invocation, invokers, invoker);
return AsyncRpcResult.newDefaultAsyncResult(null, null, invocation); // 请求失败时,会返回一个空结果
}
}
在 doInvoke() 方法中,请求失败时会调用 addFailed() 方法添加定时任务进行重试,默认每隔 5 秒执行一次,总共重试 3 次,具体实现如下:
private void addFailed(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, Invoker<T> lastInvoker) {
if (failTimer == null) {
synchronized (this) {
if (failTimer == null) { // Double Check防止并发问题
// 初始化时间轮这个时间轮有32个槽每个槽代表1秒
failTimer = new HashedWheelTimer(
new NamedThreadFactory("failback-cluster-timer", true),
1,
TimeUnit.SECONDS, 32, failbackTasks);
}
}
}
// 创建一个定时任务
RetryTimerTask retryTimerTask = new RetryTimerTask(loadbalance, invocation, invokers, lastInvoker, retries, RETRY_FAILED_PERIOD);
try {
// 将定时任务添加到时间轮中
failTimer.newTimeout(retryTimerTask, RETRY_FAILED_PERIOD, TimeUnit.SECONDS);
} catch (Throwable e) {
logger.error("...");
}
}
在 RetryTimerTask 定时任务中,会重新调用 select() 方法筛选合适的 Invoker 对象,并尝试进行请求。如果请求再次失败且重试次数未达到上限,则调用 rePut() 方法再次添加定时任务等待进行重试如果请求成功也不会返回任何结果。RetryTimerTask 的核心实现如下:
public void run(Timeout timeout) {
try {
// 重新选择Invoker对象注意这里会将上次重试失败的Invoker作为selected集合传入
Invoker<T> retryInvoker = select(loadbalance, invocation, invokers, Collections.singletonList(lastInvoker));
lastInvoker = retryInvoker;
retryInvoker.invoke(invocation); // 请求对应的Provider节点
} catch (Throwable e) {
if ((++retryTimes) >= retries) { // 重试次数达到上限,输出警告日志
logger.error("...");
} else {
rePut(timeout); // 重试次数未达到上限,则重新添加定时任务,等待重试
}
}
}
private void rePut(Timeout timeout) {
if (timeout == null) { // 边界检查
return;
}
Timer timer = timeout.timer();
if (timer.isStop() || timeout.isCancelled()) { // 检查时间轮状态、检查定时任务状态
return;
}
// 重新添加定时任务
timer.newTimeout(timeout.task(), tick, TimeUnit.SECONDS);
}
FailfastClusterInvoker
FailfastCluster 的扩展名是 failfast在其 doJoin() 方法中会创建 FailfastClusterInvoker 对象,具体实现如下:
public <T> AbstractClusterInvoker<T> doJoin(Directory<T> directory) throws RpcException {
return new FailfastClusterInvoker<>(directory);
}
FailfastClusterInvoker 只会进行一次请求,请求失败之后会立即抛出异常,这种策略适合非幂等的操作,具体实现如下:
public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
checkInvokers(invokers, invocation);
// 调用select()得到此次要调用的Invoker对象
Invoker<T> invoker = select(loadbalance, invocation, invokers, null);
try {
return invoker.invoke(invocation); // 发起请求
} catch (Throwable e) {
// 请求失败,直接抛出异常
if (e instanceof RpcException && ((RpcException) e).isBiz()) {
throw (RpcException) e;
}
throw new RpcException("...");
}
}
FailsafeClusterInvoker
FailsafeCluster 的扩展名是 failsafe在其 doJoin() 方法中会创建 FailsafeClusterInvoker 对象,具体实现如下:
public <T> AbstractClusterInvoker<T> doJoin(Directory<T> directory) throws RpcException {
return new FailsafeClusterInvoker<>(directory);
}
FailsafeClusterInvoker 只会进行一次请求,请求失败之后会返回一个空结果,具体实现如下:
public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
try {
// 检测Invoker集合是否为空
checkInvokers(invokers, invocation);
// 调用select()得到此次要调用的Invoker对象
Invoker<T> invoker = select(loadbalance, invocation, invokers, null);
// 发起请求
return invoker.invoke(invocation);
} catch (Throwable e) {
// 请求失败之后,会打印一行日志并返回空结果
logger.error("...");
return AsyncRpcResult.newDefaultAsyncResult(null, null, invocation);
}
}
ForkingClusterInvoker
ForkingCluster 的扩展名称为 forking在其 doJoin() 方法中,会创建一个 ForkingClusterInvoker 对象,具体实现如下:
public <T> AbstractClusterInvoker<T> doJoin(Directory<T> directory) throws RpcException {
return new ForkingClusterInvoker<>(directory);
}
ForkingClusterInvoker 中会维护一个线程池executor 字段,通过 Executors.newCachedThreadPool() 方法创建的线程池),并发调用多个 Provider 节点,只要有一个 Provider 节点成功返回了结果ForkingClusterInvoker 的 doInvoke() 方法就会立即结束运行。
ForkingClusterInvoker 主要是为了应对一些实时性要求较高的读操作,因为没有并发控制的多线程写入,可能会导致数据不一致。
ForkingClusterInvoker.doInvoke() 方法首先从 Invoker 集合中选出指定个数forks 参数决定)的 Invoker 对象,然后通过 executor 线程池并发调用这些 Invoker并将请求结果存储在 ref 阻塞队列中,则当前线程会阻塞在 ref 队列上,等待第一个请求结果返回。下面是 ForkingClusterInvoker 的具体实现:
public Result doInvoke(final Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
try {
// 检查Invoker集合是否为空
checkInvokers(invokers, invocation);
final List<Invoker<T>> selected;
// 从URL中获取forks参数作为并发请求的上限默认值为2
final int forks = getUrl().getParameter(FORKS_KEY, DEFAULT_FORKS);
final int timeout = getUrl().getParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT);
if (forks <= 0 || forks >= invokers.size()) {
// 如果forks为负数或是大于Invoker集合的长度会直接并发调用全部Invoker
selected = invokers;
} else {
// 按照forks指定的并发度选择此次并发调用的Invoker对象
selected = new ArrayList<>(forks);
while (selected.size() < forks) {
Invoker<T> invoker = select(loadbalance, invocation, invokers, selected);
if (!selected.contains(invoker)) {
selected.add(invoker); // 避免重复选择
}
}
}
RpcContext.getContext().setInvokers((List) selected);
// 记录失败的请求个数
final AtomicInteger count = new AtomicInteger();
// 用于记录请求结果
final BlockingQueue<Object> ref = new LinkedBlockingQueue<>();
for (final Invoker<T> invoker : selected) { // 遍历 selected 列表
executor.execute(() -> { // 为每个Invoker创建一个任务并提交到线程池中
try {
// 发起请求
Result result = invoker.invoke(invocation);
// 将请求结果写到ref队列中
ref.offer(result);
} catch (Throwable e) {
int value = count.incrementAndGet();
if (value >= selected.size()) {
// 如果失败的请求个数超过了并发请求的个数则向ref队列中写入异常
ref.offer(e);
}
}
});
}
try {
// 当前线程会阻塞等待任意一个请求结果的出现
Object ret = ref.poll(timeout, TimeUnit.MILLISECONDS);
if (ret instanceof Throwable) { // 如果结果类型为Throwable则抛出异常
Throwable e = (Throwable) ret;
throw new RpcException("...");
}
return (Result) ret; // 返回结果
} catch (InterruptedException e) {
throw new RpcException("...");
}
} finally {
// 清除上下文信息
RpcContext.getContext().clearAttachments();
}
}
BroadcastClusterInvoker
BroadcastCluster 这个 Cluster 实现类的扩展名为 broadcast在其 doJoin() 方法中创建的是 BroadcastClusterInvoker 类型的 Invoker 对象,具体实现如下:
public <T> AbstractClusterInvoker<T> doJoin(Directory<T> directory) throws RpcException {
return new BroadcastClusterInvoker<>(directory);
}
在 BroadcastClusterInvoker 中,会逐个调用每个 Provider 节点,其中任意一个 Provider 节点报错都会在全部调用结束之后抛出异常。BroadcastClusterInvoker通常用于通知类的操作例如通知所有 Provider 节点更新本地缓存。
下面来看 BroadcastClusterInvoker 的具体实现:
public Result doInvoke(final Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
// 检测Invoker集合是否为空
checkInvokers(invokers, invocation);
RpcContext.getContext().setInvokers((List) invokers);
RpcException exception = null; // 用于记录失败请求的相关异常信息
Result result = null;
// 遍历所有Invoker对象
for (Invoker<T> invoker : invokers) {
try {
// 发起请求
result = invoker.invoke(invocation);
} catch (RpcException e) {
exception = e;
logger.warn(e.getMessage(), e);
} catch (Throwable e) {
exception = new RpcException(e.getMessage(), e);
logger.warn(e.getMessage(), e);
}
}
if (exception != null) { // 出现任何异常,都会在这里抛出
throw exception;
}
return result;
}
AvailableClusterInvoker
AvailableCluster 这个 Cluster 实现类的扩展名为 available在其 join() 方法中创建的是 AvailableClusterInvoker 类型的 Invoker 对象,具体实现如下:
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
return new AvailableClusterInvoker<>(directory);
}
在 AvailableClusterInvoker 的 doInvoke() 方法中,会遍历整个 Invoker 集合,逐个调用对应的 Provider 节点,当遇到第一个可用的 Provider 节点时,就尝试访问该 Provider 节点,成功则返回结果;如果访问失败,则抛出异常终止遍历。
下面是 AvailableClusterInvoker 的具体实现:
public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
for (Invoker<T> invoker : invokers) { // 遍历整个Invoker集合
if (invoker.isAvailable()) { // 检测该Invoker是否可用
// 发起请求,调用失败时的异常会直接抛出
return invoker.invoke(invocation);
}
}
// 没有找到可用的Invoker也会抛出异常
throw new RpcException("No provider available in " + invokers);
}
MergeableClusterInvoker
MergeableCluster 这个 Cluster 实现类的扩展名为 mergeable在其 doJoin() 方法中创建的是 MergeableClusterInvoker 类型的 Invoker 对象,具体实现如下:
public <T> AbstractClusterInvoker<T> doJoin(Directory<T> directory) throws RpcException {
return new MergeableClusterInvoker<T>(directory);
}
MergeableClusterInvoker 会对多个 Provider 节点返回结果合并。如果请求的方法没有配置 Merger 合并器(即没有指定 merger 参数),则不会进行结果合并,而是直接将第一个可用的 Invoker 结果返回。下面来看 MergeableClusterInvoker 的具体实现:
protected Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
checkInvokers(invokers, invocation);
String merger = getUrl().getMethodParameter(invocation.getMethodName(), MERGER_KEY);
// 判断要调用的目标方法是否有合并器,如果没有,则不会进行合并,
// 找到第一个可用的Invoker直接调用并返回结果
if (ConfigUtils.isEmpty(merger)) {
for (final Invoker<T> invoker : invokers) {
if (invoker.isAvailable()) {
try {
return invoker.invoke(invocation);
} catch (RpcException e) {
if (e.isNoInvokerAvailableAfterFilter()) {
log.debug("No available provider for service" + getUrl().getServiceKey() + " on group " + invoker.getUrl().getParameter(GROUP_KEY) + ", will continue to try another group.");
} else {
throw e;
}
}
}
}
return invokers.iterator().next().invoke(invocation);
}
// 确定目标方法的返回值类型
Class<?> returnType;
try {
returnType = getInterface().getMethod(
invocation.getMethodName(), invocation.getParameterTypes()).getReturnType();
} catch (NoSuchMethodException e) {
returnType = null;
}
// 调用每个Invoker对象(异步方式)将请求结果记录到results集合中
Map<String, Result> results = new HashMap<>();
for (final Invoker<T> invoker : invokers) {
RpcInvocation subInvocation = new RpcInvocation(invocation, invoker);
subInvocation.setAttachment(ASYNC_KEY, "true");
results.put(invoker.getUrl().getServiceKey(), invoker.invoke(subInvocation));
}
Object result = null;
List<Result> resultList = new ArrayList<Result>(results.size());
// 等待结果返回
for (Map.Entry<String, Result> entry : results.entrySet()) {
Result asyncResult = entry.getValue();
try {
Result r = asyncResult.get();
if (r.hasException()) {
log.error("Invoke " + getGroupDescFromServiceKey(entry.getKey()) +
" failed: " + r.getException().getMessage(),
r.getException());
} else {
resultList.add(r);
}
} catch (Exception e) {
throw new RpcException("Failed to invoke service " + entry.getKey() + ": " + e.getMessage(), e);
}
}
if (resultList.isEmpty()) {
return AsyncRpcResult.newDefaultAsyncResult(invocation);
} else if (resultList.size() == 1) {
return resultList.iterator().next();
}
if (returnType == void.class) {
return AsyncRpcResult.newDefaultAsyncResult(invocation);
}
// merger如果以"."开头,后面为方法名,这个方法名是远程目标方法的返回类型中的方法
// 得到每个Provider节点返回的结果对象之后会遍历每个返回对象调用merger参数指定的方法
if (merger.startsWith(".")) {
merger = merger.substring(1);
Method method;
try {
method = returnType.getMethod(merger, returnType);
} catch (NoSuchMethodException e) {
throw new RpcException("Can not merge result because missing method [ " + merger + " ] in class [ " +
returnType.getName() + " ]");
}
if (!Modifier.isPublic(method.getModifiers())) {
method.setAccessible(true);
}
// resultList集合保存了所有的返回对象method是Method对象也就是merger指定的方法
// result是最后返回调用方的结果
result = resultList.remove(0).getValue();
try {
if (method.getReturnType() != void.class
&& method.getReturnType().isAssignableFrom(result.getClass())) {
for (Result r : resultList) { // 反射调用
result = method.invoke(result, r.getValue());
}
} else {
for (Result r : resultList) { // 反射调用
method.invoke(result, r.getValue());
}
}
} catch (Exception e) {
throw new RpcException("Can not merge result: " + e.getMessage(), e);
}
} else {
Merger resultMerger;
if (ConfigUtils.isDefault(merger)) {
// merger参数为true或者default表示使用默认的Merger扩展实现完成合并
// 在后面课时中会介绍Merger接口
resultMerger = MergerFactory.getMerger(returnType);
} else {
//merger参数指定了Merger的扩展名称则使用SPI查找对应的Merger扩展实现对象
resultMerger = ExtensionLoader.getExtensionLoader(Merger.class).getExtension(merger);
}
if (resultMerger != null) {
List<Object> rets = new ArrayList<Object>(resultList.size());
for (Result r : resultList) {
rets.add(r.getValue());
}
// 执行合并操作
result = resultMerger.merge(
rets.toArray((Object[]) Array.newInstance(returnType, 0)));
} else {
throw new RpcException("There is no merger to merge result.");
}
}
return AsyncRpcResult.newDefaultAsyncResult(result, invocation);
}
ZoneAwareClusterInvoker
ZoneAwareCluster 这个 Cluster 实现类的扩展名为 zone-aware在其 doJoin() 方法中创建的是 ZoneAwareClusterInvoker 类型的 Invoker 对象,具体实现如下:
protected <T> AbstractClusterInvoker<T> doJoin(Directory<T> directory) throws RpcException {
return new ZoneAwareClusterInvoker<T>(directory);
}
在 Dubbo 中使用多个注册中心的架构如下图所示:
双注册中心结构图
Consumer 可以使用 ZoneAwareClusterInvoker 先在多个注册中心之间进行选择,选定注册中心之后,再选择 Provider 节点,如下图所示:
ZoneAwareClusterInvoker 在多注册中心之间进行选择的策略有以下四种。
找到preferred 属性为 true 的注册中心,它是优先级最高的注册中心,只有该中心无可用 Provider 节点时,才会回落到其他注册中心。
根据请求中的 zone key 做匹配,优先派发到相同 zone 的注册中心。
根据权重(也就是注册中心配置的 weight 属性)进行轮询。
如果上面的策略都未命中,则选择第一个可用的 Provider 节点。
下面来看 ZoneAwareClusterInvoker 的具体实现:
public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
// 首先找到preferred属性为true的注册中心它是优先级最高的注册中心只有该中心无可用 Provider 节点时,才会回落到其他注册中心
for (Invoker<T> invoker : invokers) {
MockClusterInvoker<T> mockClusterInvoker = (MockClusterInvoker<T>) invoker;
if (mockClusterInvoker.isAvailable() && mockClusterInvoker.getRegistryUrl()
.getParameter(REGISTRY_KEY + "." + PREFERRED_KEY, false)) {
return mockClusterInvoker.invoke(invocation);
}
}
// 根据请求中的registry_zone做匹配优先派发到相同zone的注册中心
String zone = (String) invocation.getAttachment(REGISTRY_ZONE);
if (StringUtils.isNotEmpty(zone)) {
for (Invoker<T> invoker : invokers) {
MockClusterInvoker<T> mockClusterInvoker = (MockClusterInvoker<T>) invoker;
if (mockClusterInvoker.isAvailable() && zone.equals(mockClusterInvoker.getRegistryUrl().getParameter(REGISTRY_KEY + "." + ZONE_KEY))) {
return mockClusterInvoker.invoke(invocation);
}
}
String force = (String) invocation.getAttachment(REGISTRY_ZONE_FORCE);
if (StringUtils.isNotEmpty(force) && "true".equalsIgnoreCase(force)) {
throw new IllegalStateException("...");
}
}
// 根据权重也就是注册中心配置的weight属性进行轮询
Invoker<T> balancedInvoker = select(loadbalance, invocation, invokers, null);
if (balancedInvoker.isAvailable()) {
return balancedInvoker.invoke(invocation);
}
// 选择第一个可用的 Provider 节点
for (Invoker<T> invoker : invokers) {
MockClusterInvoker<T> mockClusterInvoker = (MockClusterInvoker<T>) invoker;
if (mockClusterInvoker.isAvailable()) {
return mockClusterInvoker.invoke(invocation);
}
}
throw new RpcException("No provider available in " + invokers);
}
总结
本课时我们重点介绍了 Dubbo 中 Cluster 接口的各个实现类的原理以及相关 Invoker 的实现原理。这里重点分析的 Cluster 实现有Failover Cluster、Failback Cluster、Failfast Cluster、Failsafe Cluster、Forking Cluster、Broadcast Cluster、Available Cluster 和 Mergeable Cluster。除此之外我们还分析了多注册中心的 ZoneAware Cluster 实现。

View File

@@ -0,0 +1,401 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
39 加餐多个返回值不用怕Merger 合并器来帮忙
你好,我是杨四正,今天我和你分享的主题是 Merger 合并器。
在上一课时中,我们分析 MergeableClusterInvoker 的具体实现时讲解过这样的内容MergeableClusterInvoker 中会读取 URL 中的 merger 参数值,如果 merger 参数以 “.” 开头,则表示 “.” 后的内容是一个方法名这个方法名是远程目标方法的返回类型中的一个方法MergeableClusterInvoker 在拿到所有 Invoker 返回的结果对象之后,会遍历每个返回结果,并调用 merger 参数指定的方法,合并这些结果值。
其实,除了上述指定 Merger 方法名称的合并方式之外Dubbo 内部还提供了很多默认的 Merger 实现,这也就是本课时将要分析的内容。本课时将详细介绍 MergerFactory 工厂类、Merger 接口以及针对 Java 中常见数据类型的 Merger 实现。
MergerFactory
在 MergeableClusterInvoker 使用默认 Merger 实现的时候,会通过 MergerFactory 以及服务接口返回值类型returnType选择合适的 Merger 实现。
在 MergerFactory 中维护了一个 ConcurrentHashMap 集合(即 MERGER_CACHE 字段),用来缓存服务接口返回值类型与 Merger 实例之间的映射关系。
MergerFactory.getMerger() 方法会根据传入的 returnType 类型,从 MERGER_CACHE 缓存中查找相应的 Merger 实现,下面我们来看该方法的具体实现:
public static <T> Merger<T> getMerger(Class<T> returnType) {
if (returnType == null) { // returnType为空直接抛出异常
throw new IllegalArgumentException("returnType is null");
}
Merger result;
if (returnType.isArray()) { // returnType为数组类型
// 获取数组中元素的类型
Class type = returnType.getComponentType();
// 获取元素类型对应的Merger实现
result = MERGER_CACHE.get(type);
if (result == null) {
loadMergers();
result = MERGER_CACHE.get(type);
}
// 如果Dubbo没有提供元素类型对应的Merger实现则返回ArrayMerger
if (result == null && !type.isPrimitive()) {
result = ArrayMerger.INSTANCE;
}
} else {
// 如果returnType不是数组类型则直接从MERGER_CACHE缓存查找对应的Merger实例
result = MERGER_CACHE.get(returnType);
if (result == null) {
loadMergers();
result = MERGER_CACHE.get(returnType);
}
}
return result;
}
loadMergers() 方法会通过 Dubbo SPI 方式加载 Merger 接口全部扩展实现的名称,并填充到 MERGER_CACHE 集合中,具体实现如下:
static void loadMergers() {
// 获取Merger接口的所有扩展名称
Set<String> names = ExtensionLoader.getExtensionLoader(Merger.class)
.getSupportedExtensions();
for (String name : names) { // 遍历所有Merger扩展实现
Merger m = ExtensionLoader.getExtensionLoader(Merger.class).getExtension(name);
// 将Merger实例与对应returnType的映射关系记录到MERGER_CACHE集合中
MERGER_CACHE.putIfAbsent(ReflectUtils.getGenericClass(m.getClass()), m);
}
}
ArrayMerger
在 Dubbo 中提供了处理不同类型返回值的 Merger 实现,其中不仅有处理 boolean[]、byte[]、char[]、double[]、float[]、int[]、long[]、short[] 等基础类型数组的 Merger 实现,还有处理 List、Set、Map 等集合类的 Merger 实现,具体继承关系如下图所示:
Merger 继承关系图
我们首先来看 ArrayMerger 实现:当服务接口的返回值为数组的时候,会使用 ArrayMerger 将多个数组合并成一个数组也就是将二维数组拍平成一维数组。ArrayMerger.merge() 方法的具体实现如下:
public Object[] merge(Object[]... items) {
if (ArrayUtils.isEmpty(items)) {
// 传入的结果集合为空,则直接返回空数组
return new Object[0];
}
int i = 0;
// 查找第一个不为null的结果
while (i < items.length && items[i] == null) {
i++;
}
// 所有items数组中全部结果都为null则直接返回空数组
if (i == items.length) {
return new Object[0];
}
Class<?> type = items[i].getClass().getComponentType();
int totalLen = 0;
for (; i < items.length; i++) {
if (items[i] == null) { // 忽略为null的结果
continue;
}
Class<?> itemType = items[i].getClass().getComponentType();
if (itemType != type) { // 保证类型相同
throw new IllegalArgumentException("Arguments' types are different");
}
totalLen += items[i].length;
}
if (totalLen == 0) { // 确定最终数组的长度
return new Object[0];
}
Object result = Array.newInstance(type, totalLen);
int index = 0;
// 遍历全部的结果数组将items二维数组中的每个元素都加到result中形成一维数组
for (Object[] array : items) {
if (array != null) {
for (int j = 0; j < array.length; j++) {
Array.set(result, index++, array[j]);
}
}
}
return (Object[]) result;
}
其他基础数据类型数组的 Merger 实现 ArrayMerger 的实现非常类似都是将相应类型的二维数组拍平成同类型的一维数组这里以 IntArrayMerger 为例进行分析
public int[] merge(int[]... items) {
if (ArrayUtils.isEmpty(items)) {
// 检测传入的多个int[]不能为空
return new int[0];
}
// 直接使用Stream的API将多个int[]数组拍平成一个int[]数组
return Arrays.stream(items).filter(Objects::nonNull)
.flatMapToInt(Arrays::stream)
.toArray();
}
剩余的其他基础类型的 Merger 实现类例如FloatArrayMergerIntArrayMergerLongArrayMergerBooleanArrayMergerByteArrayMergerCharArrayMergerDoubleArrayMerger 这里就不再赘述你若感兴趣的话可以参考源码进行学习
MapMerger
SetMergerListMerger MapMerger 是针对 Set List Map 返回值的 Merger 实现它们会将多个 Set ListMap集合合并成一个 Set ListMap集合核心原理与 ArrayMerger 的实现类似这里我们先来看 MapMerger 的核心实现
public Map<?, ?> merge(Map<?, ?>... items) {
if (ArrayUtils.isEmpty(items)) {
// 空结果集时这就返回空Map
return Collections.emptyMap();
}
// 将items中所有Map集合中的KV添加到result这一个Map集合中
Map<Object, Object> result = new HashMap<Object, Object>();
Stream.of(items).filter(Objects::nonNull).forEach(result::putAll);
return result;
}
接下来再看 SetMerger 和 ListMerger 的核心实现:
public Set<Object> merge(Set<?>... items) {
if (ArrayUtils.isEmpty(items)) {
// 空结果集时这就返回空Set集合
return Collections.emptySet();
}
// 创建一个新的HashSet集合传入的所有Set集合都添加到result中
Set<Object> result = new HashSet<Object>();
Stream.of(items).filter(Objects::nonNull).forEach(result::addAll);
return result;
}
public List<Object> merge(List<?>... items) {
if (ArrayUtils.isEmpty(items)) {
// 空结果集时这就返回空Set集合
return Collections.emptyList();
}
// 通过Stream API将传入的所有List集合拍平成一个List集合并返回
return Stream.of(items).filter(Objects::nonNull)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
自定义 Merger 扩展实现
介绍完 Dubbo 自带的 Merger 实现之后,下面我们还可以尝试动手写一个自己的 Merger 实现,这里我们以 dubbo-demo-xml 中的 Provider 和 Consumer 为例进行修改。
首先我们在 dubbo-demo-xml-provider 示例模块中发布两个服务,分别属于 groupA 和 groupB相应的 dubbo-provider.xml 配置如下:
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<dubbo:application metadata-type="remote" name="demo-provider"/>
<dubbo:metadata-report address="zookeeper://127.0.0.1:2181"/>
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<dubbo:protocol name="dubbo"/>
<!-- 配置两个Spring Bean -->
<bean id="demoService" class="org.apache.dubbo.demo.provider.DemoServiceImpl"/>
<bean id="demoServiceB" class="org.apache.dubbo.demo.provider.DemoServiceImpl"/>
<!-- 将demoService和demoServiceB两个Spring Bean作为服务发布出去分别属于groupA和groupB-->
<dubbo:service interface="org.apache.dubbo.demo.DemoService" ref="demoService" group="groupA"/>
<dubbo:service interface="org.apache.dubbo.demo.DemoService" ref="demoServiceB" group="groupB"/>
</beans>
接下来,在 dubbo-demo-xml-consumer 示例模块中进行服务引用dubbo-consumer.xml 配置文件的具体内容如下:
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<dubbo:application name="demo-consumer"/>
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<!-- 引用DemoService这里指定了group为*即可以引用任何group的Provider同时merger设置为true即需要对结果进行合并-->
<dubbo:reference id="demoService" check="false" interface="org.apache.dubbo.demo.DemoService" group="*" merger="true"/>
</beans>
然后,在 dubbo-demo-xml-consumer 示例模块的 /resources/META-INF/dubbo 目录下,添加一个名为 org.apache.dubbo.rpc.cluster.Merger 的 Dubbo SPI 配置文件,其内容如下:
String=org.apache.dubbo.demo.consumer.StringMerger
StringMerger 实现了前面介绍的 Merger 接口,它会将多个 Provider 节点返回的 String 结果值拼接起来,具体实现如下:
public class StringMerger implements Merger<String> {
@Override
public String merge(String... items) {
if (ArrayUtils.isEmpty(items)) { // 检测空返回值
return "";
}
String result = "";
for (String item : items) { // 通过竖线将多个Provider的返回值拼接起来
result += item + "|";
}
return result;
}
}
最后,我们依次启动 Zookeeper、dubbo-demo-xml-provider 示例模块和 dubbo-demo-xml-consumer 示例模块。在控制台中我们会看到如下输出:
result: Hello world, response from provider: 172.17.108.179:20880|Hello world, response from provider: 172.17.108.179:20880|
总结
本课时我们重点介绍了 MergeableCluster 中涉及的 Merger 合并器相关的知识点。
首先,我们介绍了 MergerFactory 工厂类的核心功能,它可以配合远程方法调用的返回值,选择对应的 Merger 实现,完成结果的合并。
然后,我们深入分析了 Dubbo 自带的 Merger 实现类,涉及 Java 中各个基础类型数组的 Merger 合并器实现例如IntArrayMerger、LongArrayMerger 等,它们都是将多个特定类型的一维数组拍平成相同类型的一维数组。
除了这些基础类型数组的 Merger 实现Dubbo 还提供了 List、Set、Map 等集合类的 Merger 实现,它们的核心是将多个集合中的元素整理到一个同类型的集合中。
最后,我们还以 StringMerger 为例,介绍了如何自定义 Merger 合并器。

View File

@@ -0,0 +1,447 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
40 加餐模拟远程调用Mock 机制帮你搞定
你好我是杨四正今天我和你分享的主题是Dubbo 中的 Mock 机制。
Mock 机制是 RPC 框架中非常常见、也非常有用的功能不仅可以用来实现服务降级还可以用来在测试中模拟调用的各种异常情况。Dubbo 中的 Mock 机制是在 Consumer 这一端实现的,具体来说就是在 Cluster 这一层实现的。
在前面第 38 课时中,我们深入介绍了 Dubbo 提供的多种 Cluster 实现以及相关的 Cluster Invoker 实现,其中的 ZoneAwareClusterInvoker 就涉及了 MockClusterInvoker 的相关内容。本课时我们就来介绍 Dubbo 中 Mock 机制的全链路流程,不仅包括与 Cluster 接口相关的 MockClusterWrapper 和 MockClusterInvoker我们还会回顾前面课程的 Router 和 Protocol 接口,分析它们与 Mock 机制相关的实现。
MockClusterWrapper
Cluster 接口有两条继承线(如下图所示):一条线是 AbstractCluster 抽象类,这条继承线涉及的全部 Cluster 实现类我们已经在[第 37 课时]中深入分析过了;另一条线是 MockClusterWrapper 这条线。
Cluster 继承关系图
MockClusterWrapper 是 Cluster 对象的包装类,我们在之前[第 4 课时]介绍 Dubbo SPI 机制时已经分析过 Wrapper 的功能MockClusterWrapper 类会对 Cluster 进行包装。下面是 MockClusterWrapper 的具体实现,其中会在 Cluster Invoker 对象的基础上使用 MockClusterInvoker 进行包装:
public class MockClusterWrapper implements Cluster {
private Cluster cluster;
// Wrapper类都会有一个拷贝构造函数
public MockClusterWrapper(Cluster cluster) {
this.cluster = cluster;
}
@Override
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
// 用MockClusterInvoker进行包装
return new MockClusterInvoker<T>(directory,
this.cluster.join(directory));
}
}
MockClusterInvoker
MockClusterInvoker 是 Dubbo Mock 机制的核心,它主要是通过 invoke()、doMockInvoke() 和 selectMockInvoker() 这三个核心方法来实现 Mock 机制的。
下面我们就来逐个介绍这三个方法的具体实现。
首先来看 MockClusterInvoker 的 invoke() 方法,它会先判断是否需要开启 Mock 机制。如果在 mock 参数中配置的是 force 模式,则会直接调用 doMockInvoke() 方法进行 mock。如果在 mock 参数中配置的是 fail 模式,则会正常调用 Invoker 发起请求,在请求失败的时候,会调动 doMockInvoke() 方法进行 mock。下面是 MockClusterInvoker 的 invoke() 方法的具体实现:
public Result invoke(Invocation invocation) throws RpcException {
Result result = null;
// 从URL中获取方法对应的mock配置
String value = getUrl().getMethodParameter(invocation.getMethodName(), MOCK_KEY, Boolean.FALSE.toString()).trim();
if (value.length() == 0 || "false".equalsIgnoreCase(value)) {
// 若mock参数未配置或是配置为false则不会开启Mock机制直接调用底层的Invoker
result = this.invoker.invoke(invocation);
} else if (value.startsWith("force")) {
//force:direct mock
// 若mock参数配置为force则表示强制mock直接调用doMockInvoke()方法
result = doMockInvoke(invocation, null);
} else {
// 如果mock配置的不是force那配置的就是fail会继续调用Invoker对象的invoke()方法进行请求
try {
result = this.invoker.invoke(invocation);
} catch (RpcException e) {
if (e.isBiz()) { // 如果是业务异常,会直接抛出
throw e;
}
// 如果是非业务异常会调用doMockInvoke()方法返回mock结果
result = doMockInvoke(invocation, e);
}
}
return result;
}
在 doMockInvoke() 方法中,首先调用 selectMockInvoker() 方法获取 MockInvoker 对象,并调用其 invoke() 方法进行 mock 操作。doMockInvoke() 方法的具体实现如下:
private Result doMockInvoke(Invocation invocation, RpcException e) {
Result result = null;
Invoker<T> minvoker;
// 调用selectMockInvoker()方法过滤得到MockInvoker
List<Invoker<T>> mockInvokers = selectMockInvoker(invocation);
if (CollectionUtils.isEmpty(mockInvokers)) {
// 如果selectMockInvoker()方法未返回MockInvoker对象则创建一个MockInvoker
minvoker = (Invoker<T>) new MockInvoker(getUrl(), directory.getInterface());
} else {
minvoker = mockInvokers.get(0);
}
try {
// 调用MockInvoker.invoke()方法进行mock
result = minvoker.invoke(invocation);
} catch (RpcException me) {
if (me.isBiz()) { // 如果是业务异常则在Result中设置该异常
result = AsyncRpcResult.newDefaultAsyncResult(me.getCause(), invocation);
} else {
throw new RpcException(...);
}
} catch (Throwable me) {
throw new RpcException(...);
}
return result;
}
selectMockInvoker() 方法中并没有进行 MockInvoker 的选择或是创建,它仅仅是将 Invocation 附属信息中的 invocation.need.mock 属性设置为 true然后交给 Directory 中的 Router 集合进行处理。selectMockInvoker() 方法的具体实现如下:
private List<Invoker<T>> selectMockInvoker(Invocation invocation) {
List<Invoker<T>> invokers = null;
if (invocation instanceof RpcInvocation) {
// 将Invocation附属信息中的invocation.need.mock属性设置为true
((RpcInvocation) invocation).setAttachment(INVOCATION_NEED_MOCK, Boolean.TRUE.toString());
invokers = directory.list(invocation);
}
return invokers;
}
MockInvokersSelector
在[第 32 课时]和[第 33 课时]中,我们介绍了 Router 接口多个实现类,但当时并没有深入介绍 Mock 相关的 Router 实现类—— MockInvokersSelector它的继承关系如下图所示
MockInvokersSelector 继承关系图
MockInvokersSelector 是 Dubbo Mock 机制相关的 Router 实现,在未开启 Mock 机制的时候,会返回正常的 Invoker 对象集合;在开启 Mock 机制之后,会返回 MockInvoker 对象集合。MockInvokersSelector 的具体实现如下:
public <T> List<Invoker<T>> route(final List<Invoker<T>> invokers,
URL url, final Invocation invocation) throws RpcException {
if (CollectionUtils.isEmpty(invokers)) {
return invokers;
}
if (invocation.getObjectAttachments() == null) {
// attachments为null会过滤掉MockInvoker只返回正常的Invoker对象
return getNormalInvokers(invokers);
} else {
String value = (String) invocation.getObjectAttachments().get(INVOCATION_NEED_MOCK);
if (value == null) {
// invocation.need.mock为null会过滤掉MockInvoker只返回正常的Invoker对象
return getNormalInvokers(invokers);
} else if (Boolean.TRUE.toString().equalsIgnoreCase(value)) {
// invocation.need.mock为true会过滤掉MockInvoker只返回正常的Invoker对象
return getMockedInvokers(invokers);
}
}
// invocation.need.mock为false则会将MockInvoker和正常的Invoker一起返回
return invokers;
}
在 getMockedInvokers() 方法中,会根据 URL 的 Protocol 进行过滤,只返回 Protocol 为 mock 的 Invoker 对象,而 getNormalInvokers() 方法只会返回 Protocol 不为 mock 的 Invoker 对象。这两个方法的具体实现比较简单,这里就不再展示,你若感兴趣的话可以参考源码进行学习。
MockProtocol & MockInvoker
介绍完 Mock 功能在 Cluster 层的相关实现之后,我们还要来看一下 Dubbo 在 RPC 层对 Mock 机制的支持,这里涉及 MockProtocol 和 MockInvoker 两个类。
首先来看 MockProtocol它是 Protocol 接口的扩展实现,扩展名称为 mock。MockProtocol 只能通过 refer() 方法创建 MockInvoker不能通过 export() 方法暴露服务,具体实现如下:
final public class MockProtocol extends AbstractProtocol {
public int getDefaultPort() { return 0;}
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
// 直接抛出异常,无法暴露服务
throw new UnsupportedOperationException();
}
public <T> Invoker<T> protocolBindingRefer(Class<T> type, URL url) throws RpcException {
// 直接创建MockInvoker对象
return new MockInvoker<>(url, type);
}
}
下面我们再来看 MockInvoker 是如何解析各类 mock 配置的,以及如何根据不同 mock 配置进行不同处理的。这里我们重点来看 MockInvoker.invoke() 方法,其中针对 mock 参数进行的分类处理具体有下面三条分支。
mock 参数以 return 开头:直接返回 mock 参数指定的固定值例如empty、null、true、false、json 等。mock 参数中指定的固定返回值将会由 parseMockValue() 方法进行解析。
mock 参数以 throw 开头:直接抛出异常。如果在 mock 参数中没有指定异常类型,则抛出 RpcException否则抛出指定的 Exception 类型。
mock 参数为 true 或 default 时,会查找服务接口对应的 Mock 实现;如果是其他值,则直接作为服务接口的 Mock 实现。拿到 Mock 实现之后,转换成 Invoker 进行调用。
MockInvoker.invoke() 方法的具体实现如下所示:
public Result invoke(Invocation invocation) throws RpcException {
if (invocation instanceof RpcInvocation) {
((RpcInvocation) invocation).setInvoker(this);
}
// 获取mock值(会从URL中的methodName.mock参数或mock参数获取)
String mock = null;
if (getUrl().hasMethodParameter(invocation.getMethodName())) {
mock = getUrl().getParameter(invocation.getMethodName() + "." + MOCK_KEY);
}
if (StringUtils.isBlank(mock)) {
mock = getUrl().getParameter(MOCK_KEY);
}
if (StringUtils.isBlank(mock)) { // 没有配置mock值直接抛出异常
throw new RpcException(new IllegalAccessException("mock can not be null. url :" + url));
}
// mock值进行处理去除"force:"、"fail:"前缀等
mock = normalizeMock(URL.decode(mock));
if (mock.startsWith(RETURN_PREFIX)) { // mock值以return开头
mock = mock.substring(RETURN_PREFIX.length()).trim();
try {
// 获取响应结果的类型
Type[] returnTypes = RpcUtils.getReturnTypes(invocation);
// 根据结果类型对mock值中结果值进行转换
Object value = parseMockValue(mock, returnTypes);
// 将固定的mock值设置到Result中
return AsyncRpcResult.newDefaultAsyncResult(value, invocation);
} catch (Exception ew) {
throw new RpcException("mock return invoke error. method :" + invocation.getMethodName()
+ ", mock:" + mock + ", url: " + url, ew);
}
} else if (mock.startsWith(THROW_PREFIX)) { // mock值以throw开头
mock = mock.substring(THROW_PREFIX.length()).trim();
if (StringUtils.isBlank(mock)) { // 未指定异常类型直接抛出RpcException
throw new RpcException("mocked exception for service degradation.");
} else { // 抛出自定义异常
Throwable t = getThrowable(mock);
throw new RpcException(RpcException.BIZ_EXCEPTION, t);
}
} else { // 执行mockService得到mock结果
try {
Invoker<T> invoker = getInvoker(mock);
return invoker.invoke(invocation);
} catch (Throwable t) {
throw new RpcException("Failed to create mock implementation class " + mock, t);
}
}
}
针对 return 和 throw 的处理逻辑比较简单,但 getInvoker() 方法略微复杂些,其中会处理 MOCK_MAP 缓存的读写、Mock 实现类的查找、生成和调用 Invoker具体实现如下
private Invoker<T> getInvoker(String mockService) {
// 尝试从MOCK_MAP集合中获取对应的Invoker对象
Invoker<T> invoker = (Invoker<T>) MOCK_MAP.get(mockService);
if (invoker != null) {
return invoker;
}
// 根据serviceType查找mock的实现类
Class<T> serviceType = (Class<T>) ReflectUtils.forName(url.getServiceInterface());
T mockObject = (T) getMockObject(mockService, serviceType);
// 创建Invoker对象
invoker = PROXY_FACTORY.getInvoker(mockObject, serviceType, url);
if (MOCK_MAP.size() < 10000) { // 写入缓存
MOCK_MAP.put(mockService, invoker);
}
return invoker;
}
getMockObject() 方法中会检查 mockService 参数是否为 true default如果是的话则在服务接口后添加 Mock 字符串作为服务接口的 Mock 实现如果不是的话则直接将 mockService 实现作为服务接口的 Mock 实现getMockObject() 方法的具体实现如下
public static Object getMockObject(String mockService, Class serviceType) {
if (ConfigUtils.isDefault(mockService)) {
// 如果mock为true或default值会在服务接口后添加Mock字符串得到对应的实现类名称并进行实例化
mockService = serviceType.getName() + "Mock";
}
Class<?> mockClass = ReflectUtils.forName(mockService);
if (!serviceType.isAssignableFrom(mockClass)) {
// 检查mockClass是否继承serviceType接口
throw new IllegalStateException("...");
}
return mockClass.newInstance();
}
总结
本课时我们重点介绍了 Dubbo 中 Mock 机制涉及的全部内容。
首先,我们介绍了 Cluster 接口的 MockClusterWrapper 实现类,它负责创建 MockClusterInvoker 对象,是 Dubbo Mock 机制的入口。
接下来,我们介绍了 MockClusterInvoker 这个 Cluster 层的 Invoker 实现,它是 Dubbo Mock 机制的核心,会根据配置决定请求是否启动了 Mock 机制以及在何种情况下才会触发 Mock。
随后,我们又讲解了 MockInvokersSelector 这个 Router 接口实现,它会在路由规则这个层面决定是否返回 MockInvoker 对象。
最后,我们分析了 Protocol 层与 Mock 相关的实现—— MockProtocol以及 MockInvoker 这个真正进行 Mock 操作的 Invoker 实现。在 MockInvoker 中会解析各类 Mock 配置,并根据不同 Mock 配置进行不同的 Mock 操作。

View File

@@ -0,0 +1,746 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
41 加餐:一键通关服务发布全流程
在前面的课时中,我们已经将整个 Dubbo 的核心实现进行了分析。接下来的两个课时,我们将串联 Dubbo 中的这些核心实现,分析 Dubbo服务发布和服务引用的全流程帮助你将之前课时介绍的独立知识点联系起来形成一个完整整体。
本课时我们就先来重点关注 Provider 节点发布服务的过程,在这个过程中会使用到之前介绍的很多 Dubbo 核心组件。我们从 DubboBootstrap 这个入口类开始介绍,分析 Provider URL 的组装以及服务发布流程,其中会详细介绍本地发布和远程发布的核心流程。
DubboBootstrap 入口
在[第 01 课时]dubbo-demo-api-provider 示例的 Provider 实现中我们可以看到,整个 Provider 节点的启动入口是 DubboBootstrap.start() 方法,在该方法中会执行一些初始化操作,以及一些状态控制字段的更新,具体实现如下:
public DubboBootstrap start() {
if (started.compareAndSet(false, true)) { // CAS操作保证启动一次
ready.set(false); // 用于判断当前节点是否已经启动完毕在后面的Dubbo QoS中会使用到该字段
// 初始化一些基础组件,例如,配置中心相关组件、事件监听、元数据相关组件,这些组件在后面将会进行介绍
initialize();
// 重点:发布服务
exportServices();
if (!isOnlyRegisterProvider() || hasExportedServices()) {
// 用于暴露本地元数据服务,后面介绍元数据的时候会深入介绍该部分的内容
exportMetadataService();
// 用于将服务实例注册到专用于服务发现的注册中心
registerServiceInstance();
}
// 处理Consumer的ReferenceConfig
referServices();
if (asyncExportingFutures.size() > 0) {
// 异步发布服务会启动一个线程监听发布是否完成完成之后会将ready设置为true
new Thread(() -> {
this.awaitFinish();
ready.set(true);
}).start();
} else { // 同步发布服务成功之后会将ready设置为true
ready.set(true);
}
}
return this;
}
不仅是直接通过 API 启动 Provider 的方式会使用到 DubboBootstrap在 Spring 与 Dubbo 集成的时候也是使用 DubboBootstrap 作为服务发布入口的,具体逻辑在 DubboBootstrapApplicationListener 这个 Spring Context 监听器中,如下所示:
public class DubboBootstrapApplicationListener extends OneTimeExecutionApplicationContextEventListener
implements Ordered {
private final DubboBootstrap dubboBootstrap;
public DubboBootstrapApplicationListener() {
// 初始化DubboBootstrap对象
this.dubboBootstrap = DubboBootstrap.getInstance();
}
@Override
public void onApplicationContextEvent(ApplicationContextEvent event) {
// 监听ContextRefreshedEvent事件和ContextClosedEvent事件
if (event instanceof ContextRefreshedEvent) {
onContextRefreshedEvent((ContextRefreshedEvent) event);
} else if (event instanceof ContextClosedEvent) {
onContextClosedEvent((ContextClosedEvent) event);
}
}
private void onContextRefreshedEvent(ContextRefreshedEvent event) {
dubboBootstrap.start(); // 启动DubboBootstrap
}
private void onContextClosedEvent(ContextClosedEvent event) {
dubboBootstrap.stop();
}
@Override
public int getOrder() {
return LOWEST_PRECEDENCE;
}
}
这里我们重点关注的是exportServices() 方法,它是服务发布核心逻辑的入口,其中每一个服务接口都会转换为对应的 ServiceConfig 实例,然后通过代理的方式转换成 Invoker最终转换成 Exporter 进行发布。服务发布流程中涉及的核心对象转换,如下图所示:
服务发布核心流程图
exportServices() 方法的具体实现如下:
private void exportServices() {
// 从配置管理器中获取到所有的要暴露的服务配置一个接口类对应一个ServiceConfigBase实例
configManager.getServices().forEach(sc -> {
ServiceConfig serviceConfig = (ServiceConfig) sc;
serviceConfig.setBootstrap(this);
if (exportAsync) { // 异步模式,获取一个线程池来异步执行服务发布逻辑
ExecutorService executor = executorRepository.getServiceExporterExecutor();
Future<?> future = executor.submit(() -> {
sc.export();
exportedServices.add(sc);
});
// 记录异步发布的Future
asyncExportingFutures.add(future);
} else {// 同步发布
sc.export();
exportedServices.add(sc);
}
});
}
ServiceConfig
在 ServiceConfig.export() 方法中,服务发布的第一步是检查参数,第二步会根据当前配置决定是延迟发布还是立即调用 doExport() 方法进行发布,第三步会通过 exported() 方法回调相关监听器,具体实现如下:
public synchronized void export() {
if (!shouldExport()) {
return;
}
if (bootstrap == null) {
bootstrap = DubboBootstrap.getInstance();
bootstrap.init();
}
// 检查并更新各项配置
checkAndUpdateSubConfigs();
... // 初始化元数据相关服务
if (shouldDelay()) { // 延迟发布
DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
} else { // 立即发布
doExport();
}
exported(); // 回调监听器
}
在 checkAndUpdateSubConfigs() 方法中,会去检查各项配置是否合理,并补齐一些缺省的配置信息,这个方法非常冗长,这里就不再展示,你若感兴趣的话可以参考源码进行学习。
完成配置的检查之后,再来看 doExport() 方法,其中首先调用 loadRegistries() 方法加载注册中心信息,即将 RegistryConfig 配置解析成 registryUrl。无论是使用 XML、Annotation还是 API 配置方式,都可以配置多个注册中心地址,一个服务接口可以同时注册在多个不同的注册中心。
RegistryConfig 是 Dubbo 的多个配置对象之一,可以通过解析 XML、Annotation 中注册中心相关的配置得到,对应的配置如下(当然,也可以直接通过 API 创建得到):
<dubbo:registry address="zookeeper://127.0.0.1:2181" protocol="zookeeper" port="2181" />
RegistryUrl 的格式大致如下(为了方便查看,这里将每个 URL 参数单独放在一行中展示):
// path是Zookeeper的地址
registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?
application=dubbo-demo-api-provider
&dubbo=2.0.2
&pid=9405
&registry=zookeeper // 使用的注册中心是Zookeeper
&timestamp=1600307343086
加载注册中心信息得到 RegistryUrl 之后,会遍历所有的 ProtocolConfig依次调用 doExportUrlsFor1Protocol(protocolConfig, registryURLs) 在每个注册中心发布服务。一个服务接口可以以多种协议进行发布,每种协议都对应一个 ProtocolConfig例如我们在 Demo 示例中,只使用了 dubbo 协议,对应的配置是:<dubbo:protocol name="dubbo" />。
组装服务 URL
doExportUrlsFor1Protocol() 方法的代码非常长,这里我们分成两个部分进行介绍:一部分是组装服务的 URL另一部分就是后面紧接着介绍的服务发布。
组装服务的 URL核心步骤有如下 7 步。
获取此次发布使用的协议,默认使用 dubbo 协议。
设置服务 URL 中的参数,这里会从 MetricsConfig、ApplicationConfig、ModuleConfig、ProviderConfig、ProtocolConfig 中获取配置信息,并作为参数添加到 URL 中。这里调用的 appendParameters() 方法会将 AbstractConfig 中的配置信息存储到 Map 集合中,后续在构造 URL 的时候,会将该集合中的 KV 作为 URL 的参数。
解析指定方法的 MethodConfig 配置以及方法参数的 ArgumentConfig 配置,得到的配置信息也是记录到 Map 集合中,后续作为 URL 参数。
根据此次调用是泛化调用还是普通调用,向 Map 集合中添加不同的键值对。
获取 token 配置,并添加到 Map 集合中,默认随机生成 UUID。
获取 host、port 值,并开始组装服务的 URL。
根据 Configurator 覆盖或新增 URL 参数。
下面是 doExportUrlsFor1Protocol() 方法组装 URL 的核心实现:
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
String name = protocolConfig.getName(); // 获取协议名称
if (StringUtils.isEmpty(name)) { // 默认使用Dubbo协议
name = DUBBO;
}
Map<String, String> map = new HashMap<String, String>(); // 记录URL的参数
map.put(SIDE_KEY, PROVIDER_SIDE); // side参数
// 添加URL参数例如Dubbo版本、时间戳、当前PID等
ServiceConfig.appendRuntimeParameters(map);
// 下面会从各个Config获取参数例如application、interface参数等
AbstractConfig.appendParameters(map, getMetrics());
AbstractConfig.appendParameters(map, getApplication());
AbstractConfig.appendParameters(map, getModule());
AbstractConfig.appendParameters(map, provider);
AbstractConfig.appendParameters(map, protocolConfig);
AbstractConfig.appendParameters(map, this);
MetadataReportConfig metadataReportConfig = getMetadataReportConfig();
if (metadataReportConfig != null && metadataReportConfig.isValid()) {
map.putIfAbsent(METADATA_KEY, REMOTE_METADATA_STORAGE_TYPE);
}
if (CollectionUtils.isNotEmpty(getMethods())) { // 从MethodConfig中获取URL参数
for (MethodConfig method : getMethods()) {
AbstractConfig.appendParameters(map, method, method.getName());
String retryKey = method.getName() + ".retry";
if (map.containsKey(retryKey)) {
String retryValue = map.remove(retryKey);
if ("false".equals(retryValue)) {
map.put(method.getName() + ".retries", "0");
}
}
List<ArgumentConfig> arguments = method.getArguments();
if (CollectionUtils.isNotEmpty(arguments)) {
for (ArgumentConfig argument : arguments) { // 从ArgumentConfig中获取URL参数
... ...
}
}
}
}
if (ProtocolUtils.isGeneric(generic)) { // 根据generic是否为true向map中添加不同的信息
map.put(GENERIC_KEY, generic);
map.put(METHODS_KEY, ANY_VALUE);
} else {
String revision = Version.getVersion(interfaceClass, version);
if (revision != null && revision.length() > 0) {
map.put(REVISION_KEY, revision);
}
String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
if (methods.length == 0) {
map.put(METHODS_KEY, ANY_VALUE);
} else {
// 添加method参数
map.put(METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
}
}
// 添加token到map集合中默认随机生成UUID
if(ConfigUtils.isEmpty(token) && provider != null) {
token = provider.getToken();
}
if (!ConfigUtils.isEmpty(token)) {
if (ConfigUtils.isDefault(token)) {
map.put(TOKEN_KEY, UUID.randomUUID().toString());
} else {
map.put(TOKEN_KEY, token);
}
}
// 将map数据放入serviceMetadata中这与元数据相关后面再详细介绍其作用
serviceMetadata.getAttachments().putAll(map);
// 获取host、port值
String host = findConfigedHosts(protocolConfig, registryURLs, map);
Integer port = findConfigedPorts(protocolConfig, name, map);
// 根据上面获取的host、port以及前文获取的map集合组装URL
URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map);
// 通过Configurator覆盖或添加新的参数
if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.hasExtension(url.getProtocol())) {
url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.getExtension(url.getProtocol()).getConfigurator(url).configure(url);
}
... ...
}
经过上述准备操作之后,得到的服务 URL 如下所示(为了方便查看,这里将每个 URL 参数单独放在一行中展示):
dubbo://172.17.108.185:20880/org.apache.dubbo.demo.DemoService?
anyhost=true
&application=dubbo-demo-api-provider
&bind.ip=172.17.108.185
&bind.port=20880
&default=true
&deprecated=false
&dubbo=2.0.2
&dynamic=true
&generic=false
&interface=org.apache.dubbo.demo.DemoService
&methods=sayHello,sayHelloAsync
&pid=3918
&release=
&side=provider
&timestamp=1600437404483
服务发布入口
完成了服务 URL 的组装之后doExportUrlsFor1Protocol() 方法开始执行服务发布。服务发布可以分为远程发布和本地发布,具体发布方式与服务 URL 中的 scope 参数有关。
scope 参数有三个可选值,分别是 none、remote 和 local分别代表不发布、发布到本地和发布到远端注册中心从下面介绍的 doExportUrlsFor1Protocol() 方法代码中可以看到:
发布到本地的条件是 scope != remote
发布到注册中心的条件是 scope != local。
scope 参数的默认值为 null也就是说默认会同时在本地和注册中心发布该服务。下面来看 doExportUrlsFor1Protocol() 方法中发布服务的具体实现:
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
... ...// 省略组装服务URL的过程
// 从URL中获取scope参数其中可选值有none、remote、local三个
// 分别代表不发布、发布到本地以及发布到远端,具体含义在下面一一介绍
String scope = url.getParameter(SCOPE_KEY);
if (!SCOPE_NONE.equalsIgnoreCase(scope)) { // scope不为none才进行发布
if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) {// 发布到本地
exportLocal(url);
}
if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) { // 发布到远端的注册中心
if (CollectionUtils.isNotEmpty(registryURLs)) { // 当前配置了至少一个注册中心
for (URL registryURL : registryURLs) { // 向每个注册中心发布服务
// injvm协议只在exportLocal()中有用,不会将服务发布到注册中心
// 所以这里忽略injvm协议
if (LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())){
continue;
}
// 设置服务URL的dynamic参数
url = url.addParameterIfAbsent(DYNAMIC_KEY, registryURL.getParameter(DYNAMIC_KEY));
// 创建monitorUrl并作为monitor参数添加到服务URL中
URL monitorUrl = ConfigValidationUtils.loadMonitor(this, registryURL);
if (monitorUrl != null) {
url = url.addParameterAndEncoded(MONITOR_KEY, monitorUrl.toFullString());
}
// 设置服务URL的proxy参数即生成动态代理方式(jdk或是javassist)作为参数添加到RegistryURL中
String proxy = url.getParameter(PROXY_KEY);
if (StringUtils.isNotEmpty(proxy)) {
registryURL = registryURL.addParameter(PROXY_KEY, proxy);
}
// 为服务实现类的对象创建相应的InvokergetInvoker()方法的第三个参数中会将服务URL作为export参数添加到RegistryURL中
// 这里的PROXY_FACTORY是ProxyFactory接口的适配器
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
// DelegateProviderMetaDataInvoker是个装饰类将当前ServiceConfig和Invoker关联起来而已invoke()方法透传给底层Invoker对象
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
// 调用Protocol实现进行发布
// 这里的PROTOCOL是Protocol接口的适配器
Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
exporters.add(exporter);
}
} else {
// 不存在注册中心仅发布服务不会将服务信息发布到注册中心。Consumer没法在注册中心找到该服务的信息但是可以直连
// 具体的发布过程与上面的过程类似,只不过不会发布到注册中心
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, url);
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
exporters.add(exporter);
}
// 元数据相关操作
WritableMetadataService metadataService = WritableMetadataService.getExtension(url.getParameter(METADATA_KEY, DEFAULT_METADATA_STORAGE_TYPE));
if (metadataService != null) {
metadataService.publishServiceDefinition(url);
}
}
}
this.urls.add(url);
}
本地发布
了解了本地发布、远程发布的入口逻辑之后,下面我们开始深入本地发布的逻辑。
在 exportLocal() 方法中,会将 Protocol 替换成 injvm 协议,将 host 设置成 127.0.0.1,将 port 设置为 0得到新的 LocalURL大致如下
injvm://127.0.0.1/org.apache.dubbo.demo.DemoService?anyhost=true
&application=dubbo-demo-api-provider
&bind.ip=172.17.108.185
&bind.port=20880
&default=true
&deprecated=false
&dubbo=2.0.2
&dynamic=true
&generic=false
&interface=org.apache.dubbo.demo.DemoService
&methods=sayHello,sayHelloAsync
&pid=4249
&release=
&side=provider
&timestamp=1600440074214
之后,会通过 ProxyFactory 接口适配器找到对应的 ProxyFactory 实现(默认使用 JavassistProxyFactory并调用 getInvoker() 方法创建 Invoker 对象;最后,通过 Protocol 接口的适配器查找到 InjvmProtocol 实现,并调用 export() 方法进行发布。 exportLocal() 方法的具体实现如下:
private void exportLocal(URL url) {
URL local = URLBuilder.from(url) // 创建新URL
.setProtocol(LOCAL_PROTOCOL)
.setHost(LOCALHOST_VALUE)
.setPort(0)
.build();
// 本地发布
Exporter<?> exporter = PROTOCOL.export(
PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, local));
exporters.add(exporter);
}
InjvmProtocol 的相关实现比较简单,这里就不再展示,你若感兴趣的话可以参考源码进行学习。
远程发布
介绍完本地发布之后,我们再来看远程发布的核心逻辑,远程服务发布的流程相较本地发布流程,要复杂得多。
在 doExportUrlsFor1Protocol() 方法中,远程发布服务时,会遍历全部 RegistryURL并根据 RegistryURL 选择对应的 Protocol 扩展实现进行发布。我们知道 RegistryURL 是 “registry://” 协议,所以这里使用的是 RegistryProtocol 实现。
下面来看 RegistryProtocol.export() 方法的核心流程:
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
// 将"registry://"协议转换成"zookeeper://"协议
URL registryUrl = getRegistryUrl(originInvoker);
// 获取export参数其中存储了一个"dubbo://"协议的ProviderURL
URL providerUrl = getProviderUrl(originInvoker);
// 获取要监听的配置目录这里会在ProviderURL的基础上添加category=configurators参数并封装成对OverrideListener记录到overrideListeners集合中
final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
// 初始化时会检测一次Override配置重写ProviderURL
providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
// 导出服务底层会通过执行DubboProtocol.export()方法启动对应的Server
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
// 根据RegistryURL获取对应的注册中心Registry对象其中会依赖之前课时介绍的RegistryFactory
final Registry registry = getRegistry(originInvoker);
// 获取将要发布到注册中心上的Provider URL其中会删除一些多余的参数信息
final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);
// 根据register参数值决定是否注册服务
boolean register = providerUrl.getParameter(REGISTER_KEY, true);
if (register) { // 调用Registry.register()方法将registeredProviderUrl发布到注册中心
register(registryUrl, registeredProviderUrl);
}
// 将Provider相关信息记录到的ProviderModel中
registerStatedUrl(registryUrl, registeredProviderUrl, register);
// 向注册中心进行订阅override数据主要是监听该服务的configurators节点
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
exporter.setRegisterUrl(registeredProviderUrl);
exporter.setSubscribeUrl(overrideSubscribeUrl);
// 触发RegistryProtocolListener监听器
notifyExport(exporter);
return new DestroyableExporter<>(exporter);
}
我们可以看到,远程发布流程大致可分为下面 5 个步骤。
准备 URL比如 ProviderURL、RegistryURL 和 OverrideSubscribeUrl。
发布 Dubbo 服务。在 doLocalExport() 方法中调用 DubboProtocol.export() 方法启动 Provider 端底层 Server。
注册 Dubbo 服务。在 register() 方法中,调用 ZookeeperRegistry.register() 方法向 Zookeeper 注册服务。
订阅 Provider 端的 Override 配置。调用 ZookeeperRegistry.subscribe() 方法订阅注册中心 configurators 节点下的配置变更。
触发 RegistryProtocolListener 监听器。
远程发布的详细流程如下图所示:
服务发布详细流程图
总结
本课时我们重点介绍了 Dubbo 服务发布的核心流程。
首先我们介绍了 DubboBootstrap 这个入口门面类中与服务发布相关的方法,重点是 start() 和 exportServices() 两个方法;然后详细介绍了 ServiceConfig 类的三个核心步骤:检查参数、立即(或延迟)执行 doExport() 方法进行发布、回调服务发布的相关监听器。
接下来我们分析了doExportUrlsFor1Protocol() 方法,它是发布一个服务的入口,也是规定服务发布流程的地方,其中涉及 Provider URL 的组装、本地服务发布流程以及远程服务发布流程,对于这些步骤,我们都进行了详细的分析。

View File

@@ -0,0 +1,594 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
42 加餐:服务引用流程全解析
Dubbo 作为一个 RPC 框架,暴露给用户最基本的功能就是服务发布和服务引用。在上一课时,我们已经分析了服务发布的核心流程。那么在本课时,我们就接着深入分析服务引用的核心流程。
Dubbo 支持两种方式引用远程的服务:
服务直连的方式,仅适合在调试服务的时候使用;
基于注册中心引用服务,这是生产环境中使用的服务引用方式。
DubboBootstrap 入口
在上一课时介绍服务发布的时候,我们介绍了 DubboBootstrap.start() 方法的核心流程,其中除了会调用 exportServices() 方法完成服务发布之外,还会调用 referServices() 方法完成服务引用,这里就不再贴出 DubboBootstrap.start() 方法的具体代码,你若感兴趣的话可以参考源码进行学习。
在 DubboBootstrap.referServices() 方法中,会从 ConfigManager 中获取所有 ReferenceConfig 列表,并根据 ReferenceConfig 获取对应的代理对象,入口逻辑如下:
private void referServices() {
if (cache == null) { // 初始ReferenceConfigCache
cache = ReferenceConfigCache.getCache();
}
configManager.getReferences().forEach(rc -> {
// 遍历ReferenceConfig列表
ReferenceConfig referenceConfig = (ReferenceConfig) rc;
referenceConfig.setBootstrap(this);
if (rc.shouldInit()) { // 检测ReferenceConfig是否已经初始化
if (referAsync) { // 异步
CompletableFuture<Object> future = ScheduledCompletableFuture.submit(
executorRepository.getServiceExporterExecutor(),
() -> cache.get(rc)
);
asyncReferringFutures.add(future);
} else { // 同步
cache.get(rc);
}
}
});
}
这里的 ReferenceConfig 是哪里来的呢?在[第 01 课时]dubbo-demo-api-consumer 示例中,我们可以看到构造 ReferenceConfig 对象的逻辑,这些新建的 ReferenceConfig 对象会通过 DubboBootstrap.reference() 方法添加到 ConfigManager 中进行管理,如下所示:
public DubboBootstrap reference(ReferenceConfig<?> referenceConfig) {
configManager.addReference(referenceConfig);
return this;
}
ReferenceConfigCache
服务引用的核心实现在 ReferenceConfig 之中,一个 ReferenceConfig 对象对应一个服务接口,每个 ReferenceConfig 对象中都封装了与注册中心的网络连接,以及与 Provider 的网络连接,这是一个非常重要的对象。
为了避免底层连接泄漏造成性能问题,从 Dubbo 2.4.0 版本开始Dubbo 提供了 ReferenceConfigCache 用于缓存 ReferenceConfig 实例。
在 dubbo-demo-api-consumer 示例中,我们可以看到 ReferenceConfigCache 的基本使用方式:
ReferenceConfig<DemoService> reference = new ReferenceConfig<>();
reference.setInterface(DemoService.class);
...
// 这一步在DubboBootstrap.start()方法中完成
ReferenceConfigCache cache = ReferenceConfigCache.getCache();
...
DemoService demoService = ReferenceConfigCache.getCache().get(reference);
在 ReferenceConfigCache 中维护了一个静态的 MapCACHE_HOLDER字段其中 Key 是由 Group、服务接口和 version 构成Value 是一个 ReferenceConfigCache 对象。在 ReferenceConfigCache 中可以传入一个 KeyGenerator 用来修改缓存 Key 的生成逻辑KeyGenerator 接口的定义如下:
public interface KeyGenerator {
String generateKey(ReferenceConfigBase<?> referenceConfig);
}
默认的 KeyGenerator 实现是 ReferenceConfigCache 中的匿名内部类,其对象由 DEFAULT_KEY_GENERATOR 这个静态字段引用,具体实现如下:
public static final KeyGenerator DEFAULT_KEY_GENERATOR = referenceConfig -> {
String iName = referenceConfig.getInterface();
if (StringUtils.isBlank(iName)) { // 获取服务接口名称
Class<?> clazz = referenceConfig.getInterfaceClass();
iName = clazz.getName();
}
if (StringUtils.isBlank(iName)) {
throw new IllegalArgumentException("No interface info in ReferenceConfig" + referenceConfig);
}
// Key的格式是group/interface:version
StringBuilder ret = new StringBuilder();
if (!StringUtils.isBlank(referenceConfig.getGroup())) {
ret.append(referenceConfig.getGroup()).append("/");
}
ret.append(iName);
if (!StringUtils.isBlank(referenceConfig.getVersion())) {
ret.append(":").append(referenceConfig.getVersion());
}
return ret.toString();
};
在 ReferenceConfigCache 实例对象中,会维护下面两个 Map 集合。
proxiesConcurrentMap, ConcurrentMap>类型):该集合用来存储服务接口的全部代理对象,其中第一层 Key 是服务接口的类型,第二层 Key 是上面介绍的 KeyGenerator 为不同服务提供方生成的 KeyValue 是服务的代理对象。
referredReferencesConcurrentMap> 类型):该集合用来存储已经被处理的 ReferenceConfig 对象。
我们回到 DubboBootstrap.referServices() 方法中,看一下其中与 ReferenceConfigCache 相关的逻辑。
首先是 ReferenceConfigCache.getCache() 这个静态方法,会在 CACHE_HOLDER 集合中添加一个 Key 为“*DEFAULT*”的 ReferenceConfigCache 对象(使用默认的 KeyGenerator 实现),它将作为默认的 ReferenceConfigCache 对象。
接下来,无论是同步服务引用还是异步服务引用,都会调用 ReferenceConfigCache.get() 方法,创建并缓存代理对象。下面就是 ReferenceConfigCache.get() 方法的核心实现:
public <T> T get(ReferenceConfigBase<T> referenceConfig) {
// 生成服务提供方对应的Key
String key = generator.generateKey(referenceConfig);
// 获取接口类型
Class<?> type = referenceConfig.getInterfaceClass();
// 获取该接口对应代理对象集合
proxies.computeIfAbsent(type, _t -> new ConcurrentHashMap<>());
ConcurrentMap<String, Object> proxiesOfType = proxies.get(type);
// 根据Key获取服务提供方对应的代理对象
proxiesOfType.computeIfAbsent(key, _k -> {
// 服务引用
Object proxy = referenceConfig.get();
// 将ReferenceConfig记录到referredReferences集合
referredReferences.put(key, referenceConfig);
return proxy;
});
return (T) proxiesOfType.get(key);
}
ReferenceConfig
通过前面的介绍我们知道ReferenceConfig 是服务引用的真正入口,其中会创建相关的代理对象。下面先来看 ReferenceConfig.get() 方法:
public synchronized T get() {
if (destroyed) { // 检测当前ReferenceConfig状态
throw new IllegalStateException("...");
}
if (ref == null) {// ref指向了服务的代理对象
init(); // 初始化ref字段
}
return ref;
}
在 ReferenceConfig.init() 方法中,首先会对服务引用的配置进行处理,以保证配置的正确性。这里的具体实现其实本身并不复杂,但由于涉及很多的配置解析和处理逻辑,代码就显得非常长,我们就不再一一展示,你若感兴趣的话可以参考源码进行学习。
ReferenceConfig.init() 方法的核心逻辑是调用 createProxy() 方法,调用之前会从配置中获取 createProxy() 方法需要的参数:
public synchronized void init() {
if (initialized) { // 检测ReferenceConfig的初始化状态
return;
}
if (bootstrap == null) { // 检测DubboBootstrap的初始化状态
bootstrap = DubboBootstrap.getInstance();
bootstrap.init();
}
... // 省略其他配置的检查
Map<String, String> map = new HashMap<String, String>();
map.put(SIDE_KEY, CONSUMER_SIDE); // 添加side参数
// 添加Dubbo版本、release参数、timestamp参数、pid参数
ReferenceConfigBase.appendRuntimeParameters(map);
// 添加interface参数
map.put(INTERFACE_KEY, interfaceName);
... // 省略其他参数的处理
String hostToRegistry = ConfigUtils.getSystemProperty(DUBBO_IP_TO_REGISTRY);
if (StringUtils.isEmpty(hostToRegistry)) {
hostToRegistry = NetUtils.getLocalHost();
} else if (isInvalidLocalHost(hostToRegistry)) {
throw new IllegalArgumentException("...");
}
// 添加ip参数
map.put(REGISTER_IP_KEY, hostToRegistry);
// 调用createProxy()方法
ref = createProxy(map);
...// 省略其他代码
initialized = true;
// 触发ReferenceConfigInitializedEvent事件
dispatch(new ReferenceConfigInitializedEvent(this, invoker));
}
ReferenceConfig.createProxy() 方法中处理了多种服务引用的场景,例如,直连单个/多个Provider、单个/多个注册中心。下面是 createProxy() 方法的核心流程,大致可以梳理出这么 5 个步骤。
根据传入的参数集合判断协议是否为 injvm 协议,如果是,直接通过 InjvmProtocol 引用服务。
构造 urls 集合。Dubbo 支持直连 Provider和依赖注册中心两种服务引用方式。如果是直连服务的模式我们可以通过 url 参数指定一个或者多个 Provider 地址,会被解析并填充到 urls 集合;如果通过注册中心的方式进行服务引用,则会调用 AbstractInterfaceConfig.loadRegistries() 方法加载所有注册中心。
如果 urls 集合中只记录了一个 URL通过 Protocol 适配器选择合适的 Protocol 扩展实现创建 Invoker 对象。如果是直连 Provider 的场景,则 URL 为 dubbo 协议,这里就会使用 DubboProtocol 这个实现;如果依赖注册中心,则使用 RegistryProtocol 这个实现。
如果 urls 集合中有多个注册中心,则使用 ZoneAwareCluster 作为 Cluster 的默认实现,生成对应的 Invoker 对象;如果 urls 集合中记录的是多个直连服务的地址,则使用 Cluster 适配器选择合适的扩展实现生成 Invoker 对象。
通过 ProxyFactory 适配器选择合适的 ProxyFactory 扩展实现,将 Invoker 包装成服务接口的代理对象。
通过上面的流程我们可以看出createProxy() 方法中有两个核心:一是通过 Protocol 适配器选择合适的 Protocol 扩展实现创建 Invoker 对象;二是通过 ProxyFactory 适配器选择合适的 ProxyFactory 创建代理对象。
下面我们来看 createProxy() 方法的具体实现:
private T createProxy(Map<String, String> map) {
if (shouldJvmRefer(map)) { // 根据url的协议、scope以及injvm等参数检测是否需要本地引用
// 创建injvm协议的URL
URL url = new URL(LOCAL_PROTOCOL, LOCALHOST_VALUE, 0, interfaceClass.getName()).addParameters(map);
// 通过Protocol的适配器选择对应的Protocol实现创建Invoker对象
invoker = REF_PROTOCOL.refer(interfaceClass, url);
if (logger.isInfoEnabled()) {
logger.info("Using injvm service " + interfaceClass.getName());
}
} else {
urls.clear();
if (url != null && url.length() > 0) {
String[] us = SEMICOLON_SPLIT_PATTERN.split(url); // 配置多个URL的时候会用分号进行切分
if (us != null && us.length > 0) { // url不为空表明用户可能想进行点对点调用
for (String u : us) {
URL url = URL.valueOf(u);
if (StringUtils.isEmpty(url.getPath())) {
url = url.setPath(interfaceName); // 设置接口完全限定名为URL Path
}
if (UrlUtils.isRegistry(url)) { // 检测URL协议是否为registry若是说明用户想使用指定的注册中心
// 这里会将map中的参数整理成一个参数添加到refer参数中
urls.add(url.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));
} else {
// 将map中的参数添加到url中
urls.add(ClusterUtils.mergeUrl(url, map));
}
}
}
} else {
if (!LOCAL_PROTOCOL.equalsIgnoreCase(getProtocol())) {
checkRegistry();
// 加载注册中心的地址RegistryURL
List<URL> us = ConfigValidationUtils.loadRegistries(this, false);
if (CollectionUtils.isNotEmpty(us)) {
for (URL u : us) {
URL monitorUrl = ConfigValidationUtils.loadMonitor(this, u);
if (monitorUrl != null) {
map.put(MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
}
// 将map中的参数整理成refer参数添加到RegistryURL中
urls.add(u.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));
}
}
if (urls.isEmpty()) { // 既不是服务直连,也没有配置注册中心,抛出异常
throw new IllegalStateException("...");
}
}
}
if (urls.size() == 1) {
// 在单注册中心或是直连单个服务提供方的时候通过Protocol的适配器选择对应的Protocol实现创建Invoker对象
invoker = REF_PROTOCOL.refer(interfaceClass, urls.get(0));
} else {
// 多注册中心或是直连多个服务提供方的时候会根据每个URL创建Invoker对象
List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
URL registryURL = null;
for (URL url : urls) {
invokers.add(REF_PROTOCOL.refer(interfaceClass, url));
if (UrlUtils.isRegistry(url)) { // 确定是多注册中心还是直连多个Provider
registryURL = url;
}
}
if (registryURL != null) {
// 多注册中心的场景中会使用ZoneAwareCluster作为Cluster默认实现多注册中心之间的选择
URL u = registryURL.addParameterIfAbsent(CLUSTER_KEY, ZoneAwareCluster.NAME);
invoker = CLUSTER.join(new StaticDirectory(u, invokers));
} else {
// 多个Provider直连的场景中使用Cluster适配器选择合适的扩展实现
invoker = CLUSTER.join(new StaticDirectory(invokers));
}
}
}
if (shouldCheck() && !invoker.isAvailable()) {
// 根据check配置决定是否检测Provider的可用性
invoker.destroy();
throw new IllegalStateException("...");
}
...// 元数据处理相关的逻辑
// 通过ProxyFactory适配器选择合适的ProxyFactory扩展实现创建代理对象
return (T) PROXY_FACTORY.getProxy(invoker, ProtocolUtils.isGeneric(generic));
}
RegistryProtocol
在直连 Provider 的场景中,会使用 DubboProtocol.refer() 方法完成服务引用DubboProtocol.refer() 方法的具体实现在前面[第 25 课时]中已经详细介绍过了这里我们重点来看存在注册中心的场景中Dubbo Consumer 是如何通过 RegistryProtocol 完成服务引用的。
在 RegistryProtocol.refer() 方法中,会先根据 URL 获取注册中心的 URL再调用 doRefer 方法生成 Invoker在 refer() 方法中会使用 MergeableCluster 处理多 group 引用的场景。
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
url = getRegistryUrl(url); // 从URL中获取注册中心的URL
// 获取Registry实例这里的RegistryFactory对象是通过Dubbo SPI的自动装载机制注入的
Registry registry = registryFactory.getRegistry(url);
if (RegistryService.class.equals(type)) {
return proxyFactory.getInvoker((T) registry, type, url);
}
// 从注册中心URL的refer参数中获取此次服务引用的一些参数其中就包括group
Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));
String group = qs.get(GROUP_KEY);
if (group != null && group.length() > 0) {
if ((COMMA_SPLIT_PATTERN.split(group)).length > 1 || "*".equals(group)) {
// 如果此次可以引用多个group的服务则Cluser实现使用MergeableCluster实现
// 这里的getMergeableCluster()方法就会通过Dubbo SPI方式找到MergeableCluster实例
return doRefer(getMergeableCluster(), registry, type, url);
}
}
// 如果没有group参数或是只指定了一个group则通过Cluster适配器选择Cluster实现
return doRefer(cluster, registry, type, url);
}
在 doRefer() 方法中,首先会根据 URL 初始化 RegistryDirectory 实例,然后生成 Subscribe URL 并进行注册,之后会通过 Registry 订阅服务,最后通过 Cluster 将多个 Invoker 合并成一个 Invoker 返回给上层,具体实现如下:
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
// 创建RegistryDirectory实例
RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
directory.setRegistry(registry);
directory.setProtocol(protocol);
// 生成SubscribeUrl协议为consumer具体的参数是RegistryURL中refer参数指定的参数
Map<String, String> parameters = new HashMap<String, String>(directory.getConsumerUrl().getParameters());
URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
if (directory.isShouldRegister()) {
directory.setRegisteredConsumerUrl(subscribeUrl); // 在SubscribeUrl中添加category=consumers和check=false参数
registry.register(directory.getRegisteredConsumerUrl()); // 服务注册在Zookeeper的consumers节点下添加该Consumer对应的节点
}
directory.buildRouterChain(subscribeUrl); // 根据SubscribeUrl创建服务路由
// 订阅服务toSubscribeUrl()方法会将SubscribeUrl中category参数修改为"providers,configurators,routers"
// RegistryDirectory的subscribe()在前面详细分析过了其中会通过Registry订阅服务同时还会添加相应的监听器
directory.subscribe(toSubscribeUrl(subscribeUrl));
// 注册中心中可能包含多个Provider相应地也就有多个Invoker
// 这里通过前面选择的Cluster将多个Invoker对象封装成一个Invoker对象
Invoker<T> invoker = cluster.join(directory);
// 根据URL中的registry.protocol.listener参数加载相应的监听器实现
List<RegistryProtocolListener> listeners = findRegistryProtocolListeners(url);
if (CollectionUtils.isEmpty(listeners)) {
return invoker;
}
// 为了方便在监听器中回调这里将此次引用使用到的Directory对象、Cluster对象、Invoker对象以及SubscribeUrl
// 封装到一个RegistryInvokerWrapper中传递给监听器
RegistryInvokerWrapper<T> registryInvokerWrapper = new RegistryInvokerWrapper<>(directory, cluster, invoker, subscribeUrl);
for (RegistryProtocolListener listener : listeners) {
listener.onRefer(this, registryInvokerWrapper);
}
return registryInvokerWrapper;
}
这里涉及的 RegistryDirectory、Router 接口、Cluster 接口及其相关的扩展实现,我们都已经在前面的课时详细分析过了,这里不再重复。
总结
本课时,我们重点介绍了 Dubbo 服务引用的整个流程。
首先,我们介绍了 DubboBootStrap 这个入口门面类与服务引用相关的方法,其中涉及 referServices()、reference() 等核心方法。
接下来,我们分析了 ReferenceConfigCache 这个 ReferenceConfig 对象缓存,以及 ReferenceConfig 实现服务引用的核心流程。
最后,我们还讲解了 RegistryProtocol 从注册中心引用服务的核心实现。

View File

@@ -0,0 +1,119 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
43 服务自省设计方案:新版本新方案
随着微服务架构的不断发展和普及RPC 框架成为微服务架构中不可或缺的重要角色Dubbo 作为 Java 生态中一款成熟的 RPC 框架也在随着技术的更新换代不断发展壮大。当然,传统的 Dubbo 架构也面临着新思想、新生态和新技术带来的挑战。
在微服务架构中,服务是基本单位,而 Dubbo 架构中服务的基本单位是 Java 接口,这种架构上的差别就会带来一系列挑战。从 2.7.5 版本开始Dubbo 引入了服务自省架构,来应对微服务架构带来的挑战。具体都有哪些挑战呢?下面我们就来详细说明一下。
注册中心面临的挑战
在开始介绍注册中心面临的挑战之前,我们先来回顾一下前面课时介绍过的 Dubbo 传统架构以及这个架构中最核心的组件:
Dubbo 核心架构图
结合上面这张架构图,我们可以一起回顾一下这些核心组件的功能。
Registry注册中心。 负责服务地址的注册与查找,服务的 Provider 和 Consumer 只在启动时与注册中心交互。注册中心通过长连接感知 Provider 的存在,在 Provider 出现宕机的时候,注册中心会立即推送相关事件通知 Consumer。
Provider服务提供者。 在它启动的时候,会向 Registry 进行注册操作,将自己服务的地址和相关配置信息封装成 URL 添加到 ZooKeeper 中。
Consumer服务消费者。 在它启动的时候,会向 Registry 进行订阅操作。订阅操作会从 ZooKeeper 中获取 Provider 注册的 URL并在 ZooKeeper 中添加相应的监听器。获取到 Provider URL 之后Consumer 会根据 URL 中相应的参数选择 LoadBalance、Router、Cluster 实现,创建相应的 Invoker 对象,然后封装服务接口的代理对象,返回给上层业务。上层业务调用该代理对象的方法,就会执行远程调用。
Monitor监控中心。 用于统计服务的调用次数和调用时间。Provider 和 Consumer 在运行过程中,会在内存中统计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。监控中心在上面的架构图中并不是必要角色,监控中心宕机不会影响 Provider、Consumer 以及 Registry 的功能,只会丢失监控数据而已。
通过前面对整个 Dubbo 实现体系的介绍我们知道URL 是贯穿整个 Dubbo 注册与发现的核心。Provider URL 注册到 ZooKeeper 上的大致格式如下:
dubbo://192.168.0.100:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=demo-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&group=groupA&interface=org.apache.dubbo.demo.DemoService&metadata-type=remote&methods=sayHello,sayHelloAsync&pid=59975&release=&side=provider&timestamp=1601390276192
其中包括 Provider 的 IP、Port、服务接口的完整名称、Dubbo 协议的版本号、分组信息、进程 ID 等。
我们常用的注册中心比如ZooKeeper、Nacos 或 etcd 等,都是中心化的基础设施。注册中心基本都是以内存作为核心存储,其内存使用量与服务接口的数量以及 Provider 节点的个数是成正比的,一个 Dubbo Provider 节点可以注册多个服务接口。随着业务发展,服务接口的数量会越来越多,为了支撑整个系统的流量增长,部署的 Dubbo Provider 节点和 Dubbo Consumer 节点也会不断增加,这就导致注册中心的内存压力越来越大。
在生产环境中为了避免单点故障在搭建注册中心的时候都会使用高可用方案。这些高可用方案的本质就是底层的一致性协议例如ZooKeeper 使用的是 Zab 协议etcd 使用的是 Raft 协议。当注册数据频繁发生变化的时候,注册中心集群的内部节点用于同步数据的网络开销也会增大。
从注册中心的外部看Dubbo Provider 和 Dubbo Consumer 都可以算作注册中心的客户端,都会与注册中心集群之间维护长连接,这也会造成一部分网络开销和资源消耗。
在使用类似 ZooKeeper 的注册中心实现方案时,注册中心会主动将注册数据的变化推送到客户端。假设一个 Dubbo Consumer 订阅了 N 个服务接口,每个服务接口由 M 个 Provider 节点组成的集群提供服务,在 Provider 节点进行机器迁移的时候,就会涉及 M * N 个 URL 的更新,这些变更事件都会通知到每个 Dubbo Consumer 节点,这就造成了注册中心在处理通知方面的压力。
总之,在超大规模的微服务落地实践中,从内存、网络开销、通知等多个角度看,注册中心以及整个 Dubbo 传统架构都受到了不少的挑战和压力。
Dubbo 的改进方案
Dubbo 从 2.7.0 版本开始增加了简化 URL的特性从 URL 中抽出的数据会被存放至元数据中心。但是这次优化只是缩短了 URL 的长度,从内存使用量以及降低通知频繁度的角度降低了注册中心的压力,并没有减少注册中心 URL 的数量,所以注册中心所承受的压力还是比较明显的。
Dubbo 2.7.5 版本引入了服务自省架构进一步降低了注册中心的压力。在此次优化中Dubbo 修改成应用为粒度的服务注册与发现模型,最大化地减少了 Dubbo 服务元信息注册数量,其核心流程如下图所示:
服务自省架构图
上图展示了引入服务自省之后的 Dubbo 服务注册与发现的核心流程Dubbo 会按照顺序执行这些操作(当其中一个操作失败时,后续操作不会执行)。
我们首先来看 Provider 侧的执行流程:
1.发布所有业务接口中定义的服务接口,具体过程与[第 41 课时]中介绍的发布流程相同;
2.发布 MetadataService 接口,该接口的发布由 Dubbo 框架自主完成;
3.将 Service Instance 注册到注册中心;
4.建立所有的 Service ID 与 Service Name 的映射,并同步到配置中心。
接下来我们再来看Consumer 侧的执行流程:
5.注册当前 Consumer 的 Service InstanceDubbo 允许 Consumer 不进行服务注册,所以这一步操作是可选的;
6.从配置中心获取 Service ID 与 Service Name 的映射关系;
7.根据 Service ID 从注册中心获取 Service Instance 集合;
8.随机选择一个 Service Instance从中获取 MetadataService 的元数据,这里会发起 MetadataService 的调用,获取该 Service Instance 所暴露的业务接口的 URL 列表,从该 URL 列表中可以过滤出当前订阅的 Service 的 URL
9.根据步骤 8 中获取的业务接口 URL 发起远程调用。
至于上图中涉及的一些新概念,为方便你理解,这里我们对它们的具体实现进行一个简单的介绍。
Service Name服务名称例如在一个电商系统中有用户服务、商品服务、库存服务等。
Service Instance服务实例表示单个 Dubbo 应用进程,多个 Service Instance 构成一个服务集群,拥有相同的 Service Name。
Service ID唯一标识一个 Dubbo 服务,由 ${protocol}:${interface}:${version}:${group} 四部分构成。
在有的场景中,我们会在线上部署两组不同配置的服务节点,来验证某些配置是否生效。例如,共有 100 个服务节点,平均分成 A、B 两组A 组服务节点超时时间(即 timeout设置为 3000 msB 组的超时时间(即 timeout设置为 2000 ms这样的话该服务就有了两组不同的元数据。
按照前面介绍的优化方案,在订阅服务的时候,会得到 100 个 ServiceInstance因为每个 ServiceInstance 发布的服务元数据都有可能不一样,所以我们需要调用每个 ServiceInstance 的 MetadataService 服务获取元数据。
为了减少 MetadataService 服务的调用次数Dubbo 提出了服务修订版本的优化方案,其核心思想是:将每个 ServiceInstance 发布的服务 URL 计算一个 hash 值(也就是 revision 值),并随 ServiceInstance 一起发布到注册中心;在 Consumer 端进行订阅的时候,对于 revision 值相同的 ServiceInstance不再调用 MetadataService 服务,直接共用一份 URL 即可。下图展示了 Dubbo 服务修订的核心逻辑:
引入 Dubbo 服务修订的 Consumer 端交互图
通过该流程图,我们可以看到 Dubbo Consumer 端实现服务修订的流程如下。
Consumer 端通过服务发现 API 从注册中心获取 Provider 端的 ServiceInstance 列表。
注册中心返回 100 台服务实例,其中 revision 为 1 的 ServiceInstance 编号是 0~49revision 为 2 的 ServiceInstance 编号是 50~99。
Consumer 端在这 100 台服务实例中随机选择一台,例如,选择到编号为 68 的 ServiceInstance。
Consumer 端调用 ServiceInstance 68 暴露的 MetadataService 服务,获得其发布的 Dubbo 服务 URL 列表,并在本地内存中建立 revision 为 2 的服务 URL 列表缓存。
Consumer 端再从剩余的 99 台服务实例中随机选择一台,例如,选中了 ServiceInstance 30发现其 revision 值为 1且本地缓存中没有 revision 为 1 的服务 URL 列表缓存。此时Consumer 会如步骤 4 一样发起 MetadataService 调用,从 ServiceInstance 30 获取服务 URL 列表,并更新缓存。
由于此时的本地缓存已经覆盖了当前场景中全部的 revision 值,后续再次随机选择的 ServiceInstance 的 revision 不是 1 就是 2都会落到本地缓存中不会再次发起 MetadataService 服务调用。后续其他 ServiceInstance 的处理都会复用本地缓存的这两个 URL 列表,并根据 ServiceInstance 替换相应的参数例如host、port 等),这样即可得到 ServiceInstance 发布的完整的服务 URL 列表。
一般情况下revision 的数量不会很多,那么 Consumer 端发起的 MetadataService 服务调用次数也是有限的,不会随着 ServiceInstance 的扩容而增长。这样就避免了同一服务的不同版本导致的元数据膨胀。
总结
在本课时,我们重点介绍了 Dubbo 的服务自省架构的相关内容。
首先,我们一起复习了 Dubbo 的传统架构以及传统架构中基础组建的核心功能和交互流程。然后分析了 Dubbo 传统架构在超大规模微服务落地实践中面临的各项挑战和压力。最后,我们重点讲解了 Dubbo 2.7.5 版本之后引入的服务自省方案,服务自省方案可以很好地应对 Dubbo 面临的诸多挑战,并缓解基于 Dubbo 实现的、超大规模的微服务系统压力。在此基础上,我们还特别介绍了 Dubbo 服务修订方案是如何避免元数据膨胀的具体原理。

View File

@@ -0,0 +1,337 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
45 加餐:深入服务自省方案中的服务发布订阅(上)
在前面[第 43 课时]中介绍 Dubbo 的服务自省方案时,我们可以看到除了需要元数据方案的支持之外,还需要服务发布订阅功能的支持,这样才能构成完整的服务自省架构。
本课时我们就来讲解一下 Dubbo 中服务实例的发布与订阅功能的具体实现:首先说明 ServiceDiscovery 接口的核心定义,然后再重点介绍以 ZooKeeper 为注册中心的 ZookeeperServiceDiscovery 实现,这其中还会涉及相关事件监听的实现。
ServiceDiscovery 接口
ServiceDiscovery 主要封装了针对 ServiceInstance 的发布和订阅操作,你可以暂时将其理解成一个 ServiceInstance 的注册中心。ServiceDiscovery 接口的定义如下所示:
@SPI("zookeeper")
public interface ServiceDiscovery extends Prioritized {
// 初始化当前ServiceDiscovery实例传入的是注册中心的URL
void initialize(URL registryURL) throws Exception;
// 销毁当前ServiceDiscovery实例
void destroy() throws Exception;
// 发布传入的ServiceInstance实例
void register(ServiceInstance serviceInstance) throws RuntimeException;
// 更新传入的ServiceInstance实例
void update(ServiceInstance serviceInstance) throws RuntimeException;
// 注销传入的ServiceInstance实例
void unregister(ServiceInstance serviceInstance) throws RuntimeException;
// 查询全部Service Name
Set<String> getServices();
// 分页查询时默认每页的条数
default int getDefaultPageSize() {
return 100;
}
// 根据ServiceName分页查询ServiceInstance
default List<ServiceInstance> getInstances(String serviceName) throws NullPointerException {
List<ServiceInstance> allInstances = new LinkedList<>();
int offset = 0;
int pageSize = getDefaultPageSize();
// 分页查询ServiceInstance
Page<ServiceInstance> page = getInstances(serviceName, offset, pageSize);
allInstances.addAll(page.getData());
while (page.hasNext()) {
offset += page.getDataSize();
page = getInstances(serviceName, offset, pageSize);
allInstances.addAll(page.getData());
}
return unmodifiableList(allInstances);
}
default Page<ServiceInstance> getInstances(String serviceName, int offset, int pageSize) throws NullPointerException,
IllegalArgumentException {
return getInstances(serviceName, offset, pageSize, false);
}
default Page<ServiceInstance> getInstances(String serviceName, int offset, int pageSize, boolean healthyOnly) throws
NullPointerException, IllegalArgumentException, UnsupportedOperationException {
throw new UnsupportedOperationException("Current implementation does not support pagination query method.");
}
default Map<String, Page<ServiceInstance>> getInstances(Iterable<String> serviceNames, int offset, int requestSize) throws
NullPointerException, IllegalArgumentException {
Map<String, Page<ServiceInstance>> instances = new LinkedHashMap<>();
for (String serviceName : serviceNames) {
instances.put(serviceName, getInstances(serviceName, offset, requestSize));
}
return unmodifiableMap(instances);
}
// 添加ServiceInstance监听器
default void addServiceInstancesChangedListener(ServiceInstancesChangedListener listener)
throws NullPointerException, IllegalArgumentException {
}
// 触发ServiceInstancesChangedEvent事件
default void dispatchServiceInstancesChangedEvent(String serviceName) {
dispatchServiceInstancesChangedEvent(serviceName, getInstances(serviceName));
}
default void dispatchServiceInstancesChangedEvent(String serviceName, String... otherServiceNames) {
dispatchServiceInstancesChangedEvent(serviceName, getInstances(serviceName));
if (otherServiceNames != null) {
Stream.of(otherServiceNames)
.filter(StringUtils::isNotEmpty)
.forEach(this::dispatchServiceInstancesChangedEvent);
}
}
default void dispatchServiceInstancesChangedEvent(String serviceName, Collection<ServiceInstance> serviceInstances) {
dispatchServiceInstancesChangedEvent(new ServiceInstancesChangedEvent(serviceName, serviceInstances));
}
default void dispatchServiceInstancesChangedEvent(ServiceInstancesChangedEvent event) {
getDefaultExtension().dispatch(event);
}
}
ServiceDiscovery 接口被 @SPI 注解修饰,是一个扩展点,针对不同的注册中心,有不同的 ServiceDiscovery 实现,如下图所示:
ServiceDiscovery 继承关系图
在 Dubbo 创建 ServiceDiscovery 对象的时候,会通过 ServiceDiscoveryFactory 工厂类进行创建。ServiceDiscoveryFactory 接口也是一个扩展接口Dubbo 只提供了一个默认实现—— DefaultServiceDiscoveryFactory其继承关系如下图所示
ServiceDiscoveryFactory 继承关系图
在 AbstractServiceDiscoveryFactory 中维护了一个 ConcurrentMap 类型的集合discoveries 字段)来缓存 ServiceDiscovery 对象,并提供了一个 createDiscovery() 抽象方法来创建 ServiceDiscovery 实例。
public ServiceDiscovery getServiceDiscovery(URL registryURL) {
String key = registryURL.toServiceStringWithoutResolving();
return discoveries.computeIfAbsent(key, k -> createDiscovery(registryURL));
}
在 DefaultServiceDiscoveryFactory 中会实现 createDiscovery() 方法,使用 Dubbo SPI 机制获取对应的 ServiceDiscovery 对象,具体实现如下:
protected ServiceDiscovery createDiscovery(URL registryURL) {
String protocol = registryURL.getProtocol();
ExtensionLoader<ServiceDiscovery> loader = getExtensionLoader(ServiceDiscovery.class);
return loader.getExtension(protocol);
}
ZookeeperServiceDiscovery 实现分析
Dubbo 提供了多个 ServiceDiscovery 用来接入多种注册中心,下面我们以 ZookeeperServiceDiscovery 为例介绍 Dubbo 是如何接入 ZooKeeper 作为注册中心,实现服务实例发布和订阅的。
在 ZookeeperServiceDiscovery 中封装了一个 Apache Curator 中的 ServiceDiscovery 对象来实现与 ZooKeeper 的交互。在 initialize() 方法中会初始化 CuratorFramework 以及 Curator ServiceDiscovery 对象,如下所示:
public void initialize(URL registryURL) throws Exception {
... // 省略初始化EventDispatcher的相关逻辑
// 初始化CuratorFramework
this.curatorFramework = buildCuratorFramework(registryURL);
// 确定rootPath默认是"/services"
this.rootPath = ROOT_PATH.getParameterValue(registryURL);
// 初始化Curator ServiceDiscovery并启动
this.serviceDiscovery = buildServiceDiscovery(curatorFramework, rootPath);
this.serviceDiscovery.start();
}
在 ZookeeperServiceDiscovery 中的方法基本都是调用 Curator ServiceDiscovery 对象的相应方法实现例如register()、update() 、unregister() 方法都会调用 Curator ServiceDiscovery 对象的相应方法完成 ServiceInstance 的添加、更新和删除。这里我们以 register() 方法为例:
public void register(ServiceInstance serviceInstance) throws RuntimeException {
doInServiceRegistry(serviceDiscovery -> {
serviceDiscovery.registerService(build(serviceInstance));
});
}
// 在build()方法中会将Dubbo中的ServiceInstance对象转换成Curator中的ServiceInstance对象
public static org.apache.curator.x.discovery.ServiceInstance<ZookeeperInstance> build(ServiceInstance serviceInstance) {
ServiceInstanceBuilder builder = null;
// 获取Service Name
String serviceName = serviceInstance.getServiceName();
String host = serviceInstance.getHost();
int port = serviceInstance.getPort();
// 获取元数据
Map<String, String> metadata = serviceInstance.getMetadata();
// 生成的id格式是"host:ip"
String id = generateId(host, port);
// ZookeeperInstance是Curator ServiceInstance的payload
ZookeeperInstance zookeeperInstance = new ZookeeperInstance(null, serviceName, metadata);
builder = builder().id(id).name(serviceName).address(host).port(port)
.payload(zookeeperInstance);
return builder.build();
}
除了上述服务实例发布的功能之外,在服务实例订阅的时候,还会用到 ZookeeperServiceDiscovery 查询服务实例的信息,这些方法都是直接依赖 Apache Curator 实现的例如getServices() 方法会调用 Curator ServiceDiscovery 的 queryForNames() 方法查询 Service NamegetInstances() 方法会通过 Curator ServiceDiscovery 的 queryForInstances() 方法查询 Service Instance。
EventListener 接口
ZookeeperServiceDiscovery 除了实现了 ServiceDiscovery 接口之外,还实现了 EventListener 接口,如下图所示:
ZookeeperServiceDiscovery 继承关系图
也就是说ZookeeperServiceDiscovery 本身也是 EventListener 实现,可以作为 EventListener 监听某些事件。下面我们先来看 Dubbo 中 EventListener 接口的定义其中关注三个方法onEvent() 方法、getPriority() 方法和 findEventType() 工具方法。
@SPI
@FunctionalInterface
public interface EventListener<E extends Event> extends java.util.EventListener, Prioritized {
// 当发生该EventListener对象关注的事件时该EventListener的onEvent()方法会被调用
void onEvent(E event);
// 当前EventListener对象被调用的优先级
default int getPriority() {
return MIN_PRIORITY;
}
// 获取传入的EventListener对象监听何种Event事件
static Class<? extends Event> findEventType(EventListener<?> listener) {
return findEventType(listener.getClass());
}
static Class<? extends Event> findEventType(Class<?> listenerClass) {
Class<? extends Event> eventType = null;
// 检测传入listenerClass是否为Dubbo的EventListener接口实现
if (listenerClass != null && EventListener.class.isAssignableFrom(listenerClass)) {
eventType = findParameterizedTypes(listenerClass)
.stream()
.map(EventListener::findEventType) // 获取listenerClass中定义的Event泛型
.filter(Objects::nonNull)
.findAny()
// 获取listenerClass父类中定义的Event泛型
.orElse((Class) findEventType(listenerClass.getSuperclass()));
}
return eventType;
}
... // findEventType()方法用来过滤传入的parameterizedType是否为Event或Event子类(这里省略该方法的实现)
}
Dubbo 中有很多 EventListener 接口的实现,如下图所示:
EventListener 继承关系图
我们先来重点关注 ZookeeperServiceDiscovery 这个实现,在其 onEvent() 方法(以及 addServiceInstancesChangedListener() 方法)中会调用 registerServiceWatcher() 方法重新注册:
public void onEvent(ServiceInstancesChangedEvent event) {
// 发生ServiceInstancesChangedEvent事件的Service Name
String serviceName = event.getServiceName();
// 重新注册监听器
registerServiceWatcher(serviceName);
}
protected void registerServiceWatcher(String serviceName) {
// 构造要监听的path
String path = buildServicePath(serviceName);
// 创建监听器ZookeeperServiceDiscoveryChangeWatcher并记录到watcherCaches缓存中
CuratorWatcher watcher = watcherCaches.computeIfAbsent(path, key ->
new ZookeeperServiceDiscoveryChangeWatcher(this, serviceName));
// 在path上添加上面构造的ZookeeperServiceDiscoveryChangeWatcher监听器
// 来监听子节点的变化
curatorFramework.getChildren().usingWatcher(watcher).forPath(path);
}
ZookeeperServiceDiscoveryChangeWatcher 是 ZookeeperServiceDiscovery 配套的 CuratorWatcher 实现,其中 process() 方法实现会关注 NodeChildrenChanged 事件和 NodeDataChanged 事件,并调用关联的 ZookeeperServiceDiscovery 对象的 dispatchServiceInstancesChangedEvent() 方法,具体实现如下:
public void process(WatchedEvent event) throws Exception {
// 获取监听到的事件类型
Watcher.Event.EventType eventType = event.getType();
// 这里只关注NodeChildrenChanged和NodeDataChanged两种事件类型
if (NodeChildrenChanged.equals(eventType) || NodeDataChanged.equals(eventType)) {
// 调用dispatchServiceInstancesChangedEvent()方法分发ServiceInstancesChangedEvent事件
zookeeperServiceDiscovery.dispatchServiceInstancesChangedEvent(serviceName);
}
}
通过上面的分析我们可以知道ZookeeperServiceDiscoveryChangeWatcher 的核心就是将 ZooKeeper 中的事件转换成了 Dubbo 内部的 ServiceInstancesChangedEvent 事件。
EventDispatcher 接口
通过上面对 ZookeeperServiceDiscovery 实现的分析我们知道,它并没有对 dispatchServiceInstancesChangedEvent() 方法进行覆盖,那么在 ZookeeperServiceDiscoveryChangeWatcher 中调用的 dispatchServiceInstancesChangedEvent() 方法就是 ServiceDiscovery 接口中的默认实现。在该默认实现中,会通过 Dubbo SPI 获取 EventDispatcher 的默认实现,并分发 ServiceInstancesChangedEvent 事件,具体实现如下:
default void dispatchServiceInstancesChangedEvent(ServiceInstancesChangedEvent event) {
EventDispatcher.getDefaultExtension().dispatch(event);
}
下面我们来看 EventDispatcher 接口的具体定义:
@SPI("direct")
public interface EventDispatcher extends Listenable<EventListener<?>> {
// 该线程池用于串行调用被触发的EventListener也就是direct模式
Executor DIRECT_EXECUTOR = Runnable::run;
// 将被触发的事件分发给相应的EventListener对象
void dispatch(Event event);
// 获取direct模式中使用的线程池
default Executor getExecutor() {
return DIRECT_EXECUTOR;
}
// 工具方法用于获取EventDispatcher接口的默认实现
static EventDispatcher getDefaultExtension() {
return ExtensionLoader.getExtensionLoader(EventDispatcher.class).getDefaultExtension();
}
}
EventDispatcher 接口被 @SPI 注解修饰是一个扩展点Dubbo 提供了两个具体实现——ParallelEventDispatcher 和 DirectEventDispatcher如下图所示
EventDispatcher 继承关系图
在 AbstractEventDispatcher 中维护了两个核心字段。
listenersCacheConcurrentMap, List> 类型):用于记录监听各类型事件的 EventListener 集合。在 AbstractEventDispatcher 初始化时,会加载全部 EventListener 实现并调用 addEventListener() 方法添加到 listenersCache 集合中。
executorExecutor 类型):该线程池在 AbstractEventDispatcher 的构造函数中初始化。在 AbstractEventDispatcher 收到相应事件时,由该线程池来触发对应的 EventListener 集合。
AbstractEventDispatcher 中的 addEventListener()、removeEventListener()、getAllEventListeners() 方法都是通过操作 listenersCache 集合实现的,具体实现比较简单,这里就不再展示,你若感兴趣的话可以参考源码进行学习。
AbstractEventDispatcher 中另一个要关注的方法是 dispatch() 方法,该方法会从 listenersCache 集合中过滤出符合条件的 EventListener 对象,并按照串行或是并行模式进行通知,具体实现如下:
public void dispatch(Event event) {
// 获取通知EventListener的线程池默认为串行模式也就是direct实现
Executor executor = getExecutor();
executor.execute(() -> {
sortedListeners(entry -> entry.getKey().isAssignableFrom(event.getClass()))
.forEach(listener -> {
if (listener instanceof ConditionalEventListener) { // 针对ConditionalEventListener的特殊处理
ConditionalEventListener predicateEventListener = (ConditionalEventListener) listener;
if (!predicateEventListener.accept(event)) {
return;
}
}
// 通知EventListener
listener.onEvent(event);
});
});
}
// 这里的sortedListeners方法会对listenerCache进行过滤和排序
protected Stream<EventListener> sortedListeners(Predicate<Map.Entry<Class<? extends Event>, List<EventListener>>> predicate) {
return listenersCache
.entrySet()
.stream()
.filter(predicate)
.map(Map.Entry::getValue)
.flatMap(Collection::stream)
.sorted();
}
AbstractEventDispatcher 已经实现了 EventDispatcher 分发 Event 事件、通知 EventListener 的核心逻辑,然后在 ParallelEventDispatcher 和 DirectEventDispatcher 确定是并行通知模式还是串行通知模式即可。
在 ParallelEventDispatcher 中通知 EventListener 的线程池是 ForkJoinPool也就是并行模式在 DirectEventDispatcher 中使用的是 EventDispatcher.DIRECT_EXECUTOR 线程池,也就是串行模式。这两个 EventDispatcher 的具体实现比较简单,这里就不再展示。
我们回到 ZookeeperServiceDiscovery在其构造方法中会获取默认的 EventDispatcher 实现对象,并调用 addEventListener() 方法将 ZookeeperServiceDiscovery 对象添加到 listenersCache 集合中监听 ServiceInstancesChangedEvent 事件。ZookeeperServiceDiscovery 直接继承了 ServiceDiscovery 接口中 dispatchServiceInstancesChangedEvent() 方法的默认实现,并没有进行覆盖,在该方法中,会获取默认的 EventDispatcher 实现并调用 dispatch() 方法分发 ServiceInstancesChangedEvent 事件。
总结
在本课时,我们重点介绍了 Dubbo 服务自省方案中服务实例发布和订阅的基础。
首先,我们说明了 ServiceDiscovery 接口的核心定义,其中定义了服务实例发布和订阅的核心方法。接下来我们分析了以 ZooKeeper 作为注册中心的 ZookeeperServiceDiscovery 实现,其中还讲解了在 ZookeeperServiceDiscovery 上添加监听器的相关实现以及 ZookeeperServiceDiscovery 处理 ServiceInstancesChangedEvent 事件的机制。
下一课时,我们将继续介绍 Dubbo 服务自省方案中的服务实例发布以及订阅实现,记得按时来听课。

View File

@@ -0,0 +1,621 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
46 加餐:深入服务自省方案中的服务发布订阅(下)
在课程第二部分13~15 课时)中介绍 Dubbo 传统框架中的注册中心部分实现时,我们提到了 Registry、RegistryFactory 等与注册中心交互的接口。为了将 ServiceDiscovery 接口的功能与 Registry 融合Dubbo 提供了一个 ServiceDiscoveryRegistry 实现,继承关系如下所示:
ServiceDiscoveryRegistry 、ServiceDiscoveryRegistryFactory 继承关系图
由图我们可以看到ServiceDiscoveryRegistryFactory扩展名称是 service-discovery-registry是 ServiceDiscoveryRegistry 对应的工厂类,继承了 AbstractRegistryFactory 提供的公共能力。
ServiceDiscoveryRegistry 是一个面向服务实例ServiceInstance的注册中心实现其底层依赖前面两个课时介绍的 ServiceDiscovery、WritableMetadataService 等组件。
ServiceDiscoveryRegistry 中的核心字段有如下几个。
serviceDiscoveryServiceDiscovery 类型):用于 ServiceInstance 的发布和订阅。
subscribedServicesSet 类型):记录了当前订阅的服务名称。
serviceNameMappingServiceNameMapping 类型):用于 Service ID 与 Service Name 之间的转换。
writableMetadataServiceWritableMetadataService 类型):用于发布和查询元数据。
registeredListenersSet 类型):记录了注册的 ServiceInstancesChangedListener 的唯一标识。
subscribedURLsSynthesizersList 类型):将 ServiceInstance 的信息与元数据进行合并,得到订阅服务的完整 URL。
在 ServiceDiscoveryRegistry 的构造方法中,会初始化上述字段:
public ServiceDiscoveryRegistry(URL registryURL) {
// 初始化父类其中包括FailbackRegistry中的时间轮和重试定时任务以及AbstractRegistry中的本地文件缓存等
super(registryURL);
// 初始化ServiceDiscovery对象
this.serviceDiscovery = createServiceDiscovery(registryURL);
// 从registryURL中解析出subscribed-services参数并按照逗号切分得到subscribedServices集合
this.subscribedServices = parseServices(registryURL.getParameter(SUBSCRIBED_SERVICE_NAMES_KEY));
// 获取DefaultServiceNameMapping对象
this.serviceNameMapping = ServiceNameMapping.getDefaultExtension();
// 初始化WritableMetadataService对象
String metadataStorageType = getMetadataStorageType(registryURL);
this.writableMetadataService = WritableMetadataService.getExtension(metadataStorageType);
// 获取目前支持的全部SubscribedURLsSynthesizer实现并初始化
this.subscribedURLsSynthesizers = initSubscribedURLsSynthesizers();
}
在 createServiceDiscovery() 方法中,不仅会加载 ServiceDiscovery 的相应实现,还会在外层添加 EventPublishingServiceDiscovery 装饰器,在 register()、initialize() 等方法前后触发相应的事件,具体实现如下:
protected ServiceDiscovery createServiceDiscovery(URL registryURL) {
// 根据registryURL获取对应的ServiceDiscovery实现
ServiceDiscovery originalServiceDiscovery = getServiceDiscovery(registryURL);
// ServiceDiscovery外层添加一层EventPublishingServiceDiscovery修饰器
// EventPublishingServiceDiscovery会在register()、initialize()等方法前后触发相应的事件,
// 例如在register()方法的前后分别会触发ServiceInstancePreRegisteredEvent和ServiceInstanceRegisteredEvent
ServiceDiscovery serviceDiscovery = enhanceEventPublishing(originalServiceDiscovery);
execute(() -> { // 初始化ServiceDiscovery
serviceDiscovery.initialize(registryURL.addParameter(INTERFACE_KEY, ServiceDiscovery.class.getName())
.removeParameter(REGISTRY_TYPE_KEY));
});
return serviceDiscovery;
}
Registry 接口的核心是服务发布和订阅ServiceDiscoveryRegistry 既然实现了 Registry 接口,必然也要实现了服务注册和发布的功能。
服务注册
在 ServiceDiscoveryRegistry 的 register() 中,首先会检测待发布 URL 中的 side 参数,然后调用父类的 register() 方法。我们知道 FailbackRegistry.register() 方法会回调子类的 doRegister() 方法,而 ServiceDiscoveryRegistry.doRegister() 方法直接依赖 WritableMetadataService 的 exportURL() 方法,完成元数据的发布。
public final void register(URL url) {
if (!shouldRegister(url)) { // 检测URL中的side参数是否为provider
return;
}
super.register(url);
}
@Override
public void doRegister(URL url) {
// 将元数据发布到MetadataService
if (writableMetadataService.exportURL(url)) {
... // 输出INFO日志
} else {
... // 输出WARN日志
}
}
ServiceDiscoveryRegistry.unregister() 方法的实现逻辑也是类似的,这里就不再展示,你若感兴趣的话可以参考源码进行学习。
服务订阅
接下来看 ServiceDiscoveryRegistry.subscribe() 方法的实现,其中也是首先会检测待发布 URL 中的 side 参数,然后调用父类的 subscribe() 方法。我们知道 FailbackRegistry.subscribe() 方法会回调子类的 doSubscribe() 方法。在 ServiceDiscoveryRegistry 的 doSubscribe() 方法中,会执行如下完整的订阅流程:
调用 WriteMetadataService.subscribeURL() 方法在 subscribedServiceURLs 集合中记录当前订阅的 URL
通过订阅的 URL 获取 Service Name
根据 Service Name 获取 ServiceInstance 集合;
根据 ServiceInstance 调用相应的 MetadataService 服务,获取元数据,其中涉及历史数据的清理和缓存更新等操作;
将 ServiceInstance 信息以及对应的元数据信息进行合并,得到完整的 URL
触发 NotifyListener 监听器;
添加 ServiceInstancesChangedListener 监听器。
下面来看 ServiceDiscoveryRegistry.doSubscribe() 方法的具体实现:
protected void subscribeURLs(URL url, NotifyListener listener) {
// 记录该订阅的URL
writableMetadataService.subscribeURL(url);
// 获取订阅的Service Name
Set<String> serviceNames = getServices(url);
if (CollectionUtils.isEmpty(serviceNames)) {
throw new IllegalStateException("...");
}
// 执行后续的订阅操作
serviceNames.forEach(serviceName -> subscribeURLs(url, listener, serviceName));
}
我们这就展开一步步来解析上面的这个流程。
1. 获取 Service Name
首先来看 getServices() 方法的具体实现:它会首先根据 subscribeURL 的 provided-by 参数值获取订阅的 Service Name 集合,如果获取失败,则根据 Service ID 获取对应的 Service Name 集合;如果此时依旧获取失败,则尝试从 registryURL 中的 subscribed-services 参数值获取 Service Name 集合。下面来看 getServices() 方法的具体实现:
protected Set<String> getServices(URL subscribedURL) {
Set<String> subscribedServices = new LinkedHashSet<>();
// 首先尝试从subscribeURL中获取provided-by参数值其中封装了全部Service Name
String serviceNames = subscribedURL.getParameter(PROVIDED_BY);
if (StringUtils.isNotEmpty(serviceNames)) {
// 解析provided-by参数值得到全部的Service Name集合
subscribedServices = parseServices(serviceNames);
}
if (isEmpty(subscribedServices)) {
// 如果没有指定provided-by参数则尝试通过subscribedURL构造Service ID
// 然后通过ServiceNameMapping的get()方法查找Service Name
subscribedServices = findMappedServices(subscribedURL);
if (isEmpty(subscribedServices)) {
// 如果subscribedServices依旧为空则返回registryURL中的subscribed-services参数值
subscribedServices = getSubscribedServices();
}
}
return subscribedServices;
}
2. 查找 Service Instance
接下来看 subscribeURLs(url, listener, serviceName) 这个重载的具体实现,其中会根据 Service Name 从 ServiceDiscovery 中查找对应的 ServiceInstance 集合以及注册ServiceInstancesChangedListener 监听。
protected void subscribeURLs(URL url, NotifyListener listener, String serviceName) {
// 根据Service Name获取ServiceInstance对象
List<ServiceInstance> serviceInstances = serviceDiscovery.getInstances(serviceName);
// 调用另一个subscribeURLs()方法重载
subscribeURLs(url, listener, serviceName, serviceInstances);
// 添加ServiceInstancesChangedListener监听器
registerServiceInstancesChangedListener(url, new ServiceInstancesChangedListener(serviceName) {
@Override
public void onEvent(ServiceInstancesChangedEvent event) {
subscribeURLs(url, listener, event.getServiceName(), new ArrayList<>(event.getServiceInstances()));
}
});
}
在 subscribeURLs(url, listener, serviceName, serviceInstances) 这个重载中,主要是根据前面获取的 ServiceInstance 实例集合,构造对应的、完整的 subscribedURL 集合,并触发传入的 NotifyListener 监听器,如下所示:
protected void subscribeURLs(URL subscribedURL, NotifyListener listener, String serviceName,
Collection<ServiceInstance> serviceInstances) {
List<URL> subscribedURLs = new LinkedList<>();
// 尝试通过MetadataService获取subscribedURL集合
subscribedURLs.addAll(getExportedURLs(subscribedURL, serviceInstances));
if (subscribedURLs.isEmpty()) { // 如果上面的尝试失败
// 尝试通过SubscribedURLsSynthesizer获取subscribedURL集合
subscribedURLs.addAll(synthesizeSubscribedURLs(subscribedURL, serviceInstances));
}
// 触发NotifyListener监听器
listener.notify(subscribedURLs);
}
这里构造完整 subscribedURL 可以分为两个分支。
第一个分支:结合传入的 subscribedURL 以及从元数据中获取每个 ServiceInstance 的对应参数,组装成每个 ServiceInstance 对应的完整 subscribeURL。该部分实现在 getExportedURLs() 方法中,也是订阅操作的核心。
第二个分支:当上述操作无法获得完整的 subscribeURL 集合时,会使用 SubscribedURLsSynthesizer基于 subscribedURL 拼凑出每个 ServiceInstance 对应的完整的 subscribedURL。该部分实现在 synthesizeSubscribedURLs() 方法中,目前主要针对 rest 协议。
3. getExportedURLs() 方法核心实现
getExportedURLs() 方法主要围绕 serviceRevisionExportedURLsCache 这个集合展开的,它是一个 Map> 类型的集合,其中第一层 Key 是 Service Name第二层 Key 是 Revision最终的 Value 值是 Service Name 对应的最新的 URL 集合。
1清理过期 URL
在 getExportedURLs() 方法中,首先会调用 expungeStaleRevisionExportedURLs() 方法销毁全部已过期的 URL 信息,具体实现如下:
private void expungeStaleRevisionExportedURLs(List<ServiceInstance> serviceInstances) {
// 从第一个ServiceInstance即可获取Service Name
String serviceName = serviceInstances.get(0).getServiceName();
// 获取该Service Name当前在serviceRevisionExportedURLsCache中对应的URL集合
Map<String, List<URL>> revisionExportedURLsMap = serviceRevisionExportedURLsCache
.computeIfAbsent(serviceName, s -> new LinkedHashMap());
if (revisionExportedURLsMap.isEmpty()) { // 没有缓存任何URL则无须后续清理操作直接返回即可
return;
}
// 获取Service Name在serviceRevisionExportedURLsCache中缓存的修订版本
Set<String> existedRevisions = revisionExportedURLsMap.keySet();
// 从ServiceInstance中获取当前最新的修订版本
Set<String> currentRevisions = serviceInstances.stream()
.map(ServiceInstanceMetadataUtils::getExportedServicesRevision)
.collect(Collectors.toSet());
// 获取要删除的陈旧修订版本staleRevisions = existedRevisions(copy) - currentRevisions
Set<String> staleRevisions = new HashSet<>(existedRevisions);
staleRevisions.removeAll(currentRevisions);
// 从revisionExportedURLsMap中删除staleRevisions集合中所有Key对应的URL集合
staleRevisions.forEach(revisionExportedURLsMap::remove);
}
我们看到这里是通过 ServiceInstanceMetadataUtils 工具类从每个 ServiceInstance 的 metadata 集合中获取最新的修订版本Key 为 dubbo.exported-services.revision那么该修订版本的信息是在哪里写入的呢我们来看一个新接口—— ServiceInstanceCustomizer具体定义如下
@SPI
public interface ServiceInstanceCustomizer extends Prioritized {
void customize(ServiceInstance serviceInstance);
}
关于 ServiceInstanceCustomizer 接口,这里需要关注三个点:①该接口被 @SPI 注解修饰,是一个扩展点;②该接口继承了 Prioritized 接口;③该接口中定义的 customize() 方法可以用来自定义 ServiceInstance 信息,其中就包括控制 metadata 集合中的数据。
也就说ServiceInstanceCustomizer 的多个实现可以按序调用,实现 ServiceInstance 的自定义。下图展示了 ServiceInstanceCustomizer 接口的所有实现类:
ServiceInstanceCustomizer 继承关系图
我们首先来看 ServiceInstanceMetadataCustomizer 这个抽象类,它主要是对 ServiceInstance 中 metadata 这个 KV 集合进行自定义修改,这部分逻辑在 customize() 方法中,如下所示:
public final void customize(ServiceInstance serviceInstance) {
// 获取ServiceInstance对象的metadata字段
Map<String, String> metadata = serviceInstance.getMetadata();
// 生成要添加到metadata集合的KV值
String propertyName = resolveMetadataPropertyName(serviceInstance);
String propertyValue = resolveMetadataPropertyValue(serviceInstance);
// 判断待添加的KV值是否为空
if (!isBlank(propertyName) && !isBlank(propertyValue)) {
String existedValue = metadata.get(propertyName);
boolean put = existedValue == null || isOverride();
if (put) { // 是否覆盖原值
metadata.put(propertyName, propertyValue);
}
}
}
生成 KV 值的 resolveMetadataPropertyName()、resolveMetadataPropertyValue() 方法以及 isOverride() 方法都是抽象方法,在 ServiceInstanceMetadataCustomizer 子类中实现。
在 ExportedServicesRevisionMetadataCustomizer 这个实现中resolveMetadataPropertyName() 方法返回 “dubbo.exported-services.revision” 固定字符串resolveMetadataPropertyValue() 方法会通过 WritableMetadataService 获取当前 ServiceInstance 对象发布的全部 URL然后计算 revision 值。具体实现如下:
protected String resolveMetadataPropertyValue(ServiceInstance serviceInstance) {
// 从ServiceInstance对象的metadata集合中获取当前ServiceInstance存储元数据的方式local还是remote
String metadataStorageType = getMetadataStorageType(serviceInstance);
// 获取相应的WritableMetadataService对象并获取当前ServiceInstance发布的全部元数据
WritableMetadataService writableMetadataService = getExtension(metadataStorageType);
SortedSet<String> exportedURLs = writableMetadataService.getExportedURLs();
// 计算整个exportedURLs集合的revision值
URLRevisionResolver resolver = new URLRevisionResolver();
return resolver.resolve(exportedURLs);
}
这里需要说明下计算 revision 值的核心实现:首先获取每个服务接口的方法签名以及对应 URL 参数集合,然后计算 hashCode 并加和返回,如果通过上述方式没有拿到 revision 值,则返回 “N/A” 占位符字符串。URLRevisionResolver.resolve() 方法的具体实现比较简单,这里就不再展示,你若感兴趣的话可以参考源码进行学习。
在 SubscribedServicesRevisionMetadataCustomizer 这个实现中resolveMetadataPropertyName() 方法返回的是 “dubbo.subscribed-services.revision” 固定字符串resolveMetadataPropertyValue() 方法会通过 WritableMetadataService 获取当前 ServiceInstance 对象引用的全部 URL然后计算 revision 值并返回。具体实现如下:
protected String resolveMetadataPropertyValue(ServiceInstance serviceInstance) {
String metadataStorageType = getMetadataStorageType(serviceInstance);
WritableMetadataService writableMetadataService = getExtension(metadataStorageType);
// 获取subscribedServiceURLs集合
SortedSet<String> subscribedURLs = writableMetadataService.getSubscribedURLs();
URLRevisionResolver resolver = new URLRevisionResolver();
// 计算revision值
return resolver.resolve(subscribedURLs);
}
在 MetadataServiceURLParamsMetadataCustomizer 这个实现中resolveMetadataPropertyName() 方法返回 “dubbo.metadata-service.url-params” 固定字符串resolveMetadataPropertyValue() 方法返回 MetadataService 服务 URL 的参数。
对于 RefreshServiceMetadataCustomizer 这个实现,我们首先关注其执行顺序, 它覆盖了 getPriority() 方法,具体实现如下:
public int getPriority() {
return MIN_PRIORITY; // 执行优先级最低
}
这就保证了 RefreshServiceMetadataCustomizer 在前面介绍的 ServiceInstanceMetadataCustomizer 实现之后执行ServiceInstanceMetadataCustomizer 的优先级为 NORMAL_PRIORITY
customize() 方法的实现中RefreshServiceMetadataCustomizer 会分别获取该 ServiceInstance 发布服务的 URL revision 以及引用服务的 URL revision并更新到元数据中心。具体实现如下
public void customize(ServiceInstance serviceInstance) {
String metadataStoredType = getMetadataStorageType(serviceInstance);
WritableMetadataService writableMetadataService = getExtension(metadataStoredType);
// 从ServiceInstance.metadata集合中获取两个revision并调用refreshMetadata()方法进行更新
writableMetadataService.refreshMetadata(getExportedServicesRevision(serviceInstance),
getSubscribedServicesRevision(serviceInstance));
}
在 WritableMetadataService 接口的实现中,只有 RemoteWritableMetadataService 实现了 refreshMetadata() 方法,其中会判断两个 revision 值是否发生变化,如果发生了变化,则将相应的 URL 集合更新到元数据中心。如下所示:
public boolean refreshMetadata(String exportedRevision, String subscribedRevision) {
boolean result = true;
// 比较当前ServiceInstance的exportedRevision是否发生变化
if (!StringUtils.isEmpty(exportedRevision) && !exportedRevision.equals(this.exportedRevision)) {
// 发生变化的话会更新exportedRevision字段同时将exportedServiceURLs集合中的URL更新到元数据中心
this.exportedRevision = exportedRevision;
boolean executeResult = saveServiceMetadata();
if (!executeResult) {
result = false;
}
}
// 比较当前ServiceInstance的subscribedRevision是否发生变化
if (!StringUtils.isEmpty(subscribedRevision) && !subscribedRevision.equals(this.subscribedRevision)
&& CollectionUtils.isNotEmpty(writableMetadataService.getSubscribedURLs())) {
// 发生变化的话会更新subscribedRevision字段同时将subscribedServiceURLs集合中的URL更新到元数据中心
this.subscribedRevision = subscribedRevision;
SubscriberMetadataIdentifier metadataIdentifier = new SubscriberMetadataIdentifier();
metadataIdentifier.setApplication(serviceName());
metadataIdentifier.setRevision(subscribedRevision);
boolean executeResult = throwableAction(getMetadataReport()::saveSubscribedData, metadataIdentifier,
writableMetadataService.getSubscribedURLs());
if (!executeResult) {
result = false;
}
}
return result;
}
在 EventListener 接口的实现中有一个名为 CustomizableServiceInstanceListener 的实现,它会监听 ServiceInstancePreRegisteredEvent在其 onEvent() 方法中,加载全部 ServiceInstanceCustomizer 实现,并调用全部 customize() 方法完成 ServiceInstance 的自定义。具体实现如下:
public void onEvent(ServiceInstancePreRegisteredEvent event) {
// 加载全部ServiceInstanceCustomizer实现
ExtensionLoader<ServiceInstanceCustomizer> loader =
ExtensionLoader.getExtensionLoader(ServiceInstanceCustomizer.class);
// 按序实现ServiceInstance自定义
loader.getSupportedExtensionInstances().forEach(customizer -> {
customizer.customize(event.getServiceInstance());
});
}
2更新 Revision 缓存
介绍完 ServiceInstanceMetadataCustomizer 的内容之后,下面我们回到 ServiceDiscoveryRegistry 继续分析。
在清理完过期的修订版本 URL 之后,接下来会检测所有 ServiceInstance 的 revision 值是否已经存在于 serviceRevisionExportedURLsCache 缓存中,如果某个 ServiceInstance 的 revision 值没有在该缓存中,则会调用该 ServiceInstance 发布的 MetadataService 接口进行查询,这部分逻辑在 initializeRevisionExportedURLs() 方法中实现。具体实现如下:
private List<URL> initializeRevisionExportedURLs(ServiceInstance serviceInstance) {
if (serviceInstance == null) { // 判空
return emptyList();
}
// 获取Service Name
String serviceName = serviceInstance.getServiceName();
// 获取该ServiceInstance.metadata中携带的revision值
String revision = getExportedServicesRevision(serviceInstance);
// 从serviceRevisionExportedURLsCache集合中获取该revision值对应的URL集合
Map<String, List<URL>> revisionExportedURLsMap = getRevisionExportedURLsMap(serviceName);
List<URL> revisionExportedURLs = revisionExportedURLsMap.get(revision);
if (revisionExportedURLs == null) { // serviceRevisionExportedURLsCache缓存没有命中
// 调用该ServiceInstance对应的MetadataService服务获取其发布的URL集合
revisionExportedURLs = getExportedURLs(serviceInstance);
if (revisionExportedURLs != null) { // 调用MetadataService服务成功之后更新到serviceRevisionExportedURLsCache缓存中
revisionExportedURLsMap.put(revision, revisionExportedURLs);
}
} else { // 命中serviceRevisionExportedURLsCache缓存
... // 打印日志
}
return revisionExportedURLs;
}
3请求 MetadataService 服务
这里我们可以看到,请求某个 ServiceInstance 的 MetadataService 接口的实现是在 getExportedURLs() 方法中实现的,与我们前面整个课程介绍的请求普通业务接口的原理类似。具体实现如下:
private List<URL> getExportedURLs(ServiceInstance providerServiceInstance) {
List<URL> exportedURLs = null;
// 获取指定ServiceInstance实例存储元数据的类型
String metadataStorageType = getMetadataStorageType(providerServiceInstance);
try {
// 创建MetadataService接口的本地代理
MetadataService metadataService = MetadataServiceProxyFactory.getExtension(metadataStorageType)
.getProxy(providerServiceInstance);
if (metadataService != null) {
// 通过本地代理请求该ServiceInstance的MetadataService服务
SortedSet<String> urls = metadataService.getExportedURLs();
exportedURLs = toURLs(urls);
}
} catch (Throwable e) {
exportedURLs = null; // 置空exportedURLs
}
return exportedURLs;
}
这里涉及一个新的接口——MetadataServiceProxyFactory它是用来创建 MetadataService 本地代理的工厂类,继承关系如下所示:
MetadataServiceProxyFactory 继承关系图
在 BaseMetadataServiceProxyFactory 中提供了缓存 MetadataService 本地代理的公共功能,其中维护了一个 proxies 集合HashMap 类型Key 是 Service Name 与一个 ServiceInstance 的 revision 值的组合Value 是该 ServiceInstance 对应的 MetadataService 服务的本地代理对象。创建 MetadataService 本地代理的功能是在 createProxy() 抽象方法中实现的,这个方法由 BaseMetadataServiceProxyFactory 的子类具体实现。
下面来看 BaseMetadataServiceProxyFactory 的两个实现——DefaultMetadataServiceProxyFactory 和 RemoteMetadataServiceProxyFactory。
DefaultMetadataServiceProxyFactory 在其 createProxy() 方法中,会先通过 MetadataServiceURLBuilder 获取 MetadataService 接口的 URL然后通过 Protocol 接口引用指定 ServiceInstance 发布的 MetadataService 服务,得到对应的 Invoker 对象,最后通过 ProxyFactory 在 Invoker 对象的基础上创建 MetadataService 本地代理。
protected MetadataService createProxy(ServiceInstance serviceInstance) {
MetadataServiceURLBuilder builder = null;
ExtensionLoader<MetadataServiceURLBuilder> loader
= ExtensionLoader.getExtensionLoader(MetadataServiceURLBuilder.class);
Map<String, String> metadata = serviceInstance.getMetadata();
// 在使用Spring Cloud的时候metadata集合中会包含METADATA_SERVICE_URLS_PROPERTY_NAME整个Key
String dubboURLsJSON = metadata.get(METADATA_SERVICE_URLS_PROPERTY_NAME);
if (StringUtils.isNotEmpty(dubboURLsJSON)) {
builder = loader.getExtension(SpringCloudMetadataServiceURLBuilder.NAME);
} else {
builder = loader.getExtension(StandardMetadataServiceURLBuilder.NAME);
}
// 构造MetadataService服务对应的URL集合
List<URL> urls = builder.build(serviceInstance);
// 引用服务创建Invoker注意即使MetadataService接口使用了多种协议这里也只会使用第一种协议
Invoker<MetadataService> invoker = protocol.refer(MetadataService.class, urls.get(0));
// 创建MetadataService的本地代理对象
return proxyFactory.getProxy(invoker);
}
这里我们来看 MetadataServiceURLBuilder 接口中创建 MetadataService 服务对应的 URL 的逻辑,下图展示了 MetadataServiceURLBuilder 接口的实现:
MetadataServiceURLBuilder 继承关系图
其中SpringCloudMetadataServiceURLBuilder 是兼容 Spring Cloud 的实现,这里就不深入分析了。我们重点来看 StandardMetadataServiceURLBuilder 的实现,其中会根据 ServiceInstance.metadata 携带的 URL 参数、Service Name、ServiceInstance 的 host 等信息构造 MetadataService 服务对应 URL如下所示
public List<URL> build(ServiceInstance serviceInstance) {
// 从metadata集合中获取"dubbo.metadata-service.url-params"这个Key对应的Value值
// 这个Key是在MetadataServiceURLParamsMetadataCustomizer中写入的
Map<String, Map<String, String>> paramsMap = getMetadataServiceURLsParams(serviceInstance);
List<URL> urls = new ArrayList<>(paramsMap.size());
// 获取Service Name
String serviceName = serviceInstance.getServiceName();
// 获取ServiceInstance监听的host
String host = serviceInstance.getHost();
// MetadataService接口可能被发布成多种协议遍历paramsMap集合为每种协议都生成对应的URL
for (Map.Entry<String, Map<String, String>> entry : paramsMap.entrySet()) {
String protocol = entry.getKey();
Map<String, String> params = entry.getValue();
int port = Integer.parseInt(params.get(PORT_KEY));
URLBuilder urlBuilder = new URLBuilder()
.setHost(host)
.setPort(port)
.setProtocol(protocol)
.setPath(MetadataService.class.getName());
params.forEach((name, value) -> urlBuilder.addParameter(name, valueOf(value)));
urlBuilder.addParameter(GROUP_KEY, serviceName);
urls.add(urlBuilder.build());
}
return urls;
}
接下来我们看 RemoteMetadataServiceProxyFactory 这个实现类,其中的 createProxy() 方法会直接创建一个 RemoteMetadataServiceProxy 对象并返回。在前面第 44 课时介绍 MetadataService 接口的时候,我们重点介绍的是 WritableMetadataService 这个子接口下的实现,并没有提及 RemoteMetadataServiceProxy 这个实现。下图是 RemoteMetadataServiceProxy 在继承体系中的位置:
RemoteMetadataServiceProxy 继承关系图
RemoteMetadataServiceProxy 作为 RemoteWritableMetadataService 的本地代理,其 getExportedURLs()、getServiceDefinition() 等方法的实现,完全依赖于 MetadataReport 进行实现。这里以 getExportedURLs() 方法为例:
public SortedSet<String> getExportedURLs(String serviceInterface, String group, String version, String protocol) {
// 通过getMetadataReport()方法获取MetadataReport实现对象并通过其getExportedURLs()方法进行查询查询条件封装成ServiceMetadataIdentifier传入其中包括服务接口、group、version以及revision等一系列信息以ZookeeperMetadataReport实现为例真正有用的信息是revision和protocol
return toSortedStrings(getMetadataReport().getExportedURLs(
new ServiceMetadataIdentifier(serviceInterface, group, version, PROVIDER_SIDE, revision, protocol)));
}
到此为止serviceRevisionExportedURLsCache 缓存中各个修订版本的 URL 已经更新到最新数据。
4生成 SubcribedURL
在拿到最新修订版本的 URL 集合之后,接下来会调用 cloneExportedURLs() 方法,结合模板 URL也就是 subscribedURL以及各个 ServiceInstance 发布出来的元数据,生成要订阅服务的最终 subscribedURL 集合。
private List<URL> cloneExportedURLs(URL subscribedURL, Collection<ServiceInstance> serviceInstances) {
if (isEmpty(serviceInstances)) {
return emptyList();
}
List<URL> clonedExportedURLs = new LinkedList<>();
serviceInstances.forEach(serviceInstance -> {
// 获取该ServiceInstance的host
String host = serviceInstance.getHost();
// 获取该ServiceInstance的模板URL集合getTemplateExportedURLs()方法会根据Service Name以及当前ServiceInstance的revision
// 从serviceRevisionExportedURLsCache缓存中获取对应的URL集合另外还会根据subscribedURL的protocol、group、version等参数进行过滤
getTemplateExportedURLs(subscribedURL, serviceInstance)
.stream()
// 删除timestamp、pid等参数
.map(templateURL -> templateURL.removeParameter(TIMESTAMP_KEY))
.map(templateURL -> templateURL.removeParameter(PID_KEY))
.map(templateURL -> {
// 从ServiceInstance.metadata集合中获取该protocol对应的端口号
String protocol = templateURL.getProtocol();
int port = getProtocolPort(serviceInstance, protocol);
if (Objects.equals(templateURL.getHost(), host)
&& Objects.equals(templateURL.getPort(), port)) { // use templateURL if equals
return templateURL;
}
// 覆盖host、port参数
URLBuilder clonedURLBuilder = from(templateURL)
.setHost(host)
.setPort(port);
return clonedURLBuilder.build();
})
.forEach(clonedExportedURLs::add); // 记录新生成的URL
});
return clonedExportedURLs;
}
在 getProtocolPort() 方法中会从 ServiceInstance.metadata 集合中获取 endpoints 列表Key 为 dubbo.endpoints具体实现如下
public static Integer getProtocolPort(ServiceInstance serviceInstance, String protocol) {
Map<String, String> metadata = serviceInstance.getMetadata();
// 从metadata集合中进行查询
String rawEndpoints = metadata.get("dubbo.endpoints");
if (StringUtils.isNotEmpty(rawEndpoints)) {
// 将JSON格式的数据进行反序列化这里的Endpoint是ServiceDiscoveryRegistry的内部类只有port和protocol两个字段
List<Endpoint> endpoints = JSON.parseArray(rawEndpoints, Endpoint.class);
for (Endpoint endpoint : endpoints) {
// 根据Protocol获取对应的port
if (endpoint.getProtocol().equals(protocol)) {
return endpoint.getPort();
}
}
}
return null;
}
在 ServiceInstance.metadata 集合中设置 Endpoint 集合的 ServiceInstanceCustomizer 接口的另一个实现—— ProtocolPortsMetadataCustomizer主要是为了将不同 Protocol 监听的不同端口通知到 Consumer 端。ProtocolPortsMetadataCustomizer.customize() 方法的具体实现如下:
public void customize(ServiceInstance serviceInstance) {
// 获取WritableMetadataService
String metadataStoredType = getMetadataStorageType(serviceInstance);
WritableMetadataService writableMetadataService = getExtension(metadataStoredType);
Map<String, Integer> protocols = new HashMap<>();
// 先获取将当前ServiceInstance发布的各种Protocol对应的URL
writableMetadataService.getExportedURLs()
.stream().map(URL::valueOf)
// 过滤掉MetadataService接口
.filter(url -> !MetadataService.class.getName().equals(url.getServiceInterface()))
.forEach(url -> {
// 记录Protocol与port之间的映射关系
protocols.put(url.getProtocol(), url.getPort());
});
// 将protocols这个Map中的映射关系转换成Endpoint对象然后再序列化成JSON字符串并设置到该ServiceInstance的metadata集合中
setEndpoints(serviceInstance, protocols);
}
到此为止,整个 getExportedURLs() 方法的核心流程就介绍完了。
4. SubscribedURLsSynthesizer
最后,我们再来看看 synthesizeSubscribedURLs() 方法的相关实现,其中使用到 SubscribedURLsSynthesizer 这个接口,具体定义如下:
@SPI
public interface SubscribedURLsSynthesizer extends Prioritized {
// 是否支持该类型的URL
boolean supports(URL subscribedURL);
// 根据subscribedURL以及ServiceInstance的信息合成完整subscribedURL集合
List<URL> synthesize(URL subscribedURL, Collection<ServiceInstance> serviceInstances);
}
目前 Dubbo 只提供了 rest 协议的实现—— RestProtocolSubscribedURLsSynthesizer其中会根据 subscribedURL 中的服务接口以及 ServiceInstance 的 host、port、Service Name 等合成完整的 URL具体实现如下
public List<URL> synthesize(URL subscribedURL, Collection<ServiceInstance> serviceInstances) {
// 获取Protocol
String protocol = subscribedURL.getParameter(PROTOCOL_KEY);
return serviceInstances.stream().map(serviceInstance -> {
URLBuilder urlBuilder = new URLBuilder()
.setProtocol(protocol)
// 使用ServiceInstance的host、port
.setHost(serviceInstance.getHost())
.setPort(serviceInstance.getPort())
// 设置业务接口
.setPath(subscribedURL.getServiceInterface())
.addParameter(SIDE_KEY, PROVIDER)
// 设置Service Name
.addParameter(APPLICATION_KEY, serviceInstance.getServiceName())
.addParameter(REGISTER_KEY, TRUE.toString());
return urlBuilder.build();
}).collect(Collectors.toList());
}
到这里,关于整个 ServiceDiscoveryRegistry 的内容,我们就介绍完了。
总结
本课时我们重点介绍了 Dubbo 服务自省架构中服务发布、服务订阅功能与传统 Dubbo 架构中Registry 接口的兼容实现,也就是 ServiceDiscoveryRegistry 的核心实现。
首先我们讲解了 ServiceDiscoveryRegistry 对服务注册的核心实现,然后详细介绍了 ServiceDiscoveryRegistry 对服务订阅功能的实现,其中涉及 Service Instance 和 Service Name 的查询、MetadataService 服务调用等操作,最终得到 SubcribedURL。
下一课时,我们将开始介绍 Dubbo 服务自省架构中配置中心的相关内容,记得按时来听课。

View File

@@ -0,0 +1,392 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
47 配置中心设计与实现:集中化配置 and 本地化配置,我都要(上)
从 2.7.0 版本开始Dubbo 正式支持配置中心,在服务自省架构中也依赖配置中心完成 Service ID 与 Service Name 的映射。配置中心在 Dubbo 中主要承担两个职责:
外部化配置;
服务治理,负责服务治理规则的存储与通知。
外部化配置目的之一是实现配置的集中式管理。 目前已经有很多成熟的专业配置管理系统(例如,携程开源的 Apollo、阿里开源的 Nacos 等Dubbo 配置中心的目的不是再“造一次轮子”,而是保证 Dubbo 能与这些成熟的配置管理系统正常工作。
Dubbo 可以同时支持多种配置来源。在 Dubbo 初始化过程中,会从多个来源获取配置,并按照固定的优先级将这些配置整合起来,实现高优先级的配置覆盖低优先级配置的效果。这些配置的汇总结果将会参与形成 URL以及后续的服务发布和服务引用。
Dubbo 目前支持下面四种配置来源,优先级由 1 到 4 逐级降低:
System Properties即 -D 参数;
外部化配置,也就是本课时要介绍的配置中心;
API 接口、注解、XML 配置等编程方式收到的配置,最终得到 ServiceConfig、ReferenceConfig 等对象;
本地 dubbo.properties 配置文件。
Configuration
Configuration 接口是 Dubbo 中所有配置的基础接口,其中定义了根据指定 Key 获取对应配置值的相关方法,如下图所示:
Configuration 接口核心方法
从上图中我们可以看到Configuration 针对不同的 boolean、int、String 返回值都有对应的 get() 方法,同时还提供了带有默认值的 get() 方法。这些 get*() 方法底层首先调用 getInternalProperty() 方法获取配置值,然后调用 convert() 方法将获取到的配置值转换成返回值的类型之后返回。getInternalProperty() 是一个抽象方法,由 Configuration 接口的子类具体实现。
下图展示了 Dubbo 中提供的 Configuration 接口实现包括SystemConfiguration、EnvironmentConfiguration、InmemoryConfiguration、PropertiesConfiguration、CompositeConfiguration、ConfigConfigurationAdapter 和 DynamicConfiguration。下面我们将结合具体代码逐个介绍其实现。
Configuration 继承关系图
SystemConfiguration & EnvironmentConfiguration
SystemConfiguration 是从 Java Properties 配置(也就是 -D 配置参数中获取相应的配置项EnvironmentConfiguration 是从使用环境变量中获取相应的配置。两者的 getInternalProperty() 方法实现如下:
public class SystemConfiguration implements Configuration {
public Object getInternalProperty(String key) {
return System.getProperty(key); // 读取-D配置参数
}
}
public class EnvironmentConfiguration implements Configuration {
public Object getInternalProperty(String key) {
String value = System.getenv(key);
if (StringUtils.isEmpty(value)) {
// 读取环境变量中获取相应的配置
value = System.getenv(StringUtils.toOSStyleKey(key));
}
return value;
}
}
InmemoryConfiguration
InmemoryConfiguration 会在内存中维护一个 Map 集合store 字段),其 getInternalProperty() 方法的实现就是从 store 集合中获取对应配置值:
public class InmemoryConfiguration implements Configuration {
private Map<String, String> store = new LinkedHashMap<>();
@Override
public Object getInternalProperty(String key) {
return store.get(key);
}
// 省略addProperty()等写入store集合的方法
}
PropertiesConfiguration
PropertiesConfiguration 涉及 OrderedPropertiesProvider其接口的定义如下
@SPI
public interface OrderedPropertiesProvider {
// 用于排序
int priority();
// 获取Properties配置
Properties initProperties();
}
在 PropertiesConfiguration 的构造方法中,会加载 OrderedPropertiesProvider 接口的全部扩展实现,并按照 priority() 方法进行排序。然后,加载默认的 dubbo.properties.file 配置文件。最后,用 OrderedPropertiesProvider 中提供的配置覆盖 dubbo.properties.file 文件中的配置。PropertiesConfiguration 的构造方法的具体实现如下:
public PropertiesConfiguration() {
// 获取OrderedPropertiesProvider接口的全部扩展名称
ExtensionLoader<OrderedPropertiesProvider> propertiesProviderExtensionLoader = ExtensionLoader.getExtensionLoader(OrderedPropertiesProvider.class);
Set<String> propertiesProviderNames = propertiesProviderExtensionLoader.getSupportedExtensions();
if (propertiesProviderNames == null || propertiesProviderNames.isEmpty()) {
return;
}
// 加载OrderedPropertiesProvider接口的全部扩展实现
List<OrderedPropertiesProvider> orderedPropertiesProviders = new ArrayList<>();
for (String propertiesProviderName : propertiesProviderNames) {
orderedPropertiesProviders.add(propertiesProviderExtensionLoader.getExtension(propertiesProviderName));
}
// 排序OrderedPropertiesProvider接口的扩展实现
orderedPropertiesProviders.sort((OrderedPropertiesProvider a, OrderedPropertiesProvider b) -> {
return b.priority() - a.priority();
});
// 加载默认的dubbo.properties.file配置文件加载后的结果记录在ConfigUtils.PROPERTIES这个static字段中
Properties properties = ConfigUtils.getProperties();
// 使用OrderedPropertiesProvider扩展实现按序覆盖dubbo.properties.file配置文件中的默认配置
for (OrderedPropertiesProvider orderedPropertiesProvider :
orderedPropertiesProviders) {
properties.putAll(orderedPropertiesProvider.initProperties());
}
// 更新ConfigUtils.PROPERTIES字段
ConfigUtils.setProperties(properties);
}
在 PropertiesConfiguration.getInternalProperty() 方法中,直接从 ConfigUtils.PROPERTIES 这个 Properties 中获取覆盖后的配置信息。
public Object getInternalProperty(String key) {
return ConfigUtils.getProperty(key);
}
CompositeConfiguration
CompositeConfiguration 是一个复合的 Configuration 对象,其核心就是将多个 Configuration 对象组合起来,对外表现为一个 Configuration 对象。
CompositeConfiguration 组合的 Configuration 对象都保存在 configList 字段中LinkedList<Configuration> 集合CompositeConfiguration 提供了 addConfiguration() 方法用于向 configList 集合中添加 Configuration 对象,如下所示:
public void addConfiguration(Configuration configuration) {
if (configList.contains(configuration)) {
return; // 不会重复添加同一个Configuration对象
}
this.configList.add(configuration);
}
在 CompositeConfiguration 中维护了一个 prefix 字段和 id 字段,两者可以作为 Key 的前缀进行查询,在 getProperty() 方法中的相关代码如下:
public Object getProperty(String key, Object defaultValue) {
Object value = null;
if (StringUtils.isNotEmpty(prefix)) { // 检查prefix
if (StringUtils.isNotEmpty(id)) { // 检查id
// prefix和id都作为前缀然后拼接key进行查询
value = getInternalProperty(prefix + id + "." + key);
}
if (value == null) {
// 只把prefix作为前缀拼接key进行查询
value = getInternalProperty(prefix + key);
}
} else {
// 若prefix为空则直接用key进行查询
value = getInternalProperty(key);
}
return value != null ? value : defaultValue;
}
在 getInternalProperty() 方法中,会按序遍历 configList 集合中的全部 Configuration 查询对应的 Key返回第一个成功查询到的 Value 值,如下示例代码:
public Object getInternalProperty(String key) {
Configuration firstMatchingConfiguration = null;
for (Configuration config : configList) { // 遍历所有Configuration对象
try {
if (config.containsKey(key)) { // 得到第一个包含指定Key的Configuration对象
firstMatchingConfiguration = config;
break;
}
} catch (Exception e) {
logger.error("...");
}
}
if (firstMatchingConfiguration != null) { // 通过该Configuration查询Key并返回配置值
return firstMatchingConfiguration.getProperty(key);
} else {
return null;
}
}
ConfigConfigurationAdapter
Dubbo 通过 AbstractConfig 类来抽象实例对应的配置,如下图所示:
AbstractConfig 继承关系图
这些 AbstractConfig 实现基本都对应一个固定的配置,也定义了配置对应的字段以及 getter/setter() 方法。例如RegistryConfig 这个实现类就对应了注册中心的相关配置,其中包含了 address、protocol、port、timeout 等一系列与注册中心相关的字段以及对应的 getter/setter() 方法,来接收用户通过 XML、Annotation 或是 API 方式传入的注册中心配置。
ConfigConfigurationAdapter 是 AbstractConfig 与 Configuration 之间的适配器,它会将 AbstractConfig 对象转换成 Configuration 对象。在 ConfigConfigurationAdapter 的构造方法中会获取 AbstractConfig 对象的全部字段,并转换成一个 Map 集合返回,该 Map 集合将会被 ConfigConfigurationAdapter 的 metaData 字段引用。相关示例代码如下:
public ConfigConfigurationAdapter(AbstractConfig config) {
// 获取该AbstractConfig对象中的全部字段与字段值的映射
Map<String, String> configMetadata = config.getMetaData();
metaData = new HashMap<>(configMetadata.size());
// 根据AbstractConfig配置的prefix和id修改metaData集合中Key的名称
for (Map.Entry<String, String> entry : configMetadata.entrySet()) {
String prefix = config.getPrefix().endsWith(".") ? config.getPrefix() : config.getPrefix() + ".";
String id = StringUtils.isEmpty(config.getId()) ? "" : config.getId() + ".";
metaData.put(prefix + id + entry.getKey(), entry.getValue());
}
}
在 ConfigConfigurationAdapter 的 getInternalProperty() 方法实现中,直接从 metaData 集合中获取配置值即可,如下所示:
public Object getInternalProperty(String key) {
return metaData.get(key);
}
DynamicConfiguration
DynamicConfiguration 是对 Dubbo 中动态配置的抽象,其核心方法有下面三类。
getProperties()/ getConfig() / getProperty() 方法:从配置中心获取指定的配置,在使用时,可以指定一个超时时间。
addListener()/ removeListener() 方法:添加或删除对指定配置的监听器。
publishConfig() 方法:发布一条配置信息。
在上述三类方法中,每个方法都用多个重载,其中,都会包含一个带有 group 参数的重载,也就是说配置中心的配置可以按照 group 进行分组。
与 Dubbo 中很多接口类似DynamicConfiguration 接口本身不被 @SPI 注解修饰(即不是一个扩展接口),而是在 DynamicConfigurationFactory 上添加了 @SPI 注解,使其成为一个扩展接口。
在 DynamicConfiguration 中提供了 getDynamicConfiguration() 静态方法,该方法会从传入的配置中心 URL 参数中,解析出协议类型并获取对应的 DynamicConfigurationFactory 实现,如下所示:
static DynamicConfiguration getDynamicConfiguration(URL connectionURL) {
String protocol = connectionURL.getProtocol();
DynamicConfigurationFactory factory = getDynamicConfigurationFactory(protocol);
return factory.getDynamicConfiguration(connectionURL);
}
DynamicConfigurationFactory 接口的定义如下:
@SPI("nop")
public interface DynamicConfigurationFactory {
DynamicConfiguration getDynamicConfiguration(URL url);
static DynamicConfigurationFactory getDynamicConfigurationFactory(String name) {
// 根据扩展名称获取DynamicConfigurationFactory实现
Class<DynamicConfigurationFactory> factoryClass = DynamicConfigurationFactory.class;
ExtensionLoader<DynamicConfigurationFactory> loader = getExtensionLoader(factoryClass);
return loader.getOrDefaultExtension(name);
}
}
DynamicConfigurationFactory 接口的继承关系以及 DynamicConfiguration 接口对应的继承关系如下:
DynamicConfigurationFactory 继承关系图
DynamicConfiguration 继承关系图
我们先来看 AbstractDynamicConfigurationFactory 的实现,其中会维护一个 dynamicConfigurations 集合Map 类型),在 getDynamicConfiguration() 方法中会填充该集合实现缓存DynamicConfiguration 对象的效果。同时AbstractDynamicConfigurationFactory 提供了一个 createDynamicConfiguration() 方法给子类实现来创建DynamicConfiguration 对象。
以 ZookeeperDynamicConfigurationFactory 实现为例,其 createDynamicConfiguration() 方法创建的就是 ZookeeperDynamicConfiguration 对象:
protected DynamicConfiguration createDynamicConfiguration(URL url) {
// 这里创建ZookeeperDynamicConfiguration使用的ZookeeperTransporter就是前文在Transport层中针对Zookeeper的实现
return new ZookeeperDynamicConfiguration(url, zookeeperTransporter);
}
接下来我们再以 ZookeeperDynamicConfiguration 为例,分析 DynamicConfiguration 接口的具体实现。
首先来看 ZookeeperDynamicConfiguration 的核心字段。
executorExecutor 类型):用于执行监听器的线程池。
rootPathString 类型):以 Zookeeper 作为配置中心时,配置也是以 ZNode 形式存储的rootPath 记录了所有配置节点的根路径。
zkClientZookeeperClient 类型):与 Zookeeper 集群交互的客户端。
initializedLatchCountDownLatch 类型):阻塞等待 ZookeeperDynamicConfiguration 相关的监听器注册完成。
cacheListenerCacheListener 类型):用于监听配置变化的监听器。
urlURL 类型):配置中心对应的 URL 对象。
在 ZookeeperDynamicConfiguration 的构造函数中,会初始化上述核心字段,具体实现如下:
ZookeeperDynamicConfiguration(URL url, ZookeeperTransporter zookeeperTransporter) {
this.url = url;
// 根据URL中的config.namespace参数(默认值为dubbo)确定配置中心ZNode的根路径
rootPath = PATH_SEPARATOR + url.getParameter(CONFIG_NAMESPACE_KEY, DEFAULT_GROUP) + "/config";
// 初始化initializedLatch以及cacheListener
// 在cacheListener注册成功之后会调用cacheListener.countDown()方法
initializedLatch = new CountDownLatch(1);
this.cacheListener = new CacheListener(rootPath, initializedLatch);
// 初始化executor字段用于执行监听器的逻辑
this.executor = Executors.newFixedThreadPool(1, new NamedThreadFactory(this.getClass().getSimpleName(), true));
// 初始化Zookeeper客户端
zkClient = zookeeperTransporter.connect(url);
// 在rootPath上添加cacheListener监听器
zkClient.addDataListener(rootPath, cacheListener, executor);
try {
// 从URL中获取当前线程阻塞等待Zookeeper监听器注册成功的时长上限
long timeout = url.getParameter("init.timeout", 5000);
// 阻塞当前线程,等待监听器注册完成
boolean isCountDown = this.initializedLatch.await(timeout, TimeUnit.MILLISECONDS);
if (!isCountDown) {
throw new IllegalStateException("...");
}
} catch (InterruptedException e) {
logger.warn("...");
}
}
在上述初始化过程中ZookeeperDynamicConfiguration 会创建 CacheListener 监听器。在前面[第 15 课时]中,我们介绍了 dubbo-remoting-zookeeper 对外提供了 StateListener、DataListener 和 ChildListener 三种类型的监听器。这里的 CacheListener 就是 DataListener 监听器的具体实现。
在 CacheListener 中维护了一个 Map 集合keyListeners 字段)用于记录所有添加的 ConfigurationListener 监听器,其中 Key 是配置信息在 Zookeeper 中存储的 pathValue 为该 path 上的监听器集合。当某个配置项发生变化的时候CacheListener 会从 keyListeners 中获取该配置对应的 ConfigurationListener 监听器集合,并逐个进行通知。该逻辑是在 CacheListener 的 dataChanged() 方法中实现的:
public void dataChanged(String path, Object value, EventType eventType) {
if (eventType == null) {
return;
}
if (eventType == EventType.INITIALIZED) {
// 在收到INITIALIZED事件的时候表示CacheListener已经成功注册会释放阻塞在initializedLatch上的主线程
initializedLatch.countDown();
return;
}
if (path == null || (value == null && eventType != EventType.NodeDeleted)) {
return;
}
if (path.split("/").length >= MIN_PATH_DEPTH) { // 对path层数进行过滤
String key = pathToKey(path); // 将path中的"/"替换成"."
ConfigChangeType changeType;
switch (eventType) { // 将Zookeeper中不同的事件转换成不同的ConfigChangedEvent事件
case NodeCreated:
changeType = ConfigChangeType.ADDED;
break;
case NodeDeleted:
changeType = ConfigChangeType.DELETED;
break;
case NodeDataChanged:
changeType = ConfigChangeType.MODIFIED;
break;
default:
return;
}
// 使用ConfigChangedEvent封装触发事件的Key、Value、配置group以及事件类型
ConfigChangedEvent configChangeEvent = new ConfigChangedEvent(key, getGroup(path), (String) value, changeType);
// 从keyListeners集合中获取对应的ConfigurationListener集合然后逐一进行通知
Set<ConfigurationListener> listeners = keyListeners.get(path);
if (CollectionUtils.isNotEmpty(listeners)) {
listeners.forEach(listener -> listener.process(configChangeEvent));
}
}
}
CacheListener 中调用的监听器都是 ConfigurationListener 接口实现,如下图所示,这里涉及[第 33 课时]介绍的 TagRouter、AppRouter 和 ServiceRouter它们主要是监听路由配置的变化还涉及 RegistryDirectory 和 RegistryProtocol 中的四个内部类AbstractConfiguratorListener 的子类),它们主要监听 Provider 和 Consumer 的配置变化。
ConfigurationListener 继承关系图
这些 ConfigurationListener 实现在前面的课程中已经详细介绍过了这里就不再重复。ZookeeperDynamicConfiguration 中还提供了 addListener()、removeListener() 两个方法用来增删 ConfigurationListener 监听器,具体实现比较简单,这里就不再展示,你若感兴趣的话可以参考源码进行学习。
介绍完 ZookeeperDynamicConfiguration 的初始化过程之后,我们再来看 ZookeeperDynamicConfiguration 中读取配置、写入配置的相关操作。相关方法的实现如下:
public Object getInternalProperty(String key) {
// 直接从Zookeeper中读取对应的Key
return zkClient.getContent(key);
}
public boolean publishConfig(String key, String group, String content) {
// getPathKey()方法中会添加rootPath和group两部分信息到Key中
String path = getPathKey(group, key);
// 在Zookeeper中创建对应ZNode节点用来存储配置信息
zkClient.create(path, content, false);
return true;
}
总结
本课时我们重点介绍了 Dubbo 配置中心中的多种配置接口。首先,我们讲解了 Configuration 这个顶层接口的核心方法,然后介绍了 Configuration 接口的相关实现,这些实现可以从环境变量、-D 启动参数、Properties文件以及其他配置文件或注解处读取配置信息。最后我们还着重介绍了 DynamicConfiguration 这个动态配置接口的定义,并分析了以 Zookeeper 为动态配置中心的 ZookeeperDynamicConfiguration 实现。
下一课时,我们将深入介绍 Dubbo 动态配置中心启动的核心流程,记得按时来听课。

View File

@@ -0,0 +1,304 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
48 配置中心设计与实现:集中化配置 and 本地化配置,我都要(下)
在上一课时,我们详细分析了 Configuration 接口以及 DynamicConfiguration 接口的实现,其中 DynamicConfiguration 接口实现是动态配置中心的基础。那 Dubbo 中的动态配置中心是如何启动的呢?我们将在本课时详细介绍。
基础配置类
在 DubboBootstrap 初始化的过程中,会调用 ApplicationModel.initFrameworkExts() 方法初始化所有 FrameworkExt 接口实现,继承关系如下图所示:
FrameworkExt 继承关系图
相关代码片段如下:
public static void initFrameworkExts() {
Set<FrameworkExt> exts = ExtensionLoader.getExtensionLoader(FrameworkExt.class).getSupportedExtensionInstances();
for (FrameworkExt ext : exts) {
ext.initialize();
}
}
ConfigManager 用于管理当前 Dubbo 节点中全部 AbstractConfig 对象,其中就包括 ConfigCenterConfig 这个实现的对象,我们通过 XML、Annotation 或是 API 方式添加的配置中心的相关信息(例如,配置中心的地址、端口、协议等),会转换成 ConfigCenterConfig 对象。
在 Environment 中维护了上一课时介绍的多个 Configuration 对象,具体含义如下。
propertiesConfigurationPropertiesConfiguration 类型):全部 OrderedPropertiesProvider 实现提供的配置以及环境变量或是 -D 参数中指定配置文件的相关配置信息。
systemConfigurationSystemConfiguration 类型):-D 参数配置直接添加的配置信息。
environmentConfigurationEnvironmentConfiguration 类型):环境变量中直接添加的配置信息。
externalConfiguration、appExternalConfigurationInmemoryConfiguration 类型):使用 Spring 框架且将 include-spring-env 配置为 true 时,会自动从 Spring Environment 中读取配置。默认依次读取 key 为 dubbo.properties 和 application.dubbo.properties 到这里两个 InmemoryConfiguration 对象中。
globalConfigurationCompositeConfiguration 类型):用于组合上述各个配置来源。
dynamicConfigurationCompositeDynamicConfiguration 类型):用于组合当前全部的配置中心对应的 DynamicConfiguration。
configCenterFirstboolean 类型):用于标识配置中心的配置是否为最高优先级。
在 Environment 的构造方法中会初始化上述 Configuration 对象,在 initialize() 方法中会将从 Spring Environment 中读取到的配置填充到 externalConfiguration 以及 appExternalConfiguration 中。相关的实现片段如下:
public Environment() {
// 创建上述Configuration对象
this.propertiesConfiguration = new PropertiesConfiguration();
this.systemConfiguration = new SystemConfiguration();
this.environmentConfiguration = new EnvironmentConfiguration();
this.externalConfiguration = new InmemoryConfiguration();
this.appExternalConfiguration = new InmemoryConfiguration();
}
public void initialize() throws IllegalStateException {
// 读取对应配置填充上述Configuration对象
ConfigManager configManager = ApplicationModel.getConfigManager();
Optional<Collection<ConfigCenterConfig>> defaultConfigs = configManager.getDefaultConfigCenter();
defaultConfigs.ifPresent(configs -> {
for (ConfigCenterConfig config : configs) {
this.setExternalConfigMap(config.getExternalConfiguration());
this.setAppExternalConfigMap(config.getAppExternalConfiguration());
}
});
this.externalConfiguration.setProperties(externalConfigurationMap);
this.appExternalConfiguration.setProperties(appExternalConfigurationMap);
}
启动配置中心
完成了 Environment 的初始化之后DubboBootstrap 接下来会调用 startConfigCenter() 方法启动一个或多个配置中心客户端,核心操作有两个:一个是调用 ConfigCenterConfig.refresh() 方法刷新配置中心的相关配置;另一个是通过 prepareEnvironment() 方法根据 ConfigCenterConfig 中的配置创建 DynamicConfiguration 对象。
private void startConfigCenter() {
Collection<ConfigCenterConfig> configCenters = configManager.getConfigCenters();
if (CollectionUtils.isEmpty(configCenters)) { // 未指定配置中心
... ... // 省略该部分逻辑
} else {
for (ConfigCenterConfig configCenterConfig : configCenters) { // 可能配置了多个配置中心
configCenterConfig.refresh(); // 刷新配置
// 检查配置中心的配置是否合法 ConfigValidationUtils.validateConfigCenterConfig(configCenterConfig);
}
}
if (CollectionUtils.isNotEmpty(configCenters)) {
// 创建CompositeDynamicConfiguration对象用于组装多个DynamicConfiguration对象
CompositeDynamicConfiguration compositeDynamicConfiguration = new CompositeDynamicConfiguration();
for (ConfigCenterConfig configCenter : configCenters) {
// 根据ConfigCenterConfig创建相应的DynamicConfig对象并添加到CompositeDynamicConfiguration中
compositeDynamicConfiguration.addConfiguration(prepareEnvironment(configCenter));
}
// 将CompositeDynamicConfiguration记录到Environment中的dynamicConfiguration字段
environment.setDynamicConfiguration(compositeDynamicConfiguration);
}
configManager.refreshAll(); // 刷新所有AbstractConfig配置
}
1. 刷新配置中心的配置
首先来看 ConfigCenterConfig.refresh() 方法,该方法会组合 Environment 对象中全部已初始化的 Configuration然后遍历 ConfigCenterConfig 中全部字段的 setter 方法,并从 Environment 中获取对应字段的最终值。具体实现如下:
public void refresh() {
// 获取Environment对象
Environment env = ApplicationModel.getEnvironment();
// 将当前已初始化的所有Configuration合并返回
CompositeConfiguration compositeConfiguration = env.getPrefixedConfiguration(this);
Method[] methods = getClass().getMethods();
for (Method method : methods) {
if (MethodUtils.isSetter(method)) { // 获取ConfigCenterConfig中各个字段的setter方法
// 根据配置中心的相关配置以及Environment中的各个Configuration获取该字段的最终值
String value = StringUtils.trim(compositeConfiguration.getString(extractPropertyName(getClass(), method)));
// 调用setter方法更新ConfigCenterConfig的相应字段
if (StringUtils.isNotEmpty(value) && ClassUtils.isTypeMatch(method.getParameterTypes()[0], value)) {
method.invoke(this, ClassUtils.convertPrimitive(method.getParameterTypes()[0], value));
}
} else if (isParametersSetter(method)) { // 设置parameters字段与设置其他字段的逻辑基本类似但是实现有所不同
String value = StringUtils.trim(compositeConfiguration.getString(extractPropertyName(getClass(), method)));
if (StringUtils.isNotEmpty(value)) {
// 获取当前已有的parameters字段
Map<String, String> map = invokeGetParameters(getClass(), this);
map = map == null ? new HashMap<>() : map;
// 覆盖parameters集合
map.putAll(convert(StringUtils.parseParameters(value), ""));
// 设置parameters字段
invokeSetParameters(getClass(), this, map);
}
}
}
}
这里我们关注一下 Environment.getPrefixedConfiguration() 方法,该方法会将 Environment 中已有的 Configuration 对象以及当前的 ConfigCenterConfig 按照顺序合并,得到一个 CompositeConfiguration 对象,用于确定配置中心的最终配置信息。具体实现如下:
public synchronized CompositeConfiguration getPrefixedConfiguration(AbstractConfig config) {
// 创建CompositeConfiguration对象这里的prefix和id是根据ConfigCenterConfig确定的
CompositeConfiguration prefixedConfiguration = new CompositeConfiguration(config.getPrefix(), config.getId());
// 将ConfigCenterConfig封装成ConfigConfigurationAdapter
Configuration configuration = new ConfigConfigurationAdapter(config);
if (this.isConfigCenterFirst()) { // 根据配置确定ConfigCenterConfig配置的位置
// The sequence would be: SystemConfiguration -> AppExternalConfiguration -> ExternalConfiguration -> AbstractConfig -> PropertiesConfiguration
// 按序组合已有Configuration对象以及ConfigCenterConfig
prefixedConfiguration.addConfiguration(systemConfiguration);
prefixedConfiguration.addConfiguration(environmentConfiguration);
prefixedConfiguration.addConfiguration(appExternalConfiguration);
prefixedConfiguration.addConfiguration(externalConfiguration);
prefixedConfiguration.addConfiguration(configuration);
prefixedConfiguration.addConfiguration(propertiesConfiguration);
} else {
// 配置优先级如下SystemConfiguration -> AbstractConfig -> AppExternalConfiguration -> ExternalConfiguration -> PropertiesConfiguration
prefixedConfiguration.addConfiguration(systemConfiguration);
prefixedConfiguration.addConfiguration(environmentConfiguration);
prefixedConfiguration.addConfiguration(configuration);
prefixedConfiguration.addConfiguration(appExternalConfiguration);
prefixedConfiguration.addConfiguration(externalConfiguration);
prefixedConfiguration.addConfiguration(propertiesConfiguration);
}
return prefixedConfiguration;
}
2. 创建 DynamicConfiguration 对象
通过 ConfigCenterConfig.refresh() 方法确定了所有配置中心的最终配置之后,接下来就会对每个配置中心执行 prepareEnvironment() 方法,得到对应的 DynamicConfiguration 对象。具体实现如下:
private DynamicConfiguration prepareEnvironment(ConfigCenterConfig configCenter) {
if (configCenter.isValid()) { // 检查ConfigCenterConfig是否合法
if (!configCenter.checkOrUpdateInited()) {
return null; // 检查ConfigCenterConfig是否已初始化这里不能重复初始化
}
// 根据ConfigCenterConfig中的各个字段拼接出配置中心的URL创建对应的DynamicConfiguration对象
DynamicConfiguration dynamicConfiguration = getDynamicConfiguration(configCenter.toUrl());
// 从配置中心获取externalConfiguration和appExternalConfiguration并进行覆盖
String configContent = dynamicConfiguration.getProperties(configCenter.getConfigFile(), configCenter.getGroup());
String appGroup = getApplication().getName();
String appConfigContent = null;
if (isNotEmpty(appGroup)) {
appConfigContent = dynamicConfiguration.getProperties
(isNotEmpty(configCenter.getAppConfigFile()) ? configCenter.getAppConfigFile() : configCenter.getConfigFile(),
appGroup
);
}
try {
// 更新Environment
environment.setConfigCenterFirst(configCenter.isHighestPriority());
environment.updateExternalConfigurationMap(parseProperties(configContent));
environment.updateAppExternalConfigurationMap(parseProperties(appConfigContent));
} catch (IOException e) {
throw new IllegalStateException("Failed to parse configurations from Config Center.", e);
}
return dynamicConfiguration; // 返回通过该ConfigCenterConfig创建的DynamicConfiguration对象
}
return null;
}
完成 DynamicConfiguration 的创建之后DubboBootstrap 会将多个配置中心对应的 DynamicConfiguration 对象封装成一个 CompositeDynamicConfiguration 对象,并记录到 Environment.dynamicConfiguration 字段中,等待后续使用。另外,还会调用全部 AbstractConfig 的 refresh() 方法(即根据最新的配置更新各个 AbstractConfig 对象的字段)。这些逻辑都在 DubboBootstrap.startConfigCenter() 方法中,前面已经展示过了,这里不再重复。
配置中心初始化的后续流程
完成明确指定的配置中心初始化之后DubboBootstrap 接下来会执行 useRegistryAsConfigCenterIfNecessary() 方法,检测当前 Dubbo 是否要将注册中心也作为一个配置中心使用(常见的注册中心,都可以直接作为配置中心使用,这样可以降低运维成本)。
private void useRegistryAsConfigCenterIfNecessary() {
if (environment.getDynamicConfiguration().isPresent()) {
return; // 如果当前配置中心已经初始化完成,则不会将注册中心作为配置中心
}
if (CollectionUtils.isNotEmpty(configManager.getConfigCenters())) {
return; // 明确指定了配置中心的配置,哪怕配置中心初始化失败,也不会将注册中心作为配置中心
}
// 从ConfigManager中获取注册中心的配置即RegistryConfig并转换成配置中心的配置即ConfigCenterConfig
configManager.getDefaultRegistries().stream()
.filter(registryConfig -> registryConfig.getUseAsConfigCenter() == null || registryConfig.getUseAsConfigCenter())
.forEach(registryConfig -> {
String protocol = registryConfig.getProtocol();
String id = "config-center-" + protocol + "-" + registryConfig.getPort();
ConfigCenterConfig cc = new ConfigCenterConfig();
cc.setId(id);
if (cc.getParameters() == null) {
cc.setParameters(new HashMap<>());
}
if (registryConfig.getParameters() != null) {
cc.getParameters().putAll(registryConfig.getParameters());
}
cc.getParameters().put(CLIENT_KEY, registryConfig.getClient());
cc.setProtocol(registryConfig.getProtocol());
cc.setPort(registryConfig.getPort());
cc.setAddress(registryConfig.getAddress());
cc.setNamespace(registryConfig.getGroup());
cc.setUsername(registryConfig.getUsername());
cc.setPassword(registryConfig.getPassword());
if (registryConfig.getTimeout() != null) {
cc.setTimeout(registryConfig.getTimeout().longValue());
}
cc.setHighestPriority(false); // 这里优先级较低
configManager.addConfigCenter(cc);
});
startConfigCenter(); // 重新调用startConfigCenter()方法,初始化配置中心
}
完成配置中心的初始化之后,后续需要 DynamicConfiguration 的地方直接从 Environment 中获取即可例如DynamicConfigurationServiceNameMapping 就是依赖 DynamicConfiguration 实现 Service ID 与 Service Name 映射的管理。
接下来DubboBootstrap 执行 loadRemoteConfigs() 方法,根据前文更新后的 externalConfigurationMap 和 appExternalConfigurationMap 配置信息,确定是否配置了额外的注册中心或 Protocol如果有则在此处转换成 RegistryConfig 和 ProtocolConfig并记录到 ConfigManager 中,等待后续逻辑使用。
随后DubboBootstrap 执行 checkGlobalConfigs() 方法完成 ProviderConfig、ConsumerConfig、MetadataReportConfig 等一系列 AbstractConfig 的检查和初始化,具体实现比较简单,这里就不再展示。
再紧接着DubboBootstrap 会通过 initMetadataService() 方法初始化 MetadataReport、MetadataReportInstance 以及 MetadataService、MetadataServiceExporter这些元数据相关的组件在前面的课时中已经深入分析过了这里的初始化过程并不复杂你若感兴趣的话可以参考源码进行学习。
在 DubboBootstrap 初始化的最后,会调用 initEventListener() 方法将 DubboBootstrap 作为 EventListener 监听器添加到 EventDispatcher 中。DubboBootstrap 继承了 GenericEventListener 抽象类,如下图所示:
EventListener 继承关系图
GenericEventListener 是一个泛型监听器,它可以让子类监听任意关心的 Event 事件,只需定义相关的 onEvent() 方法即可。在 GenericEventListener 中维护了一个 handleEventMethods 集合,其中 Key 是 Event 的子类即监听器关心的事件Value 是处理该类型 Event 的相应 onEvent() 方法。
在 GenericEventListener 的构造方法中,通过反射将当前 GenericEventListener 实现的全部 onEvent() 方法都查找出来,并记录到 handleEventMethods 字段中。具体查找逻辑在 findHandleEventMethods() 方法中实现:
private Map<Class<?>, Set<Method>> findHandleEventMethods() {
Map<Class<?>, Set<Method>> eventMethods = new HashMap<>();
of(getClass().getMethods()) // 遍历当前GenericEventListener子类的全部方法
// 过滤得到onEvent()方法具体过滤条件在isHandleEventMethod()方法之中:
// 1.方法必须是public的
// 2.方法参数列表只有一个参数且该参数为Event子类
// 3.方法返回值为void且没有声明抛出异常
.filter(this::isHandleEventMethod)
.forEach(method -> {
Class<?> paramType = method.getParameterTypes()[0];
Set<Method> methods = eventMethods.computeIfAbsent(paramType, key -> new LinkedHashSet<>());
methods.add(method);
});
return eventMethods;
}
在 GenericEventListener 的 onEvent() 方法中,会根据收到的 Event 事件的具体类型,从 handleEventMethods 集合中找到相应的 onEvent() 方法进行调用,如下所示:
public final void onEvent(Event event) {
// 获取Event的实际类型
Class<?> eventClass = event.getClass();
// 根据Event的类型获取对应的onEvent()方法并调用
handleEventMethods.getOrDefault(eventClass, emptySet()).forEach(method -> {
ThrowableConsumer.execute(method, m -> {
m.invoke(this, event);
});
});
}
我们可以查看 DubboBootstrap 的所有方法,目前并没有发现符合 isHandleEventMethod() 条件的方法。但在 GenericEventListener 的另一个实现—— LoggingEventListener 中,可以看到多个符合 isHandleEventMethod() 条件的方法(如下图所示),在这些 onEvent() 方法重载中会输出 INFO 日志。
LoggingEventListener 中 onEvent 方法重载
至此DubboBootstrap 整个初始化过程,以及该过程中与配置中心相关的逻辑就介绍完了。
总结
本课时我们重点介绍了 Dubbo 动态配置中心启动的核心流程,以及该流程涉及的重要组件类。
首先,我们介绍了 ConfigManager 和 Environment 这两个非常基础的配置类;然后又讲解了 DubboBootstrap 初始化动态配置中心的核心流程,以及动态配置中心启动的流程;最后,还分析了 GenericEventListener 监听器的相关内容。
关于这部分的内容,如果你有什么问题或者好的经验,欢迎你在留言区和我分享。

View File

@@ -0,0 +1,25 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
49 结束语 认真学习,缩小差距
你好我是杨四正到这里我们已经一起学习了四十多个课时Dubbo 的核心内容也介绍差不多了,你可能也需要一段时间来回顾和消化这些内容。在最后这结束语部分,我还想和你“谈谈心”,从另一个角度来聊聊我们程序员这份工作。
在刚毕业的时候我误打误撞进入一家国营企业很多人认为这是一个“旱涝保收”的养老岗位其实呢也确实如此工资没有互联网企业有竞争力但是工作时长足以让“996”的程序员垂涎三尺。因为是第一份工作所以总会碰到很多问题但我发现在这个环境中很难从旁人那里得到答案于是我就开始一边自己解决问题一边反思与人沟通的方式。自己解决问题让我延续了学校里面的学习“惯性”养成了持续学习的习惯反思沟通方式让我意识到人是有惰性的人更喜欢用选择的方式解决问题所以我养成了提出问题时自带多个解决方案的习惯。这也反过来促使我在提问题之前反复思考和打磨问题毕竟提出一个好问题也是一种能力。
两年之后,我进入一家高速发展的互联网公司,在这里我经历了职业生涯里面的第一个“阵痛期”,可以说是从“闲庭信步”一步跨到“身心俱疲”,技术栈、作息规律、工作节奏等完全变了,其痛苦程度可想而知。
在这段时间,我体会最深的是要顺势而为,抓住行业的红利期,抓住公司的红利期,这可以更快地帮我实现薪资和职位的升级。另一个心得就是要学会适时抛弃“木桶原理”,不要补齐短板。因为我们走的是技术路线,要做的是不可替代,尽量成为一方面的专家,而不是处处稀松平常的通才,毕竟“内卷”越来越严重,“木桶”到处都有。
另外,还有一个非常重要的“点”就是:面对失败的态度。工作了这么多年,面试失败过,晋级失败过,也看过很多人不同的人生轨迹:有人离开奋斗多年的一线城市;有人埋头在西二旗的写字楼里,接收福报的洗礼,已经很久没见过夕阳是什么样子;有人创业失败,负债千万……这些都算是失败吗?可能不同的人有不同的答案,毕竟每个人对失败的定义不同,答案自然也会不同。
不管怎样,人生旅途中难免沟沟坎坎,挫折或失败似乎是人生的主旋律(注意是“似乎”,人生还是很美好的),不用纠结,每个人都会遇到,但如何面对挫折或失败会把我们分成不同的“队伍”:有的人会被击垮,从此一蹶不振;而有的人会站起来继续向前,越挫越勇,直至实现自己的人生目标和价值。所以说,真正的成长从来不是追求,而是正视自己的缺憾。
感谢 2020 年不断学习的你,感谢你的一路陪伴,也期待你继续“认真学习,缩小差距”。
当然如果你觉得我这门课程不错的话,也欢迎你推荐给身边的朋友。