手机QQ公众号亿级消息实时群发架构

编者按:高可用架构分享及传播在架构领域具有典型意义的文章,本文由孙子荀分享。转载请注明来自高可用架构公众号 ArchNotes。

 

孙子荀,2009 年在华为从事内核和分布式系统的开发工作;2011 年在百度从事过高性能计算方面的工作;2012 年加入腾讯进行 QQ 群广告系统的开发,随后负责腾讯云加速的带宽调度系统的设计研发;2014 年开始手 Q 公众号后台设计开发。 腾讯优秀讲师,包括 Linux 内核的讲授,并行计算等课程。在内核、数据挖掘、计算机广告等方面有很深的造诣。

 

手 Q 公众号是从去年底的开始开发,期间封闭开发半年时间,基础能力已经对齐微信,而且在其他领域有新的拓展。目前已经对腾讯系的业务开放,对外小范围开放。等待政府批文之后,将全面开放注册。

 

现在业务规模支撑百万公众号,关系链存储上T 。机器规模春节期间在数百台,有深圳天津两个中心,分散在十几个 IDC 机房。消息规模每天数十亿,支撑了手 Q 一半以上的日活,包括腾讯新闻、QQ 音乐、附近、天气、购物号、订阅号、兴趣号等,只要是手 Q 非个人好友,基本都是公众号。

 

我去微信进行授课的时候,和微信的同学做过交流,发现大家的技术挑战和问题的场景以及背景有着很大的不同,无法简单的在技术上无法复用。

 

这里举个例子:QQ 公众号的关系链设计之初是为了承载亿级的,这点和微博的收听关系很像。而微信现在在用 MySQL 集群的方式,主要都是万级别的关系链。亿级在支持高并发访问和 IDC 一致性的时候就更加有挑战。

 

QQ 公众号从开始就支撑公司内部的亿级 DAU 的业务,比如音乐、腾讯新闻、春节红包等这样的业务。 消息峰值在每秒数十万 。 而微信主要是对外,外部用户加起来很难达到这个量级。

 

今天受高可用架构邀请,主要介绍的是 QQ 公众号的群发子系统,也是公众号业务用使用频率最高的一个功能。

 

群发需求、场景分析

 

群发,无论是微信还是 QQ 公众号都是使用功能最多的业务。

 

在微信,群发只是公众号运营者对其关注者进行消息发送,然而在 QQ 公众号里面,群发需要支持非常多的复杂组合模式。 比如自定义号码包、关系链&分组、标签、人群定向等,这是千人一面。 现在我们也支持了千人千面的群发方式。

群发我们一共开发了 4 期,从基础设施到调度能力。现在是 5 期的发送效果改造(通过 pCTR 开始从营销往生态用户考虑)。这个过程我们在不断的抽象各种层次关系。

 

我们先说最简单的场景:关系链发送。我们通过关系链接口,拉取一批用户,发送一批。当然这个过程可以并行。到了后来有了业务方提供自己挖掘的号码包(腾讯基本各个大型业务都会有自己的挖掘团队)。然后我们发现可以复用这种方式,无非是针对不同底层存储( NoSQL、文件或者 MySQL)实现一个 JDBC 那样的存储适配驱动,最终都是 getBatchFromxxx 、endBatchToxxx 。但是这样的系统,任务不能自己恢复,没有状态维护,发送和拉取严重耦合在一起,谁按照需求发展都不独立。

 

于是我们决定将所有的发送都收敛到号码包。无论来自哪个存储,都收敛到一个发送的号码包文件。通过文件解耦, 有了这个文件,就天然有了作业任务的一个输入条件,形成一个作业任务,交给作业系统。作业系统负责任务的调度分发、排队、self-heal 。

 

这个系统里面有像腾讯新闻一样,一次发送上亿用户的,而且要求峰值每秒数十万的速度,更多的有像订阅号那样一次发送几万,每秒并发几百个任务一起发送的。

 

 

