Dubbo 2.7.5在线程模型上的优化

这是why技术的第30篇原创文章
java

这多是全网第一篇解析Dubbo 2.7.5里程碑版本中的改进点之一:客户端线程模型优化的文章。git

先劝退:文本共计8190字,54张图。阅读以前须要对Dubbo相关知识点有必定的基础。内容比较硬核,劝君谨慎阅读。github

读不下去没关系,我写的真的很辛苦的,帮忙点个赞吧。算法

本文目录

第一节:官方发布

本小节主要是经过官方发布的一篇名为《Dubbo 发布里程碑版本,性能提高30%》的文章做为引子,引出本文所要分享的内容:客户端线程模型优化。apache

第二节:官网上的介绍

在介绍优化后的消费端线程模型以前,先简单的介绍一下Dubbo的线程模型是什么。同时发现官方文档对于该部分的介绍十分简略,因此结合代码对其进行补充说明。编程

第三节:2.7.5版本以前的线程模型的问题

经过一个issue串联本小节,道出并分析一些消费端应用,当面临须要消费大量服务且并发数比较大的大流量场景时(典型如网关类场景),常常会出现消费端线程数分配过多的问题。segmentfault

第四节:thredless是什么

经过第三节引出了新版本的解决方案,thredless。并对其进行一个简单的介绍。跨域

第五节:场景复现

因为条件有限,场景复现起来比较麻烦,可是我在issues#890中发现了一个很好的终结,因此我搬过来了。缓存

第六节:新旧线程模型对比

本小节经过对比新老线程模型的调用流程,并对比2.7.4.1版本和2.7.5版本关键的代码,起到一个导读的做用。微信

第七节:Dubbo版本介绍。

趁着此次的版本升级,也趁机介绍一下Dubbo目前的两个主要版本:2.6.X和2.7.X。

第八节:我内推你,来面基啊!

和本文没有相关度,纯粹是内推。本身去瞅瞅呗。

官方发布

2020年1月9日,阿里巴巴中间件发布名为《Dubbo 发布里程碑版本,性能提高30%》的文章:

文章中说这是Dubbo的一个里程碑式的版本。

在阅读了相关内容后,我发现这确实是一个里程碑式的跨域,对于Dubbo坎坷的一辈子来讲,这是展示其强大的生命力和积极探索精神的一个版本。

强大的生命力体如今新版本发布后众多的或赞赏、或吐槽的社区反馈。

探索精神体如今Dubbo在多语言和协议穿透性上的探索。

在文章中列举了9大改造点,本文仅介绍2.7.5版本中的一个改造点:优化后的消费端线程模型。

本文大部分源码为2.7.5版本,同时也会有2.7.4.1版本的源码做为对比。

官网上的介绍

在介绍优化后的消费端线程模型以前,先简单的介绍一下Dubbo的线程模型是什么。

直接看官方文档中的描述,Dubbo官方文档是一份很是不错的入门学习的文档,不少知识点都写的很是详细。

惋惜,在线程模型这块,差强人意,寥寥数语,图不达意:

官方的配图中,彻底没有体现出线程"池"的概念,也没有体现出同步转异步的调用链路。仅仅是一个远程调用请求的发送与接收过程,至于响应的发送与接收过程,这张图中也没有表现出来。

因此我结合官方文档和2.7.5版本的源码进行一个简要的介绍,在阅读源码的过程当中你会发现:

在客户端,除了用户线程外,还会有一个线程名称为DubboClientHandler-ip:port的线程池,其默认实现是cache线程池。

上图的第93行代码的含义是,当客户端没有指定threadpool时,采用cached实现方式。

上图中的setThreadName方法,就是设置线程名称:

org.apache.dubbo.common.utils.ExecutorUtil#setThreadName

能够清楚的看到,线程名称若是没有指定时,默认是DubboClientHandler-ip:port。

在服务端,除了有boss线程、worker线程(io线程),还有一个线程名称为DubboServerHandler-ip:port的线程池,其默认实现是fixed线程池。

启用线程池的dubbo.xml配置以下:

<dubbo:protocol name="dubbo" threadpool="xxx"/>

上面的xxx能够是fixed、cached、limited、eager,其中fixed是默认实现。固然因为是SPI,因此也能够自行扩展:

因此,基于最新2.7.5版本,官方文档下面红框框起来的这个地方,描述的有误导性:

从SPI接口看来,fixed确实是缺省值。

可是因为客户端在初始化线程池以前,加了一行代码(以前说的93行),因此客户端的默认实现是cached,服务端的默认实现是fixed。

我也看了以前的版本,至少在2.6.0时(更早以前的版本没有查看),客户端的线程池的默认实现就是cached。

