MongoDB最佳实践
2012-05-09 14:02:21 阿炯

已经有很多关于 NoSQL 选择的文章了。影响你选择数据库的因素有:读/写操作的吞吐量,持久性,一致性,延迟性等等。Nathan Hurst 的文章“Visual Guide to NoSQL System”  很好的总结了这一点。

大部分的NoSQL产品,为追求性能,一致性等,一般只能支持简单的基于row-key的单条或者范围查询;但是MongoDB可以针对任意列的key创建索引,甚至是内嵌文档里的key,从支持的查询的灵活性上来看,更接近传统的关系数据库,同时还能在性能上向NoSQL看齐,加上支持复制,自动分片和Map/Reduce等功能,非常的吸引眼球,正在成为一款热门的海量存储产品。通过源代码编译安装好MongoDB之后,接下来需要配置运行。在其安装目录,有几个子目录,bin下面是可执行文件,包括:
* mongod:数据库服务端,类似mysqld,每个实例启动一个进程,可以fork为Daemon运行
* mongo:客户端命令行工具,类似sqlplus/mysql,其实也是一个js解释器,支持js语法
* mongodump/mongorestore:将数据导入为bson格式的文件/将bson文件恢复为数据库,类似xtracbackup
* mongoexport/mongoimport:将collection导出为json/csv格式数据/将数据导入数据库,类似mysqldump/mysqlimport
* bsondump:将bson格式的文件转储为json格式的数据
* mongos:分片路由,如果使用了sharding功能,则应用程序连接的是mongos而不是mongod
* mongofiles:GridFS管理工具
* mongostat:实时监控工具

最简单的,通过执行mongod即可以启动MongoDB数据库服务,mongod支持很多的参数,但都有默认值,其中最重要的是需要指定数据文件路径,或者确保默认的/data/db存在并且有访问权限,否则启动后会自动关闭服务。

mongod的主要参数有:
dbpath: 数据文件存放路径,每个数据库会在其中创建一个子目录。用于防止同一个实例多次运行的mongod.lock也保存在此目录中。
logpath:错误日志文件
logappend:错误日志采用追加模式(默认是覆写模式)
bind_ip:对外服务的绑定ip,一般设置为空,及绑定在本机所有可用ip上,如有需要可以单独指定
port:对外服务端口。Web管理端口在这个port的基础上+1000
fork:以后台Daemon形式运行服务
journal:开启日志功能,通过保存操作日志来降低单机故障的恢复时间,在1.8版本后正式加入,取代在1.7.5版本中的dur参数
syncdelay:执行sync的间隔,单位为秒
directoryperdb:每个db存放在单独的目录中,建议设置该参数
maxConns:最大连接数
repairpath:执行repair时的临时目录。在如果没有开启journal,异常宕机后重启,必须执行repair操作。

在源代码中,mongod的参数分为一般参数,windows参数,replication参数,replica set参数,以及隐含参数。上面列举的都是一般参数。如果要配置replication,replica set等,还需要设置对应的参数,这里先不展开,后续会有专门的文章来讲述。执行mongo –help可以看到对大多数参数的解释。但有一些隐含参数,则只能通过看代码来获得(见db.cpp po::options_description hidden_options(“Hidden options”);),隐含参数一般要么是还在开发中,要么是准备废弃,因此在生产环境中不建议使用。

可能你已经注意到,mongod的参数中,没有设置内存大小相关的参数,是的,mongodb使用os mmap机制来缓存数据文件数据,自身目前不提供缓存机制。这样好处是代码简单,mmap在数据量不超过内存时效率很高。但是数据量超过系统可用内存后,则写入的性能可能不太稳定,容易出现大起大落,不过在最新的1.8版本中,这个情况相对以前的版本已经有了一定程度的改善,具体请参考realzyy的测试。

这么多参数,全面写在命令行中则容易杂乱而不好管理。因此mongod也和mysqld一样支持将参数写入到一个配置文本文件中,然后通过config参数来引用此配置文件:
./mongod --config /etc/mongo.cnf

