前言
在2016年5月份的某一天,我和菠萝同学怀着对昔日《红警95》的缅怀之情,相约脱胎于开源项目OpenRA,来自制Server、Web、个人系统等,重现《红警95》的昔日光芒。继而取名《红色警戒:复兴》。
在经过了将近一年的蛰伏后,作为《红色警戒:复兴》的联合创始人,终于有幸在今年4月份见证他的第一次公开亮相。
《红色警戒:复兴》官方网站
为何选用MongoDB
在构建项目之初,曾以为的天马行空、伟大蓝图在实际操作过程中遇到了非常多的阻碍。脱胎于开源项目,用.NET实现的游戏引擎,当我们还在为windows服务器不稳定而纠结的时候,MS发布了.NET CORE!这一振奋人心的事情立马让我们决定去大干一番!
(中间省略1万字……)
然后在实际开发过程中,遇到了一个比较棘手的问题。我们希望记录下战斗中所有的战斗数据以供后期采用统计分析方法对收集来的大量数据进行分析,提取有用信息并形成结论而对数据加以详细研究、概括总结。
我们先来看一个gif图
从该gif中,我们可以看到红色玩家单位中有大量的坦克并摧毁了绿色玩家的建筑、士兵、坦克等单位,这一场战斗的数据是会直接记录到我们的MongoDB中,最后当游戏结束时作统一处理。
这里考验数据库性能的点在于,我们需要记录这一回合中,红色玩家有多少A单位、B单位……,其中包括建筑、士兵、坦克、飞机、防御单位等。在游戏过程中的所有游戏数据我们都是存mongo的,考虑到大量的计算,MySQL无法胜任。
因此最终的DB选型就是MongoDB作为游戏中的数据计算容器,MySQL作为最终的数据落盘容器。这也是许多项目采用的一种MongoDB+MySQL的解决方案。
设计模式惹的祸
在使用MongoDB记录数据的时候,通过OPS记录,发现当游戏进度推进至20分钟后(注意这个时间点,由于游戏特性,决定了往往在激战20分钟后,会出现经济大于生产的情况),兵种数量急剧增加,MongoDB CPU负载就会骤然飙升,而这仅仅是在测试环境,若投入生产,,这种情况一般我是采取零容忍的,接下来就是一段改造的过程。
首先一起了解下起初的设计结构:
{
"_id" : ObjectId("58ecdc0b5003d2a379b871df"),
"profileId" : 1,
"gameId" : 1,
"units" : {
"soldiers" : {
"Rifle" : 23,
"Grenadier" : 46
},
"vehicles" : {
"V2" : 15,
"Light" : 7
}
},
"money" : {
"earned" : 12345,
"spent" : 54321
}
}
以上Json是游戏中记录的部分的战斗数据结构。需求很简单,需要记录下实时的玩家数据,比如士兵中有一种兵种类型Rifle,当前数量为23个;V2远程坦克有15辆等等。
为什么会造成数据库的负载过高呢?
目前发现一个现象,当数据量不是特别大的时候(也就是之前所强调的20分钟这个零界值),服务器的cpu load并没有表现出异常,而一旦过了零界值,在经济远大于生产的情况下,玩家往往会持续输出战斗单位、防御单位、基本建筑等。这个阶段,我们的MongoDB往往是在做高频率的update操作,且是对同一条数据中的不同数值进行操作。
分析
- 排除NUMA启动。
由于官方有明确表示,因此我也关闭了NUMA,具体NUMA对MongoDB的影响可参照 MongoDB Manual 关于NUMA的解释
这里引用一段:
Running MongoDB on a system with Non-Uniform Access Memory (NUMA) can cause a number of operational problems, including slow performance for periods of time and high system process usage.
- 排除CPU分配不均。
鉴于MongoDB Manual中有提到对于云服务或者虚机的使用注意事项,因为我们也像服务商确认过了,不存在如下引用 MongoDB on Virtual Environments
When using MongoDB with KVM, ensure that the CPU reservation does not exceed more than 2 virtual CPUs per physical core.
- 排查slow log
无论计算机发展的如何遵循摩尔定律,冯诺依曼体系结构是不会变的。也就是说我们的cpu是顺序执行的。
因此在排除了前面2种情况后,我们需要确认的是是否存在慢查询了。
也就是在slow log中,我们发现大量的10s以上的update操作。这无疑使我们更接近了真相。因为真相只有一个。
为什么在零界值前cpu load并没有表现出过载的情况?
对于update涉及到的查询条件都已经加上了索引,但并未有明显改善。
但是慢查询是摆在那里的,不离不弃。我们尝试着对profileId进行频繁update,但CPU显然没有任何波动。
至此,我们初步断定,症结点在深层嵌套文档导致的数据多层寻址引发的。
Embedded data models allow applications to store related pieces of information in the same database record. As a result, applications may need to issue fewer queries and updates to complete common operations.
显然我们错误的理解了MANUAL中对于embedded doc 的用法。
通过抓取currentOp,进一步确认,问题就在update的内嵌文档上。
纵观这条战斗数据,细心的你一定发现了,我们的数据嵌套非常深,而对于这条数据中最需要计算的部分是藏在最里面的,3层嵌套的计算对于mongodb来说是非常吃力的,需要耗费巨大的cpu去做这件事情。
经过对业务需求的再整理,达成一个共识,可以暂时抛弃对于兵种的分类,只记录实际存在的数据。
基于以上思路,我们得出了如下的model:
{
"_id" : ObjectId("58ecdeed5003d2a379b871e0"),
"profileId" : 1,
"gameId" : 1,
"Rifle" : 23,
"Grenadier" : 46,
"V2" : 15,
"Light" : 7,
"earned" : 12345,
"spent" : 54321
}
通过改造data model,除去兵种的分类,只对已有的单位进行update、新增的兵种进行初始化即可。
从3层嵌套中解放出来,放在一级目录下,再次测试,cpu毫无压力。
至此,一次关于MongoDB的Schema Design/Data Model改造到此告一段落。
最后附上原子弹的gif,这个动图其实也是对我们mongodb的一次考验,需要瞬时减去相对应数量的单位。
最后欢迎大家来一起体验我们的《红色警戒:复兴》!
也欢迎志同道合的小伙伴通过 官方网站-加入我们 来加入我们的团队,为复兴红警努力!