乐趣区

关于java:面试官要不我们聊一下心跳的设计

你好呀,我是歪歪。

是这样的,我最近又看到了这篇文章《工商银行分布式服务 C10K 场景解决方案
》。

为什么是又呢?

因为这篇文章最开始公布的时候我就看过了,过后就感觉写得挺好的,宇宙行(工商银行)果然是很叼的样子。

然而看过了也就看过了,过后没去细推敲。

这次看到的时候,刚好是在上班路上,就仔仔细细的又看了一遍。

嗯,常读常新,还是很有播种的。

所以写篇文章,给大家汇报一下我再次浏览之后的一下播种。

文章提要

我晓得很多同学应该都没有看过这篇文章,所以我先放个链接,[《工商银行分布式服务 C10K 场景解决方案
》](https://mp.weixin.qq.com/s/qc…)。

先给大家提炼一下文章的内容,然而如果你有工夫的话,也能够先去细细的读一下这篇文章,感受一下宇宙行的实力。

文章内容大略是这样的。

在宇宙行的架构中,随着业务的倒退,在可预感的将来,会呈现一个提供方为数千个、甚至上万个生产方提供服务的场景。

在如此高负载量下,若服务端程序设计不够良好,网络服务在解决数以万计的客户端连贯时、可能会呈现效率低下甚至齐全瘫痪的状况,即为 C10K 问题。

C10K 问题就不开展讲了,网上查一下,十分驰名的程序相干问题,只不过该问题曾经成为历史了。

而宇宙行的 RPC 框架应用的是 Dubbo,所以他们那篇文章就是基于这个问题去开展的:

基于 Dubbo 的分布式服务平台是否应答简单的 C10K 场景?

为此,他们搭建了大规模连贯环境、模仿服务调用进行了一系列摸索和验证。

首先他们应用的 Dubbo 版本是 2.5.9。版本的确有点低,然而银行嘛,懂的都懂,架构降级是能不动就不动,稳当运行才是王道。

在这个版本外面,他们搞了一个服务端,服务端的逻辑就是 sleep 100ms,模仿业务调用,部署在一台 8C16G 的服务器上。

对应的生产方配置服务超时工夫为 5s,而后把生产方部署在数百台 8C16G 的服务器上(我滴个乖乖,数百台 8C16G 的服务器,这都是白花花的银子啊,有钱真好),以容器化形式部署 7000 个服务生产方。

每个生产方启动后每分钟调用 1 次服务。

而后他们定制了两个测试的场景:

.png)

场景 2 先临时不说,异样是必然的,因为只有一个提供方嘛,重启期间生产方还在发申请,这必然是要凉的。

然而场景 1 按理来说不应该的啊。

你想,生产方配置的超时工夫是 5s,而提供方业务逻辑只解决 100ms。再怎么说工夫也是够够的了。

须要额定多说一句的是:本文也只聚焦于场景 1。

然而,敌人们,然而啊。

尽管调用方一分钟发一次申请的频率不高,然而架不住调用方有 7000 个啊,这 7000 个调用方,这就是传说中的突发流量,只是这个“突发”是每分钟一次。

所以,偶现超时也是能够了解的,毕竟服务端解决能力无限,有工作在队列外面略微等等就超时了。

能够写个小例子示意一下,是这样的:

就是搞个线程池,线程数是 200。而后提交 7000 个工作,每个工作耗时 100ms,用 CountDownLatch 模仿了一下并发,在我的 12 核的机器上运行耗时 3.8s 的样子。

也就是说如果在 Dubbo 的场景下,每一个申请再加上一点点网络传输的工夫,一点点框架外部的耗费,这一点点工夫再乘以 7000,最初被执行的工作实践上来说,是有可能超过 5s 的。

所以偶现超时是能够了解的。

然而,敌人们,又来然而了啊。

我后面都说的是实践上,然而实际才是测验真谛的惟一方法。

看一下宇宙行的验证后果:

首先咱们能够看到生产方不论是发动申请还是解决响应都是十分迅速的,然而卡壳就卡在服务方从收到申请到解决申请之间。

通过抓包剖析,他们得出结论: 导致交易超时的起因不在生产方侧,而在提供方侧。

这个论断其实也很好了解,因为压力都在服务提供方这边,所以阻塞也应该是在它这里。

其实到这里咱们基本上就能够确认,必定是 Dubbo 框架外面的某一些操作导致了耗时的减少。

难的就是定位到,到底是什么操作呢?

宇宙行通过一系列操作,通过周密的剖析,得出了一个论断:

心跳密集导致 netty worker 线程繁忙,从而导致交易耗时增长。

也就是论断中提到的这一点:

有了论断,找到了病灶就好办了,隔靴搔痒嘛。