关于Dispatcher部分的描述是没有问题的:

Dispatcher部分是线程模型中一个比较重要的点,后面会提到。

这里配一个稍微详细一点的2.7.5版本以前的线程模型,供你们参考:

图片来源:https://github.com/apache/dubbo/issues/890

2.7.5以前的线程模型的问题

那么改进以前的线程模型到底存在什么样的问题呢?

在《Dubbo 发布里程碑版本,性能提高30%》一文中,是这样描述的:

对 2.7.5 版本以前的 Dubbo 应用,尤为是一些消费端应用,当面临须要消费大量服务且并发数比较大的大流量场景时(典型如网关类场景),常常会出现消费端线程数分配过多的问题。

同时文章给出了一个issue的连接:

https://github.com/apache/dubbo/issues/2013

这一小节,我就顺着这个issue#2013给你们捋一下Dubbo 2.7.5版本以前的线程模型存在的问题,准确的说,是客户端线程模型存在的问题:

首先,Jaskey说到,分析了issue#1932,他说在某些状况下,会建立很是多的线程,所以进程会出现OOM的问题。

在分析了这个问题以后,他发现客户端使用了一个缓存线程池(就是咱们前面说的客户端线程实现方式是cached),它并无限制线程大小,这是根本缘由。

接下来,咱们去issue#1932看看是怎么说的:

https://github.com/apache/dubbo/issues/1932

能够看到issue#1932也是Jaskey提出的,他主要传达了一个意思:为何我设置了actives=20,可是在客户端却有超过10000个线程名称为DubboClientHandler的线程的状态为blocked?这是否是一个Bug呢?

仅就这个issue,我先回答一下这个:不是Bug!

咱们先看看actives=20的含义是什么:

按照官网上的解释:actives=20的含义是每一个服务消费者每一个方法最大并发调用数为20。

也就是说,服务端提供一个方法,客户端调用该方法,同时最多容许20个请求调用,可是客户端的线程模型是cached,接受到请求后,能够把请求都缓存到线程池中去。因此在大量的比较耗时的请求的场景下,客户端的线程数远远超过20。

这个actives配置在《一文讲透Dubbo负载均衡之最小活跃数算法》这篇文章中也有说明。它的生效须要配合ActiveLimitFilter过滤器,actives的默认值为0,表示不限制。当actives>0时,ActiveLimitFilter自动生效。因为不是本文重点,就不在这里详细说明了,有兴趣的能够阅读以前的文章。

顺着issue#2013捋下去,咱们能够看到issue#1896提到的这个问题:

问题1我已经在前面解释了,他这里的猜想前半句对,后半句错。再也不多说。

这里主要看问题2(能够点开大图看看):服务提供者多了,消费端维护的线程池就多了。致使虽然服务提供者的能力大了,可是消费端有了巨大的线程消耗。他和下面issue#4467的哥们表达的是同一个意思:想要的是一个共享的线程池。

咱们接着往下捋,能够发现issue#4467和issue#5490

对于issue#4467,CodingSinger说:为何Dubbo对每个连接都建立一个线程池?

从Dubbo 2.7.4.1的源码咱们也能够看到确实是在WarppedChannelHandler构造函数里面确实是为每个链接都建立了一个线程池:

issue#4467想要表达的是什么意思呢?

就是这个地方为何要作连接级别的线程隔离,一个客户端,就算有多个链接都应该用共享线程池呀?

我我的也以为这个地方不该该作线程隔离。线程隔离的使用场景应该是针对一些特别重要的方法或者特别慢的方法或者功能差别较大的方法。很显然,Dubbo的客户端就算一个方法有多个链接(配置了connections参数),也是一视同仁,不太符合线程隔离的使用场景。

而后chickenij大佬在2019年7月24日回复了这个issue:

现有的设计就是:provider端默认共用一个线程池。consumer端是每一个连接共享一个线程池。

同时他也说了:对于consumer线程池,当前正在尝试优化中。

言外之意是他也以为现有的consumer端的线程模型也是有优化空间的。

这里插一句:chickenlj是谁呢?

刘军,GitHub帐号Chickenlj,Apache Dubbo PMC,项目核心维护者,见证了Dubbo从重启开源到Apache毕业的整个流程。现任职阿里云云原生应用平台团队,参与服务框架、微服务相关工做,目前主要在推进Dubbo开源的云原生化。

他这篇文章的做者呀,他的话仍是颇有份量的。

以前也在Dubbo开发者日成都站听到过他的分享:

若是对他演讲的内容有兴趣的朋友能够在公众号的后台回复:1026。领取讲师PPT和录播地址。

