一种心跳,两种设计

栏目: 后端 · 发布时间: 5年前

内容简介:来源:Fate/stay night [Heaven's Feel] lost butterfly在前一篇文章

一种心跳,两种设计

来源:Fate/stay night [Heaven's Feel] lost butterfly

1 前言

在前一篇文章 《聊聊 TCP 长连接和心跳那些事》 中,我们已经聊过了 TCP 中的 KeepAlive,以及在应用层设计心跳的意义,但却对长连接心跳的设计方案没有做详细地介绍。事实上,设计一个好的心跳机制并不是一件容易的事,就我所熟知的几个 RPC 框架,它们的心跳机制可以说大相径庭,这篇文章我将探讨一下 如何设计一个优雅的心跳机制,主要从 Dubbo 的现有方案以及一个改进方案来做分析

2 预备知识

因为后续我们将从源码层面来进行介绍,所以一些服务治理框架的细节还需要提前交代一下,方便大家理解。

2.1 客户端如何得知请求失败了?

高性能的 RPC 框架几乎都会选择使用 Netty 来作为通信层的组件,非阻塞式通信的高效不需要我做过多的介绍。但也由于非阻塞的特性,导致其发送数据和接收数据是一个异步的过程,所以当存在服务端异常、网络问题时,客户端接是接收不到响应的,那我们如何判断一次 RPC 调用是失败的呢?

误区一:Dubbo 调用不是默认同步的吗?

Dubbo 在通信层是异步的,呈现给使用者同步的错觉是因为内部做了阻塞等待,实现了异步转同步。

误区二: Channel.writeAndFlush 会返回一个 channelFuture ,我只需要判断 channelFuture.isSuccess 就可以判断请求是否成功了。

注意,writeAndFlush 成功并不代表对端接受到了请求,返回值为 true 只能保证写入网络缓冲区成功,并不代表发送成功。

避开上述两个误区,我们再来回到本小节的标题:客户端如何得知请求失败? 正确的逻辑应当是以客户端接收到失败响应为判断依据 。等等,前面不还在说在失败的场景中,服务端是不会返回响应的吗?没错,既然服务端不会返回,那就只能客户端自己造了。

一个常见的设计是:客户端发起一个 RPC 请求,会设置一个超时时间 client_timeout ,发起调用的同时,客户端会开启一个延迟 client_timeout 的定时器

  • 接收到正常响应时,移除该定时器。

  • 定时器倒计时完毕,还没有被移除,则认为请求超时,构造一个失败的响应传递给客户端。

Dubbo 中的超时判定逻辑:

主要逻辑涉及的类: DubboInvokerHeaderExchangeChannelDefaultFuture ,通过上述代码,我们可以得知一个细节,无论是何种调用,都会经过这个定时器的检测, 超时即调用失败,一次 RPC 调用的失败,必须以客户端收到失败响应为准

2.2 心跳检测需要容错

网络通信永远要考虑到最坏的情况,一次心跳失败,不能认定为连接不通,多次心跳失败,才能采取相应的措施。

2.3 心跳检测不需要忙检测

忙检测的对立面是空闲检测,我们做心跳的初衷,是为了保证连接的可用性,以保证及时采取断连,重连等措施。如果一条通道上有频繁的 RPC 调用正在进行,我们不应该为通道增加负担去发送心跳包。 心跳扮演的角色应当是晴天收伞,雨天送伞。

3 Dubbo 现有方案

本文的源码对应 Dubbo 2.7.x 版本,在 apache 孵化的该版本中,心跳机制得到了增强。

介绍完了一些基础的概念,我们便来看看 Dubbo 是如何设计应用层心跳的。Dubbo 的心跳是双向心跳,客户端会给服务端发送心跳,反之,服务端也会向客户端发送心跳。

3.1 连接建立时创建定时器

<1> 默认开启心跳检测的定时器

<2> 创建了一个 HashWheelTimer 开启心跳检测 ,这是 Netty 所提供的一个经典的时间轮定时器实现,至于它和 jdk 的实现有何不同,不了解的同学也可以关注下,我就拓展了。

不仅 HeaderExchangeClient 客户端开起了定时器, HeaderExchangeServer 服务端同样开起了定时器,由于服务端的逻辑和客户端几乎一致,所以后续我并不会重复粘贴服务端的代码。

Dubbo 在早期版本版本中使用的是 shedule 方案,在 2.7.x 中替换成了 HashWheelTimer。

3.2 开启两个定时任务

Dubbo 在 startHeartbeatTimer 方法中主要开启了两个定时器: HeartbeatTimerTaskReconnectTimerTask

<1> HeartbeatTimerTask 主要用于定时发送心跳请求

<2> ReconnectTimerTask 主要用于心跳失败之后处理重连,断连的逻辑

至于方法中的其他代码,其实也是本文的重要分析内容,先容我卖个关子,后面再来看追溯。

3.3 定时任务一:发送心跳请求

详细解析下心跳检测定时任务的逻辑 HeartbeatTimerTask#doTask

前面已经介绍过, Dubbo 采取的是设计是双向心跳 ,即服务端会向客户端发送心跳,客户端也会向服务端发送心跳,接收的一方更新 lastRead 字段,发送的一方更新 lastWrite 字段,超过心跳间隙的时间,便发送心跳请求给对端。这里的 lastRead/lastWrite 同样会被同一个通道上的普通调用更新,通过更新这两个字段,实现了只在连接空闲时才会真正发送空闲报文的机制,符合我们一开始科普的做法。

注意:不仅仅心跳请求会更新 lastRead 和 lastWrite,普通请求也会。这对应了我们预备知识中的空闲检测机制。

3.4 定时任务二:处理重连和断连

继续研究下重连和断连定时器都实现了什么 ReconnectTimerTask#doTask

第二个定时器则负责根据客户端、服务端类型来对连接做不同的处理,当超过设置的心跳总时间之后,客户端选择的是重新连接,服务端则是选择直接断开连接。这样的考虑是合理的,客户端调用是强依赖可用连接的,而服务端可以等待客户端重新建立连接。

细心的朋友会发现,这个类被命名为 ReconnectTimerTask 是不太准确的,因为它处理的是重连和断连两个逻辑。

3.5 定时不精确的问题

在 Dubbo 的 issue 中曾经有人反馈过定时不精确的问题,我们来看看是怎么一回事。

Dubbo 中默认的心跳周期是 60s,设想如下的时序:

  • 第 0 秒,心跳检测发现连接活跃

  • 第 1 秒,连接实际断开

  • 第 60 秒,心跳检测发现连接不活跃

由于时间窗口的问题,死链不能够被及时检测出来,最坏情况为一个心跳周期。

为了解决上述问题,我们再倒回去看一下上面的 startHeartbeatTimer() 方法

其中 calculateLeastDuration 根据心跳时间和超时时间分别计算出了一个 tick 时间,实际上就是将两个变量除以了 3,使得他们的值缩小,并传入了 HashWeelTimer 的第二个参数之中

tick 的含义便是定时任务执行的频率。这样,通过减少检测间隔时间,增大了及时发现死链的概率,原先的最坏情况是 60s,如今变成了 20s。这个频率依旧可以加快,但需要考虑资源消耗的问题。

定时不准确的问题出现在 Dubbo 的两个定时任务之中,所以都做了 tick 操作。事实上,所有的定时检测的逻辑都存在类似的问题。

3.6 Dubbo 心跳总结

Dubbo 对于建立的每一个连接,同时在客户端和服务端开启了 2 个定时器,一个用于定时发送心跳,一个用于定时重连、断连,执行的频率均为各自检测周期的 1/3。定时发送心跳的任务负责在连接空闲时,向对端发送心跳包。定时重连、断连的任务负责检测 lastRead 是否在超时周期内仍未被更新,如果判定为超时,客户端处理的逻辑是重连,服务端则采取断连的措施。

先不急着判断这个方案好不好,再来看看改进方案是怎么设计的。

4 Dubbo 改进方案

实际上我们可以更优雅地实现心跳机制,本小节开始,我将介绍一个新的心跳机制。

4.1 IdleStateHandler 介绍

Netty 对空闲连接的检测提供了天然的支持,使用 IdleStateHandler 可以很方便的实现空闲检测逻辑。

  • readerIdleTime:读超时时间

  • writerIdleTime:写超时时间

  • allIdleTime:所有类型的超时时间

IdleStateHandler 这个类会根据设置的超时参数,循环检测 channelRead 和 write 方法多久没有被调用。当在 pipeline 中加入 IdleSateHandler 之后,可以在此 pipeline 的任意 Handler 的 userEventTriggered 方法之中检测 IdleStateEvent 事件,

为什么需要介绍 IdleStateHandler 呢?其实提到它的空闲检测 + 定时的时候,大家应该能够想到了,这不天然是给心跳机制服务的吗?很多服务治理框架都选择了借助 IdleStateHandler 来实现心跳。

IdleStateHandler 内部使用了 eventLoop.schedule(task) 的方式来实现定时任务,使用 eventLoop 线程的好处是还同时保证了 线程安全 ,这里是一个小细节。

4.2 客户端和服务端配置

首先是将 IdleStateHandler 加入 pipeline 中。

客户端:

服务端:

客户端配置了 read 超时为 60s,服务端配置了 write/read 超时为 200s,先在此埋下两个伏笔:

  1. 为什么客户端和服务端配置的超时时间不一致?

  2. 为什么客户端检测的是读超时,而服务端检测的是读写超时?

4.3 空闲超时逻辑 — 客户端

对于空闲超时的处理逻辑,客户端和服务端是不同的。首先来看客户端

检测到空闲超时之后,采取的行为是向服务端发送心跳包,具体是如何发送,以及处理响应的呢?伪代码如下

行为并不复杂,构造一个心跳包发送到服务端,接受响应结果

  • 响应成功,清空请求失败标记

  • 响应失败,心跳失败标记+1,如果超过配置的失败次数,则重新连接

不仅仅是心跳,普通请求返回成功响应时也会清空标记

4.4 空闲超时逻辑 — 服务端

服务端处理空闲连接的方式非常简单粗暴,直接关闭连接。

4.5 改进方案心跳总结

  1. 为什么客户端和服务端配置的超时时间不一致?

    因为客户端有重试逻辑,不断发送心跳失败 n 次之后,才认为是连接断开;而服务端是直接断开,留给服务端时间得长一点。60 * 3 < 200 还说明了一个问题,双方都拥有断开连接的能力,但连接的创建是由客户端主动发起的,那么客户端也更有权利去主动断开连接。

  2. 为什么客户端检测的是读超时,而服务端检测的是读写超时?

    这其实是一个心跳的共识了,仔细思考一下,定时逻辑是由客户端发起的,所以整个链路中不通的情况只有可能是:服务端接收,服务端发送,客户端接收。也就是说,只有客户端的 pong,服务端的 ping,pong 的检测是有意义的。

主动追求别人的是你,主动说分手的也是你。

利用 IdleStateHandler 实现心跳机制可以说是十分优雅的,借助 Netty 提供的空闲检测机制,利用客户端维护单向心跳,在收到 3 次心跳失败响应之后,客户端断开连接,交由异步线程重连,本质还是表现为客户端重连。服务端在连接空闲较长时间后,主动断开连接,以避免无谓的资源浪费。

5 心跳设计方案对比

一种心跳,两种设计

私下请教过 美团点评的长连接负责人:俞超(闪电侠) ,美点使用的心跳方案和 Dubbo 改进方案几乎一致,可以该方案是标准实现了。

6 Dubbo 实际改动点建议

鉴于 Dubbo 存在一些其他通信层的实现,所以可以保留现有的定时发送心跳的逻辑。

  • 建议改动点一:

双向心跳的设计是不必要的,兼容现有的逻辑,可以让客户端在连接空闲时发送单向心跳,服务端定时检测连接可用性。定时时间尽量保证:客户端超时时间 * 3 ≈ 服务端超时时间

  • 建议改动点二:

去除处理重连和断连的定时任务,Dubbo 可以判断心跳请求是否响应失败,可以借鉴改进方案的设计,在连接级别维护一个心跳失败次数的标记,任意响应成功,清除标记;连续心跳失败 n 次,客户端发起重连。这样可以减少一个不必要的定时器,任何轮询的方式,都是不优雅的。

最后再聊聊可扩展性这个话题。其实我是建议把定时器交给更加底层的 Netty 去做,也就是完全使用 IdleStateHandler ,其他通信层组件各自实现自己的空闲检测逻辑,但是 Dubbo 中 mina,grizzy 的兼容问题囿住了我的拳脚,但试问一下,如今的 2019 年,又有多少人在使用 mina 和 grizzy?因为一些不太可能用的特性,而限制了主流用法的优化,这肯定不是什么好事。抽象,功能,可扩展性并不是越多越好,开源产品的人力资源是有限的,框架使用者的理解能力也是有限的,能解决大多数人问题的设计,才是好的设计。哎,mina、grizzy,学不动了。

欢迎关注我的微信公众号:「Kirito的技术分享」,关于文章的任何疑问都会得到回复,带来更多 Java 相关的技术分享。

一种心跳,两种设计


以上所述就是小编给大家介绍的《一种心跳,两种设计》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Cyberwar

Cyberwar

Kathleen Hall Jamieson / Oxford University Press / 2018-10-3 / USD 16.96

The question of how Donald Trump won the 2016 election looms over his presidency. In particular, were the 78,000 voters who gave him an Electoral College victory affected by the Russian trolls and hac......一起来看看 《Cyberwar》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具