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

https://mp.weixin.qq.com/s/vE_u-8aF_D4ab8lmCFNwywhtml




image.png

荒腔走板

你们好,我是 why,欢迎来到我连续周更优质原创文章的第 63 篇。老规矩,先荒腔走板聊聊其余的。
java

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

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

位置极佳,条件极差。算法

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

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

宿舍里面没有冰箱,因此节假日我在宿舍只煮面条或者用电饭煲作干饭,而后就着各类酱吃。记得有一次周五领导请咱们吃饭,最后菜点多了,有几个羊蹄动都没动,领导就叫我打包带回家。我带回去,挂在墙上挂钩,准备次日中午吃。次日一闻,坏了,也就没有吃。小程序

宿舍里面也没有洗衣机,因此我在超市买了一个巨大的盆子,每周末的时候我会拿出一个下午的时间,边看电视,边手搓衣服,四季如此。c#

刚刚去北京的前一年,过的真的仍是很艰难的。可是宿舍的好处是离公司近,因此我基本上也不怎么在宿舍呆着,工做日在公司学习到很晚,周末也去公司学习。数组

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

可是我仍是简单的装饰了一下简陋的出租屋,买了贴画和绿植,由于我坚信房子是租来的,可是生活是本身的。

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

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

当时并没以为有什么困难,可是和如今的生活再对比一下,真的是天壤之别。如今回想起,才真真正正的以为:我曾经也在北京用力的生活过,离开的时候回忆满满,风华正茂。

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

因此你呢,有没有在用力的生活?

好了,说回文章。

大佬指点,纠正错误

前段时间一位大佬指出了我以前文章中的一处错误:
image.png是的,这个大佬就是公众号【肥朝】的号主。

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

错误具体是指下面红框框起来的这句话的描述:

image.png

而这段话,我是引用的官方内容。而如今这部份内容已经一字不差的加入到官网中了:

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

通过验证后发现确实官网上的描述是有问题的。

image.png

先说结论:2.7.5 版本以前,业务数据返回后,默认在 IO 线程里面进行反序列化的操做。而2.7.5 版本以后,默认是延迟到客户端线程池里面进行反序列化的操做。

因此,对于官网中,上面红框框起来这个地方的描述“交由独立的 Consumer 端线程池进行反序列化处理”是有问题

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

因此本文就主要分享两个问题:

  • Dubbo 协议的设计与解析。

  • 以 Dubbo 2.7.5 版本(由于线程池模型就是在这个版本变动的)为分界线,对比不一样版本之间,业务数据返回后,反序列化的操做究竟是在独立的 Consumer 端线程池里面进行的仍是在 IO 线程里面进行的?

须要说明的是因为本文须要作不一样版本之间的对比,因此会涉及到两个 Dubbo 版本,分别是 2.7.4.1 和 2.7.5 。写的时候我都会标注清楚,你们看的时候和本身动手的时候须要注意一下。

另外再提早说明一下,文章有点长:若是你本身看 Dubbo 源码,能够先看总体,忽略细节。把总体摸个遍了以后,再去扣细节,精进源码。本文就属于扣细节,看的似懂非懂不要紧,先一键三连,而后收藏起来,你本身学的时候老是会学到这个地方来的,并且本文也不是一个很是可贵技术点。

若是你没有学到,只能说明你潜入的深度仍是差了一点,也许你差一点就走到这个地方了,而后你想:算了吧,差很少得了。

可是你要知道,越往下,越难懂。而越难懂的,越值钱。

你想一想,正在抗住流量的东西,是你写的那几行代码吗?不是的,是你系统里面用到的 Nginx、MQ、Redis、Dubbo、SpringCloud 等等这些中间件。而这些中间件里面,抗住流量的,除了它们的集群功能、错功能、限流熔断、调用链路的优化等待这些手段以外,还有底层的网络、IO、内存、数据结构、调度算法等待这些东西。

这是值钱的。

惋惜这些值钱的,很差讲清楚,要说清楚就是长篇大论。因此我经常说的劝退长文都是说说而已的,你这么爱学习,我怎么会劝退你呢,鼓励你都来不及呢,你说是吧?

再说了,我写的长文,也并无涉及到这么底层的东西。只是我没有想过敷衍这事,我想把它作好了,尽可能把它写清楚了,中间再夹杂着几句“”,因此写着写着就长了。

