喜马拉雅亿级用户量的离线消息推送系统架构设计实践

核心提示本文由喜马拉雅技术团队李乾坤原创,原题《推送系统实践》,感谢作者的无私分享。1、引言1.1 什么是离线消息推送对于IM的开发者来说,离线消息推送是再熟悉不过的需求了,比如下图就是典型的IM离线消息通知效果。1.2 Andriod端离线推送真

本文由喜马拉雅技术团队李乾坤原创,原题《推送系统实践》。感谢作者的无私分享。

1.介绍

1.1什么是离线消息推送?

对于IM开发人员来说,离线消息推送是一个熟悉的需求。比如下图就是一个典型的IM离线消息通知效果。

1.2 Andriod端离线推送真的不容易。

移动离线消息推送不外乎涉及两个终端——iOS和Andriod。iOS没什么好说的,APNs是唯一的选择。

Andriod是一个精彩的结尾。为了实现线下推送,各种保活黑科技层出不穷。随着保命难度的不断升级,保命手段越来越少。有兴趣的可以看看下面我整理的文章,感受一下。

应用保活终极总结:Android6.0下双进程守护保活练习,Android6.0上应用保活练习,Android6.0上应用保活练习,Android P正式版即将到来:后台应用保活和消息推送的真实噩梦,全面盘点当前Android后台保活方案的真实运行效果,Android 2020看我如何优雅实现!史上最强的Android保活思想:腾讯TIM进程永生技术深度解析,Android进程永生技术终极揭秘:进程杀底层原理及APP应对被杀的技巧,Android保活从进入到放弃:引导用户上白名单。以上文章只是我在这方面整理的一些文章,尤其是最后一篇,Android从进入到放弃的保活:引导用户进入白名单。是的,目前Andriod对APP保活的容忍度几乎为零,所以那些老的保活方法在新版本系统中几乎都失效了。

让自己活下去已经不行了,还是要保持线下消息推送。我该怎么办?按照目前最好的做法,那就是打疫苗手机厂商的房间级推送渠道。这里就不赘述了。有兴趣可以详细阅读《Android P正式版即将推出:后台应用保活和消息推送的真正噩梦》。

在保活和自建推送通道的时代,离线消息推送系统的架构设计比较简单,就是每个终端计算一个deviceID,服务器通过自建通道传输消息。仅此而已。

现在自建渠道已死,只能依靠厂商的推送渠道。小米,华为,魅族,OPPO,vivo等。拥有太多的手机型号,它们的推送API和设计规范都不一样,直接导致之前的离线消息推送系统架构设计必须重新设计,以适应新时代的推送技术要求。

1.3如何合理设计?

那么,我们的后台推送架构应该如何针对不同厂商的房间级推送渠道进行合理的设计呢?

本文分享的离线消息推送系统的设计并不是针对IM产品的,无论业务层的差异,总的技术思路都是一样的。希望喜马拉雅的这篇分享能给正在设计有大量用户的线下消息推送的你一些启发。

*推荐阅读:另一个长连接网关技术专题:喜马拉雅技术团队分享的喜马拉雅自研亿API网关技术实践。有兴趣的话也可以一起看。

沟通:

-移动IM开发入门:《初学者一个入门就够:从零开始开发移动IM》-开源IM框架源代码:https://github.com/JackJiang2011/MobileIMSDK

2.技术背景

首先介绍一下推送系统在喜马拉雅APP中的作用,如下图,是一个新闻服务的推送/通知。

线下推送主要是在用户不打开APP的时候有一个触达用户的手段,保持APP的存在感,改善APP的日常生活。

目前,我们的主推服务包括:

1)主播开播:公司有直播服务,主播开播的时候会给这个主播的所有粉丝发一个推送的开播提醒。2)专辑更新:平台上有很多专辑,专辑下面有一系列特定的声音。比如一部小说是一个有很多章节的相册,那么当小说更新章节的时候,给所有订阅这个相册的用户发送一个更新提醒:3)个性化,新闻业务等。由于您希望向用户发送离线推送,因此系统必须有与用户设备联系的渠道。

做过这个的人都知道,自建推送渠道需要App留在后台,而手机厂商出于省电等原因,普遍采用“激进”的后台进程管理策略,导致自建渠道质量较差。目前渠道一般由“推送服务商”维护,也就是说公司里的推送系统并不直接向用户发送推送。

在这种情况下,离线推送流程如下:

国内几家厂商都有自己的官方推送渠道,但是每个接口都不一样,所以小米、歌推等部分厂商提供集成接口。将推送系统发送给集成商,再由集成商根据具体设备将推送通道发送给具体厂商,最后发送给用户。

向设备发送推送时,必须明确要发送的内容:标题、消息/正文,并指定将推送发送到哪个设备。

