PolarDB闪电助攻,《香肠派对》百亿好友关系实现毫秒级查询

发布时间:2024-04-18 18:18  浏览量:8

云原生数据库PolarDB分布式版(PolarDB for Xscale,简称PolarDB-X)有极强的线性扩展能力,能够多写多读;它的全局索引能力,是分布式改造的利器,成功解决了传统分布式方案中多维度查询的难题,在《香肠派对》的好友系统上,实现了百亿好友关系20万QPS的毫秒级查询。

——厦门真有趣《香肠派对》服务端主程 洪光裕

真有趣(So Funny)成立于2012年8月,致力于为全球用户提供健康有趣快乐的游戏体验与服务。目前已推出《香肠派对》、《不休的乌拉拉》、《仙侠道》等9款游戏,累计服务2亿多用户。这里聚集着一群有趣的人,秉持用户第一、热爱创作、讲逻辑的理念,为赢得百万人热爱而奋斗!

《香肠派对》是由真有趣游戏开发、由心动网络发行的一款“吃鸡”游戏,“开局一根肠,装备全靠捡”,曾连续三年获得“金翎奖”,拥有非常成熟的职业联赛。

在TAPTAP上,《香肠派对》有2.3亿的下载量,有着庞大的玩家群体:

用户关系是游戏类应用非常普遍的场景,需要存储用户或者玩家之间的相互关系,通过社交关系提升用户的活跃度以及黏性,帮助玩家及时找到有关联的好友。在用户关注关系中,主要包含几种状态:

关注我的人->我的粉丝(fans);我关注的人->我的关注(follow);相互关注的人->互关(mutual);拉黑的人->加黑(黑名单)。

以《香肠派对》为例,该游戏有比较强的社交属性,其好友功能提供了“关注”、“粉丝”、与推荐(找朋友)功能:

其核心表“关注表”对应的表结构示意:

create table user_focus(id bigint primary key,uid bigint, -- 用户IDfocus_uid bigint, -- 关注的用户IDextra varchar(1024), -- 其他业务属性index idx_focus_uid(focus_uid),index idx_uid(uid))

如果一个用户关注了100个人,那么在这张表里有100条记录,目前整个关注表达到了百亿的量级。

这张表有以下几种访问模式:

获取某个用户的关注列表(我关注了谁):select * from user_focus where uid=xxx;获取某个用户的粉丝列表(谁关注了我):select * from user_focus where focus_uid=xxx;圈定一批人,他们关注了谁,谁又关注了他们(用于好友推荐):select * from user_focus where uid in (xxxx);select * from user_focus where focus_uid in (xxxx);

很容易理解,该表存在uid与focus_uid两种查询维度。

在这个量级下,传统的单机数据库容易出现以下几类问题:

索引idx_focus_uid与idx_uid的写入均为随机写入,B+树频繁的SMO(叶子的分裂、合并等),会有各种各样的锁争抢,导致写入RT上升,CPU消耗变多等;B+树层高变高,查询代价变大等;索引太大,Buffer Pool被打爆,产生大量的IO(本质上也是随机写入的问题)等;表的DDL耗时过长,时间不可控等。

无论使用哪种选型,核心是将大表进行拆分。在对该表进行分布式改造时,业务团队有几种选择:

使用Redis、Hbase等NoSQL数据库,这类数据库可以解决扩展性问题,但是:需要业务合理设计Key。如果将uid作为Key,那么无法按照focus_uid进行查询。只能由业务侧将数据写两份,按照uid写一份,按照focus_uid再写一份,并且由业务侧维护两份数据的一致性;改变了业务之前使用关系型数据库的习惯,需要调整大量的代码。Elasticsearch、MongoDB等文档型数据库:

以ES为例,如果为了支持高性能的查询,需要设计合理的DocumentID,否则,ES中的每次查询都会涉及到所有的节点,非常低效。因此又回到了上面的问题,uid和focus_uid两个维度无法兼得。

本质上,上面几种方案,虽然数据做到了水平拆分,但几种数据库内部只能解决单维度查询,多维度的查询问题只能由应用层解决。

阿里云瑶池数据库旗下的PolarDB分布式版成为了更好的选择,在这个场景中,PolarDB分布式版有着无可比拟的优势:

最重要的一点,PolarDB分布式版支持全局索引,在对数据做了水平拆分的基础上,还能支持业务的多维度查询的需求;PolarDB分布式版兼容MySQL的语法和协议,应用从单机MySQL迁移过来无需修改代码,应用研发可以保持以前使用MySQL的思路和习惯。