总之,你要坚信三点:

一:我没有看懂,必定是由于这个博主写的太烂。

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

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

程序员就应该这样,明明每天写着这么普通的 crud,可是聊起技术来倒是那么的迷之自信。

image.png

Dubbo协议的设计与解析

为何要先聊一下 Dubbo 的协议呢?

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

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

image.png

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

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

消息体中用于存储具体的调用消息,包含七部份内容:

  • Dubbo 版本号(Dubbo version)

  • 服务接口名(service name)

  • 服务接口版本(service version)

  • 方法名(method name)

  • 参数类型(parameter types)

  • 方法参数值(arguments)

  • 上下文信息(attachments)

客服端发起请求的时候严格按照上面的顺序写入消息,服务端按照一样的顺序读取消息,这样就能解析出消息体里面的内容。

对于协议字段的解析,官网上也是有详细说明的。撸过来:

image.png

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

image.png

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

我主要再解释一下里面的某些字段。

第一个:魔数

image.png

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

0xCAFEBABY,对吧。

image.png

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

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

也许你不太清楚,可是我但愿我一说你就能恍然大悟。由于你不悟,也不是本文要讲的东西,我也很差给你解释清楚。

它是用来解决网络粘包/解包问题的。恍然大悟有没有?

没有?

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

image.png

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

第二个再说说:调用方式

image.png

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

从表里面咱们能够知道,第 16 位为 1 就是指:request 请求。

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

熟悉吗?

不熟悉?呸,你个假粉丝,这张图在个人文章中至少出现过两次:

image.png

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

因此调用分为两种类型,所以须要一个 bit 来存放调用方式。

第三个说说事件标识字段

image.png

事件标识没啥说的,取值里面的描述也说的很清楚了。只是说明一下其中的 1 (心跳包),不在本次文章的分享范围内。

第四个说说状态字段

image.png

状态里面有个省略号,说明没有枚举完。可是代码里面确定是齐的,这些状态对应的代码在这个类里面,一共 11 个,给你们补充完整:org.apache.dubbo.remoting.exchange.Response

image.png

另外,再说一下返回的类型,讲到后面的时候须要知道这个点。主要依据这个类里面定义的字段:org.apache.dubbo.rpc.protocol.dubbo.DubboCodec

image.png

对应的代码逻辑以下: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)

image.png

这个方法从名称也知道,是对响应数据作解码操做的。

标号为①的地方是判断当前版本是否支持上下文信息传递。

标号为②的地方是判断是不是异常返回。

标号为③的地方代表不是异常返回,则判断返回值是否为 null。

标号为④的地方代表是正常返回,根据是否支持上下文信息传递,从而判断是只返回响应结果的仍是既有响应结果,也有上下文信息的返回类型。

标号为⑤的地方代表是异常返回,根据是否支持上下文信息传递,从而判断是只返回异常结果的仍是既有异常结果,也有上下文信息的返回类型。

好了,写到这里,协议就差很少说完了。其实不难发现这个协议就是一个偏理论的东西,这就是一个你们的约定。

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

跨语言特性实际是RPC层的支持,本质是协议层面的支持。

我如今对这句话的理解更加深入了。

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

为何我用 Java 发送 http 请求的时候能够不用关心对方使用的是什么开发语言?

由于你们都遵照了 http 协议,协议是能够跨语言的。

Dubbo 这种 rpc 调用的框架也同样。我发起远程调用以后,只要你能按照咱们约定好的协议进行报文的解析,那你就能正常的处理我发过来的请求,我无论你的开发语言是什么。

image.png

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

业务数据返回后,反序列化的操做究竟是在哪一个线程里面进行的?

是在 IO 线程里面直接解析,仍是被派发到客户端线程池里面进行解析?

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

在线程模型的描述里面,是这样写的:

若是事件处理的逻辑能迅速完成,而且不会发起新的 IO 请求,好比只是在内存中记个标识,则直接在 IO 线程上处理更快,由于减小了线程池调度

但若是事件处理逻辑较慢,或者须要发起新的 IO 请求,好比须要查询数据库,则必须派发到线程池,不然IO 线程阻塞,将致使不能接收其它请求