至此,已经成功的运行了一个单机的mongodb实例。


选择合适的NoSQL数据库并不是本文要讨论的内容,但是请你在使用NoSQL前做一些调查。没有一个数据库可以适合所有情况,这篇文章假设你选择了MongoDB。

NoSQL 通用的最佳实践

1. 彻底的测试

模拟你的生产环境,包括流量来进行测试。假如你的测试环境不能达到生产环境的压力,你将无法发现性能瓶颈和架构缺陷。

2. RDBMS 并不一定能迁移到 NoSQL

任何在RDBMS上工作的好好的东西并不一定能在MongoDB上工作。所以请你做好心理准备,仔细对比数据库的功能。为了更好的性能,你应该根据 10gen 的建议来设计你的文档和查询。你的应用也许需要重新架构以便于迁移到非关系型数据库。

3. 考虑你的数据的一致性和持久性需求

这一点很重要!MongoDB通过多实例备份来提解决数据持久性问题。我们不推荐你在生产环境中只使用一个MongoDB实例。你必须理解为什么要这么做。

MongoDB 最佳实践

1. 始终启用备份

备份能保证你应用的高可用性。假如你的一个节点down了,第二节点可以迅速启用,你的应用不会中断。

2. 使用最新版本

10gen在不断的发布更新,特别是2.0.x包含了很高的性能提升和并行改进,索引改进和bug修复。如果你还在使用 1.6.3的话,你应该尽快升级。

3. 不要在32位的系统上跑MongoDB

MongoDB在32位系统上有“2.5GB数据限制”。它的存储引擎使用内存映射来读取文件以获得更好的性能。这个功能依赖于内存寻址,而32位系统的内存不能超过4GB。

4. 默认开启日志

MongoDB支持数据库操作的提前日志(write-ahead journaling)。这个功能有助于灾难恢复。

5. 注意你数据文件的位置

你应该保证你的MongoDB的数据文件是存储在物理驱动器上,例如 /data/mongodb。当然你也可以使用虚拟的驱动器,但是必须非常小心。因为它有可能会影响到你的集群架构。我们建议你使用 Amazon EBS 来存放你的数据库文件。

6. 保证足够大的内存

为了保证整个集群的性能,你要确保整个所有MongoDB的工作实例(working set)包括索引可以完全装入内存。如果你发现“page faults”的概率在增加,很有可能mongoDB的数据量超出了你的内存。在这种情况下你有两种选择:加内存,或者创建分片集群(Sharding)。我们建议你先考虑加内存。

7. 保持 65% 以内的压力

如果你发现你的集群压力达到了65%,那么你应该考虑扩大你的集群了。通常,你应该保证数据库压力低于65%。

8. 特别小心分片集群

分片集群需要你充分理解你应用的数据访问方式。你应该充分了解MongoDB的分片工作方式,并且确认你确实需要这个功能。还有,选择一个分片钥匙(sharding key)是对于性能也是很重要的。

配置服务器对于一个集群的健康也是很重要的。在分片集群的环境中,你必须有三台配置服务器。永远不要删除配置服务器的数据,时常备份这些数据。这些配置服务器也需要64位的环境。还有,不要把三台配置服务器放在同一台机器上!

9. 使用 Mongo MMS 来图形化的监控你的数据库

如果你还没有使用 Mongo MMS的话,我强烈推荐这个工具。10gen 正在大力开发这个产品。它提供了一个非常友好的可视化的界面来监控你的MongoDB集群。


当然,其也有一些缺点,主要表现在以下方面:

MongoDB 当前的内存模型基于内存映射文件,这是一项已经宣布脑死亡的技术。在实际应用过程中,不具备伸缩性,没有方法来控制内存的使用情况。

