首页 » MongoDB实战 » MongoDB实战全文在线阅读

《MongoDB实战》10.1 部署

关灯直达底部

要成功部署MongoDB,你需要选择正确的硬件以及合适的服务器拓扑。如果有遗留数据,则需要知道如何才能有效地进行导入(和导出)。最后,还要确保你的部署是安全的。我们会在随后的小节中讨论这些问题。

10.1.1 部署环境

本节将介绍为MongoDB选择好的部署环境所要考虑的内容。我将讨论具体的硬件要求,例如CPU、内存和磁盘要求,为优化操作系统环境做些推荐,并提供一些关于云端部署的建议。

1. 架构

下面依次是两点与硬件架构有关的说明。

首先,因为MongoDB会将所有数据文件映射到一个虚拟的地址空间里,所以全部的生产部署都应该运行在64位的机器上。正如其他部分提到的那样,32位的架构会将MongoDB限制为仅有2 GB存储。开启了Journaling日志,该限制会减小到大约1.5 GB。这在生产环境里是很危险的,因为如果超过这个限制,MongoDB的行为将无法预测。你可以随意使用32位机器进行单元测试和预发布,但在生产环境以及负载测试时,请严格使用64位架构。

其次,MongoDB必须运行于小端序(little-endian)机器上。这一点通常不难做到,但运行SPARC、PowerPC、PA-RISC以及其他大端序架构的用户就只能望洋兴叹了。1大多数的驱动同时支持小端与大端字节序,因此MongoDB的客户端通常在这两种架构上都能运行。

1. 如果你对核心服务器的大端序支持感兴趣,请访问https://jira.mongodb.org/browse/SERVER-1625。

2. CPU

MongoDB并不是特别CPU密集型的;数据库操作很少是CPU密集型的。在优化MongoDB时,首要任务是确保该操作不是I/O密集型的(详见后续关于内存和磁盘的两部分内容)。

只有当索引和工作集都完全可放入内存时,你才可能遇到CPU的瓶颈。如果有一个MongoDB实例每秒钟处理成千上万(或数百)的查询,你能想到提供更多的CPU内核来提升性能。对于那些不使用JavaScript的读请求,MongoDB能够利用全部可用内核。

如果碰巧看到读请求造成CPU饱和,请检查日志中的慢查询警告。可能是缺少合适的索引,因此强制进行了表扫描。如果你开启了很多客户端,每个客户端都在运行表扫描,那么扫描加上它所带来的上下文切换会造成CPU饱和。这个问题的解决方案是增加必要的索引。

对于写请求,MongoDB一次只会用到一个核,这是由于全局写锁的缘故。因此扩展写负载的唯一方法是确保写操作不是I/O密集型的,并且用分片进行水平扩展。这个问题在MongoDB v2.0里有所好转,因为通常写操作不会在页错误时持有锁,而是允许完成其他操作。目前,有很多并发方面的优化正在开发之中,可能实现的几个选项是集合级锁(collection-level locking)和基于范围的锁(extent-based locking)。请查看JIRA和最新的发布说明以了解这些改进的开发状态。

3. 内存

和其他数据库一样,MongoDB在有大量内存时性能最好。请一定选择有足够内存的硬件(虚拟的或其他),足够容纳常用的索引和工作数据集。随着数据的增长,密切关注内存与工作数据集的比例。如果你让工作集大小超过内存,就可能看到明显的性能下降。从磁盘载入分页及分页这个过程本身不是问题,因为它是将数据载入内存的必要步骤。但是如果你对性能不满意,过多的分页可能就是问题所在。第7章详细讨论了工作集、索引大小和内存之间的关系。在本章末尾处,你会了解到识别内存不足的方法。

有一些情况下,你能安全地放任数据尺寸超出可用内存,但这仅仅是些例外,并非常见情况。一个例子是使用MongoDB进行归档,读和写都很少发生,并且不需要快速做出应答。在这种情况下,拥有和数据量一样的内存可能代价高昂却收效甚微,因为应用程序用不到那么多内存。对于完整的数据集,关键是测试。对应用程序的典型原型进行测试,确保能够得到所需的性能基线。

4. 磁盘

在选择磁盘时,你需要考虑IOPS(每秒的输入/输出操作数)以及寻道时间。这里不得不强调运行于单块消费级硬盘、云端虚拟盘(比方说EBS)以及高性能SAN之间的区别。一些应用程序在单块由网络连接的EBS卷上的性能还能接受,但是一些开销较大的应用程序就会有更高要求。