在PolarDB分布式版中,我们使用分区表的语法对表进行水平拆分,在本例中,我们对表按照uid进行分区:

create table user_focus(id bigint primary key,uid bigint, -- 用户IDfocus_uid bigint, -- 关注的用户IDextra varchar(1024), -- 其他业务属性 index idx_focus_uid(focus_uid),index idx_uid(uid)) partition by hash(uid);

此时对于uid上的查询,自然是很高效的。为了满足focus_uid上的查询,我们只需要执行一条DDL语句,即可为表创建一个全局二级索引:

create global index gsi_focus_uid on user_focus(focus_uid) partition by hash(focus_uid);

全局二级索引本质是一种数据冗余。例如,当执行一条SQL:

INSERT INTO user_focus (id,uid,focus_uid,extra) VALUES (1,99,1000,"xxx");

可以简单理解为,会分别往主表与gsi_focus_uid写入一条记录:

INSERT INTO user_focus (id,uid,focus_uid,extra) VALUES (1,99,1000,"xxx");INSERT INTO gsi_focus_uid (id,uid,focus_uid) VALUES (1,99,1000);

其中user_focus主表的分区键是uid,gsi_focus_uid的分区键是focus_uid。

同时,由于这两条记录大概率不会在一个DN上,为了保证这两条记录的一致性,我们需要把这两次写入封装到一个分布式事务内(这与单机数据库中,二级索引通过单机事务来写入是类似的)。

当我们所有的DML操作都通过分布式事务来对全局索引进行维护,二级索引和主键索引就能够一直保持一致的状态了。

此外,PolarDB分布式版为GSI的性能也做了非常多的优化,例如:

数据库的响应时间由创建前的平均数百毫秒,下降到了创建后的1-2ms,同时TPS提升了数百倍

数据库响应时间,单位毫秒

同时,全局索引的添加,使得绝大多数查询做到了“本地化”,可以理解为,一个SQL只对一个DN发起请求。这样,整个系统拥有了极高的扩展上限,可以做到线性扩展,也即如果10个节点可以支撑50W的QPS,那么20个节点就可以支撑100W的QPS。

下图是业务上线后的效果,使用8个节点,在峰值达到20W QPS的情况下,依然保持着1ms的响应时间:

数据库QPS,峰值20W左右

数据库响应时间,单位毫秒

使用全局索引,会带来查询性能的提升,但也要注意合理使用,让它发挥更大的效果。下面给出一些最佳实践:

全局索引的数量不宜过多,通常一个表在2个以内。全局索引通常代表某种业务维度,例如,本例中典型的是关注和被关注,对于其他字段的查询加速,应使用本地索引。在某些场景下,我们也可以使用CO_HASH来实现多维度的查询,可参考《从淘宝订单号的秘密说起......》在一对多的场景下,使用聚簇的全局索引,可以有效减少回表的代价。关于聚簇的全局索引,请参考:《PolarDB-X 全局二级索引 - 知乎》时间、日期等字段,不宜建全局索引,通常使用本地索引即可。使用索引诊断功能(执行INSPECT INDEX即可),可以找到一些冗余、未使用的全局索引,避免不必要的空间、资源消耗,可参考:《这条索引在“磨洋工”吗?聊聊数据库中的烂索引》、《如何进行索引诊断_云原生数据库 PolarDB(PolarDB)-阿里云帮助中心

从单机到分布式的过程中,全局索引是非常重要的能力,也是衡量分布式数据库的重要标准。使用没有全局索引的数据库,业务将陷入无穷无尽的与多维查询斗争的局面。

PolarDB分布式版的全局索引可以很好地满足类似于游戏领域好友系统这种多维查询的需求:

它是强一致的并内聚的,不需要业务通过第三方组件来实现,不需要业务去维护索引数据的一致性;它的全局索引本身就是分区的,不需要担心全局索引本身的扩展性问题;它上线多年,稳定可靠,有超过80%的PolarDB分布式版用户都在使用全局索引;它能极大地减轻开发人员的学习成本,让开发人员能将精力集中在满足业务需求上,例如,可以沿用使用单机数据库时SQL优化的经验,通过加索引的手段去改善SQL的执行性能。

PolarDB分布式版云原生分布式弹性能力,在解决了业务多维查询需求的同时,极大地满足了客户随时扩缩容的诉求,实现了降本增效,是游戏行业中好友关系场景下典型方案的代表。”

外部推荐