锁机制: 一个可伸缩性的数据库解决方案使用全局的服务器锁是一个糟糕的设计,特别是因为当 MongoDB 支持原子操作。应该有更精细的锁操作。

查询引擎:目前 MongoDB 的每个查询只允许使用一个索引,不知道为什么会有这样的限制,完全没有理由。其实 MongoDB 的索引模型和关系数据库是差不多的。

查询语言:使用 JSON 作为查询语言是一个糟糕的决定,尽管当前 JSON 查询语言支持标准查询,但对一些操作确实有限制,无法在 JSON 中执行一些类似 SQL 的复杂查询。

Map-Reduce: MongoDB 的 Map-reduce 相似一个无用的赠送品。

数据分片:这是 MongoDB 的另外一个糟糕的功能,从一个单一的服务器到分区设置的步骤是非常巨大的,你需要最少两个复制集才能做分片,三个配置服务器和负载均衡,有点像小镇上的小房子旁建了一栋摩天大厦。

数据中心的意识:这是另外一个拼凑在一起的特性,复制集只支持一个主节点和多个从节点,只能去写一个从节点。可以在跨多个数据中心运行复制集,但写操作只能在一个数据中心的从节点。

默认关闭“安全”模式:是谁做出这样白痴的决定呢?看到很到报道称数据丢失,多数是因为这个问题。

日志:MongoDB 预先分配了 3G 的数据用于日志记录,这个数据是独立于数据库大小的,3G大小对一些小型系统来说简直是疯了。


使用入门

插入数据到集合

下面来建立一个test的集合并写入一些数据. 建立两个对象, j 和 t , 并保存到集合中去. 在例子里 ‘>’ 来表示是 shell 输入提示符

> j = { name : "mongo" };
    {"name" : "mongo"}
> t = { x : 3 };
    { "x" : 3  }
> db.things.save(j);
> db.things.save(t);
> db.things.find();
    {"name" : "mongo" , "_id" : ObjectId("497cf60751712cf7758fbdbb")}
    {"x" : 3 , "_id" : ObjectId("497cf61651712cf7758fbdbc")}
>

有几点需要注意下 :
* 不需要预先建立一个集合. 在第一次插入数据时候会自动建立.
* 在例子其实可以存储任何结构的数据, 当然在实际应用我们存储的还是相同元素的集合. 这个特性其实可以在应用里很灵活, 你不需要类似 alter table 来修改你的数据结构
* 每次插入数据时候对象都会有一个ID, 名字叫 _id.
* 当你运行不同的例子, 你的对象ID值都是不同的.

下面再加点数据:
> for( var i = 1; i < 10; i++ ) db.things.save( { x:4, j:i } ); > db.things.find();
    {"name" : "mongo" , "_id" : ObjectId("497cf60751712cf7758fbdbb")}
    {"x" : 3 , "_id" : ObjectId("497cf61651712cf7758fbdbc")}
    {"x" : 4 , "j" : 1 , "_id" : ObjectId("497cf87151712cf7758fbdbd")}
    {"x" : 4 , "j" : 2 , "_id" : ObjectId("497cf87151712cf7758fbdbe")}
    {"x" : 4 , "j" : 3 , "_id" : ObjectId("497cf87151712cf7758fbdbf")}
    {"x" : 4 , "j" : 4 , "_id" : ObjectId("497cf87151712cf7758fbdc0")}
    {"x" : 4 , "j" : 5 , "_id" : ObjectId("497cf87151712cf7758fbdc1")}
    {"x" : 4 , "j" : 6 , "_id" : ObjectId("497cf87151712cf7758fbdc2")}
    {"x" : 4 , "j" : 7 , "_id" : ObjectId("497cf87151712cf7758fbdc3")}
    {"x" : 4 , "j" : 8 , "_id" : ObjectId("497cf87151712cf7758fbdc4")}

