Mongo应用管理之数据管理
本章介绍如何管理集合与数据库。通常来讲,这部分内容并非每天都能用到,但对于应用性能却无比重要,具体包括了 :
-
- 配置用户账户和身份验证;
- 在正在运行的系统中建立索引;
- 对新服务器进行“预热”,以便快速上线;
- 整理数据文件中的碎片;
- 手动预分配新的数据文件。
1.配置身份验证
作为系统管理员,首要任务之一就是确保系统安全。确保MongoDB安全的最好办法,即在一个可信环境中运行,确保只有可信的机器能够连接到服务器。也就是说,即使是在以任务为颗粒的粗粒度(coarse-grained)访问方式中,MongoDB也支持针对单个连接进行身份验证。
提示:可登陆MongoDB企业版(http://bit.ly/15nFgI3)査看更多复杂的安全特性。在http://docs.mongodb.org/manual/security中可找到最新的认证和授权信息。
1.1 身份验证基本原理
MongoDB中,每个数据库的实例都可拥有任意多个用户。安全检查开启后,只有通过身份验证的用户才能够进行数据的读写操作。
admin (管理员)和local (本地)是两个特殊的数据库,它们当中的用户可对任何数据库进行操作。这两个数据库中的用户可被看作是超级用户。经认证后,管理员用户可对任何数据库进行读写,同时能执行某些只有管理员才能执行的命令,如listDatabases 和 shutdown。
已开启安全检查的数据库在被启动前,应至少添加一个管理员用户。我们来看一个小例子,假设在没有开启安全检查的前提下,已在shell中连接到了服务器:
> use admin switched to db admin > db.addUser("root", "abcd"); { "user" : "root", "readOnly" : false, "pwd" : "1a0f1c3c3aa1d592f490a2addc559383" } > use test switched to db test > db.addUser("test_user", "efgh"); { "user" : "test_user", "readOnly" : false, "pwd" : "6076b96fc3fe6002c810268702646eec" } > db.addUser("read_user", "ijkl", true); { "user" : "read_user", "readOnly" : true, "pwd" : "f497e180c9dc0655292fee5893c162f1" }
在以上操作中,我们增加了一名管理员用户root,又在名为test的数据库中增加了两个用户。其中名为read_user的用户只有读权限而没有写权限。在shell中用addUser创建用户时,将第三个参数readonly设置为true,即可创建一个只读权限用户。运行addUser时,必须拥有相应数据库的写入权限。这个例子中由于我们还没有启用安全检査,因此可在任一数据库上运行addUser。
提示:除添加新用户外,addUser命令还可用来更改用户密码或只读权限状态。只需在运行addUser时,将用户名和新密码或只读权限设置作为参数即可。
现在重启服务器,这次在命令行选项中加上--auth参数,以启用安全检査。启用后,在shell中重新连接并尝试以下操作:
> use test switched to db test > db.test.find(); error: { "$err" : "unauthorized for db [test] lock type: -1 " } > db.auth("read_user", "ijkl"); 1 > db.test.find(); { "_id" : ObjectId("4bb007f53e8424663ea6848a"), "x" : 1 } > db.test.insert({"x" : 2}); unauthorized > db.auth("test_user", "efgh"); 1 > db.test.insert({"x": 2}); > db.test.find(); { "_id" : ObjectId("4bb007f53e8424663ea6848a"), "x" : 1 } { "_id" : ObjectId("4bb0088cbe17157d7b9cac07"), "x" : 2 } > show dbs assert: assert failed : listDatabases failed:{ "assertion" : "unauthorized for db [admin] lock type: 1 ", "errmsg" : "db assertion failure", "ok" : 0 } > use admin switched to db admin > db.auth("root", "abcd"); 1 > show dbs admin local test
在建立连接之初,无法在名为test的数据库中进行任何读写操作。以用户read_user的身份通过验证后,可运行简单的find指令。尝试写入数据时,却因没有权限而再次操作失败。用户test_user在创建时并没有被设定为只读用户,因此可正常进行读写操作。但用户test_user并非管理员用户,因此不能通过执行show dbs指令来列出所有数据库。以上操作中的最后一步是管理员用户root的身份验证,root可对任一数据库进行操作。
1.2 配置身份验证
启用身份验证后,客户端必须登录才能进行读写。然而,在MongoDB中有一点值得注意:在admin数据库中建立用户前,服务器上的”本地“客户端可对数据库进行读写。
一般情况下这不是问题,正常新建管理员用户并进行身份验证即可。唯一的例外情况则与分片有关。分片时,数据库admin会被保存在配置服务器(config server)上,所以分片中的mongod甚至并不知道它的存在。因此,在它们看来,它们虽然开启了身份验证但却不存在管理员用户。于是,分片中会允许一个本地的(local)客户端无需身份验证便可读写数据。
希望这不会成为一个问题,将网络配置为只允许客户端访问mongos进程即可。不过,如担心客户端在分片的本地上运行,不通过mongos进程而直接连接到分片的话,可在分片中添加管理员用户。
注意:我们并不想让分片集群知道这些管理员用户的存在,因为已经存在了一个admin数据库。在分片上建立的admin数据库仅供我们使用。要进行这一操作,可连接到每个分片的主节点,然后运行addUser()函数:
> db.addUser("someUser", "theirPassword")
应保证新建用户的副本集是作为集群中的分片存在的。如果新建了管理员用户,并尝试使用addShard命令将mongod作为分片加入集群,会发现这一命令无法执行(因为集群中已经存在了名为admin的数据库)。
1.3 身份验证的工作原理
数据库中的用户是作为文档被储存在其syste.users集合中的。这种用以保存用户信息的文档结构是{user : username,readOnly : true, pwd : password hash}。其中 password hash 是基于 username 和密码生成的散列值。
了解了用户身份信息的存储位置与方法后,可方便地对其进行管理。例如,要删除一个用户,只需从system.users集合中删除这一用户的文档即可。
> db.auth("test_user", "efgh"); 1 > db.system.users.remove({"user" : "test_user"}); > db.auth("test_user", "efgh"); 0
用户进行身份验证时,服务器可通过绑定执行authenticate命令的连接,跟踪身份验证。这意味着只要驱动程序或其他工具使用了连接池或遇到故障而切换到另一节点,已经过身份验证的用户也需在新的连接上重新进行认证。这一操作应由驱动程序在后台进行处理。
2.建立和删除索引
其它文章介绍过用于建立索引的命令,但没有深入介绍这些命令的运行过程。建立索引是数据库最耗费资源的操作之一,所以应小心地安排建立索引。
建立索引需MongoDB査找集合中每一个文档内被索引的字段(或正要建立索引的字段),然后对查找到的值进行排序。不出所料,随着集合体积的增长,该操作消耗非常大。因此,建立索引时,应使用对生产服务器影响最小的方式。
2.1 在独立的服务器上建立索引
在独立的服务器上,可在空闲时间于后台建立索引。除此之外,没有什么更好的办法来减轻建立索引所需的性能开销。在后台建立索引,可利用background:true参数运行ensureIndex命令:
> db.foo.ensureIndex({"someField" : 1}, {"background" : true})
任何类型的索引均可在后台完成建立。
在前台建立索引要比在后台建立索引耗时少,但在索引建立期间会锁定数据库,从而导致其他操作无法进行数据读写。而后台在建立索引期间,则会定期释放写锁,从而保证其他操作的运行。这意味着后台建立索引耗时更长,尤其是在频繁进行写入的服务器上。但后台服务器在建立索引期间,可继续为其他客户端提供服务。
2.2 在副本集上建立索引
在副本集上建立索引最简单的方式,即在主节点中建立索引,然后等待其被复制到其他备份节点。在较小的集合中,这一操作不会造成太大的影响。
如集合较大,则会出现所有备份节点同时开始建立索引的情况。突然间所有备份节点都无法被客户端读取了,同时可能也无法及时进行同步复制。因此,对于较大的集合,推荐采用的方式是:
(1) 关闭一个备份节点;
(2) 将其作为独立的节点启动;
(3) 在这一服务器上建立索引;
(4) 重新将其作为成员加入副本集;
(5) 对每个备份节点执行同样的操作。
完成以上操作后,只剩主节点还没有建立索引。现在有两种选择。
-
- 于后台在主节点中建立索引(这会对主节点的性能造成压力);
- 关闭主节点,并执行以上步骤(1)~(4),像在备份节点中一样,在主节点上建立索 弓I。该方式需数据库停运一次,应权衡利弊进行选择。
也可以使用这种隔离创建技术,在没有被配置为建立索引的副本集内的成员上建立索引,即使用了buildlndexes: false选项。方法是将其作为独立服务器启动,建立索引,并重新加入副本集。
如果由于某种原因无法使用以上方法,则需计划在空闲时间(晚上、假期、周末等)来建立新的索引。
2.3 在分片集群上建立索引
在分片集群上建立索引,与在副本集中建立索引的步骤相同,不过需要在每个分片上分别建立一次。
首先,关闭均衡器。然后按照上一节中的步骤,依次在毎一个分片中进行操作,即把每个分片当作一个单独的副本集。最后,通过mongos运行ensurelndex,并重新启动均衡器。
只有在现存分片中添加索引时才需这样做,新的分片会在开始接收集合数据块时抓取集合中的索引。
2.4 删除索引
如不再需要某索引,可使用dropIndexes命令并指定索引名来删除索引。查询 system.indexes集合找出索引名,即使是自动生成的索引名,在不同驱动器间也会存在些许差异:
> db.runCommand({"dropIndexes" : "foo", "index" : "alphabet"})
只需将作为index的值,即可删除一个集合中的所有索引:
> db.runCommand({"dropIndexes" : "foo", "index" : "*"})
但这种方法无法删除_id索引。只有删除整个集合才能删除掉该索引。删除集合中的全部文档不会对索引产生影响,新文档插入后索引仍可正常增加。
2.5 注意内存溢出杀手
Linux的内存溢出杀手(OOM Killer, out-of-memory killer)负责终止使用过多内存的进程。考虑到MongoDB使用内存的方式,除了在建立索引的情况下,它通常不会遇到这种冋题。如在建立索引时,mongod突然消失,请检查/var/log/messages文件,其中记录了OOM Killer的输出信息。在后台建立索引或增加交换(swap)空间可避免此类情况。如拥有机器的管理员权限,可将MongoDB设置为不可被OOM Killer终止。
3.预热数据
重启机器或启动一台新的服务器,会耗费一段时间供MongoDB将所有所需数据从磁盘中载入内存。如对于性能的需求很高,要求数据必须出自内存中,则将新服务器上线,并等待应用程序载入所有所需数据,这会是一项艰巨的工作。
有几种方式可在服务器正式上线之前将数据载入内存,以避免在应用运行时带来麻烦。
提示:重启MongoDB会改变内存中的内容。内存是由操作系统进行管理的,而操作系统不会将数据清除出内存,除非有其他程序需要使用此段内存空间。因此,如果mongod进程需要重启,应不会影响内存中的数据。(然而,mongod会报告较低的常驻内存值,直到它有机会向操作系统请求所需的数据。)
3.1 将数据库移至内存
如需将数据库移至内存中,可使用UNIX中的dd工具,从而在启动mongod前载入 数据文件:
$ for file in /data/db/brains.* > do > dd if=$file of=/dev/null > done
将brains替换为需载入的数据库名。
将/data/db/brains.*替换为/data/db/*可将整个数据目录(即所有数据库)载入内存(假设内存容量足够的话)。如将一个或一组数据库载入内存,需占用的内存又要比实际内存大的话,则其中一些数据会立即被清除出内存。在这种情况下,可使用下一节中讲到的几种方法,以而将特定的数据载入内存中。
mongod启动时,会向操作系统请求数据文件。如果操作系统发现内存中已经存在了这些数据文件,就可以立即访问此mongod。
然而,只有在整个数据库可以装入内存中时,这一技术才能发挥作用。否则,可使用以下介绍的技术,来进行更多细粒度的(fine-grained)预热。
3.2 将集合移至内存
MongoDB提供了touch命令来预热数据。启动mongod (也许在另一个端口上,或关闭防火墙对它的限制),对一个集合使用touch命令,从而将其载入内存:
> db.runCommand({"touch" : "logs", "data" : true, "index" : true})
这一操作会将logs集合中的所有文档和索引载入内存。可指定内存只载入文档或只载入索引。touch操作结束后,可允许应用访问MongoDB。
然而,一整个集合(即使只有索引)依然可占用很大的空间。例如,应用可能只需要一个索引或一小部分文档在内存中。在这种情况下,需对数据进行自定义预热。
3.3 自定义预热
如需进行更复杂的预热,可自定义预热脚本。以下是一些常见的预热需求和解决方案。
- 加载一个特定的索引
假设索引必须处于内存中,如{"friends" : 1, "date" : 1}。可进行覆盖査询(covered query),从而将该索引载入内存中:
> db.users.find({}, {"_id" : 0, "friends" : 1, "date" : 1}). ... hint({"friends" : 1, "date" : 1}).explain()
以上explain命令会强制mongod遍历所有结果。必须通过find命令的第二个参数指定只返回被索引字段,否则这一查询同样会将所有文档加载入内存(也许这就是我们想要的结果,但应注意这一点)。注意,该操作总是会把无法被覆盖(covered)的文档和索引加载入内存,如多值索引(multikey index)。
- 加栽最近更新的文档
如在更新文档时同时更新了日期字段上的索引,可通过查询最近日期来加载最近更新的文档。
如没有日期字段上的索引,査询后会将集合中的所有文档加载入内存,所以就不必使用此方法了。在缺少日期字段的情况下,如主要关心的是最近插入的文档,可使用_id字段作为替代(参见下列内容)。
- 加载最近创建的文档
如_id字段使用ObjectldsIf类型,则可利用最近创建文档内的时间戳进行文档査询。如希望査找上星期建立的所有文档,可建立一个比所有要査找的文档建立时间都要早的_id:
> lastWeek = (new Date(year, month, day)).getTime()/1000 1348113600
将year、 month和date进行适当替换,返回的结果是以秒为单位的日期值。现在需要使用此日期建立一个ObjectId类型的值。首先,将其转换成一个十六进制字符串,然后在后面加上16个0:
> hexSecs = lastWeek.toString(16) 505a94c0 > minId = ObjectId(hexSecs+"0000000000000000") ObjectId("505a94c00000000000000000")
现在只需要对其查询:
> db.logs.find({"_id" : {"$gt" : minId}}).explain()
该操作会加载自上星期以来的所有文档(以及_id索引的一部分右子树)。
- 重放应用使用记录
MongoDB提供有名为诊断曰志(diaglog,diagnostic log)的功能来记录和回放操作流水。启用诊断日志会造成性能损失,所以最好通过临时使用的方式来获得一份“有代表性”的操作流水。在mongo shell中运行以下命令来记录操作流水:
> db.adminCommand({"diagLogging" : 2})
其中参数值为2意味着记录读取操作。该值为1时会记录写入操作,为3时读写都会进行记录(默认值为0,意味着不进行记录)。我们可能不希望记录写入操作,因为在重放操作流水时,该操作会导致新成员产生额外写入。
现在让mongod运行所需的时间并向其发送请求,从而令诊断日志记录操作流水。读取操作会被存放在诊断日志生成的文件中,该文件位于数据目录下。完成后将diagLogging的值重设为0:
> db.adminCommand({"diagLogging" : 0})
要想使用诊断文件,可从该文件所在的服务器启动新的服务器,运行以下命令:
$ nc hostname 27017 < /data/db/diaglog* | hexdump -c
按需对其中的IP地址、端口和数据目录进行替换。以上命令会将诊断文件中记录的操作作为普通请求发送到hostname:27017处。
注意:诊断日志会记录开启诊断日志的命令,所以,重放完成后,需登录服务器并关闭诊断日志(我们可能也想删除从重放中生成的诊断文件)。
这些技术可结合起来使用。例如,可在重放诊断记录的同时加载若干索引;如果没有遇到磁盘IO瓶颈的话,也可以同时进行这些操作;或者也可以通过多个shell或者startParallelShell (启动并行shell)命令(如果shell在mongod本地的话)来进行操作:
> p1 = startParallelShell("db.find({}, {x:1}).hint({x:1}).explain()", port) > p2 = startParallelShell("db.find({}, {y:1}).hint({y:1}).explain()", port) > p3 = startParallelShell("db.find({}, {z:1}).hint({z:1}).explain()", port)
将port替换为mongod所在的端口值。
4.压缩数据
MongoDB会占用大量的磁盘空间。有时,大量数据被删除或更新后,会在集合中产生碎片。如数据文件中有很多空闲空间,但由干这些独立的空闲区块过小,从而使得MongoDB无法对其进行重新利用时,就产生了碎片。在这种情况下,会在日志中看到类似如下信息:
Fri Oct 7 06:15:03 [conn2] info DFM::findAll(): extent 0:3000 was empty,
skipping ahead. ns:bar.foo
该信息本身是无害的。然而,这意味着某一整个区段(extent)中不包含任何文档。 为消除空区段,并高效重整集合,可使用compact命令:
> db.runCommand({"compact" : "collName"})
压缩操作会消耗大量资源,不应在mongod向客户端提供服务时计划压缩操作。推荐做法类似于建立索引时的做法,即在每个备份节点中对数据执行压缩操作,然后关闭主节点,进行最后的压缩操作。
在一个备份节点中运行压缩操作,会使其进入恢复状态(recovering state),即它会对读取请求返回错误,亦无法作为一个同步源。压缩操作结束后,其状态会重新变为备份节点(secondary state)。
压缩操作会将文档尽可能地安排在一起,文档间的间隔参数默认为1。如需更大的间隔参数,可使用额外的参数来指定它:
> db.runCommand({"compact" : "collName", "paddingFactor" : 1.5})
间隔参数最小为1,最大为4。对间隔参数的设定不会持续生效,只会影响压缩过程中MongoDB重新安排文档时的间隔。压缩完成后,间隔参数会重新返回之前的值。
压缩操作并不会减少集合占用的磁盘空间,该操作只是将所有文档都安排在集合的开始部分,这样当集合继续增大时就可以使用后面的空余部分。因此,压缩操作只是在磁盘空间不足时的临时措施,它不会减少MongoDB所使用的磁盘空间大小,但可使MongoDB不再需要分配新的空间。
可通过运行repair (修复)命令来回收磁盘空间。repair命令会对所有数据进行复制,所以必须拥有和当前数据文件一样大小的空余磁盘空间。这时常是个大问题,因为运行repair的最常见原因就是机器的磁盘空间不多了。然而,如能挂载其他磁盘,则可指定一个修复路径,即repair命令复制文件时所使用的目录(新安装的驱动)。
由于repair操作会完全复制所有数据,因此可随时中断该操作而不影响原始数据集。如在repair操作的过程中遇到问题,可删除repair产生的临时文件而不会影响到数据文件。
在启动mongod时使用--repair选项(如需要,还可使用--repairpath选项)来进行修复。
可以在shell中调用db.repairDatabase()来修复数据库。
5.移动集合
可使用renameCollection命令来重命名集合。这一命令无法在数据库间移动集合,但可更改集合名。无论重命名的集合大小,该操作都基本上是瞬间完成的。在繁忙的系统上,这一操作会耗费几秒钟,但在生产环境中运行可不用担心性能的消耗。
> db.sourceColl.renameCollection("newName")
执行这一命令时可传入第二个参数,从而决定当名为newName的集合已经存在时应如何处理。该参数为true时,会删除名为newName的集合;为false时,则会抛出错误。后者是这一参数的默认值。
要想在数据库间移动集合,必须进行转储(dump)和恢复(restore)操作,或手动复制集合中的文档(使用find命令,遍历集合中的所有文档,从而将其插入到新的数据库中)。
可使用cloneCollection命令将一个集合移动到另一个不同的mongod中。
> db.runCommand({"cloneCollection" : "collName", "from" : "hostname:27017"})
无法使用cloneCollection命令在mongod中移动集合,这一命令只能用于服务器间的集合移动。
6.预分配数据文件
如知道mongod具体需要哪些数据文件,可运行以下脚本,从而在应用上线前预分配数据文件。如能确定数据库和操作记录的大小,至少是一段时间以内的大小,这一方法则尤其有用。
#!/bin/bash # Make sure db name was passed in if test $# -lt 2 || test $# -gt 3 then echo "$0 <db> <number-of-files>" fi db=$1 num=$2 for i in {0..$num} do echo "Preallocating $db.$i" head -c 2146435072 /dev/zero > $db.$i done
将以上代码存入一个文件中(比如说preallocate文件),并将文件设置为可执行文件。切换至数据目录,按需执行以下脚本,分配数据文件:
$ # create test.0-test.100 $ preallocate test 100 $ $ # create local.0-local.4 $ preallocate local 4
数据库启动后首次访问数据文件时,不能对其中的任何文件进行删除。例如,如分配数据文件test.0~test.100,而启动数据库后却发现只需使用test.0~test.20,这时我们不应删除test.21~test.100的文件。只要MongoDB意识到这些文件的存在,文件删除后则会导致MongoDB发生异常。