在包处理层,我们处理各个需求,对于业务自己挖掘的号码包,我们做关系链过滤、CRC 过滤(不要重发)、频次配额过滤、黑名单过滤、定向条件过滤等等。

 

这里使用了一个 GoF 的责任链模式, 根据业务号的属性和当前的模式来决定到底需要开启哪些过滤器插件。

 

在这层根据包来源的介质,我们做了加速处理。

 

对于十几亿的粉丝,拉取时间受限于关系链的存储网络开销,业务机的带宽维持在 20Mbyte/s,就算并行也有时间浪费。我们希望永远不要因为预处理耽误时间。每次群发都有这么大的网络开销是十分可惜的。 

 

所以我们把超过千万的粉丝全量拉取到本地。同时受益于关系链变更的时候,我们会有专门的消息队列用于生产事件,各个需求模块监听按需处理,使得增量同步保证短时间的一致性也变得非常高效。于是我们设计了一个模块用于把大于某个粉丝级别的用户保存到本地,这里其实只需要保存二进制文件。 算一下 1亿 * 8byte 大约 760M 的样子。 这样对于 1T 的磁盘,单机就可以满足公众号 所有千万级以上号码的粉丝。

 

同时产生一个固定频率的增量文件。每次使用的时候进行全量的扫描,再做一遍增量文件的逻辑(如:{"uin":"A"; "op" :"关注";"time":""};{"uin":"A"; "op" :"取消关注";"time":""})。

 

PS:在腾讯一直有一个思想叫一切皆可控,就是系统服务能力不是压测出来的,而是设计的时候,就清楚的知道单机的服务能力,根据网络模型、级联带宽、包大小处理能力等进行预估。这就需要设计的人有内核、应用层编程、技术选件的把握,要求挺高的,腾讯的后台 T4 才能有这个 level 。:)

 

对于加速文件,当集群机器过多的时候,如何保证的关系链文件已经存在集群中所有机器?

  • 一种方式就是放在分布式文件系统上,然后本机定时从中拷贝出来,但是会导致 CFS 出口带宽压力。

  • 一种就是通过 BT 来分发,我们团队之前是从事下载的,这个在旋风时代做的已经非常多了。通过 btsync 来做到内网搭建一个 DHT 网络进行 P2P 下载。速度非常快。

 

但是优化都可能导致引入新的问题。如果有粉丝剧烈的变化的情况,刚刚的优化其实是不可靠的。

 

于是这里我们坚持一定要进行实时的合理性校验。但是如果每个都去校验效率非常低,不实际甚至不如不优化。 我们通过抽样校验,随机抽取10份样本, 每次采样的数目范围在 [100,10000], 具体是粉丝总数 / 1万,错误率控制在 10次错误率的算术平均值必须小于 2%,且单次不能超过 10% 。否则我们会放弃优化回源从原始的存储业务服务拉取。

 

说这个是希望大家知道任何优化可能都是有损的,还是要考虑可能的风险。

 

群发架构

 

层级看起来如下:

 

框架从设计的开始就有以下几个目标:

  1. 分层,任意层都可以水平扩展。

  2. 不依赖本地文件。

  3. Self-Heal(机房、IDC)。

  4. 平滑升级。

  5. 动态调度。

 

任意层水平扩展

就是对于任何运行的状态都能恢复,比如 Spark 的 check point 机制。我们所有的任务,在生命周期都通过腾讯的容灾 CDB 进行状态落地,任务文件都有流水保存。 任务都存放在 CDB 中作为一条作业,接口层和任务调度层之间通过我们设计的无主并行任务分配算法(Acentric Parallel Task Allocation)进行无损的任务分发(参考了思科的 OSPF )。任务调度层的机器任意添加故障都不会导致任务丢失,而且并行进行任务的批量调度。在调度层和任务分发层以及执行层,通过 ZMQ 这样的消息组件进行消息传递。多生产和多消费者模式。

 

APTA 介绍

APTA 其实就是一致性HASH + OSPF 路由算法。 简单介绍如下:

首先作业集群中的机器有几种状态

 

集群状态

ONLINE : 表示机器在线 ,已经被集群所有的机器知道。

JOINING: 表示机器刚刚进入集群,还没有被接受。

WORKING: 表示机器已经加入整个作业的分配中。

OFFLINE : 表示机器已经失去联系。且与任何机器都没有连接。

SILENCE: 静默状态,表示当前不进行任务的处理工作。

 

下面是集群的状态扭转图:

 

ONLINE-> SILENCE :在ONLINE状态下收到作业进程表里面所有作业进程回复的LSR包。

SILENCE -> WORKING :SILENCE状态维持12s 自动变成WORKING 。

WORKING->SILENCE :一旦收到任意一个服务过来的LSR包。

WORKING->OFFLINE: 在WORKING状态下 没有收到任何Hello包,自己发出的Hello包也没有任何回复。

 

我们不需要一台作业进程所在的机器被集群中一半以上的机器认可,才能正常工作。 因为主要这台机器不脱离集群 ,和任意一个作业机器有网络连接。都可以存活在 作业集群中。 拥有处理作业任务的能力。

 

 

 

上面这种场景,蓝色机器是完全被接受的。因为他可以进行作业任务。

 

动态调度

 

首先所有调用接口的任务都附属状态机,跟着任务的作业状态进行扭转。

 

由于执行作业的机器槽位永远小于需要被执行的任务,所以我们需要在调度层决定哪些业务可以被调度。在这里我们参考了 Linux 操作系统的优先级调度,对于每一个作业任务区分是实时作业还是非实时。

 

实时:紧急任务,无法被抢占。

非实时:支持抢占。

 

根据公式得出一个任务的调度分,然后根据这个调度分来排入优先级队列。

 

任务离开调度层都开始真正的作业。第一步就是接受预处理,就是刚刚说号码包处理的逻辑。处理完毕之后就需要真正的发送了,我们在分发的接口层会评价任务需要的资源。然后根据下游执行机器上的 manger 程序上报的负载情况来决定如何进行任务拆分和资源的分配。

 

内存和任务的大小有关,CPU 网卡带宽的需求和发送速度有关。 速度如果业务没有权利设置,那就是我们根据全局的负载计算出来的。 受限于网卡的处理能力和下游服务器的处理能力,其实就是一个受限的最优化问题 。我们会优先选择单机的资源,如果单机得不到满足就拆分任务单元,放到多个机器进行发送。 这里看到和 yarn 不同,我们把业务调度和资源分配分在了不同层和阶段来解决,其实简化了问题。

 

我们没有用 Docker 这样的技术,感觉还没必要。但是在作业执行层,我们对于不同的作业有发送的网卡需求,有需要写 CFS 的网卡需求,为了保证有限网卡的独立性,我们通过 cgroup 的 net_cls 来分配业务进程的带宽。 使得同一机器上的不同进程资源不受影响。

 

补充一点,刚刚的状态机,在各个状态转换的时候,用了MySQL 的 trigger ,由专门的程序来处理,然后生产到消息队列给外围的系统,解耦开来。

 

为了保证一定的到达率,我们发送都是在线发送,只对在线的用户进行发送, 发送完毕在线的,这个作业就算结束了,对于用户没有在线那么我们就会发到这台机器上的一个leveldb ,形成一个链表,接入我们公司的用户登录触发组件。当用户登录了,就把用户身上的这个任务链表遍历,调用接口进行发送。

容灾

 

其实有了比较好的基础设施之后,容灾会变得简单。

 

群发核心挑战是深圳的任务如果执行一半失败了,是否能在天津恢复。 这里的核心问题就是数据如何同步。

 