请注意下, 这里循环次数是10, 但是只显示到8, 还有2条数据没有显示. 如果想继续查询下面的数据只需要使用 it 命令, 就会继续下面的数据:
> it
    {"x" : 4 , "j" : 9 , "_id" : ObjectId("497cf87151712cf7758fbdc5")}
    {"x" : 4 , "j" : 10 , "_id" : ObjectId("497cf87151712cf7758fbdc6")}

从技术上讲 find() 返回一个游标对象. 但在上面的例子里, 并没有拿到一个游标的变量. 所以 shell 自动遍历游标, 返回一个初始化的set, 并允许我们继续用 it 迭代输出.
当然我们也可以直接用游标来输出, 不过这个是下一部分的内容了.

查询数据

在没有深入查询之前, 我们先看看怎么从一个查询中返回一个游标对象. 可以简单的通过 find() 来查询, 他返回一个任意结构的集合. 如果实现特定的查询稍后讲解.
实现上面同样的查询, 然后通过 while 来输出:
> var cursor = db.things.find();
> while (cursor.hasNext()) { print(tojson(cursor.next())); }
    {"name" : "mongo" , "_id" : ObjectId("497cf60751712cf7758fbdbb")}
    {"x" : 3 , "_id" : ObjectId("497cf61651712cf7758fbdbc")}
    {"x" : 4 , "j" : 1 , "_id" : ObjectId("497cf87151712cf7758fbdbd")}
    {"x" : 4 , "j" : 2 , "_id" : ObjectId("497cf87151712cf7758fbdbe")}
    {"x" : 4 , "j" : 3 , "_id" : ObjectId("497cf87151712cf7758fbdbf")}
    {"x" : 4 , "j" : 4 , "_id" : ObjectId("497cf87151712cf7758fbdc0")}
    {"x" : 4 , "j" : 5 , "_id" : ObjectId("497cf87151712cf7758fbdc1")}
    {"x" : 4 , "j" : 6 , "_id" : ObjectId("497cf87151712cf7758fbdc2")}
    {"x" : 4 , "j" : 7 , "_id" : ObjectId("497cf87151712cf7758fbdc3")}
    {"x" : 4 , "j" : 8 , "_id" : ObjectId("497cf87151712cf7758fbdc4")}
    {"x" : 4 , "j" : 9 , "_id" : ObjectId("497cf87151712cf7758fbdc5")}
>

上面的例子显示了游标风格的迭代输出. hasNext() 函数告诉我们是否还有数据, 如果有则可以调用 next() 函数. 这里我们也用了自带的 tojson() 方法返回一个标准的 JSON 格式数据.当我们使用的是 JavaScript shell, 可以用到JS的特性, forEach 就可以输出游标了. 下面的例子就是使用 forEach() 来循环输出:
> db.things.find().forEach( function(x) { print(tojson(x));});
    {"name" : "mongo" , "_id" : ObjectId("497cf60751712cf7758fbdbb")}
    {"x" : 3 , "_id" : ObjectId("497cf61651712cf7758fbdbc")}
    {"x" : 4 , "j" : 1 , "_id" : ObjectId("497cf87151712cf7758fbdbd")}
    {"x" : 4 , "j" : 2 , "_id" : ObjectId("497cf87151712cf7758fbdbe")}
    {"x" : 4 , "j" : 3 , "_id" : ObjectId("497cf87151712cf7758fbdbf")}
    {"x" : 4 , "j" : 4 , "_id" : ObjectId("497cf87151712cf7758fbdc0")}
    {"x" : 4 , "j" : 5 , "_id" : ObjectId("497cf87151712cf7758fbdc1")}
    {"x" : 4 , "j" : 6 , "_id" : ObjectId("497cf87151712cf7758fbdc2")}
    {"x" : 4 , "j" : 7 , "_id" : ObjectId("497cf87151712cf7758fbdc3")}
    {"x" : 4 , "j" : 8 , "_id" : ObjectId("497cf87151712cf7758fbdc4")}
    {"x" : 4 , "j" : 9 , "_id" : ObjectId("497cf87151712cf7758fbdc5")}
>

forEach() 必须定义一个函数供每个游标元素调用. 在 mongo shell 里, 我们也可以把游标当作数组来用 :
> var cursor = db.things.find();
> print (tojson(cursor[4]));
    {"x" : 4 , "j" : 3 , "_id" : ObjectId("497cf87151712cf7758fbdbf")}

使用游标时候请注意占用内存的问题, 特别是很大的游标对象, 有可能会内存溢出. 所以应该用迭代的方式来输出.下面的示例则是把游标转换成真实的数组类型:
> var arr = db.things.find().toArray();
> arr[5];
    {"x" : 4 , "j" : 4 , "_id" : ObjectId("497cf87151712cf7758fbdc0")}

请注意这些特性只是在 mongo shell 里使用, 而不是所有的其他应用程序驱动都支持. MongoDB 游标对象不是没有快照 – 如果有其他用户在集合里第一次或者最后一次调用 next(), 你可以得不到游标里的数据. 所以要明确的锁定你要查询的游标.

指定条件的查询

到这里我们已经知道怎么从游标里实现一个查询并返回数据对象, 下面就来看看怎么根据指定的条件来查询.下面的示例就是说明如何执行一个类似SQL的查询, 并演示了怎么在 MongoDB 里实现. 这是在 MongoDB shell 里查询, 当然你也可以用其他的应用驱动或者语言来实现:
SELECT * FROM things WHERE name="mongo"

> db.things.find({name:"mongo"}).forEach(function(x) { print(tojson(x));});
    {"name" : "mongo" , "_id" : ObjectId("497cf60751712cf7758fbdbb")}
>
    SELECT * FROM things WHERE x=4
> db.things.find({x:4}).forEach(function(x) { print(tojson(x));});
    {"x" : 4 , "j" : 1 , "_id" : ObjectId("497cf87151712cf7758fbdbd")}
    {"x" : 4 , "j" : 2 , "_id" : ObjectId("497cf87151712cf7758fbdbe")}
    {"x" : 4 , "j" : 3 , "_id" : ObjectId("497cf87151712cf7758fbdbf")}
    {"x" : 4 , "j" : 4 , "_id" : ObjectId("497cf87151712cf7758fbdc0")}
    {"x" : 4 , "j" : 5 , "_id" : ObjectId("497cf87151712cf7758fbdc1")}
    {"x" : 4 , "j" : 6 , "_id" : ObjectId("497cf87151712cf7758fbdc2")}
    {"x" : 4 , "j" : 7 , "_id" : ObjectId("497cf87151712cf7758fbdc3")}
    {"x" : 4 , "j" : 8 , "_id" : ObjectId("497cf87151712cf7758fbdc4")}
    {"x" : 4 , "j" : 9 , "_id" : ObjectId("497cf87151712cf7758fbdc5")}
>

查询条件是 { a:A, b:B, … } 类似 “where a==A and b==B and …”, 更多的查询方式可以参考 Mongo 开发教程部分. 上面显示的是所有的元素, 当然我们也可以返回特定的元素, 类似于返回表里某字段的值, 只需要在 find({x:4}) 里指定元素的名字, 比如 j:
SELECT j FROM things WHERE x=4

> db.things.find({x:4}, {j:true}).forEach(function(x) { print(tojson(x));});
    {"j" : 1 , "_id" : ObjectId("497cf87151712cf7758fbdbd")}
    {"j" : 2 , "_id" : ObjectId("497cf87151712cf7758fbdbe")}
    {"j" : 3 , "_id" : ObjectId("497cf87151712cf7758fbdbf")}
    {"j" : 4 , "_id" : ObjectId("497cf87151712cf7758fbdc0")}
    {"j" : 5 , "_id" : ObjectId("497cf87151712cf7758fbdc1")}
    {"j" : 6 , "_id" : ObjectId("497cf87151712cf7758fbdc2")}
    {"j" : 7 , "_id" : ObjectId("497cf87151712cf7758fbdc3")}
    {"j" : 8 , "_id" : ObjectId("497cf87151712cf7758fbdc4")}
    {"j" : 9 , "_id" : ObjectId("497cf87151712cf7758fbdc5")}
>

请注意 “_id” 元素会一直被返回.

findOne() – 语法糖

为了方便, mongo shell (其他驱动) 避免游标的可能带来的开销, 提供一个findOne() 函数. 这个函数和 find() 参数一样, 不过他返回游标里第一条数据, 或者返回 null 空数据库.
作为一个例子, name==’mongo’ 可以用很多方法来实现, 可以用 next() 来循环游标(需要校验是否为null), 或者当做数组返回第一个元素.
但是用 findOne() 方法则更简单和高效:
> var mongo = db.things.findOne({name:"mongo"});
> print(tojson(mongo));
    {"name" : "mongo" , "_id" : ObjectId("497cf60751712cf7758fbdbb")}
>

findOne 方法更跟 find({name:”mongo”}).limit(1) 一样.

limit() 查询

你可以需要限制结果集的长度, 可以调用 limit 方法.这是强烈推荐高性能的原因, 通过限制条数来减少网络传输, 例如:
> db.things.find().limit(3);
    in cursor for : DBQuery: example.things ->
    {"name" : "mongo" , "_id" : ObjectId("497cf60751712cf7758fbdbb")}
    {"x" : 3 , "_id" : ObjectId("497cf61651712cf7758fbdbc")}
    {"x" : 4 , "j" : 1 , "_id" : ObjectId("497cf87151712cf7758fbdbd")}
>

更多帮助

除非了一般的 help 之外, 你还可以查询 help 数据库和db.whatever 来查询具体的说明. 如果你对一个函数要做什么, 你可以不输入 {{()}} 这些结束的括号则可以输出实现的源码, 例如:
> db.foo.insert
    function (obj, _allow_dot) {
    if (!obj) {
    throw "no object passed to insert!";
    }
    if (!_allow_dot) {
    this._validateForStorage(obj);
    }
    return this._mongo.insert(this._fullName, obj);
    }

mongo 是一个完整的 JavaScript shell程序, 所以在 shell 里完全可以私用JS的方法、类、语法. 此外, MongoDB 定义很多自己的类和全局变量 (比如 db). 这里可以查看完整的API说明。http://api.mongodb.org/js/.

接下来

看完这篇教程后下一步则看MongoDB更详细的文档

打开mongodb客户端
/usr/local/mongodb/bin/mongo

以下为客户端常用命令
show dbs #查看所有数据库
db.getName() #查看当前操作的数据库

User:
db.addUser(username, password) #添加用户
db.removeUser(username) #删除用户

Database:
db.dropDatabase() #修复数据库
db.repairDatabase() #删除当前操作数据库

Collection:
db.createCollection(name, { size : …, capped : …, max : … }) #创建数据集合(表)
db.printCollectionStats() #查看数据库状态信息

Profile:
db.getProfilingLevel()
db.setProfilingLevel(level) #0=off 1=slow 2=all

Server:
db.version() #当前数据库版本
db.shutdownServer() #关闭服务端

安装PHP扩展
/usr/local/php/bin/pecl install mongo

看到下面提示:
Installing ‘/usr/local/php/lib/php/extensions/no-debug-non-zts-20090626/mongo.so’
configuration option “php_ini” is not set to php.ini location
You should add “extension=mongo.so” to php.ini
修改 php 配置文件
修改extension_dir值为以上:/usr/local/php/lib/php/extensions/no-debug-non-zts-20090626/
添加extension=mongo.so行

重启apache:
/usr/local/apache/bin/apachectl restart
查看phpinfo()可以看到mongo项说明安装成功

从 SQL 移植到 MongoDB
下图是使用 SQL 和 MongoDB 的对应信息图表: