小游戏如何应对大流量?Shopee Shake的大促实践

2021-09-13 17:57:34 浏览数 (2)

本文首发于微信公众号 “Shopee 技术团队”。

背景

Shopee 每年都会举办几场重要的大促活动。大促过程中,营销小游戏是吸引流量的主要渠道。本文将介绍大促中最常使用,同时在线人数最多的游戏——Shopee Shake——如何应对大促的大流量冲击,保证系统的可用性,为用户提供稳定可靠的服务。

1. 游戏与大促

每年 Shopee 会在五至十二月的每个大促节点举行电视直播活动。每次大促活动时,各市场的运营人员会与当地电视台合作,在节目直播过程中插入一段玩 Shopee 小游戏的互动环节。

1.1 大促游戏的选择

在大促筹备阶段,当地运营人员会根据大促时间表,在游戏管理平台设置游戏活动的开始时间、结束时间、奖池及页面素材。待大促进行时,电视台主持人将引导用户打开 Shopee APP 并进入小游戏页面。

当地运营人员会根据大促计划,从多款小游戏中选择几款参与到电视直播大促当中,而 Shopee Shake 是被使用次数最多的大促小游戏,几乎每次大促活动都会出现它的身影。通过这款游戏,当地运营人员可以发放 Shopee 金币,吸引用户在 Shopee 下单购买商品。

每次大促时,Shopee Shake 都会带来大量用户流量。2021 年 5.5 大促时,该游戏接口最高 QPS 达到 30 万 ,在大促过程中发挥了重要的引流作用。

1.2 Shopee Shake

Shopee Shake 是用户通过在游戏页面摇动手机,获得 Shopee 金币的类似摇一摇的小游戏。用户摇动次数越多,得到金币的概率越大。下图展示了三个不同阶段的游戏页面:

最左侧是游戏预热阶段的页面。游戏开始前,用户进入这个页面后,可以看到金币池的大小和游戏开始的倒计时,也可以将页面通过第三方或发送消息的方式分享给好友,获得额外金币奖励。

中间是游戏过程页面。游戏过程中,该页面会不停掉落金币。用户需要在有限的时间内摇动手机。摇动速度越快,得到金币的概率就越大。

最右边是游戏结果页面。用户在当局游戏获得的金币数量会显示在这一页面。另外,由于每一局游戏会消耗一次机会,剩余游戏机会在此页面也有直观呈现,若用户还有机会,可以直接点击按钮,继续进行游戏。有时,当地运营人员还会配置一些抽奖机会,如果用户抽中了某项奖品,同样会在这个页面显示。

2. 技术方案

从游戏机制来看,Shopee Shake 的后端系统面临以下几项挑战:

  • 瞬时并发量大:由于游戏开始时间是全局统一,所有用户都会在同一时间进入游戏,导致在短时间内产生大量请求。因此,后端系统需要在短时间内承载大量请求。
  • 游戏时间短:整场游戏环节一般持续 5 分钟,每局游戏通常在 10 秒到 30 秒之间。
  • 并发扣减金币池:所有用户共享同一个游戏奖池,每一局游戏结束后都要并发扣减游戏奖池,后端系统容易因此出现单点问题,从而影响到系统性能。

因此,我们的技术方案需要支持高并发请求,支持水平扩容,并解决潜在的奖池单点问题。

2.1 架构设计

从产品特点来看,Shopee Shake 系统主要提供两大功能,分别是供当地运营人员进行大促活动配置,以及供 Shopee 终端用户玩游戏获得金币。

相应地,系统也分成两端,即 Admin 管理端和用户端,分别面向当地运营人员和 Shopee 终端用户。

在大促前,当地运营人员会通过 Admin 端进行游戏相关配置。活动进行时,用户通过 Shopee 终端访问游戏页面,请求后端。其中,最重要的两个请求是“游戏开始”及“游戏结束”请求:

  • 游戏开始请求的业务逻辑是校验每个用户的机会是否足够,当前金币库存是否低于配置值等;
  • 游戏结束请求的业务逻辑是计算用户得到的金币数、扣减金币库存、写排行榜、给用户发金币及获奖消息。

综合以上考虑,Shopee Shake 系统整体架构分为三层,即接入层、应用层和资源层。

  • 接入层:负责接入用户请求,用户登录态校验及协议转换。
  • 应用层:核心业务逻辑处理系统,包括活动配置加载、道具模块、机会模块、库存模块,以及很多微服务,如排行榜服务、组团逻辑服务、获奖消息。
  • 资源层:主要包含 MySQL、Codis 以及 Shopee 中台服务,如通知发送服务、金币发送服务、聊天服务。

2.2 高并发技术

为了应付高并发的情况,Shopee Shake 的后端系统采用了很多高并发技术。这些技术并不是在最初设计时就被采用,而是经过不断的“踩坑-优化”,慢慢迭代而成。优化方案可能不是最优的方案,但都是结合产品特性,综合权衡得到的结果。下文将着重介绍几项主要的优化方案。

2.2.1 水平扩容

为了支撑大流量,我们的游戏系统需要支持水平扩容。只要支持水平扩容,就可以通过增加机器数量来提高系统可承载的吞吐量。

系统支持水平扩容,这就要求系统的接入层、应用层和存储层都支持水平扩容。

其中,接入层和应用层可以做成无状态服务,以支持水平扩容。但存储层支持水平扩容是不容易的,需要将存储数据均匀分布在不同存储节点上。

Shopee Shake 系统采用 Codis 作为存储层,主要考虑到了以下几个因素:

  • 水平扩容:Codis 支持水平扩容,只要增加 Codis 集群的 Redis 实例数,即可做水平扩容。
  • 高性能读写数据:游戏中的用户机会、金币库存、排行榜等数据在游戏过程中会被频繁读写,因此需要能支持高性能读写的存储系统。
  • 数据无需持久化:游戏数据只在活动过程中使用,活动结束后不需要保存。

由上面几点可以看出,Codis 作为该游戏的存储层是非常合适的。

但并不是说只要使用了 Codis 作为存储层,就支持水平扩容,还需要想办法将数据均匀分布在不同的存储节点上。我们最初设计游戏时并没有注意到这一点,以致于系统在大流量压测的时候出现了瓶颈。后经分析发现,Codis 集群有一台 Redis 实例的 CPU 使用率达到 100%,原因是游戏的金币库存使用的 Codis key 是单点 key,超出了单台 Redis 实例性能上限。在流量较小的时候,这并不是问题,但放在大流量场景下,会导致整个系统的性能瓶颈。

在 Shopee Shake 游戏中,金币库存供所有用户共享,所有用户端在游戏结束时都会并发查询及扣减这个库存值。当库存低于指定值时,游戏会提前结束,目的是防止库存超发。因此,金币库存值的读写非常频繁,而且要求数值精确可靠。

使用单点 key 虽然能保证金币库存值是实时且可靠的,但却会带来性能瓶颈。

我们解决这个问题采用的方法是分桶——将一个金币库存拆分成多个分库存,不同用户去扣减不同分库存。这样做,就可以将单点 key 拆分成多个 key,不同 key 的流量则分散到不同的 Redis 实例。因此存储层能承载的 QPS = N * 单台 Redis OPS,N 为分库存个数。

另外,分库存的数量也可以随着流量的增长而增长。示意图如下:

使用分桶的方法,会导致一种异常情况——分库存扣减不均,部分用户会因库存不足而提前结束游戏,但实际上另一个分库仍有库存。这种情况不可能完全避免,在大流量场景下,会出现大量扣减库存的请求,只要将用户流量尽量均匀分到不同分库存,则可以大大降低出现这种情况的概率。

采取分桶的方法,可以有效地提高系统的吞吐量。而因此引入的分库存扣减不均的问题,在大流量情况下,也相对可以接受。

2.2.2 缓存

对于很多面对高并发、大流量的系统来说,缓存是一种常用的技术,Shopee Shake 系统也使用了大量缓存。