首先我们群发每次发送的时候都会通过 hippo(腾讯的一款消息队列组件)进行上报。 原始的号码包保存在了深圳的 CFS 仓库。当作业失败了,天津具有了这个原始号码包文件,并且从 hippo 获得作业信息和发送的号码列表,diff 出差异文件,在天津重建余量的发送任务。如果深圳任务成功则删除该号码包文件。

 

单 IDC 问题就更简单了。任何时刻任务崩溃了,我们都可以正确的找到上一次执行的位置,然后重新建立一个任务,根据上一次执行的阶段,设置适合的状态,在状态机中重新触发相应的处理逻辑。

 

现在我们群发系统,我们除了工程上的技术优化以外,主要在整个效果控制的工程建设上进行。

 

平台控制

为了使得我们平台能够良好的运转,用户不收到过多对用户没有价值的消息 。 我们和产品从各个纬度按照规则和背景制定了下面的控制手段。

 

B 端控制

配额 ,频次。 根据业务上一次投放的效果点击率 配合产品的运营策略 来 分配配额。

 

C 端控制

频控,决定一个 pCTR ,平台策略,新鲜度决定一个用户能收到什么样的消息。频次均匀随机释放,一天用户可以收到最多6条公众号消息。

 

为了防止系统出现 B 运营者,在第二天 11:45 的时候,直接建立第二天 0:00 分的任务(系统限制只能建立 15 分钟之后的任务)导致第二天释放的用户频次被立刻抢走(因为没有竞争)。 这种先到先得的方式使得整个公众号平台不能良性的运转, 所有B的运营者都争先恐后的建立任务,出现所谓的火车票抢票情况。

 

于是我们系统变成了在当天随机释放频次。一种简单的实现方式是:假设一天一个 C 受限制只能收到 6 条,那么可以从 0 点开始,每隔 4 小时(这个间隔称之为竞争区间)随机释放一个,这样一天 24 小时就释放了 6 条。

 

实际运行中我们早上 12 小时有 2 个竞争区间,释放 2 个 C 频次。下午 12 小时有 4 个竞争区间,释放 4 个频次。 当前竞争区间没有被消耗的频次自动保存到下一竞争区间。每个B的任务,只和当前竞争区间内将要发送的任务进行竞争。

 

 

Q & A

1、刚才提到群发只给在线用户发,用户是否在线的查询是通过什么样的方式,这块有没有瓶颈?

这里我们对于用户上线有实时的收集,然后会放入消息队列,然后每台机器都会安装一个 bitmap 的共享内存,42 亿bit , 所以判断在线,只要访问共享内存就可以了。

 

2、系统的升级控制是怎么做的?多长时间可以完成升级?

我们所有的程序都有管理端口,需要升级了通过管理端口发出指令,业务程序完成自身作业之后,就会从集群中退出,然后开始升级的流程,升级完毕之后 在自动上线处理任务。是一个平滑的升级过程,全系统升级只需要发送指令即可。完成一次升级的时间,取决于当前系统任务的繁忙程度。

 

3、现在对于一条需要群发给上亿用户的消息,最快可以做到多长时间群发完?影响群发速度的瓶颈主要在哪一块?

春节上 10 亿的消息,我们用了 15 分钟,取决于机器数目,都是并行拆分,可以更快。事实上我们能做到秒级别。因为我们其实系统有一个预送达的策略,也就是实现发送到用户的终端,在时间到了之后,端进行统一展现,特别适合春节抢红包的场景。

 

4、发送时候可能不成功或用户刚好离线,这一块怎么处理的?

没有查询到的状态就进入了 leveldb 的发送任务列表,等到在线了之后,实时触发,只要用户打开手 Q 就会实时下发 。

 

5、上文提到离线用户是存在本地 leveldb,多个群发任务如果是分布在多机,这个文件怎么 merge ?用户上线时候怎么获取全部的离线消息?

leveldb 的数据来自于分布式文件系统的文件,所以其实只有一份。

 

