27

Redis:我承载了上千万人的火影青春

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=Mzg4NjA4NTAzNQ%3D%3D&%3Bmid=2247488101&%3Bidx=1&%3Bsn=42b32797eb4179c7ee4bb2834f7cf0d1
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

作者:李世顺,腾讯游戏天美J1工作室游戏后台高级工程师,先后参与过剑灵手游、火影忍者手游、写实赛车项目的研发与维护工作。

火影忍者手游已经上线4年多,活动是火影重要的运营手段,火影的活动除了签到、礼包、任务等商业化运营,最大的特色是有很多“玩法”级别的活动,包括了游戏战斗、sns等元素。如何持续稳定的输出高品质活动成了火影当前最大的挑战之一。

火影当前活动已经400+,而且还将持续稳定扩展,按理说,活动本身逻辑与核心数据逻辑是弱耦的,但是现状却是与gamesvr(火影对应的是zonesvr)耦合在一起,不但影响了核心服务的可用性,也极大限制了活动本身的扩展性。

rE3IRrr.png!mobile

改造思路

1. 服务拆分

目前火影大部分需求都是新增的活动,而活动逻辑一直在 gamesvr 中,新增活动严重影响了 gamesvr 的可靠性,拆分出独立的actsvr后,还能提高活动服的扩展性。

EZNraej.png!mobile

2. 状态迁移导致的核心问题

游戏逻辑决定活动一定会产生状态,状态不能放在svr的内存中,要转移到全局的数据库中,从gamesvr中抽出活动逻辑需要解耦,大体包含3个问题:

2.1 数据库:

  • 数据库的内存暴增

  • 数据库的 QPS 暴增

  • 对数据库提供的数据结构也有一定需求

2.2 数据一致性:

  • 最终一致性无法满足所有需求

  • 强一致性需求需要分布式锁,但要避免不必要的阻塞

2.3 模块耦合:

  • 有状态服务大量模块耦合,剥离新服务困难重重

1. 为什么是redis

目前项目组使用的 tcaplus 是互娱研制的一款高速分布式的key-value数据库,效率上没有太大问题,但是没有多样化的数据结构、lua脚本等功能,难以应对无状态化编程带来的挑战。

而 redis 作为业界标准,不仅效率高、还支持多样化的数据结构以及lua脚本等功能,公司也有专门团队提供支持,因此活动svr选用了 redis 作为数据库。

tredis 团队除了原生的redis集群支持,还自研了tredis SSD模式,该模式主要特点是用SSD替换了内存,解决了redis 的内存问题,当然也是有代价的。火影活动数据基本都是周期性数据,一般上线一两个星期,下线后数据都可以自动过期清理,不会常驻内存,数据量本身可控,所以采用低延时的 redis cache 方案更合理。

2. Redis 内存

redis 的瓶颈一般都是内存,只要游戏逻辑合理,QPS一般问题不大。游戏逻辑需要的数据经过 pb 压缩后已经没有优化的可能,在数据量固定不变的情况下,提高 redis 的内存利用率,可以极大的压缩 redis 内存。

2.1 redis string 类型内存模型

uIfMbem.png!mobile

假定redis使用的内存分配器是jemalloc,dicEntry、SDS、redisObject 等结构都是独立分配的内存块,大小只能是 16、32、64…字节(value不大时redisObject和SDS可能会共用一块内存),即使没用完也会统计到used_memory 指标中。

2.2 redis string 类型内存利用率

6bqyqu7.png!mobile

实际需求中,大部分 key 的长度在 24-55 字节之间,value 的长度在 8-39字节之间(value一般都会用 pb 等压缩,所以数据不多的情况下长度不会太长)。按照一定的规则规范化 key,可以缩减 key 长度到 23字节以内,每条记录可以缩减 32 字节的内存,value 的内存利用率将提升25%。

2.3 规范化key

nYZRNzv.png!mobile

实现方法可以用 protobuf 描述层次关系,用反射特性实现名字到id的转换,例如 RedisKey.actsvr.task 被转换成 1|1:

jmqm6bZ.png!mobile

2.4 如何大幅度提高value内存利用率

如果把要使用的 redis 数据都集中到一起,集中存放,则 value 的大小会远大于 key 和其他内存结构的大小,从而使内存利用率达到 50%~99%。然而此方案也有弊端:如果只想取某个子模块的数据也必须把整体数据都拉下来,无状态化的情况下本来就会频繁读写数据,此方案将显著增加 redis 的CPU压力。

redis 的 hash 类型既可以把数据集中存放,也支持 key 分开读写。

2.5 redis hash类型的ziplist内存模型

redis hash 类型在 key 数量少于 512字节(可配置),最大value 成员不超过 64 字节(可配置)时,会采用 ziplist 结构压缩内存占用。

MRnQ3ar.png!mobile

假设数据分为n个模块,每个模块的数据量都不大(8~39字节),两种方案:使用 n 个 string类型分别存储模块数据、使用一个 hash 表n个field存储模块数据的内存利用率对比如下:

7VrIbqV.png!mobile

