阿里 IM 技术分享 (五):闲鱼亿级 IM 消息系统的及时性优化实践
本文由阿里闲鱼技术团队有攸分享,原题“向消息延迟说 bybye:闲鱼消息及时到达方案”,有修订和改动,感谢作者的分享。
1、引言
IM 消息作为闲鱼用户重要的交易咨询工具,核心目标有两点:
1)第一是保证用户的消息不丢失;
2)第二是保证用户的消息及时送达接收方。
IM 消息根据消息的接收方设备是否在线,分为离线和在线推送。数据显示目前闲鱼每天有超过一半以上的 IM 消息是走在线通道的,而在线消息的到达率、及时性是直接影响用户体验的。
本文将根据闲鱼 IM 消息系统在消息及时性方面的优化实践,详细分析了 IM 在线通道面临的各种技术问题,并通过相应的技术手段来优化从而保证用户消息的及时到达。
PS:如果您对 IM 消息可靠性还没有概念,建议先阅读这篇入门文章《零基础IM开发入门(二):什么是IM系统的实时性?》。
学习交流:
- 移动端 IM 开发入门文章:《新手入门一篇就够:从零开发移动端IM》
- 开源 IM 框架源码:https://github.com/JackJiang2011/MobileIMSDK
(本文同步发布于:http://www.52im.net/thread-3726-1-1.html)
2、系列文章
本文是系列文章的第 5 篇,总目录如下:
《阿里IM技术分享(五):闲鱼亿级IM消息系统的及时性优化实践》(* 本文)
3、当前面临的问题
3.1 端内长连接中断
在 IM 场景中,用户与云端通信频繁,且为了实现用户的消息及时到达,往往采用云端下推消息的方式触达用户,所以用户在线时设备与云端会维持一条 TCP 长连接通道,可以更轻量级的与服务端进行交互,现代 IM 即时通讯的下行消息都是通过长连下发的。
当前闲鱼 IM 消息系统使用的是 ACCS 长连接,ACCS 是淘宝无线提供的全双工、低延时、高安全的通道服务。
但由于用户设备网络状态的不确定性,可能会发生各种各样的网络异常情况导致 ACCS 长连接通道中断。而长连接一旦意外中断,就会导致用户无法及时收到在线消息。
针对这个问题,我们需要尽可能及时的感知到长连中断并尝试重连。具体的优化思路会在本文后面的内容中分享。
3.2 下推的消息未达
感知长连中断并重连只能在大多数时间保证长连接的有效性,但是在长连接无效或不稳定期间下推的消息客户端可能根本收不到。
简单说就是仅仅有重连机制无法保证下行消息必达,可能有以下场景导致下行消息失败:
1)服务端发送下行消息时长连畅通,消息在传输路上通道断掉,客户端无法收到;
2)设备的在线状态存在延迟,服务端下行消息时认为设备在线,实际上设备已经离线,无法收到;
3)客户端收到了下行消息,但端上后续处理失败(比如落库失败,消息没有成功展示给用户)。
我们通过数据埋点统计得出,ACCS 长连接的下行成功率在 97%左右。
ACCS 长连接的下行成功率的统计方法如下:
ACCS 下行成功率 = 通过 ACCS 成功下行且客户端收到的消息量 / 服务端认为通过 ACCS 成功下行的消息量
有心急的同学就要问了,丢了 3%的消息吗?
并没有!这 3%的消息不会丢失,只是不保证及时触达给用户。
我们的消息同步模型是推拉结合模式,在用户拉取消息时会拉取到设备当前位点与服务端最新位点的所有消息,ACCS 下行失败的消息会通过主动拉模式获取到,但客户端主动拉取消息的触发时机有限。
当前客户端主动拉取消息的触发时机主要有以下几个:
1)用户冷启动 app,主动同步消息;
2)用户主动下拉刷新;
3)app 后台切换前台;
4)收到一条推送消息,客户端发现新消息的位点跟本地最新的位点有 gap,触发同步。
可见:上述主动同步消息的触发很大程度上依赖用户行为或者有没有收到新消息,难以保证消息及时到达。
如果是用户高频打开的 IM 软件,这样也不会有太大的问题。但是闲鱼 app 的活跃度较低,有时候甚至依赖 IM 消息拉活,而且一条延迟的消息触达可能导致用户错过一笔交易,闲鱼消息不允许有这样的延迟发生。
基于上述分析,我们先描述一个数据指标来反映现状。
通过上面的描述可知:ACCS 消息并不全都是推下来的,也可能是主动拉下来的。如果是推,必定可以及时到达;如果是拉,则受限于用户行为。
拉的这部分消息,我们定义为 ACCS 消息补偿到达,然后计算 ACCS 消息补偿到达耗时,消息范围限定为服务端 ACCS 成功下行但是客户端通过主动拉取同步到的消息,以往的版本这个数据在 60 分钟左右。
注意:这个数据并不是消息触达到用户的耗时,因为如果在线转离线触达,拉取到消息的时间取决于用户行为(用户何时打开了 app),但这个数据也能大致反映在线消息的到达延迟状况。
ACCS 长连接的消息补偿到达耗时的统计方法如下:
ACCS 消息补偿到达耗时 = 客户端通过拉获取到 ACCS 消息的时间 - 服务端 ACCS 下行时间
接下来本文将从长连接的重连和未达消息重发两个方面详细讲述我们是如何优化在线通道稳定性的,从而优化并保证消息的及时到达。
4、优化手段 1:增加长连接重连机制
4.1 长连接为什么会中断?
有因必有果,我们先来分析下有哪些原因会导致连接中断。
对于 IM 这种场景下来说,通常可能有以下原因:
1)用户设备断网;
2)设备发生了网络切换;
3)设备处于弱网环境,网络不稳定;
4)设备网络正常,TCP 连接由于 NAT 超时导致连接被运营商中断。
对于 APP 来说,如果是用户操作导致网络状态变化的情况,会有网络状态变化事件通知,这种情况可以监听事件并主动尝试重连。但现实中的大多数情况都是“意料之外”(正如上面列举的这些断网可能性一样)。
那么既然“意料之外”的断网无法预知,技术上可以如何有效的感知到各种异常状况呢?
PS:如果要透彻理解断网、弱网、TCP 链接有效性,并不是本文能讲的清楚的,可以参照下面的资料深入理解一下,值得好好学习。
关于 TCP 链接本身的有效性问题,可以读以下两篇:
关于移动网络的复杂性问题,可以从以下几篇入门的科普文章学习一下:
关于移动弱网带来的各种问题、优化方案等,可以通过以下几篇系统学习一下:
4.2 心跳检测机制
像大多数链路保活场景一样,IM 这种场景下最有效的检测手段就是心跳检测(如果你对 TCP 链路保活还没有什么概念,建议先读《为何基于TCP协议的移动端IM仍然需要心跳保活机制?》)。
原理就是:客户端通过定时发送心跳包,服务端收到心跳包后再反馈给客户端,通过客户端和服务端这一来一去的配合,就可以实现客户服和服务端各自都能感知到连接是否中断。
从及时性效果来看:心跳间隔越短越好,而频繁的心跳检测势必会带来用户流量以及电量的损耗,所以我们的实现目标是如何尽可能少的心跳检测而又尽量及时地感知到长连中断的意外情况。
状态机+消息心跳队列:
在心跳协议设计上,要注意心跳包的核心目标是检测长连通道是否畅通,客户端主动上行心跳包且能收到服务端回包,就认为长连通道健康。所以心跳的上行消息以及回包的数据包应尽可能小。一般来说,通过协议头标识心跳包及响应即可(这样就能节省协议包大小)。
PS:关于心跳机制的入门文章可以详读《一文读懂即时通讯应用中的网络心跳包机制:作用、原理、实现思路等》。
4.3 心跳策略
心跳策略是实现我们上述目标的核心机制,本文仅简单列举几种心跳策略。
比如以下这几种:
1)短心跳检测 初始状态连续 ping 3 次 收到 ACK 后,可以认为进入稳定状态;
2)常规固定时长心跳(根据 app 状态不同,频率可调 Mid+,Mid-, Long);
3)自适应心跳 根据设备网络状态变化自动适应的心跳间隔;
4)冗余心跳,app 后台切前台,主动心跳一次。
关于心跳策略的详细设计甚至可以单独写一篇文章,有兴趣的同学可以阅读以下推荐的文章继续深入研究。
5、优化手段 2:消息 ACK 应答与重发机制
5.1 概述
为了解决上面的问题,我们同时也引入了消息 ACK 应答与重发机制。
整体思路是:客户端在收到 ACCS 消息并处理成功后,给服务端回一个 ACK 应答包,服务端下发 ACCS 消息时将消息加入重试队列,收到 ACK 应答包后更新消息到达状态,并终止重试。
整体设计流程图如下:
该方案的难点即重试处理器的实现设计,接下来我们将重点讲述这部分的详细设计。
5.2 重试队列存储设计
我们采用阿里云表格存储 TimeLine 模型来存储下行消息的到达状态。Timeline 模型是针对消息数据场景所设计的数据模型,它能满足消息数据场景对消息保序、海量消息存储、实时同步的特殊需求,在 IM、Feed 流等消息场景应用广泛。(关于 TimeLine 模型,这里有篇详细的文章可以学习一下《现代IM系统中聊天消息的同步和存储方案探讨》)
我们给每个用户设备定义一个 TimeLine,timeline-id 定义为 userId_deviceId,sequenceId 自定义为消息位点。
存储结构如下:
每通过 ACCS 成功下行一条消息,则插入到接收用户设备的 TimeLine 中,收到 ACK 后根据消息 id 更新消息到达状态。
同时由于重试动作只发生在下行消息后较短的一段时间内,所以我们设置一个比较短的全局过期时间即可,避免数据膨胀。
5.3 延迟重试设计
如上图所示:
1)每通过 ACCS 下发一条消息,先插入到 Timeline 中,初始状态为未达,然后生产一条延迟 N 秒的延迟消息;
2)每次消费到延迟消息后,读取 tablestore 中该消息的到达状态,如到达则终止延迟,否则继续;
3)每次重试先判断设备是否在线,如果设备不在线,转发离线通道并终止重试,如果设备在线,则重推未到达的消息,并再次延迟 N 秒消费;
4)每条消息的重试生命周期中用的同一条延迟消息,最多重试消费 M 次,超过次数不再重试并打日志埋点(后续可以监控这种情况并基于这个数据进行优化)。
5.4 延迟重发策略
延迟重发策略是指在重发流程中,如何选择合适的延迟时间来使得重发的效率最高。
不同用户在不同时间、地点所处的网络环境差别较大,网络恢复到稳定态所需要的时间也有差异,需要选用合适的延迟策略来保证重发效率。
最优的延迟策略的目标是在最短的时间内,使用最少的重发次数将消息投递成功。以下是几种可选的方案。
5.4.1)固定延迟时间:
要想找到最优的延迟策略,必须从数据中通过分析得到答案,天马行空的想象往往离实际相差甚远。
我们先采用固定的延迟时间(10s)最大重试 6 次来分析一波数据:
通过这组数据可以看到:有约 85%的消息在 40s 内重发可以投递成功,还有 12%的消息在达到最大重试次数后依旧没有收到 ACK。在 4 次重试之后,第 5 次成功只有 2.03%,第 6 次只有 0.92%,继续重发的收益已经变得很低。
6 次以后还有部分消息没有收到 ACK,这部分消息如果用固定延迟时间策略,性价比很低,频繁重发浪费系统资源,我们需要继续改进策略。
5.4.2)固定延迟+固定步长递增:
考虑到部分用户的网络短时间无法恢复,频繁的短间隔重发价值不大,我们采用 4 次固定短间隔延迟 N 秒后,每次延迟时间都是上一次延迟时间递增固定步长 M 秒的策略。直到收到 ACK、用户设备离线或者达到了最大延迟时间 MAX(N)。
这种策略一定程度上可以解决固定延迟时间重发策略的问题,但如果用户短时间网络无法恢复,每次重发都要重新递增,也不是一种最优解。
5.4.3)自适应延迟:
设计流程图:
如上图:我们最终衍生出了自适应延迟策略。
自适应延迟是指:根据用户的网络状况,采取自动调整的延迟时间,以期望达到最高的重发效率。
具体是:新消息先通过 4 次固定 N 秒的短延迟来探测设备的网络状况,一旦网络恢复,我们将设备的 N 值清空(设备 N 值是指根据上几次重发经验,当前设备网络能回复 ACK 所需要的最短时间,默认情况该值为空,代表用户设备网络正常)。4 次重发后依旧收不到 ACK,我们尝试读取设备 N 值,如果为空,则取初始值,以后每次延迟都递增固定步长 M,并在重发后更新当前设备的 N 值,直到消息收到 ACK 或者达到了最大延迟时间 MAX(N)。
5.5 新老版本兼容性
需要注意的是老版本的 app 是不会回 ACK 的,如果下发给老版本设备的消息也加入重试队列,那此类消息将一直重试到最大次数才会终止,无端消耗资源。
所以我们设计在 ACCS 长连建立之后,客户端主动上行一条设备信息,其中包含 app 的版本号,服务端存储一定时间,在将消息加入重试队列之前,先校验接收者设备 app 的版本号,符合要求再加入重试队列。
6、 最终优化后的效果
消息重连重发方案上线后,我们上面定义的指标 ACCS 补偿到达时间 从 60 分钟大幅降低至 15 分钟,降幅达 75%。
从而印证了我们的技术分析,同时用户有关消息延迟的舆情反馈大幅下降,可见消息重发机制对保证用户消息及时到达成效显著。
7、未来展望
消息在线通道的稳定性优化至此已告一段落,未来我们将继续优化闲鱼消息的使用体验,包括基础功能的完善以及基础体验的提升。
基础功能方面:我们在近期的版本中已经支持了消息撤回、草稿功能,后续将逐步支持发送定位,会话分组、备注,消息搜索等功能。
基础体验方面:我们对消息的 UI 样式做了优化升级,并优化了 app 消息 tab 页的 cpu 及内存使用,后续将继续从流量、电量、性能方面继续优化消息的使用体验。
附录:参考资料
[1] 为何基于TCP协议的移动端IM仍然需要心跳保活机制?
[2] 不为人知的网络编程(十二):彻底搞懂TCP协议层的KeepAlive保活机制
[4] 现代移动端网络短连接的优化手段总结:请求速度、弱网适应、安全保障
[5] 移动端IM开发者必读(二):史上最全移动弱网络优化方法总结
[6] IM开发者的零基础通信技术入门(十二):上网卡顿?网络掉线?一文即懂!
[7] IM开发者的零基础通信技术入门(十三):为什么手机信号差?一文即懂!
[8] 移动端IM实践:实现Android版微信的智能心跳机制
[9] 融云技术分享:融云安卓端IM产品的网络链路保活技术实践
[10] Web端即时通讯实践干货:如何让你的WebSocket断网重连更快速?
本文已同步发布于“即时通讯技术圈”公众号。
评论