出于一些原因,磁盘性能是非常重要的。第一,在向MongoDB写入时,服务器默认每60 s与磁盘强制同步一次。这称为后台刷新(background flush)。对于写密集型应用与低速磁盘,后台刷新可能会因为速度慢对整体系统性能产生负面影响。第二,高速磁盘能让你更快地预热服务器。当需要重启服务器时,还要将数据集加载到内存里。这个过程是延时执行的;每次对MongoDB连续的读或写都会将一个新的虚拟内存页加载到内存里,直到物理内存被放满为止。高速磁盘能让这个过程执行得更快一些,这会提升MongoDB在冷重启之后的性能。最后,高速磁盘能改变应用程序工作集与所需内存的比例。比方说,相比其他磁盘,使用固态硬盘在运行时所需的内存更少(能有更大的容量)。

无论使用哪种类型的磁盘,通常在部署时都会比较严肃,不会只用一块硬盘,而是采用冗余磁盘阵列(RAID)。用户一般会使用Linux的LVM(Logical Volume Manager,逻辑卷管理器)来管理RAID集群,RAID级别2为10。RAID 10能在保持可接受性能的同时提供一定冗余,常用于MongoDB部署之中。

2. 如想对RAID级别有个概要认识,请访问http://en.wikipedia.org/wiki/Standard_RAID_levels。

如果数据分散在同一台MongoDB服务器的多个数据库之中,还可以通过服务器的--directoryperdb标志确保它们的容量,这将在数据文件路径中为每个数据库创建单独的目录。有了它,你还可以为每个数据库挂载单独的卷(无论是否是RAID)。这能让你充分发挥各磁盘的性能,因为你可以从不同的磁盘组(或固态硬盘)上读取数据。

5. 文件系统

如果运行在正确的文件系统上,你将从MongoDB中获得最好的性能。特别是ext4和xfs这两个文件系统,提供了高速、连续的磁盘分配。使用这些文件系统能够提升常见的MongoDB预分配的速度。

一旦挂载了高速文件系统,还可以通过禁止修改文件的最后访问时间(atime)来提升性能。通常情况下,每次文件有读写时操作系统都会修改文件的atime。在数据库环境中,这带来了很多不必要的工作。在Linux上关闭atime相对比较容易。首先,备份文件系统的配置文件;然后,用你喜欢的编辑器打开原来的文件:

sudo mv /etc/fstab /etc/fstab.baksudo vim /etc/fstab  

针对每个挂载的卷,你会看到一个列对齐的设置列表。在options列里,添加noatime指令:

# file-system mount type options dump passUUID=8309beda-bf62-43 /ssd ext4 noatime 0 2  

保存修改内容,新的设置应该能立刻生效。

6. 文件描述符

一些Linux系统最多能打开1024个文件描述符。有时,这个限制对MongoDB而言太低了,在打开连接时会引起错误(在日志里能清楚地看到这点)。MongoDB顺理成章地要求每个打开的文件和网络连接都有一个文件描述符。假设将数据文件保存在一个文件夹里,其中有data这个单词,可以通过lsof和一些管道看到数据文件描述符的数量:

lsof | grep mongo | grep data | wc -l  

统计网络连接描述符数量的方法也很简单:

lsof | grep mongo | grep TCP | wc -l  

对于文件描述符,最佳策略是一开始就设定一个很高的限额,使得在生产环境中永远都不会达到该值。可以使用ulimit工具检查当前的限额:

ulimit -Hn  

要永久提升限额,可以用编辑器打开limits.conf

sudo vim /etc/security/limits.conf  

然后,设置软、硬限额,这些限额是基于每个用户指定的。下面的例子假设mongodb用户将运行mongod进程:

mongod hard nofile 2048mongod hard nofile 10240  

新设置将在用户下次登录时生效。

7. 时钟

事实证明,复制容易受到时钟偏移(clock skew)的影响。如果托管了副本集中多个节点的机器的时钟之间存在分歧,副本集就可能无法正常运作了。这可不是理想状态,幸好存在解决方案。你要确保每台服务器都使用了NTP(Network Time Protocol,网络时间协议),借此保持服务器时钟的同步。在Unix类的系统上,也就是运行ntpd守护进程;在Windows上,Windows Time Services就能担当这个角色。

8. 云

有越来越多的用户在虚拟化环境中运行MongoDB,这些环境统称为云。其中,亚马逊的EC2因其易用性、广泛的地理分布以及强有力的价格成为了用户的首选。EC2及其他类似的环境都能部署MongoDB,但你也要牢记它们的缺点,尤其是在应用程序要将MongoDB推向其极限之时。

