体验+架构-揭开运营活动的那一年,那一夜!
前言
运营活动是各APP日活及冲量比较重要的一部分,其实很多活动的思路都是一样的,只是每次形式及奖品有所变化,
本文针对一次典型活动为切入点,剖析运营活动的设计奥秘。
本文分为架构设计和业务难点两部分。
基本功能
主要业务为刮卡,每人每天有一定数量的刮卡次数,每次可能刮落随机面额的货币、话费、流量及其他奖品,货币用于游戏及兑换;
业务架构
许多APP本身的运营都是通过货币的积累及消耗作为主线,
例如比较典型的积分商城:通过各种任务积累货币、通过兑换游戏等方式消耗货币。
难点:
A.货币控制
B.奖品定价
C.以上两点做好才会有良好的用户体验
一般活动的预算一定,奖品的价格及数量在活动开始前要确定。
因此要控制货币的发放,否则会导致通货膨胀、通货紧缩等问题。
货币如何发放、奖品如何定价是摆在面前的第一个难点。
货币控制:需要我们做到
a.无规律
之前掉落值可能会设定几个面额的货币值,需要做到在一个范围内随机
b.无断层
横轴为用户累积的货币量 纵轴为用户数量
在某些货币值点设定关卡,限制用户货币总量,会出现这样的断层,卡在某个值上
会导致用户刮不出货币,造成糟糕的用户体验
合理的做法是按照预期,累积的货币量与用户呈现平滑递减的趋势。
用户每次刮出的货币量是随机的,但对于总的用户总的货币量上限和分布是可控的
掉落策略的原则是:每次掉落多少是随机的,但是整体的上限和分布是可控制的,是可以预先计算的
奖品定价:
货币的分布与奖品数量的分布趋势是一致的
因此,只有算出每个货币段的人数才能定商品具体价格
要解决的点:
a.无规律->范围[m,n]
b.无断层->分布无断层
c.可预知->上限与分布可预知
基于以上问题,我们采用了正太分布算法
这个图大家应该很熟悉,数学中的正太分布,根据右上角的公式得出的点的分布情况
特点:看右半部分,倒金字塔型,x值越大,y值越小,每一部分比例可控,最大值上线可控
与我们活动最终结束时用户累积货币量分布吻合
算法有两个重要的参数:均值和方差
均值:描述正态分布的集中趋势位置,如下图
方差:描述数据的分散程度,如下图
java中正态分布算法:
Math.sqrt(b)*random.nextGaussian() + a
a:均值 b:方差
该公式可以作为用户每次刮卡掉落的货币值,如pm要求大部分用户刮出面额在5 左右 最大是10 最小是1,就可以通过设置a和b的大小来控制。
用户每次刮落掉落值范围有了,用户每天有40次刮卡机会,活动支持20多天,那单个用户每次累积值是否符合分布趋势呢?
理论验证:
经验证:无论是单次掉落,还是单个用户每天累积以及整个活动所有用户货币的累积值都符合正太分布趋势
至此,货币值掉落范围以及货币累积上限及趋势都在可控范围内。
发放策略:
那么新的问题来了,均值和方差如何确定呢?
首先要确定每个用户每天刮出每种奖品的次数,以确定货币掉落次数以及每天大概累积值,整个活动期间用户货币的累积值范围才能确定。
根据pm期望,确定的每种奖品每个用户每天掉落次数权重分配如下表:
前期5天每天每个用户共有40次刮卡机会,其中货币会掉落22次。
权重分配完成,如何将分配好的权重落地到每个用户每天的刮卡行为上呢?
加权轮询算法-WeightedRound-Robin
设定每种奖品权重,通过轮询调度,可以得到按权重比得到的掉落结果
效果图如下:
具体做法:
每个用户维护一个权重列表对象,分别记录每个用户每次请求权重变化,
以天为单位,重新初始化权重列表,效果见下图:
通过观察图中右侧轮询出的结果可以看到每个用户轮询得到的结果顺序都是一样的,
也就是如果两个用户每次刮卡得到的奖品进行对比,发现刮出奖品顺序是一致的。
处理方式:
a.在初始化用户权重列表的时候将权重列表打乱(也只要4种顺序)
b.在掉落奖品为空的时候也就是图中d的位置,会随机填充其他奖品如转转红包、滴滴红包等
c.在使用正态分布算法时有10%负值的可能性,返回空值
通过以上手段可以解决顺序问题,如下图
至此,货币发放问题通过正态分布和加权轮询算法得到解决;
大体思路是化可变为不变,化不可控为可控。
在可控的基础上,配合运营策略,调整高峰期间每天发放货币面额和次数,调起用户兴奋度,使用户最大限度参与活动,使活动得到最大限度的收益。
系统设计
作为互联网应用中的功能,存在高并发大流量的特点。因此需要我们系统能够应对高并发,并提供高可用的服务。
针对高可用可采取隔离、限流、降级等策略;针对高并发可 使用缓存、异步、队列等手段。
我们的系统设计主要包含以上方面。
整体设计
接入层:进行安全校验,服务降级等做基础校验拦截处理
业务拆分:分为刮卡、兑换、游戏等业务模块
服务化:可快速水平扩展,可对服务分组访问,服务隔离
数据库拆分:根据业务模块垂直拆分,模块内水平拆分
监控:监控报警,及时发现处理问题
系统存在问题及解决方案
a.安全问题:防作弊体系
无论什么活动,常用的作弊手段如同一账号,一次性发送多个请求;多个账号,一次性送多个请求;
多个账号,不同IP发送不同请求等;传统的防作弊手段如各种维度的n秒m次等,已不足以拦截逐步
升级的作弊技术。除了基本的策略以外,我们采取了行为埋点对比匹配的方式:
从活动的操作行为上来看,用户得到货币的行为包含以下几步:
其中刮卡行为需要手指按下、滑动及抬起手指,针对用户的每个行为,发送前端行为埋点;
同时在加载新的刮卡图层时,后端下发此次行为标识,前端拿此标识发送埋点,在用户加奖品时发送后端埋点;数据中心根据前后端埋点对比,根据匹配度过滤出作弊用户。
作弊体系架构如下:
如上图:
左侧log1、log2是前端埋点收集系统,通过flume上传到hdfs;右侧web1、webN是我们的应用系统,应用系统将后端埋点通过消息总线进行后端埋点收集;
数据中心根据设定 的策略进行实时及离线分析,将作弊用户写入redis供不同的功能判断拦截;
b.缓存使用问题
1)分布式缓存
我们使用memcache来缓存内存占用相对大的对象,结构如key-value的简单存储;
对于其他的复杂操作,如游戏分数排行、分布式锁等使用redis来实现;
使用缓存应注意:
1.避免阻塞:在活动刮卡业务中,使用缓存并设置过期时间,在流量到来时有每分钟20万key过期,大量key集中过期导致主从复制阻塞,客户端连接数暴涨到1万左右,
请求阻塞超时。
解决方案:不同功能缓存隔离,读写分离,对有过期性质key做过期时间的散列,减少过期时间的重复率,避免造成因过期导致的阻塞。
(注:线上切记不可使用keys、sembers大集合等慢查询操作)。
2.缓存穿透:缓存未命中导致请求落到DB,产生原因有多种,如刷接口、内存空间不足等;
解决方案:可以将不存在的key设置空值,但需要占用一定内存,并设置过期时间或者主动更新;会产生数据一致性问题,对一致性不敏感操作可忽略;
合理申请缓存容量, 若缓存空间满了,会造成数据丢失,穿透等问题。
3.使用分布式锁:避免出现死锁,如锁没有过期,没有及时删除,导致锁不能及时释放,其他请求无法进入;过期时间如果小于程序本身处理时间,
会导致程序没有处理完锁 已经释放,其他线程获取锁,进行操作等等问题;
解决方案:自己实现的redis分布式锁,功能单一,问题考虑不周全,实际情况要比我们想到的更复杂,可使用成型的redisson框架进行处理。
4.避免网卡被打满:官方信息字符串类型的value可容纳的数据长度是512M,但由于string值较大,
且流量较大时导致网卡被打爆;redis集群混合部署,因此要控制网络流量;
解决方案:将无用的信息删除掉,只存储需要的值,对值大小最必要的控制;
对比较大的值可以进行压缩处理。
2)内存缓存
对于数据量小,且不需要经常更新的热数据如商品描述,活动规则等做到内存缓存中,提高响应效率,根据不同性质数据设置合适的过期时间,
保证在可容许的范围内的最 终一致性;关注应用内存使用,防止内存溢出。
c.兑换变秒抢问题
活动在上午11点进行商品兑换,由于兑换人多商品量少,因此兑换场景演变为秒杀。
由于是按照正常兑换逻辑进行处理,使用数据库行锁update库存操作,由于qps瞬间飙高,
大量请求等待获取行锁,数据库连接数被打满,导致数据库操作因拿不到连接而超时,RT飙高。
数据库连接数如下图:
解决方案:
1)限流-过载保护
通过压测可以得到整个集群承担最大吞吐量,流量洪峰只可大概预估,实际请求量不可预知。
如果超过系统承载能力,有引发雪崩的危险,因此我们需要做过载保护。
限流的的宗旨是要将流量进行层层拦截,流量成倒金字塔型递减。需要进行前端及分布式限流等,
分布式限流可采用redis或者从nginx层限制,除此之外还需做应用层限流;
该活动主要功能为刮卡和兑换,刮卡流量特点是平稳上涨,达到峰值,持续时间长;
而兑换规定在某个整点开始,流量突增,峰值较高,持续时间短。
通过压测得到接口所能承受最大压力,为不同接口设置相应的阈值。
最简单粗暴的可以使用计数器来实现,但是没有平滑处理,瞬间请求可能都会被允许,
造成系统问题,我们采用的Guava RateLimiter令牌桶算法
该方案平滑的处理瞬时突发请求,每隔一定时间才会放一个请求进入,并有预热处理方案。
2)队列加异步
处理方式:将库存加载到redis中,通过redis原子性操作,保证数据安全,不会被发超;
并将兑换到商品的用户请求放入该商品的队列中,异步从队列中适速获取请求进行后续操作。
3)事务处理
关注关键步骤的服务的处理情况,如扣货币、减库存、创建订单为关键性服务,
需要实时同步来做,有强一致性要求,采用事务补偿方式,及时做业务数据回滚。
非关键性服务如用户兑换行为记录等,可采取异步重试等机制保证最终一致性。
4)分库分表、读写分离
对于异步处理下单的处理瓶颈在于对数据库操作。
垂直拆分:
为解决多个表之间的IO竞争,单机容量问题。我们按照业务维度进行垂直切分,
如商品、用户、订单、用户行为、游戏等。
水平拆分:
除此之外根据以往日活及日新增预估用户量,计算活动期间大概的用户数;
根据用户及商品数量预估大概订单量,为减少增量数据写入时的锁对查询的影响,
减少长时间查询造成的表锁,影响写入操作等锁竞争的情况,我们对部分表进行水平拆分。
拆分维度:
根据业务不同采取不同的拆分策略:
对用户使用uid进行拆分,游戏数据按天拆分(每天排行榜)。
问题1:如何解决数据不均匀问题
方案:采取合适的算法,如cityhash
问题2:多维度查询,例如用户表,按uid进行拆分,用name如何查询?
方案:1.建立uid和name索引表,可以建在mysql或者缓存中,缺点是多一次查询,
索引表单点,索引表数据量大了性能等问题。
2.可以借鉴@沈剑的"基因注入"法,在uid中注入其他字段散列的n位bit信息实现。
3.数据异构,多维度查询多是用在运营后台使用,通过mycat等工具订阅binlog日志,
对数据进行多维度异构,使前台业务与后台运营数据统计隔离,避免后台一次慢查询
导致前端业务垮掉。
除此以外针对数据库还需要做
a.一致性不敏感操作做读写分离,一主多从;
b.杜绝慢查询,合理建索引;
c.合理分配数据库连接数;
5)熔断机制-服务降级
用户抢到的话费、流量、Q币、红包等充值领取类商品,需要调用第三方http服务,
在第三方服务不可用或者超时情况下进行熔断,保护我们服务器负载及性能,
同时对第三方服务也进行保护;对不同商品处理类型设置单独的熔断线程池,进行隔离,
根据每种充值类接口的响应时间以及用户的请求量计算出大小合适的线程数。
对于熔断的请求采取补单降级策略,进而保证数据的最终一致性。
通过1)限流保证达到后端流量在可处理范围,通过2)有效兑换请求再次过滤,到达数据库操作量有效,并做排队处理,大大降低了数据库压力。
4)提高了数据库的处理能力。
cap曾经说过:鱼和熊掌不可兼得,通过2)3) 5)使强一致性转化为最终一致性,保证系统的可用性。
总结-设计原则
对于应用系统和存储要做到以下几点
除此之外,上线前要进行压力测试,针对不同类型请求,做读压测、写压测及混合压测。针对压测报告做瓶颈优化,充分系统承载能力,做好应急方案准备。