因为后面说过,本文只聚焦于场景一,所以咱们看一下对于场景一宇宙行给出的解决方案:

全都是围绕着心跳的优化解决,解决实现后的成果如下:

其中成果最显著的操作是“心跳绕过序列化”。

生产方与提供方之间均匀解决时差由 27ms 升高至 3m,晋升了 89%。

前 99% 的交易耗时从 191ms 降落至 133ms,晋升了 30%。

好了,写到这,就差不多是把那篇文章外面我过后看到的一些货色复述了一遍,没啥大养分。

只是我还记得第一次看到这篇文章的时候,我是这样的:

我感觉挺牛逼的,一个小小的心跳,在 C10K 的场景下居然演变成了一个性能隐患。

我得去钻研一下,顺便宇宙行给出的计划中最重要的是“心跳绕过序列化”,我还得去钻研一下 Dubbo 怎么去实现这个性能,钻研明确了这玩意就是我的了啊。

然而 …

我遗记过后为啥没去看了,然而没关系,我当初想起来了嘛,马上就开始钻研。

心跳如何绕过序列化

我是怎么去钻研呢?

间接往源码外面冲吗?

是的,就是往源码外面冲。

然而冲之前,我先去 Dubb 的 github 上逛了一圈:

https://github.com/apache/dubbo

而后在 Pull request 外面先搜寻了一下“Heartbeat”,这一搜还搜出不少好货色呢:

我一眼看到这两个 pr 的时候,眼睛都在放光。

好家伙,我原本只是想轻易看看,没想到间接定位了我要钻研的货色了。

我只须要看看这两个 pr,就晓得是怎么实现的“心跳绕过序列化”,这间接就让我少走了很多弯路。

首先看这个:

https://github.com/apache/dub…

从这段形容中能够晓得,我找到对的中央了。而从他的形容中晓得“心跳跳过序列化”,就是用 null 来代替了序列化的这个过程。

同时这个 pr 外面还阐明了本人的革新思路:

接着就带大家看一下这一次提交的代码。

怎么看呢?

能够在 git 上看到他对应这次提交的文件:

到源码外面找到对应中央即可,这也是一个去找源码的办法。

我比拟相熟 Dubbo 框架,不看这个 pr 我也大略晓得去哪里找对应的代码。然而如果换成另外一个我不相熟的框架呢?

从它的 git 动手其实是一个很好的角度。

一个翻阅源码的小技巧,送给你。

如果你不理解 Dubbo 框架也没有关系,咱们只是聚焦于“心跳是如何跳过序列化”的这一个点。至于心跳是由谁如何在什么工夫发动的,这一节临时不讲。

接着,咱们从这个类下手:

org.apache.dubbo.rpc.protocol.dubbo.DubboCodec

从提交记录能够看出次要有两处改变,且两处改变的代码是截然不同的,都位于 decodeBody 这个办法,只是一个在 if 分支,一个在 else 分支:

这个代码是干啥的?

你想一个 RPC 调用,必定是波及到报文的 encode(编码) 和 decode(解码) 的,所以这里次要就是对申请和响应报文进行 decode。

一个心跳,一来一回,一个申请,一个响应,所以有两处改变。

所以我带着大家看申请包这一处的解决就行了:

能够看到代码革新之后,对心跳包进行了一个非凡的判断。

在心跳事件非凡解决外面波及到两个办法,都是本次提交新增的办法。

第一个办法是这样的:

org.apache.dubbo.remoting.transport.CodecSupport#getPayload

就是把 InputStream 流转换成字节数组,而后把这个字节数组作为入参传递到第二个办法中。

第二个办法是这样的:

org.apache.dubbo.remoting.transport.CodecSupport#isHeartBeat

从办法名称也晓得这是判断申请是不是心跳包。

怎么去判断它是心跳包呢?

首先得看一下发动心跳的中央:

org.apache.dubbo.remoting.exchange.support.header.HeartbeatTimerTask#doTask

从发动心跳的中央咱们能够晓得,它收回去的货色就是 null。

所以在承受包的中央,判断其内容是不是 null,如果是,就阐明是心跳包。

通过这简略的两个办法,就实现了心跳跳过序列化这个操作,晋升了性能。

而下面两个办法都是在这个类中,所以外围的改变还是在这个类,然而改变点其实也不算多:

org.apache.dubbo.remoting.transport.CodecSupport

在这个类外面有两个小细节,能够带大家再看看。

首先是这里:

这个 map 外面缓存的就是不同的序列化的形式对应的 null,代码干的也就是作者这里说的这件事儿:

另外一个细节是看这个类的提交记录:

还有一次优化性的提交,而这一次提交的内容是这样的。

首先定义了一个 ThreadLocal,并使其初始化的时候是 1024 字节:

那么这个 ThreadLocal 是用在哪儿的呢?

在读取 InputStream 的时候,须要开拓一个字节数组,为了防止频繁的创立和销毁这个字节数据,所以搞了一个 ThreadLocal:

有的同学看到这里就要问了:为什么这个 ThreadLocal 没有调用 remove 办法呢,不会内存透露嘛?

不会的,敌人们,在 Dubbo 外面执行这个玩意的是 NIO 线程,这个线程是能够复用的,且外面只是放了一个 1024 的字节数组,不会有脏数据,所以不须要移除,间接复用。

正是因为能够复用,所以才晋升了性能。

这就是细节,魔鬼都在细节外面。

这一处细节,就是后面提到的另外一个 pr:

https://github.com/apache/dub…

看到这里,咱们也就晓得了宇宙行到底是怎么让心跳跳过序列化操作了,其实也没啥简单的代码,几十行代码就搞定了。

然而,敌人们,又要然而了。

写到这里的时候,我忽然感觉到不太对劲。

因为我之前写过这篇文章,Dubbo 协定那点破事。

在这篇文章外面有这样的一个图:

这是过后在官网上截下来的。

在协定外面,事件标识字段之前只有 0 和 1。

然而当初不一样了,从代码看,是把 1 的范畴给扩充了,它不肯定代表的是心跳,因为这外面有个 if-else

所以,我就去看了一下当初官网上对于协定的形容。

https://dubbo.apache.org/zh/d…

果然,产生了变动:

并不是说 1 就是心跳包,而是改口为:1 可能是心跳包。

谨严,这就是谨严。

所以开源我的项目并不是代码改完就改完了,还要思考到一些周边信息的保护。

心跳的多种设计方案

在钻研 Dubbo 心跳的时候,我还找到了这样一个 pr。

https://github.com/apache/dub…

题目是这样的:

翻译过去就是应用 IdleStateHandler 代替应用 Timer 发送心跳的倡议。

我定睛一看,好机会,这不是 95 后老徐嘛,老熟人了。

看一下老徐是怎么说的,他倡议具体是这样的:

几位 Dubbo 大佬,在这个 pr 外面替换了很多想法,我认真的浏览之后都受益匪浅。

大家也能够点进去看看,我这里给大家汇报一下本人的播种。

首先是几位老哥在心跳实时性上的一顿 battle。

总之,大家晓得 Dubbo 的心跳检测是有肯定延时的,因为是基于工夫轮做的,相当于是定时工作,触发的时效性是不能保障实时触发的。

这玩意就相似于你有一个 60 秒执行一次的定时工作,在第 0 秒的时候工作启动了,在第 1 秒的时候有一个数据筹备好了,然而须要期待下一次工作触发的时候才会被解决。因而,你解决数据的最大提早就应该是 60 秒。

这个大家应该能明确过去。

额定说一句,下面探讨的后果是“目前是 1/4 的 heartbeat 延时”,然而我去看了一下最新的 master 分支的源码,怎么感觉是 1/3 的延时呢:

从源码里能够看到,计算工夫的时候 HEARTBEAT_CHECK_TICK 参数是 3。所以我了解是 1/3 的延时。

然而不重要,这不重要,反正你晓得是有延时的就行了。

而 kexianjun 老哥认为如果基于 netty 的 IdleStateHandler 去做,每次检测超时都从新计算下一次检测的工夫,因而相对来说就能比拟及时的查看到超时了。

这是在实时性上的一个优化。

而老徐感觉,除了实时性这个思考外,其实 IdleStateHandler 更是一个针对心跳的优雅的设计。然而呢,因为是基于 Netty 的,所以当通信框架应用的不是 Netty 的时候,就回天无力了,所以能够保留 Timer 的设计来应答这种状况。

很快,carryxyh 老哥就给出了很有建设性的意见:

因为 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 心跳的演变过程,其中也波及到局部的源码。

最终他给出了这样的一个图,心跳设计方案比照:

而后,是这段话:

.png)

老徐是在阿里搞中间件的,原来搞中间件的人每天想的是这些事件。

有点意思。

看看代码

带大家看一下代码,然而不会做详细分析,相当于是指个路,如果想要深刻理解的话,本人翻源码去。

首先是这里:

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/se…

第二个是极客工夫《从 0 开始学微服务》,第 17 讲外面,老师在对于心跳这块的一点分享,提到的一个爱护机制,这是我之前没有想到过的:

反正我是感觉,我文章中提到的这一些链接,你都去仔仔细细的看了,那么对于心跳这块的货色,也就把握的七七八八了,够用了。

好了,就到这吧。

本文已收录至集体博客,欢送大家来玩。

https://www.whywhy.vip/

退出移动版