我们用token来标识一个设备,不同场景下token的含义是不一样的。通常,一个设备在一个公司内由uid或deviceId来标识,集成商和不同的制造商也有他们自己唯一的设备“编号”。因此,公司内部的推送服务负责将uid和deviceId转换为integrator token。

3.总体架构设计

如上图所示,推送系统整体上是一个基于队列的流处理系统。

上图右侧:是主链接。每个业务方通过push接口向push系统发送push,push接口会将数据发送到一个队列中,供转换和过滤服务使用。转换就是上面提到的uid/deviceId到token的转换,下面将具体描述过滤。经过转换和过滤后,将被发送到发送模块,并最终发送到集成器接口。

App启动时会向服务器发送绑定请求,报告uid/deviceId和token的绑定关系。卸载/重装App时等。导致令牌失效,集成商通过http回调通知推送系统。每个组件会通过kafka向公司的xstream实时流处理集群送水,汇总数据并下载到mysql,最后grafana会提供各种报表进行展示。

4.服务过滤机制的设计。

各业务方可以无意识地向用户发送推送,但推送系统要有所约束,所以要有选择地过滤业务消息。

过滤机制的设计包括以下几点:

1)用户开关:App支持用户开关的配置。如果用户关闭push,则不会向用户设备发送push;2)副本安排:用户无法接收副本,用于防止上游业务方发送逻辑错误;3)频率控制:每个服务对应一个msg_type,设置xx时间内推送消息的最大数量;4)静默时间:每天xx到xx不会给用户发送推送,以免打扰用户休息。5)分级管理:从用户和消息两个维度进行分级控制。对于第5点,具体来说:

1)每个msg/msg_type都有一个等级,给重要/高等级的服务更多的传输机会;2)当用户一天收到xx条推送时,不再向这些用户发送不重要的消息。5.子数据库、子表下的多维查询。

很多时候,设计是建立在理论和经验的基础上,但在实践中,总会遇到各种各样的具体问题。

喜马拉雅目前有6亿+用户,对应的推送系统的设备表也有类似的数量级,所以设备表分为库和子表,子表列是deviceId。

但实际上经常会有基于uid/token的查询需求,所以需要建立uid/token和deviceId的映射关系。因为uid查询场景也很频繁,所以uid辅助表也有与主表相同的字段。

因为每天都有一两次全局推送,还有针对静默用户的特殊推送,所以存储其实并没有什么“热点”。虽然使用了缓存,但是作用有限,占用了空的巨大空间。

多表拆分和缓存会导致三个或四个数据副本。不同的逻辑使用不同的副本,往往会导致不一致,查询代码非常复杂,性能较低。

最终我们选择将设备数据存储在tidb上,在可用性的前提下大大简化了代码。

6.特殊业务的时效性。

6.1基本概念

推送系统是基于队列的,“先到先服务”。大部分服务对实时性要求不高,但直播服务要求半小时送达,新闻服务更是“不知足”。越快越好。

如果新闻推送时队列中有巨量的“相册更新”推送等待处理,相册更新业务会严重干扰新闻业务的投放。

6.2这是隔离问题?

一开始我们以为是隔离问题:比如有10个消费节点,其中3个负责高时效业务,7个负责一般业务。当时队列中使用的是rabbitmq,所以spring-rabbit被修改为支持根据msytype将消息路由到特定节点。

该方案具有以下缺点:

1)有的机器忙的时候,有的机器在“旁观”;2)新增服务时,需要额外配置msgType和消费节点的映射关系,维护成本高;3)rabbitmq是基于内存实现的,推瞬时峰值时占用大量内存,造成rabbitmq不稳定。6.3其实是优先问题。

后来我们意识到这是一个优先级问题:高优先级的服务/消息可以插队,于是我们封装了kafka的支持优先级,较好地解决了隔离方案带来的问题。具体实现就是建立多个主题,一个主题代表一个优先级,包装卡夫卡主要是封装了消费者的逻辑。

备注:为了描述简单,本文使用consumer.poll来描述使用consumer拉num消息,这与真实的kafka api是不一致的。请注意。

PriorityConsumer的实现有三种方案,如下所述。

1)轮询到达内存后重新排序:java有一个现成的基于内存的PriorityQueue,Priority Queue或PriorityBlockingQueue。kafka消费者正常消费它,并再次将来自轮询的数据推送到优先级队列。

1.1)如果使用有界队列,队列满了之后,后面的消息无论优先级多高都放不进去,从而失去了“插队”的效果;1.2)如果使用无界队列,本来应该堆在kafka上的消息就会堆在内存里,OOM的风险很大。2)先拉高优先级话题的数据:只要有,就消耗,直到没有数据消耗较低级别话题。在消费较低级别的主题的过程中,如果有较高级别的主题消息到达,就会转向消费较高优先级的消息。

