« 谈谈陌陌争霸在数据库方面踩过的坑(排行榜篇) | 返回首页 | 谈谈陌陌争霸在数据库方面踩过的坑( Redis 篇) »

谈谈陌陌争霸在数据库方面踩过的坑(芒果篇)

我们公司开始用 mongodb 并不是因为开始的技术选型,而是我们代理的第一款游戏《 狂刃 》的开发商选择了它。这款游戏在我们代理协议签订后,就进入了接近一年的共同开发期。期间发现了很多和数据库相关的问题,迫使我们熟悉了 mongodb 。在那个期间,我们搭建的运营平台自然也选择了 mongodb 作为数据库,这样维护人员就可以专心一种数据库了。

经过一些简单的了解,我发现国内很多游戏开发者都不约而同的采用了 mongodb ,这是为什么呢?我的看法是这样的:

游戏的需求多变,很难在一开始就把数据结构设计清楚。而游戏领域的许多程序员的技术背景又和其他领域不同。在设计游戏服务器前,他们更多的是在设计游戏的客户端:画面、键盘鼠标交互、UI 才是他们花精力最多的地方。对该怎么使用数据库没有太多了解。这个时候,出现了 mongodb 这样的 NOSQL 数据库。mongodb 是基于文档的,不需要你设计数据表,和动态语言更容易结合。看起来很美好,你只需要把随便一个结构的数据对象往数据库里一塞,然后就祈祷数据库系统会为你搞定其它的事情。如果数据库干的不错,性能不够,那是数据库的责任,和我无关。看到那些评测数据又表明 mongodb 的性能非常棒,似乎没有什么可担心的了。

其实无论什么系统,在对性能有要求的环境下,完全当黑盒用都是不行的。

游戏更是如此。上篇我就谈过,我们绝对不可能把游戏里数据的变化全部扔到数据库中去做。传统数据库并非为游戏设计的。

比如,你把一群玩家的坐标同步到数据库,能够把具体某个玩家附近玩家列表查询出来么?mongodb 倒是提供了 geo 类型,可以用 near 或 within 指令查询得到附近用户。可他能满足 10Hz 的更新频率么?

我们可以把玩家的 buf 公式一一送入数据库,然后修改一些属性值,就可以查询到通过 buf 运算得到的结果么?

这类问题有很多,即使你能找到方法让数据库为你工作,那么性能也是堪忧的。当我们能在特定的数据库服务内一一去解决她们,最终数据库就是一个游戏服务器了。


狂刃这个项目在我们公司是负责平台建设的蜗牛同学跟的。我从他那里听来了许多错误使用 mongodb 的趣闻。

一开始,整个数据库完全没有为查询建索引。在没什么数据的情况下,即使所有的查询都是 O(N) 的,遍历整个数据库,也不会有问题。可想而知,用户量一上来,性能会下降的多快。

然后,数据库又被建立了大量的无用的索引,和一些错误的复合索引,同样恶化了系统。感觉就是哪里似乎有点性能问题,那就是少了个索引的缘故。这种病急乱投医的现象,在项目开发后期很容易出现。其实解决方法很简单:主导设计的人只要静下心来好好想一想,数据库系统其实也就是一个管理数据的封闭模块。如果你来管理这些数据,怎样的数据结构更利于满足特定的检索,需要哪些索引数据辅助。

最终的问题依旧是算法和数据结构,不同的是,不需要你实现它,而需要你理解它。

另外,数据库是被设计成可以并发访问的,而并发永远是复杂的东西。mongodb 缺乏事务操作,需要用文档操作的原子性来模拟。这很容易被没经验的人用错(这是个怪圈,越是没数据库经验的人越喜欢 mongodb ,因为限制少,看起来更自然。)。

狂刃出过这样一个 bug :想让用户注册的时候用户名唯一,所以在用户注册的时候先查一下数据库看用户名是否存在,如果不存在就允许创建一个这个名字的用户。可想而之,上线运营不出一天,同名用户就会出现了。


因为公司项目需要,我给 skynet 增加了 mongo driver 。老实说,实现这个 driver 的时候,我对 mongo 就兴趣寥寥。最后只实现了最底层的通讯协议,光这个部分,它的协议设计就已经是很难看的了。但是即使这样,我也耐着性子把这部分做完,而不想使用现成的 driver 。

mongo 的官方 driver 都是内置 socket 通讯模块的。这种做法很难单独把协议解析部分提取出来,附加到自己项目的 IO 模型中去。(btw, redis 这方面就好的多,因为它的协议足够简单,你可以用几十行代码就实现它的通讯协议,而不需要依赖 driver 模块。)

狂刃服务器的 IO 采用的 boost.asio ,我很好奇他是怎样把 mongodb 官方 C++ driver 整合进去的。不出所料,他们开了一个独立线程处理 mongo 的数据,然后把数据对象跨线程发出来。细究这个实现就能看出问题来。程序员很容易误解 mongodb client api 的内在含义。