EC2的第一个问题是你只能选择几种有限的实例类型。在本书编写时,还没有超过68 GB内存的虚拟实例。此类约束强迫你在工作集超过68 GB时对数据库进行分片,这并非适用于所有应用程序。如果能运行于真实的硬件之上,你可以拥有更多内存;相同的硬件成本下,这能影响分片的决定。

另一个潜在问题是EC2从本质上来说是一个黑盒,你可能会遭遇服务中断或实例变慢,却无法进行诊断或补救。

第三个问题与存储有关。EC2允许你挂载虚拟块设备,称为EBS卷。EBS卷提供了很大的灵活性,允许你按需添加存储并在机器间移动卷。它还能让你制作快照,以便用于备份。EBS卷的问题在于无法提供很高的吞吐量,尤其是在和物理磁盘进行比较时。为此,大多数MongoDB用户在EC2上托管重要应用程序时,都会对EBS做RAID 10,以此提升读吞吐量。这对高性能应用程序而言是必不可少的。

出于这些原因,比起处理EC2的限制和不可预测性,很多用户更青睐于在自己的物理硬件上运行MongoDB。但是,EC2和其他云环境非常方便,为很多用户所广泛接受。在正式使用云存储之前,要慎重考虑应用程序的情况,并在云中进行测试。

10.1.2 服务器配置

一旦决定了部署环境,你需要确定总体服务器配置。这涉及选择服务器的拓扑和决定是否使用Journaling日志,以及如何使用。

1. 选择一种拓扑结构

最小的推荐部署拓扑是三个成员的副本集。其中至少有两个是数据存储(非仲裁)副本,位于不同的机器上,第三个成员可以是另一个副本,也可以是仲裁节点。仲裁节点无需自己的机器,举例来说,你可以把它放在应用服务器上。第8章里有两套合理的副本集部署配置。

如果从一开始你就预计到工作集大小会超过内存,那开始时就可以使用分片集群了,其中至少包含两个副本集。第9章中有分片部署的详细推荐配置,还有关于何时开始分片的建议。

你可以部署单台服务器来支持测试和预发布环境。但对于生产环境部署而言,并不推荐采用单台服务器,就算开启了Journaling日志也是如此。只有一台服务器会为备份和恢复造成一定复杂性,当服务器发生故障时,无法进行故障转移。

但是,在极少的几种情况下也有例外。如果应用程序不需要高可用性或者快速恢复,数据集相对较小(比方说小于1 GB),那么运行在一台服务器上也是可以的。即使如此,考虑到日益下降的硬件成本,以及复制所带来的众多好处,先前提到的单机方案确实没什么亮点。

2. Journaling日志

MongoDB v1.8引入了Journaling日志,而MongoDB v2.0会默认开启Journaling日志。当Journaling日志开启时,MongoDB在写入核心数据文件时会先把所有写操作提交到Journaling日志文件里。这能让MongoDB服务器在发生非正常关闭时快速恢复并正常上线。

在v1.8之前没有此类特性,因此非正常关闭经常会导致灾难。这怎么可能呢?我之前多次提到MongoDB把每个数据文件映射到虚拟内存里。也就是说,当MongoDB执行写操作时,它是写入虚拟内存地址,而非直接写入磁盘。OS内核周期性地将这些写操作从虚拟内存同步到磁盘上,但是其频率和完整性是不确定的,因此MongoDB使用fsync系统调用每60 s对所有数据文件做一次强制同步。这里的问题在于,如果MongoDB进程在还有未同步的写操作时被杀掉了,那么则无法获知数据文件的状态。这就可能损坏数据文件。

在没有开启Journaling日志的mongod进程发生非正常关闭时,想将数据文件恢复到一致状态要运行一次修复。修复过程会重写数据文件,抛弃所有它无法识别的内容(损坏的数据)。因为大家通常都不太能接受停机和数据丢失,所以这种修复途径一般只能作为最后的恢复手段。从现有副本中重新同步数据几乎总是比较方便可靠的方法,这也是运行复制如此重要的原因之一。

Journaling日志让你不再需要修复数据库,因为MongoDB能用Journaling日志将数据文件恢复到一致状态。在MongoDB v2.0里,Journaling日志是默认开启的,但是你也可以通过-nojournal标志禁用它:

$ mongod --nojournal  

开启Journaling日志时,日志文件被放在一个名为journal的目录里,该目录位于主数据路径下面。

如果你在运行MongoDB服务器时开启了Journaling日志,请牢记几点。第一点,Journaling日志会降低写操作的性能。既想获得最高写入性能,又想有Journaling日志保障的用户有两个选择。其一,只在被动副本上开启Journaling日志,只要这些副本能和主节点保持一致,就无需牺牲性能。另一个解决方案,也许和前者是互补的,是为Journaling日志挂载一块单独的磁盘,然后在journal目录和辅助卷之间创建一个符号链接。辅助卷不用很大,一块120 GB的磁盘就足够了,这个大小的固态硬盘(SSD)的价格还是可以承受的。为Journaling日志挂载一块单独的SSD能确保将它运行时的性能损耗降到最小。

第二点,Journaling日志本身并不保证不会丢失写操作,它只能保证MongoDB始终能恢复到一致状态,重新上线。Journaling日志的机制是每100 ms将写缓冲和磁盘做一次同步。因此一次非正常关闭最多只会丢失100 ms里的写操作。如果你的应用程序无法接受这种风险,可以使用getlasterror命令的j选项,让服务器在Journaling日志同步后才返回:

db.runCommand({getlasterror: 1, j: true})  

在应用程序层,可以用safe模式选项(与wwtimeout类似)。在Ruby里,可以像这样使用j选项:

@collection.insert(doc, :safe => {:j => true})  

一定要注意,每次写操作都像这样做是不明智的,因为这会强迫每次写操作都等到下次Journaling日志同步才返回。也就是说,所有的写操作都可能要等100 ms才能返回。因此请谨慎使用本特性。3

3. MongoDB的未来版本里会有更细粒度的Journaling日志同步控制,请查看最新的发布说明了解详细情况。

10.1.3 数据的导入与导出

如果你正从现有系统迁移到MongoDB,或者需要从数据仓库填充数据,那么就需要一种有效的导入方法。你可能还需要一个好的导出策略,因为可能要从MongoDB里将数据导出到外部处理任务中。例如,将数据导出到Hadoop进行批处理就已成为一种常见实践。4

4. 对于这种特定用法,在http://github.com/mongodb/mongo-hadoop可以找到官方支持的MongoDB-Hadoop适配器。

有两种途径将数据导入和导出MongoDB,你可以使用自带的工具——mongoimportmongoexport,或者使用某个驱动编写一个简单的程序。5

5. 注意,数据的导入和导出与备份有所不同,本章后面会讨论备份相关的内容。

1. mongoimportmongoexport

MongoDB自带了两个导入、导出数据的工具:mongoimportmongoexport。你可以通过mongoimport导入JSON、CSV和TSV文件,这通常用于从关系型数据库向MongoDB加载数据:

$ mongoimport -d stocks -c values --type csv --headerline stocks.csv  

本例中,你将一个名为stocks.csv的CSV文件导入到了stocks数据库的values集合里。--headerline标志表明了CSV的第一行包含字段名。可以通过mongoimport –help看到所有的导入选项。

可以通过mongoexport将一个集合的所有数据导出到一个JSON或CSV文件里:

$ mongoexport -d stocks -c values -o stocks.csv  

这条命令会将数据导出到stocks.csv文件里。与mongoimport类似,你可以通过--help看到mongoexport的其他命令选项。

2. 自定义导入与导出脚本

当处理的数据相对扁平时,你可能会使用MongoDB的导入导出工具;一旦引入了子文档和数组,CSV格式就有些“力不从心”了,因为它不是设计来表示内嵌数据的。当需要将富文档导出到CSV或者从CSV导入一个富MongoDB文档,也许构建一个自定义工具会更方便。你可以使用任意驱动实现这一目标。例如,MongoDB用户通常会编写一些脚本连接关系型数据库,随后将两张表的数据整合到一个集合里。

将数据移入和移出MongoDB是件很复杂的事:数据建模的方式会因系统而异。在这些情况下,要做好将驱动当成转换工具的准备。

10.1.4 安全

大多数RDBMS都有一套复杂的安全子系统,可以对用户和用户组授权,进行细粒度的权限控制。与此相反,MongoDB v2.0只支持简单的、针对每个数据库的授权机制。这就让运行MongoDB的机器的安全性变得更加重要了。此处我们会讨论在安全环境里运行MongoDB所需考虑的一些重点内容,并解释身份验证是如何进行的。

1. 安全环境

和所有数据库一样,MongoDB应该运行在一个安全环境里。生产环境中,MongoDB的用户必须利用现代操作系统的安全特性来确保数据的安全。在这些特性之中,也许最重要的就是防火墙了。在结合使用防火墙与MongoDB时,唯一潜在的难点是了解哪台机器需要和其他机器互相通信。还好,通信规则很简单。在副本集中,每个节点都要能和其他节点通信。此外,所有数据库客户端都必须能连接到各个它可能会通信的副本集节点上。

