写点什么

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

用户头像
JackJiang
关注
发布于: 1 小时前
喜马拉雅亿级用户量的离线消息推送系统架构设计实践

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

1、引言

1.1 什么是离线消息推送

对于 IM 的开发者来说,离线消息推送是再熟悉不过的需求了,比如下图就是典型的 IM 离线消息通知效果。

1.2 Andriod 端离线推送真心不易

移动端离线消息推送涉及的端无非就是两个——iOS 端和 Andriod 端,iOS 端没什么好说的,APNs是唯一选项。

Andriod 端比较奇葩(主要指国内的手机),为了实现离线推送,各种保活黑科技层出不穷,随着保活难度的不断升级,可以使用的保活手段也是越来越少,有兴趣可以读一读我整理的下面这些文章,感受一下(文章是按时间顺序,随着 Andriod 系统保活难度的提升,不断进阶的)。

上面这几篇只是我整理的这方面的文章中的一部分,特别注意这最后一篇《Android保活从入门到放弃:乖乖引导用户加白名单吧(附7大机型加白示例)》。是的,当前 Andriod 系统对 APP 自已保活的容忍度几乎为 0,所以那些曾今的保活手段在新版本系统里,几乎统统都失效了。

自已做保活已经没戏了,保离线消息推送总归是还得做。怎么办?按照现时的最佳实践,那就是对接种手机厂商的 ROOM 级推送通道。具体我就不在这里展开,有兴趣的地可以详读《Android P正式版即将到来:后台应用保活、消息推送的真正噩梦》。

自已做保活、自建推送通道的时代(这里当然指的是 Andriod 端啦),离线消息推送这种系统的架构设计相对简单,无非就是每台终端计算出一个 deviceID,服务端通过自建通道进行消息透传,就这么点事。

而在自建通道死翘翘,只能依赖厂商推送通道的如今,小米华为魅族OPPOvivo(这只是主流的几家)等等,手机型号太多,各家的推送 API、设计规范各不相同(别跟我提什么统一推送联盟,那玩意儿我等他 3 年了——详见《万众瞩目的“统一推送联盟”上场了》),这也直接导致先前的离线消息推送系统架构设计必须重新设计,以适应新时代的推送技术要求。

1.3 怎么设计合理呢

那么,针对不同厂商的 ROOM 级推送通道,我们的后台推送架构到底该怎么设计合理呢?

本文分享的离线消息推送系统设计并非专门针对 IM 产品,但无论业务层的差别有多少,大致的技术思路上都是相通的,希望借喜马拉雅的这篇分享能给正在设计大用户量的离线消息推送的你带来些许启发。

* 推荐阅读:喜马拉雅技术团队分享的另一篇《长连接网关技术专题(五):喜马拉雅自研亿级API网关技术实践》,有兴趣也可以一并阅读。

(本文同步发布于:http://www.52im.net/thread-3621-1-1.html

2、技术背景

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

离线推送主要就是在用户不打开 APP 的时候有一个手段触达用户,保持 APP 的存在感,提高 APP 的日活。

我们目前主要用推送的业务包括:

  • 1)主播开播:公司有直播业务,主播在开直播的时候会给这个主播的所有粉丝发一个推送开播提醒

  • 2)专辑更新:平台上有非常多的专辑,专辑下面是一系列具体的声音,比如一本儿小说是一个专辑,小说有很多章节,那么当小说更新章节的时候给所有订阅这个专辑的用户发一个更新的提醒:

  • 3)个性化、新闻业务等。

既然想给一个用户发离线推送,系统就要跟这个用户设备之间有一个联系的通道。

做过这个的都知道:自建推送通道需要 App 常驻后台(就是引言里提到的应用“保活”),而手机厂商因为省电等原因普遍采取“激进”的后台进程管理策略,导致自建通道质量较差。目前通道一般是由“推送服务商”去维护,也就是说公司内的推送系统并不直接给用户发推送(就是上节内容的这篇里提到的情况:《Android P正式版即将到来:后台应用保活、消息推送的真正噩梦》)。

这种情况下的离线推送流转流程如下:

国内的几大厂商(小米华为魅族OPPOvivo等)都有自己官方的推送通道,但是每一家接口都不一样,所以一些厂商比如小米、个推提供集成接口。发送时推送系统发给集成商,然后集成商根据具体的设备,发给具体的厂商推送通道,最终发给用户。

给设备发推送的时候,必须说清楚你要发的是什么内容:即 title、message/body,还要指定给哪个设备发推送。

我们以 token 来标识一个设备, 在不同的场景下 token 的含义是不一样的,公司内部一般用 uid 或者 deviceId 标识一个设备,对于集成商、不同的厂商也有自己对设备的唯一“编号”,所以公司内部的推送服务,要负责进行 uid、deviceId 到集成商 token 的转换。

3、整体架构设计

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