模块数越多,hash类型提升内存利用率的效果越明显,主要原因是redis内部存储的辅助数据结构占用空间大大减少,次要原因是 hash 表的 field 对应的 key 缩短了不少,因为只需要标识子模块信息即可。

3. 分布式锁

无状态化后,总有一些需求对数据有强一致性要求,这种情况下,只能用分布式锁:互斥锁虽然能满足大多数需求,但是会影响效率,如果不是必要,可以考虑条件锁,符合条件的情况下即使并行也不会阻塞。

3.1 lua 互斥锁

在 redis 中,同一个 key 可以保证在一台机器上,redis 的单线程执行确保了针对此 key 操作时数据的强一致性。实现分布式锁还需要注意判断加锁解锁的条件、防止死锁等问题,用 lua 脚本实现锁可以很方便的避开以上问题。

a. 上锁

SetNxTtl

已经上锁了会直接返回失败,上锁的同时还会设置过期时间,防止死锁。

b. 释放锁

lua_str = "if (redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) end"

有了对内容的对比,确保只释放自己加的锁,不会误释放其他人加的锁。

3.2 lua 条件锁

条件锁可以减少不必要的阻塞,比如同时加入队伍场景下,可以设置条件锁:仅队伍人数小于5才能加入队伍:

"if (redis.call('zcard', KEYS[1]) < 5) then return redis.call('zadd', KEYS[1], ARGV[1], ARGV[2]) end"

程序框架

程序框架需要解决模块耦合的问题,还需要提供统一的数据管理功能,包括读写数据、加锁解锁等。

1. 模块切分

以插件化的思路分离各模块,每个模块作为插件自治,可以单独处理客户端发来的拉详情、指定命令、GM 指令。每个活动都可以灵活的 load 指定插件集合,无需关心插件内部实现,只需要实现活动特有逻辑即可。

ne2AZ3M.png!mobile

2. 数据管理

以 hash 类型存储整个活动数据,每个模块占用一个 field,既可以支持整体活动数据的读写,也支持各模块单独读写数据,便于插件模块自治,同时 redis 也能保持很高的内存利用率。

对数据一致性要求很高的模块有对整体或插件模块上锁的需求,从而实现精准控制冲突域。

eeyiIz6.png!mobile

接口众多,新增需求实现起来必定很复杂,封装一个宏来自动生成代码就显得很有必要了,一个宏可包含以上所有接口及实现:

JfaQrei.png!mobile

五人派对活动设计实例

1. 玩法

五人派对活动是为了增加玩家活跃而设计的组队玩法,玩家可以邀请好友组成最多五人的小队,每个队员只可以翻一张牌,五张牌都不一样,翻牌进度共享,翻牌进度会触发所有队员的任务进度,五个人都翻完牌后所有人都能领取丰厚的任务奖励。

FvQ7RnM.png!mobile

2. 队伍核心操作

2.1 创建队伍

玩家1邀请玩家2、3,玩家2、3同时接受邀请,有可能会创建两个队伍,所以需要加锁

j2qINbE.png!mobile

2.2 加入和退出队伍

队伍已存在时,队伍成员是个 set 类型,即使多名玩家同时操作也不会有问题。

bUNfMfi.png!mobile

2.3 同时加入触发队伍满

用lua条件锁保证后来的成员一定抢不到锁,加入失败。

仅队伍人数小于5才能加入队伍

"if (redis.call('zcard', KEYS[1]) < 5) then return redis.call('zadd', KEYS[1], ARGV[1], ARGV[2]) end"

yuUFFbf.png!mobile

3. 活动玩法核心操作

翻牌集合做成 set 类型,同时翻不同的牌不会冲突。

3.1   玩家同时翻到相同的牌

用 lua 脚本实现条件锁,仅此牌没被翻才翻牌,此牌已翻翻牌失败:

if (redis.call('zadd', KEYS[1], ARGV[1], ARGV[2]) == 1) then return redis.call('zcard', KEYS[1]) end

zInyYre.png!mobile

总结与展望

1. 开发效率保障

插件化的活动框架极大提高了开发效率,插件模块的高度自治使得新活动开发不再需要关注任务、排行、队伍、分享、兑换等功能。

统一的数据管理方案确保了数据的高效可靠,完备的锁机制为数据一致性问题提供了保障。

2. 服务可用性

将不断扩展的活动从 gamesvr 中解耦,独立成actsvr,不仅增强了核心服务的稳定性,也给了活动服良好的扩展性。把活动数据的状态从 gamesvr 转移到redis中,也就是把gamesvr上的部分风险转移到了redis中。一系列优化措施有效稳定了redis的内存、QPS,确保风险可控。一方面redis作为业界标准,可靠性有保障,另一方面火影使用的redis 集群是由公司专业团队提供的支持,不仅支持在线扩缩容,还有完善的监控告警。

目前火影已经为1000w+用户提供了稳定的游戏活动体验,而redis集群的内存和QPS都在掌控中。

MNVfQjr.png!mobile

QJzqQzq.png!mobile

扫码报名赢公仔! 

Zz2iYvj.png!mobile

↓↓200份公仔等你来拿~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK