MongoDB 那些坑
本文内容纲要:
MongoDB是目前炙手可热的NoSQL文档型数据库,它提供的一些特性很棒:如自动failover机制,自动sharding,无模式schemaless,大部分情况下性能也很棒。但是薄荷在深入使用MongoDB过程中,遇到了不少问题,下面总结几个我们遇到的坑。特别申明:我们目前用的MongoDB版本是2.4.10,曾经升级到MongoDB2.6.0版本,问题依然存在,又回退到2.4.10版本。
MongoDB数据库级锁
坑爹指数:5星(最高5星)
MongoDB的锁机制和一般关系数据库如MySQL(InnoDB),Oracle有很大的差异,InnoDB和Oracle能提供行级粒度锁,而MongoDB只能提供库级粒度锁,这意味着当MongoDB一个写锁处于占用状态时,其它的读写操作都得干等。
初看起来库级锁在大并发环境下有严重的问题,但是MongoDB依然能够保持大并发量和高性能,这是因为MongoDB的锁粒度虽然很粗放,但是在锁处理机制和关系数据库锁有很大差异,主要表现在:
- MongoDB没有完整事务支持,操作原子性只到单个document级别,所以通常操作粒度比较小;
- MongoDB锁实际占用时间是内存数据计算和变更时间,通常很快;
- MongoDB锁有一种临时放弃机制,当出现需要等待慢速IO读写数据时,可以先临时放弃,等IO完成之后再重新获取锁。
通常不出问题不等于没有问题,如果数据操作不当,依然会导致长时间占用写锁,比如下面提到的前台建索引操作,当出现这种情况的时候,整个数据库就处于完全阻塞状态,无法进行任何读写操作,情况十分严重。
解决问题的方法,尽量避免长时间占用写锁操作,如果有一些集合操作实在难以避免,可以考虑把这个集合放到一个单独的MongoDB库里,因为MongoDB不同库锁是相互隔离的,分离集合可以避免某一个集合操作引发全局阻塞问题。
建索引导致数据库阻塞
坑爹指数:3星
上面提到了MongoDB库级锁的问题,建索引就是一个容易引起长时间写锁的问题,MongoDB在前台建索引时需要占用一个写锁(而且不会临时放弃),如果集合的数据量很大,建索引通常要花比较长时间,特别容易引起问题。
解决的方法很简单,MongoDB提供了两种建索引的访问,一种是background方式,不需要长时间占用写锁,另一种是非background方式,需要长时间占用锁。使用background方式就可以解决问题。
例如,为超大表posts建立索引,
千万不用使用
db.posts.ensureIndex({user_id:1})
而应该使用
db.posts.ensureIndex({user_id:1},{background:1})
不合理使用嵌入embeddocument
坑爹指数:5星
embeddocument是MongoDB相比关系数据库差异明显的一个地方,可以在某一个document中嵌入其它子document,这样可以在父子document保持在单一collection中,检索修改比较方便。
比如薄荷的应用情景中有一个Groupdocument,用户申请加入Group建模为GroupRequestdocument,我们最初的时候使用embed方式把GroupRequest放置到Group中。
Ruby代码如下所示(使用了MongoidORM):
classGroup
includeMongoid::Document...embeds_many:group_requests...endclassGroupRequestincludeMongoid::Document...embedded_in:group...end
这个使用方式让我们掉到坑里了,差点就爬不出来,它导致有接近两周的时间系统问题,高峰时段常有几分钟的系统卡顿,最严重一次甚至引起MongoDB宕机。
仔细分析后,发现某些活跃的Group的group_requests增加(当有新申请时)和更改(当通过或拒绝用户申请时)异常频繁,而这些操作经常长时间占用写锁,导致整个数据库阻塞。原因是当有增加group_request操作时,Group预分配的空间不够,需要重新分配空间(内存和硬盘都需要),耗时较长,另外Group上建的索引很多,移动Group位置导致大量索引更新操作也很耗时,综合起来引起了长时间占用锁问题。
解决问题的方法,说起来也简单,就是把embed关联更改成的普通外键关联,就是类似关系数据库的做法,这样group_request增加或修改都只发生在GroupRequest上,简单快速,避免长时间占用写锁问题。当关联对象的数据不固定或者经常发生变化时,一定要避免使用embed关联,不然会死的很惨。
不合理使用Array字段
坑爹指数:4星
MongoDB的Array字段是比较独特的一个特性,它可以在单个document里存储一些简单的一对多关系。
薄荷有一个应用情景使用遇到严重的性能问题,直接上代码如下所示:
classUser
includeMongoid::Document...field:follower_user_ids,type:Array,default:[]...end
User中通过一个Array类型字段follower_user_ids保存用户关注的人的id,用户关注的人从10个到3000个不等,变化是比较频繁的,和上面embed引发的问题类似,频繁的follower_user_ids增加修改操作导致大量长时间数据库写锁,从而引发MongoDB数据库性能急剧下降。
解决问题的方法:我们把follower_user_ids转移到了内存数据库redis中,避免了频繁更改MongoDB中的User,从而彻底解决问题。如果不使用redis,也可以建立一个UserFollower集合,使用外键形式关联。
先列举上面几个坑吧,都是害人不浅的陷阱,使用MongoDB过程一定要多加注意,避免掉到坑里。
针对使用中遇到的问题,谈点我自己的感受吧,就谈点注意事项而已。
-
一定要合理创建索引,有很多人都被宣传片迷惑,认为mongo的读取速度本身就应该很快,所以从mysql转过来后,就连创建索引都忘了,当表(collection)很大时,不创建索引是非常影响性能的。创建索引很简单,如果你不想使用shell那么麻烦,直接在model里面声明就是了:index({xxx:1},{unique:true,background:true});然后运行一个rake命令:rakedb:mongoid:create_indexes就ok了,这个命令不会重复创建的。
-
大表查询时,只返回你想要的列,楼主讲了很多write的性能问题,可能是场景不同的原因,我们大量遇到了查询的性能问题;这一点就不用多说了吧,其他关系型数据库也有这种问题。特别是单collection字段数据量比较大时,非常容易引起性能问题,在rails里面也很简单,查询时加上only就是了。比如User.where(xxx).only(:f1,:f2)。
-
尽量一次返回所有需要的数据,避免GET_MORE,避免游标操作,当用户进行查询迭代时,mongo会首先返回一个数据块供你迭代,当你迭代的数据超过这个数据块时,mongoid发起GET_MORE命令移动游标获取下一个数据块,而就是这个移动游标的操作就非常慢,特别是你返回的列比较多的时候,性能非常低。每次返回的数据块的大小是由batchSize控制的,可以通过修改它的默认值进行控制。
-
尽量避免在model里面使用Array类型的字段,原因楼主已经说了,不过我们遇到的还是查询的问题,因为你使用了Array,查询时,你不可避免的会使用##in##操作,in操作无法利用索引,这个在关系型数据库里面也是存在的,大表操作一定要避免。
-
不要在和数据库直接相关的model里面使用继承,什么意思呢?就是modelB<modelA,而他们都是mongo里面的document,为什么不能这样?因为mongoid的内部实现其实只会创建一张表就是documentA,然后在documentA里面用一个_type字段来标识documentB,这样当你查询modelB时,内部会生成一个查询到documentA的语句,那个查询就是用的_typein[xxxx]类似这样的语句,你看又是in操作。如果这种情况你是在后期才发现的,你真是回天无术,想死的心都有:)。
-
事务,还是事务,mongodb不支持事务,所以你一定要考虑清楚,权衡利弊。我们有些功能就必须使用事务,没办法,我想到一个非常丑陋的方法,记录每个创建和更新的model,它的id和更新数据,如果一旦有异常,我就撤销更新和创建,真的是非常麻烦。想想看在一个支持事务的关系型数据库里面,这些是非常简单的。
-
主从备份还不是很成熟,这一点,估计是我研究的不深入的原因,我仍然认为主从备份不是很成熟,有些时候简直就是提心吊胆,如果有经验的同学在这里,可以多多讨论。
本文内容总结:
原文链接:https://www.cnblogs.com/l1pe1/p/7871859.html