上图右侧:是主链路,各个业务方通过推送接口给推送系统发推送,推送接口会把数据发到一个队列,由转换和过滤服务消费。转换就是上文说的 uid/deviceId 到 token 的转换,过滤下文专门讲,转换过滤处理后发给发送模块,最终给到集成商接口。

App 启动时:会向服务端发送绑定请求,上报 uid/deviceId 与 token 的绑定关系。当卸载/重装 App 等导致 token 失效时,集成商通过 http 回调告知推送系统。各个组件都会通过 kafka 发送流水到公司的 xstream 实时流处理集群,聚合数据并落盘到 mysql,最终由 grafana 提供各种报表展示。

4、业务过滤机制设计

各个业务方可以无脑给用户发推送,但推送系统要有节制,因此要对业务消息有选择的过滤。

过滤机制的设计包括以下几点(按支持的先后顺序):

  • 1)用户开关:App 支持配置用户开关,若用户关闭了推送,则不向用户设备发推送;

  • 2)文案排重:一个用户不能收到重复的文案,用于防止上游业务方发送逻辑出错;

  • 3)频率控制:每一个业务对应一个 msg_type,设定 xx 时间内最多发 xx 条推送;

  • 4)静默时间:每天 xx 点到 xx 点不给用户发推送,以免打扰用户休息。

  • 5)分级管理:从用户和消息两维度进行分级控制。

针对第 5 点,具体来说就是:

  • 1)每一个 msg/msg_type 有一个 level,给重要/高 level 业务更多发送机会;

  • 2)当用户一天收到 xx 条推送时,不是重要的消息就不再发给这些用户。

5、分库分表下的多维查询问题

很多时候,设计都是基于理论和经验,但实操时,总会遇到各种具体的问题。

喜马拉雅现在已经有 6 亿+用户,对应的推送系统的设备表(记录 uid/deviceId 到 token 的映射)也有类似的量级,所以对设备表进行了分库分表,以 deviceId 为分表列。

但实际上:经常有根据 uid/token 的查询需求,因此还需要建立以 uid/token 到 deviceId 的映射关系。因为 uid 查询的场景也很频繁,因此 uid 副表也拥有和主表同样的字段。

因为每天会进行一两次全局推,且针对沉默用户(即不常使用 APP 的用户)也有专门的推送,存储方面实际上不存在“热点”,虽然使用了缓存,但作用很有限,且占用空间巨大。

多分表以及缓存导致数据存在三四个副本,不同逻辑使用不同副本,经常出现不一致问题(追求一致则影响性能), 查询代码非常复杂且性能较低。

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

6、特殊业务的时效性问题

6.1 基本概念

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

若进行新闻推送时:队列中有巨量的“专辑更新”推送等待处理,则专辑更新业务会严重干扰新闻业务的送达。

6.2 这是隔离问题?

一开始我们认为这是一个隔离问题:比如 10 个消费节点,3 个专门负责高时效性业务、7 个节点负责一般业务。当时队列用的是rabbitmq,为此改造了 spring-rabbit 支持根据 msytype 将消息路由到特定节点。

该方案有以下缺点:

  • 1)总有一些机器很忙的时候,另一些机器在“袖手旁观”;

  • 2)新增业务时,需要额外配置 msgType 到消费节点的映射关系,维护成本较高;

  • 3)rabbitmq 基于内存实现,推送瞬时高峰时占用内存较大,进而引发 rabbitmq 不稳定。

6.3 其实是个优先级问题

后来我们觉察到这是一个优先级问题:高优先级业务/消息可以插队,于是封装 kafka 支持优先级,比较好的解决了隔离性方案带来的问题。具体实现是建立多个 topic,一个 topic 代表一个优先级,封装 kafka 主要是封装消费端的逻辑(即构造一个 PriorityConsumer)。

备注:为描述简单,本文使用 consumer.poll(num) 来描述使用 consumer 拉取 num 个消息,与真实 kafka api 不一致,请知悉。

PriorityConsumer 实现有三种方案,以下分别阐述。

1)poll 到内存后重新排序:java 有现成的基于内存的优先级队列 PriorityQueue 或 PriorityBlockingQueue,kafka consumer 正常消费,并将 poll 到的数据重新 push 到优先级队列。

  • 1.1)如果使用有界队列,队列打满后,后面的消息优先级再高也 put 不进去,失去“插队”效果;

  • 1.2)如果使用无界队列,本来应堆在 kafka 上的消息都会堆到内存里,OOM 的风险很大。

2)先拉取高优先级 topic 的数据:只要有就一直消费,直到没有数据再消费低一级 topic。消费低一级 topic 的过程中,如果发现有高一级 topic 消息到来,则转向消费高优先级消息。

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

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

一次循环的逻辑为:

consumer-1.poll(topic1-num);

cosumer-i.poll(topic-i-num);

consumer-max.priority.poll(topic-max.priority-num)

