优测小优有话说:
你以为优社区只有测试相关的知识吗?手机QQ大牛带你走一波红包后台设计!
- 前言
2016除夕夜注定是一个不平凡的夜晚,除了陪家人吃团圆饭、看春晚,还得刷一刷、摇一摇、咻一咻,忙得不亦乐。相信大部分读者也已经体验过手Q的刷一刷抢红包,玩法简单中奖率高,得到了许多用户的好评。
那么对于后台而言,要实现这个亿万级用户的抢红包系统,我们将会面临哪些问题?
(1)海量的并发请求,预估峰值800w/s
800w/s的预估峰值请求,红包系统必须要保证在如此高并发的请求下,能够对每一个请求都及时正确的处理并做出响应。
(2)亿万数量的红包如何派发
刷一刷红包的奖品,除了现金红包外,还有各种卡券(如京东券、滴滴打车券等)和个性装扮类奖品(如明星气泡、挂件等),如何保证亿万数量的奖品公平公正的发放,不多发、不错发,并且保证奖品尽可能的发放出去,是红包系统面临的一道难题。另外,与各类奖品相对应的业务,他们的能力也不尽相同,奖品发放速度的控制也成为了后端业务系统正常运作的关键。
(3)安全如何保障
对于一个掌握着亿万资金发放的系统,一些不法分子肯定会想方设法利用各种漏洞来盗取资金,那么一套完备的安全防护措施就显得及其重要。
(4)异常情况如何处理
刷一刷红包系统部署上线,真正提供抢红包服务也就几个小时的时间,常规系统的灰度发布、逐步优化的流程显然不适用。所以我们必须分析出各个可能过载或故障的节点,并对这些节点设计好柔性策略。
- 抽奖最小系统
在解决上面提及到的问题前,我们先看看一个能基本运作的红包最小系统,如下图所示:
主要的处理流程如下:
首先,刷一刷是以活动的形式进行的,每轮活动的信息资源和商家素材资源会预先从CDN预下载到客户端,客户端在需要使用的时候,可以直接从本地加载,避免在抽奖峰值时下载从而对CDN带宽造成冲击,同时本地取资源也可以有更好的用户体验;
客户端判断到活动开始时,会展示出刷一刷的入口,用户进行刷的操作,会产生抽奖请求,抽奖请求发送到手Q接入层SSO,然后转发到抽奖逻辑层;
抽奖逻辑层经过一系列的逻辑判断,给客户端返回一个结果:中了现金、中了虚拟奖品或者没中奖,中奖后相关信息会保存在存储层;
如果是中了现金,交由财付通的支付系统对用户进行充值并发送公众号消息通知用户中奖;
如果是中了虚拟奖品,会调用基础IM后台的接口发送公众号消息通知用户中奖并提供领取入口。用户点击领取奖品,抽奖系统会给对应的业务返回领用资格和设置已领用标志。另外,对于某些内外部卡券类的奖品,抽奖系统统一只对接增值部的礼包发货系统,用户领用这部分奖品,会调用礼包发货系统接口,走MP营销平台进行发货。
那么,如何完善上述的系统,使之既能够满足业务的需求,又能够应对文章开头所描述的几个问题呢?
- 完善抽奖系统架构
3.1 缓存机制
业务要求在每个刷一刷的活动中,能对用户中奖次数、参与时间(30秒)进行限制。常规的实现里,用户的每个抽奖请求到来时,先到存储层获取用户的中奖历史信息,判定用户是否还有资格进行抽奖,但在高并发的请求下,对存储层势必也会造成巨大的压力。所以这里我们引入了本地内存缓存层,用于保存用户的中奖历史信息,每次请求到来时,会先到缓存层获取用户的中奖历史信息,如果在缓存层没找到,才会到存储层获取,这样就不会对存储层造成大压力,同时也能实现业务的需求。缓存层使用Memcached实现,但单靠这个缓存机制是不是就解决了存储层访问压力的问题了呢?
3.2 一致性hash寻址
虽然引入缓存层能够缓解存储层的访问压力,但别忘了,抽奖逻辑层是一个分布式的系统,在使用普通的路由算法下,同一个用户的请求不一定会落在同一台逻辑机器处理,也就是说,不一定能够命中之前保存在本地的缓存。所以为了使缓存生效,我们在手Q接入层SSO使用了一致性hash的路由算法进行寻址,同一个uin的请求会落在同一台逻辑机器进行处理。
3.3 存储层选型
存储层的设计向来都是后台架构设计中的重点和难点。目前公司内部较成熟的存储系统有CMEM、Grocery,经过一番对比我们选择使用Grocery,主要原因有以下几点:
(1)强大的带条件判断的分布式原子算数运算
抽奖逻辑里需要对每个奖品进行计数,避免多发少发,所以一个高效可靠的分布式原子加计数器显得格外重要,Grocery支持带条件判断的原子加计数器,调用一次接口就能完成奖品计数值与配额的判断以及奖品计数值的增加;
(2)灵活的数据类型
Grocery支持Key-Key-Row类型的数据存储格式,可以灵活的存储用户的红包中奖信息,为获取用户单个红包或者红包列表提供了丰富的接口;
(3)部署、扩容方便
Grocery在公司内部有专门的团队支持,易于部署和扩容。
3.4 奖品配额系统设计
刷一刷红包的玩法是以活动的形式存在的,业务要求每场活动要发放多少份的现金,多少份的虚拟奖品,每种奖品的发放比例是多少,这些都是该场活动的配额数据。另外,业务还要求奖品的发放额度要能够根据用户参与数灵活配置,保证在用户多的时候奖品多,而且不管在活动的任何一个有效时间点进来,都有机会中奖。
在活动期间,如果某个奖品对应的业务出现了故障,需要停止发放该奖品(发放比例修改为0),要求各台抽奖逻辑机器能够快速稳定的同步到修改的配置。基于以上几点我们设计了这样一个配额系统:
在当天活动开始前,根据产品给的配额数据生成一份当天所有活动的Json格式配置;
配额管理工具可以对该Json配置进行相关合法性检查,检查OK后,把配置导入到Grocery数据库,并更新Seq;
运行在各台抽奖逻辑机器的配额agent会定期的检查Grocery里的Seq,如果Seq发生了变化,表明配额数据发生了变化,然后就会从Grocery获取最新的配置,并更新到抽奖逻辑的本地共享内存shm;
抽奖逻辑的每个进程也会定期检查本地shm的内容,发现有变化,就会重新加载shm的配置。
这样配额系统就实现了可以秒级别控制奖品的发放额度,可以随时根据现场情况调整发放比例,并在短时间内(10s)同步配置到所有的抽奖逻辑机器。
3.5 奖品发放限频设计
每一种奖品,对应的业务都有他们自己的能力,且各业务的能力也不尽相同(如黄钻8w/s,京东1w/s)。一般的,我们在配置配额时(按照100%的兑换率),都不会配置到各业务的峰值,但配额系统有一个风险的不可控制的,就是进来的用户数是不确定的,但为了保证奖品都能发放出去,奖品前1秒没发完的额度,会累加到下1秒发放,这样奖品发放峰值就有可能超过业务能力。
举个例子,黄钻的处理能力为8w/s,第1秒有5w的配额,第2秒有5w的配额,假如从第1秒开始就有足够多的用户参与抽奖,用户中奖后100%会去兑换黄钻,那么就有5w/s的请求到达黄钻业务,业务能够正常处理。假如由于一些意想不到的情况(例如xx宝在第1秒也在搞活动吸引走了部分用户),第1秒只有1w的用户,第2秒突然涌进9w/s的用户,那么第2秒将有9w/s的请求到达业务致使业务过载。
基于以上原因,奖品发放限频逻辑就成为了保障后端业务正常运作的关键。那么,如何设计这个限频逻辑呢?首先想到的方法就是到存储层Grocery获取当前奖品计数器的值,拿回本地与前1秒的值做判断,即可保证不超峰值,但这种做法会对存储层造成巨大的冲击,显然的不可行的。那么如何在保证性能的前提下,精确实现这个限频逻辑呢?
我们是这么做的,活动前抽奖逻辑层的机器数是确定的,我们只要控制每台机器奖品的最高发放频率,就能整体控制该奖品的发放频率,最坏的情况可能是某些逻辑层机器故障了,可能会造成该奖品在配额充足的情况下不能按峰值发放,但我们可以通过修改配置重新设置每台机器该奖品最高的发放频率来解决这个问题。对于单机奖品发放频率计数我们使用了Memcached的increment原子加操作,以时间和奖品ID作为Key,计数值作为Value存储在内存里,即可实现精确的计数。
上图左侧是我们一开始的实现,当时时间是以秒作为计数周期的,也就是说,如果配额和用户数都充足的话,奖品会在这1秒的最开始全部发送出去,这样的话,问题又来了,一般来说业务方给的能力例如8w/s,是指在这1秒内相对平均的来了8w的请求,业务方刚好能够正确处理这8w的请求。但是如果请求是在1秒的最开始全部涌到业务方,受限于业务方不同的架构实现,有可能会触发业务方的频率限制或者是过载保护。因此,我们将计数周期调小到百毫秒,这样奖品就会在1秒内相对平均的发放,从而解决了上述问题。
3.6 抽奖算法设计
抽奖算法的设计并不复杂,大体流程如下:
需要注意的是,每个奖品都有发放的时间段,只有在该时间段内,才会把该奖品放入奖池里。另外,从奖池里按照比例挑选奖品,只要该奖品未成功派发,就继续从奖池再次挑选,直到奖池空为止,这样就能保证奖品尽可能的派发出去。
3.7 流水系统设计
流水系统主要用于活动后对奖品发放和设置领用进行统计和对账。同时,该系统还对设置领用失败的请求进行重做,确保奖品发放到用户账户里。流水系统架构如下:
由于流水需要记录用户中奖的信息和设置领用的的情况,数据量巨大,所以抽奖逻辑层本地采用顺序写文件的方式进行记录。抽奖逻辑层会定期的把本地的流水文件同步到远程流水系统进行汇总和备份,同时,流水系统会对领用失败的流水进行重做,发送请求到抽奖逻辑层,抽奖逻辑层会调用支付系统的接口完成领用操作。
3.8 安全防护设计
安全防护对于抽奖系统来说,也是必不可少的,我们有以下几种措施来保障系统的安全:
(1)账号鉴权
对于到达抽奖系统的请求,都要求带上登录态,利用基础IM后台完善的PTLogin接口,对用户进行身份验证。
(2)实时安全审计
对于每一个抽奖请求,我们都会记录必要的信息(如客户端IP、版本、序列号等),加上原始请求一并转发到安全部门,由安全部门对所有的抽奖请求进行多维度的监控,对恶意请求进行打击。
(3)uin请求频率限制
对于每个用户(uin)的每种请求(抽奖、设置领用等),我们都做了请求频率限制,超过频率限制的请求会直接返回错误。频率限制必要的信息(时间、令牌)存储在Memcached里。
3.9 完善后的抽奖系统
- 异常处理策略
4.1 接入层SSO过载
接入层SSO是手Q终端使用后台服务的大门,如果SSO出现问题,后果将不堪设想。抽奖请求在SSO预估的峰值为800w/s,虽然评估预留了一定的余量,但评估的数据还是具有风险的,所以在接入层SSO我们做了以下保护措施:
(1)错峰机制
抽奖活动开始后,用户会随机被错峰一定的时间,避免大量用户在同一时间发起抽奖。错峰的最大时间配置会随活动配置一起保存在CDN资源库中,在活动开始前预下载到客户端本地。奖品的配额数据也会根据错峰后的用户模型进行合理配置,保证公平性。
(2)实时可调的抽奖间隔
客户端发起抽奖请求是有时间间隔的,默认间隔也是保存在活动配置里。如果发现SSO即将过载,SSO可以在抽奖回包里带上新的抽奖时间间隔,从而达到减少抽奖请求的目的。
4.2 抽奖逻辑层过载
抽奖逻辑层设计的峰值能力为300w/s,假如即将超过该峰值,会使用如下措施进行保护:
(1)SSO调整请求透过率
SSO设计了一个抽奖请求透过率的配置,默认100%透过,正常情况下,客户端发起的抽奖请求到达SSO后,都会转发到抽奖逻辑层。假如抽奖逻辑层即将过载,SSO调整该透过率,就会按照比例随机对请求进行直接回包,回包内容指示用户未中奖。
4.3 存储层Grocery过载
Grocery如果即将过载,也可以通过调整SSO透过率来减少请求。
4.4 财付通现金充值接口过载
财付通现金充值接口能力为20w/s,配额系统配置该峰值也是配置成20w/s,如果发现20w/s财付通出现异常,可以减小峰值,然后通过配额系统快速同步修改后的峰值到所有的逻辑层机器。
4.5 虚拟奖品对应业务过载
每个虚拟奖品对应的业务都有自己的峰值能力,配额系统也是按照业务给定的峰值能力进行配置,如果业务反馈异常,可以实时修改业务对应奖品的峰值或直接关闭该奖品的发放。
4.6 其他异常处理
其他模块的异常一般都是可以通过调整中奖率、关闭开关或关闭重试进行处理:
- 运营部署
接入层和抽奖逻辑层IDC级容灾部署;
业务接入织云运维自动化平台,支持平滑扩容,进程实时监控并支持秒起;
存储层数据双份容灾。