若是用 IO 线程处理事件,又在事件处理过程当中发起新的 IO 请求,好比在链接事件中发起登陆请求,会报“可能引起死锁”异常,但不会真死锁。

所以,须要经过不一样的派发策略和不一样的线程池配置的组合来应对不一样的场景。

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

image.png

默认的派发策略是 all。

一看到这几个策略,熟悉 Dubbo 的朋友确定就知道了,按照 Dubbo 的尿性,这必须得是一个 SPI 接口啊。

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

image.png

而后官方还给出了一张描述不太清晰的图片:

image.png

图片中的 Dispatch 就是派发策略发挥做用的地方。

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

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

不能够,推不出来的。

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

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

IO 线程。

若是 IO 线程没有解析,那就在客户端线程里面去解析。

根据上面这段话。咱们能提炼出一个关键语句,或者说是需求:咱们如今要实现响应报文能够在不一样的地方进行解析的功能,请问你怎么作?

image.png

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

因此咱们找到这个地方,问题就了然于心了。

咱们去哪里找答案呢?

这个类里面,这个类是一个请求/响应解码的很是核心的类:org.apache.dubbo.rpc.protocol.dubbo.DubboCodec

image.png

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

接下来咱们怎么搞?强撸源码吗?不可能的。直接撸确定费劲。

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

我这里的 Demo 很是简单,服务端接口实现类以下:

image.png

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

image.png

而后 Debug 起来,注意,下面演示的代码没有特别说明的地方,都是 2.7.5 版本。

运行起来后先不看别的,看看当前卡在这个地方,被 Debug 的线程是什么线程:

image.png

到这里你先冷静一下,你想一下这个问题:

在这个方法里面能够对响应和请求进行解析。那它怎么知道当前究竟是响应仍是请求报文呢?

image.png

答案就在前面说的 Dubbo 协议里面:

image.png

呼应上了没有?header 里面第 16 bit 若是是 0 表明响应,若是是 1 表明请求。

你说巧不巧,上面这个方法的入参里面就有一个 header 数组。

让咱们看看他里面装的是什么东西:

image.png

长度是 16,和 header 的长度吻合,可是里面装的玩意仍是没看出来。

可是这样一看,看前两个字节,你就明白了:

image.png

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

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

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

image.png

前面,咱们解决了怎么知道当前究竟是响应仍是请求报文这个问题。

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

image.png

首先,上面标记为①的地方是判断当前数据包是否是一个心跳包,通过 Debug 咱们能够知道这不是一个心跳包:

image.png

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

image.png

从 Debug 的截图里面咱们能够看出,当前的状态为 20,表示正常返回。

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

标记为④的地方,是咱们须要重点关注的地方,也是咱们一直在寻找的代码。

这个地方就很关键了,你们集中注意力了。

image.png

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

image.png

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

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

下面这张图片对 2.7.4.1 和 2.7.5 版本这个地方的代码进行一个对比:

image.png

你仔细看着两个版本之间的代码,发现如出一辙,也没有差别啊。

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

别忘了,上面的代码里面是有一个变量的:

image.png

差别就差别在这个地方。

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

image.png

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

同时这个参数,无论在哪一个版本里面,都是能够配置。虽然基本上也没有人更改过这个配置,配置方法以下:

image.png

朋友们,到这里还跟的上不?跟不上你就再捋捋?别硬看,伤身体。

解码操做源码解析

接下来咱们再看看解码操做的代码究竟是怎么样的。

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

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

image.png

标号为①的地方表明序列化类型是 2 。

2 是什么?看表:

image.png

Hessian2Serialization,就是 Dubbo 默认的序列化器。

标号为②的地方表明本次响应类型为 4。

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

image.png

因此,在标号为③的地方即处理了返回值(handleValue)也处理了上下文信息(handleAttachment)。

handleValue 就不细看了,你就关注这个地方解析出来的就是咱们的响应内容:

image.png

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

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

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

image.png

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

答案就是设置标识位。

由于咱们知道若是是在 IO 线程里面解码,那么该操做调用解码方法后,确定是先于客户端线程池调用的。

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

image.png

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

接下来,我给你们对比一下 decodeBody 方法在 IO 线程里面解码和在客户端线程池里面解码时分别返回什么。也就是这行代码返回的时候:

image.png

这样一对比就很清晰了:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

