数据库 MongoDB 那些坑

vincent · 2014年06月23日 · 最后由 vincent 回复于 2018年02月23日 · 40809 次阅读
本帖已被设为精华帖!

MongoDB 是目前炙手可热的 NoSQL 文档型数据库,它提供的一些特性很棒:如自动 failover 机制,自动 sharding,无模式 schemaless,大部分情况下性能也很棒。但是薄荷在深入使用 MongoDB 过程中,遇到了不少问题,下面总结几个我们遇到的坑。特别申明:我们目前用的 MongoDB 版本是 2.4.10,曾经升级到 MongoDB 2.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})

不合理使用嵌入 embed document

坑爹指数:5星

embed document 是 MongoDB 相比关系数据库差异明显的一个地方,可以在某一个 document 中嵌入其它子 document,这样可以在父子 document 保持在单一 collection 中,检索修改比较方便。

比如薄荷的应用情景中有一个 Group document,用户申请加入 Group 建模为 GroupRequest document,我们最初的时候使用 embed 方式把 GroupRequest 放置到 Group 中。 Ruby 代码如下所示(使用了 Mongoid ORM):

class Group
  include Mongoid::Document
  ...
  embeds_many :group_requests
  ...
end

class GroupRequest
  include Mongoid::Document
  ...
  embedded_in :group
  ...
end

这个使用方式让我们掉到坑里了,差点就爬不出来,它导致有接近两周的时间系统问题,高峰时段常有几分钟的系统卡顿,最严重一次甚至引起 MongoDB 宕机。

仔细分析后,发现某些活跃的 Group 的 group_requests 增加(当有新申请时)和更改(当通过或拒绝用户申请时)异常频繁,而这些操作经常长时间占用写锁,导致整个数据库阻塞。原因是当有增加 group_request 操作时,Group 预分配的空间不够,需要重新分配空间(内存和硬盘都需要),耗时较长,另外 Group 上建的索引很多,移动 Group 位置导致大量索引更新操作也很耗时,综合起来引起了长时间占用锁问题。

解决问题的方法,说起来也简单,就是把 embed 关联更改成的普通外键关联,就是类似关系数据库的做法,这样 group_request 增加或修改都只发生在 GroupRequest 上,简单快速,避免长时间占用写锁问题。当关联对象的数据不固定或者经常发生变化时,一定要避免使用 embed 关联,不然会死的很惨。

不合理使用 Array 字段

坑爹指数:4星

MongoDB 的 Array 字段是比较独特的一个特性,它可以在单个 document 里存储一些简单的一对多关系。

薄荷有一个应用情景使用遇到严重的性能问题,直接上代码如下所示:

class User
  include Mongoid::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 过程一定要多加注意,避免掉到坑里。

参考资料:

原创首发于我的blog http://xiewenwei.github.io/

共收到 74 条回复

太好了,谢谢分享

#1楼 @winnie 嘿嘿,这些都是比较深的坑,大家有遇到也欢迎补充啊。

开始还以为楼主是医生同学,暗自感叹8个月就这么厉害。后来细看不是。虽然楼主和医生同学仍旧很厉害。

想请教楼主,为什么你选型会选MongoDB? MongoDB有什么PostgreSQL不能做到的吗?

用了mongodb后就不用写数据迁移那些文件不知道是好处还是坏处。。。

谢谢薄荷折腾出这么多经验,相信你们的程序员也在折腾中成功瘦身。

我还是先用着 mysql 吧,至少 Mysql 撅屁股我就知道它拉什么屎,但 MongoDB 还是女人心海底针的阶段。

#3楼 @billy 哈,我是医生同学的同事啦。 选型 MongoDB 是一个复杂的问题,最主要说来是两大方面:

1. 更容易映射数据对象

如果被 MySQL 的 alter table 操作折腾过,你一定会觉得 schemaless 非常棒。 PostgreSQL 我了解不多。

2. 更好的扩展性,伸缩性

MongoDB 很容易分布式集群,failover, auto sharding 直接就提供了的,其它数据库做起来会很麻烦。

不过掉到上面几个坑的时候的确有悔不当初的感觉,但是既然已经用了就难以回去了,只有硬着头皮去填坑了。

#5楼 @Peter 确有悔不当初的感觉,冲动的时候曾想全部换回 MySQL 算了,但是既然已经用了就难以回去了,只有硬着头皮去填坑了。

哈,人生不就在于折腾吗 珍爱生命,远离肥胖

写的太好了

PostgreSQL 9.4 将于第三季度发布,该版本最大特色是新增:JSONB 类型,即二进制json格式。 用json替代可变表内容属性(比如不同商品的不同特定属性:颜色,重量等)将成为一种可能,这也是文档型数据库我最喜欢的一点。 JSONB的查询性能已经快于MongoDB,惊讶不? https://plus.google.com/+ThomBrownUK/posts/1JizRBGPYBq http://www.reddit.com/r/programming/comments/1q3skb/postgresql_94_is_now_faster_than_mongodb_for/