如果 topic1-num=topic-i-num=topic-max.priority-num,则该方案是没有优先级效果的。topic1-num 可以视为权重,我们约定:topic-高-num=2 * topic-低-num,同一时刻所有 topic 都会被消费,通过一次消费数量的多少来变相实现“插队效果”。具体细节上还借鉴了“滑动窗口”策略来优化某个优先级的 topic 长期没有消息时总的消费性能。

从中我们可以看到,时效问题先是被理解为一个隔离问题,后被视为优先级问题,最终转化为了一个权重问题。

7、过滤机制的存储和性能问题

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

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

第一版实现时:redis kv 结构为 <deviceId_msgtype,已发送推送数量>。

频控实现逻辑为:

  • 1)发送时,incr key,发送次数加 1;

  • 2)如果超限(incr 命令返回值>发送次数上限),则不推送;

  • 3)若未超限且返回值为 1,说明在 msgtype 频控周期内第一次向该 deviceId 发消息,需 expire key 设置过期时间(等于频控周期)。

上述方案有以下缺点:

  • 1)目前公司有 60+推送业务, 6 亿+ deviceId,一共 6 亿*60 个 key ,占用空间巨大;

  • 2)很多时候,处理一个 deviceId 需要 2 条指令:incr+expire。

为此,我们的解决方法是:

  • 1)使用 pika(基于磁盘的 redis)替换 redis,磁盘空间可以满足存储需求;

  • 2)委托系统架构组扩充了 redis 协议,支持新结构 ehash。

ehash 基于 redis hash 修改,是一个两级 map <key,field,value>,除了 key 可以设置有效期外,field 也可以支持有效期,且支持有条件的设置有效期。

频控数据的存储结构由<deviceId_msgtype,value>变为 <deviceId,msgtype,value>,这样对于多个 msgtype,deviceId 只存一次,节省了占用空间。

incr 和 expire 合并为 1 条指令:incr(key,filed,expire),减少了一次网络通信:

  • 1)当 field 未设置有效期时,则为其设置有效期;

  • 2)当 field 还未过期时,则忽略有效期参数。

因为推送系统重度使用 incr 指令,可以视为一条写指令,大部分场景还用了 pipeline 来实现批量写的效果,我们委托系统架构组小伙伴专门优化了pika 的写入性能,支持“写模式”(优化了写场景下的相关参数),qps 达到 10w 以上。

ehash 结构在流水记录时也发挥了重要作用,比如<deviceId,msgId,100001002>,其中 100001002 是我们约定的一个数据格式示例值,前中后三个部分(每个部分占 3 位)分别表示了某个消息(msgId)针对 deviceId 的发送、接收和点击详情,比如头 3 位“100”表示因发送时处于静默时间段所以发送失败。

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

iOS的推送服务APNs详解:设计思路、技术原理及缺陷等

信鸽团队原创:一起走过 iOS10 上消息推送(APNS)的坑

Android端消息推送总结:实现原理、心跳保活、遇到的问题等

扫盲贴:认识MQTT通信协议

一个基于MQTT通信协议的完整Android推送Demo

IBM技术经理访谈:MQTT协议的制定历程、发展现状等

求教android消息推送:GCM、XMPP、MQTT三种方案的优劣

移动端实时消息推送技术浅析

扫盲贴:浅谈iOS和Android后台实时消息推送的原理和区别

绝对干货:基于Netty实现海量接入的推送服务技术要点

移动端IM实践:谷歌消息推送服务(GCM)研究(来自微信)

为何微信、QQ这样的IM工具不使用GCM服务推送消息?

极光推送系统大规模高并发架构的技术实践分享

从HTTP到MQTT:一个基于位置服务的APP数据通信实践概述

魅族2500万长连接的实时消息推送架构的技术实践分享

专访魅族架构师:海量长连接的实时消息推送系统的心得体会

深入的聊聊Android消息推送这件小事

基于WebSocket实现Hybrid移动应用的消息推送实践(含代码示例)

一个基于长连接的安全可扩展的订阅/推送服务实现思路

实践分享:如何构建一套高可用的移动端消息推送系统?

Go语言构建千万级在线的高并发消息推送系统实践(来自360公司)

腾讯信鸽技术分享:百亿级实时消息推送的实战经验

百万在线的美拍直播弹幕系统的实时推送技术实践之路

京东京麦商家开放平台的消息推送架构演进之路

了解iOS消息推送一文就够:史上最全iOS Push技术详解

基于APNs最新HTTP/2接口实现iOS的高性能消息推送(服务端篇)

解密“达达-京东到家”的订单即时派发技术原理和实践

技术干货:从零开始,教你设计一个百万级的消息推送系统

长连接网关技术专题(四):爱奇艺WebSocket实时推送网关技术实践

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

>> 更多同类文章 ……

本文已同步发布于“即时通讯技术圈”公众号。

同步发布链接是:http://www.52im.net/thread-3621-1-1.html

用户头像

JackJiang

关注

还未添加个人签名 2019.08.26 加入

开源IM框架MobileIMSDK、BeautyEye的作者。

评论

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