乐趣区

关于后端:国民应用QQ如何实现高可用的订阅推送系统

导语|腾讯工程师许扬从 QQ 揭示理论业务场景登程,论述一个订阅推送零碎的技术要点和实现思路。如何通过推拉联合、异构存储、多重触发、可控调度、打散执行、牢靠推送等技术,实现推送可靠性、推送可控性和推送高效性?本篇为你具体解答。

目录

1 业务背景与诉求

1.1 业务背景

1.2 技术诉求

2 实现计划

2.1 推拉联合

2.2 异构存储

2.3 多重触发

2.4 可控温度

2.5 打散执行

2.6 引入音讯队列

2.7 At least once 推送

2.8 容灾计划

3 总结

01 业务背景与诉求

1.1 业务背景

QQ 服务了大量的挪动互联网用户。作为一个超大流量的平台,其订阅揭示性能无论对于用户还是业务方而言,都施展着至关重要的作用。QQ 揭示的业务场景十分多样,举个例子,《使命与号召》手游在某日早上 10 点公布,QQ 则揭示预约用户下载并支付礼包;春节刷一刷领红包在小年当天早晨 8 点 05 分开始,QQ 则揭示订阅用户参加。

QQ 揭示整体业务实现流程是:

  • 业务方在治理端建设推送工作;
  • 用户在终端订阅推送工作;
  • 预设工夫到时,通过音讯服务给所有订阅的用户推送音讯。

1.2 技术诉求

不难看出,这是一个通过预设工夫触发的订阅推送零碎,QQ 团队冀望它能达到的技术要点波及 3 个方面。

  • 推送可靠性:任何业务方在零碎上配置的工作,都应该失去触发;任何订阅了揭示工作的用户,都应该收到推送音讯。
  • 推送可控性:音讯服务的容量是有下限的,零碎的总体音讯推送速率不能超过该下限。而业务投放的工作却有肯定随机性,可能某一时刻没有工作,可能某一时刻多个工作同时触发。所以零碎必须在总体上做速率把控,防止推送过快导致上游解决失败,影响业务体验。如果造成上游音讯服务雪崩,结果不堪设想。
  • 推送高效性:QQ 团队布局进步零碎的推送速度,以满足业务的更高时效性的要求。实际上,QQ 团队的业务场景下做高并发是绝对简略的,而做到高牢靠和可控反而较简单。话不多说,上面谈谈 QQ 团队如何实现这些技术要点。

02 实现计划

以下是整体架构图,供各位读者进行宏观理解。接下来讲 8 个重点实现思路。

2.1 推拉联合

首先给各位读者抛出一个疑难:揭示推送零碎肯定要通过推送来下发揭示吗?答案是否定的。既然推送的内容是固定的,那么 QQ 团队能够提前将工作数据下发到客户端,让客户端自行计时触发揭示。这相似于配置下发零碎。

但如果采纳相似于配置预下发的形式,就波及到一个问题:提前多久下发呢?提前太久,如果下发后工作须要批改怎么办?对于 QQ 业务而言,这是很常见的问题。比方一个游戏原定工夫公布不了(这也被称为跳票),须要批改到一个月后或者更久触发揭示。这个批改如果没有被客户端拉取到,那么客户端就会在原定工夫触发揭示。尤其是 IOS 客户端本地,采纳零碎级别 localnotification 触发揭示,无奈阻止。这最初必然导致用户投诉,业务方口碑受损。

音讯推送模式次要分为 拉取和推送 两种,通过组合能够造成如下表出现的几种模式。各种模式各有优劣,须要依据具体业务场景进行考量。

通过衡量,QQ 团队采取图示 混合模式——推拉联合。即容许局部用户提前拉取到工作,未拉取的走推送。这个预下发的提前量是揭示当天 0 点开始。因而 QQ 团队也强制要求业务方不能在揭示当天再批改工作信息,包含揭示工夫和揭示内容。因为当天 0 点之后用户就开始拉取,所以必须保障工作工夫和内容不变。

2.2 异构存储