Postgres-XL 是 PostgreSQL 的 集群解决方案,不过版本有点跟不上

#9楼 @winnie @billy @Peter 其实我们薄荷也就在一个比较新的项目上用了 MongoDB,目前绝大部分还是用 MySQL 和 Redis。 #9楼 @winnie 那相当值得期待,回头好好研究一下,非常感谢。

@vincent 多谢你的观点!

收藏下来慢慢琢磨,@vincent能把这种干货分享出来,实在难能可贵

mongo事务锁可以用redis的锁来搞嘛哈哈,用mongo正嗨皮的时候肯定会忽略若干这样的问题

mongo 2.6 2.8 会解决一些问题,比如行级锁定

#14楼 @shiguodong 2.6 还没有集合锁,据说 2.8 会有文档锁。

infoq 上有 MongoDB 产品营销总监 Kelly Stirman 的访谈。

更细粒度的锁可能是请求最多的特性。与数据库级相比,你们更进一步的路线图是什么?与集合级锁相比,更进一步的主要障碍是什么? 重要的是要记住,MongoDB中的锁与RDBMS中的“闩(latch)”非常接近——它们非常简单,通常持有10微秒或者更少。MongoDB 2.2引入了更高级的锁让步算法,显著减少了我们在社区中看到的与锁争用相关的问题的数量。不过,我们认识到,还有机会改进并发性,其中包括更细粒度的锁。 MongoDB 2.8将具备文档级锁。我们认为,与集合级锁相比,这会更显著地改进更广泛应用程序的并发性。但是,更细粒度的锁只是改进并发性的一部分,我们将改进数据库的其它方面,以便在整体上提供更大的并发。MongoDB 2.6已经包含了部分改进(参见下文),MongoDB 2.8将带来更多。

希望不是营销人员的空头支票。访谈详细点这里

Embed 和 Array 不合理使用的问题不光是锁的问题,哪怕锁的问题解决了,这种情景下的如此使用的代价还是比较大的。

#17楼 @outman 非常棒的分享,哈哈,做一个主题贴发出来完全足够了。我呢主要谈了写操作引发的一些严重问题,关于读取方面,@outman 的分享很到位,这些都是需要注意的地方。

MongoDB 头上带了很多光环,也许它的商业宣传太成功了,以致让人误以为它就应该如此高性能了。但是实际深入使用过后,才好发现并非如此,它不是银弹,该怎么着还得怎么着,得摸清它的脾气,深入了解其中机制,了解很多最佳实践和注意事项,才可能调教好它。

真不错的文章,我们也一直在用mongodb,以后遇到此类坑一定跳过

我和 @hysios 现在在填ember的坑 😄 有机会也来分享分享

感谢分享

:plus1: 感谢分享

#17楼 @outman 继承的模型的查询问题,明明可以避免的啊,关键在于你为啥非要在modelB里开找呢,直接modelA.where(_type: 'modelB').where(oo: 'xx')不就成了,查出来的就是modelB的instance,这_type默认有索引的,不会有啥问题啊……

24楼 已删除

#24楼 @outman 可以在 _type 上建索引啊,而且会利用这个索引的。

不错,遇到过...

#17楼 @outman 事务又两种解决方案:

一种是把事务涉及到的模型都设计到一个文档中用原子操作搞定, 当然如果单文档会变得巨大就行不通了, 这时可以用另一种方法, 把事务涉及的主要对象 id 作为 key, 用分布式锁管理系统例如 redlock 锁定.

#25楼 @birdfrank #23楼 @aptx4869 是建了索引的,在_type上面,我说的是使用了 in 语句无法利用索引;不过我的这个观点估计也是错误的,因为我今天用explain分析后,即使继承后用 in 语句也可以hit索引,所以为什么查询比较慢,只能从其他地方找原因了。谢谢你们的提醒。

#27楼 @luikore 你说的第一种方法,用原子方法搞定,不知道如何具体操作,有实践方案么? 我用的估计是你说的第二种方案,用id作为key来管理事务对象,但是否能利用上你说的 redlock,这个我没有具体研究过。

mongoDB 对于单一collection的单次操作, 是原子而可靠的.

比方说某个场景是 如果该条数据不存在则新增,否则更新该数据, 那就使用update with upset = true, 而不是写两句命令. 比方说FindAndModify 则可以避免先查询再修改可能产生的脏数据问题.

MongoDB给出了建议的跨collection操作的transaction解决方案 (当然得自己实现), http://docs.mongodb.org/manual/tutorial/perform-two-phase-commits/

