gitbook/即时消息技术剖析与实战/docs/134231.md
2022-09-03 22:05:03 +08:00

131 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 08 | 智能心跳机制:解决网络的不确定性
你好,我是袁武林。
在前面的章节里,我讲到了在即时消息场景中非常重要的两个特性:“可靠投递”和“实时性”。
为了让消息能更加实时、可靠、快速地触达到接收方大部分IM系统会通过“长连接”的方式来建立收发双方的通信通道这些基于TCP长连接的通信协议在用户上线连接时会在服务端维护好连接到服务器的用户设备和具体TCP连接的映射关系通过这种方式服务端也能通过这个映射关系随时找到对应在线的用户的客户端而且这个长连接一旦建立就一直存在除非网络被中断。
因为“长连接”方式相比“短连接轮询”,不仅能节约不必要的资源开销,最重要的是能够通过“服务端推送”,提供更加实时的消息下发。
同样对于发送方来说如果发送消息也能通过“长连接”通道把消息给到IM服务端相对于短连接方式也能省略TCP握手和TLS握手的几个RTT的时间开销在用户体验和实时性上也会更好。
## 为什么需要心跳机制
“长连接”方式给我们带来了众多好处,那么要让消息通过“长连接”实现可靠投递,最重要的环节就在于如何维护好这个“长连接”。
由于这个“长连接”底层使用的TCP连接并不是一个真正存在的物理连接实际上只是一个无感知的虚拟连接中间链路的断开连接的两端不会感知到因此维护好这个“长连接”一个关键的问题在于能够让这个“长连接”能够在中间链路出现问题时让连接的两端能快速得到通知然后通过“重连”来重新建立新的可用连接从而让我们这个“长连接”一直保持“高可用”状态。
这个能够“快速”“不间断”识别连接可用性的机制,被称为“心跳机制”。“心跳机制”通过持续地往连接上发送“模拟数据”来试探连接的可用性,同时也让我们的连接在没有真正业务数据收发的时候,也持续有数据流通,而不会被中间的网络运营商以为连接已经没有在使用而把连接切断。
下面,我会从两个方面带你详细了解一下心跳机制在长连接维护中的必要性。
### 降低服务端连接维护的开销
首先心跳机制可以让IM服务端能尽快感知到连接的变化从而尽早清理服务端维护连接使用的资源。
对于大部分即时通讯场景,消息收发双方经常处于移动网络环境中,手机信号强弱变化及中间路由故障等,都可能导致“长连接”实际处于不可用状态。
比如用户拿着手机进电梯了手机网络信号忽然完全没了长连接此时已经不可用但IM服务端无法感知到这个“连接不可用”的情况另外假如我们上网的路由器忽然掉线了之前App和IM服务端建立的长连接此时实际也处于不可用状态但是客户端和IM服务器也都无法感知。
我在前面讲过之所以能够实现消息的“服务端推送”是因为我们针对每一台上线的设备都会在IM服务端维护相应的“用户设备”和“网络连接”这么一个映射关系除此之外很多时候为了节省网络开销还会在服务端临时缓存一些没必要每次请求都携带的客户端的信息比如app版本号、操作系统、网络状态等这样客户端一旦建好长连后只需要首次携带这些信息后续请求可以不用再携带而是使用IM服务端缓存的这些信息。另外在很多IM的实现上还会在服务端维护一些“用户在线状态”和“所有在线设备”这些信息便于业务使用。
如果IM服务端无法感知到这些连接的异常情况会导致的一个问题是IM服务端可能维护了大量“无效的连接”从而导致严重的连接句柄的资源浪费同时也会缓存了大量实际上已经没有用了的“映射关系”“设备信息”“在线状态”等信息也是对资源的浪费另外IM服务端在往“无效长连接”推送消息以及后续的重试推送都会降低服务的整体性能。
### 支持客户端断线重连
通过“心跳”快速识别连接的可用性,除了可以降低服务端的资源开销,也被用于支撑客户端的断开重连机制。
对于客户端发出心跳包如果在一定的超时时间内考虑到网络传输具有一定的延迟性这个超时时间至少要大于一个心跳的间隔比如连续两次发送心跳包都没有收到IM服务端的响应那么客户端可以认为和服务端的长连接不可用这时客户端可以断线重连。
导致服务端没有响应的原因可能是和服务端的网络在中间环节被断开,也可能是服务器负载过高无法响应心跳包,不管什么情况,这种场景下断线重连是很有必要的,它能够让客户端快速自动维护连接的可用性。
### 连接保活
维护一条“高可用”的长连接,还有一个重要的任务就是**尽量让建立的长连接存活时间更长**。
这里你可能会问:难道在用户网络和中间路由网络都正常的情况下,长连接还可能会被杀死?
答案是:确实会。
探究这个原因的话我可能要从IPv4说起。由于IPv4的公网IP的资源有限性约43亿个为了节省公网IP的使用通过移动运营商上网的手机实际上只是分配了一个运营商内网的IP。
在访问Internet时运营商网关通过一个“外网IP和端口”到“内网IP和端口”的双向映射表来让实际使用内网IP的手机能和外网互通这个网络地址的转换过程叫做NATNetwork Address Translation
NAT本身的实现机制并没有什么不妥问题在于很多运营商为了节省资源和降低自身网关的压力对于一段时间没有数据收发的连接运营商会将它们从NAT映射表中清除掉而且这个清除动作也不会被手机端和IM服务端感知到。
这样的话如果没有了NAT映射关系长连接上的消息收发都无法正常进行。而且多长时间会从NAT映射表清除每个地方的运营商也是不尽相同从几分钟到几小时都有。假设用户有几分钟没有收发消息可能这个长连接就已经处于不可用状态了。
那么如果我们的客户端能在没有消息收发的空闲时间给服务端发送一些信令就能避免长连接被运营商NAT干掉了这些“信令”一般就是通过心跳包来实现。
## 心跳检测的几种实现方式
介绍完了心跳机制的重要性我们来学习一下如何去实现心跳检测。目前业界有三种常用的实现方法TCP Keepalive、应用层心跳及智能心跳。下面我们分别来看一看。
### TCP Keepalive
TCP的Keepalive作为操作系统的TCP/IP协议栈实现的一部分对于本机的TCP连接会在连接空闲期按一定的频次自动发送不携带数据的探测报文来探测对方是否存活。操作系统默认是关闭这个特性的需要由应用层来开启。
默认的三个配置项心跳周期是2小时失败后再重试9次超时时间75s。三个配置项均可以调整。
这样来看TCP的Keepalive作为系统层TCP/IP协议栈的已有实现不需要其他开发工作量用来作为连接存活与否的探测机制是非常方便的上层应用只需要处理探测后的连接异常情况即可而且心跳包不携带数据带宽资源的浪费也是最少的。
由于易用性好、网络消耗小等优势TCP Keepalive在很多IM系统中被开启使用之前抓包就发现WhatsApps使用空闲期10秒间隔的TCP Keepalive来进行存活探测。
虽然拥有众多优势但TCP Keepalive本身还是存在一些缺陷的比如心跳间隔灵活性较差一台服务器某一时间只能调整为固定间隔的心跳另外TCP Keepalive虽然能够用于连接层存活的探测但并不代表真正的应用层处于可用状态。
我举一个例子比如IM系统出现代码死锁、阻塞的情况下实际上已经无法处理业务请求了但此时连接层TCP Keepalive的探针不需要应用层参与仍然能够在内核层正常响应。这种情况就会导致探测的误判让已失去业务处理能力的机器不能被及时发现。
### 应用层心跳
为了解决TCP Keepalive存在的一些不足的问题很多IM服务使用应用层心跳来提升探测的灵活性和准确性。**应用层心跳实际上就是客户端每隔一定时间间隔向IM服务端发送一个业务层的数据包告知自身存活。**
如果IM服务端在一定时间内没有收到心跳包就认定客户端由于某种原因连接不可达了此时就会从IM服务端把这个连接断开同时清除相应分配的其他资源。
应用层心跳和TCP Keepalive心跳相比由于不属于TCP/IP协议栈的实现因此会有一些额外的数据传输开销但是大部分应用层心跳的设计上心跳包都尽量精简一般就几个字节比如有些应用层心跳包只是一个空包用于保活有的心跳包只是携带了心跳间隔用于客户端调整下一次的心跳所以额外的数据开销都非常小。
应用层心跳相比TCP Keepalive由于需要在应用层进行发送和接收的处理因此更能反映应用的可用性而不是仅仅代表网络可用。
而且应用层心跳可以根据实际网络的情况来灵活设置心跳间隔对于国内运营商NAT超时混乱的实际情况下灵活可设置的心跳间隔在节省网络流量和保活层面优势更明显。
目前大部分IM都采用了应用层心跳方案来解决连接保活和可用性探测的问题。比如之前抓包中发现WhatApps的应用层心跳间隔有30秒和1分钟微信的应用层心跳间隔大部分情况是4分半钟目前微博长连接采用的是2分钟的心跳间隔。
每种IM客户端发送心跳策略也都不一样最简单的就是按照固定频率发送心跳包不管连接是否处于空闲状态。之前抓手机QQ的包就发现App大概按照45s的频率固定发心跳还有稍微复杂的策略是客户端在发送数据空闲后才发送心跳包这种相比较对流量节省更好但实现上略微复杂一些。
下面是一个典型的应用层心跳的客户端和服务端的处理流程图,从图中可以看出客户端和服务端,各自通过心跳机制来实现“断线重连”和“资源清理”。
![](https://static001.geekbang.org/resource/image/bc/01/bcb269cadc8b781b56c35071e316e301.png)
需要注意的是:对于客户端来说,判断连接是否空闲的时间是既定的心跳间隔时间,而对于服务端来说,考虑到网络数据传输有一定的延迟,因此判断连接是否空闲的超时时间需要大于心跳间隔时间,这样能避免由于网络传输延迟导致连接可用性的误判。
### 智能心跳
在国内移动网络场景下各个地方运营商在不同的网络类型下NAT超时的时间差异性很大。采用固定频率的应用层心跳在实现上虽然相对较为简单但为了避免NAT超时只能将心跳间隔设置为小于所有网络环境下NAT超时的最短时间虽然也能解决问题但对于设备CPU、电量、网络流量的资源无法做到最大程度的节约。
为了优化这个现象很多即时通讯场景会采用“智能心跳”的方案来平衡“NAT超时”和“设备资源节约”。所谓智能心跳就是让心跳间隔能够根据网络环境来自动调整通过不断自动调整心跳间隔的方式逐步逼近NAT超时临界点在保证NAT不超时的情况下尽量节约设备资源。据说微信就采用了智能心跳方案来优化心跳间隔。
不过从个人角度看随着目前移动资费的大幅降低手机端硬件设备条件也越来越好智能心跳对于设备资源的节约效果有限。而且智能心跳方案在确认NAT超时临界点的过程中需要不断尝试可能也会从一定程度上降低“超时确认阶段”连接的可用性因此我建议你可以根据自身业务场景的需要来权衡必要性。
## 小结
简单回顾一下今天的内容:为了保证消息下发的实时性,很多即时通讯场景使用“长连接”来降低每次建立连接消耗的时间,同时避免了“短连接轮询”带来的不必要的资源浪费。
但是由于移动网络环境错综复杂网络状态变化、中间链路断开、运营商NAT超时都可能导致这个“长连接”处于不可用状态而且收发双发无法感知到。
通过客户端和IM服务端建立的“心跳机制”可以快速自动识别连接是否可用同时避免运营商NAT超时被断开的情况。“心跳机制”解决了以下三方面的问题
* **降低服务端连接维护无效连接的开销。**
* **支持客户端快速识别无效连接,自动断线重连。**
* **连接保活避免被运营商NAT超时断开。**
心跳探测的实现业界大部分综合采用以下两种方式:
* **TCP Keepalive。**操作系统TCP/IP协议栈自带无需二次开发使用简单不携带数据网络流量消耗少。但存在灵活性不够和无法判断应用层是否可用的缺陷。
* **应用层心跳。**应用自己实现心跳机制需要一定的代码开发量网络流量消耗稍微多一点但心跳间隔的灵活性好配合智能心跳机制可以做到“保证NAT不超时的情况下最大化节约设备资源消耗”同时也能更精确反馈应用层的真实可用性。
最后给大家留一道思考题:
**心跳机制中可以结合TCP的keepalive和应用层心跳来一起使用吗**
以上就是今天课程的内容,欢迎你给我留言,我们可以在留言区一起讨论。感谢你的收听,我们下期再见。