先看一下
心跳包(英语:Heartbeat)在
简而言之心跳机制是用于检测对端存活的一种常用方式。
有点类似icu里面的心跳检测机(服务端),你的心脏(客户端)跳一下,他就更新一下状态,认为你还活着,你要太长时间没跳,他就认为你已经不行了,然后发出bi 的警告。
在网络中,心跳的作用是,在一种需要对端保持连接的状态中,并且存在无法通过上一次的请求判断当前的状态(虽然心跳也不能保证下一次发送成功,但是现实是,我上一次请求是12h前发的,所以你现在还在不在),心跳包可以单独检测对端的存活状态,从而防止发送无用的数据包,另外在分布式系统中,可以避免将数据发送到不可用的节点上(这是比较麻烦的,我成功地把包发到一个不可用的节点上,他给我反错误了,我怎么办,发给其他节点吗,会造成双花吗?)
当然心跳机制不会完美地解决上面的这些问题(毕竟我也不能保证这一秒你的心还在跳,下一秒你就一定还活着),在高可用的系统中还是需要设计另外的机制来防止双花。
另外值得说的一点是:我们不能在网络中发送过多的心跳包,因为在很多时候,网络也是一直有限的资源(心跳虽好,可不要贪杯o),当然也有设计感觉网络情况动态调整的心跳机制,不过那就涉及一些网络底层的东西了。
说到常见的心跳包,就不得不说tcp keepalive机制了
依然是wiki:
存活时长(英语:Keepalive time)即空闲时,两次传输存活包的持续时间。TCP存活包时长可手动配置,默认不少于2个小时。
存活间隔(英语:Keepalive interval)即未收到上个存活包时,两次连续传输存活包的时间间隔。
存活重试次数(英语:Keepalive retry)即在判断远程主机不可用前的发送存活包次数。当两个主机透过TCP/IP协议相连时,TCP存活包可用于判断连接是否可用,并按需中断。
多数支持TCP协议的主机也同时支持TCP存活包。每个主机按一定周期向其他主机发送TCP包来请求回应。若发送主机未收到特定主机的回应(ACK),则将从发送主机一侧中断连接。 若其他主机在连接关闭后发送TCP存活包,关闭连接的一方将发送RST包来表明旧连接已不可用。其他主机将关闭它一侧的连接以新建连接。
空闲的TCP连接通常会每隔45秒或60秒发送一次存活包。在未连续收到三次ACK包时,连接将中断。此行为因主机而异,如默认情况下的Windows主机将在7200000ms(2小时)后发送首个存活包,随后再以1000ms的间隔发送5个存活包。若任意存活包未收到回应,连接将被中断。
keepalive作为最基础的心跳机制,其设计已经融入tcp协议中了。
wireshark keepalive捉包分析
TLS心跳原理
简单来讲TLS心跳拓展主要解决的是在tls链路中,判断对方存活需要进行一次tls协商(这是比较费时),这个心跳拓展的主要目的是通过一个简单的心跳过程来保留tls链路的存活,在之前是使用tcp的keepalive来做的,但tcp的keepalive只能保证tcp链路的可用性。
看完这篇rfc,有两个比较有意思的点
对于每个心跳包,我们需要给他一个payload,而服务端返回的时候需要原封不动的返回这个payload。这么做我猜测是外来解决网络超时的问题,防止我受到之前的包
不需要时时刻刻的发送心跳包,感觉rfc的定义,我们只需要在网络空闲的时候发送心跳包,而在链路中有请求的情况下则不需要发送请求包。
IM系统(通讯系统)中的心跳机制主要是获取用户在线状态,以及向用户推送数据用
与前面两种心跳类似,不过IM系统需要面对一个麻烦的东西 -- NAT(当然TLS的心跳也有考虑NAT的因素)。对于IM系统,本质是是一个C/S架构的系统,而大部分的C都是没有独立IP的,与server通讯,全靠NAT分配的临时IP与端口,而NAT的反配权又不是C端掌控的,实际上是运营商在控制NAT的分配与释放。同时IM系统中一般C的数量会是S数量的几千-几万倍,维护心跳状态将会耗费大量的资源,不过值得庆幸的是,IM实际上是一种弱可用的系统,服务端不需要对客户端的心跳做出反应,也不需要向客户端发送心跳包,有点类似于UDP,客服端发出去就不管了。当然IM的心跳机制还是颇为复杂的,而他的复杂也不是我想要了解的信息,所以这里只给出一片作者认为还可以的
上面谈到的心跳机制基本上都是网络层面的心跳机制,更多的是确认一个链路是否还可用。当然我们可以把这个链路再抽象一下,比如在一个对等网络中,你连接了某个数据库的资源,我连接了另一个数据库的资源,而我们需要保证相互之间 到数据库的链路是连通的。那么这就不是简单的tcp keepalive这种模式能解决的问题了。我们没法在网络传输模块完成整个心跳过程,网络层甚至不知道数据库是什么,所以这个请求必须上抛到应用层,而应用层在根据自身情况,去找数据库那状态(这里有个情况,为什么不让数据库也跳起来,主要是浪费资源,心跳是检测活性,如果没有client,实际上也用不到数据库的活性,对于心跳包中负责的请求,应该被动的等待需要的时候在去操作,而不是在这一边主动那自己的状态,推上去)。
当然这还会有一些问题我们包心跳的逻辑全部放在应用层,是不是对心跳是不很友好,这回导致应用层的逻辑与网络层的逻辑耦合在一起,本来应该网络层做的心跳,可所有的逻辑都在应用层,网络层就像完全没有心跳这回事。
说这么多,最后我们还是回到现实,最近我有一开发任务,为我们的分布式系统添加一个心跳检测机制。
简单描述一下我们的系统:
一个分布式的系统,每个peer会连接一个或多个资源,一个资源会被多个peer连接,当peer受到请求后会随即的把请求发送到他知道能处理这个请求的peer中。peer之间使用json_rpc通行(用rpc协议是这个过程实际上就是一个远程调用)。
比如我是a,我知道b,c,d能处理 x
的请求,我会随即的把请求发到b,c,d某个peer中(或者发送到第一个peer)。
现在遇到的一个问题是,如果b,c,d中间有人挂机了,a是不知道的,而a还会随即的把请求发到一个节点里面。
我现在的设计方案是,在rpc模块实现心跳的逻辑,包括自动发送心跳包,判断返回结果是否正常,对于多次心跳异常的节点进行处理,(对方节点死了也要周期性的那,用于复活),也就是心跳的主要逻辑在rpc中实现(网络模块),它用于控制心跳的评论等等,然后在rpc中抽象出两个接口,HeartbeatHandler,HeartbeatClient
HeartbeatHandler 接口表示心跳服务端处理的接口
HeartbeatClient 接口表示心跳客户端的接口: 客户端接口只需要提供两个方法,一个属如果构造心跳请求的接口,一个属如何处理心跳结果的接口
在server 启动时,将一个HeartbeatHandler的接口注册到server中,在client实例化的时候设置HeartbeatClient 并开启心跳,而心跳的流程控制还在网络模块中。