Operations on a single document are always atomic with MongoDB databases; however, operations that involve multiple documents, which are often referred to as “transactions,” are not atomic. Since documents can be fairly complex and contain multiple “nested” documents, single-document atomicity provides necessary support for many practical use cases.

#28楼 @outman

第一种应用常见情形, 是维护用户自身的数据一致性.

例如用户既有比特币又有狗币, 系统提供自由兑换的功能, 那么比特币账户和狗币账户都内嵌到用户模型中就好了. 然后对这两个账户的修改就是针对单 document 的原子操作.

当然还有很多事务都是典型的一个账户打钱给另一个账户, 或者顶贴积分-1, 这种内嵌做法就不适用了...

#30楼 @luikore 原来你说的时这种内嵌式的,但很多需要事务的场景,里面有大量model,他们是无法满足这种情况的。

#31楼 @outman 嗯, 除了 redlock 以外, 还有软件事务内存的做法, 如果跑在 maglev ruby 上, 就能用软件事务内存了并且可以保证多进程多机器事务一致性... 还有 100 核机器每秒万个事务之类的变态 benchmark, 不过貌似没见过人部署 maglev 的...

#33楼 @gene_wu 嘿嘿,什么时候有活动啊?

#34楼 @vincent 这次是年中和GDG合办,可能搞到EF高大上场地支持 💃

#35楼 @gene_wu 哈哈,可以啊,不过我更喜欢讲一个 Redis 相关的主题,也正打算写一篇 Redis 的文章。

#36楼 @vincent 好啊,这个好,上次@quakewang 也讲了很多Redis的做缓存和job

mongodb不支持多表join查询, 有什么方法可以补救吗?

#38楼 @gclsoft MongoDB 就是通过牺牲 join,牺牲 跨文档跨集合(跨行跨表)事务支持,才获得了高性能,自动 sharding 等优良特性,这就是代价吧。

在应用要实现 join 的数据需求,有几种解决方法:

  • 最基本的方法,在应用中通过多次的数据查询解决

  • 在 MongoDB 中使用 embed document 方式

  • 在 MongoDB 中建立冗余数据集,预先把数据 join 到一个集合中

  • 使用外部检索引擎,比如使用 Solr 或者 Elastic Search 解决

具体的解释和例子说明够再写一大遍,有兴趣的话,我再专门弄一个帖子展开。

@vincent 谢谢! 当然有兴趣啊. 我的解决方法就是第一种, 查了又查, 尤其是列表循环地再查, 让人越发怀念mysql的好处. embed document就是sub document吧, 如果embed document和别的集合join查询就不行: https://github.com/LearnBoost/mongoose/issues/2141 . 据说可以通过数据库的集合结构设计来解决, 但太难了, 设计地不好, 以后要查的时候又要痛苦啊, 不如mysql直接随便设计随便查容易.

#7楼 @vincent mongodb的坑的确很多,quora上的一个帖子,说了很多从mongodb迁移出去的公司例子:http://www.quora.com/MongoDB/Which-companies-have-moved-away-from-MongoDB-and-why

@vincent 另外其实background建索引是有代价的,一般会比前台的索引大不少,如果你有replica set的话,其实建议根据这个去操作比较好。在大的collection上,用background建的索引真的会大很多。

http://docs.mongodb.org/manual/tutorial/build-indexes-on-replica-sets/

#41楼 @benzheren 其实我也想迁移出去了,无奈的是“上了贼船”,下来就没有那么容易了 ...

#42楼 @benzheren 仔细读了 http://docs.mongodb.org/manual/tutorial/build-indexes-on-replica-sets/, 了解到为了避免建索引对 MongoDB 的冲击,还有这种方法:先把 secondary 停止重启进入 standalone 模式,然后再建立索引。建一个索引搞得如此麻烦,真是不爽。 background 建索引比前台建索引数据大很多 ,以前没有注意过,抽空用一个实例测试一下看看。

#40楼 @gclsoft 哈,能否把你遇到的实际问题提出来,看看什么方法更合适。

#45楼 @vincent https://github.com/LearnBoost/mongoose/issues/2141 抽象成一个简单的模型了. 这个实际问题: 一个所有用户投票结果的集合CSchema, 一个投票项目集合ASchema(每个投票选项是子文档BSchema), 现在要找某个用户的所有投票列表(找出用户名、用户投给的选项、投票项目的名字)

ASchema = new mongoose.Schema({
  name: String,
  B: [BSchema]
});

var BSchema = new Schema({
  name: String
});

var CSchema = new Schema({
  name: String,
  B: {
    type: Schema.ObjectId,
    ref: 'BSchema'
  }
});

范德萨萨达发 adhjahd

void test()
{
  sfjkds;
}

#47楼 @jasjia 这是啥么意思啊?

比如 安全的坑?

@huacnlee @Rei LS这图是被爆库了?

#51楼 @comensontin ruby china 没查到这几个用户名,是不是楼主部署了 ruby china 但是没有配置端口白名单?mongodb 官方包的配置是无任何认证(我也中招过)。

#50楼 @ihacku @huacnlee @Rei 什么情况?里面有我的 ID,我得赶紧改一下密码。

#53楼 @vincent 给服务器设上端口白名单。

# Firewall
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw logging on
sudo ufw enable

@Rei 哈,我根本没有部署 ruby china 的。一般外网的服务器上都有严格的防火墙设置,而且存储的服务器都不允许外网直接访问的。 @Rei 是广西老乡哦,我是广西南宁的,哈

#55楼 @vincent 42.121.111.183 这个 IP 是你的吗?

#56楼 @Rei 不是的,没有见过这个 IP 这个 IP 貌似 浙江省杭州市 阿里软件有限公司 的

#57楼 @vincent 给那个论坛的管理员发邮件了。

#58楼 @Rei 那个数据,看下来像是我们的

#58楼 @Rei 又好像不是,除了 @vincent 其他帐号在我们的库里面都没有。只是 @vincent 那个数据很奇怪,和我们的数据有些相似

#58楼 @Rei #59楼 @huacnlee 别吓我,难道 ruby china 曾经被拖过库? 貌似不太对,在4月30号的时候,我的 sign_in_count 应该远大于 10 ,而 topics_count 还没有到 17。

MongoDB 的安全性确实没有经过长时间考验,除了服务器、端口的安全,还有就是语句注入的问题,关系数据库的 SQL 注入曾经有血淋淋的经验教训,但是 MongoDB 安全这块才刚刚开始...

#61楼 @vincent writings.io 泄露过,但不清楚有没有被拖。

#61楼 @vincent Ubuntu 打的包已经关闭了外部访问,但是官方的包没有任何权限保护。与此类似的还有 elasticsearch,默认安装除了可以访问所有数据外,还可以执行脚本。

#60楼 @huacnlee 别人部署了 ruby-china。

#60楼 @huacnlee #64楼 @Rei 但愿是虚惊一场。安全是个大问题,不出问题没有任何感觉,出了问题通常就是致命的。

#65楼 @vincent MongoDB 确实默认就很不安全,允许远程ip连接,如果你部署时还用了个默认同样不安全的乌班图的话(默认没开防火墙,很多端口可以随便访问),那么被拖库不是分分钟的事情么?直接在控制台就可以连上去了。如果你购买的vps外部还提供防火墙服务,那就太好了,这些都不用担心。

数据库级锁 mysql也有該問題很煩人

#67楼 @Lucifer 嗯,不过目前 MySQL 的锁粒度有很多种,通常数据读写是用的是行级或表级,引起发锁库的操作还是比较少见的。

#17楼 @outman 表的概念在mongo中是collection吧?难道是我看官方文档的时候看错了?Document的概念是Collection的一条记录,难道不是这样么?为什么看你的回复感觉这些概念都混在一起了,完全是误导别人麻。

#69楼 @wudixiaotie 哦,我本来不应该提什么表得概念的,那个地方确实写的有误。如果误导了大家,确实非常抱歉。

我们是走mongoDB1.8开始使用mongoDB的,和lz一样见识到mongoDB的各种坑。

lz说的第一点其实还好,之前的全局锁才是坑爹。不过如果没有GIS方面的需求,建议可以试试TokuMX这个开源项目。

其实mongoDB的排序也很坑,在大量数据下没有索引就会报错。悲催的是我们项目中的排序字段都是动态的,不可能在每一列上都建上索引,这个太坑了。

还有mongoDB的GIS,简直就是一个忽悠人的东西,奇慢无比,虽然2.4版本做了一点改进。 我们公司都卖了mongoDB的服务了,但是他们提出的建议的确很面,感觉mongoDB的性能实在是太坑爹。目前还是不能用的感觉,至少需求复杂一点就不行。

关于建索引的坑,我们碰到过好几次,特别是我们的MongoDB(2.4.10)环境是Replica Set,尽管在Primary的节点上加background来建索引,同步到secondary的节点上时,依然是前台的操作,依然会锁住Replica Set。

这个问题在MongoDB 2.6 之后才修复,现在正在计划升级到MongoDB 2.6, 不过看到楼主的文章,还需要再考量一下。

还有个地方就是mongodb的类型,弱类型语言要注意ID确定一个类型,不然如果你的ID查询是数字,保存的是字符串就查不到了,比如php这类,一定要注意手动转换类型。

现在还用mongodb吗?

还有一个子系统在用。重构程序,迁移数据实在太麻烦了,现在的系统也还能工作,所以难以下定决心花很大成本重构,只能继续留着。好在把一两张关键表迁移到 MySQL 后,系统趋于稳定了。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册