好了,咱们接着往下看以前提到的issue#5490,刘军大佬在2019年12月16日就说了,在2.7.5版本时会引入threadless executor机制,用于优化、加强客户端线程模型。

threadless是什么?

根据类上的说明咱们能够知道:

这个Executor和其余正常Executor之间最重要的区别是这个Executor无论理任何线程。

经过execute(Runnable)方法提交给这个执行器的任务不会被调度到特定线程,而其余的Executor就把Runnable交给线程去执行了。

这些任务存储在阻塞队列中,只有当thead调用waitAndDrain()方法时才会真正执行。简单来讲就是,执行task的thead与调用waitAndDrain()方法的thead彻底相同。

其中说到的waitAndDrain()方法以下:

execute(Runnable)方法以下:

同时咱们还能够看到,里面还维护了一个名称叫作sharedExecutor的线程池。见名知意,咱们就知道了,这里应该是要作线程池共享了。

场景复现

上面说了这么多2.7.5版本以前的线程模型的问题,咱们怎么复现一次呢?

我这里条件有限,场景复现起来比较麻烦,可是我在issues#890中发现了一个很好的终结,我搬过来便可:

根据他接下来的描述作出思惟导图以下:

上面说的是corethreads大于0的场景。可是根据现有的线程模型,即便核心池数(corethreads)为0,当消费者应用依赖的服务提供者处理很慢时且请求并发量比较大时,也会出现消费者线程数不少问题。你们能够对比着看一下。

新旧线程模型对比

在以前的介绍中你们已经知道了,此次升级主要是加强客户端线程模型,因此关于2.7.5版本以前和以后的线程池模型咱们主要关心Consumer部分。

老的线程模型

老的线程池模型以下,注意线条颜色:

一、业务线程发出请求,拿到一个 Future 实例。

二、业务线程紧接着调用 future.get 阻塞等待业务结果返回。
三、当业务数据返回后,交由独立的 Consumer 端线程池进行反序列化等处理,并调用 future.set 将反序列化后的业务结果置回。
四、业务线程拿到结果直接返回。

新的线程模型

新的线程池模型以下,注意线条颜色:

一、业务线程发出请求,拿到一个 Future 实例。
二、在调用 future.get() 以前,先调用 ThreadlessExecutor.wait(),wait 会使业务线程在一个阻塞队列上等待,直到队列中被加入元素。
三、当业务数据返回后,生成一个 Runnable Task 并放ThreadlessExecutor 队列。
四、业务线程将 Task 取出并在本线程中执行反序列化业务数据并 set 到 Future。
五、业务线程拿到结果直接返回。

能够看到,相比于老的线程池模型,新的线程模型由业务线程本身负责监测并解析返回结果,免去了额外的消费端线程池开销。

代码对比

接下来咱们对比一下2.7.4.1版本和2.7.5版本的代码,来讲明上面的变化。

须要注意的是,因为涉及到的变化代码很是的多,我这里仅仅起到一个导读的做用,若是读者想要详细了解相关变化,还须要本身仔细阅读源码

首先两个版本的第一步是同样的:业务线程发出请求,拿到一个Future实例。

可是实现代码却有所差别,在2.7.4.1版本中,以下代码所示:

上图圈起来的request方法最终会走到这个地方,能够看到确实是返回了一个Future实例:

而newFuture方法源码以下,请记住这个方法,后面会进行对比:

同时经过源码能够看到在获取到Future实例后,紧接着调用了subscribeTo方法,实现方法以下:

用了Java 8的CompletableFuture,实现异步编程。

可是在2.7.5版本中,以下代码所示:

在request方法中多了个executor参数,而该参数就是的实现类就是ThreadlessExecutor。

接下来,和以前的版本同样,会经过newFuture方法去获取一个DefaultFuture对象:

经过和2.7.4.1版本的newFuture方法对比你会发现这个地方就大不同了。虽然都是获取Future,可是Future里面的内容不同了。

直接上个代码对比图,一目了然:

第二步:业务线程紧接着调用 future.get 阻塞等待业务结果返回。

因为Dubbo默认是同步调用,而同步和异步调用的区别我在第一篇文章《Dubbo 2.7新特性之异步化改造》中就进行了详细解析:

咱们找到异步转同步的地方,先看2.7.4.1版本的以下代码所示:

而这里的asyncResult.get()对应的源码是,CompletableFuture.get():

而在2.7.5版本中对应的地方发生了变化:

变化就在这个asyncResult.get方法上。

在2.7.5版本中,该方法的实现源码是:

先说标号为②的地方,和2.7.4.1版本是同样的,都是调用的CompletableFuture.get()。可是多了标号为①的代码逻辑。而这段代码就是以前新的线程模型里面体现的地方,下面红框框起来的部分:

在调用 future.get() 以前(即调用标号为②的代码以前),先调用 ThreadlessExecutor.wait()(即标号为①处的逻辑),wait 会使业务线程在一个阻塞队列上等待,直到队列中被加入元素。

接下来再对比两个地方:

第一个地方:以前提到的WrappedChannelHandler,能够看到2.7.5版本其构造函数的改造很是大:

第二个地方:以前提到的Dispatcher,是须要再写一篇文章才能说的清楚的,我这仅仅是作一个抛砖引玉,提一下:

AllChannelHandler是默认的策略,证实代码以下:

首先仍是看标号为②的地方,看起来变化很大,其实就是对代码进行了一个抽离,封装。sendFeedback方法以下,和2.7.4.1版本中标号为②的地方的代码是同样的:

因此咱们重点对比一下两个标号为①的地方,它们获取executor的方法变了:

2.7.4.1版本的方法是getExecutorService()
2.7.5版本的方法是getPreferredExecutorService()

代码以下,你们品一品两个版本以前的差别:

主要翻译一下getPreferredExecutorService方法上的注释:

Currently, this method is mainly customized to facilitate the thread model on consumer side.
1. Use ThreadlessExecutor, aka., delegate callback directly to the thread initiating the call.   
2. Use shared executor to execute the callback.

目前,使用这种方法主要是为了客户端的线程模型而定制的。

1.使用ThreadlessExceutor,aka.,将回调直接委托给发起调用的线程。
2.使用shared executor执行回调。

小声说一句:这里这个aka怎么翻译,我实在是不知道了。难道是嘻哈里面的AKA?你们好,我是宝石GEM,aka(又名) 你的老舅。又画彩虹又画龙的。

好了,导读就到这里了。能看到这个地方的人我相信已经很少了。仍是以前那句话因为涉及到的变化代码很是的多,我这里仅仅起到一个导读的做用,若是读者想要详细了解相关变化,还须要本身仔细阅读源码。但愿你能本身搭个Demo跑一跑,对比一下两个版本的差别。

Dubbo版本介绍

趁着此次的版本升级,也趁机介绍一下Dubbo目前的主要版本吧。

据刘军大佬的分享:Dubbo 社区目前主力维护的有 2.6.x 和 2.7.x 两大版本,其中:

2.6.x 主要以 bugfix 和少许 enhancements 为主,所以能彻底保证稳定性。

2.7.x 做为社区的主要开发版本,获得持续更新并增长了大量新 feature 和优化,同时也带来了一些稳定性挑战。

为方便 Dubbo 用户升级,社区在如下表格对 Dubbo 的各个版本进行了总结,包括主要功能、稳定性和兼容性等,从多个方面评估每一个版本,以期能帮助用户完成升级评估:

能够看到社区对于最新的2.7.5版本的升级建议是:不建议大规模生产使用。

同时你去看Dubbo最新的issue,有不少都是对于2.7.5版本的"吐槽"。

可是我却是以为2.7.5是Dubbo发展进程中浓墨重彩的一笔,该版本打响了对于 Dubbo向整个微服务云原生体系靠齐的第一枪。对于多语言的支持方向的探索。实现了对 HTTP/2 协议的支持,同时增长了与 Protobuf 的结合。

开源项目,共同维护。咱们固然知道Dubbo不是一个完美的框架,可是咱们也知道,它的背后有一群知道它不完美,可是仍然不言乏力、不言放弃的工程师,他们在努力改造它,让它趋于完美。咱们做为使用者,咱们少一点"吐槽",多一点鼓励。只有这样咱们才能骄傲的说,咱们为开源世界贡献了一点点的力量,咱们相信它的明天会更好。

向开源致敬,向开源工程师致敬。

总之,牛逼。

来新网,作兄弟

在这里插播一个招聘,我行(是的,别人都是我司,我这里我行,银行的行)最近在大量招人,海量HC,提供有竞争力的薪水,简介以下:

欢迎有兴趣的朋友欢迎扫描投递简历,有问题能够在公众号后台加到个人微信,来新网面基呀:

最后说一句

才疏学浅,不免会有纰漏,若是你发现了错误的地方,还请你留言给我指出来,我对其加以修改。

感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

以上。

欢迎关注公众号【why技术】。在这里我会分享一些技术相关的东西,主攻java方向,用匠心敲代码,对每一行代码负责。偶尔也会荒腔走板的聊一聊生活,写一写书评,影评。愿你我共同进步。

公众号-why技术

相关文章
相关标签/搜索