由于 Dubbo 是支持多个通讯框架的。
这里说的“多个”,其实不提我都忘记了,除了 Netty 之外,它还支持 Girzzly 和 Mina 这两种底层通讯框架,而且还支持自定义。
但是我寻思都 2021 年了,Girzzly 和 Mina 还有人用吗?
从源码中我们也能找到它们的影子:
org.apache.dubbo.remoting.transport.AbstractEndpoint
Girzzly、Mina 和 Netty 都各有自己的 Server 和 Client。
其中 Netty 有两个版本,是因为 Netty4 步子迈的有点大,难以在之前的版本中进行兼容,所以还不如直接多搞一个实现。
但是不管它怎么变,它都还是叫做 Netty。
好了,说回前面的建设性意见。
如果是采用 IdleStateHandler 的方式做心跳,而其他的通讯框架保持 Timer 的模式,那么势必会出现类似于这样的代码:
if transport == netty { don't start heartbeat timer }
这是一个开源框架中不应该出现的东西,因为会增加代码复杂度。
所以,他的建议是最好还是使用相同的方式来进行心跳检测,即都用 Timer 的模式。
正当我觉得这个哥们说的有道理的时候,我看了老徐的回答,我又瞬间觉得他说的也很有道理:
我觉得上面不需要我解释了,大家边读边思考就行了。
接着看看 carryxyh 老哥的观点:
这个时候对立面就出现了。
老徐的角度是,心跳肯定是要有的,只是他觉得不同通讯框架的实现方式可以不必保持一致(现在都是基于 Timer 时间轮的方式),他并不认为 Timer 抽象成一个统一的概念去实现连接保活是一个优雅的设计。
在 Dubbo 里面我们主要用的就是 Netty,而 Netty 的 IdleStateHandler 机制,天生就是拿来做心跳的。
所以,我个人认为,是他首先觉得使用 IdleStateHandler 是一种比较优雅的实现方式,其次才是时效性的提升。
但是 carryxyh 老哥是觉得 Timer 抽象的这个定时器,是非常好的设计,因为它的存在,我们才可以不关心底层是netty还是mina,而只需要关心具体实现。
而对于 IdleStateHandler 的方案,他还是认为在时效性上有优势。但是我个人认为,他的想法是如果真的有优势的话,我们可以参考其实现方式,给其他通讯框架也赋能一个 “Idle” 的功能,这样就能实现大统一。
看到这里,我觉得这两个老哥 battle 的点是这样的。
首先前提是都围绕着“心跳”这个功能。
一个认为当使用 Netty 的时候“心跳”有更好的实现方案,且 Netty 是 Dubbo 主要的通讯框架,所以应该可以只改一下 Netty 的实现。
一个认为“心跳”的实现方案应该统一,如果 Netty 的 IdleStateHandler 方案是个好方案,我们应该把这个方案拿过来。
我觉得都有道理,一时间竟然不知道给谁投票。
但是最终让我选择投老徐一票的,是看了他写的这篇文章:《一种心跳,两种设计》。
这篇文章里面他详细的写了 Dubbo 心跳的演变过程,其中也涉及到部分的源码。
最终他给出了这样的一个图, 心跳设计方案对比:
然后,是这段话:
老徐是在阿里搞中间件的,原来搞中间件的人每天想的是这些事情。
有点意思。
看看代码
带大家看一下代码,但是不会做详细分析,相当于是指个路,如果想要深入了解的话,自己翻源码去。
首先是这里:
org.apache.dubbo.remoting.exchange.support.header.HeaderExchangeClient
可以看到在 HeaderExchangeClient 的构造方法里面调用了 startHeartBeatTask 方法来开启心跳。
同时这里面有个 HashedWheelTimer,这玩意我熟啊,时间轮嘛,之前分析过的。
然后我们把目光放在这个方法 startHeartBeatTask:
这里面就是构建心跳任务,然后扔到时间轮里面去跑,没啥复杂的逻辑。
这一个实现,就是 Dubbo 对于心跳的默认处理。
但是需要注意的是,整个方法被 if 判断包裹了起来,这个判断可是大有来头,看名字叫做 canHandleIdle,即是否可以处理 idle 操作,默认是 false:
所以,前面的 if 判断的结果是 true。
那么什么情况下 canHandleIdle 是 true 呢?
在使用 Netty4 的时候是 true。
也就是 Netty4 不走默认的这套心跳实现。
那么它是怎么实现的呢?
由于服务端和客户端的思路是一样的,所以我们看一下客户端的代码就行。
关注一下它的 doOpen 方法:
org.apache.dubbo.remoting.transport.netty4.NettyClient#doOpen
在 pipeline 里面加入了我们前面说到的 IdleStateHandler 事件,这个事件就是如果 heartbeatInterval 毫秒内没有读写事件,那么就会触发一个方法,相当于是一个回调。
heartbeatInterval 默认是 6000,即 60s。
然后加入了 nettyClientHandler,它是干什么呢?
看一眼它的这个方法:
org.apache.dubbo.remoting.transport.netty4.NettyClientHandler#userEventTriggered
这个方法里面在发送心跳事件。
也就是说你这样写,含义是在 60s 内,客户端没有发生读写时间,那么 Netty 会帮我们触发 userEventTriggered 方法,在这个方法里面,我们可以发送一次心跳,去看看服务端是否正常。
从目前的代码来看, Dubbo 最终是采用的老徐的建议,但是默认实现还是没变,只是在 Netty4 里面采用了 IdleStateHandler 机制。
这样的话,其实我就觉得更奇怪了。
同样是 Netty,一个采用的是时间轮,一个采用的 IdleStateHandler。
同时我也很理解,步子不能迈的太大了,容易扯着蛋。
但是,在翻源码的过程中,我发现了一个代码上的小问题。
org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#decode(org.apache.dubbo.remoting.Channel, org.apache.dubbo.remoting.buffer.ChannelBuffer, int, byte[])
在上面这个方法中,有两行代码是这样的:
你先别管它们是干啥的,我就带你看看它们的逻辑是怎么样的:
可以看到两个方法都执行了这样的逻辑:
int payload = getPayload(channel); boolean overPayload = isOverPayload(payload, size);
如果 finishRespWhenOverPayload 返回的不是 null,没啥说的,返回 return 了,不会执行 checkPayload 方法。
如果 finishRespWhenOverPayload 返回的是 null,则会执行 checkPayload 方法。
这个时候会再次做检查报文大小的操作,这不就重复了吗?
所以,我认为这一行的代码是多余的,可以直接删除。
你明白我意思吧?
又是一个给 Dubbo 贡献源码的机会,送给你,可以冲一波。
最后,再给大家送上几个参考资料。
第一个是可以去了解一下 SOFA-RPC 的心跳机制。 SOFA-PRC 也是阿里开源出来的框架。
在心跳这块的实现就是完完全全的基于 IdleStateHandler 来实现的。
可以去看一下官方提供的这两篇文章:
https://www.sofastack.tech/search/?page=1&query=%E5%BF%83%E8%B7%B3&type=all
第二个是极客时间《从0开始学微服务》,第 17 讲里面,老师在关于心跳这块的一点分享,提到的一个保护机制,这是我之前没有想到过的:
反正我是觉得,我文章中提到的这一些链接,你都去仔仔细细的看了,那么对于心跳这块的东西,也就掌握的七七八八了,够用了。
好了,就到这吧。
本文已收录至个人博客,欢迎大家来玩。