数据库 MongoDB 那些坑

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

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/

太好了,谢谢分享

#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 后,系统趋于稳定了。

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