在游戏过程中,有很多数据是读多写少的,甚至是只读的,如游戏配置、静态资源等。这些读多写少的数据非常适合使用缓存。利用缓存可以极大地缓解数据库压力,也能减少接口响应延时。

缓存有个漏斗模型:经过层层过滤后,透传到后面的系统流量越来越低。所以设计游戏系统时,考虑到数据的读写频率,应当尽量将数据放到更接近用户的缓存。

Shopee Shake 在每一层系统都使用了缓存,如 Web 缓存、进程缓存、Codis 缓存。对于热点数据,需要提前缓存,这样做可以防止当缓存不存在时,大流量瞬间冲击到数据库的情况。Web 静态数据则使用 CDN 边缘缓存,以此提高游戏加载速度。

系统后端使用缓存时,需要解决另一个问题——缓存如何更新?对于流量较小的系统来说,当缓存过期后,直接尝试从数据库获取数据,这是比较常见的方案。但在大流量场景下,这种方案会导致大量请求直接请求数据库,会造成数据库受到极大的冲击,甚至导致 “雪崩”现象。

解决这个问题,可以使用锁的机制:多个并发请求发现缓存过期,需要去查询数据库时,先去获取“更新缓存锁”,只有获取到“更新缓存锁”的请求才会去查询数据库,当查询数据库完成且更新缓存成功后,释放“更新缓存锁”,而其他请求则进入等待状态,直到缓存被更新时,直接返回缓存。示意图如下:

但该方案存在一个弊端:如果出现网络抖动或后端数据库出现异常,导致查询后端数据库耗时过长,会有大量读请求因此被阻塞,最终占满内存。

Shopee Shake 曾在一次压测过程中暴露出这一问题。当时,游戏服务内存在短时间内大幅增长,且接口延时变大。经过分析,我们最终发现是缓存更新时后端数据库出现异常,无法及时响应,导致大量请求被阻塞。

为了解决这一问题,我们采用了有限等待时间的方法,如果在设定的等待时间内无法获取到最新的缓存,则使用旧缓存。但这种解决方案有可能会导致数据不一致的情况。对于 Shopee Shake 系统来说,需要查询数据库的数据都是游戏的基本配置,而这些配置在游戏开始后的修改频率是非常低的,所以有限等待时间的方案比较符合我们的业务场景。

2.3 异步

在高并发场景下,为了提高接口性能,有时会将一些耗时高,但不需要阻塞主流程的操作放到消息队列,减少在线系统的压力。同时使用另一个异步处理的服务,不断处理消息队列的数据。Shopee Shake 系统也采用了这样的方案。

在一次压测过程中,我们发现游戏系统的游戏结束接口延时相比其他接口要大,而该接口主要用于接收用户摇动手机的次数、计算用户获得的金币数以及给用户发放金币,是整个游戏中最重要的写接口,会直接影响整个系统的吞吐量。

经过性能分析及代码分析发现,游戏结束接口在以下两个功能上耗时最大,大约占接口总延时的 30%:

  • 将用户游戏得分数写入排行榜;
  • 给用户发放金币及发送获奖通知。

对用户场景进行分析并和产品经理讨论后,我们梳理了游戏结束接口中所有可以异步处理的功能,并将这些功能异步处理。

可以异步处理的功能有以下几项:

  • 用户游戏得分写入排行榜:查看排行榜的入口比较深,请求流量比较低,且同时有大量用户正在玩游戏时,排行榜数据变化很快,用户并不需要看到榜单实时变化过程,因此只需在一定时间内输出最终的排行榜数据即可。
  • 发放金币:派发金币并不需要实时到账,这样的体验对于用户来说是可以接受的。
  • 发送获奖通知:获奖通知用于告知用户获得金币,不需要实时通知。
  • 数据埋点上报:上报用于离线分析的游戏数据。离线分析的数据不需要实时上报。

异步处理要解决两个问题:一是不能丢失消息;二是幂等,即重复消费消息,要保证结果一致。

第一点是因为异步处理无法实时感知消息处理的结果,所以需要保证消息不会丢失。第二点的原因是,消息队列的消息有可能会出现重复消费的问题,则需要保证重复消息不会产生副作用。

对于不能丢失消息这点,使用现有成熟消息队列中间件,如 Kafka,是能保证的。但存在消息队列出现故障或消息队列容量满载,无法写入新消息的情况,这时需要提取日志,重新生成消息,发送到异步处理服务。所以对于消息队列,需要加强监控,及时发现故障及进行数据补发。

对于重复消费的问题,可以在每一次游戏生成全局唯一的 ID,作为请求 ID 传给派奖系统,派奖系统根据请求 ID 作为唯一键,派奖前先查询当前请求 ID 是否曾经派过奖,确保同一个请求不会被重复处理。

由此可见,引入异步处理会增加系统的复杂度,但也能有效提高接口的性能。

3. 容量规划

在每次大促前,我们需要评估系统容量是否足够,是否能支持当地运营人员预估的用户量。这时就需要对系统的容量进行规划。

容量规划主要由下面几个步骤组成:

  1. 单容器容量评估。该数据通过全链路压测获得。通过部署单容器容量,压测得到系统的极限容量。单容器容量粒度精确到每个接口可承载的 QPS。
  2. 将当地运营人员预估的 PCU(最大同时在线用户数)与 QPS 进行转换,计算得出系统需要承载最大的 QPS。
  3. 计算部署的容器数量。
  4. 容器数量 = 最大 QPS / 单容器容量
  5. 预留部分机动资源,防止突发流量
  6. 计算下游容量要求。如 Codis OPS、下游服务 QPS。
  7. Codis OPS = 最大 QPS * 每请求 Codis 操作次数
  8. 计算得出的容量后,按需求容量实际部署,包含业务容器数及下游中间件及游戏的需求部署。部署成功后,再做一次全链路压测,验证容量是否满足预估的 QPS 需求。

4. 立体监控

为了能实时观察服务运行情况,及时发现异常,团队要对系统进行全方位、立体的监控。立体监控包括接入层、应用层、资源层、硬件层等监控,每层监控指标各不一样。只有做到完整的立体监控,才能及时发现潜在问题。

下表展示了不同层次的监控指标:

5. 大促预案

凡事预则立,不预则废。在大促过程中,面对突发情况,如何应对?

突发情况可能会在系统任何一个环节出现。因此,在梳理预案的时候,我们会根据系统的关键链路寻找链路上的关键节点,并针对关键节点制定预案。

常见的突发情况包括:活动配置错误、接入层不稳定、服务自身出现 bug、依赖的存储层服务不稳定、依赖的下游服务不稳定、依赖的中间件不稳定等等。

根据这些突发情况出现的阶段不同,预案也相应地分为前置预案、应急预案和恢复预案三种类型,有针对地展开处理。

例如:针对“活动配置错误”的突发情况,我们准备了相应的前置预案,在大促活动开始前,检查大促游戏各项配置是否正确。

6. 故障演练

有了预案,并不代表就高枕无忧。这些预案在故障发生时是否真的有效?处理问题的人是否熟练?沟通机制是否顺畅?我们并不希望线上真正出现故障时才去验证这些问题,这样风险太大,成本太大。所以应在线上环境隔离真实流量的情况下,提前模拟各种可能产生的故障,来观察系统的反应和人员处理情况,以验证预期策略。

于是,在大促前我们都会进行故障演练,以低成本的方式发现预案的不足,暴露系统的问题,不断提高人员及系统的能力。

6.1 人员分工

故障演练不仅仅包含突发情况的应对预案,也包含不同职能人员的分工。不同职能人员聚集职能内的责任,同时又能保证信息的有效流转,提高发现问题、执行预案的效率。

上述职能中,除了破坏组是为故障演练而设,其他都是在真实大促活动中切实存在的。在每次大促时,相关职能的人员都会值班待命,随时处理大促过程中出现的异常情况。