6、这个 CFS 仓库是跨 IDC 的吗?如果深圳的机房出现问题,天津机房能获得号码包文件吗?

问题问的好,CFS 不跨机房,天津深圳是两个仓库,我们依赖 VLAN 专线进行两个仓库的同步。事实上我们明年会使用支持 geo 的分布式文件系统,ceph 或者 glusterfs 这里还在做验证。

 

7、这个群发系统会落地吗?比如手机端重装了,再打开的话是否能看到之前群发的消息?

这个依赖于终端的实现,从后台看只要用户阅读过消息,我们后台存储会被抹去,就不会再次下发的。

 

8、请问你们的任务的状态机有多大规模?

状态机的规模大概有 2 个状态空间,二十几种状态。中断之后都是秒级别恢复 。 任务一天的级别在几十万 这些都是可以水平扩展的。

 

9、听说微信一秒推送 1.2 亿,您的数据里峰值提到了数十万,这个差在哪?

微信消息通道机器规模是我们的 20 倍甚至更多。

 

10、每个人收到的消息频率是不一样的或者每个人消息内容不一样,发送的时候做这种过滤,数据量这么大怎么计算?

这里的计算量非常大。我们采用的是对于一个号码包进行并行的多线程处理拆分。发送的时候处理,统一在预处理层进行处理。 其实按照用户维度差不多是14亿用户, 每个用户的信息都存放在 leveldb 上,单机 1T 磁盘就可以 hold 住存储。leveldb 进行按照 hash 的拆分,可以把 CPU 性能榨干。

 

11、群发时有无流量控制?或者 QOS 级别?cgroup 的 netcls 能否具体一些?

tc 建立两个 classid ,cgexec -g net_cls 创建2个 cgroup 组,然后把需要限制带宽的进程加入到这个 cgroup 组中。

 

12、群一般都有人数限制,比如1000,2000,这个更多的是考虑业务上再多也没啥意,还是技术上消息广播有压力?还有类似于聊天室可能没有人数上限这一说,比如一个主播有一千万的观众的弹幕聊天,这种推送和我们的群处理起来有区别吗?

聊天室应该在万级别,而且聊天消息推送的场景 应该类似一个广播写入用户的未读列表就可以了。是一个 Pull 的过程,群发是一个过亿的 Push 的过程 在 Push 之前并不知道要 Push 的对象 需要实时产生。

 

13、随机释放频次,会不会导致消息到达客户端的时间不一致?这种情况怎么处理?

不会不一致。消息到达客户端取决于公众化运营者设置的发送时间和持续时间。这个我们会严格遵守。

 

14、对于 iOS 是用长链接发送还是苹果的消息通知,如果中间经过苹果怎么保证不把苹果搞挂?

这里对于离线走的是苹果的 PUSH 通道,然后触发一次长链接拉取。但是我们没有遇到把他们搞挂的情况,对于营销性质的发送,我们会选择走预送达通道。

 

15、前面提到 IDC 之间数据同步,微信的分布式文件系统或分布式数据库是跨 IDC 的吗(如果跨 IDC ,对于 IDC 之间通讯时延有要求吧)?这一块能否介绍详细一点?

微信应该没有用 CFS,这个问题刚刚回答过,现在的 CFS 是不支持 geo 的,我们用了 VLAN 专线,我们正在测试 ceph 等文件系统进行替换。

 

16、刚才有提到定期增量文件,这个频率大概是多少?什么时候会做 merge?

实时 merge,收到一个增量就进行处理。 每天会通过 BT 同步全量。

 

17、想请教一下发送消息的状态是怎么控制的?是传统的那种在数据库设置发送表做的么?

状态先写入 ZooKeeper ,然后由总控程序从 ZooKeeper 获得已经确认的状态写入数据库。

 

转载自http://chuansong.me/n/2071796

 

 

posted @ 2018-09-04 12:25  杉枫  阅读(874)  评论(0编辑  收藏  举报