该方案实现复杂,可能导致低优先级业务在晚高峰等推送密集时段完全失去推送机会。

3)优先级从高到低,循环拉取数据:

循环的逻辑是:

消费者-1 .投票;消费者调查;消费者-最高优先级.投票

如果topic 1-num = topic-I-num = topic-max . priority-num,则该方案没有优先级效果。Topic1-num可以看作是权重。我们达成一致:话题-高-数=2 *话题-低-数。所有话题都会同时消费,通过一次消费的金额来变相实现“插队效应”。具体来说,就是借鉴“滑动窗口”策略,在长时间没有消息的情况下,优化某个优先级的话题的总消费性能。

由此可见,限制问题首先被理解为隔离问题,然后被视为优先级问题,最后转化为权重问题。

7.过滤机制的存储和性能。

在我们的架构中,tidb查询和过滤逻辑是影响推送发送速度的主要因素,过滤机制分为存储和性能两个问题。

这里我们以xx服务频率控制限制“一小时最多发一条”为例进行分析。

第一版实现时:redis kv结构为。

频率控制实现逻辑如下:

1)发送时,incr键,发送次数增加1;2)超过限度,就不推了;3)如果不超过限值,返回值为1,则表示该消息在msgtype频率控制周期内第一次发送到deviceId,到期时间需要通过expire键设置。上述方案具有以下缺点:

1)目前公司有60+推送业务,6亿+deviceId,共6亿*60个密钥,占据空的巨大空间;2)很多时候,处理一个deviceId: incr+expire需要2条指令。为此,我们的解决方案是:

1)用pika替代redis,磁盘间可以满足存储需求空;2)委托系统架构组扩展redis协议,支持新结构ehash。E-Hash是基于redis hash修改的两级映射。除了密钥之外,该字段还可以支持有效期,并且支持有效期的条件设置。

频率控制数据的存储结构由改为,这样对于多个msgtype,deviceId只存储一次,节省了空的空间。

Incr和expire合并成一条指令:incr,减少了一次网络通信:

1)当该字段没有设置有效期时,为其设置有效期;2)当字段未过期时,有效期参数被忽略。因为推送系统大量使用incr指令,所以可以看作是一个写指令,大部分场景使用流水线来达到批量写的效果。我们委托系统架构组的小伙伴对pika的写性能进行优化,支持“写模式”,qps在10w以上。

电子哈希结构在运行记录中也起着重要的作用。比如100001002就是我们约定的一个数据格式示例值。第一、中间和最后三个部分分别表示deviceId的消息的发送、接收和点击细节。例如,前三位“100”表示发送失败,因为它处于静默期。

附录:更多关于消息推送的技术文章

iOS的push服务APNs详解:设计思路、技术原理及缺陷等。、信鸽团队原创:一起走过iOS10上消息推送的坑,安卓上消息推送的总结:实现原理、心跳保活、遇到的问题等。扫盲贴:了解MQTT通信协议,基于MQTT通信协议的完整android推送演示,采访IBM技术经理:MQTT协议的制定过程和发展现状,求教Android消息推送:GCM,XMPP,MQTT三种方案的优缺点,移动实时消息推送技术简析,扫盲贴:谈iOS和Android后台实时消息推送的原理和区别, 绝对干货:基于Netty的推送服务技术要点,移动IM实践:Google消息推送服务研究,为什么微信、QQ这样的IM工具不用GCM服务推送消息? 极光推送系统大规模高并发架构的技术实践分享,从HTTP到MQTT:基于位置服务的APP数据通信实践概述,魅族2500万长连接实时消息推送架构的技术实践分享,魅族架构师访谈:海量长连接实时消息推送系统体验,深入谈Android消息推送,基于WebSocket的H实现,ybrid移动应用的消息推送实践,基于长连接的安全可扩展订阅/推送服务的实现思路,以及实践Go语言构建千万级在线高并发消息推送系统的实践、腾讯信鸽技术分享:百亿级实时消息推送的实践经验、百万级在线直播弹幕系统实时推送技术的实践之路、京东京麦商开放平台消息推送架构的演进之路、了解iOS消息推送就够了:史上最全面的iOS推送技术讲解、基于最新HTTP/ 2接口实现iOS的高性能消息推送、解密“Dada-JD”即时传送技术的原理与实践COM到家》,技术干货:教你从零开始设计百万级消息推送系统,长连接网关技术专题:爱奇艺WebSocket实时推送网关技术实践,拥有数亿喜马拉雅用户的离线消息推送系统架构设计实践,更多类似文章…

本文已在微信官方账号“即时通讯科技圈”发表。同步链接是:http://www.52im.net/thread-3621-1-1.html.

 
友情链接
鄂ICP备19019357号-22