关于dubbo:没想到吧关于Dubbo的『消费端线程池模型』官网也写错了

4次阅读

共计 9674 个字符,预计需要花费 25 分钟才能阅读完成。

[
文件](https://www.mdnice.com/#)格局性能查看主题代码主题设置帮忙

这是 why 的第 **63** 篇原创文章

![](https://p6-juejin.byteimg.com…

# 荒腔走板

大家好,我是 why,欢送来到我间断周更优质原创文章的第 63 篇。老规矩,先荒腔走板聊聊其余的。

下面这张图片是我前几天整顿相册的时候看到的。拍摄于 2016 年 8 月 20 日,北京。

那个时候我刚刚去北京没多久,住在公司的提供的宿舍外面。宿舍位于北京二环内的一个叫做东廊下的胡同里。

地位极佳,条件极差。

我刚刚进入宿舍的时候,房间外面只有一张大床、一个矮矮的电视柜、一个不能点头的风扇。我的房间也没有空调,到处都是灰蒙蒙的,用卫生间都是去楼下的公共卫生间。

有一次北京下暴雨,我才发现窗户那边有一个缺口,雨下的太大,能够顺着那个缺口流下来,把我的鞋都打湿了。

宿舍外面没有冰箱,所以节假日我在宿舍只煮面条或者用电饭煲做干饭,而后就着各种酱吃。记得有一次周五领导请咱们吃饭,最初菜点多了,有几个羊蹄动都没动,领导就叫我打包带回家。我带回去,挂在墙上挂钩,筹备第二天中午吃。第二天一闻,坏了,也就没有吃。

宿舍外面也没有洗衣机,所以我在超市买了一个微小的盆子,每周末的时候我会拿出一个下午的工夫,边看电视,边手搓衣服,四季如此。

刚刚去北京的前一年,过的真的还是很艰巨的。然而宿舍的益处是离公司近,所以我基本上也不怎么在宿舍呆着,工作日在公司学习到很晚,周末也去公司学习。

艰辛的环境更能激发人的斗志。

然而我还是简略的装璜了一下简陋的出租屋,买了贴画和绿植,因为我深信房子是租来的,然而生存是本人的。

而且每周洗完衣服后我会用洗衣服的水再拖一下地。我的房间很小,摆上一张 1.5 米的大床之后基本上就没有什么空间了,所以我用不上拖把,一张帕子就够了。

我能够蹲在地上,把房间外面的每一块地砖的边边角角都仔仔细细的擦拭一遍,而后跳到床上去,静静的坐着,开始放空本人。

过后并没感觉有什么艰难,然而和当初的生存再比照一下,真的是天壤之别。当初回想起,才真真正正的感觉:我已经也在北京使劲的生存过,来到的时候回顾满满,风华正茂。

就像我之前写过的:北漂就像在黑屋子里洗衣服,你不晓得洗洁净了没有,只能一遍又一遍地去洗。等到来到北京的那一刻,灯光亮了,你发现,如果你认真洗过了,那件衣服光洁如新。让你当前每次穿这件衣服都会想起那段岁月。

所以你呢,有没有在使劲的生存?

好了,说回文章。

# 大佬指导,纠正错误

前段时间一位大佬指出了我之前文章中的一处谬误:

![](https://p6-juejin.byteimg.com…

文章是这篇文章[《Dubbo 2.7.5 在线程模型上的优化》](https://mp.weixin.qq.com/s/tD…。

谬误具体是指上面红框框起来的这句话的形容:

![](https://p1-juejin.byteimg.com…

这是 why 的第 63 篇原创文章

荒腔走板

大家好,我是 why,欢送来到我间断周更优质原创文章的第 63 篇。老规矩,先荒腔走板聊聊其余的。

下面这张图片是我前几天整顿相册的时候看到的。拍摄于 2016 年 8 月 20 日,北京。

那个时候我刚刚去北京没多久,住在公司的提供的宿舍外面。宿舍位于北京二环内的一个叫做东廊下的胡同里。地位极佳,条件极差。

我刚刚进入宿舍的时候,房间外面只有一张大床、一个矮矮的电视柜、一个不能点头的风扇。我的房间也没有空调,到处都是灰蒙蒙的,用卫生间都是去楼下的公共卫生间。

有一次北京下暴雨,我才发现窗户那边有一个缺口,雨下的太大,能够顺着那个缺口流下来,把我的鞋都打湿了。

宿舍外面没有冰箱,所以节假日我在宿舍只煮面条或者用电饭煲做干饭,而后就着各种酱吃。记得有一次周五领导请咱们吃饭,最初菜点多了,有几个羊蹄动都没动,领导就叫我打包带回家。我带回去,挂在墙上挂钩,筹备第二天中午吃。第二天一闻,坏了,也就没有吃。

宿舍外面也没有洗衣机,所以我在超市买了一个微小的盆子,每周末的时候我会拿出一个下午的工夫,边看电视,边手搓衣服,四季如此。

刚刚去北京的前一年,过的真的还是很艰巨的。然而宿舍的益处是离公司近,所以我基本上也不怎么在宿舍呆着,工作日在公司学习到很晚,周末也去公司学习。

艰辛的环境更能激发人的斗志。

然而我还是简略的装璜了一下简陋的出租屋,买了贴画和绿植,因为我深信房子是租来的,然而生存是本人的。

而且每周洗完衣服后我会用洗衣服的水再拖一下地。我的房间很小,摆上一张 1.5 米的大床之后基本上就没有什么空间了,所以我用不上拖把,一张帕子就够了。

我能够蹲在地上,把房间外面的每一块地砖的边边角角都仔仔细细的擦拭一遍,而后跳到床上去,静静的坐着,开始放空本人。

过后并没感觉有什么艰难,然而和当初的生存再比照一下,真的是天壤之别。当初回想起,才真真正正的感觉:我已经也在北京使劲的生存过,来到的时候回顾满满,风华正茂。

就像我之前写过的:北漂就像在黑屋子里洗衣服,你不晓得洗洁净了没有,只能一遍又一遍地去洗。等到来到北京的那一刻,灯光亮了,你发现,如果你认真洗过了,那件衣服光洁如新。让你当前每次穿这件衣服都会想起那段岁月。所以你呢,有没有在使劲的生存?

好了,说回文章。

大佬指导,纠正错误

前段时间一位大佬指出了我之前文章中的一处谬误:

文章是这篇文章《Dubbo 2.7.5 在线程模型上的优化》。

谬误具体是指上面红框框起来的这句话的形容:

而这段话,我是援用的官网内容。而当初这部分内容曾经一字不差的退出到官网中了:

http://dubbo.apache.org/zh-cn/docs/user/demos/consumer-threadpool.html

通过验证后发现的确官网上的形容是有问题的。

所以本文就次要分享两个问题:

  • Dubbo 协定的设计与解析。
  • 以 Dubbo 2.7.5 版本(因为线程池模型就是在这个版本变更的)为分界线,比照不同版本之间,业务数据返回后,反序列化的操作到底是在独立的 Consumer 端线程池外面进行的还是在 IO 线程外面进行的?

须要阐明的是因为本文须要做不同版本之间的比照,所以会波及到两个 Dubbo 版本,别离是 2.7.4.1 和 2.7.5。写的时候我都会标注分明,大家看的时候和本人入手的时候须要留神一下。

另外再提前阐明一下,文章有点长:如果你本人看 Dubbo 源码,能够先看整体,疏忽细节。把整体摸个遍了之后,再去扣细节,精进源码。本文就属于扣细节,看的似懂非懂没关系,先一键三连,而后珍藏起来,你本人学的时候总是会学到这个中央来的,而且本文也不是一个十分难的技术点。

如果你没有学到,只能阐明你潜入的深度还是差了一点,兴许你差一点就走到这个中央了,而后你想:算了吧,差不多得了。

然而你要晓得,越往下,越难懂。而越难懂的,越值钱。

你想想,正在抗住流量的货色,是你写的那几行代码吗?不是的,是你零碎外面用到的 Nginx、MQ、Redis、Dubbo、SpringCloud 等等这些中间件。而这些中间件外面,抗住流量的,除了它们的集群性能、容错性能、限流熔断、调用链路的优化期待这些伎俩之外,还有底层的网络、IO、内存、数据结构、调度算法等等这些货色。

这是值钱的。

惋惜这些值钱的,不好讲清楚,要说分明就是简明扼要。所以我经常说的劝退长文都是说说而已的,你这么爱学习,我怎么会劝退你呢,激励你都来不及呢,你说是吧?

再说了,我写的长文,也并没有波及到这么底层的货色。只是我没有想过搪塞这事,我想把它做好了,尽量把它写分明了,两头再夹杂着几句“骚话”,所以写着写着就长了。

总之,你要深信三点:

一:我没有看懂,肯定是因为这个博主写的太烂。

二:我没有看懂,实践上大多数人也应该看不懂。

三:我没有看懂,那我本人钻研一下得让本人懂。

程序员就应该这样,明明天天写着这么一般的 crud,然而聊起技术来却是那么的迷之自信。

Dubbo 协定的设计与解析

为什么要先聊一下 Dubbo 的协定呢?

因为反序列化的时候波及到一些响应头(head)和响应体(body)解析的相干内容,是须要先进行一下铺垫的。

首先去官网上撸个图片过去:

能够看到 Dubbo 数据包分为音讯头(head)和音讯体(body)。

音讯头用于存储一些元信息,包含:魔数、数据包类型、调用形式、事件标识、序列化器编号、状态、申请编号、音讯体长度。

音讯体中用于存储具体的调用音讯,蕴含七局部内容:

  • Dubbo 版本号(Dubbo version)
  • 服务接口名(service name)
  • 服务接口版本(service version)
  • 办法名(method name)
  • 参数类型(parameter types)
  • 办法参数值(arguments)
  • 上下文信息(attachments)

客服端发动申请的时候严格依照下面的程序写入音讯,服务端依照同样的程序读取音讯,这样就能解析出音讯体外面的内容。

对于协定字段的解析,官网上也是有具体阐明的。撸过去:

再具体的解释一下,首先这图得和协定图一起看,我怕你不会,再给你搞一张示意图:

下面的截图只是演示了三个对应关系,然而这两张图就是这样看的。

我次要再解释一下外面的某些字段。

第一个:魔数

作为 Java 开发者,提到魔数,你第一个想到了什么?

0xCAFEBABY,对吧。

每个 class 文件的头 4 个字节就是魔数,它的惟一作用就是确定这个文件是否为一个能被 JVM 承受的 class 文件。

在 Dubbo 中这个魔数是用来干什么的呢?

兴许你不太分明,然而我心愿我一说你就能豁然开朗。因为你不悟,也不是本文要讲的货色,我也不好给你解释分明。

它是用来解决网络粘包 / 解包问题的。豁然开朗有没有?

没有?

对不起,本文不扩大相干内容。大学的时候《计算机网络》课程的时候逃课处对象去了吧?

在 Dubbo 协定中,它的魔数:0xdabb。你能够简略的把它了解为一个分隔符,用来解决粘包问题的。

第二个再说说:调用形式

首先这个字段仅在第 16 位设置为 1 的状况下无效。

从表外面咱们能够晓得,第 16 位为 1 就是指:request 申请。

在 rpc 中既然是 request,那么就分为两种调用形式:有去无回(单向)、有来有回(双向)。

相熟吗?

不相熟?呸,你个假粉丝,这张图在我的文章中至多呈现过两次:

oneway 就是单向,其余的调用类型都是有返回的。

所以调用分为两种类型,因而须要一个 bit 来寄存调用形式。

第三个说说事件标识字段

事件标识没啥说的,取值外面的形容也说的很分明了。只是阐明一下其中的 1(心跳包),不在本次文章的分享范畴内。

第四个说说状态字段

状态外面有个省略号,阐明没有枚举完。然而代码外面必定是齐的,这些状态对应的代码在这个类外面,一共 11 个,给大家补充残缺:org.apache.dubbo.remoting.exchange.Response

另外,再说一下返回的类型,讲到前面的时候须要晓得这个点。次要根据这个类外面定义的字段:org.apache.dubbo.rpc.protocol.dubbo.DubboCodec

对应的代码逻辑如下:org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#encodeResponseData(org.apache.dubbo.remoting.Channel, org.apache.dubbo.common.serialize.ObjectOutput, java.lang.Object, java.lang.String)

这个办法从名称也晓得,是对响应数据做解码操作的。

标号为①的中央是判断以后版本是否反对上下文信息传递。

标号为②的中央是判断是否是异样返回。

标号为③的中央表明不是异样返回,则判断返回值是否为 null。

标号为④的中央表明是失常返回,依据是否反对上下文信息传递,从而判断是只返回响应后果的还是既有响应后果,也有上下文信息的返回类型。

标号为⑤的中央表明是异样返回,依据是否反对上下文信息传递,从而判断是只返回异样后果的还是既有异样后果,也有上下文信息的返回类型。

好了,写到这里,协定就差不多说完了。其实不难发现这个协定就是一个偏实践的货色,这就是一个大家的约定。

所以我记起之前在一个分享大会上,一位嘉宾说的:

跨语言个性理论是 RPC 层的反对,实质是协定层面的反对。

我当初对这句话的了解更加粗浅了。

跨语言,也就是服务异构的一种。

为什么我用 Java 发送 http 申请的时候能够不必关怀对方应用的是什么开发语言?

因为大家都恪守了 http 协定,协定是能够跨语言的。

Dubbo 这种 rpc 调用的框架也一样。我发动近程调用之后,只有你能依照咱们约定好的协定进行报文的解析,那你就能失常的解决我发过来的申请,我不论你的开发语言是什么。

反序列化操作到底在哪进行?

业务数据返回后,反序列化的操作到底是在哪个线程外面进行的?

是在 IO 线程外面间接解析,还是被派发到客户端线程池外面进行解析?

这个问题咱们先试着在官网的线程模型介绍中去寻找答案。http://dubbo.apache.org/zh-cn/docs/user/demos/thread-model.html

在线程模型的形容外面,是这样写的:

如果事件处理的逻辑能迅速实现,并且不会发动新的 IO 申请,比方只是在内存中记个标识,则间接在 IO 线程上解决更快,因为 缩小了线程池调度

但如果事件处理逻辑较慢,或者须要发动新的 IO 申请,比方须要查询数据库,则必须派发到线程池,否则IO 线程阻塞,将导致不能接管其它申请

如果用 IO 线程处理事件,又在事件处理过程中发动新的 IO 申请,比方在连贯事件中发动登录申请,会报“可能引发死锁”异样,但不会真死锁。

因而,须要通过不同的派发策略和不同的线程池配置的组合来应答不同的场景。

本文不关怀线程池配置,咱们只看派发策略:

默认的派发策略是 all。

一看到这几个策略,相熟 Dubbo 的敌人必定就晓得了,依照 Dubbo 的尿性,这必须得是一个 SPI 接口啊。

果不其然,源码外面就是这样的,你说巧不巧:

而后官网还给出了一张形容不太清晰的图片:

图片中的 Dispatch 就是派发策略发挥作用的中央。

所以咱们能从这部分得出一个论断:在默认的状况下,客户端接管到响应后,因为 Dubbo 应用 all 的派发策略,会把响应申请派发到客户端线程池中去。

那咱们能够推导:出响应的解析肯定是在客户端线程池外面进行的吗?

不能够,推不进去的。

只能说响应会进入客户端线程池中去,然而这个响应可能是一个通过解析后的响应,也可能是一个没有通过解析的响应。

所以,这个响应有可能在进入线程池之前就被解析过了。被谁解析?

IO 线程。

如果 IO 线程没有解析,那就在客户端线程外面去解析。

依据下面这段话。咱们能提炼出一个要害语句,或者说是需要:咱们当初要实现响应报文能够在不同的中央进行解析的性能,请问你怎么做?

你用脚指头想也晓得了。首先必定是有一个 if 判断的,判断到底在哪(IO 线程 / 客户端线程池)进行响应解析。而这个 if 判断的判断条件,依照 Dubbo 的尿性,必定是能够配置的。

所以咱们找到这个中央,问题就了然于心了。

咱们去哪里找答案呢?

这个类外面,这个类是一个申请 / 响应解码的十分外围的类:org.apache.dubbo.rpc.protocol.dubbo.DubboCodec

这个类的次要干了两件事,一个是对响应报文进行解码,一个是对申请报文进行解码。

接下来咱们怎么搞?强撸源码吗?不可能的。间接撸必定吃力。

还是要搞个 Demo 跑起来,而后 Debug。

我这里的 Demo 非常简单,服务端接口实现类如下:

消费者在测试类中进行生产:

而后 Debug 起来,留神,上面演示的代码没有特地阐明的中央,都是 2.7.5 版本。

运行起来后先不看别的,看看以后卡在这个中央,被 Debug 的线程是什么线程:

到这里你先沉着一下,你想一下这个问题:

在这个办法外面能够对响应和申请进行解析。那它怎么晓得以后到底是响应还是申请报文呢?

答案就在后面说的 Dubbo 协定外面:

响应上了没有?header 外面第 16 bit 如果是 0 代表响应,如果是 1 代表申请。

你说巧不巧,下面这个办法的入参外面就有一个 header 数组。

让咱们看看他外面装的是什么货色:

长度是 16,和 header 的长度吻合,然而外面装的玩意还是没看进去。

然而这样一看,看前两个字节,你就明确了:

嘿,你说巧了吗,这不是巧了吗,这不是。

魔数也对上了。阐明这是一个 Dubbo 的 header。

而后取出第 3 字节,进行位运算,判断这是什么报文:

后面,咱们解决了怎么晓得以后到底是响应还是申请报文这个问题。

接下来,进入分支外面就重点关注对响应报文的解析了:

首先,下面标记为①的中央是判断以后数据包是不是一个心跳包,通过 Debug 咱们能够晓得这不是一个心跳包:

而后标记为②的中央获取 header 中的第 4 个字节,第 4 个字节代表的是状态位:

从 Debug 的截图外面咱们能够看出,以后的状态为 20,示意失常返回。

标记为③的中央,是对心跳包的解析,咱们这里不关怀。

标记为④的中央,是咱们须要重点关注的中央,也是咱们始终在寻找的代码。

这个中央就很要害了,大家集中注意力了。

首先,上面代码的截图是 2.7.5 版本的:

这里的 if 分支和分支外面的判断条件,就是咱们后面说的:

你用脚指头想也晓得了。首先必定是有一个 if 判断的,判断到底在哪(IO 线程 / 客户端线程池)进行响应解析。而这个分支判断的判断条件,依照 Dubbo 的尿性,必定是能够配置的。

上面这张图片对 2.7.4.1 和 2.7.5 版本这个中央进行一个比照:

你认真看着两个版本之间的代码,发现截然不同,也没有差别啊。

这就把我干懵逼了:咋回事?说好的差别呢?

别忘了,下面的代码外面是有一个变量的:

差别就差别在这个中央。

2.7.5 版本之后,这个参数的默认值从 true 变为了 false。

换句话说就是:2.7.5 版本之前,业务数据返回后,默认在 IO 线程外面进行反序列化的操作。而 2.7.5 版本之后,默认是提早到客户端线程池外面进行反序列化的操作。

(倡议朗诵并背诵)

同时这个参数,不论在哪个版本外面,都是能够配置。尽管基本上也没有人更改过这个配置,配置办法如下:

敌人们,到这里还跟的上不?跟不上你就再捋捋?别硬看,伤身材。

解码操作源码解析

接下来咱们再看看解码操作的代码到底是怎么样的。

首先解码操作,解的什么码?

解的是响应报文的响应体,也就是咱们的返回内容:org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcResult#decode(org.apache.dubbo.remoting.Channel, java.io.InputStream)

标号为①的中央代表序列化类型是 2。

2 是什么?看表:

标号为②的中央代表本次响应类型为 4。

4 是什么?后面说了,看截图:

所以,在标号为③的中央即解决了返回值(handleValue)也解决了上下文信息(handleAttachment)。

handleValue 就不细看了,你就关注这个中央解析进去的就是咱们的响应内容:

响应内容的解码就是下面说的逻辑。

不论是在 IO 线程外面解码还是在客户端线程池外面解码,都要调用这个办法。只不过是谁先谁后的问题。

那么问题又来了,需要又发生变化了。

因为 IO 线程和客户端线程池都要调用这个办法进行解码,咱们总不能解码两次吧,那怎么保障只解码一次呢?

答案就是设置标识位。

因为咱们晓得如果是在 IO 线程外面解码,那么该操作调用解码办法后,必定是先于客户端线程池调用的。

有先后顺序就好办了。咱们就能够设置标识位:

当在 IO 线程解析后,会把标识位设置为 true。而后客户端线程池再走到这个逻辑的时候,发现标识位是 true 了,不进行再次操作,问题就这样被解决了。

接下来,我给大家比照一下 decodeBody 办法在 IO 线程外面解码和在客户端线程池外面解码时别离返回什么。也就是这行代码返回的时候:

这样一比照就很清晰了:

这样也解释了,为什么说是“提早”到客户端线程池外面解码。

好了,到这里你有没有发现一个问题。后面解析的这么多源码,而后咔一下,间接咱们就看到了最终返回的“Hello why”了。

这个是响应音讯体,是 body。

头呢?header 呢?

别急,这不是马上就给你讲一下嘛。

后面讲这个办法的时候说了:header 是作为参数传进来的嘛,那咱们还能够去找一下 header 到底是怎么传进来的:

怎么看呢?

顺着调用链往回找就行,一个调试小技巧,送给大家,不客气:

能够看到 header 是从 buffer 外面取出来的,最多读取 HEADER_LENGTH (16) 个字节。

什么?你还问我为什么最多读 16 个字节?

我狐疑后面讲协定的时候你就在走神。别问,问就是协定规定。大家恪守就好了。

再跟着调用链往前一步,你会发现这里次要是在做解码响应头的局部:

下面这个办法外面就是在搞 header 的事件。

其中有一个查看报文长度的办法:checkPayLoad。

那么问题又来了:请问 Dubbo 默认的报文长度限度是多少呢?

带大家去源码外面找答案:

答案是 8M。

另外,既然是有默认值,那必须是能够配置的。所以上图标号为①的中央是从配置中获取,获取不到,就返回默认值。

略微有点意思的是标号为②的中央,我第一次看的时候愣是看了一分钟没反馈过去。次要是后面的这个 payload > 0,我想着这不是废话嘛,长度不都是大于 0 的。兴奋的我认为发现了一个无用代码呢。

起初才了解到,如果当 payload 设置为正数的时候,就代表不限度报文长度。

能够进行如下配置:

一个基本上用不到的 Dubbo 小知识点,收费赠送给大家。

好了,header 和 body 都齐活了。

到这里,再总结一下:2.7.5 版本之前,业务数据返回后,默认在 IO 线程外面进行反序列化的操作。而 2.7.5 版本之后,默认是提早到客户端线程池外面进行反序列化的操作。

所以,对于官网中,红框框起来这个中央的形容是有问题的:http://dubbo.apache.org/zh-cn/docs/user/demos/consumer-threadpool.html

正确的说法应该是:在老的(2.7.5 版本之前)线程池模型中,当业务数据返回后,默认在 IO 线程上进行反序列化操作,如果配置了 decode.in.io 参数为 false,则提早到独立的客户端线程池进行反序列化操作。

聊聊线程池模型的变动

接下来再聊聊线程池模型的变动。这里的线程池指的都是客户端线程池。

先抛两个知识点:

  • 不论是新老线程池模型,默认的 Dispatch 策略都是 all。所有响应还是会转发到客户端线程池外面,在这个外面进行解码操作(如果 IO 线程没有解码的话)把后果返回到用户线程中去。
  • 对于线程池客户端的默认实现是 cached,服务端的默认实现是 fixed。

官网这里的 fixed 缺省,特指服务端:

上面是官网上的截图:

首先,不论 2.7.5 版本之前还是之后客户端的默认实现都是 cached,这个线程池并没有限度线程数量:

所以会呈现生产端线程数调配多的问题。

但官网的形容是:调配过多。多和过多还不一样。

为什么会过多呢?

因为在 2.7.5 版本之前,是每一个链接都对应一个客户端线程池。相当于做了链接级别的线程隔离,然而实际上这个线程隔离是没有必要的。反而影响了性能。

而在 2.7.5 版本外面,就是不论你多少链接,大家共用一个客户端线程池,引入了 threadless executor 的概念。

简略的来说,优化后果就是从多个线程池改为了共用一个线程池。

线程池模型的变动,我在《Dubbo 2.7.5 在线程模型上的优化》外面比拟具体的聊过了,就不在反复讲了,有趣味的能够去翻一下。

感谢您的浏览,我保持原创,非常欢送并感谢您的关注。

我是 why,一个被代码耽搁的文学创作者,不是大佬,然而喜爱分享,是一个又暖又有料的四川好男人。

正文完
 0