6.2 演练过程

制定好人员分工及应对流程后,应该如何进行演练?下文将从故障演练前、故障演练中和故障演练后三个阶段来展开介绍。

6.2.1 故障演练前

  • 检查必备基础能力:流量注入及流量染色等能力,保证流量不影响真实用户;
  • 确定故障演练范围、环境:跟当地运营人员确定可演练的真实线上环境,确保没有真实流程,或实施流量隔离,不影响真实用户;
  • 确定故障及应对预案:确定要演练的故障,制定好故障剧本,并针对故障制定相应预案;
  • 通知涉及的外部人员:将可能的影响面提前通知相应外部人员。

6.2.2 故障演练中

被染色的流量注入系统的那一刻起,故障演练就正式拉开序幕。据上图所示,整个演练流程分为八个主要步骤:

  1. 破坏组按故障剧本,注入故障到演练系统;
  2. 定位组观察各指标,及时发现故障,进行初步定位,排查问题;
  3. 定位组汇总关键信息上报故障给决策组;
  4. 决策组收到故障报告,根据关键信息决定是否执行预案,并告知执行组决策结果,要求执行某个预案;
  5. 执行组收到决策组的决策结果,执行某个预案;
  6. 执行组将执行结果反馈给决策组;
  7. 决策组同步预案执行结果给定位组;
  8. 定位组观察预案执行后的指标是否恢复,并将结果反馈给决策组及其他人员。

6.2.3 故障演练后

  • 现场清理:如流量关闭、撤销故障、关闭预案、清理演练的数据等;
  • 通知相关人员演练结束;
  • 演练报告与总结:包括是否达到预期目标、预案有无生效、是否有预料之外的状况发生,并对关键指标(业务指标、机器负载指标)收集归纳,整理后续改进点。

7. 总结

本文从游戏的逻辑、系统架构、使用的高并发技术,和团队的立体监控、大促前的容量规划、大促预案以及故障演练等方面介绍了小游戏 Shopee Shake 如何应对大促。

在大流量冲击下,系统保持稳定需要靠系统性思维,不仅仅要考虑系统自身情况,更重要的是技术团队要不断总结教训,积累实践经验,提高自身能力。面对大促,团队应该要做到以下几点:

  • 要充分了解业务的特点,设计出符合业务特点的技术构架,并且全面考虑高并发场景,综合运用高并发技术;
  • 要充分了解大促流量分布情况、流量预估,做出合理的容量规划;
  • 要通过各种外部系统,如监控、日志等了解系统的运行情况,及时发现问题、解决问题,提高可用性;
  • 要有应急预案思维,提前做好各关键节点的预案,多做故障演练,提高应对各种突发情况的处理能力。

Shopee Shake 的现有系统及预案还有不少可以更加完善的地方。为进一步提升游戏系统在大促中的响应能力,后续我们将继续在以下几方面做出优化:

  • 系统接入层增加限流机制,防止系统过载而崩溃;
  • 增加更多的预案,扩大预案覆盖面。有些预案由于系统架构或外部原因导致无法执行,后续也会持续改进;
  • 增加故障演练次数及演练的场景,提高团队应对能力。

要想做到从容应对大促,并非能一蹴而就,这需要技术团队平时不断积累与磨练,不断提高技术水平及应急能力。大促路上,我们仍需努力。

本文作者

Zhiwang,后端工程师,兴趣方向为高并发分布式系统,来自 Shopee Games 团队。

团队介绍

我们不仅会做一些游戏化的营销工具,而且还做真正的游戏!经营、养成、关卡、AR、PVP 等,多达几十种并不断增加。“好玩”是我们做游戏的初衷和目标,因为我们相信,游戏能建立情感、拉近距离,给用户带来快乐,为平台创造价值。 目前大量 iOS、前端、后端、测试、大数据开发岗位空缺中,感兴趣的同学可将简历发送至:vicky.zeng@shopee.com(邮件主题请注明:Shopee Games - 来自技术博客)。

0 人点赞