MongoDB 是目前炙手可热的 NoSQL 文档型数据库,它提供的一些特性很棒:如自动 failover 机制,自动 sharding,无模式 schemaless,大部分情况下性能也很棒。但是薄荷在深入使用 MongoDB 过程中,遇到了不少问题,下面总结几个我们遇到的坑。特别申明:我们目前用的 MongoDB 版本是 2.4.10,曾经升级到 MongoDB 2.6.0 版本,问题依然存在,又回退到 2.4.10 版本。
坑爹指数:5 星(最高 5 星)
MongoDB 的锁机制和一般关系数据库如 MySQL(InnoDB), Oracle 有很大的差异,InnoDB 和 Oracle 能提供行级粒度锁,而 MongoDB 只能提供 库级粒度锁,这意味着当 MongoDB 一个写锁处于占用状态时,其它的读写操作都得干等。
初看起来库级锁在大并发环境下有严重的问题,但是 MongoDB 依然能够保持大并发量和高性能,这是因为 MongoDB 的锁粒度虽然很粗放,但是在锁处理机制和关系数据库锁有很大差异,主要表现在:
通常不出问题不等于没有问题,如果数据操作不当,依然会导致长时间占用写锁,比如下面提到的前台建索引操作,当出现这种情况的时候,整个数据库就处于完全阻塞状态,无法进行任何读写操作,情况十分严重。
解决问题的方法,尽量避免长时间占用写锁操作,如果有一些集合操作实在难以避免,可以考虑把这个集合放到一个单独的 MongoDB 库里,因为 MongoDB 不同库锁是相互隔离的,分离集合可以避免某一个集合操作引发全局阻塞问题。
坑爹指数:3 星
上面提到了 MongoDB 库级锁的问题,建索引就是一个容易引起长时间写锁的问题,MongoDB 在前台建索引时需要占用一个写锁(而且不会临时放弃),如果集合的数据量很大,建索引通常要花比较长时间,特别容易引起问题。
解决的方法很简单,MongoDB 提供了两种建索引的访问,一种是 background 方式,不需要长时间占用写锁,另一种是非 background 方式,需要长时间占用锁。使用 background 方式就可以解决问题。 例如,为超大表 posts 建立索引, 千万不用使用
db.posts.ensureIndex({user_id: 1})
而应该使用
db.posts.ensureIndex({user_id: 1}, {background: 1})
坑爹指数: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 关联,不然会死的很惨。
坑爹指数: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 过程一定要多加注意,避免掉到坑里。
参考资料:
开始还以为楼主是医生同学,暗自感叹 8 个月就这么厉害。后来细看不是。虽然楼主和医生同学仍旧很厉害。
想请教楼主,为什么你选型会选 MongoDB? MongoDB 有什么 PostgreSQL 不能做到的吗?
谢谢薄荷折腾出这么多经验,相信你们的程序员也在折腾中成功瘦身。
我还是先用着 mysql 吧,至少 Mysql 撅屁股我就知道它拉什么屎,但 MongoDB 还是女人心海底针的阶段。
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 的 集群解决方案,不过版本有点跟不上
#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 不合理使用的问题不光是锁的问题,哪怕锁的问题解决了,这种情景下的如此使用的代价还是比较大的。
我们项目组,使用 mongodb 已经 2 年多了,目前的版本是:2.2.1。针对使用中遇到的问题,谈点我自己的感受吧,既然这个帖子基调已定:),所以好处就不用废话了,就谈点注意事项而已。
为了在开发环境下统计耗时的查询,我 monkey 了 Moped 的代码,这样可以很容易的查看哪些比较耗时的查询。代码如下:
# encoding: utf-8
module Moped
# Represents a client to a node in a server cluster.
#
# @api private
class Node
if Rails.env == 'development'
@@time_consuming_msg = []
alias_method :log_operations_old, :log_operations
def log_operations(logger, ops, duration_ms)
log_operations_old(logger,ops,duration_ms)
time = duration_ms.round(2)
if time > 30.0 # > 50ms
#p '='*100
t_s = "%.4fms" % duration_ms
@@time_consuming_msg << {msg: "#{ops.first.log_inspect} (#{t_s})", time: time}
end
end
def self.print_consuming_time_stat_info(logger)
if @@time_consuming_msg.size > 0
log = MongoidColoredLogger::LoggerDecorator.new(logger)
log.warn "\n"
log.warn "\e[33m#{'*'*100}\e[0m"
log.warn "\e[31m#{' '*32}Query consuming time -- Top ranking list\e[0m\n"
@@time_consuming_msg.sort{|x,y| x[:time] <=> y[:time] }.each do |msg|
log.warn msg[:msg]
end
log.warn "\e[33m#{'*'*100}\e[0m\n"
@@time_consuming_msg = []
end
end
end
end
end
以上如果有问题的地方,还请多多指教。
一种是把事务涉及到的模型都设计到一个文档中用原子操作搞定,当然如果单文档会变得巨大就行不通了,这时可以用另一种方法,把事务涉及的主要对象 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.
第一种应用常见情形,是维护用户自身的数据一致性。
例如用户既有比特币又有狗币,系统提供自由兑换的功能,那么比特币账户和狗币账户都内嵌到用户模型中就好了。然后对这两个账户的修改就是针对单 document 的原子操作。
当然还有很多事务都是典型的一个账户打钱给另一个账户,或者顶贴积分 -1, 这种内嵌做法就不适用了...
@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/
#42 楼 @benzheren 仔细读了 http://docs.mongodb.org/manual/tutorial/build-indexes-on-replica-sets/,了解到为了避免建索引对 MongoDB 的冲击,还有这种方法:先把 secondary 停止重启进入 standalone 模式,然后再建立索引。建一个索引搞得如此麻烦,真是不爽。 background 建索引比前台建索引数据大很多 ,以前没有注意过,抽空用一个实例测试一下看看。
#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'
}
});
#43 楼 @vincent 没事,看到很多公司都有迁移出 mongodb 的经历,http://www.quora.com/MongoDB/Which-companies-have-moved-away-from-MongoDB-and-why
#51 楼 @comensontin ruby china 没查到这几个用户名,是不是楼主部署了 ruby china 但是没有配置端口白名单?mongodb 官方包的配置是无任何认证(我也中招过)。
# Firewall
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw logging on
sudo ufw enable
我们是走 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 这类,一定要注意手动转换类型。
还有一个子系统在用。重构程序,迁移数据实在太麻烦了,现在的系统也还能工作,所以难以下定决心花很大成本重构,只能继续留着。好在把一两张关键表迁移到 MySQL 后,系统趋于稳定了。