转自:
http://mp.weixin.qq.com/s?__biz=MjM5MDE0Mjc4MA==&mid=2650995675&idx=1&sn=712855991e235aeb89b5d3e51c5be312&chksm=bdbf03888ac88a9ef1157c5ab6c075912b651f835dae1ecb7e2e5a635231321a2a2e135edb87&mpshare=1&scene=23&srcid=0324lK9FxjIsXEMooOcHCMJn#rd原作者:许灵锋,周海发,InfoQ
QQ春节红包以一个又一个的整点刷红包活动贯穿年三十,在除夕夜达到顶峰,是典型的海量用户秒杀场景,如何应对海量的用户刷红包洪流,保证刷得爽,红包安全到账,是QQ红包设计要解决的关键技术难点。
另外,红包项目涉及手Q终端、手Q后台、QQ钱包(财付通)系统、礼券系统、公众号等诸多业务系统,流程长且多,各系统性能吞吐量差异很大,如何保证各系统形成一个有机整体,协调高效提供服务,也是难点之一。
下图为简化后QQ红包的架构,包括接入层、抽奖系统、存储系统、发货系统、公众号消息通知和CDN资源等几部分,请大家先有一个整体的认知,便于阅读下文。
本文将重点讲解接入层、抽奖系统和发货系统。
接入层是红包后台服务的大门,负责抽奖请求预处理,确保有效的请求才透传给后端服务。为保证自身高可用、高稳定,接入层还可实时控制手Q请求频率,避免海量请求压垮接入层,出现不可控局面。
在海量服务场景下,为避免网络开销,方便后端服务使用cache提升性能,接入层采用了一致性Hash寻址,保证同一个用户的请求只会落在同一台红包抽奖逻辑机器处理。
抽奖系统作为QQ红包的核心系统,在承接用户抽奖请求,按设计合理的几率完成抽奖操作,将抽奖结果安全落地保存,并顺利发货等过程中,起到了关键作用。面对海量抽奖请求,如何及时作出响应,是抽奖系统面临的难题。
为了解决这些问题,我们采用了一些设计方法:
抽奖系统的架构如下图所示:
业务要求在每个刷一刷的活动中,能对用户中奖次数、参与时间(30秒)进行限制。如果用户的每个抽奖请求到来时,先到存储层获取用户的中奖历史信息,再判定用户是否还有抽奖资格,在海量高并发的请求场景下,势必会对存储层造成巨大的压力。
所以这里我们引入了本地内存缓存层,用于保存用户的中奖历史信息,每次请求到来时,会先到缓存层获取用户的中奖历史信息,如果在缓存层没找到,才会到存储层获取,这样就不会对存储层造成太大的压力,同时也能实现业务的需求。
缓存层我们采用开源Memcached组件实现。
红包抽奖系统是一个分布式的系统,因此为了使缓存机制生效,我们在手Q接入层使用了一致性hash的路由算法进行寻址,保证同一个用户(uin)的请求总会落在同一台逻辑机器进行处理。
由于手Q后台既要处理客户端的二进制请求,也要处理其他Web系统的HTTP请求,所以协议处理模块的首要任务就是兼容各种格式的协议,给后端模块一个最简单的结构。为此我们制定了Protobuf格式的交互协议(兼容JSON格式,会统一转换成Protobuf处理),传给后端模块。
手Q春节红包是通过很多场定时“活动”来发放红包的。每场活动里面能发放多少现金,能发放多少虚拟物品,发放的比例如何,这些都是配额数据。
更进一步,我们要做到精确控制现金和虚拟物品的发放速度,使得无论何时用户来参加活动,都有机会获得红包,而不是所有红包在前几分钟就被用户横扫一空。
聚焦到抽奖,QQ红包的抽奖算法其实并不复杂,但是能否满足产品需要非常重要。我们的设计思路是至少需要满足如下需求:
为此,我们设计了如下的抽奖流程算法:
需要说明的是,只要是因为配额限制发放红包失败,我们都会继续尝试给用户发放其他奖品的红包,直到没有奖品可以发放,这样我们就能保证把奖品尽可能发放出去。
流水系统用于保存活动过程中的抽奖流水记录,在活动后对奖品发放和领用进行统计和对账。该系统还定时对领用失败的请求进行重做和对账,确保奖品发放到用户账户里。
由于流水需要记录用户中奖的信息和领用的的情况,数据量巨大,所以抽奖逻辑层本地采用顺序写文件的方式进行记录。
抽奖逻辑层会定期的把本地的流水文件同步到远程流水系统进行汇总和备份,同时,流水系统会对领用失败的流水进行重做,发送请求到抽奖逻辑层,抽奖逻辑层会调用发货系统的接口完成发货操作。
存储层的设计向来都是后台架构设计中的重点和难点。目前腾讯公司内部较成熟的NoSQL存储系统有CKV、Grocery,经过一番对比我们选择使用Grocery,主要原因有以下几点:
强大的带条件判断的分布式原子算数运算
抽奖逻辑里需要对每个奖品进行计数,避免多发少发,所以一个高效可靠的分布式原子加计数器显得格外重要,Grocery支持带条件判断的原子加计数器,调用一次接口就能完成奖品计数值与配额的判断以及奖品计数值的增加;
灵活的数据类型
Grocery支持Key-Key-Row类型的数据存储格式,可以灵活的存储用户的红包中奖信息,为获取用户单个红包或者红包列表提供了丰富的接口;
部署、扩容方便
Grocery有专门的团队支持,易于部署和扩容。
平滑限频设计
每一种奖品,对应的业务都有他们自己的容量能力,且各业务的能力也不尽相同(如黄钻4w/s,京东2k/s等)。为保证红包活动持续进行,抽奖系统必须严格按业务控制派发峰值。派发峰值支持实时可调,避免由于业务方评估不足引起过载。
考虑这样一种场景,如果请求是在1秒的最开始全部涌到业务方,受限于业务方不同的架构实现,有可能会触发业务方的频率限制或者是过载保护。为此,我们将频限粒度调整到百毫秒,这样奖品就会在1秒内相对平均的发放,从而解决了上述问题。
QQ红包奖品包括现金和礼券两类,现金对接财付通,礼券又分腾讯公司内部虚拟物品和第三方礼券。最终礼品落地到用户的账户(QQ钱包余额、QQ卡券或第三方系统账户)中。虽然抽奖系统有作平滑处理,但持续长时间的大流量发货,也可能导致业务系统不能正常提供峰值下的服务能力。
如何承上启下,既预防抽奖系统不能平滑地发货导致压跨发货系统(自身),又能保护后端业务系统的情况下,以较快的速度将奖品安全发放到账,是发货系统的设计要点。
发货系统设计遵循以下策略:
发货系统架构如下图所示:
现金和礼券后端的系统完全不同,现金通过QQ钱包系统发放入财付通账户,要求实时到账不能延迟。
而礼券对接的后端业务千差万别,服务容量和性能各不相同。为了不让慢速的礼券发放影响快速的现金发放,将现金通道与礼券通道分离,互不干扰。
由于用户来抽奖的时机完全是随机的,抽奖系统并不能做到绝对平滑发货。任由抽奖系统将发货请求直接透传到业务系统,将出现不可预知的问题,严重时可能会导致业务系统雪崩,这是不能接受的。
另外象游戏礼包类、滴滴券等第三方礼券,可能用户账户并不存在(用户并不玩该款游戏,或用户并没有第三方账户),需要先引导用户创建账户才能发货,这就要求发货系统有暂存奖品信息,具备延后发货的能力。
发货系统采用开源的RocketMQ消息中间件作为异步消息队列,暂存发货请求,再由礼券发货模块根据各业务的限速配置均匀地调用业务接口进行发货。
礼券类奖品通过异步方式发放到用户账户,在除夕高峰值可能发放速度跟不上抽奖速度,会延后一些时间才能到账,这对不明真相用户可能会造成困扰。因此在用户中奖信息页面中,会提示用户24小时(或48小时)到账。
发货过程的每个步骤,都有可以异常失败,导致发货不成功,因此在物品详细页面的按钮支持多次发起发货,在“礼券发货”模块根据发货状态,可以多次尝试发货,并保证一个奖品只发放一次。
前面已经提过,发货系统通过异步消息队列,将抽奖系统与业务开发隔离开,抽奖洪峰不会直接影响业务系统,对业务系统起来隔离保护作用。
礼券发货模块针对每个业务单独配置限速阈值,对各业务的发货严格以不超过限速阈值的速度发放奖品,如果业务有超时或提示超速,再按一定比较再减速。
礼券发货模块首先会到存储系统检查奖品是否真实有效,再到发货状态存储检查状态是否正常,只有真正需要的发货的奖品才向业务系统发起发货请求,确保发货的有效性,避免错发和多发。
由于采用异步发货,抽奖时刻奖品不能保证立即发放到用户账户中。但用户的奖品不会丢失,通过在异步队列中暂存,礼券发货模块逐步以合适的速度将奖品发放到用户账户中。
如果发货过程中有延时或失败,用户可以通过多次领取提起发货请求,系统支持多次提交。
如果多次发货仍然失败,对账工具第2天会从流水系统中将用户抽奖数据与发货数据进行对账,对发货异常用户再次发起发货。如果对账仍然失败,则提醒管理人员介入处理。
普通用户不会关心QQ红包的后台有多复杂,他们在手Q终端抢红包时的体验直接决定着用户对QQ红包的评价。对用户来说,看到红包后能否顺畅的抢和刷,是最直接的体验痛点,因此需要尽可能降低延迟以消除卡顿体验,甚至在弱网环境下,也要能有较好的体验。
为了实现该目标,手Q终端采取了以下优化策略:
QQ红包中用到的不经常变化的静态资源,如页面,图片,JS等,会分发到各地CDN以提高访问速度,只有动态变化的内容,才实时从后台拉取。然而即使所有的静态资源都采用了CDN分发,如果按实际流量评估,CDN的压力仍然无法绝对削峰。
因为同时访问红包页面的人数比较多,按83万/秒的峰值,一个页面按200K评估,约需要158.3G的CDN带宽,会给CDN带来瞬间很大的压力。
为减轻CDN压力,QQ红包使用了手Q离线包机制提前把红包相关静态资源预加载到手Q终端,这样可大大降低CDN压力。
目前手Q离线包有两种预加载方式:
2.59 亿用户同时在线,用户刷一刷时的峰值高达83万/秒,如果这些用户的操作请求全部同时拥向后台,即使后台能抗得住,需要的带宽、设备资源成本也是天文数字。
为了尽可能减轻后台服务器压力,根据用户刷一刷的体验,用户每次刷的操作都向后台发起请求是没有必要的,因此手Q在终端对用户刷一刷的操作进行计数,定时(1~3秒)异步将汇总数据提交到后台抽奖,再将抽奖结果回传到手Q终端显示。
这样既保证了“刷”的畅快体验,也大大减轻后台压力,抽奖结果也在不经意间生产,用户体验完全无损。
对用户进行分组,不同组的用户刷一刷红包(企业明星红包、AR红包等)的开始时间并不相同,而是错开一段时间(1~5分钟),这样通过错开每一轮刷红包的开始时间,可以有效平滑用户刷一刷的请求峰值。
手Q终端和后台并不是两个孤立的系统,而是一个整体。手Q系统搭建有一整套的负载监控体系,当后台负载升高到警戒线时,手Q终端可以根据后台负载情况,动态减少发向后台的请求,以防止后台出现超载而雪崩。
在刷一刷红包和AR红包过程中,当用户已经抽中的奖品数达到一个限值(例如5个),用户不能再中奖,这时用户的抽奖请求不再向后台发送,而是终端直接告知用户“未中奖,请稍后再试”,和清除AR红包地图中的红包显示。
春节红包大战,从企业红包演变到刷一刷红包、个性化红包和AR红包,玩法不断创新,用户体验更好,活跃度提升,参与人数也从2亿增长到17年春节的3.42亿。
QQ个性红包是在红包外观上的一次大胆尝试,借助该功能,用户可使用霸气的书法体将自己的姓氏/或其他文字(提供自动简繁体转换)镌刻在红包封皮上。
此外,我们还提供了具有新年氛围的贺岁红包、与腾讯IP紧密结合的QQ family、游戏形象、动漫形象等卡通红包,大大提高了QQ红包的趣味性与观赏性。
个性红包功能上线后,有超过30%的红包用户选择使用个性红包。
在2016年春节期间共有1500万用户使用该功能,2016年除夕当晚突破8千万的个性红包发送量。
个性红包在普通基础上,允许用户修改红包封皮,展示个性,应合场景,因此设计的要点是使用户操作顺畅,既保持发、抢红包的流畅体验,又能显示个性和有趣好玩。
个性化红包流程架构如下图所示:
从上图可以看出,简化后的红包的发放过程经历红包终端->财付通->红包后台->手QAIO(聊天交互窗口)->拆(抢)红包页面等过程,流程较长(忽略了一些细节,实际流程更复杂),在这些步骤过程中如果每一步都走后台判断个性化红包状态,必然影响到红包的发放流畅性。
为了尽量不影响用户发红包体验,个性化红包在架构和运营上作了很多解藕和柔性设计。包括个性字体提前绘制,资源预加载,功能开关和容灾柔性处理等。
个性化红包支持所有简体与繁体汉字,并支持部分简体汉字转换成繁体汉字,为了改善使用“姓氏红包”用户的体验,我们把常用的300个姓氏,使用预生成的方式,在用户手Q空闲的时候生成常用的姓氏图片保存到本地。其他的非常用姓氏,在展示的时候合成,合成一次保存在本地,下次在本地读取。
手Q终端在空闲时绘制好字体贴图,支持定时更新背景图和字体库,对非常用字,则启动个性化字体引擎生成对应的个性化贴图。
用户在发放或收到红包时,个性化背景和字体贴图已经生成好,不需要再生成,收发红包流畅体验无损。
个性化红包封素材提前制作好,上传到CDN网络,手Q在空闲时提前从CDN下载素材文件,并定时检查素材更新情况,及时更新。
用户是否设置个性红包,选择的个性红包贴图样式,是否启用个性红包等信息,如果每次判断都从后台拉取,势必增加后台压力。用户对个性红包的设置信息,其实变化不大,并且访问红包商场实时设置的状态的结果在手Q终端是存在的。因此我们设计将这些用户状态FLAG在手Q登录时,从后台拉取一次后保存在手Q终端,在发红包的过程中将FLAG信息传递到下游服务中,通过红包商城设置的个性化红包标志,实时更新手Q本地配置。
这样的设计有几个好处:
相对于手Q平台功能,个性红包系统相对独立,运营和更新很快,系统各功能组件出现问题的几率可能较多,如果个性红包业务出现问题,而影响到正常红包发放或手Q功能的使用,会对QQ口碑造成很大负面影响。我们在系统中设计了多处容灾和柔性处理措施,在个性红包业务异常时,能降级提供服务,最差时取消个性红包功能。
柔性措施一:用户登录时拉取个性红包FLAG失败时,采用默认红包样式。
柔性措施二:红包后台向个性化红包后台拉取个性化设置鉴权详情(是否付费、是否会员专享等)时,如果拉取异常,采用默认红包样式。
柔性措施三:个性化红包由用户输入姓氏,指定显示文字,可能遇到敏感字或需要临时下线,可以通过向手Q下发FLAG标志,临时取消个性红包功能,恢复到默认红包样式。
AR红包是“LBS+AR天降红包”的简称,这个创新的玩法得到了用户的一致好评,参与用户2.57亿次,共计领取红包和礼券20.5亿个,获得了口碑和活跃的双丰收。
LBS+AR红包与以往的红包最大的不同在于多了一重地理位置关联,全国有上千万的地理位置信息,结合活动的任务奖品数据产生了海量的配置数据,而这些数据都需要快速实时读取。这是系统设计的一大挑战。
配置数据有以下特点:
上千兆的配置数据,如何供抽奖系统快速检索?考虑到业务使用场景、配置数据大小及MySQL性能,可以采用预先构建全量缓存并进行有序组织,由同步模块负责将构建好的配置数据同步到抽奖系统,供业务进程直接使用。为保证配置数据完整性,构建缓存采用双Buffer设计,只有构建或同步完成后才切换到最新配置。
基于LBS的红包活动离不开地理位置相关的业务交互。在AR红包中,用户打开地图会定期向后台上报坐标,后台需要根据坐标获取周围可用的活动任务投放点,投放点事先都会进行安全筛查,去掉具有安全隐患的区域,避免给用户带来人身安全问题,本节主要介绍如何管理这些投放点。
地图格子
将整个二维平面根据坐标分成边长相等的正方形格子,根据用户的坐标用简单的数学运算即可获取相应的格子ID,时间复杂度O(1)。一个格子是一次查询的最小粒度。每次查询会返回以用户为中心周围5*5共计25个格子的任务点。
打点
红包是以任务维度投放的,每个任务关联一个POI集合,每个POI集合中包含几个到上百万不等的POI点,每个POI点都有一个经纬度信息。
打点即是事先建立格子到任务列表的映射。所有格子数据有序组织并存储在共享内存里,使用二分查找提升读性能。
查点流程
采集系统主要负责汇总各行政区红包发放状态数据,主要提供以下功能:
由于红包是按行政区进行投放的,每个行政区约投放10个任务,每个任务又关联多种类型的红包,如果每次查询区级红包余量时,都实时计算和汇总红包状态数据,扩散带来的包量开销会比较大,为此,我们还是采用双Buffer缓存来解决该问题,一个进程负责将采集到的数据写到缓存,另一组进程提供查询服务。另外,还可以根据存储层的压力,适当地调整采集的频率,使得统计数据尽可能实时。