零碎次要会有两局部数据:

  • 业务方创立的工作数据。蕴含工作的揭示工夫和揭示内容;
  • 用户订阅生成的订阅数据。次要是订阅用户 uin 列表数据,这个列表元素级别可达到千万以上,并且必须要可能疾速读取。

该我的项目存储选型次要从访问速度上思考。工作数据可靠性要求高,不须要疾速存取,应用 MySQL 即可。订阅列表数据须要频繁读写,且推送触发时对于存取效率要求较高,思考应用内存型数据库。

最终 QQ 团队采纳的是 Redis 的 set 类型来存储订阅列表,有以下益处

  • Redis 单线程模型,无效防止读写抵触;
  • set 底层基于 intset 和 hash 表实现,存储整型 uin 在空间和工夫上均高效;
  • 原生反对去重;
  • 原生反对高效的批量取接口(spop),适宜于推送时应用。

2.3 多重触发

再问各位读者一个问题,计时服务个别是怎么做的?分布式计时工作有很多成熟的实现计划,个别是采纳提早队列来实现,比方 Redis sorted set 或者利用 RabbitMQ 死信队列。QQ 团队应用的挪动端 QQ 通用计时器组件,即是基于 Redis sorted set 实现。

为了保障工作可能被牢靠触发,QQ 团队又减少了本地数据库轮询。如果内部组件通用计时器没有准时回调 QQ 团队,本地轮询会在提早 3 秒后将还未触发的工作进行触发。这次要是为了避免内部组件可能的故障导致业务触发失败,减少一个本地的扫描查漏补缺。值得注意的是,引入这样的机制可能会带来工作屡次触发的可能(例如本地扫描触发了,同一时间计时器也复原),这就须要 QQ 团队保障工作触发的幂等性(即屡次触发最终成果统一,不会反复推送)。触发流程如下:

2.4 可控调度

如前所述,当多个千万级别的推送工作在同一时间触发时,推送量是很可观的,零碎须要具备总体的工作间调度控制能力。因而须要引入 调度器,由调度器来管制每一秒钟的推送量。调度器必须是分布式,以防止单点服务。因而这是一个分布式限频的问题。

这里 QQ 团队简略用 Redis INCR 命令 计数。记录以后秒钟的申请量,所有调度器都尝试将当前任务须要下发的量累加到这个值上。如果累加的后果没有超过配置值,则持续累加。最初超过配置值时,每个调度器依照本人抢到的下发量进行下发。简略点说就是下发工作前先抢额度,抢到额度再下发。当额度用完或者没有抢到额度,则期待下一秒。伪代码如下:

CREATE TABLE table_xxx(
    ds BIGINT COMMENT '数据日期',
    label_name STRING COMMENT '标签名称',
    label_id BIGINT COMMENT '标签 id',
    appid STRING COMMENT '小程序 appid',
    useruin BIGINT COMMENT 'useruin',
    tag_name STRING COMMENT 'tag 名称',
    tag_id BIGINT COMMENT 'tag id',
    tag_value BIGINT COMMENT 'tag 权重值'
)
PARTITION BY LIST(ds)
SUBPARTITION BY LIST(label_name)(SUBPARTITION sp_xxx VALUES IN ( 'xxx'),
    SUBPARTITION sp_xxxx VALUES IN ('xxxx')
)

调度流程如下:

值得关注的是,幂等性如何保障呢?讲完了调度的实现,再来论证下幂等性是否成立。

假如第一种状况,调度器执行一半挂了,前面又再次对同一个工作进行调度。因为调度器每次对一个工作进行调度时,都会先查看工作以后残余推送量(即工作还剩多少块),依据工作的残余块数来持续调度。所以,当工作再次触发时,调度器能够接着后面的工作持续实现。

假如第二种状况,一个工作被同时触发两次,由两个调度器同时进行调度,那么两个调度器会相互抢额度,抢到后用在同一个工作。从执行成果来看,和一个调度器没有差异。因而,工作能够被反复触发。

2.5 打散执行

工作分块执行的必要性在于:将工作打散分成小工作了,能力实现细粒度的调度。否则,几个 1000w 级别的工作,各位开发者如何调度?如果将所有工作都拆分成 5000 量级的小工作块,那么速率管制就转化成分发小工作块的块数管制。假如配置的总体速率是 3w uin/s,那么调度器每一秒最多能够下发 6 个工作块。这 6 个工作块能够是多个工作的。如下图所示:

工作分块执行还有其余益处。将工作分成多块平衡调配给后端的 worker 去执行,能够进步推送的并发量,同时缩小后端 worker 异样的影响粒度。

那么有开发者会问到:如何分块呢?具体实现时调度器负责按配置值下发指令,指令相似到某个工作的列表上取一个工作块,工作块大小 5000 个 uin,并执行下发。后端的推送器 worker 收到指令后,便到指定的工作订阅列表上(redis set 实现),通过 spop 获取到 5000 个 uin,执行推送。

2.6 引入音讯队列

一般来说,音讯队列的意义次要是 削峰填谷、异步解耦。对本我的项目而言,引入音讯队列有以下益处:

  • 将任务调度和工作执行解耦(调度服务并不需要关怀工作执行后果);
  • 异步化,保障调度服务的高效执行,调度服务的执行是以 ms 为单位;
  • 借助音讯队列实现工作的牢靠生产(At least once);
  • 将刹时高并发的任务量打散执行,达到削峰的作用。

具体的实现形式上,采纳队列模型,调度器在进行上文所述的工作分块后,将每一块子工作写入到音讯队列中,由推送器节点进行竞争生产。

2.7 At least once 推送

实现用户级别的可靠性,即要保障所有订阅用户都被至多推送一次(At least once)。如何做到这一点呢?前提是当把用户 uin 从订阅列表中取出进行推送后,在推送后果返回之前,必须保障用户 uin 被妥善保留,以避免推送失败后没有机会再推送。因为 Redis 没有提供从一个 set 中批量 move 数据到另一个 set 中,这里采取的做法是通过 redis lua 脚本来保障这个操作的原子性,具体 lua 代码如下(近似):

redis.replicate_commands()
local set_key, task_key = KEYS [1], KEYS [2]
local num = tonumber(ARGV [1])
local array
array = redis.call('SPOP', set_key, num)
if #array > 0 then
    redis.call("SADD", task_key, unpack(array))
end
return redis.call('scard', task_key)

推送流程整体如下

2.8 容灾计划

订阅推送零碎最重要的是保障推送的可靠性。用户的订阅数据对于零碎来说是重中之重。因而,业务团队采纳了异构的存储来保证数据的可靠性。每一个用户订阅事件,都会在 CKV(腾讯自主研发的 KV 型数据库)中记录,并将用户 uin 增加到 Redis 中的订阅汇合。在任一零碎产生故障时,能够从任意一份数据中复原出另一份数据,造成互备。同时,Redis 存储也应用了腾讯云的 Redis 集群架构。采纳了 2 正本、3 分片的模型,以进一步提高可靠性。

03 总结

上文阐述了如何在高并发的根底上实现可控和牢靠的工作推送。这个计划能够总结为 Dispatcher+Worker 模型,其核心思想是分治思维,相似于在一条快递流水线上先将大包裹化整为零,宰割成规范的小件,再分发给流水线上的泛滥快递员,执行标准化的配送服务。高性能大流量推送机制是腾讯 QQ 在实在业务高并发场景下积淀的高效经营能力,在无效晋升用户活跃度与粘性方面效果显著。

腾讯 QQ 团队在服务外部各个业务条线的同时,也将这部分外围能力进行了形象、解耦和积淀,能够作为通用能力服务于各个行业及 B 端业务。相干技术服务信息,在腾讯挪动开发平台(TMF)能够获取。以上便是整个 QQ 揭示订阅推送零碎的实现思路和计划。欢送各位读者在评论区分享交换。

-End-

原创作者|许扬
技术责编|许扬

你可能感兴趣的腾讯工程师作品

| 算法工程师深度解构 ChatGPT 技术

| 腾讯云开发者 2022 年度热文盘点

| 3 小时!开发 ChatGPT 微信小程序

| 7 天 DAU 超亿级,《羊了个羊》技术架构降级实战

技术盲盒:前端|后端|AI 与算法|运维|工程师文化

退出移动版