一开始,狂刃的开发同学以为从 mongo 中取到一组查询结果后,调用 cursor 的 findnext 只在对象内存中迭代,所有结果都是一开始一次性返回的。以为把一开始的 bson 对象从 mongo 线程转移到主线程中就好了。可事实并不是这样,mongo 一次只会返回一组查询结果,当结果迭代完时,findnext 还会自动提交新的查询请求。这时,对象已经不在原有的 mongo 线程中了。

学过 C++ 的同学可以想像一下,让你去 code review 不是你参于的 C++ 项目去找到 bug 需要多少功夫?对了,你还要在想像中要加上被各种 boost.asio 回调函数拆得支离破碎的业务流程。所以去年有那么一段日子,我们需要完全停下手头其他的工作,认真的从头阅读那数以万行计的 C++ 代码。


老八卦别人似乎不太厚道,下面来谈谈我们自己犯的错误。

陌陌争霸出的第一起服务器事故是在 2014 年一月中旬的一个周末。准确说,这次算不上重大运营事故,因为没有玩家数据受损,也没有意外停服。但却是我们第一次发现早先设计中有考虑不足的地方。

1 月 12 日周日。下午 17 点左右,我们的 SA Aply 发现我们运营用的 log 延迟了 3 个小时才到运营平台。但数据还是源源不断的进入,系统也很稳定,就没有特别深究。

到了晚上 20 点半,平台组的刘阳报告说运营数据已经延迟了 5 个小时了,这才引起了大家的警觉。由于是周末,开发人员都回家休息了,晓靖 21 点上线检查,这时发现游戏服务器内存占用比平常同期高了 10G 之多,并在持续上升。

我大约是在 21 点接到电话的,在电话中讨论分析了一下,觉得是 log 数据从 skynet 的 log 服务发走,可能被积压在 socket server 的一个链表上。这段代码并不复杂,插入新的写入数据是 O(1) 操作,所以没有阻塞玩家游戏的风险。而输出 log 的频率还不至于短期把所有内存吃光。游戏服务器暂时是安全的。

晚 21 点 40 分,虽然没能分析出事故的源头,但我们立刻采取了应急方案。重新启动了一套游戏服务器,在线将旧服务器上的 80% 玩家导到新的备用服务器上。并同时启动了新的 log 数据库集群。打算挺到周一再在固定维护时间处理。

晚 23 点,新启动的游戏服务器也出现了 log 输出延迟。因为运营 log 是输出到一个 mongos 管理的集群中的,我们尝试在旧的集群(已无新数据写入,但依旧没有消化完滞留的旧数据)做了删除部分索引的尝试,没有什么效果。

凌晨 0:45 ,开启了新的备机群,取消了 mongos ,让每台机器独立连接一个单独的 mongodb ,情况终于好转了。

以上,是当时事故记录的节选。


彻底搞明白事故起源是周二的事情了。

表面上看起来是在 mongos 服务上堆积了大量的数据库插入操作。让这个单点过载了。我们起初的运营 log 输出是有点偏多,比如每个士兵的训练都有一条单独的 log ,而陌陌争霸游戏中这种 log 是巨量的。我们裁减并精简了一部分 log 但似乎并不能从根本上解释这起事故。

问题出在 mongos 的 shard key 的选择上。mongo 可以指定 document 的若干字段为 shard key ,mongos 把这个 key 当成一个整数,按整数区间把 document 分成若干个桶。再把桶均匀分配到背后的从机上。

如果你的 key 是有规律的数字,而你又需要这种规律不至于破坏桶分配的公平性,你还可以将一个 hash 算法应用于原始选择的 key 上,让 key 足够散列开。我们一开始就是按自增 id 的散列结果做 key 的。

错误的 shard key 选择就是这起事故的罪魁祸首。

因为我们是大量的顺序写操作,应该优先保证写入的流畅。如果用随机散列的方式去看待这些 document 的话,新旧 log 就很大几率被分配到一起。而 mongo 并不是一条一个单位将数据落地的,而是一块块的进行。这种冷热数据的交织会导致写盘 IO 量远远大于 log 实际的输出量。

最后我们调整了 shard key ,按 log 时间和自增 id 分开,就把 mongo 数据落地的 IO 量下降了几个数量级。

看吧,理解系统如何工作的很重要。


ps, 这起事故后,我给 skynet 加了更多的监控,方便预警单个模块的过载。这帮助我们更快的定位后面出现的问题。那些关于 redis 的故事,且听下回分解。


3 月 5 日补充:

根据下面的留言讨论,总结一下:

关于 shard key 的选择在 mongoDB 文档中被讨论过 。但和我们遇到的情况有所不同。

有同学提到,这篇文章里描述在批量写入的时候,数字做 key 要比 hash 过的有更高的效率

我们没有使用批量插入,而我们是单条逐条插入的。所以性能低下并不在于逐条调用 getLastError ,我们为了保证写入性能,都是单向推送,不获取 getLastError 的(最低 Write concern 级别)。我认为在我们的业务情况下,按时间片让一台机器接受一组数据是更好的利用方式。

Comments

"最后我们调整了 shard key ,按 log 时间和自增 id 分开" 这样岂不是会让同一时间的所有log都写到同一个分片中,反而把IO集中了。之前散列反而出现问题,集中写倒是好了?如果以说冷热数据一起写磁盘导致的话,应该只会在第一次满足buffer才会将冷数据写磁盘,后面的都是热数据了吧
"在线将旧服务器上的 80% 玩家导到新的备用服务器上。"想请教下这里指的是目前在线的玩家吗?是如何操作的?
名字唯一的那个问题,讲名字字段设置为唯一索引不就可以在一定程度上避免这个问题?
shard key用mongo的hash特性效率会很低吗? 如下配置shard key时 db.runCommand({shardcollection:'user_data.users', key:{uid:"hashed"}}) 请看看,我还没做过这种性能测试,也没看到mongo官方对hashed有介绍性能方面。
可以一两句话很清晰很逻辑说明现象、原因然后分析下?
说了这么多,无非就是log是顺序写场景,给弄成了随机写造成的性能下降罢了
关注性能的系统,都需要吃透其中各种关键任务的关键动作是如何做的。对数据库系统来说,必须理解数据从应用到磁盘的整个流动过程。对运维人员来说,学习任何一种数据库都是一件大任务,因为要求吃透。这一点和应用开发人员完全不同
mongodb 中的 mongo 是 humongous 的意思,和 mango 芒果一点关系也没有,希望风云修正一下,以免误导别人。
看了一下云风贴的文档,似乎跟文章中的解释有出入。 文档中写的是,自增id由于其规律的增长,这样同一时段产生的log会堆积给一个shard(一台机器)处理,从而导致集群的写入性能被单个机器的写入性能决定。 因此更好的方法是采用更加随机的shard key,这样能充分利用上集群的性能。 跟文中所说的冷热数据似乎无关。理解得不当请指正。
@cloud 下面这篇blog也提到了sharding时,插入很慢的问题,不过他是批量插入。 后来他把文档批量插入前按照Shard Key排了下序就改善了,不过还是没达到单shard插入的效率。 看来mongodb是还很不成熟啊。 http://cfc.kizzx2.com/index.php/slow-batch-insert-with-mongodb-sharding-and-how-i-debugged-it/
@下雨在家 我没有特别仔细读文档,因为这部分的设计、实现、调整、优化等都是我的同事做的。 所以文章涉及的技术细节大多是我听来的 :) 不过做的同学特别强调,是因为我们一开始对分片用的 key 做 hash 导致了写性能低劣。而后通过调整 key 而解决了问题。
@cloud “因为 mongo 不会让单条记录落地,而是把分到一起的数据一起打包。这样每次有热数据进来,就会连同冷数据一起存盘” 原来如此啊,可是在你给的文档链接里http://docs.mongodb.org/manual/core/sharding-shard-key/ 说的却是如果用自增的id,会让新增的Insert由一台shard执行,这样“As a result, the write capacity of this shard will define the effective write capacity of the cluster.”,从而无法scaling. 看来官方文档是在挖坑啊。
"在线将旧服务器上的 80% 玩家导到新的备用服务器上" 可否将这部分展开说一下
这个故事等于告诉你老程序员是有价值。
深有感触啊,很多人总是对关系数据库没有足够的理解,就一味使用nosql 显然当年的数据库原理一再强调的一致性、原子性都被抛在脑后了 无论mongo还是redis,除非能够像关系型数据库那样有专门研究优化的DBA,否则任何使用她的程序员,哪怕是设计者,都必须清楚她的每一行代码是做什么的
的确, 理解系统很重要啊. 不然一个"自认为"很好的设计用到不对的系统上...会适得其反...
狂刃3年,满满都是泪呀
这里应该是出现了磁盘上换入换出热数据(索引,热数据之类的)频率增高,而同时又有写入的数据,所以导致磁盘读写io剧增。
因为 mongo 不会让单条记录落地,而是把分到一起的数据一起打包。这样每次有热数据进来,就会连同冷数据一起存盘。
最后一节关于事故的原因没有看懂,如果是按自增id的散列值作为Shard Key, 这样日志会被均分到多个机器上,应该是对写入性能有利的,怎么会导致“冷热数据的交织会导致写盘 IO 量远远大于 log 实际的输出量。”呢?
干货
很多干货,学习了

Post a comment

非这个主题相关的留言请到:留言本