MongoDB 实践教程(三)
八、MongoDB 解释
“MongoDB explained 涵盖了 MongoDB 的深层概念。”
在这一章中,你将学习如何在 MongoDB 中存储数据,以及如何使用日志进行写操作。最后,您将了解 GridFS 和 MongoDB 中可用的不同类型的索引。
8.1 数据存储引擎
在前一章中,您查看了作为 MongoDB 的一部分部署的核心服务;您还了解了副本集和分片。在本节中,我们将讨论数据存储引擎。
MongoDB 使用 MMAP 作为默认的存储引擎。该引擎处理内存映射文件。内存映射文件是操作系统使用mmap()系统调用放置在内存中的数据文件。mmap 是 OS 的一个特性,它将磁盘上的文件映射到虚拟内存中。
虚拟内存不等同于物理内存。虚拟内存是计算机硬盘上与物理 RAM 一起使用的空间。
MongoDB 将内存映射文件用于任何数据交互或数据管理活动。当文档被访问时,数据文件被存储器映射到存储器。MongoDB 允许操作系统控制内存映射并分配最大数量的 RAM。这样做的结果是 MongoDB 级别的工作和编码最少。缓存是基于 LRU 行为完成的,其中最近最少使用的文件从工作集中移到磁盘,为最近使用和经常使用的新页面腾出空间。
然而,这种方法有其自身的缺点。例如,MongoDB 无法控制哪些数据应该保留在内存中,哪些应该删除。因此,每次服务器重启都会导致页面错误,因为被访问的每个页面在工作集中都不可用,从而导致很长的数据检索时间。
MongoDB 也无法控制内存内容的优先级。在撤离的情况下,它可以指出哪些内容需要保留在缓存中,哪些内容可以删除。例如,如果对未编制索引的大型集合进行读取,可能会导致将整个集合加载到内存中,这可能会导致清空 RAM 内容,包括删除可能非常重要的其他集合的索引。当 MongoDB 之外的任何外部进程试图访问大部分内存时,这种缺乏控制也可能导致分配给 MongoDB 的缓存收缩;这最终会导致 MongoDB 响应缓慢。
随着 3.0 版的发布,MongoDB 附带了一个可插拔的存储引擎 API,它使您能够根据工作负载、应用需求和可用的基础设施在存储引擎之间进行选择。
可插拔存储引擎层背后的愿景是拥有一个数据模型、一种查询语言和一组操作关注点,但在幕后有许多针对不同用例优化的存储引擎选项,如图 8-1 所示。
图 8-1。
Pluggable storage engine API
可插拔存储引擎功能还在部署方面提供了灵活性,其中多种类型的存储引擎可以在同一部署中共存。
MongoDB 版附带了两个存储引擎。
默认的 MMAPv1 是以前版本中使用的 MMAP 引擎的改进版本。更新后的 MongoDB MMAPv1 存储引擎实现了集合级并发控制。该存储引擎擅长处理大量读取、插入和就地更新的工作负载。
新的 WiredTiger 存储引擎是由世界上部署最广泛的嵌入式数据管理软件 Berkeley DB 的架构师开发的。WiredTiger 在现代多 CPU 架构上扩展。它旨在利用具有多核 CPU 和更多 RAM 的现代硬件。
WIredTiger 将数据以压缩格式存储在磁盘上。根据所使用的压缩算法,压缩可将数据大小减少 70%(仅磁盘),将索引大小减少 50%(磁盘和内存)。除了减少存储空间之外,压缩还支持更高的 I/O 可扩展性,因为从磁盘读取的位更少。它在更高的硬件利用率、更低的存储成本和更可预测的性能方面提供了显著的优势。
以下压缩算法可供选择:
- Snappy 是默认设置,用于文档和日志。它以很少的 CPU 开销提供了很好的压缩率。根据数据类型的不同,压缩率在 70%左右。
- zlib 提供了非常好的压缩,但代价是额外的 CPU 开销。
- 前缀压缩是索引的默认使用方式,它将索引存储的内存占用减少了大约 50%(取决于工作负载),并为频繁访问的文档释放了更多的工作集。
管理员可以修改所有集合和索引的默认压缩设置。在集合和索引创建期间,还可以基于每个集合和每个索引来配置压缩。
WiredTiger 还提供细粒度的文档级并发。写操作不再被其他写操作阻塞,除非它们正在访问同一个文档。因此,它支持读者和作者同时访问集合中的文档。客户端可以在写操作进行的同时读取文档,多个线程可以同时修改集合中的不同文档。因此,它非常适合写入密集型工作负载(写入性能提高 7-10 倍)。
更高的并发性也推动了基础设施的简化。应用可以充分利用可用的服务器资源,简化满足性能 SLA 所需的架构。使用前几代 MongoDB 的更粗粒度的数据库级锁定,用户经常必须实现分片,以便扩展由于对数据库的单个写锁定而停止的工作负载,即使主机系统中仍然有足够的内存、I/O 带宽和磁盘容量可用。通过细粒度并发实现的更高系统利用率降低了这一开销,消除了不必要的成本和管理负载。
这个存储引擎为您提供了对每个索引级别的每个集合的控制,以决定压缩什么和不压缩什么。
WiredTiger 存储引擎仅适用于 64 位 MongoDB。
WiredTiger 通过其缓存管理数据。WiredTiger 存储引擎允许您配置分配给 WiredTiger 缓存的 RAM 大小,默认为 1GB 或 50%的可用内存,以较大者为准,从而提供更多的内存控制。
接下来,您将简要了解数据是如何存储在磁盘上的。
8.2 数据文件(与 MMAPv1 相关)
首先,让我们检查数据文件。如您所见,在核心服务下,mongod 使用的默认数据目录是/data/db/。
在这个目录下,每个数据库都有单独的文件。每个数据库都有一个.ns文件和多个数据文件,这些文件的扩展名都是单调递增的数字。
比如你创建了一个名为mydbpoc的数据库,它会被存储在以下文件中:mydb.ns、mydb.1、mydb.2等等,如图 8-2 。
图 8-2。
Data files
对于数据库的每个新数字数据文件,其大小将是前一个数字数据文件大小的两倍。文件大小的限制是 2GB。如果文件大小已达到 2GB,所有后续编号的文件将保持 2GB 大小。这种行为是故意的。这种行为确保小型数据库不会浪费太多的磁盘空间,而大型数据库大多保存在磁盘上的连续区域中。
注意,为了确保一致的性能,MongoDB 会预分配数据文件。预分配在后台进行,并在每次填充数据文件时启动。这意味着 MongoDB 服务器总是试图为每个数据库保留一个额外的空数据文件,以避免阻塞文件分配。
如果磁盘上存在多个小型数据库,使用storage.mmapv1.smallFiles选项将减小这些文件的大小。
接下来,您将看到数据实际上是如何存储的。双向链表是用于存储数据的关键数据结构。
8.2.1 命名空间(。ns 文件)
在数据文件中,数据空间被划分为名称空间,其中名称空间可以对应于一个集合或一个索引。
这些名称空间的元数据存储在.ns文件中。如果您检查您的数据目录,您会发现一个名为[dbname].ns的文件。
用于存储元数据的.ns文件的大小是 16MB。这个文件可以被认为是一个大的哈希表,它被划分成小的存储桶,大小大约为 1KB。
每个桶存储特定于名称空间的元数据(图 8-3 )。
图 8-3。
Namespace data structure
8.2.1.1 集合命名空间
如图 8-4 所示,集合命名空间桶包含元数据,如
图 8-4。
Collection namespace details
- 集合的名称
- 收集的一些统计数据,如计数、大小等。(这就是为什么每当对集合发出计数时,它都返回快速响应。)
- 索引详细信息,因此它可以维护到每个创建的索引的链接
- 删除的列表
- 存储盘区细节的双向链表(它存储指向第一个和最后一个盘区的指针)
程度
范围是索引据文件中的一组数据记录,因此一组范围构成了一个名称空间的完整数据。扩展区使用磁盘位置来引用数据在磁盘上实际驻留的位置。它由两部分组成:文件号和偏移量。
文件号指定了它所指向的数据文件(0,1,等等。).
Offset 是文件中的位置(您需要在文件中寻找数据的深度)。偏移大小为 4KB。因此,偏移量的最大值可以达到 2 31 -1,这是数据文件可以增长到的最大文件大小(2048MB 或 2 GB)。
如图 8-5 所示,范围数据结构由以下内容组成:
图 8-5。
Extent
- 磁盘上的位置,也就是它所指向的文件号。
- 因为一个范围被存储为一个双向链表元素,所以它有一个指向下一个和上一个范围的指针。
- 一旦它有了它所引用的文件号,它所指向的文件中的数据记录组就被进一步存储为双向链表。因此,它维护了一个指向它所指向的数据块的第一个数据记录和最后一个数据记录的指针,它们只不过是文件内的偏移量(数据在文件内存储的深度)。
数据记录
接下来,您将看到数据记录结构。数据结构由以下细节组成:
- 因为数据记录结构是区的双向链表的一个元素,所以它存储前一个和下一个记录的信息。
- 它有带标题的长度。
- 数据块。
数据块可以有一个 BTree 桶(在索引命名空间的情况下)或一个 BSON 对象。一会儿你会看到 BTree 结构。
BSON 对象对应于集合的实际数据。BSON 对象的大小不需要与数据块相同。默认情况下使用 2 次方大小的分配,这样每个文档都存储在一个包含该文档加上额外空间或填充的空间中。每当更新导致对象大小改变时,这种设计决策有助于避免对象从一个块移动到另一个块。
MongoDB 支持多种分配策略,这些策略决定了如何给文档添加填充(图 8-6 )。由于就地更新比重定位更有效,所有填充策略都以额外的空间换取效率的提高和碎片的减少。不同的策略支持不同的工作负载。例如,精确匹配分配非常适合于具有仅插入工作负载的集合,其中大小是固定的并且从不变化,而 2 的幂分配对于插入/更新/删除工作负载是有效的。
图 8-6。
Record data structure
已删除列表
删除列表存储其数据已被删除或移动的盘区的详细信息(每当更新导致大小改变时的移动,导致数据不适合分配的空间)。
记录的大小决定了需要放置空闲区的桶。基本上这些都是分桶的单链表。当需要新的盘区来适应命名空间的数据时,它将首先搜索空闲列表,以检查是否有任何适当大小的盘区可用。
概括起来
因此,您可以假设数据文件(带有数字扩展名的文件)被划分到不同的集合名称空间,其中名称空间的范围指定了属于相应集合的数据文件的数据范围。
了解了数据是如何存储的,现在让我们看看db.users.find()是如何工作的。
它将首先检查mydbpoc.ns文件以到达users名称空间,并找出它所指向的第一个范围。它将跟随第一个区段链接到第一条记录,并跟随下一条记录指针,它将读取第一个区段的数据记录,直到到达最后一条记录。那么它将跟随下一个盘区指针,以类似的方式读取其数据记录。遵循这种模式,直到读取最后一个盘区数据记录。
8.2.1.2 $免费列表
对于扩展区,.ns文件有一个名为$freelist的特殊名称空间。$freelist跟踪不再使用的范围,比如被删除的索引或集合的范围。
8.2.1.3 索引树
现在让我们看看索引是如何存储的。BTree 结构用于存储索引。BTree 如图 8-7 所示。
图 8-7。
BTree
在 BTree 的标准实现中,每当一个新的键被插入 BTree 时,默认行为如图 8-8 所示。
图 8-8。
B-Tree standard implementation
MongoDB 实现 BTree 的方式略有不同。
在上面的场景中,如果您有 Timestamp、ObjectID 或一个递增数字之类的键,那么存储桶将总是半满的,导致大量空间浪费。
为了克服这一点,MongoDB 对此进行了小幅修改。每当它识别出索引键是递增键时,它不是进行 50/50 分割,而是进行 90/10 分割,如图 8-9 所示。
图 8-9。
MongoDB’s B-Tree 90/10 split
图 8-10 为铲斗结构示意图。BTree 的每个桶是 8KB。
图 8-10。
BTree bucket data structure
铲斗由以下部分组成:
- 指向父节点的指针
- 指向正确子节点的指针
- 指向关键节点的指针
- 关键对象的列表(这些对象具有不同的大小,并且以未排序的方式存储;这些对象实际上是索引键的值)
关键节点
关键节点是固定大小的节点,并且以排序的方式存储。它们支持在 BTree 的不同节点之间轻松拆分和移动元素。
关键节点包含以下内容:
- 指向左边子节点的指针
- 索引键所属的数据记录
- 键偏移量(键对象的偏移量,它基本上告诉我们键值存储在桶中的什么位置)
8.3 数据文件(与 WiredTiger 相关)
在这一节中,您将看到用 WiredTiger 存储引擎启动 mongod 时数据目录的内容。
当选择的存储选项是 WiredTiger 时,数据、日志和索引在磁盘上被压缩。压缩是基于启动 mongod 时指定的压缩算法完成的。
Snappy 是默认的压缩选项。
在数据目录下,有对应于每个集合和索引的单独的压缩 wt 文件。日志在数据目录下有自己的文件夹。
压缩文件实际上是在数据插入集合时创建的(文件是在写入时分配的,没有预分配)。
例如,如果您创建一个名为users的集合,它将被存储在collection-0—2259994602858926461文件中,而相关的索引将被存储在index-1—2259994602858926461、index-2—2259994602858926461中,以此类推。
除了集合和索引压缩文件之外,还有一个_mdb_catalog文件,它存储将集合和索引映射到数据目录中的文件的元数据。在上面的例子中,它将存储集合用户到 wt 文件collection-0—2259994602858926461的映射。见图 8-11 。
图 8-11。
WiredTiger Data folder
可以指定单独的卷来存储索引。
在指定 DBPath 时,您需要确保该目录对应于存储引擎,这是在启动 mongod 时使用–storageEngine选项指定的。如果 dbpath 包含由存储引擎而不是使用–storageEngine选项指定的存储引擎创建的文件,mongod 将无法启动。所以如果在 DBPath 中找到 MMAPv1 文件,那么 WT 将无法启动。
在内部,WiredTiger 使用传统的 B+树结构来存储和管理数据,但这是相似性的终点。与 B+ tree 不同,它不支持就地更新。
WiredTiger 缓存用于对数据的任何读/写操作。缓存中的树针对内存访问进行了优化。
8.4 读写
您将简要了解读写是如何发生的。如前所述,当 MongoDB 更新和读取数据库时,它实际上是读取和写入内存。
如果 MongoDB MMAPv1 存储引擎中的修改操作增加的记录大小大于为其分配的空间,那么整个记录将被移动到一个更大的空间,并带有额外的填充字节。默认情况下,MongoDB 使用 2 次方的分配,因此 MongoDB 中的每个文档都存储在包含文档本身和额外空间(填充)的记录中。填充允许文档随着更新而增长,同时最小化重新分配的可能性。一旦记录被移动,最初被占用的空间将被释放,并将作为不同大小的空闲列表被跟踪。如前所述,它是.ns文件中的$freelist名称空间。
在 MMAPv1 存储引擎中,随着对象的删除、修改或创建,随着时间的推移会出现碎片,这会影响性能。应该执行compact命令将碎片数据移动到连续的空间中。
RAM 中的文件每 60 秒刷新到磁盘。为了防止断电时数据丢失,默认设置是打开日志记录运行。日志的行为取决于配置的存储引擎。
MMAPv1 日志文件每 100ms 刷新到磁盘,如果断电,它用于将数据库恢复到一致状态。
在 WiredTiger 中,缓存中的数据存储在 B+树结构中,该结构针对内存进行了优化。高速缓存维护一个与索引相关联的磁盘页面映像,该映像用于识别被请求的数据实际驻留在页面中的什么位置(参见图 8-12 )。
图 8-12。
WiredTiger cache
WiredTiger 中的写操作从不就地更新。
每当向 WiredTiger 发出一个操作时,它在内部被分解成多个事务,其中每个事务都在内存快照的上下文中工作。快照是事务开始之前提交的版本。作者可以与读者同时创作新版本。
写操作不改变页面;相反,更新是分层在页面的顶部。skipList 数据结构用于维护所有更新,其中最新的更新位于顶部。因此,每当用户读取/写入数据时,索引都会检查 skiplist 是否存在。如果没有 skiplist,它将从磁盘上的页面映像返回数据。如果 skiplist 存在,列表头部的数据将返回给线程,然后线程更新数据。一旦执行了提交,更新的数据就被添加到列表的头部,指针也相应地被调整。这样,多个用户可以同时访问数据,而不会发生任何冲突。只有当多个线程试图更新同一个记录时,才会发生冲突。在这种情况下,一个更新成功,另一个并发更新需要重试。
由于更新而导致的树结构的任何变化,例如,如果页面大小增加,则拆分数据,重新定位等。,稍后由后台进程进行协调。这说明了 WiredTiger 引擎的快速写操作;数据整理的任务留给后台进程。参见图 8-13 。
图 8-13。
SkipList
WiredTiger 使用 MVCC 方法来确保并发控制,其中维护多个版本的数据。它还确保每个试图访问数据的线程都看到最一致的数据版本。如你所见,写得不到位;相反,它们被附加在 skipList 数据结构中的数据的顶部,最新的更新在顶部。访问数据的线程获得最新的副本,并且它们继续不间断地使用该副本,直到提交为止。一旦提交,更新将被附加到列表的顶部,此后任何访问数据的线程都将看到最新的更新。
这使得多个线程可以同时访问相同的数据,而不会发生任何锁定或争用。这也使得作者能够与读者同时创建新版本。只有当多个线程试图更新同一个记录时,才会发生冲突。在这种情况下,一个更新成功,另一个并发更新需要重试。
图 8-14 描绘了运行中的 MVCC。
图 8-14。
Update in action
WiredTiger 日志确保写操作在检查点之间被持久化到磁盘上。默认情况下,WiredTiger 每 60 秒或在写入 2GB 数据后使用检查点将数据刷新到磁盘。因此,默认情况下,如果在没有日志记录的情况下运行,WiredTiger 可能会丢失长达 60 秒的写入,尽管如果使用复制来保证持久性,这种丢失的风险通常会小得多。WiredTiger 事务日志不需要在非正常关闭的情况下保持数据文件的一致状态,因此在不启用日志记录的情况下运行是安全的,尽管为了确保持久性,应该配置“副本安全”写问题。WiredTiger 存储引擎的另一个特性是能够压缩磁盘上的日志,从而减少存储空间。
8.5 如何使用日志记录写入数据
在本节中,您将简要了解如何使用日志记录来执行写操作。
MongoDB 磁盘写入是懒惰的,这意味着如果一秒钟内有 1000 个增量,它将只写入一次。物理写入发生在操作后几秒钟。
现在您将看到 mongod 中的更新是如何发生的。
如您所知,在 MongoDB 系统中,mongod 是主要的守护进程。所以磁盘有数据文件和日志文件。参见图 8-15 。
图 8-15。
mongod
当 mongod 启动时,数据文件被映射到一个共享视图。换句话说,数据文件被映射到一个虚拟地址空间。参见图 8-16 。
图 8-16。
maps to shared view
基本上,操作系统识别出您的数据文件在磁盘上是 2000 字节,所以它将它映射到内存地址 1,000,000–1,002,000。请注意,数据在被访问之前不会被真正加载;操作系统只是映射并保存它。
直到现在,你仍然有备份内存的文件。因此,操作系统会将内存中的任何更改刷新到底层文件中。
当没有启用日志记录时,mongod 就是这样工作的。操作系统每 60 秒刷新一次内存中的更改。
在这种情况下,让我们看看启用了日志记录的写入。当启用日志记录时,mongod 会对私有视图进行第二次映射。
这就是为什么当启用日志记录时,mongod 使用的虚拟内存量会翻倍。参见图 8-17 。
图 8-17。
maps to private view
您可以在图 8-17 中看到数据文件如何不直接连接到私有视图,因此操作系统不会将私有视图中的更改刷新到磁盘。
让我们来看看启动写操作时会发生什么样的事件序列。当启动写操作时,它首先写入私有视图(图 8-18 )。
图 8-18。
Initiated write operation
接下来,将更改写入日志文件,附加文件中更改内容的简要描述(图 8-19 )。
图 8-19。
Updating the journal file
当日志获得更改时,它会不断追加更改描述。如果 mongod 在这一点上失败了,那么即使数据文件还没有被修改,日志也可以重放所有的更改,从而使写操作在这一点上是安全的。
日志现在将在共享视图上重放记录的更改(图 8-20 )。
图 8-20。
Updating the shared view
最后,以非常快的速度将更改写入磁盘。默认情况下,mongod 要求操作系统每 60 秒执行一次(图 8-21 )。
图 8-21。
Updating the data file
最后一步,mongod 将共享视图重新映射到私有视图。这样做是为了防止私人视图变得太脏(图 8-22 )。
图 8-22。
Remapping
8.6 grid fs–MongoDB 文件系统
你看到了引擎盖下发生的事情。您看到了 MongoDB 将数据存储在 BSON 文档中。BSON 文档的文档大小限制为 16MB。
GridFS 是 MongoDB 处理超过 BSON 文档大小限制的大文件的规范。本节将简要介绍 GridFS。
这里的“规范”是指它本身不是 MongoDB 的特性,所以 MongoDB 中没有实现它的代码。它只是指定需要处理多大的文件。PHP、Python 等语言驱动。实现该规范并向该驱动程序的用户公开一个 API,使他们能够在 MongoDB 中存储/检索大文件。
8 . 6 . 1 grid fs 的基本原理
按照设计,MongoDB 文档(即 BSON 对象)不能大于 16MB。这是为了将性能保持在最佳水平,并且大小非常适合我们的需求。例如,4MB 的空间可能足以存储声音剪辑或个人资料图片。但是,如果需要存储高质量的音频或电影剪辑,甚至是超过几百兆字节大小的文件,MongoDB 可以通过使用 GridFS 满足您的需求。
GridFS 指定了一种在多个文档中划分大文件的机制。实现它的语言驱动程序,例如 PHP 驱动程序,负责存储文件的拆分(或者在检索文件时合并拆分的块)。使用驱动程序的开发者不需要知道这些内部细节。这样,GridFS 允许开发者以透明和高效的方式存储和操作文件。
GridFS 使用两个集合来存储文件。一个集合维护文件的元数据,另一个集合通过将文件分成称为块的小块来存储文件的数据。这意味着文件被分成更小的块,每个块存储为一个单独的文档。默认情况下,块大小限制为 255KB。
这种方法不仅使数据的存储变得可伸缩和容易,而且当检索文件的特定部分时,使范围查询更容易使用。
每当在 GridFS 中查询一个文件时,数据块都会按照客户机的要求重新组合。这也为用户提供了访问文件任意部分的能力。例如,用户可以直接移动到视频文件的中间。
GridFS 规范在文件大小超过 MongoDB BSON 文档默认的 16MB 限制的情况下非常有用。它还用于存储您需要访问的文件,而无需将整个文件加载到内存中。
8.6.2 发动机罩下的格栅
GridFS 是一种用于存储文件的轻量级规范。
对于 GridFS 请求,在 MongoDB 服务器上没有“特殊情况”处理。所有的工作都在客户端完成。
GridFS 使您能够通过将大文件分割成较小的块并将每个块存储为单独的文档来存储大文件。除了这些块之外,还有一个包含文件元数据的文档。使用这些元数据信息,块被分组在一起,形成完整的文件。
块的存储开销可以保持最小,因为 MongoDB 支持在文档中存储二进制数据。
GridFS 用于存储大文件的两个集合默认命名为fs.files和fs.chunks,尽管可以选择不同于fs的桶名。
默认情况下,块存储在fs.chunks集合中。如果需要,这可以被覆盖。因此,所有数据都包含在fs.chunks集合中。
chunks 集合中各个文档的结构非常简单:
{
"_id" : ObjectId("..."),"n" : 0,"data" : BinData("..."),
"files_id" : ObjectId("...")
}
chunk 文档有以下重要的键。
"_id":这是唯一的标识符。"files_id":这是包含与块相关的元数据的文档的唯一标识符。"n":这基本上描述了块在原始文件中的位置。"data":这是构成这个块的实际二进制数据。
fs.files集合存储每个文件的元数据。这个集合中的每个文档代表 GridFS 中的一个文件。除了常规元数据信息之外,集合中的每个文档都可以包含特定于它所代表的文件的自定义元数据。
以下是 GridFS 规范规定的键:
_id:这是文件的唯一标识符。Length:描述了组成文件完整内容的总字节数。chunkSize:这是文件的块大小,以字节为单位。默认情况下,它是 255KB,但如果需要,这可以调整。uploadDate:这是文件存储在 GridFS 中的时间戳。md5:在服务器端生成,是文件内容的 md5 校验和。MongoDB 服务器通过使用filemd5命令生成它的值,该命令计算上传块的 md5 校验和。这意味着用户可以检查该值以确保文件被正确上传。
典型的fs.files文件如下所示(参见图 8-23 ):
图 8-23。
GridFS
{
"_id" : ObjectId("..."), "length" : data_number,
"chunkSize" : data_number,
"uploadDate" : data_date,
"md5" : data_string
}
使用 GridFS
在本节中,您将使用 PyMongo 驱动程序来了解如何开始使用 GridFS。
添加对文件系统的引用
首先需要的是对 GridFS 文件系统的引用:
>>> import pymongo
>>> import gridfs
>>>myconn=pymongo.Connection()
>>>mydb=myconn.gridfstest
>>>myfs=gridfs.GridFS(db)
写( )
接下来,您将执行一个基本写操作:
>>> with myfs.new_file() as myfp:
myfp.write('This is my new sample file. It is just grand!')
查找( )
使用 mongo shell,让我们看看底层集合包含什么:
>>> list(mydb.myfs.files.find())
[{u'length': 38, u'_id': ObjectId('52fdd6189cd2fd08288d5f5c'), u'uploadDate': datetime.datetime(2014, 11, 04, 4, 20, 41, 800000), u'md5': u'332de5ca08b73218a8777da69293576a', u'chunkSize': 262144}]
>>> list(mydb.myfs.chunks.find())
[{u'files_id': ObjectId('52fdd6189cd2fd08288d5f5c'), u'_id': ObjectId('52fdd6189cd2fd08288d5f5d'), u'data': Binary('This is my new sample file. It is just grand!', 0), u'n': 0}]
强制分割文件
让我们强制分割文件。这是通过在创建文件时指定一个小的 chunkSize 来实现的,如下所示:
>>> with myfs.new_file(chunkSize=10) as myfp:
myfp.write('This is second file. I am assuming it will be split into various chunks')
>>>
>>>myfp
<gridfs.grid_file.GridIn object at 0x0000000002AC79B0>
>>>myfp._id
ObjectId('52fdd76e9cd2fd08288d5f5e')
>>> list(mydb.myfs.chunks.find(dict(files_id=myfp._id)))
.................
ObjectId('52fdd76e9cd2fd08288d5f65'), u'data': Binary('s', 0), u'n': 6}]
阅读( )
您现在知道了文件实际上是如何存储在数据库中的。接下来,使用客户端驱动程序,您现在将读取该文件:
>>> with myfs.get(myfp._id) as myfp_read:
print myfp_read.read()
“这是第二份文件。我假设它会被分成不同的块。”
用户根本不需要知道组块。您需要使用客户端公开的 API 从 GridFS 中读取和写入文件。
8.6.3.1 把 GridFS 看得更像一个文件系统
new_file( ) -在 GridFS 中创建新文件
您可以将任意数量的关键字作为参数传递给new_file()。这将被添加到fs.files文档中:
>>> with myfs.new_file(
filename='practicalfile.txt',
content_type='text/plain',
my_other_attribute=42) as myfp:
myfp.write('My New file')
>>>myfp
<gridfs.grid_file.GridIn object at 0x0000000002AC7AC8>
>>> db.myfs.files.find_one(dict(_id=myfp._id))
{u'contentType': u'text/plain', u'chunkSize': 262144, u'my_other_attribute': 42, u'filename': u'practicalfile.txt', u'length': 8, u'uploadDate': datetime.datetime(2014, 11, 04, 9, 01, 32, 800000), u'_id': ObjectId('52fdd8db9cd2fd08288d5f66'), u'md5': u'681e10aecbafd7dd385fa51798ca0fd6'}
>>>
可以使用文件名覆盖文件。由于_id用于索引 GridFS 中的文件,旧文件不会被删除。只维护一个文件版本。
>>> with myfs.new_file(filename='practicalfile.txt', content_type='text/plain') as myfp:
myfp.write('Overwriting the "My New file"')
get_version( )/get_last_version()
在上述情况下,可以使用get_version或get_last_version来检索带有文件名的文件。
>>>myfs.get_last_version('practicalfile.txt').read()
'Overwriting the "My New file"'
>>>myfs.get_version('practicalfile.txt',0).read()
'My New file'
您还可以在 GridFS 中列出文件:
>>>myfs.list()
[u'practicalfile.txt', u'practicalfile2.txt']
删除( )
也可以删除文件:
>>>myfp=myfs.get_last_version('practicalfile.txt')
>>>myfs.delete(myfp._id)
>>>myfs.list()
[u'practicalfile.txt', u'practicalfile2.txt']
>>>myfs.get_last_version('practicalfile.txt').read()
'My New file'
>>>
请注意,只有一个版本的practicalfile.txt被删除。文件系统中仍然有一个名为practicalfile.txt的文件。
exists()和 put()
接下来,您将使用exists()检查文件是否存在,并使用put()将一个短文件快速写入 GridFS:
>>>myfs.exists(myfp._id)
False
>>>myfs.exists(filename='practicalfile.txt')
True
>>>myfs.exists({'filename':'practicalfile.txt'}) # equivalent to above
True
>>>myfs.put('The red fish', filename='typingtest.txt')
ObjectId('52fddbc69cd2fd08288d5f6a')
>>>myfs.get_last_version('typingtest.txt').read()
'The red fish'
>>>
8.7 索引
在本书的这一部分中,您将简要地研究一下什么是 MongoDB 上下文中的索引。接下来,我们将强调 MongoDB 中可用的各种类型的索引,通过强调行为和限制来结束本节。
索引是一种加速读取操作的数据结构。通俗地说,它相当于书籍索引,通过在索引中查找章节并直接跳转到页码,而不是扫描整本书来找到章节,如果没有索引,就会出现这种情况。
类似地,在字段上定义索引,这可以帮助以更好和更有效的方式搜索信息。
和其他数据库一样,在 MongoDB 中也有类似的感觉(用于加速find ()操作)。您运行的查询类型有助于为数据库创建有效的索引。例如,如果大多数查询使用日期字段,那么在日期字段上创建索引将会很有好处。确定哪个索引最适合您的查询可能很棘手,但值得一试,因为如果有合适的索引,否则需要几分钟的查询将立即返回结果。
在 MongoDB 中,可以在文档的任何字段或子字段上创建索引。在查看可以在 MongoDB 中创建的各种类型的索引之前,让我们列出一些索引的核心特性:
- 索引是在每个集合级别定义的。对于每个集合,都有不同的索引集。
- 与 SQL 索引一样,MongoDB 索引也可以在单个字段或一组字段上创建。
- 在 SQL 中,尽管索引提高了查询性能,但是每次写操作都会产生开销。因此,在创建任何索引之前,都要考虑查询的类型、频率、工作负载的大小、插入负载以及应用需求。
- 所有 MongoDB 索引都使用 BTree 数据结构。
- 每个使用更新操作的查询只使用一个索引,这是由查询优化器决定的。这可以通过使用提示来覆盖。
- 如果所有字段都是索引的一部分,则称查询被索引覆盖,而不管它是用于查询还是用于投影。
- 覆盖索引最大化了 MongoDB 的性能和吞吐量,因为只使用索引就可以满足查询,而无需在内存中加载完整的文档。
- 只有在创建索引的字段发生变化时,索引才会更新。并非所有对文档的更新操作都会导致索引发生变化。只有当关联字段受到影响时,它才会被更改。
索引的类型
在这一节中,您将看到 MongoDB 中可用的不同类型的索引。
8.7.1.1 _ id 索引
这是在_id字段上创建的默认索引。无法删除该索引。
8.7.1.2 二级索引
用户在 MongoDB 中使用ensureIndex()创建的所有索引都被称为二级索引。
These indexes can be created on any field in the document or the sub document. Let’s consider the following document: {"_id": ObjectId(...), "name": "Practical User", "address": {"zipcode": 201301, "state": "UP"}} In this document, an index can be created on the name field as well as the state field. These indexes can be created on a field that is holding a sub-document. If you consider the above document where address is holding a sub-document, in that case an index can be created on the address field as well. These indexes can either be created on a single field or a set of fields. When created with set of fields, it’s also termed a compound index. To explain it a bit further, let’s consider a products collection that holds documents of the following format: { "_id": ObjectId(...),"category": ["food", "grocery"], "item": "Apple", "location": "16 th Floor Store", "arrival": Date(...)} If the maximum of the queries use the fields Item and Location, then the following compound index can be created: db.products.ensureIndex ({"item": 1, "location": 1}) In addition to the query that is referring to all the fields of the compound index, the above compound index can also support queries that are using any of the index prefixes (i.e. it can also support queries that are using only the item field). If the index is created on a field that holds an array as its value, then a multikey index is used for indexing each value of the array separately. Consider the following document: { "_id" : ObjectId("..."),"tags" : [ "food", "hot", "pizza", "may" ] } An index on tags is a multikey index, and it will have the following entries: { tags: "food" } { tags: "hot" } { tags: "pizza" } { tags: "may" } Multikey compound indexes can also be created. However, at any point, only one field of the compound index can be of the array type. If you create a compound index of {a1: 1, b1: 1}, the permissible documents are as follows: {a1: [1, 2], b1: 1} {a1: 1, b1: [1, 2]} The following document is not permissible; in fact, MongoDB won’t be even able to insert this document: {a1: [21, 22], b1: [11, 12]} If an attempt is made to insert such a document, the insertion will be rejected and the following error results will be produced: “cannot index parallel arrays”.
接下来,您将看到在创建索引时可能有用的各种选项/属性。
带有键排序的索引
MongoDB 索引维护对字段的引用。引用以升序或降序维护。这是通过在创建索引时用键指定一个数字来实现的。该数字表示索引方向。可能的选项有 1 和-1,其中 1 代表升序,-1 代表降序。
在单个键索引中,它可能不太重要;但是,方向在复合索引中非常重要。
考虑一个包含了username和timestamp的Events集合。您的查询是首先返回按username排序的事件,然后首先返回最近的事件。将使用以下索引:
db.events.ensureIndex({ "username" : 1, "timestamp" : -1 })
该索引包含对按以下方式排序的文档的引用:
First by the username field in ascending order. Then for each username sorted by the timestamp field in the descending order.
唯一索引
创建索引时,需要确保存储在索引字段中的值的唯一性。在这种情况下,您可以将Unique属性设置为 true(默认为 false)来创建索引。
说你想要一个unique_index在userid场上。可以运行以下命令来创建唯一索引:
db.payroll.ensureIndex( { "userid": 1 }, { unique: true } )
该命令确保在user_id字段中有唯一的值。对于唯一性约束,您需要注意以下几点
- 如果在这种情况下对复合索引使用 unique 约束,则值的组合必须具有唯一性。
- 如果没有为唯一索引的字段指定值,则存储空值。
- 在任何时候,只允许一个没有唯一值的文档。
dropDups
如果您正在已有文档的集合上创建唯一索引,创建可能会失败,因为您可能有一些文档在索引字段中包含重复值。在这种情况下,dropDups选项可用于强制创建唯一索引。这是通过保留第一次出现的键值并删除所有后续值来实现的。默认情况下dropDups为假。
稀疏索引
稀疏索引是保存集合中文档条目的索引,该集合具有创建索引的字段。如果您想要在User集合的LastName字段上创建一个稀疏索引,可以发出以下命令:
db.User.ensureIndex( { "LastName": 1 }, { sparse: true } )
该索引将包含以下文档
{FirstName: Test, LastName: User}
or
{FirstName: Test2, LastName: }
但是,以下文档将不属于稀疏索引:
{FirstName: Test1}
之所以说索引是稀疏的,是因为它只包含带有索引字段的文档,并且当字段丢失时会丢失文档。由于这一特性,稀疏索引可以显著节省空间。
相反,非稀疏索引包括所有文档,而不管索引字段在文档中是否可用。如果字段丢失,则存储空值。
TTL 索引(生存时间)
版本 2.2 中引入了一个新的 index 属性,使您能够在指定的时间段过后自动从集合中删除文档。该属性非常适合于日志、会话信息和机器生成的事件数据等场景,在这些场景中,数据只需要在有限的时间内保持持久。
如果您想在集合logs上设置一个小时的 TTL,可以使用以下命令:
db.Logs.ensureIndex( { "Sample_Time": 1 }, { expireAfterSeconds: 3600} )
但是,您需要注意以下限制:
- 创建索引的字段只能是日期类型。在上面的例子中,字段
sample_time必须保存日期值。 - 它不支持复合索引。
- 如果被索引的字段包含具有多个日期的数组,则当数组中最小的日期与过期阈值匹配时,文档过期。
- 不能在已经创建了索引的字段上创建它。
- 无法在上限集合上创建此索引。
- TTL 索引使用后台任务使数据过期,该任务每分钟运行一次,以删除过期的文档。因此您不能保证过期的文档不再存在于集合中。
地理空间索引
随着智能手机的兴起,查询当前位置附近的事物变得越来越常见。为了支持这种基于位置的查询,MongoDB 提供了地理空间索引。
要创建地理空间索引,文档中必须存在以下形式的坐标对:
- 要么是包含两个元素的数组
- 或者具有两个密钥的嵌入式文档(密钥名称可以是任何名称)。
以下是有效的例子:
{ "userloc" : [ 0, 90 ] }
{ "loc" : { "x" : 30, "y" : -30 } }
{ "loc" : { "latitude" : -30, "longitude" : 180 } }
{"loc" : {"a1" : 0, "b1" : 1}}.
以下内容可用于在userloc字段创建地理空间索引:
db.userplaces.ensureIndex( { userloc : "2d" } )
默认情况下,地理空间索引假定值的范围为-180 到 180。如果需要更改,可与ensureIndex一起指定,如下所示:
db.userplaces.ensureIndex({"userloc" : "2d"}, {"min" : -1000, "max" : 1000})
任何超过最大值和最小值的文档都将被拒绝。您还可以创建复合地理空间索引。
让我们用一个例子来理解这个索引是如何工作的。假设您有以下类型的文档:
{"loc":[0,100], "desc":"coffeeshop"}
{"loc":[0,1], "desc":"pizzashop"}
如果用户的查询是找到她所在位置附近的所有咖啡店,下面的复合索引会有所帮助:
db.ensureIndex({"userloc" : "2d", "desc" : 1})
地质干草堆索引
Geohaystack 索引是基于桶的地理空间索引(也称为地理空间 haystack 索引)。它们对于需要找出小区域内的位置并且还需要沿着另一个维度进行过滤的查询非常有用,例如查找坐标在 10 英里以内并且类型字段值为restaurant的文档。
在定义索引时,必须指定bucketSize参数,因为它决定了 haystack 索引的粒度。例如,
db.userplaces.ensureIndex({ userpos : "geoHaystack", type : 1 }, { bucketSize : 1 })
这个例子创建了一个索引,其中纬度或经度 1 个单位内的关键字一起存储在同一个桶中。您还可以在索引中包含一个附加类别,这意味着在查找位置详细信息的同时将查找信息。
如果您的用例通常搜索“附近”的位置(即“25 英里内的餐馆”),那么干草堆索引可能更有效。
可以在每个桶中找到并计算附加索引字段(例如类别)的匹配。
相反,如果您正在搜索“最近的餐馆”,并且希望不管距离远近都返回结果,那么普通的 2d 索引会更有效。
目前(从 MongoDB 2.2.0 开始)对 haystack 索引有一些限制:
- 干草堆索引中只能包含一个附加字段。
- 附加索引字段必须是单个值,而不是数组。
- 不支持空的经度/纬度值。
除了上面提到的类型,2.4 版中还引入了一种新的索引类型,它支持对集合进行文本搜索。
之前在 beta 中,在 2.6 版本中,文本搜索是一个内置的特性。它包括 15 种语言的搜索选项和一个聚合选项,可用于在电子商务网站上按产品或颜色设置分面导航。
8.7.1.3 索引交叉点
版本 2.6 中引入了索引交集,其中多个索引可以相交以满足一个查询。为了进一步解释,让我们考虑一个包含以下格式文档的产品集合
{ "_id": ObjectId(...),"category": ["food", "grocery"], "item": "Apple", "location": "16th
让我们进一步假设这个集合有以下两个索引:
{ "item": 1 }.
{ "location": 1 }.
以上两个索引的交集可用于以下查询:
db.products.find ({"item": "xyz", "location": "abc"})
您可以运行explain()来确定索引交集是否用于上述查询。解释输出将包括以下阶段之一:AND_SORTED 或 AND_HASH。进行索引交集时,可以使用整个索引,也可以只使用索引前缀。
接下来您需要理解这个索引交集特性如何影响复合索引的创建。
创建复合索引时,索引中键的排列顺序和排序顺序(升序和降序)都很重要。因此,复合索引可能不支持没有索引前缀或具有不同排序顺序的键的查询。
为了进一步解释,让我们考虑一个具有以下复合索引的产品集合:
db.products.ensureIndex ({"item": 1, "location": 1})
除了引用复合索引的所有字段的查询之外,上述复合索引还可以支持使用任何索引前缀的查询(它还可以支持仅使用 item 字段的查询)。但是它不能支持只使用位置字段或者使用具有不同排序顺序的项目关键字的查询。
相反,如果您创建两个单独的索引,一个关于项目,另一个关于位置,这两个索引可以单独或通过交叉支持上面提到的四个查询。因此,是创建复合索引还是依赖索引交集的选择取决于系统的需求。
注意,当sort()操作需要一个完全独立于查询谓词的索引时,索引交集将不适用。
例如,假设产品集合有以下索引:
{ "item": 1 }.
{ "location": 1 }.
{ "location": 1, "arrival_date":-1 }.
{ "arrival_date": -1 }.
索引交集将不会用于以下查询:
db.products.find( { item: "xyz"} ).sort( { location: 1 } )
也就是说,MongoDBwill 不会使用{ item: 1 }索引进行查询,而使用单独的{ location: 1 }或{ location: 1, arrival_date: -1 }索引进行排序。
但是,索引交集可以用于以下查询,因为索引{location: 1,arrival_date: -1 }可以满足部分查询谓词:
db.products.find( { item: { "xyz"} , location: "A" } ).sort( { arrival_date: -1 } )
行为和限制
最后,以下是您需要了解的一些行为和限制:
- 集合中不允许超过 64 个索引。
- 索引键不能大于 1024 字节。
- 如果文档的字段值大于此大小,则无法对文档进行索引。
- 以下命令可用于查询太大而无法索引的文档:
db.practicalCollection.find({<key>: <too large to index>}).hint({$natural: 1})
- 索引名(包括名称空间)必须少于 128 个字符。
- 插入/更新速度在一定程度上受到索引的影响。
- 不要维护没有使用或不会使用的索引。
- 由于
$or查询的每个子句并行执行,每个子句可以使用不同的索引。 - 使用
sort()方法和$or运算符的查询将无法使用$or字段上的索引。 - 第二个地理空间查询不支持使用
$or运算符的查询。
8.8 摘要
在这一章中,你讲述了数据是如何存储的,以及写操作是如何使用日志的。您还了解了 GridFS 和 MongoDB 中可用的不同类型的索引。
在下一章中,您将从管理的角度来看 MongoDB。
九、管理 MongoDB
“管理 MongoDB 不同于管理传统的 RDBMS 数据库。尽管大多数管理任务不是必需的,或者是由系统自动完成的,但仍有少数任务需要人工干预。”
在本章中,您将了解备份和恢复、导入和导出数据、管理服务器以及监控数据库实例的基本管理操作过程。
9.1 管理工具
在开始管理任务之前,这里有一个工具的快速概述。由于 MongoDB 没有 GUI 风格的管理界面,大多数管理任务都是使用命令行 mongo shell 来完成的。然而,一些 ui 可以作为单独的社区项目使用。
蒙哥语
mongo shell 是 MongoDB 发行版的一部分。这是一个用于 MongoDB 数据库的交互式 JavaScript shell。它为管理员和开发者直接使用数据库测试查询和操作提供了一个强大的接口。
在前面的章节中,您介绍了使用 shell 进行开发。在本章中,您将使用 shell 完成系统管理任务。
9.1.2 第三方管理工具
MongoDB 提供了许多第三方工具。大多数工具都是基于网络的。
10gen 在 MongoDB 网站 https://docs.mongodb.org/ecosystem/tools/administration-interfaces/ 上维护了所有支持 MongoDB 的第三方管理工具的列表。
9.2 备份和恢复
备份是最重要的管理任务之一。它确保数据是安全的,并在任何紧急情况下可以恢复回来。
如果数据无法恢复,备份是没有用的。因此,在进行备份后,管理员需要确保备份的格式可用,并且以一致的状态捕获数据。
管理员需要学习的第一项技能是如何进行备份和恢复。
数据文件备份
备份数据库最简单的方法是将数据复制到数据目录文件夹中。
所有的 MongoDB 数据都存储在一个数据目录中,默认情况下是C:\data\db(在 Windows 中)或/data/db(在 LINUX 中)。启动 mongod 时,可以使用–dbpath选项将默认路径更改为不同的目录。
数据目录内容是存储在 MongoDB 数据库中的数据的完整图片。因此,备份 MongoDB 只是复制数据目录文件夹的全部内容。
通常,在 MongoDB 运行时复制数据目录内容是不安全的。一种选择是在复制数据目录内容之前关闭 MongoDB 服务器。
如果服务器正常关闭,数据目录的内容代表 MongoDB 数据的安全快照,因此可以在服务器重新启动之前复制它。
尽管这是一种安全有效的备份方式,但它不是一种理想的方式,因为它需要停机。
接下来,您将讨论不需要停机的备份技术。
9.2.2 mongodump 和 mongorestore
mongodump 是作为 MongoDB 发行版的一部分提供的 MongoDB 备份实用程序。它通过查询一个 MongoDB 实例并将所有读取的文档写入磁盘,作为一个普通的客户机工作。
让我们执行备份,然后恢复它,以验证备份的格式是否可用且一致。
以下代码片段来自在 Windows 平台上运行实用程序。MongoDB 服务器运行在 localhost 实例上。
打开终端窗口,输入以下命令:
C:\> cd c:\practicalmongodb\bin
c:\practicalmongodb\bin> mongod --rest
2015-07-15T22:26:47.288-0700 I CONTROL [initandlisten] MongoDB starting : pid=3820 port=27017 dbpath=c:\data\db\ 64-bit host=ANOC9
.....................................................................................
2015-07-15T22:28:23.563-0700 I NETWORK [websvr] admin web console waiting for connections on port 28017
为了运行 mongodump,请在新的终端窗口中执行以下命令:
C:\> cd c:\practicalmongodb\bin
c:\practicalmongodb\bin> mongodump
2015-07-15T22:29:41.538-0700 writing admin.system.indexes to dump\admin\system.indexes.bson
................................
2015-07-14T22:29:46.720-0700 writing mydbproc.users to dump\mydbproc\users.bson
c:\practicalmongodb\bin>
这会将整个数据库转储到bin文件夹目录下的dump文件夹下,如图 9-1 所示。
图 9-1。
The dump folder
默认情况下,mongodump 实用程序在默认端口上连接到数据库的 localhost 接口。
接下来,它将每个数据库和集合的相关数据文件提取并存储到一个预定义的文件夹结构中,默认为./dump/[databasename]/[collectionname].bson。
数据以.bson格式保存,这种格式类似于 MongoDB 在内部存储数据时使用的格式。
如果内容已经在目录中,它将保持不变,除非转储包含相同的文件。例如,如果转储包含文件c1.bson和c2.bson,而输出目录有文件c3.bson和c1.bson,那么 mongodump 会用它的c1.bson文件替换文件夹的c1.bson文件,并会复制c2.bson文件,但不会删除或更改c3.bson文件。
除非您需要在备份中覆盖数据,否则在将目录用于 mongodump 之前,您应该确保该目录是空的。
9.2.2.1 单一数据库备份
在上面的示例中,您使用默认设置执行了 mongodump,这将转储 MongoDB 数据库服务器上的所有数据库。
在现实生活中,您将在一台服务器上运行多个应用数据库,每个数据库都有不同的备份策略要求。
在 mongodump 实用程序中指定–d参数将允许您明智地备份数据库。
c:\practicalmongodb\bin> mongodump -d mydbpoc
2015-07-14T22:37:49.088-0700 writing mydbproc.mapreducecount1 to dump\mydbproc\ mapreducecount1.bson
......................
2015-07-14T22:37:54.217-0700 writing mydbproc.users metadata to dump\mydbproc\users.metadata.json
2015-07-14T22:37:54.218-0700 done dumping mydbproc.users
c:\practicalmongodb\bin>
从 MongoDB-2.6 开始,数据库管理员必须能够访问管理数据库,以便为给定的数据库备份用户和用户定义的角色,因为 MongoDB 只将这些信息存储在管理数据库中。
9.2.2.2 集合级备份
每个数据库中有两种类型的数据:很少更改的数据,例如维护用户、用户角色和任何与应用相关的配置的配置数据,还有经常更改的数据,例如事件数据(对于监控应用)、帖子数据(对于博客应用)等等。
因此,备份要求是不同的。例如,完整的数据库可以每周备份一次,而快速变化的集合需要每小时备份一次。
在 mongodump 实用程序中指定–c参数使用户能够为指定的集合单独实现备份。
c:\practicalmongodb\bin> mongodump -d mydbpoc -c users
2015-07-14T22:41:19.850-0700 writing mydbproc.users to dump\mydbproc\users.bson
2015-07-14T22:41:30.710-0700 writing mydbproc.users metadata to dump\mydbproc\users.metadata.json
...........................................................
2015-07-14T22:41:30.712-0700 done dumping mydbproc.users
c:\practicalmongodb\bin>
如果没有指定需要转储数据的文件夹,默认情况下,它会将数据转储到当前工作目录中名为dump的目录中,在本例中是c:\practicalmongodb\bin。
9.2.2.3 蒙古垃圾场——救命
您已经了解了执行 mongodump 的基础知识。除了上面提到的选项,mongodump 还提供了其他选项,让您可以根据需要定制备份。与所有其他实用程序一样,使用–help选项执行实用程序将提供所有可用选项的列表。
9.2.2.4 蒙古恢复
如上所述,管理员必须确保备份以一致且可用的格式进行。所以下一步是使用 mongorestore 恢复数据转储。
该实用程序会将数据库恢复到进行转储时的状态。在 3.0 版本之前,甚至不需要启动 mongod/mongos 就可以运行该命令。从版本 3.0 开始,如果在启动 mongod/mongos 之前执行该命令,将显示以下错误:
c:\>``cd
c:\ practicalmongodb\bin> mongorestore
2015-07-15T22:43:07.365-0700 using default 'dump' directory
2015-07-15T22:43:17.545-0700 Failed: error connecting to db server: no reachable servers
在运行mongorestore命令之前,您必须运行 mongod/mongos 实例。
c:\>``cd
c:\ practicalmongodb\bin> mongod --rest
2015-07-15T22:43:25.765-0700 I CONTROL [initandlisten] MongoDB starting : pid=3820 port=27017 dbpath=c:\data\db\ 64-bit host=ANOC9
.....................................................................................
2015-07-15T22:43:25.865-0700 I NETWORK [websvr] admin web console waiting for connections on port 28017
c:\ practicalmongodb\bin> mongorestore
2015-07-15T22:44:09.786-0700 using default 'dump' directory
2015-07-15T22:44:09.792-0700 building a list of dbs and collections to restore from dump dir
...................................
2015-07-15T22:44:09.732-0700 restoring indexes for collection mydbproc.users from metadata
2015-07-15T22:44:09.864-0700 finished restoring mydbproc.users
c:\practicalmongodb\bin>
该力将数据追加到现有数据的后面。
要覆盖默认行为,应该在上面的代码片段中使用–drop。
–drop命令向 mongorestore 实用程序指出,它需要删除上述数据库中的所有集合和数据,然后将转储数据恢复到数据库中。
如果不使用–drop,该命令会将数据附加到现有数据的末尾。
注意,从版本 3.0 开始,mongorestore命令也可以接受来自标准输入的输入。
9.2.2.5 恢复单个数据库
正如您在备份一节中看到的,备份策略可以在单个数据库级别指定。您可以通过使用–d选项运行mongodump来备份单个数据库。
类似地,您可以将–d选项指定给mongorestore来恢复单个数据库。
c:\ practicalmongodb\bin> mongorestore -d mydbpocc:\practicalmongodb\bin\dump\mydbproc -drop
2015-07-14T22:47:01.155-0700 building a list of collections to restore from C :\practicalmongodb\bin\dump\mydbproc dir
2015-07-14T22:47:01.156-0700 reading metadata file from C :\practicalmongodb\bin\dump\mydbproc \users.metadata.json
..........................................................................
2015-07-14T22:50:09.732-0700 restoring indexes for collection mydbproc.users from metadata
2015-07-14T22:50:09.864-0700 finished restoring mydbproc.users
c:\practicalmongodb\bin>
9.2.2.6 修复单一系列
对于 mongodump,您可以使用–c选项来指定集合级备份,您也可以通过使用 mongorestore 实用程序的–c选项来恢复单个集合。
c:\ practicalmongodb\bin> mongorestore -d mydbpoc -c users C:\ practicalmongodb\bin\dum\mydb\user.bson -drop
2015-07-14T22:52:14.732-0700 restoring indexes for collection mydbproc.users from metadata
2015-07-14T22:52:14.864-0700 finished restoring mydbproc.users
c:\practicalmongodb\bin>
9.2.2.7 蒙古恢复——救命
mongorestore 也有多个选项,可以使用–help选项查看。还可以参考以下网站: http://docs.mongodb.org/manual/core/backups/ 。
fsync 和锁定
虽然上面的两种方法(mongodump 和 mongorestore)使您能够在不停机的情况下进行数据库备份,但是它们不提供获得时间点数据视图的能力。
您了解了如何复制数据文件来进行备份,但是这需要在复制数据之前关闭服务器,这在生产环境中是不可行的。
MongoDB 的fsync命令允许您通过运行 MongoDB 复制数据目录的内容,而不改变任何数据。
fsync命令强制将所有挂起的写入刷新到磁盘。或者,它持有一个锁,以防止进一步的写入,直到服务器解锁。这个锁只使fsync命令可用于备份。
要从 shell 运行该命令,请在新的终端窗口中连接到 mongo 控制台。
c:\practicalmongodb\bin> mongo
MongoDB shell version: 3.0.4
connecting to: test
>
接下来,切换到 admin 并发出runCommand到fsync:
> use admin
switched to db admin
> db.runCommand({"fsync":1, "lock":1})
{
"info" : "now locked against writes, use db.fsyncUnlock() to unlock",
"seeAlso" : "http://dochub.mongodb.org/core/fsynccommand
"ok" : 1
}
>
此时,对于任何写入,服务器都是锁定的,确保数据目录代表数据的一致的时间点快照。可以安全地复制数据目录内容以用作数据库备份。
您必须在备份活动完成后解锁数据库。为此,发出以下命令:
> db.$cmd.sys.unlock.findOne()
{ "ok" : 1, "info" : "unlock completed" }
>
currentOp命令可以用来检查数据库锁是否已经被释放。
> db.currentOp()
{ "inprog" : [ ] }
(It may take a moment after the unlock is first requested.)
fsync命令允许您在不停机的情况下进行备份,并且不会牺牲备份的时间点特性。但是,会有短暂的写入阻塞(也称为短暂的写入停机时间)。
从 3.0 版本开始,使用 WiredTiger 时,fsync无法保证数据文件不会发生变化。因此它不能用于确保创建备份的一致性。
接下来,您将了解从属备份。这是唯一能够在不停机的情况下拍摄时间点快照的备份技术。
从属备份
从备份是 MongoDB 中推荐的数据备份方式。从机总是存储与主机几乎同步的数据副本,从机的可用性或性能不是大问题。您可以在从服务器而不是主服务器上应用前面讨论过的任何技术:关机、fsync带锁,或者转储和恢复。
9.3 进口和出口
当您试图将应用从一个环境迁移到另一个环境时,通常需要导入或导出数据。
蒙古进口
MongoDB 提供了 mongoimport 实用程序,允许您将数据直接批量装载到数据库集合中。它从文件中读取数据,并将数据大容量加载到集合中。
这些方法不适合生产环境。
mongoimport 支持以下三种文件格式:
- JSON:在这种格式中,每行都有 JSON 块,代表一个文档。
- CSV:这是一个逗号分隔的文件。
- TSV: TSV 文件与 CSV 文件相同;唯一的区别是它使用制表符作为分隔符。
将–help与mongoimport一起使用将提供该实用程序可用的所有选项。
mongoimport 非常简单。大多数情况下,您最终会使用以下选项:
-h或–host:指定数据需要恢复到的 mongod 主机名。如果没有指定该选项,默认情况下,该命令将连接到本地主机上运行的 mongod 的端口 27017。或者,可以指定一个端口号来连接到运行在不同端口上的 mongod。-d或–db:指定需要导入数据的数据库。-c或–collection:指定需要上传数据的集合。--type:这是文件类型(即 CSV、TSV 或 JSON)。--file:这是需要导入数据的文件路径。--drop:如果设置了此选项,将删除集合,并从导入的数据中重新创建集合。否则,数据将追加到集合的末尾。--headerLine:仅用于 CSV 或 TSV 文件,用于表示第一行是标题行。
以下命令将数据从 CSV 文件导入到本地主机上的testimport集合中:
c:\practicalmongodb\bin> mongoimport --host localhost --db mydbpoc --collection testimport --type csv –file c:\exporteg.csv –-headerline
2015-07-14T22:54:08.407-0700 connected to: localhost
2015-07-14T22:54:08.483-0700 imported 15 documents
c:\ practicalmongodb\bin>
蒙古出口
与 mongoimport 实用程序类似,MongoDB 提供了一个 mongoexport 实用程序,允许您从 MongoDB 数据库中导出数据。顾名思义,这个实用程序从现有的 MongoDB 集合中导出文件。
使用–help显示了 mongoexport 实用程序的可用选项。以下选项是您最终最常使用的选项:
-q:用于指定将需要导出的记录作为输出返回的查询。这类似于当您必须检索匹配选择标准的记录时,您在db.CollectionName.find()函数中指定的内容。如果没有指定查询,则导出所有文档。-f:用于指定所选单据中需要导出的字段。
以下命令将数据从Users集合导出到 CSV 文件:
c:\practicalmongodb\bin> mongoexport -d mydbpoc -c myusers -f _id,Age –type=csv > myusers.csv
2015-07-14T22:54:48.604-0700 connected to: 127.0.0.1
2015-07-14T22:54:48.604-0700 exported 22 records
c:\practicalmongodb\bin>
9.4 管理服务器
在本节中,您将看到作为系统管理员需要了解的各种选项。
9.4.1 启动服务器
本节介绍如何启动服务器。之前,您使用 mongo shell 通过运行mongod.exe来启动服务器。
MongoDB 服务器可以通过在 Windows 中打开命令提示符(以管理员身份运行)或在 Linux 系统上打开终端窗口并键入以下命令来手动启动:
C:\>cd c:\practicalmongodb\bin
c:\ practicalmongodb\bin>mongod
mongod --help for help and startup options
.........................................
这个窗口将显示所有与 mongod 的连接。它还显示可用于监控服务器的信息。
如果没有指定配置,MongoDB 在 Windows 上以默认数据库路径C:\data\db启动,在 Linux 上以默认数据库路径/data/db启动,并使用默认端口 27017 和 27018 绑定到本地主机。
键入^C将彻底关闭服务器。
MongoDB 提供了两种方法来指定启动服务器的配置参数。
第一种是使用命令行选项来指定(参考第 tk 章)。
第二种方法是加载配置文件。可以通过编辑文件然后重新启动服务器来更改服务器配置。
停止服务器
在 mongod 控制台中按 CTRL+C 可以关闭服务器。否则,您可以从 mongo 控制台使用shutdownServer命令。
打开一个终端窗口,并连接到 mongo 控制台。
C:\> cd c:\practicalmongodb\bin
c:\practicalmongodb\bin> mongo
MongoDB shell version: 3.0.4
connecting to: test
>
切换到 admin db 并发出shutdownServer命令:
> use admin
switched to db admin
> db.shutdownServer()
2015-07-14T22:57:20.413-0700 I NETWORK DBClientCursor::init call() failed server should be down...
2015-07-14T22:57:20.418-0700 I NETWORK trying reconnect to 127.0.0.1:27017
2015-07-14T22:57:21.413-0700 I NETWORK 127.0.0.1:27017 failed couldn't connect to server 127.0.0.1:27017
>
如果您检查在上一步中启动服务器的 mongod 控制台,您将看到服务器已经成功关闭。
.......................
2015-07-14T22:57:30.259-0700 I COMMAND [conn1] terminating, shutdown command received
2015-07-14T22:57:30.260-0700 I CONTROL [conn1] now exiting
.................................................
2015-07-14T22:57:30.380-0700 I STORAGE [conn1] shutdown: removing fs lock...
2015-07-14T22:57:30.380-0700 I CONTROL [conn1] dbexit: rc: 0
查看日志文件
默认情况下,MongoDB 的整个日志输出被写入到stdout中,但是这可以通过在启动服务器时指定配置中的 logpath 选项来改变,以便将输出重定向到一个文件中。
日志文件内容可用于识别异常等问题,这些问题可能表明某些数据问题或连接问题。
服务器状态
db.ServerStatus()是 MongoDB 提供的一个简单方法,用于检查服务器状态,比如连接数、正常运行时间等等。server status 命令的输出取决于操作系统平台、MongoDB 版本、使用的存储引擎和配置类型(如独立、副本集和分片集群)。
从 3.0 版开始,输出中删除了以下部分:workingSet、indexCounters 和 recordStats。
为了检查使用 MMAPv1 存储引擎的服务器的状态,连接到 mongo 控制台,切换到 admin db,并发出db.serverStatus()命令。
c:\practicalmongodb\bin> mongo
MongoDB shell version: 3.0.4
connecting to: test
> use admin
switched to db admin
> db.serverStatus()
host" : "ANOC9",
"version" : "3.0.4",
"process" : "mongod",
"pid" : NumberLong(1748),
"uptime" : 14,
"uptimeMillis" : NumberLong(14395),
"uptimeEstimate" : 13,
"localTime" : ISODate("2015-07-14T22:58:44.532Z"),
"asserts" : {
"regular" : 0,
"warning" : 0,
"msg" : 0,
"user" : 1,
"rollovers" : 0
},
.........................................................
上面的serverStatus输出还将有一个“backgroundflushing”部分,它显示与 MongoDB 使用 MMAPv1 作为存储引擎将数据刷新到磁盘的过程相对应的报告。
“操作计数器”和“断言”部分提供了有用的信息,可以对这些信息进行分析以对任何问题进行分类。
“操作计数器”部分显示每种类型的操作数。为了发现是否有任何问题,您应该对这些操作有一个基线。如果计数器开始偏离基线,这表明存在问题,需要采取措施将其恢复到正常状态。
“asserts”部分描述了已经发生的客户端和服务器警告或异常的数量。如果您发现此类异常和警告增多,您需要仔细查看日志文件,以确定问题是否正在发展。断言数量的增加也可能表明数据有问题,在这种情况下,应该使用 MongoDB validate 函数来检查数据是否完好无损。
接下来,让我们使用 WiredTiger 存储引擎启动服务器,并查看 serverStatus 输出。
c:\practicalmongodb\bin> mongod –storageEngine wiredTiger
2015-07-14T22:51:05.965-0700 I CONTROL Hotfix KB2731284 or later update is installed, no need to zero-out data files
2015-07-29T22:51:05.965-0700 I STORAGE [initandlisten] wiredtiger_open config: create,cache_size=1G,session_max=20000,eviction=(threads_max=4),statistics=(fast),log=(enabled=true,archive=true,path=journal,compressor=snappy),file_manager=(close_idle_time=100000),checkpoint=(wait=60,log_size=2GB),statistics_log=(wait=0)
..................................................
为了检查服务器状态,连接到 mongo 控制台,切换到 admin db,并发出db.serverStatus()命令。
c:\practicalmongodb\bin> mongo
MongoDB shell version: 3.0.4
connecting to: test
> use admin
switched to db admin
> db.serverStatus()
"wiredTiger" : {
"uri" : "statistics:",
"LSM" : {
"...........................................................,
"tree maintenance operations scheduled":0,
............................................................,
},
"async" : {
"number of allocation state races":0,
"number of operation slots viewed for allocation":0,
"current work queue length" : 0,
"number of flush calls" : 0,
"number of times operation allocation failed":0,
"maximum work queue length" : 0,
............................................................,
},
"block-manager" : {
"mapped bytes read" : 0,
"bytes read" : 966656,
"bytes written" : 253952,
...................................,
"blocks written" : 45
},
............................................................,
如您所见,当使用存储引擎 wiredTiger 启动时,服务器状态输出有一个新的部分,称为 wiredTiger statistics。
9.4.5 识别和修复 MongoDB
在本节中,您将了解如何修复损坏的数据库。
如果您遇到类似以下的错误
- 数据库服务器拒绝启动,声称数据文件已损坏
- 在日志文件或
db.serverStatus()命令中可以看到断言 - 奇怪或意外的查询结果
这意味着数据库已损坏,必须运行修复才能恢复数据库。
在开始修复之前,您需要做的第一件事是让服务器离线(如果它还没有离线的话)。你可以使用上面提到的任何一个选项。在本例中,在 mongod 控制台中键入^C。这将关闭服务器。
接下来,使用–repair选项启动 mongod,如下所示:
c:\practicalmongodb\bin> mongod --repair
2015-07-14T22:58:31.171-0700 I CONTROL Hotfix KB2731284 or later update is installed, no need to zero-out data files
2015-07-14T22:58:31.173-0700 I CONTROL [initandlisten] MongoDB starting : pid=3996 port=27017 dbpath=c:\data\db\ 64-bit host=ANOC9
2015-07-14T22:58:31.174-0700 I CONTROL [initandlisten] db version v3.0.4
.....................................
2015-07-14T22:58:31.447-0700 I STORAGE [initandlisten] shutdown: removing fs lock...
2015-07-14T22:58:31.449-0700 I CONTROL [initandlisten] dbexit: rc: 0
c:\ practicalmongodb\bin>
这将修复 mongod。如果您查看输出,您会发现该实用程序正在修复的各种差异。一旦修复过程结束,它就会退出。
修复过程完成后,服务器可以正常启动,然后可以使用最新的数据库备份来恢复丢失的数据。
有时,您可能会注意到在修复大型数据库时,驱动器磁盘空间不足。这是因为 MongoDB 需要在数据文件所在的驱动器上创建文件的临时副本。为了解决这个问题,在修复数据库时,您应该使用–repairpath参数来指定在修复过程中可以创建临时文件的驱动器。
9.4.6 识别和修复集合级数据
有时,您可能希望验证集合是否包含有效数据和有效索引。对于这种情况,MongoDB 提供了一个validate()方法来验证指定集合的内容。
以下示例验证了Users集合的数据:
c:\practicalmongodb\bin> mongo
MongoDB shell version: 3.0.4
connecting to: test
> use mydbpoc
switched to db mydbpoc
> db.myusers.validate()
{
"ns" : "mydbpoc.myusers",
"firstExtent" : "1:4322000 ns:mydbpoc.myusers",
"lastExtent" : "1:4322000 ns:mydbpoc.myusers",
"...............
"valid" : true,
"errors" : [ ],
"warning" : "Some checks omitted for speed. use {full:true} option to do
more thorough scan.",
"ok" : 1
}
默认情况下,通过validate()选项检查数据文件和相关索引。提供收集统计信息是为了帮助识别数据文件或索引是否有问题。
如果运行validate()表明索引被损坏,在这种情况下reIndex可以用于重新索引集合的索引。这将删除并重新生成集合的所有索引。
以下命令重新索引Users集合的索引:
> use mydbpoc
switched to db mydbpoc
> db.myusers.reIndex()
{
"nIndexesWas" : 1,
"msg" : "indexes dropped for collection",
"nIndexes" : 1,
"indexes" : [
{
"key" : {
"_id" : 1
},
"ns" : "mydbpoc.myusers",
"name" : "_id_"
}
],
"ok" : 1
}
>
如果集合的数据文件损坏,那么运行–repair选项是修复所有数据文件的最佳方式。
9.5 监控 MongoDB
作为 MongoDB 服务器管理员,监控系统的性能和健康状况非常重要。在本节中,您将学习监控系统的方法。
mongostat
mongostat 是 MongoDB 发行版的一部分。这个工具在服务器上提供简单的统计数据;虽然它不全面,但它提供了一个很好的概述。下面显示了本地主机的统计信息。打开终端窗口并执行以下操作:
c:\> cd c:\practicalmongodb\bin
c:\practicalmongodb\bin> mongostat
前六列显示了 mongod 服务器处理各种操作的速度。除了这些列之外,下面的列也值得一提,在诊断问题时可能有用:
- Conn:这是 mongod 实例的连接数指标。这里的高值可能表示应用没有释放或关闭连接,这意味着尽管应用正在发出一个打开的连接,但在操作完成后它没有关闭连接。
从 3.0 版本开始,mongostat 也可以使用选项–json以 json 格式返回响应。
c:\> cd c:\practicalmongodb\bin
c:\practicalmongodb\bin> mongostat –json
{"ANOC9":{"ar|aw":"0|0","command":"1|0","conn":"1","delete":"*0","faults":"1","flushes":"0","getmore":"0","host":"ANOC9","insert":"*0","locked":"", "mapped":"560.0M","netIn":"79b","netOut":"10k","non mapped":"","qr|qw":"0|0","query":"*0","res":"153.0M","time":"05:16:17","update":"*0","vsize":"1.2G"}}
9.5.2 mongod Web 界面
每当 mongod 启动时,它都会默认创建一个 web 端口,这个端口比 mongod 用来监听连接的端口号大 1000。默认情况下,HTTP 端口是 28017。
这个 mongod web 界面是通过您的 web 浏览器访问的,它显示了大部分统计信息。如果 mongod 运行在 localhost 上,并且正在监听端口 27017 上的连接,那么可以使用下面的 URL 访问 HTTP 状态页面:http://localhost:28017。页面如图 9-2 所示。
图 9-2。
Web interface
9.5.3 第三方插件
除了这个工具,还有各种第三方适配器可用于 MongoDB,这些适配器允许您使用常见的开源或商业监控系统,如 cacti、Ganglia 等。在其网站上,10gen 维护着一个页面,分享关于可用 MongoDB 监控接口的最新信息。
要获得第三方插件的最新列表,请访问 www.mongodb.org/display/DOCS/Monitoring+and+Diagnostics 。
9.5.4 MongoDB 云管理器
除了上面讨论的用于监控和备份目的的工具和技术之外,还有 MongoDB 云管理器(以前称为 MMS–MongoDB 监控服务)。它是由开发 MongoDB 的团队开发的,可以免费使用(30 天试用许可)。与上面讨论的技术相比,MongoDB Cloud Manager 以图形和图表的形式提供了用户界面以及日志和性能细节。
MongoDB 云管理器图表是交互式的,允许用户设置自定义的日期范围,如图 9-3 所示。
图 9-3。
Setting a custom date range
云管理器的另一个简洁的特性是在不同事件发生时使用电子邮件和文本提醒的能力。这在图 9-4 中进行了描述。
图 9-4。
Email and text alerts
Cloud Manager 不仅提供图形和警报,还允许您查看按响应时间排序的较慢的查询。您可以在一个地方很容易地看到您的查询是如何执行的。图 9-5 显示了查询性能的图表。
图 9-5。
Query response time
云管理器允许您执行以下操作:
- 自动化您的 MongoDB 部署(MongoDB 节点、集群的配置和现有部署的升级)
- 通过持续备份保护您的数据
- 提供与 AWS 集成的任何拓扑
- 在您的仪表板中监控性能
- 执行操作任务,如增加容量
对于 AWS 用户,它提供了直接集成,因此可以在 AWS 上启动 MongoDB,而无需离开 Cloud Manager。您在第 tk 章中看到了如何使用 AWS 进行配置。
云管理器还可以帮助您发现系统中的低效之处,并进行纠正以实现平稳运行。
它使用您安装的代理收集和报告指标。Cloud Manager 提供了 MongoDB 系统健康的快速浏览,并帮助您确定性能问题的根本原因。
接下来,您将看到应该用于任何性能调查的关键指标。在这个过程中,您还会看到指标的组合表明了什么。
9.5.4.1 度量
您将主要关注以下关键指标:这些指标在调查性能问题时起着关键作用。它们提供了 MongoDB 系统内部正在发生的事情以及哪些系统资源(即 CPU、RAM 或磁盘)是瓶颈的快速浏览。
- 页错误
- 操作计数器
- 锁定百分比
- 行列
- CPU 时间(等待和用户)
要查看下面提到的图表,您可以单击部署部分下的部署链接。选择已经被配置为由云管理器监控的 MongoDB 实例。接下来,从管理图表部分选择所需的图形/图表。
页面错误显示系统中每秒发生的平均页面错误数。图 9-6 显示了页面故障图。
图 9-6。
Page faults
OpCounters 显示系统上每秒执行的平均操作数。参见图 9-7 。
图 9-7。
OpCounters
在页面错误与操作数的比率中,页面错误取决于系统上正在执行的操作以及当前内存中的内容。因此,每秒页面错误数与每秒操作计数器数的比率可以提供磁盘 I/O 需求的大致情况。参见图 9-8 。
图 9-8。
Page fault to Opcounters ratio
如果该比率为
- < 1,这被归类为低磁盘 I/O
- 接近 1 时,这被归类为常规磁盘 I/O。
-
1,这被归类为高磁盘 I/O。
Queues 图显示了在任何给定时间等待释放锁的操作数。参见图 9-9 。
图 9-9。
Queues
CPU 时间(低等待和用户)图显示了 CPU 内核如何度过它们的周期。参见图 9-10 。
图 9-10。
CPU Time
IOWait 表示 CPU 等待其他资源(如磁盘或网络)所花费的时间。参见图 9-11 。
图 9-11。
IOWait
用户时间表示执行计算所花费的时间,例如文档更新、更新和重新平衡索引、选择或排序查询结果、运行聚合框架命令、Map/Reduce 或服务器端 JavaScripts。参见图 9-12 。
图 9-12。
User time
要查看 CPU 时间图,您需要安装 munin。
这些关键指标及其组合应该用于调查任何性能问题。
9.6 总结
在本章中,您了解了如何使用打包成 MongoDB 发行版一部分的各种实用程序来管理和维护系统。
您了解了作为管理员必须了解的主要操作,以便详细了解这些实用程序。请通读参考资料。在下一章中,您将研究 MongoDB 的用例,并且您还将看到 MongoDB 不是一个好选择的情况。
十、MongoDB 用例
"MongoDB: Is it useful to me at all?"
在本章中,我们将提供 MongoDB 的特性和它适合解决的业务问题之间的必要联系。我们将使用两个用例来分享用于解决此类问题的技术和模式。
10.1 用例 1 -性能监控
在本节中,您将探索如何使用 MongoDB 来存储和检索性能数据。您将关注用于存储数据的数据模型。检索将包括简单地从各自的集合中读取。您还将了解如何应用分片和复制来提高性能和数据安全性。
我们假设一个监控工具正在以 CSV 格式收集服务器定义的参数数据。通常,监控工具要么将数据作为文本文件存储在服务器上的指定文件夹中,要么将其输出重定向到任何报告数据库服务器。在这个用例中,有一个调度程序将读取这个共享文件夹路径,并将数据导入 MongoDB 数据库。
模式设计
设计解决方案的第一步是确定模式。该模式取决于监控工具正在捕获的数据的格式。
日志文件中的一行可能类似于表 10-1 。
表 10-1。
Log File
| 节点 UUID | ip 地址 | 节点名 | 管理信息库 | 时间戳 _ 毫秒) | 公制 Ve | | --- | --- | --- | --- | --- | --- | | 3 beb 1 至 8b-040d-4b46-932a-2d31bd353186 | 10.161.1.73 | 公司 xyz 萨达尔 | 取指令单元 | 1369221223384 | Zero point two |以下是将每行存储为文本的最简单方法:
{
_id: ObjectId(...),
line: '10.161.1.73 - corp_xyz_sardar [15/July/2015:13:55:36 -0700] "Interface Util" ...
}
虽然这捕获了数据,但对用户来说没有意义,所以如果您想找出来自特定服务器的事件,您需要使用正则表达式,这将导致对集合的全面扫描,效率非常低。
相反,您可以从日志文件中提取数据,并将其作为有意义的字段存储在 MongoDB 文档中。
请注意,在设计结构时,使用正确的数据类型非常重要。这不仅节省了空间,而且对性能也有显著影响。
例如,如果将日志的日期和时间字段存储为一个字符串,不仅会使用更多的字节,而且也很难执行日期范围查询。如果不使用字符串,而是将日期存储为 UTC 时间戳,那么它将需要 8 个字节,而字符串需要 28 个字节,因此执行日期范围查询会更容易。正如您所看到的,使用正确的数据类型增加了查询的灵活性。
您将使用以下文档来存储您的监控数据:
{
_id: ObjectID(...),
Host:,
Time:ISODate(‘’),
ParameterName:’aa’,
Value:10.23
}
Note
实际的日志数据可能有额外的字段;如果你全部捕获,就会产生一个大文档,这是对存储和内存的低效使用。设计模式时,应该省略不需要的细节。为了满足您的需求,确定您必须捕获哪些字段是非常重要的。
在您的方案中,满足报告应用要求的最重要信息如下:
Host Timestamp Parameter Value
操作
设计完文档结构后,接下来您将看到需要在系统上执行的各种操作。
插入数据
用于插入数据的方法取决于您的应用写操作。
If you are looking for fast insertion speed and can compromise on the data safety, then the following command can be used: > db.perfpoc.insert({Host:"Host1", GeneratedOn: new ISODate("2015-07-15T12:02Z"),ParameterName:"CPU",Value:13.13},w=0) > Although this command is the fastest option available, since it doesn’t wait for any acknowledgment of whether the operation was successful or not, you risk losing data. If you want just an acknowledgment that at least the data is getting saved, you can issue the following command: > db.perfpoc.insert({Host:"Host1", GeneratedOn: new ISODate("2015-07-15T12:07Z"),ParameterName:"CPU",Value:13.23},w=1) > Although this command acknowledges that the data is saved, it will not provide safety against any data loss because it is not journaled. If your primary focus is to trade off increased insertion speed for data safety guarantees, you can issue the following command: > db.perfpoc.insert({Host:"Host1", GeneratedOn: new ISODate("2015-07-15T12:09Z"),ParameterName:"CPU",Value:30.01},j=true,w=2) >
在这段代码中,您不仅要确保数据被复制,还要启用日志记录。除了复制确认之外,它还会等待成功的日志提交。
Note
尽管这是最安全的选择,但它对插入性能有严重的影响,因此是最慢的操作。
批量插入
在使用严格的写操作时,批量插入事件总是有好处的,就像您的情况一样,因为这使 MongoDB 能够在一组 insert 中分配所产生的性能损失。
如果可能的话,应该使用大容量插入来插入监控数据,因为数据将会很大,并且会在几秒钟内生成。将它们组合成一组并插入会有更好的效果,因为在相同的等待时间内,会保存多个事件。因此,对于这个用例,您将使用批量插入对多个事件进行分组。
查询性能数据
您已经看到了如何插入事件数据。当您能够通过查询数据来响应特定的查询时,维护数据的价值就体现出来了。
例如,您可能想要查看与某个特定字段相关的所有性能数据,比如说Host。
您将看到一些获取数据的查询模式,然后您将看到如何优化这些操作。
Query1: Fetching the Performance Data of a Particular Host
> db.perfpoc.find({Host:"Host1"})
{ "_id" : ObjectId("553dc64009cb76075f6711f3"), "Host" : "Host1", "GeneratedOn": ISODate("2015-07-18T12:02:00Z"), "ParameterName" : "CPU", "Value" : 13.13 }
{ "_id" : ObjectId("553dc6cb4fd5989a8aa91b2d"), "Host" : "Host1", "GeneratedOn": ISODate("2015-07-18T12:07:00Z"), "ParameterName" : "CPU", "Value" : 13.23 }
{ "_id" : ObjectId("553dc7504fd5989a8aa91b2e"), "Host" : "Host1", "GeneratedOn": ISODate("2015-07-18T12:09:00Z"), "ParameterName" : "CPU", "Value" : 30.01 }
>
如果需要分析主机的性能,可以使用这种方法。
在Host上创建索引将优化上述查询的性能:
> db.perfpoc.ensureIndex({Host:1})
>
Query2: Fetching Data Within a Date Range from July 10, 2015 to July 20, 2015
> db.perfpoc.find({GeneratedOn:{"$gte": ISODate("2015-07-10"), "$lte": ISODate("2015-07-20")}})
{ "_id" : ObjectId("5302fec509904770f56bd7ab"), "Host" : "Host1", "GeneratedOn"
...............
>
如果您想要考虑和分析在特定日期范围内收集的数据,这一点很重要。在这种情况下,关于“时间”的索引将对性能产生积极的影响。
> db.perfpoc.ensureIndex({GeneratedOn:1})
>
Query3: Fetching Data Within a Date Range from July 10, 2015 to July 20, 2015 for a Specific Host
> db.perfpoc.find({GeneratedOn:{"$gte": ISODate("2015-07-10"), "$lte": ISODate("2015-07-20")},Host: "Host1"})
{ "_id" : ObjectId("5302fec509904770f56bd7ab"), "Host" : "Host1", "GeneratedOn"
.................
>
如果您想要查看特定时间段内主机的性能数据,这将非常有用。
在涉及多个字段的查询中,所使用的索引对性能有很大的影响。例如,对于上面的查询,创建一个复合索引将是有益的。
另请注意,字段在复合索引中的顺序会产生影响。让我们用一个例子来理解其中的区别。让我们创建一个复合索引,如下所示:
> db.perfpoc.ensureIndex({"GeneratedOn":1,"Host":1})
>
接下来,对此进行解释:
> db.perfpoc.find({GeneratedOn:{"$gte": ISODate("2015-07-10"), "$lte": ISODate("2015-07-20")}, Host: "Host1"}).explain(“allPlansExecution”)
.......................................................................
"allPlansExecution" : [
{
"nReturned" : 4,
"executionTimeMillisEstimate" : 0,
"totalKeysExamined" : 4,
"totalDocsExamined" : 4
"indexName" : "GeneratedOn_1_Ho
.......................................................................
"isMultiKey" : false,
"direction" : "forward",
}]
.......................................................................
放下复合索引,像这样:
> db.perfpoc.dropIndexes()
{
"nIndexesWas" : 2,
"msg" : "non-_id indexes dropped for collection",
"ok" : 1
}
或者,创建字段颠倒的复合索引:
> db.perfpoc.ensureIndex({"Host":1,"GeneratedOn":1})
>
做一个解释:
> db.perfpoc.find({GeneratedOn:{"$gte": ISODate("2015-07-10"), "$lte": ISODate("2015-07-20")}, Host: "Host1"}).explain("allPlansExecution")
{
.............................................
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 4,
"executionTimeMillis" : 0,
"totalKeysExamined" : 4,
"totalDocsExamined" : 4,
......................................................
"allPlansExecution" : [ ]
....................................................
}
>
您可以在 explain 命令的输出中看到不同之处。
使用explain(),您可以计算出索引的影响,并根据您的应用使用情况相应地决定索引。
还建议使用单个复合索引来覆盖最大数量的查询,而不是多个单键索引。
根据您的应用使用情况和解释统计的结果,您将在{'GeneratedOn':1, 'Host': 1}上仅使用一个复合索引来涵盖所有上述查询。
Query4: Fetching Count of Performance Data by Host and Day
列出数据很好,但是最常见的性能数据查询是在分析过程中找出计数、平均值、总和或其他聚合操作。在这里,您将看到如何使用aggregate命令来选择、处理和聚集结果,以满足强大的特别查询的需要。
为了进一步解释这一点,让我们编写一个每月统计数据的查询:
> db.perfpoc.aggregate(
... [
... {$project: {month_joined: {$month: "$GeneratedOn"}}},
... {$group: {_id: {month_joined: "$month_joined"}, number: {$sum:1}}},
... {$sort: {"_id.month_joined":1}}
... ]
... )
{ "_id" : { "month_joined" : 7 }, "number" : 4 }
>
为了优化性能,您需要确保筛选字段有一个索引。您已经创建了一个包含相同内容的索引,因此对于这个场景,您不需要创建任何额外的索引。
分片
性能监控数据集非常庞大,因此迟早会超出单个服务器的容量。因此,您应该考虑使用碎片集群。
在这一节中,您将看到哪个碎片键适合您的性能数据用例,以便负载分布在集群中,没有一个服务器过载。
shard 键控制数据如何分布,以及最终系统的查询和写入能力。理想情况下,分片密钥应该具有以下两个特征:
- 插入在 shard 集群中是平衡的。
- 大多数查询可以被路由到要满足的碎片的子集。
让我们看看哪些字段可以用于分片。
Time field: In choosing this option, although the data will be distributed evenly among the shards, neither the inserts nor the reads will be balanced. As in the case of performance data, the time field is in an upward direction, so all the inserts will end up going to a single shard and the write throughput will end up being same as in a standalone instance. Most reads will also end up on the same shard, assuming you are interested in viewing the most recent data frequently. Hashes: You can also consider using a random value to cater to the above situations; a hash of the _id field can be considered the shard key. Although this will cater to the write situation of the above (that is, the writes will be distributed), it will affect querying. In this case, the queries must be broadcasted to all the shards and will not be routable. Use the key, which is evenly distributed, such as Host. This has following advantages: if the query selects the host field, the reads will be selective and local to a single shard, and the writes will be balanced. However, the biggest potential drawback is that all data collected for a single host must go to the same chunk since all the documents in it have the same shard key. This will not be a problem if the data is getting collected across all the hosts, but if the monitoring collects a disproportionate amount of data for one host, you can end up with a large chunk that will be completely unsplittable, causing an unbalanced load on one shard. Combining the best of options 2 and 3, you can have a compound shard key, such as {host:1, ssk: 1} where host is the host field of the document and ssk is _id field’s hash value. In this case, the data is distributed largely by the host field making queries, accessing the host field local to either one shard or group of shards. At the same time, using ssk ensures that data is distributed evenly across the cluster. In most of the cases, such keys not only ensure ideal distribution of writes across the cluster but also ensure that queries access only the number of shards that are specific to them.
然而,最好的方法是分析应用的实际查询和插入,然后选择一个合适的碎片键。
管理数据
由于性能数据非常庞大,而且还在继续增长,因此您可以定义一个数据保留策略,规定您将在一段指定的时间内(比如 6 个月)维护数据。
那么如何删除旧数据呢?您可以使用以下模式:
Multiple collections to store the data: The third pattern is to have a day-wise collection created, which contains documents that store that day’s performance data. This way you will end up having multiple collections within a database. Although this will complicate the querying (in order to fetch two days’ worth of data, you might need to read from two collections), dropping a collection is fast, and the space can be reused effectively without any data fragmentation. In your use case, you are using this pattern for managing the data.
-
在这种情况下,在集合上定义了生存时间索引,这使得 MongoDB 能够定期从集合中获取旧文档。然而,这并不具备封顶集合的性能优势;另外,
remove()可能会导致数据碎片。 -
使用有上限的集合:虽然有上限的集合可以用来存储性能数据,但是不能共享有上限的集合。
-
使用 TTL 集合:这种模式创建一个类似于 capped 集合的集合,但是它可以分片。
10.2 用例 2–社交网络
在本节中,您将探索如何使用 MongoDB 来存储和检索社交网站的数据。
这个用例基本上是一个友好的社交网站,允许用户共享他们的状态和照片。为此使用案例提供的解决方案假设如下:
A user can choose whether or not to follow another user. A user can decide on the circle of users with whom he wants to share updates. The circle options are Friends, Friends of Friends, and Public. The updates allowed are status updates and photos. A user profile displays interests, gender, age, and relationship status.
模式设计
您提供的解决方案旨在最大限度地减少为显示任何给定页面而必须加载的文档数量。该应用有两个主页面:第一个页面显示用户墙(用于显示由特定用户创建或针对特定用户的帖子),另一个是社交新闻页面,显示关注该用户或该用户正在关注的所有人的所有通知和活动。
除了这两个页面之外,还有一个用户个人资料页面,显示用户的个人资料相关的详细信息,以及他的朋友群(关注他的人或他正在关注的人)的信息。为了满足这个需求,这个用例的模式由以下集合组成。
第一个集合是user.profile,它存储用户的档案相关数据:
{
_id: "User Defined unique identifier",
UserName: "user name"
ProfilDetaile:
{Age:.., Place:.., Interests: ...etc},
FollowerDetails: {
"User_ID":{name:..., circles: [circle1, circle2]}, ....
},
CirclesUserList: {
"Circle1":
{"User_Id":{name: "username"}, ......
}, .......
} ,
ListBlockedUserIDs: ["user1",...]
}
- 在这种情况下,您需要手动指定
_id字段。 Follower列出关注该用户的用户。CirclesUserList由该用户关注的圈子组成。Blocked由被用户阻止查看其更新的用户组成。
第二个集合是user.posts集合,其模式如下:
{
_id: ObjectId(...),
by: {id: "user id", name: "user name"},
VisibleTocircles: [],
PostType: "post type",
ts: ISODate(),
Postdetail: {text: "",
Comments_Doc:
[
{Commentedby: {id: "user_id", name: "user name"}, ts: ISODate(), Commenttext: "comment text"}, .....
]
}
- 这个集合主要用于显示用户的所有活动。
by提供发帖用户的信息。Circles控制帖子对其他用户的可见性。Type用于标识帖子的内容。ts是创建帖子的日期时间。detail包含文章文本,其中嵌入了评论。 - 一个
comment文档由以下细节组成:by提供对帖子发表评论的用户 id 和姓名的细节,ts是评论的时间,text是用户发表的实际评论。
第三个集合是user.wall,用于在几分之一秒内渲染用户的墙页。该集合从第二个集合中获取数据,并将其以汇总格式存储,以便快速呈现 wall page。
该集合具有以下格式:
{
_id: ObjectId(...),
User_id: "user id"
PostMonth: "201507",
PostDetails: [
{
_id: ObjectId(..), ts: ISODate(), by: {_id: .., Name:.. }, circles: [..], type: ....
, detail: {text: "..."}, comments_shown: 3
,comments: [
{by: {_id:., Name:....}, ts: ISODate(), text:""}, ......]
},....]}
- 如您所见,您每月为每个用户维护这个文档。第一次可见的评论数量是有限的(在这个例子中是 3 条);如果需要为该特定帖子显示更多评论,则需要查询第二个集合。
- 换句话说,这是一种快速加载用户墙页面的汇总视图。
第四个集合是social.posts,用于社交新闻屏幕的快速渲染。这是显示所有文章的屏幕。
与第三个集合一样,第四个集合也是一个依赖集合。它包含了许多与user.wall信息相同的信息,因此为了清楚起见,本文被简化为:
{
_id: ObjectId(...),
user_id: "user id",
postmonth: '2015_07',
postlists: [ ... ]
}
操作
这些模式针对读取性能进行了优化。
查看帖子
由于social.posts和user.wall集合被优化用于在几分之一秒内呈现新闻提要或墙贴,所以查询相当简单。
这两个集合具有相似的模式,因此获取操作可以由相同的代码支持。下面是相同的伪代码。该函数将以下内容作为参数:
- 需要查询的集合。
- 需要查看其数据的用户。
- 月份是可选参数;如果指定了,它应该列出日期小于或等于指定月份的所有帖子。
Function Fetch_Post_Details
(Parameters: CollectionName, View_User_ID, Month)
SET QueryDocument to {"User_id": View_User_ID}
IF Month IS NOT NULL
APPEND Month Filter ["Month":{"$lte":Month}] to QueryDocument
Set O_Cursor = (resultset of the collection after applying the QueryDocument filter)
Set Cur = (sort O_Cursor by "month" in reverse order)
while records are present in Cur
Print record
End while
End Function
上面的函数按时间倒序检索给定用户的留言板或新闻提要上的所有帖子。
提交帖子时,您需要进行某些检查。以下是其中的几个。
首先,当用户查看他或她的页面时,在墙上呈现帖子时,您需要检查同样的帖子是否可以显示在他们自己的墙上。用户墙包含他发布的帖子或他们关注的用户的帖子。下面的函数有两个参数:墙所属的用户和正在呈现的帖子:
function Check_VisibleOnOwnWall
(Parameters: user, post)
While Loop_User IN user.Circles List
If post by = Loop_User
return true
else
return false
end while
end function
上面的循环遍历了在user.profile集合中指定的圆圈,如果提到的帖子是由列表中的用户发布的,它将返回 true。
此外,您还需要注意用户的阻止列表中的用户:
function ReturnBlockedOrNot(user, post)
if post by user id not in user blocked list
return true
else
return false
endfunction
当用户查看另一个用户的墙时,您还需要负责权限检查:
Function visibleposts(parameter user, post)
if post circles is public
return true
If post circles is public to all followed users
Return true
set listofcircles = followers circle whose user_id is the post's by id.
if listofcircles in post's circles
return true
return false
end function
这个函数首先检查帖子的圈子是否公开。如果是公开的,帖子将显示给所有用户。
如果帖子的圈子未设置为公开,则当用户关注该用户时,会向用户显示该圈子。如果两者都不成立,它将进入跟踪登录用户的所有用户的圈子。如果圈子列表在帖子圈子列表中,这意味着用户在接收帖子的圈子中,因此帖子将可见。如果两个条件都不满足,用户将看不到帖子。
为了有更好的性能,您需要在social.posts和user.wall集合中的user_id和month上有一个索引。
创建注释
要创建用户对包含给定文本的给定帖子的评论,您需要执行类似如下的代码:
Function postcomment(
Parameters: commentedby, commentedonpostid, commenttext)
Set commentedon to current datetime
Set month to month of commentedon
Set comment document as {"by": {id: commentedby[id], "Name": commentedby["name"]}, "ts": commentedon, "text": commenttext}
Update user.posts collection. Push comment document.
Update user.walls collection. Push the comment document.
Increment the comments_shown in user.walls collection by 1.
Update social.posts collection. Push the comment document.
Increment the comments_shown counter in social.posts collection by 1.
End function
因为您在两个依赖集合中最多显示三个注释(user.wall和social.posts集合),所以您需要定期运行以下更新语句:
Function MaintainComments
SET MaximumComments = 3
Loop through social.posts
If posts.comments_shown > MaximumComments
Pop the comment which was inserted first
Decrement comments_shown by 1
End if
Loop through user.wall
If posts.comments_shown > MaximumComments
Pop the comment which was inserted first
Decrement comments_shown by 1
End if
End loop
End Function
为了快速执行这些更新,您需要在posts.id和posts.comments_shown上创建索引。
创建新帖子
该代码中的基本操作序列如下:
The post is first saved into the “system of record,” the user.posts collection. Next, the user.wall collection is updated with the post. Finally, the social.posts collection of everyone who is circled in the post is updated with the post.
Function createnewpost
(parameter createdby, posttype, postdetail, circles)
Set ts = current timestamp.
Set month = month of ts
Set post_document = {"ts": ts, "by":{id:createdby[id], name: createdby[name]}, "circles":circles, "type":posttype, "details":postdetails}
Insert post_document into users.post collection
Append post_document into user.walls collection
Set userlist = all users who’s circled in the post based on the posts circle and the posted user id
While users in userlist
Append post_document to users social.posts collection
End while
End function
分片
可以通过对上面提到的四个集合进行分片来实现缩放。由于user.profile、user.wall和social.posts包含用户特定的文档,user_id是这些集合的完美分片键。_id是users.post系列最好的碎片钥匙。
10.3 摘要
在本章中,您使用了两个用例来了解如何使用 MongoDB 来解决某些问题。在下一章,我们将列出 MongoDB 的局限性和不适合它的用例。