本文由美团技术团队分享,作者“健午、佳猛、陆凯、冯江”,原题“美团终端消息投递服务Pike的演进之路”,有修订。
传统意义上来说,实时消息推送通常都是IM即时通讯这类应用的技术范畴,随着移动端互联网的普及,人人拥有手机、随时都是“在线”已属常态,于是消息的实时触达能力获得了广泛的需求,已经不再局限于IM即时通讯这类应用中。
对于美团这种移动端“入口”级应用来说,实时消息的推送能力已经深入整个APP的方方面面。目前美团应用中使用的推送技术,是一个被命名为Pike的一套易接入、高可靠、高性能的双向消息实时投递服务。
本文将首先从Pike的系统架构升级、工作模式升级、长稳保活机制升级等方面介绍2.0版本的技术演进,随后介绍其在直播、游戏等新业务场景下的技术特性支持,并对整个系统升级过程中的技术实践进行了总结,希望本文能给消息实时推送服务感兴趣或从事相关工作的读者以帮助和启发。
(本文同步发布于:http://www.52im.net/thread-3662-1-1.html)
实时消息推送技术文章参考:
美团技术团队分享的其它文章:
2015年,美团诞生了Shark终端网络通道,为公司移动端提供长连代理加速服务。Shark通过网络接入点的全球多地部署和保持长连来提升网络请求的端到端成功率,降低端到端延时,从而提升用户体验。
Pike 1.0是基于Shark长连通道实现的应用内推送服务。由于底层传输基于Shark长连通道,使得Pike 1.0天生便具有了低延时、高可靠、防DNS劫持等优秀基因。目前Pike 1.0在美团内部的即时通讯聊天、营销推送、状态下发、配置同步等业务场景都有广泛使用。
Pike 1.0 移动端SDK会在每次长连接创建成功后:
整体工作流程参见下图:
Pike 1.0底层传输基于Shark长连通道。
所以Pike 1.0在以下几个方面有不错的表现:
PS:移动网络下HTTP、DNS的优化文章,可以看看下面这几篇:
Pike 1.0作为Shark的衍生产品固然有其闪光的地方,但是对Shark的强依赖所带来的痛点更是让开发人员叫苦不迭,主要痛点如下。
3.4.1)代码结构耦合:
在客户端SDK方面,Pike 1.0代码与Shark代码结构耦合,共用底层通道建连、数据加解密、二进制协议等逻辑。
Pike 1.0与Shark代码结构示意图:
耦合带来的弊端一:代码优化升级困难。针对一个SDK的变更经常需要更多地考虑对另一个SDK是否有负面影响,是否影响面可控,这就无端地增加了开发成本。
耦合带来的弊端二:Shark与Pike 1.0的网络配置环境共用,如上图所示,通过DebugPanel对SharkTunnel进行网络环境配置都会同时对Shark和Pike 1.0生效,但是业务方在使用的时候往往只关注其中的一个SDK,不同SDK之间的相互影响引入了很多客服问题,也给客服问题的排查带来了较多干扰因素。
3.4.2)账号体系混乱:
Pike 1.0在同一个App上只支持一种设备唯一标识UnionID,不同App上注册使用的UnionID会有不同,例如美团使用美团唯一标识,点评则使用点评唯一标识。
假如一个业务只在一个App上使用的话Pike 1.0自然可以很好地工作,但是同一个业务有可能需要在多个App上同时使用(如下图所示),如果业务方不对账号体系进行兼容的话,美团App上使用点评唯一标识作为推送标识的业务将无法工作,点评App上使用美团唯一标识作为推送标识的的业务也会无法工作。
这就导致同一个业务在不同App上的推送标识ID逻辑会非常复杂,后端要同时维护多套账号体系之间的映射,才能解决账号体系混乱的问题。
Pike 1.0账号体系不兼容示意图:
3.4.3)推送连接不稳定:
Pike 1.0由于共用Shark的通道逻辑而缺乏推送场景专项优化,在检测通道异常、断连恢复等方面表现不够优秀。在通道可用性上,Shark与Pike 1.0关注的SLA也有着很大的不同。
例如:Shark在长连接通道不可用的情况下,可以通过降级短连接来规避业务网络请求持续失败所带来的成功率下降问题。但是对于Pike 1.0此时如果通道不能快速恢复的话就会造成业务消息投送失败,将直接影响消息投递成功率。所以Shark通道针对连接保活的公共逻辑并不能完美地应用在Pike 1.0业务场景上。
虽然Pike 1.0在Shark通道的基础上进一步在协议层强化了心跳探测机制以提高通道可用性,但通道不能及时检测异常还是时有发生。
此外:Pike 1.0内部使用的事件分发技术的可靠性还暂时没能达到100%,零星地会上报一些异常断连而导致推送不成功的客服问题。
综上:针对推送连接不稳定专项优化的诉求也就不断被提上日程。
Pike 1.0现有的技术痛点在业务场景日益丰富的现状下遭遇了诸多挑战。
为求解决Pike 1.0现有在Android和iOS平台运营上遇到的问题:
进而推出全新的升级产品——Pike 2.0。
下图展示了Pike 2.0的产品全景。针对Pike 1.0的现状,Pike 2.0前后端都做了诸多优化,包括技术架构升级、集群独立、协议扩展等。
其中在客户端方面Pike 2.0提供了基于多语言实现服务于多平台的SDK,在服务端方面Pike使用部署Java应用的分布式集群来提供服务。
Pike 2.0产品全景图:
以下内容将主要从客户端视角,详细阐述Pike 2.0 客户端SDK的技术方案设计,从原理上说明Pike 2.0带来的技术优势。
针对上文提及的Pike 1.0代码结构耦合的技术痛点,Pike 2.0进行了全新的架构升级,在代码结构、环境配置、服务集群等方面上都与Shark保持产品隔离。
经过接近一年的技术积累与沉淀,从Shark提炼的TunnelKit长连内核组件和TNTunnel通用通道组件已经趋于稳定,所以Pike 2.0选择基于TunnelKit与TNTunnel来构建双向消息通道服务。
具体优势有:
客户端架构演进图:
整体架构如上图所示,包括:
4.2.1)接口层:
Pike接口层旨在为主流前端技术栈中所有需要应用内消息服务的业务提供简洁可靠的接口。
主要是:
针对第 2)点,我们是这样设计的:
4.2.2)通道层:
Pike通道层是特性的实现层,所有Pike接口层的API调用都会通过线程调度转变成封装的Task在Pike通道层完成具体的操作,Pike通道层是单线程模型,最大程度规避掉了线程安全问题。
Pike特性如下:
4.2.3)TNTunnel通道层:
TNTunnel通道层是封装通用通道逻辑的功能层,主要涉及通道状态管理、协议封装、数据加解密等通用核心模块,是将通用通道逻辑从原先Shark通道中提炼而成的独立分层。
Pike协议虽然是构建在现有Shark协议之上的应用层协议,但是Pike通道已经和原先的Shark通道在逻辑上完全解耦。
4.2.4)TunnelKit长连内核层:
TunnelKit长连内核层主要功能是对接Socket来处理TCP或者UDP数据的发送与接收,管理各个连接的可用性等。
每条Pike 2.0通道在TunnelKit中都是维护一条连接的,通过心跳保活机制和连接管理来保证在网络环境正常的情况下永远有一条连接来承载Pike数据。
TunnelKit作为所有通道层的基础,是决定上层长连接通道稳定性最重要的一层。
在进行了全新推送架构升级的基础上,Pike针对上文提及的Pike 1.0账号体系混乱、推送连接不稳定的痛点重新设计并完善了工作机制。
其中,PikeClient作为Pike系统对接业务方的门户,在整个Pike 2.0系统中起着至关重要的作用,本节将以PikeClient为切入点介绍其工作机制。
为了更好地维护Pike 2.0内部状态,PikeClient使用状态机来负责生命周期管理。
PikeClient生命周期图:
如上图所示,PikeClient生命周期主要包括如下几个部分:
通过基于状态机的生命周期管理,既严格定义了PikeClient的工作流程,也可以准确监控其内部状态,提高了PikeClient的可维护性。
针对Pike 1.0混乱的账号体系痛点,Pike 2.0设计了全新的工作模式。
如下图所示,Pike通过通道代理模块提供共享通道和独立通道两种模式来满足不通业务场景的需求。
5.2.1)共享通道模式:
共享通道模式是Pike 2.0基本的工作模式,新增的业务方在默认情况下都会使用该模式接入Pike 2.0。
在Pike 2.0中PikeClient与Pike通道服务是多对一的共享关系,每个业务方都会有自己的PikeClient,每个PikeClient都可以自定义消息推送标识ID而避免使用全局标识ID。业务后端可以精简推送标识逻辑,避免同时维护多套账号体系。
不同业务的PikeClient仅在接入层面做了业务隔离,在Pike 2.0通道中会由Pike通道服务完成统一的管理。这种多对一的共享关系使得所有Pike业务共享Pike 2.0通道特性,同时又可以针对每个业务的使用场景设置其特定的消息处理能力,每个接入Pike 2.0的业务方都只需要关注其自己的PikeClient即可。
5.2.2)独立通道模式:
独立通道模式是共享通道模式的拓展能力,Pike 2.0通过配置控制来决策是否切换至该模式。
Pike 2.0默认情况下所有业务方都是共享同一个Pike通道服务,然而鉴于业务场景的不同,每个业务对于消息吞吐量,消息时延等SLA指标的诉求也有差异,例如游戏业务对于消息时延过长的容忍性就比较差。针对特殊业务Pike 2.0提供了独立通道切换的能力支持。
所有PikeClient都通过Pike通道代理模块来对接Pike通道服务,Pike通道代理模块可以通过开关配置来控制PikeClient与特定的Pike通道服务协同工作。通过运用代理模式,既保证了原有结构的完整性,在不需要调整Pike通道代码逻辑的基础上就能够完成独立通道能力支持;又可以扩展通道切换能力,有效地管理通道切换的流程,让Pike 2.0通道最大化提供业务能力的同时避免资源浪费。
PikeClient的保活完全依赖Pike 2.0通道的保活,针对Pike 1.0推送连接不稳定的痛点,Pike 2.0通道在吸收Pike 1.0在保活机制方面沉淀的技术的基础上继续优化,最后设计出基于心跳探测、重连机制和通道巡检的三重保活机制。
保活机制如下图:
5.3.1)心跳探测:
心跳探测是一种检查网络连接状态的常见手段,Pike长连接是TCP连接,而TCP是虚拟连接:如果实际物理链路中出现诸如异常网络节点等因素导致连接出现异常,客户端和服务端并不能及时感应到连接异常,这时就会出现连接的状态处于ESTABLISHED状态,但连接可能已死的现象,心跳探测就是为了解决这种网络异常的技术方案。
PS:关于tcp协议为什么还需要心跳保活,可以详读这篇《为何基于TCP协议的移动端IM仍然需要心跳保活机制?》。
客户端在心跳巡检计时器设置的心跳周期到达时判断是否存在上次心跳超时的异常,如果心跳超时则认为该连接已经不可用了,则会从连接池移除该连接并触发下文的重连机制。
为了更快地发现通道异常,Pike 2.0对于心跳周期与心跳超时都是可配置的,针对不同App使用的场景可以灵活地设置。
而且在每次发送上行数据的时候都会及时检测上次心跳是否超时,使得心跳探测结果不必等到下次心跳周期到达的时刻才知悉。
Pike 2.0并不是采用固定心跳频率来发送心跳包,Pike 2.0会利用通道的上下行数据包来动态减少心跳包的发送次数。
此外,智能心跳也是Pike 2.0持续关注的话题,感兴趣的读读下面这些:
5.3.2)重连机制:
重连机制是Pike 2.0作为长连接通道最核心的特性,也是Pike 2.0连接稳定性建设最重要的一环。
客户端会在发送消息、接收消息和心跳探测三个环节来决策是否需要触发重连:
Pike 2.0在重连的过程中会采用斐波那契数列退避算法来发起建连请求直至建连成功:
有关断线重连这方面的文章,可以系统的读一读下面这些:
PS:如果需要实践性的代码,也可读一下开源工程MobileIMSDK ,它对于im的心跳和重连机制有完整的逻辑实现,可以借鉴参考。
5.3.3)通道巡检:
通道巡检是在心跳探测和重连机制的基础上进一步提升Pike 2.0稳定性的有效机制。
客户端会根据心跳周期设置一个全局的巡检定时器,在每次定时器设置的时刻到达时,客户端会触发通道异常检测逻辑,一旦发现异常都会尝试重启通道。
Pike 2.0首先会在触发通道异常检测的时候获取当前通道状态,如果通道当前没有主动关闭但是通道处于不可用的状态,Pike 2.0会强制执行一次自启动。
此外,在通道巡检的过程中,巡检管理器会不断收集消息收发过程中出现的超时异常,当超时异常次数连续累计超过配置的最大阈值时,Pike 2.0会认为当前通道可用性较低,需要强制关闭并执行一次自启动。
Pike 2.0作为Pike 1.0的升级版,不只是为了解决Pike 1.0的技术痛点,通过新增特性以开拓新的应用场景也是Pike 2.0关注的点。
随着公司内直播业务的兴起,公司内部也有很多业务方使用Pike 1.0作为弹幕、评论、直播间控制信令等下行实时消息的传输通道。
但Pike 1.0基于早先的设计架构为弹幕、评论这种短时间需要处理海量消息的场景提供可靠服务的能力渐渐力不从心。
主要表现在QPS大幅增长时,消息投递成功率降低、延时增加和系统性能开销增长等方面。Pike通过引入聚合消息为直播场景中消息的投递提出更加通用的解决方案。
6.1.1)设计思想:
直播场景中涉及的消息主要具备以下特点:
聚合消息在设计上主要采用下述思想:
针对第 4)点:相比传统的服务端推送策略,主动拉取是利用客户端天然分布式的特点将用户状态保存在客户端,服务端通过减少状态维护进而可以留出更多的资源用于业务处理。
6.1.2)方案流程:
Pike 2.0针对每个聚合单元都使用环形队列来维护消息列表,发送到该聚合单元的消息在经过优先级过滤之后都会插入队列tail指针标示的位置,随着该聚合单元内消息不断增加最后达到最大队列长度时,head指针会不断移动来给tail指针腾出位置。聚合单元通过控制最大长度的环形队列来避免消息短时间井喷式增长带来的服务性能问题。
客户端在主动拉取的时候都会携带上一次获取到的消息处在环形队列中的偏移量,这样服务就会将偏移量标示的位置到tail指针标示的位置之间的消息进行聚合作为本次拉取的结果一次性返回给客户端。不同客户端各自维护自己的偏移量,以此来避免服务端对于客户端的状态维护。
客户端与服务端的具体交互如下图所示:客户端在加入聚合单元之后主动拉取,如果本次拉取携带的偏移量能够从服务的环形队列中获取到聚合消息,那么就将消息回调给业务之后马上进行下一次拉取操作。如果本次携带的偏移量已经位于环形队列tail指针的位置,那么服务端将不做任何响应,客户端等待本次拉取超时之后开始下一次拉取操作,重复该流程直至客户端离开该聚合单元。与此同时,业务服务端如果有消息需要推送,则通过RPC的方式发送给Pike服务端,消息处理模块将执行消息分级策略过滤之后的有效消息插入环形队列。
聚合消息交互流程图:
Pike 1.0在设计之初就只适用于消息推送的场景,而Pike 2.0在其基础上演进为双向消息投递服务,即不仅支持下行的消息推送,还支持上行的消息投递。Pike 2.0在上行的消息投递方面进一步拓展了消息保序的功能。
这里的消息保序主要包含两个层面的含义:
6.2.1)粘性会话:
为了使每一个业务客户端发送的消息都最大程度地到达同一个业务服务器,Pike 2.0引入了粘性会话的概念。
粘性会话指的是:同一客户端连接上的消息固定转发至某一特定的业务方机器处理,客户端断连重连后,保持新连接上的消息仍转发至该业务机器。
粘性会话可以归纳为如下的流程:
6.2.2)时序一致性:
我们都知道TCP是有序的,那么在同一个TCP连接的前提下什么情况会出现客户端发送的消息乱序到达业务服务器呢?
原因就是:Pike 2.0服务器从TCP中读出消息之后将其投递给业务服务器是通过RPC异步调用的。
为了解决这种问题:最简单的方案当然是客户端将消息队列的发送窗口限定为1,每一条发送消息都在Pike 2.0服务器投递给业务服务器之后才能收到ACK,这时再发送下一条消息。但是考虑到网络传输在链路上的时延远远大于端上处理的时延,所以该方案的QPS被网络传输设了瓶颈,假设一个RTT是200ms,那么该方案理论也只能达到5的QPS。
Pike 2.0为了提高上行消息保序投递的QPS,采用服务端设置消息队列缓存的方案。
如下图所示:客户端可以在发送窗口允许的范围内一次性将多条消息发送出去,服务端把收到的消息都按顺序缓存在消息队列中,然后串行的通过RPC调用将这些缓存的消息依序投递给业务服务器。
这种保序方案将QPS性能的瓶颈点从之前网络传输在链路上的时延转移到了RPC调用的时延上,而实际场景中一次RPC调用往往在几个毫秒之间,远远小于网络传输在链路上的时延,继而显著地提升了QPS。
消息时序一致性问题,在实时通信领域是个很热门的技术点:
Pike 2.0依赖美团监控平台Raptor完成监控体系建设,服务端和客户端都建设了各自完善的指标监控。
Pike 2.0客户端通过利用Raptor的端到端指标能力和自定义指标能力输出了超过10+个监控指标来实时监控Pike系统,这些指标覆盖通道建立、消息投递、业务登录、系统异常等多维度。
在实时指标监控的基础上Pike 2.0针对不同指标配置了报警阈值,以推送消息为例,如果特定App的大盘数据在每分钟的上下波动幅度超过10%,那么Raptor系统就会向Pike项目组成员推送告警信息。
基于所有Raptor监控指标,Pike 2.0提炼核心SLA指标如下:
Pike 2.0会定期输出基于核心SLA指标的大盘数据报表,同时可以基于App、业务类型、网络类型等多维度对数据进行筛选以满足不同用户对于指标数据的需求。
监控体系能从全局的角度反映推送系统稳定性,针对个案用户,Pike管理平台提供完整的链路追踪信息。
每个Pike 2.0连接都由唯一标识Token来区分,通过该唯一标识Token在Pike管理平台的“连接嗅探”模块主动探测便能获得对应连接上所有信令的交互流程。
如下图所示:流程中明确标注了客户端建立连接、发起鉴权、绑定别名等信令,点击对应信令可以跳转信令详情进一步查看该信令所携带的信息,再结合SDK埋点在美团日志服务Logan的离线日志就可以快速发现并定位问题。
截至2021年6月,Pike共接入业务200+个,日均消息总量约50亿+,Pike 2.0消息到达率 >99.5%(相比Pike 1.0提升0.4%),Pike 2.0平均端到端延时<220ms(相比Pike 1.0减少约37%)。
部分应用案例:
Pike实时消息推送服务在美团应用广泛,目前主要集中在实时触达、互动直播、移动同步等业务场景。随着公司业务的快速发展,Pike对可用性、易用性、可扩展性提出了更高要求,希望提升各种业务场景下的网络体验。
因此未来Pike的规划重点主要是:提供多端、多场景下的网络通信方案,不断完善协议生态,在各种应用场景下对抗复杂网络。
具体就是:
本文已同步发布于“即时通讯技术圈”公众号。
▲ 本文在公众号上的链接是:点此进入。同步发布链接是:http://www.52im.net/thread-3662-1-1.html