这样也解释了,为何说是“延迟”到客户端线程池里面解码。

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

这个是响应消息体,是 body。

头呢?header 呢?

image.png

别急,这不是立刻就给你讲一下嘛。

前面讲这个方法的时候说了:header 是做为参数传进来的嘛,那咱们还能够去找一下 header 究竟是怎么传进来的:

image.png

怎么看呢?

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

image.png

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

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

我怀疑前面讲协议的时候你就在走神。别问,问就是协议规定。你们遵照就行了。

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

image.png

上面这个方法里面就是在搞 header 的事情。

其中有一个检查报文长度的方法:checkPayLoad。

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

带你们去源码里面找答案:

image.png

答案是 8M。

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

稍微有点意思的是标号为②的地方,我第一次看的时候愣是看了一分钟没反应过来。主要是前面的这个 payload > 0,我想着这不是废话嘛,长度不都是大于 0 的。兴奋的我觉得发现了一个无用代码呢。

后来才理解到,若是当 payload 设置为负数的时候,就表明不限制报文长度。

能够进行以下配置:

image.png

一个基本上用不到的 Dubbo 小知识点,免费赠送给你们。

好了,header 和 body 都齐活了。

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

因此,对于官网中,红框框起来这个地方的描述是有问题的:http://dubbo.apache.org/zh-cn/docs/user/demos/consumer-threadpool.html

image.png

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


聊聊线程池模型的变化


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

先抛两个知识点:

  • 不管是新老线程池模型,默认的 Dispatch 策略都是 all。全部响应仍是会转发到客户端线程池里面,在这个里面进行解码操做(若是 IO 线程没有解码的话)把结果返回到用户线程中去。

  • 对于线程池客户端的默认实现是 cached,服务端的默认实现是 fixed。

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

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

下面是官网上的截图:

image.png

首先,无论 2.7.5 版本以前仍是以后客户端的默认实现都是 cached ,这个线程池并无限制线程数量:

image.png

因此会出现消费端线程数分配多的问题。

但官网的描述是:分配过多。多和过多还不同。

为何会过多呢?

由于在 2.7.5 版本以前,是每个连接都对应一个客户端线程池。至关于作了连接级别的线程隔离,可是实际上这个线程隔离是没有必要的。反而影响了性能。

而在 2.7.5 版本里面,就是无论你多少连接,你们共用一个客户端线程池,引入了 threadless executor 的概念。

简单的来讲,优化结果就是从多个线程池改成了共用一个线程池。

线程池模型的变化,我在《Dubbo 2.7.5在线程模型上的优化》里面比较详细的聊过了,就不在重复讲了,有兴趣的能够去翻一下。

送点买书福利

若是你们对 Dubbo 学习感兴趣的话,能够去仔细读读官方文档和官方博客。里面写仍是挺全的。

另外也有人叫我推荐一下 Dubbo 相关的书籍,能够看一下这两本,我都看过:

广告

深刻理解Apache Dubbo与实战

做者:诣极

当当

广告

深度剖析Apache Dubbo核心技术内幕(博文视点出品)

做者:翟陆续(加多)

京东

跟着书进行一个系统学习也是不错的。

另外最近买书的话能够看看当当网,最近开学季搞活动,绝大部分书籍都是满 100 减 50 的。

我作为一个小号主,给你们申请了一批实付满 200 减 40 的优惠券,至关于花 160 买 400 元的书。真的很香,我本身也买了几本。数量很少,先到先得。

推荐书单能够看这里:《那些在我文章中出现过的技术书籍

优惠码:WT9HXS

使用渠道:当当小程序或当当APP

使用方法:结算时点击【优惠券/码】

image.png

最后说一句(求关注)

好了,看到了这里安排个一键三连(转发、在看、点赞)吧,周更很累的,不要白嫖我,须要一点正反馈。image.png

才疏学浅,不免会有纰漏,若是你发现了错误的地方,能够在问答区提出来(相似于留言功能了,可把我神气坏了),我对其加以修改。

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

image.png

我是 why,一个被代码耽误的文学创做者,不是大佬,可是喜欢分享,是一个又暖又有料的四川好男人。

还有,重要的事情说三遍:

欢迎关注我呀。

欢迎关注我呀。

欢迎关注我呀。


640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

相关文章
相关标签/搜索