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