分片集群中含有副本集,因此所有副本集的规则都能适用;在分片的情况下,客户端是mongos路由器。除此之外:

  • 所有分片都必须能与其他分片直接通信;

  • 分片与mongos路由器都必须能连上配置服务器。

相关的安全关注点是绑定地址(bind address)。默认情况下,MongoDB会监听本机的所有地址,但你可能只想监听一个或几个特殊的地址。为此,可以在启动mongodmongos时带上--bind_ip选项,它接受一个或多个逗号分隔的IP地址。例如,想要监听loopback接口和内部IP地址10.4.1.55,可以像这样启动mongod

mongod --bind_ip 127.0.0.1,10.4.1.55  

请注意,机器之间发送数据都使用明文,官方的SSL支持安排在MongoDB v2.2中发布。

2. 身份验证

MongoDB的身份验证最早是为那些在共享环境下托管MongoDB服务器的用户构建的。它的功能并不多,但在需要一些额外安全保障时还是很有用的。我们先讨论一下身份验证API,然后再描述如何在副本集和分片中使用该API。

  • 身份验证API

要着手了解身份验证,先创建一个管理员用户,切换到admin数据库,运行db.addUser,该方法接受两个参数:一个用户名和一个密码。

> use admin> db.addUser("boss", "supersecret")  

管理员用户能创建其他用户,还能访问服务器上的所有数据库。有了它,你就能开启身份验证了,在重启mongod实例时加上--auth选项:

$ mongod --auth  

现在,只有通过身份验证的用户才能访问数据库。重启Shell,随后使用db.auth方法以管理员身份登录:

> use admin> db.auth("boss", "supersecret")  

现在可以为个别数据库创建用户了。如果想要创建只读用户,将true作为db.addUser方法的最后一个参数。这里将为stocks数据库添加两个用户。第一个用户拥有所有权限,第二个只能读取数据库的数据:

> use stocks> db.addUser("trader", "moneyfornuthin")> db.addUser("read-only-trader", "foobar", true)  

现在,只有三个用户能访问stocks数据库,他们是bosstraderread-only-trader。如果你希望查看拥有某个数据库访问权限的所有用户的列表,可以查询system.users集合:

> db.system.users.find{ "_id" : ObjectId("4d82100a6dfa7bb906bc4df7"),  "user" : "trader", "readOnly" : false,  "pwd" : "e9ee53b89ef976c7d48bb3d4ea4bffc1" }{ "_id" : ObjectId("4d8210176dfa7bb906bc4df8"),  "user" : "read-only-trader", "readOnly" : true,  "pwd" : "c335fd71fb5143d39698baab3fdc2b31" }  

从该集合中删除某个用户,就能撤销它对某个数据库的访问权限。如果你更青睐于辅助方法,可以使用Shell里的db.removeUser方法,它的作用是一样的。

你并不需要显式注销,中断连接(关闭Shell)就行了。但是如果你需要,也有注销命令可用:

> db.runCommand({logout: 1})  

当然,你也可以通过驱动使用我们此处看到的全部身份验证逻辑,请查看驱动的API了解更多详情。

  • 副本集身份验证

副本集也支持刚才介绍的身份验证API,但是为副本集开启身份验证还需要额外的几个步骤。开始时,创建一个文件,其中至少包含6个Base64字符集6中的字符。文件的内容会被作为某种密码,每个副本集成员都会用它来和其他成员进行身份验证。举个例子,你可以创建一个名为secret.txt的文件,其内容如下:

6. Base64字符集由以下字符组成:英文字母中的全部大写和小写字母、数字0~9以及+和/。

tOps3cr3tpa55word  

将该文件放到每个副本集成员的机器上,调整文件权限,以便只有文件的拥有者才能访问它:

sudo chmod 600 /home/mongodb/secret.txt  

最后,在启动每个副本集成员时使用--keyFile选项指定密码文件的位置:

mongod --keyFile /home/mongodb/secret.txt  

现在副本集就已经开启身份验证了,你会希望事先创建一个管理员用户,就像上一节里那样。

  • 分片身份验证

分片身份验证是副本集身份验证的一个扩展。集群里的每个副本集都已经像刚才介绍的那样,通过密钥文件保护起来了。此外,所有的配置服务器和每个mongos实例也都拥有一个包含相同密码的密钥文件。在启动每个进程时都用--keyFile选项指定包含密码的文件,整个分片集群都使用该密码。完成这个步骤,整个集群就能使用身份验证了。