MongoDB-实践教程-全-
MongoDB 实践教程(全)
一、大数据
"Big data is a term used to describe data with massive data, various structures and high-speed generation. This kind of data challenges the traditional RDBMS system for storing and processing data. Bidding data paves the way for new methods of processing and storing data. "
在本章中,我们将讨论大数据的基础知识、来源和挑战。我们将向您介绍大数据的三个 v(容量、速度和多样性),以及传统技术在处理大数据时面临的限制。
1.1 入门
大数据以及云、社交、分析和移动性是当今信息技术领域的热门词汇。大众对互联网和电子设备的可用性与日俱增。具体而言,智能手机、社交网站和平板电脑和传感器等其他数据生成设备正在创造数据爆炸。数据是从各种来源以各种格式生成的,如视频、文本、语音、日志文件和图像。一秒钟的高清视频产生的字节数是一页文本的 2000 倍。
考虑一下脸书公司网站上报道的以下统计数据:
There were 968 million daily active users on average for June of 2015. There were 844 million mobile daily active users on average for June of 2015. There were 1.49 billion monthly active users as of June 30, 2015. There were 1.31 billion mobile monthly active users as of June 30, 2015. There were 4.5 billion likes generated daily as of May 2013, which is a 67 percent increase from August 2012.
图 1-1 描绘了 Twitter 的统计数据。
图 1-1。
If you printed Twitter…
这里有另一个例子:考虑像去看电影这样的简单事件可以生成的数据量。你首先在电影评论网站上搜索一部电影,阅读关于这部电影的评论,并提出疑问。你可以在推特上谈论这部电影,或者在脸书上发布去看电影的照片。在去剧院的途中,您的 GPS 系统会跟踪您的路线并生成数据。
你可以想象一下:智能手机、社交网站和其他媒体正在产生大量数据,供公司处理和存储。当数据的规模对典型软件工具捕获、处理、存储和管理数据的能力构成挑战时,我们就有了大数据。图 1-2 图形化定义了大数据。
图 1-2。
Definition of Big Data
1.2 大数据
大数据是指具有高容量、高速生成、多品种的数据。我们来看几个大数据的事实和数字。
1.2.1 关于大数据的事实
世界各地的各种研究团队已经对生成的数据量进行了分析。例如,IDC 的分析显示,仅一年(2007 年)产生的数字数据量就超过了全球的总存储容量,这意味着无法存储所有产生的数据。此外,数据生成的速度将很快超过数据存储容量的增长速度。
以下章节涵盖了 2011 年 5 月发布的 MGI(麦肯锡全球研究院)报告( www.mckinsey.com/insights/business_technology/big_data_the_next_frontier_for_innovation
)中的观点。这项研究表明,大数据的商业和经济可能性及其更广泛的影响是商业领袖和政策制定者必须解决的重要问题。that)
1.2.1.1 大数据的规模因行业而异
大数据的增长是每个行业都存在的现象。MGI 估计,2010 年,全球企业使用了超过 7eb 的增量磁盘驱动器数据存储容量;有趣的是,其中近 80%的数据似乎是存储在其他地方的重复数据。MGI 还估计,到 2009 年,美国经济中几乎所有行业的每家公司平均至少存储 200 TB 的数据,许多行业的每家公司平均存储数据超过 1pb。
一些部门的数据密集程度远远高于其他部门;在这种情况下,数据密集度是指该行业的公司/企业积累的平均数据量,这意味着它们更有潜力从大数据中获取价值。
金融服务部门,包括银行、投资和证券服务,高度面向事务;法规还要求它们存储数据。分析表明,平均每个公司存储的数字数据最多。
通信和媒体公司、公用事业和政府也有每个企业或组织存储的大量数字数据,这似乎反映了这样的事实,即这些实体具有大量的操作和多媒体数据。
离散制造业和流程制造业拥有最高的总数据存储字节数。然而,这些部门的密集程度要低得多,因为它们分散在许多公司中。
1.2.1.2 大数据类型因行业而异
MGI 的研究还表明,存储的数据类型也因行业而异。例如,零售和批发、政府行政部门以及金融服务都会产生大量的文本和数字数据,包括客户数据、事务信息以及数学建模和模拟。制造业、医疗保健、媒体和通信等行业负责更高百分比的多媒体数据。X 射线、CT 和其他扫描形式的图像数据在医疗保健的数据存储量中占主导地位。
就大数据的地理分布而言,北美和欧洲目前拥有全球总量的 70%。由于云计算,一个地区产生的数据可以存储在另一个国家的数据中心。因此,拥有大量云和托管提供商产品的国家往往拥有大量数据存储。
1.3 大数据源
在本节中,我们将讨论导致数据不断增长的主要因素。图 1-3 描述了主要来源。
图 1-3。
Sources of data
正如 MGI 报告所强调的,这一数据的主要来源是
- 企业现在以更精细的方式收集数据,为每笔事务附加更多细节,以了解消费者行为。
- 医疗保健、产品公司等行业中多媒体使用的增加。
- 脸书、推特等社交媒体网站越来越受欢迎。
- 智能手机的快速普及,使用户能够积极使用社交媒体网站和其他互联网应用。
- 在日常生活中,通过网络连接到计算资源的传感器和设备的使用越来越多。
MGI 报告还预测,未来五年,传感器等机器对机器设备(也被称为物联网,或物联网)的数量将以每年超过 30%的速度增长。
因此,数据的增长率和多样性都在增加。此外,数据生成的模式已经从少数公司生成数据,其他公司消费数据,转变为人人生成数据,人人消费数据。这是由于消费 IT 和互联网技术的渗透以及社交媒体等趋势。图 1-4 描述了数据生成模型的变化。
图 1-4。
Data model
1.4 大数据的三个 v
我们将大数据定义为具有三个 v 的数据:量、速度和多样性,如图 1-5 所示。让我们来看看这三个方面。组织和 It 领导者必须关注这些方面。
图 1-5。
The three Vs of big data. The “big” isn’t just the volume
1.4.1 体积
大数据中的体积意味着数据的大小。如前所述,各种因素影响着大数据的规模:随着业务变得越来越以事务为导向,我们看到事务数量不断增加;越来越多的设备连接到互联网,这增加了容量;互联网的使用越来越多;内容的数字化也在增加。图 1-6 描绘了自 2009 年以来数字世界的增长。
图 1-6。
Digital universe size
在今天的场景中,数据不仅仅是从企业内部产生的;它也是基于与扩展企业和客户的事务而生成的。这需要企业对客户数据进行大量维护。如今,十亿字节的规模变得越来越普遍。图 1-7 描绘了数据增长率。
图 1-7。
Growth rate
如此庞大的数据量是大数据技术面临的最大挑战。以及时且经济的方式存储、处理和访问数据所需的存储和处理能力是巨大的。
1.4.2 品种
从各种设备和来源生成的数据没有固定的格式或结构。与文本相比,CSV 或 RDBMS 数据不同于文本文件、日志文件、流视频、照片、仪表读数、股票行情数据、pdf、音频和各种其他非结构化格式。
如今,数据的结构无法控制。新的数据源和数据结构正在快速创建。因此,技术上的责任是找到一种解决方案来分析和可视化存在的大量数据。例如,要为通勤者提供备用路线,交通分析应用需要来自数百万部智能手机和传感器的数据,以提供对交通状况和备用路线的准确分析。
1.4.3 速度
大数据中的速度是数据创建的速度和处理数据的速度。如果数据不能以要求的速度处理,它就失去了意义。由于来自社交媒体网站、传感器、报价器、计量和监控的数据流,无论数据是动态的还是静态的,组织都必须快速处理数据(参见图 1-8 )。对于大数据技术来说,以足够快的速度应对和处理数据是另一个挑战。
图 1-8。
The three aspects of data
在许多大数据用例中,实时洞察至关重要。例如,算法事务系统从市场和 Twitter 等社交媒体网站获取实时信息,以做出股票事务决策。处理这些数据的任何延迟都可能意味着损失数百万美元的股票事务机会。
每当讨论大数据时,都会谈到第四个 V。第四个 V 是准确性,这意味着并不是所有的数据都是重要的,所以确定什么将提供有意义的洞察力,什么应该被忽略是至关重要的。
1.5 大数据的使用
本节将重点介绍使用大数据为组织创造价值的方法。在我们深入研究如何让大数据为组织所用之前,我们先来看看大数据为什么重要。
大数据是一个全新的数据来源;它是当你在博客上发表文章时产生的数据,比如一个产品或旅行。以前,这种细微的可用信息没有被捕获。现在,it 和采用此类数据的组织可以追求创新、提高灵活性并增加盈利能力。
大数据可以以多种方式为任何组织创造价值。正如 MGI 报告中所列,这可以大致分为五种使用大数据的方式。
能见度
相关利益相关者及时访问数据会产生巨大的价值。我们用一个例子来理解这个。假设一家制造公司的 R&D、工程和制造部门分散在不同的地理位置。如果这些数据可以在所有这些部门之间访问,并且可以很容易地集成,它不仅可以减少搜索和处理时间,而且还有助于根据当前的需要提高产品质量。
1.5.2 发现和分析信息
大数据的大部分价值来自于从外部来源收集的数据可以与组织的内部数据合并。组织正在获取关于库存、员工和客户的详细数据。使用所有这些数据,他们可以发现和分析新的信息和模式;因此,这些信息和知识可以用来改进过程和性能。
细分和定制
大数据使组织能够创建量身定制的产品和服务,以满足特定细分市场的需求。这也可用于社会部门,以准确划分人口,并针对具体需求制定福利计划。基于各种参数的客户细分有助于有针对性的营销活动和定制产品以满足客户的需求。
辅助决策
大数据可以极大地降低风险,改善决策,并揭示有价值的见解。信用卡处理中的自动化欺诈警报系统和库存的自动微调是基于大数据分析帮助或自动化决策的系统示例。
创新
大数据以产品和服务的形式实现了新思想的创新。它可以在现有的基础上进行创新,从而接触到更多的人。使用为实际产品收集的数据,制造商不仅可以创新以创造下一代产品,还可以创新销售产品。
例如,可以分析来自机器和车辆的实时数据,以提供对维护计划的洞察;可以监控机器的磨损,以制造更有弹性的机器;油耗监控可以提高发动机的效率。实时交通信息已经为通勤者提供了选择替代路线的选项,让他们的生活变得更加轻松。
因此,大数据不仅仅是数据量。这是从不断增加的数据中发现有意义的见解的机会。它帮助组织做出更明智的决策,使他们更加敏捷。它不仅为组织提供了通过做出明智的决策来加强现有业务的机会,还帮助识别新的机会。
1.6 大数据挑战
大数据也带来了一些挑战。在本节中,我们将重点介绍其中的几个。
政策和程序
随着越来越多的数据在全球范围内被收集、数字化和移动,策略和法规遵从性问题变得越来越重要。数据隐私、安全、知识产权和保护对组织来说非常重要。
遵守各种法令和法律要求给数据处理带来了挑战。围绕数据的所有权和责任问题是大数据案例中需要处理的重要法律问题。
此外,许多大数据项目利用公共云计算提供商的可扩展性功能。这给合规性带来了挑战。
还需要回答关于谁拥有数据、什么被定义为数据的合理使用以及谁对数据的准确性和保密性负责的政策问题。
数据的获取
访问供消费的数据是大数据项目面临的一项挑战。有些数据可能会被第三方获取,获取这些数据可能会面临法律和合同方面的挑战。
关于产品或服务的数据可以在脸书、Twitter feeds、评论和博客上获得,那么产品所有者如何从不同提供商拥有的不同来源访问这些数据呢?
同样,需要将访问大数据的合同条款和经济激励捆绑在一起,以使消费者能够获得数据。
技术和工艺
必须利用专门为满足大数据需求而构建的新工具和技术,而不是试图通过遗留系统来解决上述问题。一方面,遗留系统在处理大数据方面的不足,另一方面,新技术中缺乏经验丰富的资源,这是任何大数据项目都必须应对的挑战。
1.7 遗留系统和大数据
在本节中,我们将讨论组织在使用传统系统管理大数据时面临的挑战。
1.7.1 大数据结构
遗留系统设计用于处理结构化数据,其中定义了带有列的表。列中保存的数据的格式也是已知的。
但是,大数据是有很多结构的数据。基本上是图像、视频、日志等非结构化数据。
由于大数据可能是非结构化的,为通过基于不同列中保存的特定数据类型的索引等技术来执行快速查询和分析而创建的遗留系统无法用于保存或处理大数据。
数据存储
传统系统使用大型服务器、NAS 和 SAN 系统来存储数据。随着数据的增加,服务器大小和后端存储大小也必须增加。传统的传统系统通常在纵向扩展模式下工作,需要向服务器添加越来越多的计算、内存和存储来满足不断增长的数据需求。因此,处理时间呈索引级增长,这破坏了大数据的另一个重要要求,即速度。
数据处理
遗留系统中的算法设计用于处理结构化数据,如字符串和整数。它们也受到数据大小的限制。因此,遗留系统无法处理非结构化数据、海量此类数据以及需要执行处理的速度。
因此,为了从大数据中获取价值,我们需要在存储、计算和检索领域部署更新的技术,并且我们需要分析数据的新技术。
1.8 大数据技术
你见过什么是大数据。在这一节中,我们将简要地看一下哪些技术可以处理这个庞大的数据源。讨论中的技术需要有效地接受和处理不同类型的数据。
使组织能够充分利用大数据的最新技术进步如下:
New storage and processing technologies designed specifically for large unstructured data Parallel processing Clustering Large grid environments High connectivity and high throughput Cloud computing and scale-out architectures
越来越多的技术正在利用这些技术进步。在本书中,我们将讨论 MongoDB,这是一种可用于存储和处理大数据的技术。
1.9 摘要
在本章中,您了解了大数据。您研究了产生大数据的各种来源,以及大数据的用途和带来的挑战。您还了解了为什么需要更新的技术来存储和处理大数据。
在接下来的章节中,您将了解一些有助于组织管理大数据并使他们能够从大数据中获得有意义的见解的技术。
二、NoSQL
“NoSQL 是一种设计互联网规模的数据库解决方案的新方法。它不是一种产品或技术,而是一个术语,它定义了一组不是基于传统 RDBMS 原则的数据库技术。”
在这一章,我们将涵盖 NoSQL 的定义和基础知识。我们将向你介绍 CAP 定理,并讨论 NRW 符号。我们将比较 ACID 和 BASE 方法,并通过比较 NoSQL 和 SQL 数据库技术来结束本章。
2.1 SQL
RDBMS 的想法来自 e . f . Codd 1970 年的白皮书《大型共享数据库的关系数据模型》用于查询 RDBMS 系统的语言是 SQL (Sequel 查询语言)。
RDBMS 系统非常适合存储在列和行中的结构化数据,可以使用 SQL 查询这些数据。RDBMS 系统基于 ACID 事务的概念。ACID 代表原子性、一致性、孤立性和持久性,其中
- 原子意味着要么完全应用事务的所有更改,要么根本不应用。
- 一致意味着数据在应用事务后处于一致状态。这意味着提交事务后,获取特定数据的查询将看到相同的结果。
- 隔离意味着应用于同一组数据的事务相互独立。因此,一个事务不会干扰另一个事务。
- 持久意味着更改在系统中是永久性的,即使出现任何故障也不会丢失。
2.2 NoSQL
NoSQL 是一个用来指非关系数据库的术语。因此,它包含了大多数不基于传统 RDBMS 原则的数据存储,并用于处理 Internet 规模的大型数据集。
正如上一章所讨论的,大数据对传统的数据存储和处理方式(如 RDBMS 系统)提出了挑战。因此,我们看到了 NoSQL 数据库的兴起,这种数据库的设计是为了在时间和成本的限制下处理如此巨大数量和种类的数据。
因此,NoSQL 数据库是从处理大数据的需求发展而来的;传统的 RDBMS 技术不能提供足够的解决方案。图 2-1 显示了与结构化数据相比,这些年来非/半结构化数据的增长。
图 2-1。
Structured vs. un/Semi-Structured data
下面是一些非常适合 NoSQL 数据库的大数据用例示例:
- 社交网络图:谁和谁有联系?谁的帖子应该出现在社交网站的用户墙或主页上?
- 搜索和检索:用特定的关键字搜索所有相关的页面,按关键字在页面上出现的次数排序。
2.2.1 定义
NoSQL 没有正式的定义。它代表了一种与 RDBMS 根本不同的持久性/数据存储机制。但是如果硬要定义 NoSQL,这里就是:NoSQL 是不遵循 RDBMS 原则的数据存储的总称。
Note
该术语最初用于表示“如果您想要伸缩,就不要使用 SQL”后来,这被重新定义为“不仅仅是 SQL”,这意味着除了 SQL 之外,还存在其他补充的数据库解决方案。
2 . 2 . 2 NoSQL 简史
1998 年,Carlo Strozzi 创造了术语 NoSQL。他用这个术语来标识他的数据库,因为数据库没有 SQL 接口。这个术语在 2009 年初再次出现,当时 Eric Evans(Rackspace 的一名员工)在一次关于开源分布式数据库的活动中使用这个术语来指代非关系型的分布式数据库,并且没有遵循关系型数据库的 ACID 特性。
2.3 酸与碱
在介绍中,我们提到了传统的 RDBMS 应用关注于 ACID 事务。无论这些品质看起来多么重要,它们都与 Web 规模的应用的可用性和性能要求不相容。
比方说,你有一家像 OLX 这样的公司,销售诸如未使用的家庭用品(旧家具、车辆等)之类的产品。)并使用 RDBMS 作为其数据库。让我们考虑两种情况。
第一个场景:让我们看一个电子商务购物网站,用户正在购买产品。在事务过程中,用户锁定数据库的一部分,即库存,其他用户必须等待,直到锁定的用户完成事务。
第二种情况:应用可能最终使用缓存的数据,甚至未锁定的记录,导致不一致。在这种情况下,当库存实际为零时,两个用户可能最终购买了该产品。
系统可能会变慢,影响可扩展性和用户体验。
与传统 RDBMS 系统的 ACID 方法相反,NoSQL 使用一种通常称为 BASE 的方法来解决这个问题。在解释 BASE 之前,我们先来探讨一下 CAP 定理的概念。
2.3.1 上限定理(布鲁尔定理)
埃里克·布鲁尔在 2000 年概述了上限定理。这是一个重要的概念,需要处理分布式数据库的开发者和架构师很好地理解。该定理指出,在分布式环境中设计应用时,存在三个基本要求,即一致性、可用性和分区容差。
- 一致性意味着在执行任何更改数据的操作后,数据保持一致,并且访问应用的所有用户或客户端看到相同的更新数据。
- 可用性意味着系统始终可用。
- 分区容差意味着,即使系统被划分为无法相互通信的服务器组,系统也将继续运行。
CAP 定理指出,在任何时间点,分布式系统只能满足上述三个保证中的两个(图 2-2 )。
图 2-2。
CAP Theorem
基地
埃里克·布鲁尔创造了基本的首字母缩写词。基础可以解释为
- 基本可用意味着系统在 CAP 定理中是可用的。
- 软状态表示即使没有输入提供给系统,状态也会随着时间而改变。这符合最终的一致性。
- 最终一致性意味着系统将在长期内达到一致性,前提是在此期间没有输入被发送到系统。
因此,BASE 与 RDBMS ACID 事务相反。
您已经看到 NoSQL 数据库最终是一致的,但是不同的 NoSQL 数据库最终的一致性实现可能会有所不同。
NRW 是用于描述最终一致性模型如何在 NoSQL 数据库中实现的符号,其中
- n 是数据库维护的数据副本的数量。
- r 是应用在返回读取请求的输出之前需要引用的副本数。
- w 是在将写操作标记为成功完成之前需要写入的数据副本的数量。
使用这些符号配置,数据库实现了最终一致性的模型。
可以在读取和写入操作级别实现一致性。
- 写操作
- N=W 意味着在将控制权返回给客户端并将写操作标记为成功之前,写操作将更新所有数据副本。这类似于传统 RDBMS 数据库在实现同步复制时的工作方式。此设置将降低写入性能。
- 如果写入性能是一个问题,这意味着您希望快速写入,您可以设置 W=1,R=N。这意味着写入将只更新任何一个拷贝,并将写入标记为成功,但每当用户发出读取请求时,它将读取所有拷贝以返回结果。如果任一拷贝未更新,它将确保更新相同的拷贝,然后只有读取会成功。这种实现会降低读取性能。
- 因此,大多数 NoSQL 实现使用 N>W>1。这意味着需要成功更新不止一个节点;然而,并非所有节点都需要同时更新。
- 读取操作
- 如果 R 设置为 1,读取操作将读取任何可能过时的数据副本。如果 R>1,则读取多个副本,并将读取最近的值。但是,这可能会降低读取操作的速度。
- 使用 N
表 2-1 比较了酸和碱。
表 2-1。
ACID vs. BASE
| 酸 | 基础 | | --- | --- | | 原子数 | 基本可用 | | 一致性 | 最终一致性 | | 隔离 | 柔软状态 | | 持久耐用 | |2.4 NoSQL 的优势和劣势
在这一节中,您将看到 NoSQL 数据库的优点和缺点。
2 . 4 . 1 NoSQL 的优势
让我们谈谈 NoSQL 数据库的优势。
- 高可伸缩性:当事务率和快速响应需求增加时,这种向上扩展的方法就会失败。与此相反,新一代 NoSQL 数据库旨在向外扩展(即使用低端商用服务器进行水平扩展)。
- 可管理性和管理性:NoSQL 数据库主要用于自动修复、分布式数据和更简单的数据模型,导致可管理性和管理性较低。
- 低成本:NoSQL 数据库通常被设计成与廉价的商用服务器集群一起工作,使用户能够以较低的成本存储和处理更多的数据。
- 灵活的数据模型:NoSQL 数据库有一个非常灵活的数据模型,使它们能够处理任何类型的数据;它们不符合严格的 RDBMS 数据模型。因此,任何涉及更新数据库模式的应用更改都可以轻松实现。
NoSQL 的缺点
除了上面提到的优势之外,在开始使用这些平台开发应用之前,您还需要了解许多障碍。
- 成熟度:大多数 NoSQL 数据库都是预生产版本,其关键特性仍有待实现。因此,在决定使用 NoSQL 数据库时,您应该对产品进行适当的分析,以确保这些特性被完全实现,而不是仍然在待办事项列表中。
- 支持:支持是你需要考虑的一个限制。大多数 NoSQL 数据库来自开源的初创企业。因此,与企业软件公司相比,支持是非常少的,并且可能没有全球影响力或支持资源。
- 有限的查询能力:由于 NoSQL 数据库通常是为了满足 web 级应用的伸缩需求而开发的,所以它们提供的查询能力有限。一个简单的查询需求可能涉及大量的编程专业知识。
- 管理:尽管 NoSQL 旨在提供一个无管理的解决方案,但它仍然需要技巧和精力来安装和维护该解决方案。
- 专业知识:由于 NoSQL 是一个不断发展的地区,开发者和管理员社区对该技术的专业知识非常有限。
虽然 NoSQL 正在成为数据库领域的重要组成部分,但是您需要了解这些产品的局限性和优势,以便正确选择 NoSQL 数据库平台。
2.5 SQL 与 NoSQL 数据库
现在您已经了解了关于 NoSQL 数据库的细节。尽管 NoSQL 越来越多地被用作数据库解决方案,但它并不是要取代 SQL 或 RDBMS 数据库。在这一节中,您将看到 SQL 和 NoSQL 数据库之间的差异。
让我们快速回顾一下 RDBMS 系统。RDBMS 系统已经流行了大约 30 年,甚至现在它们还是应用数据存储解决方案架构师的默认选择。如果我们要列出 RDBMS 系统的几个优点,首先也是最重要的是 SQL 的使用,它是一种用于数据处理的丰富的声明式查询语言。很好的被用户理解。此外,RDBMS 系统提供了对事务的 ACID 支持,这在许多领域是必不可少的,例如银行应用。
然而,RDBMS 系统的最大缺点是,随着数据的增加,它很难处理模式变化和伸缩问题。随着数据的增加,读/写性能会下降。您面临 RDBMS 系统的伸缩问题,因为它们主要是为纵向扩展而不是横向扩展而设计的。
与 SQL RDBMS 数据库相反,NoSQL 提倡脱离 RDBMS 范式的数据存储。
让我们讨论一下技术场景,以及它们在 RDBMS 和 NoSQL 中的比较:
- 模式灵活性:这对于将来的增强和与外部应用(出站或入站)的集成是必不可少的。RDBMS 在设计上相当不灵活。添加列是绝对不允许的,尤其是当表中有一些数据时。原因包括默认值、索引和性能影响。通常,您最终会创建新的表,并通过引入跨表的关系来增加复杂性。
- 复杂的查询:传统的表设计导致开发者编写复杂的连接查询,这不仅难以实现和维护,而且需要大量的数据库资源来执行。
- 数据更新:跨表更新数据可能是比较复杂的场景之一,尤其是当它们是事务的一部分时。请注意,长时间保持事务打开会影响性能。您还必须计划将更新传播到系统中的多个节点。如果系统不支持多个主机或同时写入多个节点,则存在节点故障和整个应用进入只读模式的风险。
- 可伸缩性:通常唯一需要的可伸缩性是读操作。但是,随着运营的增长,有几个因素会影响这一速度。要问的一些关键问题是:基于 NoSQL 的解决方案为上面列出的大多数挑战提供了答案。现在让我们来看看 NoSQL 对上面提到的每个技术问题提供了什么。
- 跨物理数据库实例同步数据需要多长时间?
- 跨数据中心同步数据需要多长时间?
- 同步数据的带宽要求是什么?
- 交换的数据优化了吗?
- 跨服务器同步任何更新时的延迟是多少?通常,记录会在更新期间被锁定。NoSQL-based solutions provide answers to most of the challenges listed above.Let’s now see what NoSQL has to offer against each technical question mentioned above.
- 模式灵活性:面向列的数据库将数据存储为列,而不是 RDBMS 中的行。这允许根据需要动态地添加一个或多个列。类似地,允许存储半结构化数据的文档存储也是不错的选择。
- 复杂查询:NoSQL 数据库不支持关系或外键。没有复杂的查询。没有联接语句。这是缺点吗?如何跨表查询?毫无疑问,这是一个功能上的缺陷。要跨表查询,必须执行多个查询。数据库是一种共享资源,跨应用服务器使用,不能尽快停止使用。这些选项包括简化要执行的查询、缓存数据和在应用层执行复杂操作的组合。许多数据库都提供内置的实体级缓存。这意味着当一个记录被访问时,它可以被数据库自动透明地缓存。为了性能和规模,缓存可以是内存中分布式缓存。
- 数据更新:跨物理实例的数据更新和同步是很难解决的工程问题。与跨多个数据中心同步相比,数据中心内节点间的同步有一组不同的要求。人们希望延迟最好在几毫秒或几十毫秒之内。NoSQL 解决方案提供了很好的同步选项。例如,MongoDB 允许跨节点的并发更新、带冲突解决的同步,以及最终在几毫秒内运行的可接受时间内跨数据中心的一致性。因此,MongoDB 没有隔离的概念。注意,现在因为管理事务的复杂性可能从数据库中移出,应用将不得不做一些艰苦的工作。这方面的一个例子是在实现事务时的两阶段提交(
http://docs.mongodb.org/manual/tutorial/perform-two-phase-commits/
)。大量数据库提供多版本并发控制(MCC)来实现事务一致性。正如易贝大学的技术研究员丹·普里切特所说,eBay.com 不使用事务。注意,PayPal 确实使用事务。 - 可伸缩性:NoSQL 解决方案提供了更好的可伸缩性,原因显而易见。面向事务的 RDBMS 所需的许多复杂性在不符合 ACID 的 NoSQL 数据库中并不存在。有趣的是,因为 NoSQL 不提供跨表引用,也不可能有连接查询,而且因为您不能编写一个查询来比较多个表中的数据,所以一个简单而合理的解决方案是——有时——跨表复制数据。在某些情况下,将信息嵌入到主实体中——尤其是在一对一映射的情况下——可能是个好主意。
表 2-2 比较了 SQL 和 NoSQL 技术。
表 2-2。
SQL vs. NoSQL
| | SQL 数据库 | NoSQL 数据库 | | --- | --- | --- | | 类型 | 所有类型都支持 SQL 标准。 | 存在多种类型,如文档存储、键值存储、列数据库等。 | | 发展历史 | 开发于 1970 年。 | 开发于 2000 年代。 | | 例子 | SQL Server、Oracle、MySQL。 | 蒙戈布,巴塞,卡珊德拉。 | | 数据存储模型 | 数据存储在表的行和列中,其中每一列都有特定的类型。这些表格通常是根据标准化原则创建的。联接用于从多个表中检索数据。 | 数据模型取决于数据库类型。比方说,数据存储为键值存储的键值对。在基于文档的数据库中,数据存储为文档。与 RDBMS 的僵硬的表模型相比,数据模型是灵活的。 | | 计划 | 固定的结构和模式,因此对模式的任何更改都会涉及到数据库的更改。 | 动态模式、新的数据类型或结构可以通过扩展或改变当前模式来适应。可以动态添加新字段。 | | 可量测性 | 使用放大方法;这意味着随着负载的增加,需要购买更大、更贵的服务器来容纳数据。 | 使用横向扩展方法;这意味着将数据负载分布在廉价的商用服务器上。 | | 支持事务 | 支持 ACID 和事务。 | 支持分区和可用性,以及对事务的妥协。事务存在于特定的级别,例如数据库级别或文档级别。 | | 一致性 | 一致性强。 | 取决于产品。很少有人选择提供强一致性,而很少有人提供最终一致性。 | | 支持 | 提供高水平的企业支持。 | 开源模式。通过构建开源产品的第三方或公司提供支持。 | | 成熟 | 已经存在很久了。 | 有的是成熟的;其他的在进化。 | | 查询功能 | 通过易于使用的 GUI 界面提供。 | 查询可能需要编程技能和知识。而不是一个用户界面,重点是功能和编程接口。 | | 专家的意见 | 利用 SQL 语言和 RDBMS 概念来设计和开发应用的大型开发者社区。 | 开发这些开源工具的小型开发者社区。 |2.6 数据库的类别
在这一部分,你将快速探索 NoSQL 的风景。你会看到 NoSQL 数据库的新兴类别。表 2-3 显示了 NoSQL 景观中的几个项目,以及每个类别中的类型和参与者。
表 2-3。
NoSQL Categories
| 种类 | 简要描述 | 例如 | | --- | --- | --- | | 基于文档的 | 数据以文档的形式存储。例如,{Name= "测试用户",Address="Address1 ",年龄:8} | MongoDB | | XML 数据库 | XML 用于存储数据。 | MarkLogic | | 图形数据库 | 数据存储为节点集合。节点通过边连接。节点相当于编程语言中的对象。 | GraphDB(图形数据库) | | 键值存储 | 将数据存储为键值对。 | Cassandra 再说一遍 memcached |NoSQL 数据库根据数据的存储方式进行分类。NoSQL 大多遵循水平结构,因为需要提供大量精选信息,通常是近实时的。它们针对大规模的插入和检索操作进行了优化,内置了复制和集群功能。
表 2-4 简要提供了不同类别的 NoSQL 数据库之间的特性比较。
表 2-4。
Feature Comparison
| 特征 | 面向列 | 文档存储 | 键值存储 | 图表 | | --- | --- | --- | --- | --- | | 类似表格的模式支持(列) | 是 | 不 | 不 | 是 | | 完成更新/提取 | 是 | 是 | 是 | 是 | | 部分更新/获取 | 是 | 是 | 是 | 不 | | 对值进行查询/过滤 | 是 | 是 | 不 | 是 | | 跨行聚合 | 是 | 不 | 不 | 不 | | 实体之间的关系 | 不 | 不 | 不 | 是 | | 跨实体视图支持 | 不 | 是 | 不 | 不 | | 批量提取 | 是 | 是 | 是 | 是 | | 批量更新 | 是 | 是 | 是 | 不 |在考虑 NoSQL 项目时,重要的是您感兴趣的特性集。当决定使用 NoSQL 的产品时,首先你需要非常仔细地理解问题需求,然后你应该看看其他已经使用 NoSQL 产品解决类似问题的人。请记住,NoSQL 仍在不断成熟,因此这将使您能够从同行和以前的部署中学习,并做出更好的 choi ces。
此外,你还需要考虑以下问题。
- 需要处理的数据有多大?
- 读写的吞吐量是多少?
- 如何在系统中实现一致性?
- 系统需要支持高写性能还是高读性能?
- 可维护性和管理性有多容易?
- 需要查询什么?
- 使用 NoSQL 的好处是什么?
我们建议您从小处着手,但意义重大,并尽可能考虑混合方法。
2.7 摘要
在这一章中,你了解了 NoSQL。您现在应该明白什么是 NoSQL,以及它与 SQL 有何不同。你还研究了 NoSQL 的各种类别。
在接下来的章节中,您将了解 MongoDB,这是一个基于文档的 NoSQL 数据库。
三、MongoDB 简介
"MongoDB is one of NoSQL's leading document storage databases. It enables organizations to process big data and gain meaningful insights from it. "
一些领先的企业和消费者 IT 公司已经在其产品和解决方案中利用了 MongoDB 的功能。MongoDB 3.0 版本引入了可插拔存储引擎和 Ops Manager,这扩展了最适合 MongoDB 的应用集。
MongoDB 的名字来源于单词“humungous”。像其他 NoSQL 数据库一样,MongoDB 也不符合 RDBMS 原则。它没有表、行和列的概念。此外,它不提供 ACID 遵从性、连接、外键等特性。
MongoDB 将数据存储为二进制 JSON 文档(也称为 BSON)。文档可以有不同的模式,这意味着模式可以随着应用的发展而改变。MongoDB 是为可伸缩性、性能和高可用性而构建的。
在这一章中,我们将讨论一下 MongoDB 的创建和设计决策。我们将在接下来的章节中研究 MongoDB 的关键特性、组件和架构。
3.1 历史
2007 年下半年,Dwight Merriman、Eliot Horowitz 和他们的团队决定开发一个在线服务。该服务的目的是为开发、托管和自动扩展 web 应用提供一个平台,这与 Google App Engine 或 Microsoft Azure 等产品非常相似。很快他们意识到没有开源数据库平台适合这项服务的需求。
梅里曼说:“我们觉得许多现有的数据库并没有真正具备你希望它们具备的‘云计算’原则:弹性、可伸缩性,以及……易于管理,同时也便于开发者和运营商使用。”。“[MySQL]不具备所有这些特性。”所以他们决定建立一个不符合 RDBMS 模型的数据库。
一年后,该服务的数据库就可以使用了。该服务本身从未发布,但该团队在 2009 年决定将数据库开源为 MongoDB。2010 年 3 月,MongoDB 1.4.0 的发布被认为是生产就绪的。最新的生产版本是 3.0,于 2015 年 3 月发布。MongoDB 是在纽约初创公司 10gen 的赞助下建立的。
3.2 MongoDB 设计理念
在他的一次演讲中,Eliot Horowitz 提到 MongoDB 不是在实验室中设计的,而是根据构建大规模、高可用性和健壮系统的经验构建的。在这一节中,我们将简要地看一下导致 MongoDB 今天这个样子的一些设计决策。
3.2.1 速度、可扩展性和敏捷性
设计团队在设计 MongoDB 时的目标是创建一个快速、大规模可伸缩且易于使用的数据库。为了在分区数据库中实现速度和水平可伸缩性,正如 CAP 定理中所解释的,一致性和事务支持必须折衷。因此,根据这个定理,MongoDB 以一致性和事务支持为代价提供了高可用性、可伸缩性和分区。实际上,这意味着 MongoDB 使用文档而不是表和行来使其灵活、可伸缩和快速。
非关系方法
传统的 RDBMS 平台使用纵向扩展方法提供可伸缩性,这需要更快的服务器来提高性能。RDBMS 系统中的以下问题导致了 MongoDB 和其他 NoSQL 数据库的设计方式:
- 为了向外扩展,RDBMS 数据库需要链接两个或更多系统中的可用数据,以便报告结果。这在 RDBMS 系统中很难实现,因为它们被设计成当所有数据都可以一起计算时才工作。因此,数据必须可用于在单个位置进行处理。
- 在多台主动-主动服务器的情况下,当两台服务器都从多个来源获取更新时,很难确定哪个更新是正确的。
- 当应用尝试从第二个服务器读取数据,并且信息已经在第一个服务器上更新,但尚未与第二个服务器同步时,返回的信息可能是陈旧的。
MongoDB 团队决定采用非关系方法来解决这些问题。如前所述,MongoDB 将其数据存储在 BSON 文档中,所有相关数据都放在一起,这意味着一切都在一个地方。MongoDB 中的查询基于文档中的键,因此文档可以分布在多个服务器上。查询每个服务器意味着它将检查自己的文档集并返回结果。这实现了线性可伸缩性和改进的性能。
MongoDB 有一个主-从复制,主服务器接受写请求。如果写性能需要提高,那么可以使用分片;这将数据分割到多台机器上,使这些机器能够更新数据集的不同部分。在 MongoDB 中分片是自动的;随着机器数量的增加,数据会自动分发。
基于 JSON 的文档存储
MongoDB 使用基于 JSON(JavaScript 对象表示法)的文档存储来存储数据。JSON/BSON 提供了一个无模式模型,在数据库设计方面提供了灵活性。与 RDBMSs 不同,可以无缝地对模式进行更改。
这种设计还通过在内部将相关数据分组在一起并使其易于搜索来实现高性能。
JSON 文档包含实际数据,相当于 SQL 中的一行。然而,与 RDBMS 行相反,文档可以有动态模式。这意味着集合中的文档可以有不同的字段或结构,或者公共字段可以有不同类型的数据。
文档包含键值对形式的数据。让我们用一个例子来理解这一点:
{
"Name": "ABC",
"Phone": ["1111111",
........"222222"
........],
"Fax":..
}
如上所述,键和值是成对出现的。文档中的键值可以留空。在上面的例子中,文档有三个键,即“姓名”、“电话”和“传真”“传真”键没有价值。
性能与功能对比
为了让 MongoDB 高性能、快速度,RDBMS 系统中常见的某些特性在 MongoDB 中是没有的。MongoDB 是一个面向文档的 DBMS,其中数据存储为文档。它不支持连接,也没有完全一般化的事务。但是,它确实提供了对二级索引的支持,它使用户能够使用查询文档进行查询,并且它提供了对每个文档级别的原子更新的支持。它提供了一个副本集,这是一种具有自动故障转移的主从复制形式,并且它具有内置的水平伸缩功能。
3.2.5 在任何地方运行数据库
一个主要的设计决策是从任何地方运行数据库的能力,这意味着它应该能够运行在服务器、虚拟机甚至使用按使用付费服务的云上。用于实现 MongoDB 的语言是 C++,这使得 MongoDB 能够实现这个目标。10gen 站点为不同的操作系统平台提供了二进制文件,使得 MongoDB 可以在几乎任何类型的机器上运行。
3.3 SQL 比较
以下是 MongoDB 与 SQL 的不同之处。
MongoDB uses documents for storing its data, which offer a flexible schema (documents in same collection can have different fields). This enables the users to store nested or multi-value fields such as arrays, hashes, etc. In contrast, RDBMS systems offer a fixed schema where a column’s value should have a similar data type. Also, it’s not possible to store arrays or nested values in a cell. MongoDB doesn’t provide support for JOIN operations, like in SQL. However, it enables the user to store all relevant data together in a single document, avoiding at the periphery the usage of JOINs. It has a workaround to overcome this issue. We will be discussing this in more detail in a later chapter. MongoDB doesn’t provide support for transactions in the same way as SQL. However, it guarantees atomicity at the document level. Also, it uses an isolation operator to isolate write operations that affect multiple documents, but it does not provide “all-or-nothing” atomicity for multi-document write operations.
3.4 总结
在本章中,您将了解 MongoDB、它的历史以及 MongoDB 系统设计的简要细节。在接下来的章节中,您将了解更多关于 MongoDB 的数据模型。
Footnotes 1
The Register,Cade Metz,“MongoDB daddy:我的宝贝打败了 Google BigTable”,
www.theregister.co.uk/2011/05/25/the_once_and_future_mongodb/
,2011 年 5 月 25 日。
四、MongoDB 数据模型
“MongoDB 设计用于处理文档,不需要任何预定义的列或数据类型(不像关系数据库),这使得数据模型非常灵活。”
在本章中,您将了解 MongoDB 数据模型。您还将了解灵活模式(多态模式)的含义,以及为什么它是 MongoDB 数据模型的一个重要方面。
4.1 数据模型
在前一章中,您看到了 MongoDB 是一个基于文档的数据库系统,其中的文档可以有一个灵活的模式。这意味着集合中的文档可以有不同(或相同)的字段集。这为您处理数据提供了更大的灵活性。
在本章中,您将探索 MongoDB 灵活的数据模型。在任何需要的地方,我们将展示这种方法与 RDBMS 系统的不同之处。
一个 MongoDB 部署可以有许多数据库。每个数据库都是一组集合。集合类似于 SQL 中的表的概念;然而,它们是无模式的。每个集合可以有多个文档。将文档想象成 SQL 中的一行。图 4-1 描绘了 MongoDB 数据库模型。
图 4-1。
MongoDB database model
在 RDBMS 系统中,由于表结构和每列的数据类型是固定的,所以只能在一列中添加特定数据类型的数据。在 MongoDB 中,集合是文档的集合,其中数据存储为键值对。
让我们通过一个例子来理解数据是如何存储在文档中的。以下文档包含用户的姓名和电话号码:
{"Name": "ABC", "Phone": ["1111111", "222222" ] }
动态模式意味着同一集合中的文档可以有相同或不同的字段或结构集,甚至公共字段也可以跨文档存储不同类型的值。在集合的文档中存储数据的方式没有严格的限制。
让我们看一个区域集合的例子:
{ "R_ID" : "REG001", "Name" : "United States" }
{ "R_ID" :1234, "Name" : "New York", "Country" : "United States" }
在这段代码中,区域集合中有两个文档。虽然这两个文档都是一个集合的一部分,但是它们有不同的结构:第二个集合有一个额外的信息字段,即 country。事实上,如果您查看“R_ID”字段,它在第一个文档中存储一个字符串值,而在第二个文档中存储一个数字。
因此,一个集合的文档可以有完全不同的模式。应用需要将特定集合中的文档存储在一起,或者拥有多个集合。
4.1.1 JSON 和 BSON
MongoDB 是一个基于文档的数据库。它使用二进制 JSON 来存储数据。
在本节中,您将了解 JSON 和二进制 JSON (BSON)。JSON 代表 JavaScript 对象符号。它是当今现代网络中用于数据交换的标准(和 XML 一起)。该格式是人和机器可读的。这不仅是交换数据的好方法,也是存储数据的好方法。
JSON 支持所有基本的数据类型(比如字符串、数字、布尔值和数组)。
以下代码显示了 JSON 文档的样子:
{
"_id" : 1,
"name" : { "first" : "John", "last" : "Doe" },
"publications" : [
{
"title" : "First Book",
"year" : 1989,
"publisher" : "publisher1"
},
{ "title" : "Second Book",
"year" : 1999,
"publisher" : "publisher2"
}
]
}
JSON 允许您将所有相关的信息保存在一个地方,这提供了出色的性能。它还使文档的更新变得独立。这是无模式的。
4.1.1.1 二进制 JSON (BSON)
MongoDB 以二进制编码格式存储 JSON 文档。这被称为 BSON。BSON 数据模型是 JSON 数据模型的扩展形式。
MongoDB 对 BSON 文档的实现是快速的、高度可穿越的和轻量级的。它支持在其他数组中嵌入数组和对象,还支持 MongoDB 在对象内部构建索引,并根据查询的表达式匹配对象,包括顶级和嵌套的 BSON 键。
4.1.2 标识符(_id)
您已经看到 MongoDB 将数据存储在文档中。文档由键值对组成。虽然文档可以比作 RDBMS 中的行,但与行不同,文档具有灵活的模式。键只不过是一个标签,可以粗略地比作 RDBMS 中的列名。密钥用于从文档中查询数据。因此,就像 RDBMS 主键(用于惟一标识每一行)一样,您需要有一个惟一标识集合中每个文档的键。这在 MongoDB 中被称为 _id。
如果您没有为一个键显式指定任何值,MongoDB 将自动生成一个惟一的值并分配给它。这个键值是不可变的,可以是除数组之外的任何数据类型。
4.1.3 加盖收藏
您现在已经非常熟悉集合和文档了。我们来谈谈一种特殊类型的集合,叫做封顶集合。
MongoDB 有一个集合封顶的概念。这意味着它按照插入的顺序存储集合中的文档。当集合达到其限制时,文档将按 FIFO(先进先出)顺序从集合中移除。这意味着最近最少插入的文档将首先被删除。
这对于需要自动维护插入顺序以及需要在固定大小后删除记录的用例来说很好。一个这样的用例是在达到一定大小后自动截断的日志文件。
Note
MongoDB 本身使用上限集合来维护其复制日志。Capped 集合保证了按插入顺序保存数据,因此按插入顺序检索数据的查询可以快速返回结果,并且不需要索引。不允许更改文档大小的更新。
4.2 多态模式
既然您已经熟悉了 MongoDB 数据结构的无模式特性,现在让我们来探索多态模式和用例。
多态模式是一种模式,其中集合具有不同类型或模式的文档。这种模式的一个很好的例子是名为 Users 的集合。一些用户文档可能有额外的传真号码或电子邮件地址,而其他用户文档可能只有电话号码,然而所有这些文档都共存于同一个用户集合中。这种模式通常被称为多态模式。
在本章的这一部分,你将探究使用多态模式的各种原因。
面向对象的编程
面向对象编程使您能够使用继承让类共享数据和行为。它还允许您在父类中定义可以在子类中覆盖的函数,从而在不同的上下文中以不同的方式运行。换句话说,您可以使用相同的函数名来操作子类和父类,尽管在底层实现可能会有所不同。这种特性被称为多态性。
在这种情况下,要求能够拥有一个模式,其中所有相关的对象集或层次结构中的对象集可以放在一起,并且还可以进行相同的检索。
让我们考虑一个例子。假设您有一个应用,允许用户上传和共享不同的内容类型,如 HTML 页面、文档、图像、视频等。尽管许多字段在上述所有内容类型中都是通用的(如姓名、ID、作者、上传日期和时间),但并非所有字段都是相同的。例如,对于图像,您有一个保存图像内容的二进制字段,而 HTML 页面有一个保存 HTML 内容的大文本字段。
在这个场景中,可以使用 MongoDB 多态模式,其中所有的内容节点类型都存储在同一个集合中,比如 LoadContent,每个文档只有相关的字段。
// "Document collections" - "HTMLPage" document
{
_id: 1,
title: "Hello",
type: "HTMLpage",
text: "<html>Hi..Welcome to my world</html>"
}
...
// Document collection also has a "Picture" document
{
_id: 3,
title: "Family Photo",
type: "JPEG",
sizeInMB: 10,........
}
这种模式不仅使您能够将不同结构的相关数据存储在同一个集合中,还简化了查询。同一个集合可用于对常见字段执行查询,例如获取在特定日期和时间上传的所有内容,以及对特定字段执行查询,例如查找大小大于 X MB 的图像。
因此,面向对象编程是具有多态模式有意义的用例之一。
模式演变
当您使用数据库时,您需要考虑的最重要的事项之一是模式演变(即模式的变化对正在运行的应用的影响)。设计应该以对应用影响最小或没有影响的方式进行,也就是说没有或只有很少的停机时间,没有或只有很少的代码更改,等等。
通常,模式演变是通过执行迁移脚本将数据库模式从旧版本升级到新版本来实现的。如果数据库不在生产环境中,脚本可以简单地删除并重新创建数据库。但是,如果数据库在生产环境中,并且包含实时数据,迁移脚本将会很复杂,因为需要保留数据。剧本应该考虑到这一点。虽然 MongoDB 提供了一个更新选项,如果添加了一个新的字段,可以使用它来更新集合中所有文档的结构,但是想象一下,如果集合中有数千个文档,那么这样做会产生什么影响。它会非常慢,并且会对底层应用的性能产生负面影响。其中一种方法是将新的结构包含到添加到集合中的新文档中,然后在应用仍在运行时逐渐在后台迁移集合。这是拥有多态模式将带来优势的众多用例之一。
例如,假设您正在处理一个票据集合,其中有包含票据详细信息的文档,如下所示:
// "Ticket1" document (stored in "Tickets" collection")
{
_id: 1,
Priority: "High",
type: "Incident",
text: "Printer not working"
}...........
在某个时候,应用团队决定在票据文档结构中引入一个“简短描述”字段,所以最好的替代方法是在新的票据文档中引入这个新字段。在应用中,嵌入一段代码来处理检索“旧样式”文档(没有简短描述字段)和“新样式”文档(有简短描述字段)。旧样式文档可以逐渐迁移到新样式文档。迁移完成后,如果需要,可以更新代码以删除嵌入的用于处理缺失字段的代码。
4.3 总结
在本章中,您学习了 MongoDB 数据模型。您还了解了标识符和上限集合。在本章结束时,您已经理解了灵活的模式是如何帮助您的。
在下一章中,您将开始使用 MongoDB。您将执行 MongoDB 的安装和配置。
五、MongoDB 安装和配置
"MongoDB is a cross-platform database."
在本章中,您将了解在 Windows 和 Linux 上安装 MongoDB 的过程。
5.1 选择您的版本
MongoDB 运行在大多数平台上。MongoDB 下载页面上的 www.mongodb.org/downloads
提供了所有可用包的列表。
适合您环境的正确版本将取决于服务器的操作系统和处理器的种类。MongoDB 支持 32 位和 64 位架构,但是建议在您的生产环境中使用 64 位架构。
32-bit limitation
这是因为在 MongoDB 中使用了内存映射文件。这将 32 位版本的数据限制在 2GB 左右。出于性能原因,建议在生产环境中使用 64 位版本。
在撰写本书时,最新的 MongoDB 生产版本是 3.0.4。MongoDB 的下载适用于 Linux、Windows、Solaris 和 Mac OS X。
MongoDB 下载页面分为以下几个部分:
- 当前稳定版本(3 . 0 . 4)–2015 年 6 月 16 日
- 以前的版本(稳定)
- 开发版本(不稳定)
当前版本是目前最稳定的最新版本,在撰写本书时是 3.0.4。当一个新版本发布时,先前的稳定版本被移到先前版本部分。
开发版本,顾名思义,是仍在开发中的版本,因此被标记为不稳定。这些版本可以有额外的功能,但它们可能不稳定,因为它们仍处于开发阶段。您可以使用开发版本来尝试新功能,并向 10gen 提供有关功能和面临的问题的反馈。
5.2 在 Linux 上安装 MongoDB
本节介绍在 LINUX 系统上安装 MongoDB。在下面的演示中,我们将使用 Ubuntu Linux 发行版。您可以手动或通过存储库安装 MongoDB。我们将向您介绍这两个选项。
5.2.1 使用资料库安装
在 LINUX 中,存储库是包含软件的在线目录。Aptitude 是用来在 Ubuntu 上安装软件的程序。尽管 MongoDB 可能存在于默认的存储库中,但也有可能是过时的版本,所以第一步是配置 Aptitude 来查看定制的存储库。
Issue the following to import the public.GPG
key for MongoDB: sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
Next, use the following command to create the /etc/apt/sources.list.d/mongodb-org-3.0.list
file: echo "deb
http://repo.mongodb.org/apt/ubuntu
"$(lsb_release -sc)"/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.0.list
Finally, use the following command to reload the repository: sudo apt-get update
Now Aptitude is aware of the manually added repository. Next, you need to install the software. The following command should be issued in the shell to install MongoDB’s current stable version: sudo apt-get install -y mongodb-org
您已经成功安装了 MongoDB,这就是它的全部内容。
手动安装
在本节中,您将看到如何手动安装 MongoDB。这一知识在以下情况下很重要:
- 当 Linux 发行版不使用 Aptitude 时。
- 当您需要的版本无法通过存储库获得或者不属于存储库时。
- 当你需要同时运行多个 MongoDB 版本时。
手动安装的第一步是决定要使用的 MongoDB 版本,然后从站点下载。接下来,需要使用以下命令提取包:
$ tar -xvf mongodb-linux-x86_64-3.0.4.tgz
mongodb-linux-i686-3.0.4/THIRD-PARTY-NOTICES
mongodb-linux-i686-3.0.4/GNU-AGPL-3.0
mongodb-linux-i686-3.0.4/bin/mongodump
............
mongodb-linux-i686-3.0.4/bin/mongosniff
mongodb-linux-i686-3.0.4/bin/mongod
mongodb-linux-i686-3.0.4/bin/mongos
mongodb-linux-i686-3.0.4/bin/mongo
这会将包内容提取到一个新目录中,即mongodb-linux-x86_64-3.0.4
(位于当前目录下)。该目录包含许多子目录和文件。主可执行文件在子目录bin
下。
这就成功完成了 MongoDB 的安装。
5.3 在 Windows 上安装 MongoDB
在 Windows 上安装 MongoDB 很简单,只需下载所选 Windows 版本的 msi 文件并运行安装程序。
安装程序将引导您完成 MongoDB 的安装。
按照向导,您将到达选择安装类型屏幕。有两种安装类型可供您自定义安装。在本例中,选择“自定义”安装类型。
选择自定义时需要指定安装目录,所以将目录指定到C:\PracticalMongoDB
。
注意,MongoDB 可以从用户选择的任何文件夹中运行,因为它是自包含的,不依赖于系统。如果选择了完整的设置类型,则默认选择的文件夹是C:\Program Files\MongoDB
。
单击“下一步”将带您进入“准备安装”屏幕。单击安装。
这将开始安装,并在屏幕上显示进度。安装完成后,向导将带您进入完成屏幕。
单击“完成”完成设置。成功完成上述步骤后,您就有了一个名为C:\PracticalMongoDB
的目录,在bin
文件夹中有所有相关的应用。这就是全部了。
5.4 运行 MongoDB
让我们看看如何开始运行和使用 MongoDB。
先决条件
存储文件需要一个数据文件夹。这在 Windows 中默认为C:\data\db
,在 LINUX 系统中默认为/data/db
。
这些数据目录不是由 MongoDB 创建的,因此在启动 MongoDB 之前,需要手动创建数据目录,并且您需要确保设置了适当的权限(例如 MongoDB 具有读、写和目录创建权限)。
如果在创建文件夹之前启动 MongoDB,它将抛出一条错误消息,并且无法运行。
5.4.2 启动服务
一旦创建了目录并获得了权限,就执行 mongod 应用(位于bin
目录下)来启动 MongoDB 核心数据库服务。
继续上述安装,可以通过在 Windows 中打开命令提示符(需要以管理员身份运行)并执行以下命令来启动:
c:\>
c:\practicalmongodb\bin\mongod.exe
对于 Linux,mongod 进程是在 shell 中启动的。
这将在本地主机界面上启动 MongoDB 数据库。它将在端口 27017 上侦听来自 mongo shell 的连接。
如上所述,需要在启动数据库之前创建文件夹路径,默认情况下是c:\data\db
。使用–dbpath 参数启动数据库服务时,也可以提供备用路径。
C``:\>``C:\``practicalmongodb
C:\NewDBPath\DBContents
5.5 验证安装
相关的可执行文件将出现在子目录bin
下。为了检查安装步骤是否成功,可以在bin
目录下检查以下内容:
- Mongod:核心数据库服务器
- Mongo:数据库 Shell
- Mongos:自动分片过程
- Mongoexport:导出实用程序
- Mongoimport:导入实用程序
除此之外,bin
文件夹中还有其他应用。
mongo 应用启动 mongo shell,它提供对数据库内容的访问,并允许您对 MongoDB 中的数据执行选择性查询或聚合。
如上所述,mongod 应用用于启动数据库服务或守护进程。
启动应用时,可以设置多个标志。例如,–dbpath
可用于指定存储数据库文件的替代路径。要获得所有可用选项的列表,请在启动服务时包含--help
标志。
5.6 蒙戈布 Shell
mongo shell 是 MongoDB 标准发行版的一部分。shell 为 MongoDB 提供了一个完整的数据库接口,使您能够使用 JavaScript 环境处理存储在 MongoDB 中的数据,该环境可以完全访问该语言和所有标准函数。
一旦数据库服务启动,您就可以启动 mongo shell 并开始使用 MongoDB。这可以使用 Linux 中的 Shell 或 Windows 中的命令提示符来完成(以管理员身份运行)。
您必须指出可执行文件的确切位置,例如在 Windows 环境中的C:\practicalmongodb\bin\
文件夹中。
打开命令提示符(以管理员身份运行)并键入mongo.exe
。按回车键。这将启动 mongo shell。
C:\>
C:\practicalmongodb\bin\mongo.exe
MongoDB shell version: 3.0.4
connecting to: test
>
如果在启动服务时没有指定参数,它将连接到 localhost 实例上名为test
的默认数据库。
当连接到数据库时,将自动创建数据库。如果试图访问一个不存在的数据库,MongoDB 提供了自动创建数据库的特性。
下一章提供了更多关于使用 mongo shell 的信息。
5.7 保护部署
您知道如何通过默认配置安装和开始使用 MongoDB。接下来,您需要确保存储在数据库中的数据在各个方面都是安全的。
在本节中,您将了解如何保护您的数据。您将更改默认安装的配置,以确保您的数据库更加安全。
5.7.1 使用认证和授权
身份验证验证用户的身份,而授权确定用户可以对经过身份验证的数据库执行的操作级别。
这意味着用户只有在使用有权访问数据库的凭证登录时才能访问数据库。这将禁用对数据库的匿名访问。用户通过身份验证后,可以使用授权来确保用户只拥有完成手头任务所需的访问权限。
身份验证和授权都存在于每个数据库级别。用户存在于单个逻辑数据库的上下文中。
用户信息保存在 admin 数据库中的一个名为system.users
的集合中。该集合维护对用户进行身份验证所需的凭证,其中存储了用户 id、密码和创建该集合所依据的数据库,以及授权用户所需的特权。
MongoDB 使用基于角色的方法进行授权(read、readWrite、readAnyDatabase 等角色)。).如果需要,用户管理员可以创建自定义角色。
system.users
集合中的特权文档用于存储每个用户角色。同一文档维护经过身份验证的用户的凭证。
system.users
集合中的文档示例如下:
{
_id : "practicaldb.Shaks",
user : "Shaks",
db : "practicaldb",
credentials : {.......},
roles : [
{ role: "read", db: "practicaldb" },
{ role: "readWrite", db: "MyDB" }
],
......
}
这个文档告诉我们,用户 Shaks 与数据库practicaldb
相关联,它在practicaldb
数据库中有读取角色,在MyDB
数据库中有读写角色。请注意,用户名和关联的数据库唯一地标识了 MongoDB 中的一个用户,因此,如果有两个同名的用户,但是他们与不同的数据库相关联,那么他们将被视为两个唯一的用户。因此,一个用户可以在不同的数据库上拥有多个具有不同授权级别的角色。
可用的角色有
- read:这提供了对指定数据库的所有集合的只读访问。
- readWrite:这提供了对指定数据库中任何集合的读写访问。
- dbAdmin:这使用户能够在指定的数据库中执行管理操作,例如使用 ensureIndex、dropIndexes、reIndex、indexStats 进行索引管理、重命名集合、创建集合等。
- userAdmin:这使用户能够对指定数据库的
system.users
集合执行读写操作。它还支持更改现有用户的权限或创建新用户。这实际上是指定数据库的超级用户角色。 - clusterAdmin:该角色使用户能够授予更改或显示整个系统信息的管理操作的访问权限。clusterAdmin 仅适用于管理数据库。
- readAnyDatabase:该角色使用户能够读取 MongoDB 环境中的任何数据库。
- readWriteAnyDatabase:这个角色类似于 readWrite,只是它适用于所有数据库。
- userAdminAnyDatabase:该角色类似于 userAdmin 角色,只是它适用于所有数据库。
- dbAdminAnyDatabase:该角色与 dbAdmin 相同,只是它适用于所有数据库。
- 从 2.6 版开始,用户管理员还可以创建用户定义的角色,通过提供集合级别和命令级别的访问权限来遵守最低权限策略。用户定义角色的范围是创建它的数据库,并且由数据库和角色名称的组合唯一标识。所有用户定义的角色都存储在
system.roles
集合中。
5.7.1.1 启用身份验证
默认情况下,身份验证是禁用的,因此使用--auth
来启用身份验证。启动 mongod 的同时,使用mongod --auth
。在启用身份验证之前,您需要至少有一个管理员用户。如上所述,管理员用户是负责创建和管理其他用户的用户。
建议在生产部署中,此类用户仅用于管理用户,不应用于任何其他角色。在 MongoDB 部署中,这个用户是需要创建的第一个用户;该用户可以创建系统的其他用户。
管理员用户可以通过两种方式创建:在启用身份验证之前或之后。
在本例中,您将首先创建 admin 用户,然后启用 auth 设置。以下步骤应在 Windows 平台上执行。
使用默认设置启动 mongod:
C:\>C:\practicalmongodb\bin\mongod.exe
C:\practicalmongodb\bin\mongod.exe --help for help and startup options
2015-07-03T23:11:10.716-0700 I CONTROL Hotfix KB2731284 or later update is installed, no need to zero out data files
2015-07-03T23:11:10.716-0700 I JOURNAL [initandlisten] journal dir=C:\data\db\journal
...................................................
2015-07-03T23:11:10.763-0700 I CONTROL [initandlisten] MongoDB starting : pid=2776 port=27017 dbpath=C:\data\db\ 64-bit host=ANOC9
2015-07-03T23:11:10.763-0700 I CONTROL [initandlisten] targetMinOS: Windows 7/W
indows Server 2008 R2
2015-07-03T23:11:10.763-0700 I CONTROL [initandlisten] db version v3.0.4
2015-07-03T23:11:10.764-0700 I CONTROL [initandlisten] OpenSSL version: OpenSSL 1.0.1j-fips 19 Mar 2015
2015-07-03T23:11:10.764-0700 I CONTROL [initandlisten] build info: windows sys. getwindowsversion(major=6, minor=1, build=7601, platform=2, service_pack='Service Pack 1') BOOST_LIB_VERSION=1_49
2015-07-03T23:11:10.771-0700 I NETWORK [initandlisten] waiting for connections
on port 27017
5.7.1.2 创建管理员用户
以管理员身份运行命令提示符的另一个实例,并执行 mongo 应用:
C:\>
C:\practicalmongodb\bin\mongo.exe
MongoDB shell version: 3.0.4
connecting to: test
>
切换到管理数据库
Note
admin db 是一个特权数据库,用户需要访问它才能执行某些管理命令,比如创建 admin 用户。
>
db = db.getSiblingDB('admin')
管理
需要使用以下角色之一创建用户:userAdminAnyDatabase 或 userAdmin:
>db.createUser({user: "AdminUser", pwd: "password", roles:["userAdminAnyDatabase"]})
Successfully added user: { "user" : "AdminUser", "roles" : [ "userAdminAnyDatabase" ] }
接下来,使用该用户进行身份验证。使用auth
设置重启 mongod:
C:\>
C:\practicalmongodb\bin\mongod.exe -auth
C:\practicalmongodb\bin\mongod.exe --help for help and startup options
2015-07-03T23:11:10.716-0700 I CONTROL Hotfix KB2731284 or later update is installed, no need to zero out data files
2015-07-03T23:11:10.716-0700 I JOURNAL [initandlisten] journal dir=C:\data\db\journal
...................................................
2015-07-03T23:11:10.763-0700 I CONTROL [initandlisten] MongoDB starting : pid=2776 port=27017 dbpath=C:\data\db\ 64-bit host=ANOC9
2015-07-03T23:11:10.763-0700 I CONTROL [initandlisten] targetMinOS: Windows 7/W
indows Server 2008 R2
2015-07-03T23:11:10.763-0700 I CONTROL [initandlisten] db version v3.0.4
2015-07-03T23:11:10.764-0700 I CONTROL [initandlisten] OpenSSL version: OpenSSL 1.0.1j-fips 19 Mar 2015
2015-07-03T23:11:10.764-0700 I CONTROL [initandlisten] build info: windows sys. getwindowsversion(major=6, minor=1, build=7601, platform=2, service_pack='Service Pack 1') BOOST_LIB_VERSION=1_49
2015-07-03T23:11:10.771-0700 I NETWORK [initandlisten] waiting for connections
on port 27017
启动 mongo 控制台,使用上面创建的 AdminUser 用户对管理数据库进行身份验证:
C:\>c:\practicalmongodb\bin\mongo.exe
MongoDB shell version: 3.0.4
connecting to: test
>use admin
switched to db admin
>db.auth("AdminUser", "password")
1
>
5.7.1.3 创建用户并启用授权
在本节中,您将创建一个用户,并为新创建的用户分配一个角色。您已经使用 admin 用户进行了身份验证,如下所示:
C:\>c:\practicalmongodb\bin\mongo.exe
MongoDB shell version: 3.0.4
connecting to: test
>use admin
switched to db admin
>db.auth("AdminUser", "password")
1
>
切换到Product
数据库,创建用户 Alice,并分配对产品数据库的读取权限,如下所示:
>
use product
switched to db product
>db.createUser({user: "Alice"
... , pwd:"Moon1234"
... , roles: ["read"]
... }
... )
Successfully added user: { "user" : "Alice", "roles" : [ "read" ] }
接下来,验证用户对数据库具有只读访问权限:
>db
product
>show users
{
"_id" : "product.Alice",
"user" : "Alice",
"db" : "product",
"roles" : [
{
"role" : "read",
"db" : "product"
}
]
}
接下来,连接到一个新的 mongo 控制台,以 Alice 的身份登录到Products
数据库,发出只读命令:
C:\>
c:\practicalmongodb\bin\mongo.exe -u Alice -p Moon1234 product
2015-07-03T23:11:10.716-0700 I CONTROL Hotfix KB2731284 or later update is installed, no need to zero-out data files
MongoDB shell version: 3.0.4
connecting to: products
Post successful authentication the following entry will be seen on the mongod console.
2015-07-03T23:11:26.742-0700 I ACCESS [conn2] Successfully authenticated as principal Alice on product
控制对网络的访问
默认情况下,mongod 和 mongos 绑定到系统上所有可用的 IP 地址。在本节中,您将了解用于限制网络暴露的配置选项。以下代码在 Windows 平台上执行:
C:\>
c:\practicalmongodb\bin\mongod.exe --bind_ip 127.0.0.1 --port 27017 --rest
2015-07-03T00:33:49.929-0700 I CONTROL Hotfix KB2731284 or later update is installed, no need to zero out data files
2015-07-03T00:33:49.946-0700 I JOURNAL [initandlisten] journal dir=C:\data\db\journal
2015-07-03T00:33:49.980-0700 I CONTROL [initandlisten] MongoDB starting : pid=1144 port=27017 dbpath=C:\data\db\ 64-bit host=ANOC9
2015-07-03T00:33:49.980-0700 I CONTROL [initandlisten] targetMinOS: Windows 7/Windows Server 2008 R2
2015-07-03T00:33:49.980-0700 I CONTROL [initandlisten] db version v3.0.4
2015-07-03T00:33:49.980-0700 I CONTROL [initandlisten] OpenSSL version: OpenSSL1.0.1j-fips 19 Mar 2015
2015-07-03T00:33:49.980-0700 I CONTROL [initandlisten] build info: windows sys.getwindowsversion(major=6, minor=1, build=7601, platform=2, service_pack='Service Pack 1') BOOST_LIB_VERSION=1_49
2015-07-03T00:33:49.981-0700 I CONTROL [initandlisten] allocator: system
2015-07-03T00:33:49.981-0700 I CONTROL [initandlisten] options: { net: { bindIp: "127.0.0.1", http: { RESTInterfaceEnabled: true, enabled: true }, port: 27017} }
2015-07-03T00:33:49.990-0700 I NETWORK [initandlisten] waiting for connections on port 27017
2015-07-03T00:33:49.990-0700 I NETWORK [websvr] admin web console waiting for connections on port 28017
2015-07-03T00:34:22.277-0700 I NETWORK [initandlisten] connection accepted from 127.0.0.1:49164 #1 (1 connection now open)
您已经用bind_ip
启动了服务器,它有一个值设置为 127.0.0.1,这是 localhost 接口。
bind_ip
限制程序将监听的输入连接的网络接口。可以指定逗号分隔的 IP 地址。在您的例子中,您已经将 mongod 限制为只监听 localhost 接口。
当 mongod 实例启动时,默认情况下,它会在端口 27017 上等待任何传入的连接。您可以使用–port
进行更改。
仅仅改变端口并不能降低太多风险。为了完全保护环境,您需要仅允许受信任的客户端使用防火墙设置连接到端口。
更改此端口也会更改 HTTP 状态接口端口,默认端口为 28017。此端口在 X+1000 端口上可用,其中 X 代表连接端口。
此网页公开了诊断和监控信息,其中包括运行数据、各种日志和有关数据库实例的状态报告。它提供可用于管理目的的管理级统计数据。默认情况下,此页面是只读的;为了使它完全交互,您将使用 REST 设置。这种配置使页面具有充分的交互性,有助于管理员解决任何性能问题。使用防火墙时,应该只允许可信客户端访问此端口。
建议在生产环境中禁用 HTTP 状态页面以及 REST 配置。
5.7.2.1 使用防火墙
防火墙用于控制网络中的访问。它们可用于允许从特定 IP 地址到特定 IP 端口的访问,或阻止来自任何不受信任主机的任何访问。它们可以用来为 mongod 实例创建一个可信的环境,在这个环境中,您可以指定哪些 IP 地址或主机可以连接到 mongod 的哪些端口或接口。
在 Windows 平台上,使用netsh
为端口 27017 配置传入流量:
C:\>
netsh advfirewall firewall add rule name="Open mongod port 27017" dir=in action=allow protocol=TCP localport=27017
Ok.
C:\>
这段代码说明所有的传入流量都允许通过端口 27017,因此任何应用服务器都可以连接到 mongod。
5.7.2.2 加密数据
您已经看到 MongoDB 将其所有数据存储在一个数据目录中,该目录在 Windows 中默认为C:\data\db
和/data/db
。文件以不加密的方式存储在目录中,因为在 Mongo 中没有提供自动加密文件的方法。任何具有文件系统访问权限的攻击者都可以读取文件中存储的数据。应用有责任确保敏感信息在写入数据库之前被加密。
此外,应实施操作系统级机制,如文件系统级加密和权限,以防止对文件的未授权访问。
5.7.2.3 加密通信
通常要求 mongod 和客户机(例如 mongo shell)之间的通信是加密的。在这个设置中,您将看到如何通过配置 SSL 为上述安装增加一个安全级别,以便 mongod 和 mongo shell(客户机)之间的通信使用 SSL 证书和密钥。
建议使用 SSL 进行服务器和客户端之间的通信。
从版本 3.0 开始,大多数 MongoDB 发行版现在都支持 SSL。以下命令在 Windows 平台上执行。
第一步是生成包含公钥证书和私钥的.pem
文件。MongoDB 可以使用自签名证书或由证书颁发机构颁发的任何有效证书。
在本书中,您将使用以下命令来生成自签名证书和私钥。
Install OpenSSL and Microsoft Visual C++ 2008 redistributable as per the MongoDB distribution and the Windows platform. In this book, you have installed the 64-bit version. Run the following command to create a public key certificate and a private key: C:\> cd c:\OpenSSL-Win64\bin
C:\OpenSSL-Win64\bin\>openssl
This opens the OpenSSL shell where you need to enter the following command: OpenSSL>req -new -x509 -days 365 -nodes -out C:\practicalmongodb\mongodb-cert.crt -keyout C:\practicalmongodb\mongodb-cert.key
The above step generates a certificate key named mongodb-cert.key
and places it in the C:\practicalmongodb
folder. Next, you need to concatenate the certificate and the private key to the .pem
file. In order to achieve this, run the following commands at the command prompt: C:\> more C:\practicalmongodb\mongodb-cert.key > temp
C:\> copy \b temp C:\practicalmongodb\mongodb-cert.crt > C:\practicalmongodb\mongodb.pem
现在你有了一个.pem
文件。启动 mongod 时使用以下运行时选项:
C:\> C:\practicalmongodb\bin\mongod –sslMode requireSSL --sslPEMKeyFile C:\practicalmongodb\mongodb.pem
2015-07-03T03:45:33.248-0700 I CONTROL Hotfix KB2731284 or later update is installed, no need to zero-out data files
2015-07-03T02:54:30.630-0700 I JOURNAL [initandlisten] journal dir=C:\data\db\journal
2015-07-03T02:54:30.670-0700 I CONTROL [initandlisten] MongoDB starting : pid=2
816 port=27017 dbpath=C:\data\db\ 64-bit host=ANOC9
2015-07-03T02:54:30.670-0700 I CONTROL [initandlisten] targetMinOS: Windows 7/Windows Server 2008 R2
2015-07-03T02:54:30.670-0700 I CONTROL [initandlisten] db version v3.0.4
2015-07-03T02:54:30.670-0700 I CONTROL [initandlisten] OpenSSL version: OpenSSL1.0.1j-fips 19 Mar 2015
2015-07-03T02:54:30.670-0700 I CONTROL [initandlisten] build info: windows sys. getwindowsversion(major=6, minor=1, build=7601, platform=2, service_pack='Service Pack 1') BOOST_LIB_VERSION=1_49
2015-07-03T02:54:30.671-0700 I CONTROL [initandlisten] allocator: system
2015-07-03T02:54:30.671-0700 I CONTROL [initandlisten] options: { net: { ssl: {
PEMKeyFile: "c:\practicalmongodb\mongodb.pem", mode: "requireSSL" } } }
2015-07-03T02:54:30.680-0700 I NETWORK [initandlisten] waiting for connections
on port 27017 ssl
2015-07-03T03:33:43.708-0700 I NETWORK [initandlisten] connection accepted from
127.0.0.1:49194 #2 (1 connection now open)
Note
不建议在生产环境中使用自签名证书,除非它是一个受信任的网络,因为它会使您容易受到中间人攻击。
接下来,您将使用 mongo shell 连接到上面的 mongod。当您使用–ssl
选项运行 mongo 时,您需要指定–sslAllowInvalidCertificates
或–sslCAFile
。还是用–sslAllowInvalidCertificates
吧。
打开终端窗口,输入以下内容:
C:\>
C:\practicalmongodb\bin>mongo --ssl --sslAllowInvalidCertificates
2015-07-03T02:30:10.774-0700 I CONTROL Hotfix KB2731284 or later update is installed, no need to zero-out data files
MongoDB shell version: 3.0.4
connecting to: test
5.8 使用 MongoDB 云管理器进行配置
在本章的开始,您学习了如何使用 Windows 和 Linux 安装和配置 MongoDB。在本章的这一部分,你将看到如何使用 MongoDB 云管理器。
Mongo DBCloud Manager 是数据库开发者内置的一个监控解决方案。在 2.6 版之前,MongoDB 云管理器(以前称为 MongoDB 监控服务或 MMS)仅用于监控和管理 MongoDB。从 2.6 版开始,MongoDB Cloud Manager 引入了一些重要的增强功能,包括备份、时间点恢复和自动化特性,使得操作 MongoDB 的任务比以前更简单。自动化特性为管理员提供了强大的功能,只需点击几下鼠标就可以快速创建、升级、扩展或关闭 MongoDB 实例。
在本书的这一部分,您将看到如何开始使用 MongoDB 云管理器。您将使用 MongoDB 云管理器在 AWS 上部署一个独立的 MongoDB 实例。
当您启动 MongoDB 云管理器时,它会要求在每台服务器上安装一个自动化代理,然后由 MongoDB 云管理器用来与服务器通信。
为了开始供应,您首先需要在 MongoDB Cloud Manager 上创建您的配置文件。
输入以下网址: https://cloud.mongodb.com
。根据您是否拥有帐户,点按“登录”或“免费注册”按钮。
由于您是第一次开始,请单击“注册免费”按钮。这将使您进入图 5-1 所示的页面。
图 5-1。
Account Profile
您将创建一个新的个人资料。然而,MongoDB 提供了作为现有云管理器组加入的选项。
输入所有相关细节,如图 5-1 所示,点击继续。这会将您转到提供公司信息的页面。完成个人资料和公司信息后,接受条款并单击创建帐户按钮。这就完成了概要文件的创建。下一步是创建一个组(图 5-2 )。
图 5-2。
Create Group
为该组提供一个唯一的名称,然后单击创建组。接下来是部署选择页面,如图 5-3 所示,您可以选择构建新的部署或管理现有的部署。
图 5-3。
Deployment
选择以构建新部署。接下来,将提示您构建部署的位置(即本地、AWS 或其他远程环境)。在本例中,选择 AWS。单击“在 AWS 中部署”选项将引导您在自行调配和使用 Cloud Manager 进行调配之间进行选择。
选择“I will Provision”选项,这意味着您将使用已经在 AWS 上提供给您的机器。
下一个屏幕提供了部署类型的选项(即独立、副本集或分片集群)。您正在进行独立部署,因此请单击“创建独立”框。这将使您进入图 5-4 所示的屏幕。
图 5-4。
Details for a standalone instance
提供实例名和数据目录前缀,然后单击继续。接下来是如图 5-5 所示的屏幕,它提示您在每台服务器上安装一个自动化代理
图 5-5。
Installing an automation agent
该屏幕有一个指定服务器数量的选项。在本例中,您指定 1。
接下来,您需要指定平台。选择 Ubuntu。然后出现图 5-6 中的屏幕。
图 5-6。
Automation agent installation instructions
按照步骤操作。
在实现启动代理的步骤之前,需要确保所有相关端口都是打开的(443、4949、27000 到 27018)。
完成所有步骤后,单击“验证代理”按钮。验证后,如果一切正常,您会看到一个继续按钮。
当您单击 Continue 时,您将转到图 5-7 所示的审查和部署页面,在这里您可以看到将要部署的所有流程。在这里,自动化代理下载并安装监控和备份代理。
图 5-7。
Review and deploy
单击 Deploy 按钮将转到 deployment 页面,部署更改状态为“正在进行”安装完成后,部署状态将变为“目标状态”,配置的服务器将出现在拓扑视图中。
如果您的部署支持 SSL 或使用任何身份验证机制,您需要手动下载并安装监控代理。
为了检查所有代理是否正常工作,您可以单击控制台上的管理选项卡。
云管理器可以在任何连接互联网的服务器上部署 MongoDB 副本集、分片集群和独立服务器。服务器只需要能够与云管理器建立出站 TCP 连接。
5.9 总结
在本章中,您学习了如何在 Windows 和 Linux 平台上安装 MongoDB。您还了解了一些重要的配置,这些配置对于确保数据库的安全使用是必要的。您通过使用 MongoDB 云管理器进行配置结束了这一章。
在下一章中,您将开始使用 MongoDB Shell。
六、使用 MongoDB Shell
"mongo shell comes with the standard distribution of MongoDB. It provides a JavaScript environment with full access to the language and standard functions. It provides a complete interface for MongoDB database. "
在本章中,您将学习 mongo shell 的基础知识以及如何使用它来管理 MongoDB 文档。在深入研究创建与数据库交互的应用之前,理解 MongoDB shell 的工作原理是很重要的。
没有比从 MongoDB shell 开始体验 MongoDB 数据库更好的方法了。MongoDB shell 简介分为三个部分,以便读者更容易理解和实践这些概念。
第一部分涵盖了数据库的基本特性,包括基本的 CRUD 操作符。下一节将介绍高级查询。本章的最后一节解释了存储和检索数据的两种方式:嵌入和引用。
6.1 基本查询
本节将简要讨论 CRUD 操作(创建、读取、更新和删除)。通过使用基本的示例和练习,您将学习如何在 MongoDB 中执行这些操作。此外,您将了解在 MongoDB 中查询是如何执行的。
与用于查询的传统 SQL 不同,MongoDB 使用自己的类似 JSON 的查询语言从存储的数据中检索信息。
成功安装 MongoDB 后,如第 5 章所述,您将导航到目录C:\practicalmongodb\bin\
。该文件夹包含运行 MongoDB 的所有可执行文件。
MongoDB shell 可以通过执行 mongo 可执行文件来启动。
第一步总是启动数据库服务器。打开命令提示符(以管理员身份运行)并发出命令CD \
。
接下来,运行命令C:\practicalmongodb\bin\mongod.exe
。(如果安装在其他文件夹中,路径会相应改变。对于本章中的示例,安装在C:\practicalmongodb
文件夹中。)这将启动数据库服务器。
C:\>c:\practicalmongodb\bin\mongod.exe
2015-07-06T02:29:24.501-0700 I CONTROL Hotfix KB2731284 or later update is insalled, no need to zero-out data files
2015-07-06T02:29:24.522-0700 I JOURNAL [initandlisten] journal dir=c:\data\db\ournal
....................................................
2015-07-06T02:29:24.575-0700 I CONTROL [initandlisten] MongoDB starting : pid=384 port=27017 dbpath=c:\data\db\ 64-bit host=ANC09
2015-07-06T02:29:24.575-0700 I CONTROL [initandlisten] targetMinOS: Windows 7/windows Server 2008 R2
2015-07-06T02:29:24.575-0700 I CONTROL [initandlisten] db version v3.0.4
2015-07-06T02:29:24.575-0700 I CONTROL [initandlisten] OpenSSL version: OpenSSL1.0.1j-fips 19 Mar 2015
2015-07-06T02:29:24.575-0700 I CONTROL [initandlisten] build info: windows sys getwindowsversion(major=6, minor=1, build=7601, platform=2, service_pack='Service Pack 1') BOOST_LIB_VERSION=1_49
2015-07-06T02:29:24.575-0700 I CONTROL [initandlisten] allocator: system
2015-07-06T02:29:24.575-0700 I CONTROL [initandlisten] options: {}
2015-07-06T02:29:24.584-0700 I NETWORK [initandlisten] waiting for connections on port 27017
默认情况下,MongoDB 在 localhost 接口的端口 27017 上监听任何传入的连接。
既然数据库服务器已经启动,您可以开始使用 mongo shell 向服务器发出命令。
在查看 mongo shell 之前,让我们简单地看一下如何使用导入/导出工具将数据导入和导出 MongoDB 数据库。
首先,创建一个 CSV 文件,用以下结构保存学生的记录:
Name, Gender, Class, Score, Age.
CSV 的样本数据如图 6-1 所示。
图 6-1。
Sample CSV file
接下来,将数据从 MongoDB 数据库导入到一个新的集合中,以便了解导入工具是如何工作的。
以管理员身份运行命令提示符,将其打开。以下命令用于获得关于import
命令的帮助:
C:\>c:\practicalmongodb\bin\mongoimport.exe --help
Import CSV, TSV or JSON data into MongoDB.
When importing JSON documents, each document must be a separate line of the input file.
Example:
mongoimport --host myhost --db my_cms --collection docs < mydocfile.json
....
C:\>
发出以下命令,将数据从文件exporteg.csv
导入到MyDB
数据库中名为importeg
的新集合中:
C:\>c:\practicalmongodb\bin\mongoimport.exe --host localhost --db mydb --collection importeg --type csv --file c:\exporteg.csv --headerline
2015-07-06T01:53:23.537-0700 connected to: localhost
2015-07-06T01:53:23.608-0700 imported 15 documents
为了验证集合是否被创建以及数据是否被导入,您使用 mongo shell 连接到数据库(在本例中是localhost
),并发出命令来验证集合是否存在。
要启动 mongo shell,以管理员身份运行命令提示符并发出命令C:\PracticalMongoDB\bin\mongo.exe
(路径会因安装文件夹而异;在这个例子中,文件夹是C:\PracticalMongoDB\
),并按回车键。
默认情况下,它连接到正在监听端口 27017 的localhost
数据库服务器。
C:\>c:\practicalmongodb\bin\mongo.exe
MongoDB shell version: 3.0.4
connecting to: test
> use mydb
switched to db mydb
> show collections
importeg
system.indexes
> db.importeg.find()
{ "_id" : ObjectId("5450af58c770b7161eefd31d"), "Name" : "S1", "Gender" : "M", "Class" : "C1", "Score" : 95, "Age" : 25 }
.......
{ "_id" : ObjectId("5450af59c770b7161eefd31e"), "Name" : "S2", "Gender" : "M", "Class" : "C1", "Score" : 85, "Age" : 18 }
>
简而言之,你在这里做的是
Connecting to the mongo shell Switching to your database, which is MyDB
in this case Checking for the collections that exist in the MyDB
database using show collections
. Checking the count of the collection that you imported using the import tool. Finally, executing the find()
command to check for the data in the new collection.
要连接到不同的主机和端口,可以将–host
和–port
与命令一起使用。
如图 6-1 所示,默认情况下,数据库test
用于上下文。
在任一时间点,执行db
命令将显示 shell 连接到的当前数据库:
> db
test
>
要显示所有数据库名称,可以运行show dbs
命令。执行此命令将列出连接的服务器的所有数据库。
> show dbs
在任何时候,都可以使用help()
命令访问帮助。
> help
db.help() help on db methods
db.mycoll.help() help on collection methods
sh.help() sharding helpers
rs.help() replica set helpers
help admin administrative help
help connect connecting to a db help
help keys key shortcuts
help misc misc things to know
help mr mapreduce
show dbs show database names
show collections show collections in current database
show users show users in current database
.............
exit quit the mongo shell
如上图所示,如果你需要关于db
或collection
的任何方法的帮助,你可以使用db.help()
或db.<CollectionName>.help()
。例如,如果你需要关于db
命令的帮助,执行db.help()
。
> db.help()
DB methods:
db.addUser(userDocument)
...
db.shutdownServer()
db.stats()
db.version() current version of the server
>
到目前为止,您一直使用默认的test
db。命令use <newdbname>
可用于切换到新的数据库。
> use mydb
switched to db mydb
在开始您的探索之前,让我们先简要地看一下与 SQL 术语和概念相对应的 MongoDB 术语和概念。表 6-1 对此进行了总结。
表 6-1。
SQL and MongoDB Terminology
| 结构化查询语言 | MongoDB | | --- | --- | | 数据库ˌ资料库 | 数据库ˌ资料库 | | 桌子 | 募捐 | | 排 | 文件 | | 圆柱 | 田 | | 索引 | 索引 | | 表内联接 | 嵌入和引用 | | 主键:可以指定一列或一组列 | 主键:自动设置为 _id 字段 |让我们开始探索 MongoDB 中的查询选项。切换到MYDBPOC
数据库。
> use mydbpoc
switched to db mydbpoc
>
这将上下文从test
切换到MYDBPOC
。同样可以使用db
命令来确认。
> db
mydbpoc
>
尽管上下文被切换到了MYDBPOC
,但是如果发出了show dbs
命令,数据库名称将不会出现,因为 MongoDB 直到数据被插入到数据库中时才创建数据库。这与 MongoDB 的动态数据简化方法、动态名称空间分配以及简化和加速的开发过程是一致的。如果此时发出show dbs
命令,它将不会在数据库列表中列出MYDBPOC
数据库,因为在数据被插入数据库之前,数据库不会被创建。
以下示例假设一个名为users
的多态集合,其中包含以下两个原型的文档:
{
_id: ObjectID(),
FName: "First Name",
LName: "Last Name",
Age: 30, Gender: "M",
Country: "Country"
}
and
{
_id: ObjectID(),
Name: "Full Name",
Age: 30,
Gender: "M",
Country: "Country"
}
and
{
_id: ObjectID(), Name: "Full Name", Age: 30 }
创建和插入
现在,您将了解如何创建数据库和集合。如前所述,MongoDB 中的文档是 JSON 格式的。
首先,通过发出db
命令,您将确认上下文是mydbpoc
数据库。
> db
mydbpoc
>
现在您将看到如何创建文档。
第一文档符合第一原型,而第二文档符合第二原型。您已经创建了两个名为 user1 和 user2 的文档。
> user1 = {FName: "Test", LName: "User", Age:30, Gender: "M", Country: "US"}
{
"FName" : "Test",
"LName" : "User",
"Age" : 30,
"Gender" : "M",
"Country" : "US"
}
> user2 = {Name: "Test User", Age:45, Gender: "F", Country: "US"}
{ "Name" : "Test User", "Age" : 45, "Gender" : "F", "Country" : "US" }
>
接下来,您将按照以下操作顺序将这两个文档(用户 1 和用户 2)添加到users
集合中:
> db.users.insert(user1)
> db.users.insert(user2)
>
上面的操作不仅将两个文档插入到users
集合中,还将创建集合和数据库。同样可以使用show collections
和show dbs
命令进行验证。
如上所述,show dbs
将显示数据库列表。
> show dbs
admin 0.078GB
local 0.078GB
mydb 0.078GB
mydbproc 0.078GB
并且show collections
会显示当前数据库中的收藏列表。
> show collections
system.indexes
users
>
与集合users
一起,system.indexes
集合也被显示。这个system.indexes
集合是在创建数据库时默认创建的。它管理数据库中所有集合的所有索引的信息。
执行命令db.users.find()
将显示users
集合中的文档。
> db.users.find()
{ "_id" : ObjectId("5450c048199484c9a4d26b0a"), "FName" : "Test", "LName" : "User", "Age" : 30, "Gender": "M", "Country" : "US" }
{ "_id" : ObjectId("5450c05d199484c9a4d26b0b"), "Name" : "Test", User", "Age" : 45,"Gender" : "F", "Country" : "US" }
>
您可以看到显示了您创建的两个文档。除了您添加到文档中的字段之外,还有一个为所有文档生成的额外的_id
字段。
所有文档必须有一个唯一的 _ _id
字段。如果您没有明确指定,MongoDB 会自动将它指定为惟一的对象 ID,如上例所示。
您没有明确插入一个_id
字段,但是当您使用find()
命令显示文档时,您可以看到一个与每个文档相关联的_id
字段。
这背后的原因是默认情况下会在 _ _id
字段上创建一个索引,这可以通过在system.indexes
集合上发出find
命令来验证。
>db.system.indexes.find()
{ "v" : 1, "key" : { "_id" : 1 }, "ns" : "mydbpoc.users", "name" : "_id_" }
>
可以使用ensureIndex()
和dropIndex()
命令在集合中添加或删除新的索引。我们将在本章的后面讨论这一点。默认情况下,在所有集合的_id
字段上创建一个索引。不能删除此默认索引。
显式创建集合
在上面的示例中,第一个插入操作隐式地创建了集合。但是,用户也可以在执行 insert 语句之前显式创建一个集合。
db.createCollection("users")
6.1.3 使用循环插入文档
还可以使用 for 循环将文档添加到集合中。下面的代码使用 for 插入用户。
> for(var i=1; i<=20; i++) db.users.insert({"Name" : "Test User" + i, "Age": 10+i, "Gender" : "F", "Country" : "India"})
>
为了验证插入是否成功,在集合上运行find
命令。
> db.users.find()
{ "_id" : ObjectId("52f48cf474f8fdcfcae84f79"), "FName" : "Test", "LName" : "User", "Age" : 30, "Gender" : "M", "Country" : "US" }
{ "_id" : ObjectId("52f48cfb74f8fdcfcae84f7a"), "Name" : "Test User", "Age" : 45
, "Gender" : "F", "Country" : "US" }
................
{ "_id" : ObjectId("52f48eeb74f8fdcfcae84f8c"), "Name" : "Test User18", "Age" :
28, "Gender" : "F", "Country" : "India" }
Type "it" for more
>
用户出现在集合中。在您继续之前,让我们先理解“Type”it“for more”语句。
find
命令将光标返回到结果集。光标不是在屏幕上一次显示所有文档(可能有数千或数百万个结果),而是显示前 20 个文档,并等待请求迭代(it)以显示下 20 个,依此类推,直到显示所有结果集。
也可以将结果游标赋给一个变量,然后通过编程使用 while 循环对其进行迭代。光标对象也可以作为数组来操作。
在您的情况下,如果您键入“it”并按 Enter,将出现以下内容:
>
it
{ "_id" : ObjectId("52f48eeb74f8fdcfcae84f8d"), "Name" : "Test User19", "Age" :
29, "Gender" : "F", "Country" : "India" }
{ "_id" : ObjectId("52f48eeb74f8fdcfcae84f8e"), "Name" : "Test User20", "Age" :
30, "Gender" : "F", "Country" : "India" }
>
因为只剩下两个文档,所以它显示剩下的两个文档。
6.1.4 通过显式指定 _id 进行插入
在前面的 insert 示例中,没有指定_id
字段,所以它是隐式添加的。在下面的例子中,您将看到在集合中插入文档时如何显式地指定_id
字段。
当显式指定_id
字段时,您必须记住字段的唯一性;否则插入将会失败。
以下命令明确指定了_id
字段:
> db.users.insert({"_id":10, "Name": "explicit id"})
插入操作在users
集合中创建以下文档:
{ "_id" : 10, "Name" : "explicit id" }
这可以通过发出以下命令来确认:
>db.users.find()
更新
在本节中,您将探索用于更新集合中文档的update()
命令。
默认情况下,update()
方法更新单个文档。如果需要更新所有符合选择标准的文档,可以通过将multi
选项设置为 true 来完成。
让我们从更新现有列的值开始。$set
操作符将用于更新记录。
以下命令将所有女性用户的国家更新为英国:
> db.users.update({"Gender":"F"}, {$set:{"Country":"UK"}})
为了检查更新是否已经发生,发出一个find
命令来检查所有的女性用户。
> db.users.find({"Gender":"F"})
{ "_id" : ObjectId("52f48cfb74f8fdcfcae84f7a"), "Name" : "Test User", "Age" : 45
, "Gender" : "F", "Country" : "UK" }
{ "_id" : ObjectId("52f48eeb74f8fdcfcae84f7b"), "Name" : "Test User1", "Age" : 11, "Gender" : "F", "Country" : "India" }
{ "_id" : ObjectId("52f48eeb74f8fdcfcae84f7c"), "Name" : "Test User2", "Age" : 12, "Gender" : "F", "Country" : "India" }
...................
Type "it" for more
>
如果您检查输出,您将看到只有第一个文档记录被更新,这是 update 的默认行为,因为没有指定multi
选项。
现在让我们更改update
命令,并包含multi
选项:
>db.users.update({"Gender":"F"},{$set:{"Country":"UK"}},{multi:true})
>
再次发出find
命令,检查是否已经为所有女性员工更新了国家。发出find
命令将返回以下输出:
> db.users.find({"Gender":"F"})
{ "_id" : ObjectId("52f48cfb74f8fdcfcae84f7a"), "Name" : "Test User", "Age" : 45, "Gender" : "F", "Country" : "UK" }
..............
Type "it" for more
>
如您所见,所有符合条件的记录的国家都更新为英国。
在实际的应用中工作时,您可能会遇到一种模式演变,最终可能会在文档中添加或删除字段。让我们看看如何在 MongoDB 数据库中执行这些更改。
update()
操作可以在文档级使用,这有助于更新集合中的单个文档或一组文档。
接下来,让我们看看如何向文档添加新字段。为了给文档添加字段,使用带有$set
操作符和multi
选项的update()
命令。
如果您使用不存在的带$set
的字段名,那么该字段将被添加到文档中。以下命令将把字段company
添加到所有文档中:
> db.users.update({},{$set:{"Company":"TestComp"}},{multi:true})
>
对用户的集合发出find
命令,您会发现新字段被添加到所有文档中。
> db.users.find()
{ "Age" : 30, "Company" : "TestComp", "Country" : "US", "FName" : "Test", "Gender" : "M", "LName" : "User", "_id" : ObjectId("52f48cf474f8fdcfcae84f79") }
{ "Age" : 45, "Company" : "TestComp", "Country" : "UK", "Gender" : "F", "Name" : "Test User", "_id" : ObjectId("52f48cfb74f8fdcfcae84f7a") }
{ "Age" : 11, "Company" : "TestComp", "Country" : "UK", "Gender" : "F", ....................
Type "it" for more
>
如果使用文档中已有的字段执行update()
命令,它将更新该字段的值;但是,如果文档中不存在该字段,则该字段将被添加到文档中。
接下来您将看到如何使用相同的update()
命令和$unset
操作符从文档中删除字段。
以下命令将从所有文档中删除字段Company
:
> db.users.update({},{$unset:{"Company":""}},{multi:true})
>
这可以通过对Users
集合发出find()
命令来检查。您可以看到,Company
字段已经从文档中删除。
> db.users.find()
{ "Age" : 30, "Country" : "US", "FName" : "Test", "Gender" : "M", "LName" : "User", "_id" : ObjectId("52f48cf474f8fdcfcae84f79") }
.............
Type "it" for more
删除
要删除集合中的文档,请使用remove ()
方法。如果指定了选择标准,则只会删除符合标准的文档。如果没有指定标准,所有文档都将被删除。
以下命令将删除性别= 'M '的文档:
> db.users.remove({"Gender":"M"})
>
同样可以通过在Users
上发出find()
命令来验证:
> db.users.find({"Gender":"M"})
>
不返回任何文档。
以下命令将删除所有文档:
> db.users.remove({})
> db.users.find()
>
如您所见,没有文档被返回。
最后,如果您想要删除集合,以下命令将删除集合:
> db.users.drop()
true
>
为了验证集合是否被删除,发出show collections
命令。
> show collections
system.indexes
>
正如您所看到的,没有显示集合名称,这表明集合已经从数据库中删除。
已经介绍了基本的创建、更新和删除操作,下一节将向您展示如何执行读操作。
阅读
在本章的这一部分,您将看到各种示例,展示作为 MongoDB 一部分的查询功能,它使您能够从数据库中读取存储的数据。
为了从基本查询开始,首先创建users
集合,并按照insert
命令插入数据。
> user1 = {FName: "Test", LName: "User", Age:30, Gender: "M", Country: "US"}
{
"FName" : "Test",
"LName" : "User",
"Age" : 30,
"Gender" : "M",
"Country" : "US"
}
> user2 = {Name: "Test User", Age:45, Gender: "F", Country: "US"}
{ "Name" : "Test User", "Age" : 45, "Gender" : "F", "Country" : "US" }
> db.users.insert(user1)
> db.users.insert(user2)
> for(var i=1; i<=20; i++) db.users.insert({"Name" : "Test User" + i, "Age": 10+i, "Gender" : "F", "Country" : "India"})
现在让我们从基本的查询开始。find()
命令用于从数据库中检索数据。
启动一个find()
命令返回集合中的所有文档。
> db.users.find()
{ "_id" : ObjectId("52f4a823958073ea07e15070"), "FName" : "Test", "LName" : "User", "Age" : 30, "Gender" : "M", "Country" : "US" }
{ "_id" : ObjectId("52f4a826958073ea07e15071"), "Name" : "Test User", "Age" : 45, "Gender" : "F", "Country" : "US" }
......
{ "_id" : ObjectId("52f4a83f958073ea07e15083"), "Name" : "Test User18", "Age" :28, "Gender" : "F", "Country" : "India" }
Type "it" for more
>
6.1.7.1 查询文档
MongoDB 提供了一个丰富的查询系统。查询文档可以作为参数传递给find()
方法,以过滤集合中的文档。
查询文档在左“{”和右“}”花括号中指定。在返回结果集之前,将查询文档与集合中的所有文档进行匹配。
使用不带任何查询文档或空查询文档(如find({})
)的find()
命令返回集合中的所有文档。
查询文档可以包含选择器和投影仪。
选择器类似于 SQL 中的 where 条件或用于过滤结果的过滤器。
投影仪类似于用于显示数据字段的选择条件或选择列表。
6.1.7.2 选择器
现在您将看到如何使用选择器。以下命令将返回所有女性用户:
> db.users.find({"Gender":"F"})
{ "_id" : ObjectId("52f4a826958073ea07e15071"), "Name" : "Test User", "Age" : 45, "Gender" : "F", "Country" : "US" }
.............
{ "_id" : ObjectId("52f4a83f958073ea07e15084"), "Name" : "Test User19", "Age" :29, "Gender" : "F", "Country" : "India" }
Type "it" for more
>
让我们更进一步。MongoDB 还支持将不同条件合并在一起的操作符,以便根据您的需求来细化您的搜索。
让我们细化上面的查询,现在寻找来自印度的女性用户。以下命令将返回相同的结果:
> db.users.find({"Gender":"F", $or: [{"Country":"India"}]})
{ "_id" : ObjectId("52f4a83f958073ea07e15072"), "Name" : "Test User1", "Age" : 11, "Gender" : "F", "Country" : "India" }
...........
{ "_id" : ObjectId("52f4a83f958073ea07e15085"), "Name" : "Test User20", "Age" :30, "Gender" : "F", "Country" : "India" }
>
接下来,如果您想要查找属于印度或美国的所有女性用户,请执行以下命令:
>db.users.find({"Gender":"F",$or:[{"Country":"India"},{"Country":"US"}]})
{ "_id" : ObjectId("52f4a826958073ea07e15071"), "Name" : "Test User", "Age" : 45, "Gender" : "F", "Country" : "US" }
..............
{ "_id" : ObjectId("52f4a83f958073ea07e15084"), "Name" : "Test User19", "Age" :29, "Gender" : "F", "Country" : "India" }
Type "it" for more
对于聚合需求,需要使用聚合函数。接下来,您将学习如何使用count()
函数进行聚合。
在上面的例子中,不是显示文档,而是想找出居住在印度或美国的女性用户的数量。因此,执行以下命令:
>db.users.find({"Gender":"F",$or:[{"Country":"India"}, {"Country":"US"}]}).count()
21
>
如果您想在不考虑任何选择器的情况下计算用户数,请执行以下命令:
> db.users.find().count()
22
>
6.1.7.3 投影仪
您已经看到了如何使用选择器过滤掉集合中的文档。在上面的例子中,find()
命令返回与选择器匹配的文档的所有字段。
让我们向查询文档添加一个投影仪,除了选择器之外,您还将提到需要显示的特定细节或字段。
假设您想显示所有女性雇员的名字和年龄。在这种情况下,除了选择器,还使用了投影仪。
执行以下命令返回所需的结果集:
> db.users.find({"Gender":"F"}, {"Name":1,"Age":1})
{ "_id" : ObjectId("52f4a826958073ea07e15071"), "Name" : "Test User", "Age" : 45 }
..........
Type "it" for more
>
6.1.7.4 排序( )
在 MongoDB 中,排序顺序指定如下:1 表示升序,1 表示降序。
如果在上面的示例中,您希望按年龄的升序对记录进行排序,您可以执行以下命令:
>db.users.find({"Gender":"F"}, {"Name":1,"Age":1}).sort({"Age":1})
{ "_id" : ObjectId("52f4a83f958073ea07e15072"), "Name" : "Test User1", "Age" : 11 }
{ "_id" : ObjectId("52f4a83f958073ea07e15073"), "Name" : "Test User2", "Age" : 12 }
{ "_id" : ObjectId("52f4a83f958073ea07e15074"), "Name" : "Test User3", "Age" : 13 }
..............
{ "_id" : ObjectId("52f4a83f958073ea07e15085"), "Name" : "Test User20", "Age" :30 }
Type "it" for more
如果要按姓名降序和年龄升序显示记录,请执行以下命令:
>db.users.find({"Gender":"F"},{"Name":1,"Age":1}).sort({"Name":-1,"Age":1})
{ "_id" : ObjectId("52f4a83f958073ea07e1507a"), "Name" : "Test User9", "Age" : 19 }
............
{ "_id" : ObjectId("52f4a83f958073ea07e15072"), "Name" : "Test User1", "Age" : 11 }
Type "it" for more
6.1.7.5 极限( )
现在,您将了解如何限制结果集中的记录。例如,在拥有数千个文档的大型集合中,如果您只想返回五个匹配的文档,那么可以使用limit
命令,这就使您能够做到这一点。
回到您之前对居住在印度或美国的女性用户的查询,假设您想要限制结果集,只返回两个用户。需要执行以下命令:
>db.users.find({"Gender":"F",$or:[{"Country":"India"},{"Country":"US"}]}).limit(2)
{ "_id" : ObjectId("52f4a826958073ea07e15071"), "Name" : "Test User", "Age" : 45, "Gender" : "F", "Country" : "US" }
{ "_id" : ObjectId("52f4a83f958073ea07e15072"), "Name" : "Test User1", "Age" : 11, "Gender" : "F", "Country" : "India" }
6.1.7.6·斯基普
如果要求跳过前两个记录并返回第三个和第四个用户,则使用skip
命令。需要执行以下命令:
>db.users.find({"Gender":"F",$or:[{"Country":"India"}, {"Country":"US"}]}).limit(2).skip(2)
{ "_id" : ObjectId("52f4a83f958073ea07e15073"), "Name" : "Test User2", "Age" : 12, "Gender" : "F", "Country" : "India" }
{ "_id" : ObjectId("52f4a83f958073ea07e15074"), "Name" : "Test User3", "Age" : 13, "Gender" : "F", "Country" : "India" }
>
6.1.7.7 findOne()
与find()
类似的是findOne()
命令。findOne()
方法可以接受与find()
相同的参数,但是它返回的不是一个光标,而是一个文档。假设您想要返回一个居住在印度或美国的女性用户。这可以使用以下命令来实现:
> db.users.findOne({"Gender":"F"}, {"Name":1,"Age":1})
{
"_id" : ObjectId("52f4a826958073ea07e15071"),
"Name" : "Test User",
"Age" : 45
}
>
类似地,如果您想在这种情况下返回第一条记录而不考虑任何选择器,您可以使用findOne()
,它将返回集合中的第一个文档。
> db.users.findOne()
{
"_id" : ObjectId("52f4a823958073ea07e15070"),
"FName" : "Test",
"LName" : "User",
"Age" : 30,
"Gender" : "M",
"Country" : "US"}
使用光标的 6.1.7.8
当使用find()
方法时,MongoDB 将查询结果作为游标对象返回。为了显示结果,mongo shell 对返回的光标进行迭代。
MongoDB 允许用户使用find
方法的光标对象。在下一个示例中,您将看到如何将光标对象存储在一个变量中,并使用 while 循环来操作它。
假设您想要返回美国的所有用户。为了做到这一点,您创建了一个变量,将find()
的输出分配给这个变量,它是一个游标,然后使用 while 循环迭代并打印输出。
代码片段如下:
> var c = db.users.find({"Country":"US"})
> while(c.hasNext()) printjson(c.next())
{
"_id" : ObjectId("52f4a823958073ea07e15070"),
"FName" : "Test",
"LName" : "User",
"Age" : 30,
"Gender" : "M",
"Country" : "US"
}
{
"_id" : ObjectId("52f4a826958073ea07e15071"),
"Name" : "Test User",
"Age" : 45,
"Gender" : "F",
"Country" : "US"
}
>
next()
函数返回下一个文档。如果文档存在,hasNext()
函数返回 true,并且printjson()
以 JSON 格式呈现输出。
光标对象所分配到的变量也可以作为数组来操作。如果不想遍历变量,而是想在数组索引 1 处显示文档,可以运行以下命令:
> var c = db.users.find({"Country":"US"})
> printjson(c[1])
{
"_id" : ObjectId("52f4a826958073ea07e15071"),
"Name" : "Test User",
.... "Gender" : "F",
"Country" : "US"}
>
6.1.7.9 解释( )
explain()
函数可用于查看 MongoDB 数据库在执行查询时正在运行哪些步骤。从 3.0 版开始,函数的输出格式和传递给函数的参数都发生了变化。它带有一个名为verbose
的可选参数,该参数决定解释输出应该是什么样子。以下是详细度模式:allPlansExecution
、executionStats
、queryPlanner
。默认的详细模式是queryPlanner
,这意味着如果没有指定,它默认为queryPlanner
。
以下代码涵盖了对username
字段进行过滤时执行的步骤:
> db.users.find({"Name":"Test User"}).explain("allPlansExecution")
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "mydbproc.users",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [ ]
},
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
"$and" : [ ]
},
"direction" : "forward"
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 20,
"executionTimeMillis" : 0,
"totalKeysExamined" : 0,
"totalDocsExamined" : 20,
"executionStages" : {
"stage" : "COLLSCAN",
"filter" : {
"$and" : [ ]
},
"nReturned" : 20,
"executionTimeMillisEstimate" : 0,
"works" : 22,
"advanced" : 20,
"needTime" : 1,
"needFetch" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"direction" : "forward",
"docsExamined" : 20
},
"allPlansExecution" : [ ]
},
"serverInfo" : {
"host" : " ANOC9",
"port" : 27017,
"version" : "3.0.4",
"gitVersion" : "534b5a3f9d10f00cd27737fbcd951032248b5952"
},
"ok" : 1
如您所见,explain()
输出返回关于queryPlanner
、executionStats
和serverInfo
的信息。如上所述,输出返回的信息取决于所选的详细模式。
您已经看到了如何执行基本的查询、排序、限制等。您还了解了如何使用 while 循环或作为数组来操作结果集。在下一节中,您将了解索引以及如何在查询中使用它们。
使用索引
索引用于为频繁使用的查询提供高性能的读取操作。默认情况下,每当创建一个集合并将文档添加到其中时,都会在文档的_id
字段上创建一个索引。
在这一节中,您将了解如何创建不同类型的索引。让我们首先使用 for 循环在一个名为testindx
的新集合中插入 100 万个文档。
>for(i=0;i<1000000;i++){db.testindx.insert({"Name":"user"+i,"Age":Math.floor(Math.random()*120)})}
接下来,发出find()
命令获取一个值为 user101 的名称。运行explain()
命令来检查 MongoDB 正在执行哪些步骤来返回结果集。
> db.testindx.find({"Name":"user101"}).explain("allPlansExecution")
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "mydbproc.testindx",
"indexFilterSet" : false,
"parsedQuery" : {
"Name" : {
"$eq" : "user101"
}
},
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
"Name" : {
"$eq" : "user101"
}
},
"direction" : "forward"
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 645,
"totalKeysExamined" : 0,
"totalDocsExamined" : 1000000,
"executionStages" : {
"stage" : "COLLSCAN",
"filter" : {
"Name" : {
"$eq" : "user101"
}
},
"nReturned" : 1,
"executionTimeMillisEstimate" : 20,
"works" : 1000002,
"advanced" : 1,
"needTime" : 1000000,
"needFetch" : 0,
"saveState" : 7812,
"restoreState" : 7812,
"isEOF" : 1,
"invalidates" : 0,
"direction" : "forward",
"docsExamined" : 1000000
},
"allPlansExecution" : [ ]
},
"serverInfo" : {
"host" : " ANOC9",
"port" : 27017,
"version" : "3.0.4",
"gitVersion" : "534b5a3f9d10f00cd27737fbcd951032248b5952"
},
"ok" : 1
如您所见,数据库扫描了整个表。这会对性能产生重大影响,这是因为没有索引。
6.1.8.1 单键索引
让我们在文档的Name
字段上创建一个索引。使用ensureIndex()
创建索引。
> db.testindx.ensureIndex({"Name":1})
创建索引需要几分钟时间,具体取决于服务器和集合大小。
让我们运行您之前使用explain()
运行的相同查询,以检查数据库在索引创建后执行的步骤。检查输出中的n
、nscanned
和millis
字段。
>db.testindx.find({"Name":"user101"}).explain("allPathsExecution")
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "mydbproc.testindx",
"indexFilterSet" : false,
"parsedQuery" : {
"Name" : {
"$eq" : "user101"
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"Name" : 1
},
"indexName" : "Name_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"Name" : [
"[\"user101\", \"user101\"]"
]
}
}
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 0,
"totalKeysExamined" : 1,
"totalDocsExamined" : 1,
"executionStages" : {
"stage" : "FETCH",
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 2,
"advanced" : 1,
"needTime" : 0,
"needFetch" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 1,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 2,
"advanced" : 1,
"needTime" : 0,
"needFetch" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"Name" : 1
},
"indexName" : "Name_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"Name" : [
"[\"user101\", \"user101\"]"
]
},
"keysExamined" : 1,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0,
"matchTested" : 0
}
},
"allPlansExecution" : [ ]
},
"serverInfo" : {
"host" : "ANOC9",
"port" : 27017,
"version" : "3.0.4",
"gitVersion" : "534b5a3f9d10f00cd27737fbcd951032248b5952"
},
"ok" : 1
}
>
正如您在结果中看到的,没有表扫描。索引的创建对查询执行时间有很大的影响。
6.1.8.2 综合索引
创建索引时,您应该记住索引涵盖了您的大多数查询。如果您有时只查询Name
字段,有时同时查询Name
和Age
字段,那么在Name
和Age
字段上创建复合索引将比在任一字段上创建索引更有益,因为复合索引将涵盖这两个查询。
下面的命令在集合testindx
的字段Name
和Age
上创建一个复合索引。
> db.testindx.ensureIndex({"Name":1, "Age": 1})
复合索引有助于 MongoDB 更高效地执行包含多个子句的查询。创建复合索引时,记住将用于精确匹配的字段(如Name
: "S1"
)排在最前面,后面是用于范围的字段(如Age
: {"$gt":20}
),这一点也非常重要。
因此,上述索引将有利于以下查询:
>db.testindx.find({"Name": "user5","Age":{"$gt":25}}).explain("allPlansExecution")
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "mydbproc.testindx",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"Name" : {
"$eq" : "user5"
}
},
{
"Age" : {
"$gt" : 25
}
}
]
},
"winningPlan" : {
"stage" : "KEEP_MUTATIONS",
"inputStage" : {
"stage" : "FETCH",
"filter" : {
"Age" : {
"$gt" : 25
}
},
............................
"indexBounds" : {
"Name" : [
"[\"user5\", \"user5\"
},
"rejectedPlans" : [
{
"stage" : "FETCH",
......................................................
"indexName" : "Name_1_Age_1",
"isMultiKey" : false,
"direction" : "forward",
.....................................................
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 0,
"totalKeysExamined" : 1,
"totalDocsExamined" : 1,
.....................................................
"inputStage" : {
"stage" : "FETCH",
"filter" : {
"Age" : {
"$gt" : 25
}
},
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 2,
"advanced" : 1,
"allPlansExecution" : [
{
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"totalKeysExamined" : 1,
"totalDocsExamined" : 1,
"executionStages" : {
.............................................................
"serverInfo" : {
"host" : " ANOC9",
"port" : 27017,
"version" : "3.0.4",
"gitVersion" : "534b5a3f9d10f00cd27737fbcd951032248b5952"
},
"ok" : 1
}
>
6.1.8.3 对排序操作的支持
在 MongoDB 中,使用索引字段对文档进行排序的排序操作提供了最高的性能。
正如在其他数据库中一样,MongoDB 中的索引也因此有了顺序。如果使用索引来访问文档,它返回结果的顺序与索引的顺序相同。
对多个字段进行排序时,需要创建复合索引。在复合索引中,输出可以是索引前缀或完整索引的排序顺序。
索引前缀是复合索引的子集,复合索引包含从索引开始的一个或多个字段。
例如,以下是复合索引的索引前缀:{ x:1, y: 1, z: 1}
。
排序操作可以是索引前缀的任意组合,如{x: 1}, {x: 1, y: 1}
。
复合索引只有在作为排序的前缀时才能帮助排序。
例如,Age
、Name
和Class,
的复合索引如下
> db.testindx.ensureIndex({"Age": 1, "Name": 1, "Class": 1})
将对以下查询有用:
> db.testindx.find().sort({"Age":1})
> db.testindx.find().sort({"Age":1,"Name":1})
> db.testindx.find().sort({"Age":1,"Name":1, "Class":1})
上述索引在以下查询中不会有太大帮助:
> db.testindx.find().sort({"Gender":1, "Age":1, "Name": 1})
您可以使用explain()
命令来诊断 MongoDB 是如何处理查询的。
6.1.8.4 唯一索引
在一个字段上创建索引并不能确保唯一性,所以如果在Name
字段上创建索引,那么两个或更多的文档可以有相同的名称。但是,如果惟一性是需要启用的约束之一,那么在创建索引时需要将unique
属性设置为 true。
首先,让我们删除现有的索引。
>db.testindx.dropIndexes()
以下命令将在testindx
集合的Name
字段上创建唯一索引:
> db.testindx.ensureIndex({"Name":1},{"unique":true})
现在,如果您尝试在集合中插入重复的名称,如下所示,MongoDB 将返回一个错误,并且不允许插入重复的记录:
> db.testindx.insert({"Name":"uniquename"})
> db.testindx.insert({"Name":"uniquename"})
"E11000 duplicate key error index: mydbpoc.testindx.$Name_1 dup key: { : "uniquename" }"
如果您检查集合,您会看到只存储了第一个uniquename
。
> db.testindx.find({"Name":"uniquename"})
{ "_id" : ObjectId("52f4b3c3958073ea07f092ca"), "Name" : "uniquename" }
>
也可以为复合索引启用唯一性,这意味着尽管单个字段可以有重复值,但组合将始终是唯一的。
例如,如果您有一个关于{"name":1, "age":1}
的唯一索引,
> db.testindx.ensureIndex({"Name":1, "Age":1},{"unique":true})
>
则允许插入以下内容:
> db.testindx.insert({"Name":"usercit"})
> db.testindx.insert({"Name":"usercit", "Age":30})
然而,如果你执行
> db.testindx.insert({"Name":"usercit", "Age":30})
它会抛出一个错误,比如
E11000 duplicate key error index: mydbpoc.testindx.$Name_1_Age_1 dup key: { : "usercit", : 30.0 }
您可以先创建集合并插入文档,然后在集合上创建索引。如果在集合上创建唯一索引,而该集合在创建索引的字段中可能有重复值,则索引创建将会失败。
为了迎合这种情况,MongoDB 提供了一个dropDups
选项。dropDups
选项保存找到的第一个文档,并删除任何具有重复值的后续文档。
以下命令将在name
字段上创建一个唯一的索引,并将删除任何重复的文档:
>db.testindx.ensureIndex({"Name":1},{"unique":true, "dropDups":true})
>
6.1.8.5 系统.索引
无论何时创建数据库,默认情况下都会创建一个system.indexes
集合。关于数据库索引的所有信息都存储在system.indexes
集合中。这是一个保留的集合,因此您不能修改它的文档或从中删除文档。您只能通过ensureIndex
和dropIndexes
数据库命令来操作它。
每当创建一个索引时,可以在system.indexes
中看到它的元信息。以下命令可用于获取上述集合的所有索引信息:
db.collectionName.getIndexes()
例如,以下命令将返回在testindx
集合上创建的所有索引:
> db.testindx.getIndexes()
6.1.8.6 下降索引
dropIndex
命令用于删除索引。
以下命令将从testindx
集合中删除Name
字段索引:
> db.testindx.dropIndex({"Name":1})
{ "nIndexesWas" : 3, "ok" : 1 }
>
6.1.8.7 重新索引
当您对集合执行了多次插入和删除操作后,您可能需要重新生成索引,以便能够以最佳方式使用索引。reIndex
命令用于重建索引。
以下命令重建集合的所有索引。它将首先删除索引,包括_id
字段上的默认索引,然后重新构建索引。
db.collectionname.reIndex()
以下命令重建testindx
集合的索引:
> db.testindx.reIndex()
{
"nIndexesWas" : 2,
"msg" : "indexes dropped for collection",
"nIndexes" : 2,
..............
"ok" : 1
}
>
我们将在下一章详细讨论 MongoDB 中可用的不同类型的索引。
6.1.8.8 索引是如何工作的
MongoDB 将索引存储在 BTree 结构中,因此自动支持范围查询。
如果在一个查询中使用了多个选择标准,MongoDB 会尝试找到最佳的单个索引来选择一个候选集。之后,它依次遍历集合,以评估其他标准。
当第一次执行查询时,MongoDB 为查询可用的每个索引创建多个执行计划。它让计划在一定数量的周期内轮流执行,直到执行最快的计划完成。然后将结果返回给系统,系统会记住最快的执行计划所使用的索引。
对于后续查询,将使用记住的索引,直到集合中发生了一定数量的更新。超过更新限制后,系统将再次执行该过程,以找出当时适用的最佳索引。
当下列任一事件发生时,将重新评估查询计划:
- 集合接收 1,000 次写操作。
- 添加或删除了一个索引。
- mongod 进程重启。
- 发生了用于重建索引的重新索引。
如果想覆盖 MongoDB 的默认索引选择,同样可以使用hint()
方法。
版本 2.6 中引入了索引过滤器。它由优化器将对查询进行评估的索引组成,包括查询、投影和排序。MongoDB 将使用索引过滤器提供的索引,并忽略hint()
。
在 2.6 版之前,MongoDB 在任何时候都只使用一个索引,所以您需要确保存在复合索引来更好地匹配您的查询。这可以通过检查查询的排序和搜索标准来完成。
索引交集是在 2.6 版中引入的。它支持索引的交集,以满足具有复合条件的查询,其中条件的一部分由一个索引满足,而另一部分由另一个索引满足。
一般来说,一个索引交集由两个索引组成;但是,可以使用多个索引交集来解析查询。这种能力提供了更好的优化。
与其他数据库一样,索引维护也有成本。每个更改集合的操作(比如创建、更新或删除)都有开销,因为索引也需要更新。为了保持最佳平衡,您需要定期检查索引的有效性,该索引可以通过您在系统上执行的读写操作的比率来衡量。找出不常用的索引并删除它们。
6.2 超越基础
本节将介绍在选择器部分使用条件操作符和正则表达式的高级查询。这些操作符和正则表达式中的每一个都为您提供了对所编写的查询的更多控制,从而也为您提供了对从 MongoDB 数据库中获取的信息的更多控制。
使用条件运算符
条件运算符使您能够更好地控制试图从数据库中提取的数据。在本节中,您将重点关注以下操作员:$lt
、$lte
、$gt
、$gte
、$in
、$nin
和$not
。
以下示例假设名为Students
的集合包含以下类型的文档:
{
_id: ObjectID(),
Name: "Full Name",
Age: 30,
Gender: "M",
Class: "C1",
Score: 95
}
您将首先创建集合并插入几个示例文档。
>db.students.insert({Name:"S1",Age:25,Gender:"M",Class:"C1",Score:95})
>db.students.insert({Name:"S2",Age:18,Gender:"M",Class:"C1",Score:85})
>db.students.insert({Name:"S3",Age:18,Gender:"F",Class:"C1",Score:85})
>db.students.insert({Name:"S4",Age:18,Gender:"F",Class:"C1",Score:75})
>db.students.insert({Name:"S5",Age:18,Gender:"F",Class:"C2",Score:75})
>db.students.insert({Name:"S6",Age:21,Gender:"M",Class:"C2",Score:100})
>db.students.insert({Name:"S7",Age:21,Gender:"M",Class:"C2",Score:100})
>db.students.insert({Name:"S8",Age:25,Gender:"F",Class:"C2",Score:100})
>db.students.insert({Name:"S9",Age:25,Gender:"F",Class:"C2",Score:90})
>db.students.insert({Name:"S10",Age:28,Gender:"F",Class:"C3",Score:90})
> db.students.find()
{ "_id" : ObjectId("52f874faa13cd6a65998734d"), "Name" : "S1", "Age" : 25, "Gender" : "M", "Class" : "C1", "Score" : 95 }
.......................
{ "_id" : ObjectId("52f8758da13cd6a659987356"), "Name" : "S10", "Age" : 28, "Gender" : "F", "Class" : "C3", "Score" : 90 }
>
6.2.1.1 lt
让我们从$lt
和$lte
操作符开始。它们分别代表“小于”和“小于或等于”。
如果您想查找所有小于 25 岁的学生(年龄< 25),您可以使用选择器执行下面的find
:
> db.students.find({"Age":{"$lt":25}})
{ "_id" : ObjectId("52f8750ca13cd6a65998734e"), "Name" : "S2", "Age" : 18, "Gender" : "M", "Class" : "C1", "Score" : 85 }
.............................
{ "_id" : ObjectId("52f87556a13cd6a659987353"), "Name" : "S7", "Age" : 21, "Gender" : "M", "Class" : "C2", "Score" : 100 }
>
如果您想找出所有大于 25 岁(年龄< = 25)的学生,请执行以下命令:
> db.students.find({"Age":{"$lte":25}})
{ "_id" : ObjectId("52f874faa13cd6a65998734d"), "Name" : "S1", "Age" : 25, "Gender" : "M", "Class" : "C1", "Score" : 95 }
....................
{ "_id" : ObjectId("52f87578a13cd6a659987355"), "Name" : "S9", "Age" : 25, "Gender" : "F", "Class" : "C2", "Score" : 90 }
>
6.2.1.2 gte
$gt
和$gte
运算符分别代表“大于”和“大于或等于”。
让我们找出所有年龄大于 25 岁的学生。这可以通过执行以下命令来实现:
> db.students.find({"Age":{"$gt":25}})
{ "_id" : ObjectId("52f8758da13cd6a659987356"), "Name" : "S10", "Age" : 28, "Gender" : "F", "Class" : "C3", "Score" : 90 }
>
如果将上述示例更改为返回年龄> = 25 岁的学生,则命令为
> db.students.find({"Age":{"$gte":25}})
{ "_id" : ObjectId("52f874faa13cd6a65998734d"), "Name" : "S1", "Age" : 25, "Gender" : "M", "Class" : "C1", "Score" : 95 }
......................................
{ "_id" : ObjectId("52f8758da13cd6a659987356"), "Name" : "S10", "Age" : 28, "Gender" : "F", "Class" : "C3", "Score" : 90 }
>
6.2.1.3 nin
让我们找出属于 C1 班或 C2 班的所有学生。同样的命令是
> db.students.find({"Class":{"$in":["C1","C2"]}})
{ "_id" : ObjectId("52f874faa13cd6a65998734d"), "Name" : "S1", "Age" : 25, "Gender" : "M", "Class" : "C1", "Score" : 95 }
................................
{ "_id" : ObjectId("52f87578a13cd6a659987355"), "Name" : "S9", "Age" : 25, "Gender" : "F", "Class" : "C2", "Score" : 90 }
>
使用$nin
可以返回相反的结果。
接下来让我们找出不属于 C1 或 C2 班的学生。命令是
> db.students.find({"Class":{"$nin":["C1","C2"]}})
{ "_id" : ObjectId("52f8758da13cd6a659987356"), "Name" : "S10", "Age" : 28, "Gender" : "F", "Class" : "C3", "Score" : 90 }
>
接下来让我们看看如何组合上述所有操作符并编写一个查询。假设您想找出所有性别为“M”或属于“C1”或“C2”班级且年龄大于或等于 25 岁的学生。这可以通过执行以下命令来实现:
>db.students.find({$or:[{"Gender":"M","Class":{"$in":["C1","C2"]}}], "Age":{"$gte":25}})
{ "_id" : ObjectId("52f874faa13cd6a65998734d"), "Name" : "S1", "Age" : 25, "Gender" : "M", "Class" : "C1", "Score" : 95 }
>
正则表达式
在本节中,您将了解如何使用正则表达式。正则表达式在您想要查找姓名以“A”开头的学生的情况下非常有用。
为了理解这一点,我们再补充三四个名字不同的同学。
> db.students.insert({Name:"Student1", Age:30, Gender:"M", Class: "Biology", Score:90})
> db.students.insert({Name:"Student2", Age:30, Gender:"M", Class: "Chemistry", Score:90})
> db.students.insert({Name:"Test1", Age:30, Gender:"M", Class: "Chemistry", Score:90})
> db.students.insert({Name:"Test2", Age:30, Gender:"M", Class: "Chemistry", Score:90})
> db.students.insert({Name:"Test3", Age:30, Gender:"M", Class: "Chemistry", Score:90})
>
假设您想要查找姓名以“St”或“Te”开头且班级以“Che”开头的所有学生。同样可以使用正则表达式进行过滤,如下所示:
> db.students.find({"Name":/(St|Te)*/i, "Class":/(Che)/i})
{ "_id" : ObjectId("52f89ecae451bb7a56e59086"), "Name" : "Student2", "Age" : 30, "Gender" : "M", "Class" : "Chemistry", "Score" : 90 }
.........................
{ "_id" : ObjectId("52f89f06e451bb7a56e59089"), "Name" : "Test3", "Age" : 30, "Gender" : "M", "Class" : "Chemistry", "Score" : 90 }
>
为了理解正则表达式是如何工作的,让我们以查询"Name":/(St|Te)*/i
为例。
- //i 表示正则表达式不区分大小写。
- (St|Te)*表示名称字符串必须以“St”或“Te”开头。
- 末尾的*表示它将匹配其后的任何内容。
当您将所有内容放在一起时,您正在对以“St”或“Te”开头的名称进行不区分大小写的匹配。在该类的正则表达式中,也发出相同的正则表达式。
接下来,让我们使查询变得复杂一点。让我们把它和上面提到的操作符结合起来。
获取姓名为 student1、student2 且年龄> =25 岁的男性学生。执行此操作的命令如下:
>db.students.find({"Name":/(student*)/i,"Age":{"$gte":25},"Gender":"M"})
{ "_id" : ObjectId("52f89eb1e451bb7a56e59085"), "Name" : "Student1", "Age" : 30,
"Gender" : "M", "Class" : "Biology", "Score" : 90 }
{ "_id" : ObjectId("52f89ecae451bb7a56e59086"), "Name" : "Student2", "Age" : 30,
"Gender" : "M", "Class" : "Chemistry", "Score" : 90 }
MapReduce
MapReduce 框架支持任务的划分,在这种情况下是跨计算机集群的数据聚合,以减少聚合数据集所需的时间。它由两部分组成:Map 和 Reduce。
这里有一个更具体的描述:MapReduce 是一个框架,用于处理高度分布在巨大数据集上并使用多个节点运行的问题。如果所有节点都有相同的硬件,这些节点统称为集群;否则,它被称为网格。这种处理可以发生在结构化数据(存储在数据库中的数据)和非结构化数据(存储在文件系统中的数据)上。
- “Map”:在这一步中,充当主节点的节点接受输入参数,并将大问题分成多个小的子问题。这些子问题然后分布在工作节点上。工作者节点可以进一步将问题分成子问题。这导致了多级树结构。然后,工作节点将处理其中的子问题,并将答案返回给主节点。
- “Reduce”:在这一步中,所有子问题的答案都可以通过主节点获得,然后主节点将所有答案组合起来并产生最终输出,这就是您试图解决的大问题的答案。
为了理解它是如何工作的,让我们考虑一个小例子,在这个例子中,您将找出您的集合中男女学生的数量。
这包括以下步骤:首先创建map
和reduce
函数,然后调用mapReduce
函数并传递必要的参数。
让我们从定义map
函数开始:
> var map = function(){emit(this.Gender,1);};
>
该步骤将文档作为输入,并基于Gender
字段发出类型为{"F", 1}
或{"M", 1}
的文档。
接下来,创建reduce
函数:
> var reduce = function(key, value){return Array.sum(value);};
>
这将对键字段上的map
函数发出的文档进行分组,在您的示例中是Gender
,并将返回值的总和,在上面的示例中是“1”。上面定义的 reduce 函数的输出是基于性别的计数。
最后,使用mapReduce
函数将它们放在一起,就像这样:
> db.students.mapReduce(map, reduce, {out: "mapreducecount1"})
{
"result" : "mapreducecount1",
"timeMillis" : 29,
"counts" : {
"input" : 15,
"emit" : 15,
"reduce" : 2,
"output" : 2
},
"ok" : 1,
}
>
这实际上是应用了您在students
集合中定义的map
,reduce
函数。最终结果存储在一个名为mapreducecount1
的新集合中。
为了检查它,在mapreducecount1
集合上运行find()
命令,如下所示:
> db.mapreducecount1.find()
{ "_id" : "F", "value" : 6 }
{ "_id" : "M", "value" : 9 }
>
这里还有一个例子来解释MapReduce
的工作原理。让我们用MapReduce
找出班级的平均分数。正如您在上面的例子中看到的,您需要首先创建map
函数,然后创建reduce
函数,最后将它们组合起来,将输出存储在数据库的一个集合中。代码片段是
> var map_1 = function(){emit(this.Class,this.Score);};
> var reduce_1 = function(key, value){return Array.avg(value);};
>db.students.mapReduce(map_1,reduce_1, {out:"MR_ClassAvg_1"})
{
"result" : "MR_ClassAvg_1",
"timeMillis" : 4,
"counts" : {
"input" : 15, "emit" : 15,
"reduce" : 3 , "output" : 5
},
"ok" : 1,
}
> db.MR_ClassAvg_1.find()
{ "_id" : "Biology", "value" : 90 }
{ "_id" : "C1", "value" : 85 }
{ "_id" : "C2", "value" : 93 }
{ "_id" : "C3", "value" : 90 }
{ "_id" : "Chemistry", "value" : 90 }
>
第一步是定义map
函数,该函数循环遍历集合文档,并将输出作为{"Class": Score},
返回,例如{"C1":95}
。第二步是对班级进行分组,并计算该班级的平均分数。第三步合并结果;它定义了需要应用map
、reduce
函数的集合,最后它定义了在哪里存储输出,在本例中是一个名为MR_ClassAvg_1
的新集合。
在最后一步中,使用find
来检查结果输出。
6.2.4 聚合()
上一节介绍了MapReduce
功能。在本节中,您将大致了解一下 MongoDB 的聚合框架。
聚合框架使您能够在不使用MapReduce
函数的情况下找出聚合值。在性能方面,聚合框架比MapReduce
函数更快。你需要时刻记住MapReduce
是用于批量方法而不是实时分析的。
接下来,您将使用aggregate
函数描述上面讨论的两个输出。首先,输出是查找男女学生的数量。这可以通过执行以下命令来实现:
> db.students.aggregate({$group:{_id:"$Gender", totalStudent: {$sum: 1}}})
{ "_id" : "F", "totalStudent" : 6 }
{ "_id" : "M", "totalStudent" : 9 }
>
类似地,为了找出类的平均分数,可以执行下面的命令:
> db.students.aggregate({$group:{_id:"$Class", AvgScore: {$avg: "$Score"}}})
{ "_id" : "Biology", "AvgScore" : 90 }
{ "_id" : "C3", "AvgScore" : 90 }
{ "_id" : "Chemistry", "AvgScore" : 90 }
{ "_id" : "C2", "AvgScore" : 93 }
{ "_id" : "C1", "AvgScore" : 85 }
>
6.3 设计应用的数据模型
在本节中,您将了解如何为应用设计数据模型。MongoDB 数据库为设计数据模型提供了两种选择:用户可以将相关对象嵌入到另一个对象中,也可以使用 ID 相互引用。在本节中,您将探索这些选项。
为了理解这些选项,您将设计一个博客应用并演示这两个选项的用法。
典型的博客应用由以下场景组成:
有人在不同的主题上发表博客。除了主题分类,还可以使用不同的标签。举例来说,如果类别是政治,帖子谈论一个政治家,那么这个政治家的名字可以作为标签添加到帖子中。这有助于用户快速找到与他们兴趣相关的帖子,并让他们将相关帖子链接在一起。
浏览博客的人可以对博客文章发表评论。
6.3.1 关系数据建模和规范化
在开始使用 MongoDB 的方法之前,让我们先绕一小段路,看看如何在关系数据库(如 SQL)中对此建模。
在关系数据库中,数据建模通常是通过定义表并逐渐删除数据冗余来实现范式的。
6.3.1.1 什么是范式?
在关系数据库中,范式通常从根据应用需求创建表开始,然后逐渐移除冗余以实现最高范式,这也被称为第三范式或 3NF。为了更好地理解这一点,让我们将博客应用数据放在表格中。初始数据如图 6-2 所示。
图 6-2。
Blogging application initial data
这个数据其实是第一范式的。你会有很多冗余,因为你可以对文章有多个评论,多个标签可以与文章相关联。当然,冗余的问题是它引入了不一致的可能性,即相同数据的不同副本可能具有不同的值。要消除这种冗余,您需要通过将数据拆分到多个表中来进一步规范化数据。作为此步骤的一部分,您必须标识唯一标识表中每一行的键列,以便您可以在表之间创建链接。当使用 3NF 范式建模时,上述场景将看起来像图 6-3 所示的 RDBMs 图。
图 6-3。
RDBMS diagram
在这种情况下,您有一个没有冗余的数据模型,允许您更新它,而不必担心更新多行。特别是,您不再需要担心数据模型中的不一致性。
6.3.1.2 范式的问题
如前所述,标准化的好处是它允许轻松更新,没有任何冗余(即,它有助于保持数据的一致性)。更新用户名意味着更新Users
表中的名称。
然而,当您试图取回数据时,问题出现了。例如,为了找到与特定用户的帖子相关联的所有标签和评论,关系数据库程序员使用一个 JOIN。通过使用连接,数据库根据应用屏幕设计返回所有数据,但是真正的问题是数据库执行什么操作来获得结果集。
一般来说,任何 RDBMS 从磁盘读取并进行寻道,这要花费 99%以上的时间来读取一行。当涉及到磁盘访问时,随机寻道是敌人。这在这个上下文中如此重要的原因是因为连接通常需要随机查找。连接操作是关系数据库中开销最大的操作之一。此外,如果最终需要将数据库扩展到多台服务器,就会引入生成分布式连接的问题,这是一个复杂且通常很慢的操作。
6.3.2 MongoDB 文档数据模型方法
如您所知,在 MongoDB 中,数据存储在文档中。幸运的是,作为应用设计者,这为模式设计带来了一些新的可能性。不幸的是,这也使我们的模式设计过程变得复杂。现在,当面临模式设计问题时,不再像关系数据库那样有固定的规范化数据库设计路径。在 MongoDB 中,模式设计取决于您试图解决的问题。
如果您必须使用 MongoDB 文档模型对上述内容进行建模,您可以将博客数据存储在一个文档中,如下所示:
{
"_id" : ObjectId("509d27069cc1ae293b36928d"),
"title" : "Sample title",
"body" : "Sample text.",
"tags" : [
"Tag1",
"Tag2",
"Tag3",
"Tag4"
],
"created_date" : ISODate("2015-07-06T12:41:39.110Z"),
"author" : "Author 1",
"category_id" : ObjectId("509d29709cc1ae293b369295"),
"comments" : [
{
"subject" : "Sample comment",
"body" : "Comment Body",
"author " : "author 2",
"created_date":ISODate("2015-07-06T13:34:23.929Z")
}
]}
如您所见,您只在一个文档中嵌入了注释和标签。或者,您可以通过引用_id
字段的注释和标签来“规范化”模型:
// Authors document:
{
"_id": ObjectId("509d280e9cc1ae293b36928e "),
"name": "Author 1",}
// Tags document:
{
"_id": ObjectId("509d35349cc1ae293b369299"),
"TagName": "Tag1",.....}
// Comments document:
{
"_id": ObjectId("509d359a9cc1ae293b3692a0"),
"Author": ObjectId("508d27069cc1ae293b36928d"),
.......
"created_date" : ISODate("2015-07-06T13:34:59.336Z")
}
//Category Document
{
"_id": ObjectId("509d29709cc1ae293b369295"),
"Category": "Catgeory1"......
}
//Posts Document
{
"_id" : ObjectId("509d27069cc1ae293b36928d"),
"title" : "Sample title","body" : "Sample text.",
"tags" : [ ObjectId("509d35349cc1ae293b369299"),
ObjectId("509d35349cc1ae293b36929c")
],
"created_date" : ISODate("2015-07-06T13:41:39.110Z"),
"author_id" : ObjectId("509d280e9cc1ae293b36928e"),
"category_id" : ObjectId("509d29709cc1ae293b369295"),
"comments" : [
ObjectId("509d359a9cc1ae293b3692a0"),
]}
这一章的剩余部分将致力于确定哪种解决方案将在您的环境中工作(即是否使用引用或是否嵌入)。
6.3.2.1 嵌入
在本节中,您将看到嵌入是否会对性能产生积极的影响。当您想要获取一些数据集并将其显示在屏幕上时,例如显示与博客相关的评论的页面,嵌入会很有用;在这种情况下,评论可以嵌入到博客文档中。
这种方法的好处是,由于 MongoDB 将文档连续存储在磁盘上,所以所有相关数据都可以在一次寻道中获取。
除此之外,由于不支持连接,并且在这种情况下使用了引用,所以应用可能会执行如下操作来获取与博客相关联的评论数据。
Fetch the associated comments_id
from the blogs document. Fetch the comments document based on the comments_id
found in the first step.
如果您采用这种方法(引用),不仅数据库必须进行多次查找才能找到您的数据,而且还会在查找中引入额外的延迟,因为现在需要两次往返数据库才能检索到您的数据。
如果应用经常访问博客中的评论数据,那么几乎可以肯定的是,在博客文档中嵌入评论会对性能产生积极的影响。
支持嵌入的另一个考虑是在编写数据时对原子性和隔离性的需求。MongoDB 的设计没有多文档事务。在 MongoDB 中,操作的原子性只在单个文档级别提供,因此需要一起原子更新的数据需要一起放在单个文档中。
当您更新数据库中的数据时,您必须确保您的更新要么成功,要么完全失败,决不能有“部分成功”,并且没有其他数据库读取器会看到未完成的写操作。
6.3.2.2 参考
您已经看到,嵌入是在许多情况下提供最佳性能的方法;它还提供数据一致性保证。然而,在某些情况下,更规范化的模型在 MongoDB 中工作得更好。
拥有多个集合并添加引用的一个原因是它在查询数据时增加了灵活性。让我们用上面提到的博客例子来理解这一点。
您了解了如何使用嵌入式模式,当在一个页面上显示所有数据时(即显示博客文章和所有相关评论的页面),这种模式会工作得很好。
现在假设您需要搜索某个用户发布的评论。查询(使用这个嵌入式模式)如下所示:
db.posts.find({'comments.author': 'author2'},{'comments': 1})
那么,该查询的结果将是以下形式的文档:
{
"_id" : ObjectId("509d27069cc1ae293b36928d"),
"comments" : [ {
"subject" : "Sample Comment 1 ",
"body" : "Comment1 Body.",
"author_id" : "author2",
"created_date" : ISODate("2015-07-06T13:34:23.929Z")}...]
}
"_id" : ObjectId("509d27069cc1ae293b36928d"),
"comments" : [
{
"subject" : "Sample Comment 2",
"body" : "Comments Body.",
"author_id" : "author2",
"created_date" : ISODate("2015-07-06T13:34:23.929Z")
}...]}
这种方法的主要缺点是得到的数据比实际需要的多得多。特别是不能只要求 author2 的评论;你必须询问作者 2 评论过的帖子,这也包括对这些帖子的所有其他评论。这些数据需要在应用代码中进一步过滤。
另一方面,假设您决定使用规范化模式。在这种情况下,您将有三个文档:“作者”、“帖子”和“评论”
“作者”文档将包含特定于作者的内容,如姓名、年龄、性别等。,而“Posts”文档将包含特定于帖子的详细信息,如帖子创建时间、帖子作者、实际内容和帖子主题。
“评论”文档将包含帖子的评论,如日期时间、作者创建的评论,以及评论的文本。这描述如下:
// Authors document:
{
"_id": ObjectId("508d280e9cc1ae293b36928e "),
"name": "Jenny",
..........
}
//Posts Document
{
"_id" : ObjectId("508d27069cc1ae293b36928d"),....................
}
// Comments document:
{
"_id": ObjectId("508d359a9cc1ae293b3692a0"),
"Author": ObjectId("508d27069cc1ae293b36928d"),
"created_date" : ISODate("2015-07-06T13:34:59.336Z"),
"Post_id": ObjectId("508d27069cc1ae293b36928d"),
..........
}
在这个场景中,通过“author2”查找评论的查询可以通过对评论集合执行简单的find()
来完成:
db.comments.find({"author": "author2"})
一般来说,如果您的应用的查询模式是众所周知的,并且数据往往只能以一种方式访问,那么嵌入式方法就可以很好地工作。或者,如果您的应用可能以许多不同的方式查询数据,或者您无法预测查询数据的模式,那么更“规范化”的方法可能更好。
例如,在上面的模式中,您将能够使用 limit、skip 操作符对注释进行排序或返回一组更受限制的注释。在嵌入的情况下,你不得不按照存储在文章中的顺序来检索所有的评论。
另一个可能支持使用文档引用的因素是当您有一对多关系时。
例如,一个有大量读者参与的热门博客可能会有数百甚至数千条评论。在这种情况下,嵌入会带来很大的损失:
- 对读取性能的影响:随着文档大小的增加,它将占用更多的内存。内存的问题是,MongoDB 数据库将频繁访问的文档缓存在内存中,文档越大,放入内存的可能性就越小。这将导致在检索文档时出现更多的页面错误,这将导致随机的磁盘 I/O,从而进一步降低性能。
- 对更新性能的影响:随着文件大小的增加以及对这类文档执行更新操作以追加数据,MongoDB 最终需要将文档移动到一个有更多可用空间的区域。这种移动一旦发生,会显著降低更新性能。
除此之外,MongoDB 文档有一个 16MB 的硬性大小限制。尽管需要注意这一点,但在达到 16MB 的大小限制之前,您通常会由于内存压力和文档复制而遇到问题。
支持使用文档引用的最后一个因素是多对多或 M:N 关系。
例如,在上面的例子中,有一些标签。每个博客可以有多个标签,每个标签可以与多个博客条目相关联。
实现 blogs-tags M:N 关系的一种方法是拥有以下三个集合:
Tags
集合,它将存储标签的详细信息Blogs
集合,其中将有博客的详细信息- 第三个集合叫做
Tag-To-Blog Mapping
,它将在标签和博客之间进行映射
这种方法类似于关系数据库中的方法,但是这会对应用的性能产生负面影响,因为查询最终会执行大量应用级的“连接”
或者,您可以使用嵌入模型,在博客文档中嵌入标签,但是这将导致数据重复。虽然这将稍微简化读取操作,但它将增加更新操作的复杂性,因为在更新标签细节时,用户需要确保更新的标签在嵌入到其他博客文档中的每个地方都得到更新。
因此,对于多对多连接,折衷的方法通常是最好的,嵌入一列_id
值而不是整个文档:
// Tags document:
{
"_id": ObjectId("508d35349cc1ae293b369299"),
"TagName": "Tag1",
..........
}
// Posts document with Tag IDs added as References
//Posts Document
{ "_id" : ObjectId("508d27069cc1ae293b36928d"),
"tags" : [
ObjectId("509d35349cc1ae293b369299"),
ObjectId("509d35349cc1ae293b36929a"),
ObjectId("509d35349cc1ae293b36929b"),
ObjectId("509d35349cc1ae293b36929c")
],....................................
}
尽管查询会有点复杂,但您不再需要担心到处更新标签。
总之,MongoDB 中的模式设计是您需要做出的最早决策之一,它取决于应用的需求和查询。
正如您所看到的,当您需要一起访问数据或者需要进行原子更新时,嵌入将会产生积极的影响。但是,如果您在查询时需要更多的灵活性,或者如果您有多对多的关系,使用引用是一个不错的选择。
最终,决定取决于应用的访问模式,在 MongoDB 中没有严格的规则。在下一节中,您将了解各种数据建模注意事项。
数据建模的 6.3.2.3 决策
这包括决定如何组织文档,以便有效地对数据建模。需要决定的重要一点是,您是需要嵌入数据还是使用对数据的引用(即,是使用嵌入还是引用)。
这一点最好用一个例子来说明。假设你有一个书评网站,里面有作者和书籍,还有带线索评论的评论。
现在的问题是如何组织收藏。这个决定取决于每本书预期的注释数量以及执行读写操作的频率。
6.3.2.4 操作注意事项
除了元素之间的交互方式(即是否以嵌入式方式存储文档或使用引用),在为应用设计数据模型时,许多其他操作因素也很重要。这些因素将在以下章节中介绍。
数据生命周期管理
如果您的应用有只需要在数据库中保留有限时间的数据集,则需要使用此功能。
比如说你需要保留一个月的评审和评论的相关数据。可以考虑这个特点。
这是通过使用集合的生存时间(TTL)功能实现的。集合的 TTL 功能确保文档在一段时间后过期。
此外,如果应用要求只处理最近插入的文档,那么使用上限集合将有助于优化性能。
索引
可以创建索引来支持常用的查询,以提高性能。默认情况下,MongoDB 会在_id
字段上创建一个索引。
以下是创建索引时需要考虑的几点:
- 每个索引至少需要 8KB 的数据空间。
- 对于写操作,添加索引会对性能产生一些负面影响。因此,对于具有大量写入的集合,索引可能很昂贵,因为对于每次插入,必须将键添加到所有索引中。
- 索引对于具有大量读取操作的集合非常有用,例如读写操作比例很高的情况。未索引的读取操作不受索引的影响。
分片
设计应用模型时的一个重要因素是是否对数据进行分区。这是在 MongoDB 中使用分片实现的。
分片也称为数据分区。在 MongoDB 中,集合被分区,其文档分布在机器集群上,这些机器集群被称为碎片。这可能会对性能产生重大影响。我们将在 tk 章节中更多地讨论分片。
大量的收藏
拥有多个集合与将数据存储在单个集合中的设计考虑事项如下:
- 选择多个集合来存储数据不会影响性能。
- 对于不同类型的数据拥有不同的集合可以提高高吞吐量批处理应用的性能。
当您设计具有大量集合的模型时,需要考虑以下行为:
- 几千字节的某个最小开销与每个集合相关联。
- 每个索引至少需要 8KB 的数据空间,包括
_id
索引。
现在您已经知道每个数据库的元数据都存储在<database>.ns
文件中。每个集合和索引在名称空间文件中都有自己的条目,因此在决定实现大量集合时,需要考虑名称空间文件的大小限制。
文档的增长
一些更新,比如将一个元素推送到一个数组,添加新的字段等等。会导致文档尺寸的增加,这可能会导致文档从一个槽移动到另一个槽以适合文档。这一文档重定位过程既耗费资源又耗费时间。尽管 MongoDB 提供了填充来最小化重定位的发生,但是您可能需要手动处理文档的增长。
6.4 总结
在本章中,您学习了基本的 CRUD 操作和高级查询功能。您还研究了存储和检索数据的两种方式:嵌入和引用。
在下一章中,您将了解 MongoDB 架构、其核心组件和特性。
七、MongoDB 架构
“MongoDB 架构涵盖了 MongoDB 的深层架构概念。”
在本章中,您将了解 MongoDB 架构,尤其是核心流程和工具、独立部署、分片概念、复制概念和生产部署。
7.1 核心流程
MongoDB 包中的核心组件是
- mongod,这是核心数据库进程
- mongos,它是分片集群的控制器和查询路由
- mongo,这是交互式 MongoDB shell
这些组件在 bin 文件夹下作为应用提供。让我们详细讨论这些组件。
7.1.1 蒙戈布
MongoDB 系统中的主要守护进程称为 mongod。这个守护进程处理所有数据请求,管理数据格式,并执行后台管理操作。
当一个 mongod 在没有任何参数的情况下运行时,它连接到默认的数据目录,即C:\data\db
或/data/db
,以及默认的端口 27017,在这里它监听套接字连接。
在 mongod 进程启动之前,确保数据目录存在并且您拥有对该目录的写权限是很重要的。
如果该目录不存在,或者您对该目录没有写权限,此过程的开始将失败。如果默认端口 27017 不可用,服务器将无法启动。
mongod 还有一个 HTTP 服务器,它监听比默认端口高 1000 的端口,所以如果您使用默认端口 27017 启动 mongod,在这种情况下,HTTP 服务器将位于端口 28017 上,并且可以使用 URL http://localhost:28017 进行访问。这个基本的 HTTP 服务器提供关于数据库的管理信息。
蒙哥
mongo 为开发者直接在数据库上测试查询和操作以及系统管理员管理数据库提供了一个交互式 JavaScript 接口。这都是通过命令行完成的。当 mongo shell 启动时,它将连接到名为test
的默认数据库。这个数据库连接值被分配给全局变量db
。
作为开发者或管理员,您需要在第一次连接建立后将数据库从test
更改为您的数据库。您可以通过使用<databasename>.
来完成此操作
7.1.3 僧侣
mongos 用于 MongoDB 分片。它充当路由服务,处理来自应用层的查询,并确定所请求的数据在分片集群中的位置。
我们将在分片部分更详细地讨论 mongos。现在,您可以将 mongos 视为将查询路由到保存数据的正确服务器的过程。
7.2 MongoDB 工具
除了核心服务之外,MongoDB 安装中还提供了各种工具:
- mongodump:这个工具被用作有效备份策略的一部分。它创建数据库内容的二进制导出。
- mongorestore:使用 mongorestore 实用程序将 mongodump 实用程序创建的二进制数据库转储导入到新的或现有的数据库中。
- bsondump:这个实用程序将 BSON 文件转换成人类可读的格式,比如 JSON 和 CSV。例如,这个实用程序可以用来读取 mongodump 生成的输出文件。
- mongoimport、mongoexport: mongoimport 提供了一种方法,用于获取 JSON 、 CSV 或 TSV 格式的数据,并将其导入到 mongod 实例中。mongoexport 提供了一种将数据从 mongod 实例导出为 JSON、CSV 或 TSV 格式的方法。
- mongostat、mongotop、mongosniff:这些实用程序提供与 mongod 实例的当前操作相关的诊断信息。
7.3 独立部署
独立部署用于开发目的;它不能确保任何数据冗余,也不能确保故障时的恢复。因此不建议在生产环境中使用。独立部署有以下组件:一个 mongod 和一个连接到 mongod 的客户端,如图 7-1 所示。
图 7-1。
Standalone deployment
MongoDB 使用分片和复制,通过分发和复制数据来提供高可用性系统。在接下来的章节中,您将会看到分片和复制。接下来,您将看到推荐的生产部署架构。
7.4 复制
在独立部署中,如果 mongod 不可用,您就有丢失所有数据的风险,这在生产环境中是不可接受的。复制用于防止这种数据丢失。
复制通过在不同节点上复制数据来提供数据冗余,从而在节点出现故障时提供数据保护。复制在 MongoDB 部署中提供了高可用性。
复制还简化了某些管理任务,在这些任务中,备份等日常任务可以转移到复制副本拷贝,从而释放主拷贝来处理重要的应用请求。
在某些情况下,它还可以让客户端从不同的数据拷贝中读取数据,从而有助于扩展读取。
在本节中,您将了解复制在 MongoDB 及其各种组件中是如何工作的。MongoDB 支持两种类型的复制:传统的主/从复制和副本集。
7.4.1 主/从复制
在 MongoDB 中,传统的主/从复制是可用的,但建议仅用于超过 50 个节点的复制。首选的复制方法是副本集,我们将在后面解释。在这种类型的复制中,有一个主服务器和许多从服务器,它们从主服务器复制数据。这种复制的唯一优点是集群中的从属服务器数量没有限制。然而,数千个从节点会使主节点负担过重,所以在实际场景中,最好少于十几个从节点。此外,这种类型的复制不会自动执行故障切换,并且提供的冗余较少。
在基本的主/从设置中,您有两种类型的 mongod 实例:一个实例处于主模式,其余的处于从模式,如图 7-2 所示。因为从设备是从主设备复制的,所以所有从设备都需要知道主设备的地址。
图 7-2。
Master/slave replication
主节点维护有上限的集合(操作日志),该集合存储对数据库的逻辑写入的有序历史。
从设备使用该操作日志集合复制数据。由于操作日志是有上限的集合,如果从设备的状态远远落后于主设备的状态,则从设备可能变得不同步。在这种情况下,复制将停止,需要手动干预来重新建立复制。
从机变得不同步有两个主要原因:
- 从机关闭或停止,稍后重新启动。在此期间,操作日志可能已经删除了需要在从设备上应用的操作的日志。
- 从设备在执行从主设备获得的更新时速度很慢。
7.4.2 副本集
副本集是传统主从复制的复杂形式,是 MongoDB 部署中的推荐方法。
副本集基本上是一种主从复制,但它们提供自动故障转移。一个副本集有一个主副本集和多个从副本集,主副本集被称为主要副本集,从副本集被称为次要副本集;然而,与主-从复制不同,在副本集中没有一个固定的主节点。
如果副本集中的主节点出现故障,一个从节点会自动提升为主节点。客户端开始连接到新的主服务器,数据和应用都将保持可用。在副本集中,这种故障转移以自动方式发生。我们将在后面解释这个过程如何发生的细节。
通过选举机制选择主节点。如果主节点关闭,选定的节点将被选为主节点。
图 7-3 显示了两个成员的副本集故障转移是如何发生的。让我们讨论故障转移中两个成员的副本集发生的各个步骤。
The primary goes down, and the secondary is promoted as primary. The original primary comes up, it acts as slave, and becomes the secondary node.
图 7-3。
Two-member replica set failover
需要注意的几点是
- 副本集是 mongod 的集群,它在彼此之间复制并确保自动故障转移。
- 在副本集中,一个 mongod 将是主要成员,其他的将是次要成员。
- 主要成员由副本集的成员选举产生。所有写入都指向主成员,而辅助成员使用操作日志从主成员异步复制。
- 辅助节点的数据集反映了主节点的数据集,使它们能够在当前主节点不可用时升级到主节点。
副本集复制对成员数量有限制。在 3.0 版之前,限制为 12 个,但在 3.0 版中已更改为 50 个。因此,现在副本集复制最多只能有 50 个成员,并且在任何给定的时间点,在 50 个成员的副本集中,只有 7 个成员可以参与投票。我们将在一个副本集中详细解释投票的概念。
从 3.0 版开始,副本集成员可以使用不同的存储引擎。例如,次要成员可以使用 WiredTiger 存储引擎,而主要成员可以使用 MMAPv1 引擎。在接下来的小节中,您将看到 MongoDB 中可用的不同存储引擎。
7.4.2.1 小学和中学成员
在继续讨论副本集如何工作之前,让我们先看看副本集可以拥有的成员类型。有两种类型的成员:主要成员和次要成员。
- 主要成员:一个副本集只能有一个主要成员,由副本集中的投票节点选举产生。关联优先级为 1 的任何节点都可以被选为主要节点。客户端将所有写操作重定向到主要成员,然后再复制到次要成员。
- 次要成员:普通的次要成员持有数据的副本。次要成员可以投票,也可以在当前主要成员发生故障转移的情况下成为提升为主要成员的候选成员。
除此之外,副本集可以有其他类型的次要成员。
次要成员的 7.4.2.2 类型
优先级为 0 的成员是辅助成员,维护主成员的数据副本,但在发生故障转移时永远不会成为主成员。除此之外,它们还充当普通的辅助节点,可以参与投票并接受读取请求。优先级为 0 的成员是通过将优先级设置为 0 来创建的。
这种类型的成员特别有用,原因如下:
They can serve as a cold standby. In replica sets with varied hardware or geographic distribution, this configuration ensures that only the qualified members get elected as primary. In a replica set that spans multiple data centers across network partitioning, this configuration can help ensure that the main data center has the eligible primary. This is used to ensure that the failover is quick.
隐藏成员是对客户端应用隐藏的 0 优先级成员。与 0 优先级成员一样,该成员也维护主节点数据的副本,不能成为主节点,并可以参与投票,但与 0 优先级成员不同,它不能处理任何读取请求或接收复制要求之外的任何流量。通过将 hidden 属性设置为 true,可以将节点设置为隐藏成员。在副本集中,这些成员可以专用于报告需求或备份。
延迟成员是从主成员的操作日志中延迟复制数据的辅助成员。这有助于从人为错误中恢复,例如意外删除数据库或由不成功的应用升级导致的错误。
在决定延迟时间时,请考虑您的维护周期和操作日志的大小。延迟时间应等于或大于维护窗口,并且操作日志大小的设置应确保在复制时不会丢失任何操作。
请注意,由于延迟成员没有作为主节点的最新数据,因此优先级应该设置为 0,这样它们就不能成为主节点。此外,hidden 属性应该为 true,以避免任何读取请求。
仲裁员是次要成员,不持有主要成员数据的副本,因此他们永远不会成为主要成员。他们仅作为成员参与投票。这使得副本集可以具有奇数个节点,而不会产生数据复制所产生的任何复制成本。
无投票权的成员持有主要成员的数据副本,他们可以接受客户端读取操作,也可以成为主要成员,但他们不能在选举中投票。
成员的投票能力可以通过将其票数设置为 0 来禁用。默认情况下,每个成员都有一票。假设您有一个包含七个成员的副本集。在 mongo shell 中使用以下命令,第四、第五和第六个成员的投票被设置为 0:
cfg_1 = rs.conf()
cfg_1.members[3].votes = 0
cfg_1.members[4].votes = 0
cfg_1.members[5].votes = 0
rs.reconfig(cfg_1)
虽然此设置允许第四、第五和第六名成员被选为主要成员,但在投票时,他们的投票将不被计算在内。他们成为无投票权的成员,这意味着他们可以参选,但不能投票。
您将在本章后面看到如何配置成员。
7.4.2.3 选举
在本节中,您将了解选择主要成员的选举过程。为了当选,服务器不仅需要拥有多数票,还需要拥有总票数的多数。
如果有 X 个服务器,每个服务器有 1 票,那么一个服务器只有当它至少有[(X/2) + 1]票时才能成为主服务器。
如果一个服务器获得了所需的票数或更多,那么它将成为主要的。
发生故障的主设备仍然是设备的一部分;当它启动时,它将充当辅助服务器,直到它再次获得多数选票。
这种类型的投票系统的复杂性在于,不能只有两个节点充当主节点和从节点。在这种情况下,您总共有两票,要成为主节点,一个节点需要多数票,在这种情况下是两票。如果其中一台服务器出现故障,另一台服务器将最终拥有两票中的一票,它将永远不会被提升为主服务器,因此它将仍然是从服务器。
在网络分区的情况下,主节点将失去多数票,因为它将只有自己的一票,并且它将被降级为从节点,并且在没有多数票的情况下,充当从节点的节点也将保持为从节点。您将最终拥有两个从服务器,直到两个服务器再次相互连接。
副本集有许多方法可以避免这种情况。最简单的方法是使用仲裁器来帮助解决这种冲突。它非常轻量级,只是一个投票器,所以它可以在任何一个服务器上运行。
现在让我们看看使用仲裁器后,上述场景会发生什么变化。让我们首先考虑网络分区场景。如果你有一个主服务器、一个从服务器和一个仲裁服务器,每个都有一票,总共三票。如果发生网络分区,主设备和仲裁器位于一个数据中心,而从设备位于另一个数据中心,则主设备仍将是主设备,因为它仍将拥有多数投票权。
如果主设备在没有网络分区的情况下失败,从设备可以提升为主设备,因为它将拥有两票(从设备+仲裁器)。
这种三台服务器的设置提供了强大的故障转移部署。
示例-更详细的选举流程工作
这一节将解释选举是如何发生的。
让我们假设您有一个包含以下三个成员的副本集:A1、B1 和 C1。每个成员每隔几秒钟与其他成员交换一次心跳请求。成员用他们的当前情况信息来响应这种请求。A1 向 B1 和 C1 发出心跳请求。B1 和 C1 用它们的当前状况信息来响应,例如它们所处的状态(主节点或次节点)、它们当前的时钟时间、它们被提升为主节点的资格等等。A1 接收所有这些信息,并更新其集合的“map ”,该“map”维护诸如成员改变的状态、已经下降或上升的成员以及往返时间之类的信息。
在更新 A1 的地图更改时,它将根据其状态检查一些事情:
-
A1 要做第一个任务是运行一个健全性检查,检查几个问题的答案,例如,A1 认为它已经是主的了吗?另一个成员认为它是主要的吗?A1 没有参选资格吗?如果它不能回答任何一个基本问题,A1 就会继续这样空转下去;否则,它将继续选举进程:
- A1 向该组的其他成员(在本例中是 B1 和 C1)发送消息,说“我计划成为主要成员”。请建议”
- 当 B1 和 C1 收到消息时,他们将检查他们周围的景色。它们将运行一个大的健全性检查列表,例如,是否有任何其他节点可以作为主节点?A1 是否有最新的数据,或者是否有任何其他节点有最新的数据?如果所有的检查看起来都没问题,他们发送一个“继续”信息;但是,如果任何检查失败,就会发送“停止选举”消息。
- 如果任何成员发送“停止选举”回复,则选举被取消,A1 仍然是次要成员。
- 如果所有人都同意,A1 将进入选举过程的最后阶段。
-
在第二(最终)阶段,
- A1 向其余成员重新发送消息,宣布其候选资格。
- 成员 B1 和 C1 做最后一次检查,以确保所有的答案仍然像以前一样正确。
- 如果是,则允许 A1 获取其选举锁,这将阻止其投票能力 30 秒,并发回一个投票。
- 如果任何一项检查不成立,就会发出否决。
- 如果收到任何否决,选举停止。
- 如果没有人否决,A1 获得了大多数选票,它就成为初选。
-
如果 A1 是次要的,并且如果映射没有改变,它将偶尔检查它是否应该选举自己。
-
降级:当 A1 被降级时会有一个问题。在 MongoDB 中,默认情况下写操作是一劳永逸的(即客户端发出写操作,但不等待响应)。如果应用在主应用退出时执行默认写入,它将永远不会意识到写入实际上并未发生,最终可能会丢失数据。因此,建议使用安全写入。在这种情况下,当主节点退出时,它会关闭所有的客户端连接,这将导致客户端出现套接字错误。然后,客户端库需要重新检查谁是新的主服务器,以免丢失其写操作数据。
-
如果 A1 是主要的,并且其中一个成员已经关闭,那么它将确保它仍然能够到达集合的大多数。如果它不能这样做,它会将自己降级到次级状态。
选举受到优先级设置的影响。优先级为 0 的成员永远不能成为主要成员。
7.4.2.4 数据复制流程
让我们看看数据复制是如何工作的。副本集的成员不断复制数据。包括主要成员在内的每个成员都维护一个操作日志。操作日志是有上限的集合,其中成员维护对数据集执行的所有操作的记录。
次要成员复制主要成员的操作日志,并以异步方式应用所有操作。
升起来
操作日志代表操作日志。操作日志是一个有上限的集合,其中记录了修改数据的所有操作。
操作日志保存在一个特殊的数据库中,即集合oplog.$main
中的local
。每个操作都作为一个文档来维护,其中每个文档对应于在主服务器上执行的一个操作。该文档包含各种密钥,包括以下密钥:
ts
:存储操作执行时的时间戳。它是一个内部类型,由一个 4 字节的时间戳和一个 4 字节的递增计数器组成。op
:存储所执行操作的类型信息。该值存储为 1 字节代码(例如,它将为插入操作存储“I”)。ns
:这个键存储执行操作的集合名称空间。o
:该键指定执行的操作。在插入的情况下,这将存储要插入的文档。
只有更改数据的操作才会保留在操作日志中,因为这是一种确保辅助节点数据与主节点数据同步的机制。
存储在操作日志中的操作被转换,以便它们保持等幂,这意味着即使在辅助节点上多次应用,辅助节点数据也将保持一致。由于操作日志是一个有上限的集合,随着操作的每一个新添加,最旧的操作被自动移出。这样做是为了确保它不会增长超过预设的界限,即操作日志大小。
根据操作系统的不同,每当副本集成员首次启动时,MongoDB 都会创建一个默认大小的操作日志。
在 MongoDB 中,默认情况下,Windows 和 64 位 Linux 实例上的操作日志使用 5%的可用空间。如果大小低于 1GB,那么 1GB 的空间由 MongoDB 分配。
虽然默认大小在大多数情况下已经足够,但是您可以在启动服务器时使用–oplogsize
选项来指定操作日志的大小(以 MB 为单位)。
如果您有以下工作负载,可能需要重新考虑操作日志大小:
- 同时更新多个文档:由于操作需要被转换成幂等的操作,这种情况可能最终需要大量的操作日志大小。
- 删除和插入以相同的速率发生,涉及相同的数据量:在这种情况下,尽管数据库大小不会增加,但是转换成幂等操作的操作会导致更大的操作日志。
- 大量就地更新:尽管这些更新不会改变数据库大小,但是在操作日志中将更新记录为幂等操作会导致操作日志变大。
初始同步和复制
当成员处于以下两种情况之一时,完成初始同步:
The node has started for the first time (i.e. it’s a new node and has no data). The node has become stale, where the primary has overwritten the oplog and the node has not replicated the data. In this case, the data will be removed.
在这两种情况下,初始同步都包括以下步骤:
First, all databases are cloned. Using oplog of the source node, the changes are applied to its dataset. Finally, the indexes are built on all the collections.
初始同步后,副本集成员不断复制更改,以便保持最新。
大多数同步从主节点发生,但是可以启用链式复制,其中同步仅从辅助节点发生(即,同步目标根据 ping 时间和其他成员复制的状态而改变)。
同步–正常操作
在正常操作中,辅助节点选择一个成员,并从中同步其数据,然后从所选源的操作日志集合中提取操作(local.oplog.rs
)。
一旦获得操作(op ),辅助节点将执行以下操作:
It first applies the op to its data copy. Then it writes the op to its local oplog. Once the op is written to the oplog, it requests the next op.
假设它在第一步和第二步之间崩溃了,然后又回来了。在这种情况下,它将假设操作尚未执行,并将重新应用它。
由于操作日志操作是幂等的,相同的操作可以被应用任意次,并且每次结果文档将是相同的。
- 如果您有以下文档
- 在相同的上执行递增操作,例如
- 主服务器上的{$inc:{I:1}}
- 在这种情况下,以下内容将存储在主操作日志中:
- {我:12}。
这将被辅助节点复制。因此,即使多次应用日志,该值也保持不变。
启动
当一个节点启动时,它检查它的本地集合以找出lastOpTimeWritten
。这是在辅助节点上应用的最新操作的时间。
以下 shell 助手可用于在 shell 中查找最新的 op:
> rs.debug.getLastOpWritten()
输出返回一个名为ts
的字段,它描述了最后一次操作时间。
如果一个成员启动并找到了ts
条目,它首先选择一个要同步的目标,然后像正常操作一样开始同步。但是,如果没有找到条目,节点将开始初始同步过程。
从谁同步?
在本节中,您将了解如何选择同步源。从 2.0 开始,基于平均 ping 时间,服务器自动从“最近”的节点同步。
当您启动一个新节点时,它会向所有节点发送心跳并监控响应时间。根据收到的数据,它使用以下算法决定要同步的成员:
for each healthy member Loop:
if state is Primary
add the member to possible sync target set
if member’s lastOpTimeWritten is greater then the local lastOpTime Written
add the member to possible sync target set
Set sync_from = MIN (PING TIME to members of sync target set)
Note: A “healthy member” can be thought of as a “normal” primary or secondary member.
在 2.0 版本中,从属节点的延迟节点被有争议地包含在“健康”节点中。从 2.2 版开始,延迟节点和隐藏节点被排除在“健康”节点之外。
运行以下命令将显示被选为同步源的服务器:
db.adminCommand({replSetGetStatus:1})
syncingTo
的输出字段仅出现在辅助节点上,并提供同步节点的相关信息。
让写操作与链接从机一起工作
您已经看到,上述选择同步源的算法意味着从属链接是半自动的。当服务器启动时,它很可能会选择同一数据中心内的服务器进行同步,从而减少 WAN 流量。
然而,这绝不会导致循环,因为节点将仅从具有大于其自身的最新值lastOpTimeWritten
的辅助节点同步。你永远不会陷入 N1 从 N2 同步、N2 从 N1 同步的局面。永远不是 N1 从 N2 同步,就是 N2 从 N1 同步。
在本节中,您将看到 w(写操作)如何与从链接一起工作。如果 N1 从 N2 同步,而又从 N3 同步,在这种情况下,N3 如何知道 N1 同步到哪一点。
当 N1 从 N2 开始同步时,会发送一个特殊的“握手”消息,通知 N2 N1 将从其操作日志同步。由于 N2 不是主节点,它会将消息转发到它正在同步的节点(即,它打开一个到 N3 的连接,假装是 N1)。到上述步骤结束时,N2 已经打开了与 N3 的两个连接:一个是它自己的连接,另一个是 N1 的连接。
每当 N1 向 N2 发出 op 请求时,N2 就会从其操作日志中发送 op,并在 N1 到 N3 的链路上转发一个伪请求,如图 7-4 所示。
图 7-4。
Writes via chaining slaves
虽然这最大限度地减少了网络流量,但增加了写操作到达所有成员的绝对时间。
7.4.2.5 故障转移
在本节中,您将了解在副本集中如何处理主要和次要成员故障转移。副本集的所有成员都相互连接。如图 7-5 所示,它们相互交换心跳消息。
图 7-5。
Heartbeat message exchange
因此,心跳丢失的节点被认为是崩溃的。
如果该节点是次节点
如果该节点是辅助节点,它将从副本集的成员中删除。在未来,当它恢复时,它可以重新加入。一旦它重新加入,它需要更新最新的变化。
If the down period is small, it connects to the primary and catches up with the latest updates. However, if the down period is lengthy, the secondary server will need to resync with primary where it deletes all its data and does an initial sync as if it’s a new server.
如果该节点是主节点
如果该节点是主节点,在这种情况下,如果原始副本集的大多数成员能够相互连接,则这些节点将选出新的主节点,这符合副本集的自动故障转移能力。
选举过程将由无法到达主节点的任何节点发起。
新的主节点由大多数副本集节点选举产生。仲裁器可用于打破平局,例如当网络分区将参与节点分成两半并且大多数节点无法到达时。
优先级最高的节点将成为新的主节点。如果有一个以上的节点具有相同的优先级,数据新鲜度可用于打破平局。
主节点使用心跳来跟踪有多少节点对其可见。如果可见节点的数量低于多数,主节点会自动退回到辅助节点状态。当主服务器被网络分区分开时,这种情况会阻止主服务器运行。
7.4.2.6 回滚
在主节点发生变化的情况下,新主节点上的数据被认为是系统中的最新数据。当以前的主节点重新加入时,对其应用的任何操作也将回滚。然后,它将与新的主服务器同步。
回滚操作将恢复所有未在副本集中复制的写操作。这样做是为了维护整个副本集的数据库一致性。
当连接到新的主节点时,所有节点都要经历一个重新同步过程,以确保完成回滚。这些节点检查新主服务器上没有的操作,然后查询新主服务器以返回受操作影响的文档的更新副本。节点处于重新同步过程中,称为正在恢复;在这一过程完成之前,他们没有资格参加初选。
这种情况很少发生,如果发生了,通常是由于复制滞后的网络分区,辅助节点无法跟上前一个主节点上的操作吞吐量。
需要注意的是,如果写操作在主节点降级之前复制到其他成员,并且这些成员对于副本集的大多数节点都是可访问的,则回滚不会发生。
回滚数据被写入数据库的 dbpath
目录下的文件,文件名如<database>.<collection>.<timestamp>.bson
。
管理员可以决定忽略或应用回滚数据。只有当所有节点都与新的主节点同步并且已经回滚到一致状态时,才能开始应用回滚数据。
回滚文件的内容可以使用Bsondump
读取,然后需要使用 mongorestore 手动应用到新的主服务器。
对于 MongoDB,没有自动处理回滚情况的方法。因此,需要手动干预来应用回滚数据。在应用回滚时,确保将这些复制到集合中的所有成员或至少部分成员是至关重要的,以便在发生任何故障转移时可以避免回滚。
7.4.2.7 一致性
您已经看到,副本集成员通过读取操作日志不断在彼此之间复制数据。如何维护数据的一致性?在这一节中,您将看到 MongoDB 如何确保您总是访问一致的数据。
在 MongoDB 中,虽然读操作可以路由到辅助节点,但是写操作总是路由到主节点,消除了两个节点同时尝试更新同一数据集的情况。主节点上的数据集总是一致的。
如果读请求被路由到主节点,它将总是看到最新的更改,这意味着读操作总是与最后的写操作一致。
但是,如果应用已将读取首选项更改为从辅助节点读取,用户可能看不到最新的更改或以前的状态。这是因为写入在辅助节点上异步复制。
这种行为的特征是最终一致性,这意味着虽然辅助节点的状态与主节点的状态不一致,但随着时间的推移,它最终会变得一致。
没有任何方法可以保证从辅助节点的读取是一致的,除非发出写入问题,以确保在操作实际标记为成功之前,所有成员上的写入都成功。我们稍后将讨论写问题。
7.4.2.8 可能的复制部署
您选择用来部署副本集的体系结构会影响其功能和容量。在这一节中,您将看到在决定架构时需要注意的一些策略。我们还将讨论部署架构。
Odd number of members: This should be done in order to ensure that there is no tie when electing a primary. If the number of nodes is even, then an arbiter can be used to ensure that the total nodes participating in election is odd, as shown in Figure 7-6.
图 7-6。
Members replica set with primary, secondary, and arbiter Replica set fault tolerance is the count of members, which can go down but still the replica set has enough members to elect a primary in case of any failure. Table 7-1 indicates the relationship between the member count in the replica set and its fault tolerance. Fault tolerance should be considered when deciding on the number of members.
表 7-1。
Replica Set Fault Tolerance
| 成员人数 | 初选所需的多数票 | 容错 | | --- | --- | --- | | three | Two | one | | four | three | one | | five | three | Two | | six | four | Two |If the application has specific dedicated requirements, such as for reporting or backups, then delayed or hidden members can be considered as part of the replica set, as shown in Figure 7-7.
图 7-7。
Members replica set with primary, secondary, and hidden members If the application is read-heavy, the read can be distributed across secondaries. As the requirement increases, more nodes can be added to increase the data duplication; this can have a positive impact on the read throughput. The members should be distributed geographically in order to cater to main data center failure. As shown in Figure 7-8, the members that are kept at a geographically different location other than the main data center can have priority set as 0, so that they cannot be elected as primary and can act as a standby only.
图 7-8。
Members replica set with primary, secondary, and a priority 0 member distributed across the data center When replica set members are distributed across data centers, network partitioning can prevent data centers from communicating with each other. In order to ensure a majority in the case of network partitioning, it keeps a majority of the members in one location.
7.4.2.9 缩放读取
虽然辅助节点的主要目的是在主节点停机时确保数据可用性,但是辅助节点还有其他有效的使用案例。它们可以专门用于执行备份操作或数据处理作业,或者扩展读取。扩展读取的方法之一是对辅助节点发出读取查询;通过这样做,减少了主服务器上的工作量。
在使用辅助节点进行扩展读取操作时,需要考虑的一个要点是,在 MongoDB 中,复制是异步的,这意味着如果对主节点的数据执行任何写入或更新操作,辅助节点的数据将暂时过时。如果有问题的应用需要大量读取,并且是通过网络访问的,并且不需要最新数据,则可以使用辅助节点来横向扩展读取,以便提供良好的读取吞吐量。虽然默认情况下,读取请求被路由到主节点,但是可以通过指定读取首选项将请求分布到辅助节点。图 7-9 描述了默认的读取偏好。
图 7-9。
Default read preference
以下是理想的使用情形,通过这些情形,在辅助节点上路由读取可以帮助显著提高读取吞吐量,还可以帮助减少延迟:
Applications that are geographically distributed: In such cases, you can have a replica set that is distributed across geographies. The read preferences should be set to read from the nearest secondary node. This helps in reducing the latency that is caused when reading over network and this improves the read performance. See Figure 7-10.
图 7-10。
Read Preference – Nearest If the application always requires up-to-date data, it uses the option primaryPreferred
, which in normal circumstances will always read from the primary node, but in case of emergency will route the read to secondaries. This is useful during failovers. See Figure 7-11.
图 7-11。
Read Preference – primaryPreferred If you have an application that supports two types of operations, the first operation is the main workload that involves reading and doing some processing on the data, whereas the second operation generates reports using the data. In such a scenario, you can have the reporting reads directed to the secondaries.
MongoDB 支持以下读取首选模式:
- 主要:这是默认模式。所有读取请求都被路由到主节点。
- primaryPreferred:在正常情况下,读取将从主节点进行,但是在紧急情况下,例如主节点不可用,读取将从辅助节点进行。
- 次要:从次要成员读取。
- secondaryPreferred:从次要成员读取。如果辅助节点不可用,则从主节点读取。
- nearest:从最近的副本集成员中读取。
除了扩展读取,使用辅助节点的第二个理想用例是卸载密集型处理、聚合和管理任务,以避免降低主节点的性能。可以在辅助节点上执行阻塞操作,而不会影响主节点的性能。
7.4.2.10 应用编写问题
当客户机应用与 MongoDB 交互时,它通常不知道数据库是在独立部署上还是作为副本集部署。然而,当处理副本集时,客户机应该注意写问题和读问题。
由于副本集复制数据并将其存储在多个节点上,这两个问题为客户端应用提供了在执行读或写操作时跨节点实施数据一致性的灵活性。
使用写关注使应用能够从 MongoDB 获得成功或失败的响应。
当在 MongoDB 的副本集部署中使用时,写操作会从服务器向应用发送一个确认,表明主节点上的写操作已经成功。但是,可以对此进行配置,以便仅当写入操作被复制到维护数据的所有节点时,写入问题才返回成功。
在实际场景中,这是不可行的,因为它会降低写性能。理想情况下,客户端可以使用写操作来确保数据被复制到除主节点之外的另一个节点,这样即使主节点出现故障,数据也不会丢失。
写问题返回一个指示错误或无错误的对象。
w
选项确保写入已被复制到指定数量的成员。可以指定一个数字或多数作为w
选项的值。
如果指定了一个数字,则在返回成功之前,写操作会复制到该数量的节点。如果指定了多数,则在返回结果之前,写操作会复制到大多数成员。
图 7-12 显示了如何对 w: 2 进行写操作。
图 7-12。
writeConcern
如果在指定 number 时,数量大于实际保存数据的节点,该命令将继续等待,直到成员可用。为了避免这种不确定的等待时间,wtimeout
也应该与w
一起使用,这将确保它将等待指定的时间段,如果到那时写操作还没有成功,它将超时。
写操作是如何发生的
为了确保所写的数据存在于至少两个成员上,发出以下命令:
>db.testprod.insert({i:”test”, q: 50, t: “B”}, {writeConcern: {w:2}})
为了理解如何执行这个命令,假设您有两个成员,一个名为 primary,另一个名为 secondary,它正在从主服务器同步数据。
但是,主映像如何知道辅助映像同步的时间点呢?因为辅助节点向主节点的操作日志查询要应用的操作结果,所以如果辅助节点请求在比如说 t 时间写入的操作,这向主节点暗示辅助节点已经复制了在 t 之前写入的所有操作
以下是写问题需要采取的步骤。
The write operation is directed to the primary. The operation is written to the oplog of primary with ts
depicting the time of operation. A w: 2
is issued, so the write operation needs to be written to one more server before it’s marked successful. The secondary queries the primary’s oplog for the op, and it applies the op. Next, the secondary sends a request to the primary requesting for ops with ts
greater than t. At this point, the primary sends an update that the operation until t has been applied by the secondary as it’s requesting for ops with {ts: {$gt: t}}
. The writeConcern finds that a write has occurred on both the primary and secondary, satisfying the w: 2
criteria, and the command returns success.
7.4.3 使用副本集实现高级集群
学习了副本集的体系结构和内部工作原理后,现在您将重点关注副本集的管理和使用。您将关注以下内容:
Setting up a replica set. Removing a server. Adding a server. Adding an arbiter. Inspecting the status. Forcing a new election of a primary. Using the web interface to inspect the status of the replica set.
下面的例子假设一个名为testset
的副本集,其配置如表 7-2 所示。
表 7-2。
Replica Set Configuration
| 成员 | 守护进程 | 主机:端口 | 数据文件路径 | | --- | --- | --- | --- | | 活动成员 1 | 蒙戈布 | [主机名]:27021 | C:\db1\active1\data | | 活动成员 2 | 蒙戈布 | [主机名]:27022 | C:\db1\active2\data | | 被动 _ 成员 _1 | 蒙戈布 | [主机名]:27023 | C:\db1\passive1\data |使用以下命令可以找到上表中使用的主机名:
C:\>hostname
ANOC9
C:\>
在以下示例中,[hostname]需要替换为 hostname 命令在您的系统上返回的值。在我们的例子中,返回的值是 ANOC9,它在下面的例子中使用。
在以下实现中使用默认的(MMAPv1)存储引擎。
7.4.3.1 设置副本集
为了设置并运行副本,您需要启动并运行所有活动成员。
第一步是启动第一个活动成员。打开终端窗口,创建data
目录:
C:\>mkdir C:\db1\active1\data
C:\>
连接到 mongod:
c:\practicalmongodb\bin>mongod --dbpath C:\db1\active1\data --port 27021 --replSet testset/ANOC9:27021 –rest
2015-07-13T23:48:40.543-0700 I CONTROL ** WARNING: --rest is specified without --httpinterface,
2015-07-13T23:48:40.543-0700 I CONTROL ** enabling http interface
2015-07-13T23:48:40.543-0700 I CONTROL Hotfix KB2731284 or later update is installed, no need to zero-out data files
2015-07-13T23:48:40.563-0700 I JOURNAL [initandlisten] journal dir=C:\db1\active1\data\journal
2015-07-13T23:48:40.564-0700 I JOURNAL [initandlisten] recover : no journal files present, no recovery needed
..................................... port=27021 dbpath=C:\db1\active1\data 64-bit host=ANOC9
2015-07-13T23:48:40.614-0700 I CONTROL [initandlisten] targetMinOS: Windows 7/Windows Server 2008 R2
2015-07-13T23:48:40.615-0700 I CONTROL [initandlisten] db version v3.0.4
正如您所看到的,–replSet
选项指定了实例正在加入的副本集的名称以及该副本集的另一个成员的名称,在上面的示例中是 Active_Member_2。
虽然您在上面的示例中只指定了一个成员,但是可以通过指定逗号分隔的地址来提供多个成员,如下所示:
mongod –dbpath C:\db1\active1\data –port 27021 –replset testset/[hostname]:27022,[hostname]:27023 --rest
在下一步中,您将启动并运行第二个活动成员。在新的终端窗口中为第二个活动成员创建data
目录。
C:\>mkdir C:\db1\active2\data
C:\>
连接到 mongod:
c:\ practicalmongodb \bin>mongod --dbpath C:\db1\active2\data --port 27022 –replSet testset/ANOC9:27021 –rest
2015-07-13T00:39:11.599-0700 I CONTROL ** WARNING: --rest is specified without --httpinterface,
2015-07-13T00:39:11.599-0700 I CONTROL ** enabling http interface
2015-07-13T00:39:11.604-0700 I CONTROL Hotfix KB2731284 or later update is installed, no need to zero-out data files
2015-07-13T00:39:11.615-0700 I JOURNAL [initandlisten] journal dir=C:\db1\active2\data\journal
2015-07-13T00:39:11.615-0700 I JOURNAL [initandlisten] recover : no journal files present, no recovery needed
2015-07-13T00:39:11.664-0700 I JOURNAL [durability] Durability thread started
2015-07-13T00:39:11.664-0700 I JOURNAL [journal writer] Journal writer thread started rs.initiate() in the shell -- if that is not already done
最后,您需要启动被动成员。打开一个单独的窗口,为被动成员创建data
目录。
C:\>mkdir C:\db1\passive1\data
C:\>
连接到 mongod:
c:\ practicalmongodb \bin>mongod --dbpath C:\db1\passive1\data --port 27023 --replSet testset/ ANOC9:27021 –rest
2015-07-13T05:11:43.746-0700 I CONTROL Hotfix KB2731284 or later update is installed, no need to zero-out data files
2015-07-13T05:11:43.757-0700 I JOURNAL [initandlisten] journal dir=C:\db1\passive1\data\journal
2015-07-13T05:11:43.808-0700 I CONTROL [initandlisten] MongoDB starting : pid=620 port=27019 dbpath=C:\db1\passive1\data 64-bit host= ANOC9
......................................................................................
2015-07-13T05:11:43.812-0700 I CONTROL [initandlisten] options: { net: { http:
{ RESTInterfaceEnabled: true, enabled: true }, port: 27019 }, replication: { re
lSet: "testset/ ANOC9:27017" }, storage: { dbPath: "C:\db1\passive1\data" }
在前面的示例中,--rest
选项用于激活+1000 端口上的 REST 接口。激活 REST 使您能够使用 web 界面检查副本集状态。
在上述步骤结束时,您已经有了三台启动并运行的服务器,它们正在相互通信;然而,副本集仍未初始化。在下一步中,您将初始化副本集,并向每个成员说明他们的职责和角色。
为了初始化副本集,您需要连接到其中一台服务器。在本例中,它是第一台服务器,运行在端口 27021 上。
打开一个新的命令提示符,并连接到第一个服务器的 mongo 界面:
C:\>cd c:\practicalmongodb\bin
c:\practicalmongodb\bin>mongo ANOC9 --port 27021
MongoDB shell version: 3.0.4
connecting to: ANOC9:27021/test
>
接下来,切换到admin
数据库。
> use admin
switched to db admin
>
接下来,建立一个配置数据结构,其中提到了服务器方面的角色:
>cfg = {
... _id: 'testset',
... members: [
... {_id:0, host: 'ANOC9:27021'},
... {_id:1, host: 'ANOC9:27022'},
... {_id:2, host: 'ANOC9:27023', priority:0}
... ]
... }
{ "_id" : "testset",
"members" : [
{
"_id" : 0,
"host" : "ANOC9:27021"
},
..........
{
"_id" : 2,
"host" : "ANOC9:27023",
"priority" : 0
} ]}>
在此步骤中,副本集结构被配置。
您在为被动成员定义角色时使用了 0 优先级。这意味着该成员不能提升为主成员。
下一个命令启动副本集:
> rs.initiate(cfg)
{ "ok" : 1}
现在让我们来查看副本集的状态,以便检查它的设置是否正确:
testset:PRIMARY> rs.status()
{
"set" : "testset",
"date" : ISODate("2015-07-13T04:32:46.222Z")
"myState" : 1,
"members" : [
{
"_id" : 0,
...........................
testset:PRIMARY>
输出表明一切正常。副本集现在已成功配置和初始化。
让我们看看如何确定主节点。为此,请连接到任何成员,发出以下命令并验证主服务器:
testset:PRIMARY> db.isMaster()
{
"setName" : "testset",
"setVersion" : 1,
"ismaster" : true,
"primary" : " ANOC9:27021",
"me" : "ANOC9:27021",
...........................................
"localTime" : ISODate("2015-07-13T04:36:52.365Z"),
.........................................................
"ok" : 1
}testset:PRIMARY>
7.4.3.2 删除服务器
在本例中,您将从集合中移除第二个活动成员。让我们连接到次要成员 mongo 实例。打开一个新的命令提示符,如下所示:
C:\>cd c:\practicalmongodb\bin
c:\practicalmongodb\bin>mongo ANOC9 --port 27022
MongoDB shell version: 3.0.4
connecting to: 127.0.0.1:27022/ANOC9
testset:SECONDARY>
Issue the following command to shut down the instance:
testset:SECONDARY> use admin
switched to db admin
testset:SECONDARY> db.shutdownServer()
2015-07-13T21:48:59.009-0700 I NETWORK DBClientCursor::init call() failed server should be down...
接下来,您需要连接到主要成员 mongo 控制台,并执行以下操作来删除该成员:
testset:PRIMARY> use admin
switched to db admin
testset:PRIMARY> rs.remove("ANOC9:27022")
{ "ok" : 1 }
testset:PRIMARY>
为了检查成员是否被删除,您可以发出rs.status()
命令。
7.4.3.3 添加服务器
接下来,您将向副本集添加一个新的活动成员。与其他成员一样,首先打开一个新的命令提示符并创建data
目录:
C:\>mkdir C:\db1\active3\data
C:\>
接下来,使用以下命令启动 mongod:
c:\practicalmongodb\bin>mongod --dbpath C:\db1\active3\data --port 27024 --replSet testset/ANOC9:27021 --rest
..........
您已经运行了新的 mongod,所以现在您需要将它添加到副本集中。为此,您需要连接到主服务器的 mongo 控制台:
C:\>c:\practicalmongodb\bin\mongo.exe --port 27021
MongoDB shell version: 3.0.4
connecting to: 127.0.0.1:27021/test
testset:PRIMARY>
接下来,您切换到admin
db:
testset:PRIMARY> use admin
switched to db admin
testset:PRIMARY>
最后,需要发出以下命令来将新的 mongod 添加到副本集:
testset:PRIMARY> rs.add("ANOC9:27024")
{ "ok" : 1 }
可以使用rs.status()
检查副本集状态,以检查是否添加了新的活动成员。
7.4.3.4 向副本集添加仲裁器
在本例中,您将向集合中添加一个仲裁成员。与其他成员一样,首先为 MongoDB 实例创建data
目录:
C:\>mkdir c:\db1\arbiter\data
C:\>
接下来使用以下命令启动 mongod:
c:\practicalmongodb\bin>mongod --dbpath c:\db1\arbiter\data --port 30000 --replSet testset/ANOC9:27021 --rest
2015-07-13T22:05:10.205-0700 I CONTROL [initandlisten] MongoDB starting : pid=3700 port=30000 dbpath=c:\db1\arbiter\data 64-bit host=ANOC9
..........................................................
连接到主服务器的 mongo 控制台,切换到admin
db,将新创建的 mongod 作为仲裁器添加到副本集:
C:\>c:\practicalmongodb\bin\mongo.exe --port 27021
MongoDB shell version: 3.0.4
connecting to: 127.0.0.1:27021/test
testset:PRIMARY> use admin
switched to db admin
testset:PRIMARY> rs.addArb("ANOC9:30000")
{ "ok" : 1 }
testset:PRIMARY>
使用rs.status().
可以验证该步骤是否成功
7.4.3.5 使用 rs.status()检查状态
我们在上面的例子中一直引用rs.status()
来检查副本集的状态。在本节中,您将了解这个命令是关于什么的。
它使您能够检查其控制台所连接的成员的状态,还使他们能够查看其在副本集中的角色。
从主节点的 mongo 控制台发出以下命令:
testset:PRIMARY> rs.status()
{
"set" : "testset",
"date" : ISODate("2015-07-13T22:15:46.222Z")
"myState" : 1,
"members" : [
{
"_id" : 0,
...........................
"ok" : 1
testset:PRIMARY>
myState 字段的值指示成员的状态,它可以具有表 7-3 中所示的值。
表 7-3。
Replica Set Status
| 我的状态 | 描述 | | --- | --- | | Zero | 第一阶段,启动 | | one | 主要成员 | | Two | 次要杆件 | | three | 恢复状态 | | four | 致命错误状态 | | five | 第二阶段,启动 | | six | 未知状态 | | seven | 仲裁员成员 | | eight | 停机或不可达 | | nine | 当写操作在从主节点转换后由辅助节点回滚时,就会达到这种状态。 | | Ten | 从副本集中删除成员时,成员将进入此状态。 |因此,上面的命令将 myState 值返回为 1,这表明这是主要成员。
7.4.3.6 强行举行新的选举
可以使用rs.stepDown ()
命令强制当前的主服务器退出运行。这股力量开始了新的初选。
该命令在下列情况下很有用:
When you are simulating the impact of a primary failure, forcing the cluster to fail over. This lets you test how your application responds in such a scenario. When the primary server needs to be offline. This is done for either a maintenance activity or for upgrading or to investigating the server. When a diagnostic process need to be run against the data structures.
以下是对 testset 副本集运行该命令时的输出:
testset:PRIMARY> rs.stepDown()
2015-07-13T22:52:32.000-0700 I NETWORK DBClientCursor::init call() failed
2015-07-13T22:52:32.005-0700 E QUERY Error: error doing query: failed
2015-07-13T22:52:32.009-0700 I NETWORK trying reconnect to 127.0.0.1:27021 (127.0.0.1) failed
2015-07-13T22:52:32.011-0700 I NETWORK reconnect 127.0.0.1:27021 (127.0.0.1) ok testset:SECONDARY>
执行命令后,提示符从 testset:PRIMARY 变为 testset:SECONDARY。
rs.status()
可用于检查stepDown ()
是否成功。
请注意,它返回的 myState 值现在是 2,这意味着“成员作为辅助成员运行。”
7.4.3.7 使用 Web 界面检查副本集的状态
MongoDB 维护了一个基于 web 的控制台,用于查看系统状态。在您的示例中,可以通过 http://localhost:28021 访问控制台。
默认情况下,web 接口端口号被设置为 X+1000,其中 X 是 mongod 实例端口号。在本章的示例中,由于主实例位于 27021 上,因此 web 界面位于端口 28021 上。
图 7-13 显示了副本集状态的链接。点击链接会将你带到如图 7-14 所示的副本集仪表盘。
图 7-14。
Replica set status report
图 7-13。
Web interface
7.5 分片
在上一节中,您看到了如何使用 MongoDB 中的副本集来复制数据,以防止任何不利情况,并分布读取负载以提高读取效率。
MongoDB 大量使用内存进行低延迟数据库操作。当您比较从内存读取数据和从磁盘读取数据的速度时,从内存读取大约比从磁盘读取快 100,000 倍。
在 MongoDB 中,理想情况下工作集应该适合内存。工作集由最常访问的数据和索引组成。
当 MongoDB 访问内存中没有的数据时,就会发生页面错误。如果有可用的空闲内存,操作系统会直接将请求的页面加载到内存中;但是,在没有空闲内存的情况下,内存中的页面被写入磁盘,然后请求的页面被加载到内存中,从而减慢了该过程。很少有操作会意外地从内存中清除大部分工作集,从而对性能产生负面影响。一个例子是扫描数据库中所有文档的查询,其中文档的大小超过了服务器内存。这导致将文档加载到内存中,并将工作集移到磁盘上。
确保在项目的模式设计阶段已经为查询定义了适当的索引覆盖范围,可以最大限度地降低发生这种情况的风险。MongoDB explain 操作可用于提供有关查询计划和所用索引的信息。
MongoDB 的serverStatus
命令返回一个 workingSet 文档,提供对实例工作集大小的估计。操作团队可以跟踪实例在给定时间段内访问了多少页面,以及工作集最早的文档和最新的文档之间经过的时间。跟踪所有这些指标,就有可能检测到工作集何时将达到当前的内存限制,因此可以采取主动措施,以确保系统的可伸缩性足以处理这种情况。
在 MongoDB 中,扩展是通过横向扩展数据(即在多个商用服务器上划分数据)来处理的,这也称为分片(横向扩展)。
分片通过在服务器之间水平划分数据集来解决扩展以支持大型数据集和高吞吐量的挑战,其中每个服务器负责处理其数据部分,没有一个服务器负担过重。这些服务器也称为碎片。
每个碎片都是一个独立的数据库。所有的碎片共同构成了一个逻辑数据库。
分片减少了每个分片处理的操作数。例如,当插入数据时,只需要访问负责存储这些记录的碎片。
随着集群的增长,每个分片需要处理的进程会减少,因为分片包含的数据子集会减少。这导致横向吞吐量和容量的增加。
假设您有一个 1TB 大小的数据库。如果碎片的数量是 4,每个碎片将处理大约 265GB 的数据,而如果碎片的数量增加到 40,每个碎片将只保存 25GB 的数据。
图 7-15 描绘了当分布在三个分片上时,一个分片的集合将如何出现。
图 7-15。
Sharded collection across three shards
虽然分片是一个引人注目的强大功能,但它有很高的基础设施要求,并且增加了整体部署的复杂性。因此,您需要了解您可能考虑使用分片的场景。
在下列情况下使用分片:
- 数据集的规模非常庞大,已经开始挑战单个系统的容量。
- 由于 MongoDB 使用内存来快速获取数据,因此当达到活动工作集限制时,向外扩展就变得很重要。
- 如果应用是写入密集型的,可以使用分片将写入分散到多个服务器上。
7.5.1 分片组件
接下来,您将看到 MongoDB 中支持分片的组件。在 MongoDB 中,分片是通过分片集群实现的。
以下是分片集群的组件:
- 陶瓷或玻璃碎片
- 芒果!芒果
- 配置服务器
碎片是存储实际数据的组件。对于分片集群,它包含一个数据子集,可以是 mongod 或副本集。所有 shard 的数据组合在一起形成了完整的分片集群数据集。
分片是基于每个集合启用的,因此可能存在未分片的集合。在每个分片集群中都有一个主分片,除了分片集合数据之外,所有非共享集合都放在这个主分片中。
部署分片集群时,默认情况下第一个分片会成为主分片,尽管它是可配置的。参见图 7-16 。
图 7-16。
Primary shard
配置服务器是保存分片集群元数据的特殊 mongods。该元数据描述了分片的系统状态和组织。
配置服务器存储单个分片集群的数据。配置服务器应该可用于集群的正常运行。
一个配置服务器可能导致集群的单点故障。对于生产部署,建议至少有三个配置服务器,这样即使一个配置服务器不可访问,集群也能继续运行。
配置服务器将数据存储在配置数据库中,这使得客户端请求能够路由到相应的数据。不应更新此数据库。
只有当为了平衡集群而改变了数据分布时,MongoDB 才会将数据写入配置服务器。
蒙哥人充当路由。它们负责将读写请求从应用路由到碎片。
与 mongo 数据库交互的应用不需要担心数据是如何存储在碎片中的。对他们来说,这是透明的,因为他们只和蒙古人交流。mongos 依次将读写路由到碎片。
mongos 缓存来自配置服务器的元数据,这样对于每个读和写请求,它们不会使配置服务器负担过重。
但是,在下列情况下,数据是从配置服务器中读取的:
- 要么一个现有的 mongos 已经重新启动,要么一个新的 mongos 已经第一次启动。
- 语块迁移。我们将在后面详细解释组块迁移。
数据分发流程
接下来,您将看到在启用了分片的集合中,数据是如何分布在各个分片中的。在 MongoDB 中,数据在集合级别被分片或分布。集合由 shard 键进行分区。
7.5.2.1 碎片钥匙
存在于集合的所有文档中的任何索引单/复合字段都可以是一个分片键。您指定这是集合的文档需要分发的字段基础。在内部,MongoDB 根据字段的值将文档划分成块,并将它们分布在各个碎片上。
MongoDB 支持数据分发的方式有两种:基于范围的分区和基于散列的分区。
基于范围的划分
在基于范围的分区中,shard 键值被划分为多个范围。假设您考虑将一个timestamp
字段作为分片键。在这种划分方式中,这些值被认为是从最小值到最大值的一条直线,其中最小值是起始周期(比如 1970 年 1 月 1 日),最大值是结束周期(比如 9999 年 12 月 31 日)。集合中的每个文档都只有这个范围内的时间戳值,并且它代表行中的某个点。
根据可用碎片的数量,该行将被划分为多个范围,文档将基于这些范围进行分发。
在这种划分方案中,如图 7-17 所示,分片键值在附近的文档很可能落在同一个分片上。这可以显著提高范围查询的性能。
图 7-17。
Range-based partitioning
然而,缺点是它可能导致数据分布不均匀,使其中一个碎片过载,这可能最终接收到大多数请求,而其他碎片仍然负载不足,因此系统将无法正常伸缩。
基于散列的分区
在基于散列的分区中,数据是根据 shard 字段的散列值分布的。如果选中,与基于范围的分区相比,这将导致更随机的分布。
带有 close shard 键的文档不太可能是同一个块的一部分。例如,对于基于_id
字段的散列的范围,将会有一条散列值的直线,这将再次基于碎片的数量进行划分。基于散列值,文档将位于任一碎片中。参见图 7-18 。
图 7-18。
Hash-based partitioning
与基于范围的分区相比,这可以确保数据均匀分布,但这是以高效的范围查询为代价的。
大块
数据以块的形式在碎片之间移动。碎片键范围被进一步划分为子范围,这些子范围也被称为块。参见图 7-19 。
图 7-19。
Chunks
对于分片集群,64MB 是默认的块大小。在大多数情况下,这是块分割和迁移的合适大小。
让我们用一个例子来讨论分片和组块的执行。假设你有一个博客文章集合,它被分割在字段date
中。这意味着集合将根据date
字段值进行拆分。让我们进一步假设你有三个碎片。在这种情况下,数据可能分布在各个分片上,如下所示:
- 碎片#1:时间开始到 2009 年 7 月
- 碎片# 2:2009 年 8 月至 2009 年 12 月
- 碎片# 3:2010 年 1 月到时间的尽头
为了检索从 2010 年 1 月 1 日到今天的文档,查询被发送到 mongos。
在这种情况下,
The client queries mongos. The mongos know which shards have the data, so mongos sends the queries to Shard #3. Shard #3 executes the query and returns the results to mongos. Mongos combines the data received from various shards, which in this case is Shard #3 only, and returns the final result back to the client.
应用不需要知道分片。它可以像询问普通的蒙哥一样询问蒙哥。
让我们考虑另一个场景,您插入一个新文档。新文档有今天的日期。事件的顺序如下:
The document is sent to the mongos. Mongos checks the date and on basis of that, sends the document to Shard #3. Shard #3 inserts the document.
从客户端的角度来看,这同样与单个服务器设置相同。
以上场景中 ConfigServers 的角色
考虑一个场景,您开始收到日期为 2009 年 9 月的数百万个文档的插入请求。在这种情况下,碎片#2 开始过载。
一旦配置服务器意识到碎片#2 变得太大,它就会介入。它将分割碎片上的数据,并开始将其迁移到其他碎片。迁移完成后,它将更新后的状态发送给 mongos。所以现在碎片#2 有从 2009 年 8 月到 2009 年 9 月 18 日的数据,碎片#3 包含从 2009 年 9 月 19 日到时间结束的数据。
当一个新的碎片被添加到集群中时,配置服务器的责任就是找出如何处理它。数据可能需要立即迁移到新的分片,或者新的分片可能需要保留一段时间。总之,配置服务器是大脑。无论何时移动任何数据,配置服务器都会让 mongos 知道最终的配置,以便 mongos 可以继续进行适当的路由。
数据平衡过程
接下来,您将看到集群是如何保持平衡的(例如,MongoDB 如何确保所有的碎片都被同等地加载)。
添加新数据或修改现有数据,或者添加或删除服务器,都可能导致数据分布的不平衡,这意味着要么一个碎片的区块数过多,而其他碎片的区块数较少,要么导致区块大小增加,远远大于其他区块。
MongoDB 确保与以下后台进程的平衡:
- 组块分割
- 平衡器
7.5.3.1 语块分裂
块分割是确保块具有指定大小的过程之一。如您所见,选择了一个分片键,它用于标识文档将如何跨分片分布。文档被进一步分组为 64MB 的块(默认,可配置),并根据其托管的范围存储在碎片中。
如果块的大小由于插入或更新操作而改变,并且超过了默认的块大小,那么 mongos 会将块分成两个更小的块。参见图 7-20
图 7-20。
Chunk splitting
该过程将块保持在指定大小或小于指定大小的碎片内(即,它确保块具有配置的大小)。
插入和更新操作触发拆分。当元数据被修改时,分割操作导致配置服务器中的数据被修改。尽管分割不会导致数据迁移,但此操作会导致集群不平衡,一个碎片比另一个碎片有更多的区块。
7.5.3.2 平衡器
平衡器是一个后台进程,用于确保所有碎片的负载相等或处于平衡状态。该过程管理区块迁移。
组块的分裂会导致不平衡。添加或删除文档也会导致集群不平衡。在集群不平衡的情况下,使用平衡器,平衡器是平均分配数据的过程。
当一个碎片的块比其他碎片多的时候,MongoDB 会自动在这些碎片之间进行块平衡。这个过程对应用和您都是透明的。
集群中的任何 mongos 都可以启动平衡器进程。他们通过获取配置服务器的配置数据库上的锁来做到这一点,因为平衡器涉及到从一个碎片到另一个碎片的块迁移,这可能导致元数据的改变,这将导致配置服务器数据库的改变。平衡器进程会对数据库性能产生巨大影响,因此它可以
Be configured to start the migration only when the migration threshold has reached. The migration threshold is the difference in the number of maximum and minimum chunks on the shards. Threshold is shown in Table 7-4.
表 7-4。
Migration Threshold
| 组块数量 | 迁移阈值 | | --- | --- | | < 20 | Two | | 21-80 | four | | >80 | eight |Or it can be scheduled to run in a time period that will not impact the production traffic.
平衡器一次迁移一个块(参见图 7-21 )并遵循以下步骤:
图 7-21。
Chunk migration The moveChunk
command is sent to the source shard. An internal moveChunk
command is started on the source where it creates the copy of the documents within the chunk and queues it. In the meantime, any operations for that chunk are routed to the source by the mongos because the config database is not yet changed and the source will be responsible for serving any read/write request on that chunk. The destination shard starts receiving the copy of the data from the source. Once all of the documents in the chunks have been received by the destination shard, the synchronization process is initiated to ensure that all changes that have happened to the data while migration are updated at the destination shard. Once the synchronization is completed, the next step is to update the metadata with the chunk’s new location in the config database. This activity is done by the destination shard that connects to the config database and carries out the necessary updates. Post successful completion of all the above, the document copy that is maintained at the source shard is deleted.
如果同时平衡器需要从源碎片进行额外的块迁移,它可以开始新的迁移,甚至不需要等待当前迁移的删除步骤完成。
如果在迁移过程中出现任何错误,平衡器会中止迁移过程,将块留在原始碎片上。成功完成该过程后,MongoDB 会从原始碎片中删除块数据。
添加或删除碎片也会导致集群不平衡。当添加一个新的碎片时,立即开始向该碎片的数据迁移。但是,集群达到平衡需要时间。
当一个碎片被移除时,平衡器确保数据被迁移到其他碎片,并且元数据信息被更新。两个活动完成后,碎片被安全地移除。
操作
接下来,您将了解如何在分片集群上执行读写操作。如上所述,配置服务器维护集群元数据。这些数据存储在配置数据库中。mongos 使用配置数据库的这些数据为应用的读写请求提供服务。
数据由 mongos 实例缓存,然后用于将读写操作路由到碎片。这样,配置服务器就不会过载。
mongos 只会在以下情况下从配置服务器中读取数据:
- 蒙古人第一次开始
- 现有的 mongos 已重新启动或
- 在块迁移之后,当 mongos 需要用新的集群元数据更新其缓存的元数据时。
无论何时发出任何操作,mongos 需要做的第一步是识别将服务于请求的碎片。由于分片键用于在分片集群中分发数据,如果操作使用分片键字段,那么基于该字段,可以将特定的分片作为目标。
如果碎片键是employeeid
,可能会发生以下情况:
If the find
query contains the employeeid
field, then to satiate the query, only specific shards will be targeted by the mongos. If a single update operation uses employeeid
for updating the document, the request will be routed to the shard holding that employee data.
但是,如果操作没有使用碎片键,那么请求将被广播到所有碎片。通常,多次更新或删除操作是针对整个群集的。
在查询数据时,除了识别碎片并从中获取数据之外,mongos 可能还需要处理从各种碎片返回的数据,然后再将最终输出发送给客户机。
假设一个应用用sort()
发出了一个find()
请求。在这种情况下,蒙哥将把$orderby
选项传递给碎片。碎片将从它们的数据集中获取数据,并以有序的方式发送结果。一旦 mongos 拥有了所有 shard 的排序数据,它将对整个数据执行增量合并排序,然后将最终输出返回给客户机。
与 sort 类似的还有limit()
、skip(),
等聚合函数。,它要求 mongos 在接收来自碎片的数据之后,在将最终结果集返回给客户机之前执行操作。
mongos 消耗最少的系统资源,并且没有持久状态。因此,如果应用需求是一个简单的find ()
查询,可以由碎片单独满足,并且不需要在 mongos 级别进行操作,那么您可以在运行应用服务器的同一系统上运行 mongos。
实施分片
在本节中,您将学习在 Windows 平台上的一台机器上配置分片。
您将通过仅使用两个碎片来保持示例的简单性。在此配置中,您将使用表 7-5 中列出的服务。
表 7-5。
Sharding Cluster Configuration
| 成分 | 类型 | 港口 | 数据文件路径 | | --- | --- | --- | --- | | 碎片控制器 | 莽哥 | Twenty-seven thousand and twenty-one | - | | 配置服务器 | 蒙戈布 | Twenty-seven thousand and twenty-two | C:\db1\config\data | | 沙尔多 | 蒙戈布 | Twenty-seven thousand and twenty-three | C:\db1\shard1\data | | Shard1 | 蒙戈布 | Twenty-seven thousand and twenty-four | C:\db1\shard2\data |您将关注以下内容:
Setting up a sharded cluster. Creating a database and collection, and enable sharding on the collection. Using the import command to load data in the sharded collection. Distributed data amongst the shards. Adding and removing shards from the cluster and checking how data is distributed automatically.
7.5.5.1 设置碎片集群
为了设置集群,第一步是设置配置服务器。在新的终端窗口中输入以下代码,为配置服务器创建data
目录,并启动 mongod:
C:\> mkdir C:\db1\config\data
C:\>CD C:\practicalmongodb\bin
C:\ practicalmongodb\bin>mongod --port 27022 --dbpath C:\db1\config\data --configsvr
2015-07-13T23:02:41.982-0700 I JOURNAL [journal writer] Journal writer thread started
2015-07-13T23:02:41.984-0700 I CONTROL [initandlisten] MongoDB starting : pid=3084 port=27022 dbpath=C:\db1\config\data master=1 64-bit host=ANOC9
......................................
2015-07-13T23:02:42.066-0700 I REPL [initandlisten] ******
2015-07-13T03:02:42.067-0700 I NETWORK [initandlisten] waiting for connections on port 27022
接下来,开始蒙哥。在新的终端窗口中键入以下内容:
C:\>cd c:\practicalmongodb\bin
c:\practicalmongodb\bin>mongos --configdb localhost:27022 --port 27021 --chunkSize 1
2015-07-13T23:06:07.246-0700 W SHARDING running with 1 config server should be done only for testing purposes and is not recommended for production
...............................................................
2015-07-13T23:09:07.464-0700 I SHARDING [Balancer] distributed lock 'balancer/ ANOC9:27021:1429783567:41' unlocked
你现在有了碎片控制器(即 mongos)并开始运行。
如果您切换到启动配置服务器的窗口,您会发现 shard 服务器注册到了配置服务器。
在本例中,您使用了 1MB 的块大小。请注意,这在现实生活中并不理想,因为大小小于 4MB(文档的最大大小)。但是,这只是出于演示目的,因为这将创建必要数量的块,而不会加载大量数据。除非另外指定,否则默认情况下 chunkSize 为 128MB。
接下来,打开分片服务器 Shard0 和 Shard1。
打开一个新的终端窗口。为第一个碎片创建data
目录并启动 mongod:
C:\>mkdir C:\db1\shard0\data
C:\>cd c:\practicalmongodb\bin
c:\practicalmongodb\bin>mongod --port 27023 --dbpath c:\db1\shard0\data –shardsvr
2015-07-13T23:14:58.076-0700 I CONTROL [initandlisten] MongoDB starting : pid=1996 port=27023 dbpath=c:\db1\shard0\data 64-bit host=ANOC9
.................................................................
2015-07-13T23:14:58.158-0700 I NETWORK [initandlisten] waiting for connections on port 27023
打开新的终端窗口。为第二个碎片创建data
目录并启动 mongod:
C:\>mkdir c:\db1\shard1\data
C:\>cd c:\practicalmongodb\bin
c:\practicalmongodb\bin>mongod --port 27024 --dbpath C:\db1\shard1\data --shardsvr
2015-07-13T23:17:01.704-0700 I CONTROL [initandlisten] MongoDB starting : pid=3672 port=27024 dbpath=C:\db1\shard1\data 64-bit host=ANOC9
2015-07-13T23:17:01.704-0700 I NETWORK [initandlisten] waiting for connections on port 27024
在上述步骤结束时,与设置相关的所有服务器都已启动并运行。下一步是将碎片信息添加到碎片控制器。
mongos 对应用来说是一个完整的 MongoDB 实例,尽管实际上并不是一个完整的实例。mongo shell 可以用来连接到 mongo 来执行任何操作。
打开 mongos mongo 控制台:
C:\>cd c:\practicalmongodb\bin
c:\ practicalmongodb\bin>mongo localhost:27021
MongoDB shell version: 3.0.4
connecting to: localhost:27021/test
mongos>
切换到admin
数据库:
mongos> use admin
switched to db admin
mongos>
通过运行以下命令添加碎片信息:
mongos> db.runCommand({addshard:"localhost:27023",allowLocal:true})
{ "shardAdded" : "shard0000", "ok" : 1 }
mongos> db.runCommand({addshard:"localhost:27024",allowLocal:true})
{ "shardAdded" : "shard0001", "ok" : 1 }
mongos>
这将激活两个 shard 服务器。
下一个命令检查碎片:
mongos> db.runCommand({listshards:1})
{
"shards" : [
{
"_id" : "shard0000",
"host" : "localhost:27023"
}, {
"_id" : "shard0001",
"host" : "localhost:27024"
}
], "ok" : 1}
7.5.5.2 创建数据库和碎片收集
为了继续这个例子,您将创建一个名为testdb
的数据库和一个名为testcollection
的集合,您将在键testkey
上对其进行分片。
连接到 mongos 控制台,发出以下命令来获取数据库:
mongos> testdb=db.getSisterDB("testdb")
testdb
接下来,在数据库级别为testdb
启用分片:
mongos> db.runCommand({enableSharding system: "testdb"})
{ "ok" : 1 }
mongos>
接下来,指定需要分片的集合以及将对集合进行分片的键:
mongos> db.runCommand({shardcollection: "testdb.testcollection", key: {testkey:1}})
{ "collectionsharded" : "testdb.testcollection", "ok" : 1 }
mongos>
完成上述步骤后,您现在就有了一个分片集群,所有组件都已启动并运行。您还创建了一个数据库,并对集合启用了分片。
接下来,将数据导入到集合中,以便可以检查碎片上的数据分布。
您将使用 import 命令在testcollection
中加载数据。连接到新的终端窗口,并执行以下操作:
C:\>cd C:\practicalmongodb\bin
C:\practicalmongodb\bin>mongoimport --host ANOC9 --port 27021 --db testdb --collection testcollection --type csv --file c:\mongoimport.csv –-headerline
2015-07-13T23:17:39.101-0700 connected to: ANOC9:27021
2015-07-13T23:17:42.298-0700 [##############..........] testdb.testcollection 1.1 MB/1.9 MB (59.6%)
2015-07-13T23:17:44.781-0700 imported 100000 documents
mongoimport.csv
由两个字段组成。第一个是testkey
,这是一个随机产生的数字。第二个字段是文本字段;它用于确保文档占据足够数量的块,使得使用分片机制变得可行。
这将在集合中插入 100,000 个对象。
为了检查记录是否被插入,连接到 mongos 的 mongo 控制台并发出以下命令:
C:\Windows\system32>cd c:\practicalmongodb\bin
c:\practicalmongodb\bin>mongo localhost:27021
MongoDB shell version: 3.0.4
connecting to: localhost:27021/test
mongos> use testdb
switched to db testdb
mongos> db.testcollection.count()
100000
mongos>
接下来,连接到两个 shard(shard 0 和 Shard1)的控制台,查看数据是如何分布的。打开一个新的终端窗口并连接到 Shard0 的控制台:
C:\>cd C:\practicalmongodb\bin
C:\ practicalmongodb\bin>mongo localhost:27023
MongoDB shell version: 3.0.4
connecting to: localhost:27023/test
切换到testdb
,发出count()
命令,检查分片上的文件数量:
> use testdb
switched to db testdb
> db.testcollection.count()
57998
接下来,打开一个新的终端窗口,连接到 Shard1 的控制台,按照上述步骤操作(即切换到testdb
并检查testcollection
集合的计数):
C:\>cd c:\practicalmongodb\bin
c:\practicalmongodb\bin>mongo localhost:27024
MongoDB shell version: 3.0.4
connecting to: localhost:27024/test
> use testdb
switched to db testdb
> db.testcollection.count()
42002
>
当您运行上面的命令一段时间后,您可能会看到每个碎片中文档的编号有所不同。当加载文档时,mongos 将所有的块放在一个分片上。随着时间的推移,碎片集通过在所有碎片之间平均分配块而重新平衡。
7.5.5.3 增加了一个新碎片
您已经设置了一个分片集群,还对一个集合进行了分片,并查看了数据在分片之间的分布情况。接下来,您将向集群添加一个新的碎片,以便负载分布得更广一些。
您将重复上述步骤。首先,在新的终端窗口中为新碎片创建一个数据目录:
c:\>mkdir c:\db1\shard2\data
接下来,在端口 27025 启动 mongod:
c:\>cd c:\practicalmongodb\bin
c:\ practicalmongodb\bin>mongod --port 27025 --dbpath C:\db1\shard2\data --shardsvr
2015-07-13T23:25:49.103-0700 I CONTROL [initandlisten] MongoDB starting : pid=3744 port=27025 dbpath=C:\db1\shard2\data 64-bit host=ANOC9
................................
2015-07-13T23:25:49.183-0700 I NETWORK [initandlisten] waiting for connections on port 27025
接下来,新的 shard 服务器将被添加到 shard 集群中。为了配置它,在一个新的终端窗口中打开 mongos mongo 控制台:
C:\>cd c:\practicalmongodb\bin
c:\practicalmongodb\bin>mongo localhost:27021
MongoDB shell version: 3.0.4
connecting to: localhost:27021/test
mongos>
切换到admin
数据库并运行addshard
命令。该命令将分片服务器添加到分片集群中。
mongos> use admin
switched to db admin
mongos> db.runCommand({addshard: "localhost:27025", allowlocal: true})
{ "shardAdded" : "shard0002", "ok" : 1 }
mongos>
为了检查添加是否成功,运行listshards
命令:
mongos> db.runCommand({listshards:1})
{
"shards" : [
{
"_id" : "shard0000",
"host" : "localhost:27023"
},
{
"_id" : "shard0001",
"host" : "localhost:27024"
},
{
"_id" : "shard0002",
"host" : "localhost:27025"
}
],
"ok" : 1
}
接下来,检查testcollection
数据是如何分布的。在新的终端窗口中连接到新碎片的控制台:
C:\>cd c:\practicalmongodb\bin
c:\practicalmongodb\bin>mongo localhost:27025
MongoDB shell version: 3.0.4
connecting to: localhost:27025/test
切换到testdb
并检查碎片上列出的收藏:
> use testdb
switched to db testdb
> show collections
system.indexes
testcollection
发出三次testcollection.count
命令:
> db.testcollection.count()
6928
> db.testcollection.count()
12928
> db.testcollection.count()
16928
有趣的是,收藏品的数量正在慢慢增加。蒙古人正在重新平衡星团。
随着时间的推移,数据块将从分片服务器 Shard0 和 Shard1 迁移到新添加的分片服务器 Shard2,这样数据就可以均匀地分布在所有服务器上。完成此过程后,配置服务器元数据将被更新。这是一个自动的过程,即使 testcollection 中没有新的数据添加,它也会发生。这是决定块大小时需要考虑的重要因素之一。
如果chunkSize
的值非常大,那么数据分布将会不均匀。chunkSize
越小,数据分布越均匀。
7.5.5.4 取出一块碎片
在下面的例子中,您将看到如何删除一个 shard 服务器。对于本例,您将删除在上例中添加的服务器。
为了启动这个过程,您需要登录到 mongos 控制台,切换到admin
db,并执行以下命令从 shard 集群中删除 shard:
C:\>cd c:\practicalmongodb\bin
c:\practicalmongodb\bin>mongo localhost:27021
MongoDB shell version: 3.0.4
connecting to: localhost:27021/test
mongos> use admin
switched to db admin
mongos> db.runCommand({removeShard: "localhost:27025"})
{
"msg" : "draining started successfully",
"state" : "started",
"shard" : "shard0002",
"ok" : 1
}
mongos>
如您所见,removeShard
命令返回了一条消息。其中一个消息字段是state
,它指示进程状态。该消息还指出排空过程已经开始。这由字段msg
指示。
您可以重新发出removeShard
命令来检查进度:
mongos> db.runCommand({removeShard: "localhost:27025"})
{
"msg" : "draining ongoing",
"state" : "ongoing",
"remaining" : {
"chunks" : NumberLong(2),
"dbs" : NumberLong(0)
},
"ok" : 1
}
mongos>
响应告诉您仍然需要从服务器中排出的块和数据库的数量。如果您重新发出该命令,并且该进程终止,则该命令的输出将显示相同的内容。
mongos> db.runCommand({removeShard: "localhost:27025"})
{
"msg" : "removeshard completed successfully",
"state" : "completed",
"shard" : "shard0002",
"ok" : 1
}
mongos>
你可以用listshards
来检验removeShard
是否成功。
正如您所看到的,数据成功地迁移到了其他 Shard,因此您可以删除存储文件并终止 Shard2 mongod 进程。
这种在不离线的情况下修改 shard 集群的能力是 MongoDB 的关键组件之一,这使它能够支持高可用性、高可伸缩性、大容量的数据存储。
7.5.5.5 列出了分片集群的状态
printShardingStatus()
命令提供了很多关于分片系统内部的信息。
mongos> db.printShardingStatus()
--- Sharding Status ---
sharding version: {
"_id" : 1,
"version" : 3,
"minCompatibleVersion" : 5,
"currentVersion" : 6,
"clusterId" : ObjectId("52fb7a8647e47c5884749a1a")
}
shards:
{ "_id" : "shard0000", "host" : "localhost:27023" }
{ "_id" : "shard0001", "host" : "localhost:27024" }
balancer:
Currently enabled: yes
Currently running: no
Failed balancer rounds in last 5 attempts: 0
Migration Results for the last 24 hours:
17 : Success
databases:
{ "_id" : "admin", "partitioned" : false, "primary" : "config" }
{ "_id" : "testdb", "partitioned" : true, "primary" : "shard0000" }
...............
输出列出了以下内容:
- 碎片集群的所有碎片服务器
- 每个分片数据库/集合的配置
- 共享数据集的所有块
可以从上面的命令中获得的重要信息是分片键范围,它与每个块相关联。这也显示了特定的块存储在哪里(在哪个 shard 服务器上)。输出可以用来分析 shard 服务器的密钥和块分布。
7.5.6 控制集合分发(基于标签的分片)
在上一节中,您看到了数据分布是如何发生的。在本节中,您将了解基于标记的分片。这个特性是在版本 2.2.0 中引入的。
标签使运营商能够控制哪些集合将被分配到哪个碎片。
为了理解基于标签的分片,让我们建立一个分片集群。您将使用上面创建的 shard 集群。对于本例,您需要三个 Shard,因此您将再次向集群添加 Shard2。
7.5.6.1 先决条件
您将首先启动集群。再次重申,请遵循以下步骤。
Start the config server. Enter the following command in a new terminal window (if it’s not already running): C:\> mkdir C:\db1\config\data
C:\>cd c:\practicalmongodb\bin
C:\practicalmongodb\bin>mongod --port 27022 --dbpath C:\db\config\data --configsvr
Start the mongos. Enter the following command in a new terminal window (if it’s not already running): C:\>cd c:\practicalmongodb\bin
c:\practicalmongodb\bin>mongos --configdb localhost:27022 --port 27021
You will start the shard servers next.
开始分享。在新的终端窗口中输入以下命令(如果它尚未运行):
C:\>cd c:\practicalmongodb\bin
c:\practicalmongodb\bin>mongod --port 27023 --dbpath c:\db1\shard0\data --shardsvr
开始 Shard1。在新的终端窗口中输入以下命令(如果它尚未运行):
C:\>cd c:\practicalmongodb\bin
C:\practicalmongodb\bin>mongod --port 27024 --dbpath c:\db1\shard1\data –shardsvr
开始 Shard2。在新的终端窗口中输入以下命令(如果它尚未运行):
C:\>cd c:\practicalmongodb\bin
c:\practicalmongodb\bin>mongod --port 27025 --dbpath c:\db1\shard2\data –shardsvr
由于在前面的示例中已经从分片集群中删除了 Shard2,因此必须将 Shard2 添加到分片集群中,因为在本示例中需要三个分片。
为此,您需要连接到 mongos。输入以下命令:
C:\Windows\system32>cd c:\practicalmongodb\bin
c:\practicalmongodb\bin>mongo localhost:27021
MongoDB shell version: 3.0.
4
connecting to: localhost:27021/test
mongos>
在将碎片添加到集群之前,您需要删除testdb
数据库:
mongos> use testdb
switched to db testdb
mongos> db.dropDatabase()
{ "dropped" : "testdb", "ok" : 1 }
mongos>
接下来,使用以下步骤添加 Shard2 碎片:
mongos> use admin
switched to db admin
mongos> db.runCommand({addshard: "localhost:27025", allowlocal: true})
{ "shardAdded" : "shard0002", "ok" : 1 }
mongos>
如果您尝试在不删除testdb
数据库的情况下添加已删除的碎片,将会出现以下错误:
mongos>db.runCommand({addshard: "localhost:27025", allowlocal: true})
{
"ok" : 0,
"errmsg" : "can't add shard localhost:27025 because a local database 'testdb' exists in another shard0000:localhost:27023"}
为了确保所有三个碎片都出现在集群中,请运行以下命令:
mongos> db.runCommand({listshards:1})
{
"shards" : [
{
"_id" : "shard0000",
"host" : "localhost:27023"
}, {
"_id" : "shard0001",
"host" : "localhost:27024"
}, {
"_id" : "shard0002",
"host" : "localhost:27025"
}
], "ok" : 1}
7.5.6.2 标签
在上述步骤结束时,您已经拥有了带有一个配置服务器、三个分片和一个运行中的 mongos 的分片集群。接下来,在新的终端窗口中连接到 mongos 的 30999 端口和 configdb 的 27022 端口:
C:\ >cd c:\practicalmongodb\bin
c:\ practicalmongodb\bin>mongos --port 30999 --configdb localhost:27022
2015-07-13T23:24:39.674-0700 W SHARDING running with 1 config server should be done only for testing purposes and is not recommended for production
...................................
2015-07-13T23:24:39.931-0700 I SHARDING [Balancer] distributed lock 'balancer /ANOC9:30999: 1429851279:41' unlocked..
接下来,启动一个新的终端窗口,连接到 mongos,并在集合上启用分片:
C:\ >cd c:\practicalmongodb\bin
c:\practicalmongodb\bin>mongo localhost:27021
MongoDB shell version: 3.0.4
connecting to: localhost:27021/test
mongos> show dbs
admin (empty)
config 0.016GB
testdb 0.078GB
mongos> conn=new Mongo("localhost:30999")
connection to localhost:30999
mongos> db=conn.getDB("movies")
movies
mongos> sh.enableSharding("movies")
{ "ok" : 1 }
mongos> sh.shardCollection("movies.drama", {originality:1})
{ "collectionsharded" : "movies.hindi", "ok" : 1 }
mongos> sh.shardCollection("movies.action", {distribution:1})
{ "collectionsharded" : "movies.action", "ok" : 1 }
mongos> sh.shardCollection("movies.comedy", {collections:1})
{ "collectionsharded" : "movies.comedy", "ok" : 1 }
mongos>
步骤如下:
Connect to the mongos console. View the running databases connected to the mongos instance running at port 30999. Get reference to the database movies
. Enable sharding of the database movies
. Shard the collection movies.drama
by shard key originality
. Shard the collection movies.action
by shard key distribution
. Shard the collection movies.comedy
by shard key collections
.
接下来,使用以下命令序列在集合中插入一些数据:
mongos>for(var i=0;i<100000;i++){db.drama.insert({originality:Math.random(), count:i, time:new Date()});}
mongos>for(var i=0;i<100000;i++){db.action.insert({distribution:Math.random(),
count:i, time:new Date()});}
mongos>for(var i=0;i<100000;i++) {db.comedy.insert({collections:Math.random(), count:i, time:new Date()});}
mongos>
到上述步骤结束时,您已经有了三个分片和三个启用了分片的集合。接下来,您将看到数据是如何跨碎片分布的。
切换到 configdb:
mongos> use config
switched to db config
mongos>
您可以使用chunks.find
来查看块是如何分布的:
mongos> db.chunks.find({ns:"movies.drama"}, {shard:1, _id:0}).sort({shard:1})
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
mongos> db.chunks.find({ns:"movies.action"}, {shard:1, _id:0}).sort({shard:1})
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
mongos> db.chunks.find({ns:"movies.comedy"}, {shard:1, _id:0}).sort({shard:1})
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
mongos>
如你所见,大块相当均匀地分布在碎片中。见图 7-22
图 7-22。
Distribution without tagging
接下来,您将使用标记来分隔集合。这样做的目的是让每个碎片有一个集合(也就是说,你的目标是让块分布如表 7-6 所示)。
表 7-6。
Chunk Distribution
| 集合块 | 在碎片上 | | --- | --- | | 电影.戏剧 | 沙尔德 0000 | | 电影.动作 | Shard0001 | | 电影.喜剧 | Shard0002 |标签描述了碎片的属性,可以是任何东西。因此,您可以将一个碎片标记为“慢”或“快”或“机架空间”或“西海岸”
在以下示例中,您将把碎片标记为属于每个集合:
mongos> sh.addShardTag("shard0000", "dramas")
mongos> sh.addShardTag("shard0001", "actions")
mongos> sh.addShardTag("shard0002", "comedies")
mongos>
这意味着:
- 在 shard0000 上贴上“戏剧”标签。
- 将标记为“actions”的块放在 shard0001 上。
- 并在 shard0002 上贴上“喜剧”的标签。
接下来,您将创建相应的规则来标记集合块。
规则 1:在movies.drama
集合中创建的所有块都将被标记为“戏剧”
mongos> sh.addTagRange("movies.drama", {originality:MinKey}, {originality:MaxKey}, "dramas")
mongos>
该规则使用表示负无穷大的 MinKey 和表示正无穷大的 MaxKey。因此,上面的规则意味着用标签“戏剧”来标记集合movies.drama
的所有块
与此类似,您将为其他两个集合制定规则。
规则 2:在movies.action
集合中创建的所有块都将被标记为“动作”
mongos> sh.addTagRange("movies.action", {distribution:MinKey}, {distribution:MaxKey}, "actions")
mongos>
规则 3:在movies.comedy
集合中创建的所有块都将被标记为“喜剧”
mongos> sh.addTagRange("movies.comedy", {collection:MinKey}, {collection:MaxKey}, "comedies")
mongos>
您需要等待集群重新平衡,以便根据上面定义的标记和规则来分配块。如前所述,块分配是一个自动过程,所以过一段时间后,块将自动重新分配,以实现您所做的更改。
接下来,发出chunks.find
来检查组块组织:
mongos> use config
switched to db config
mongos> db.chunks.find({ns:"movies.drama"}, {shard:1, _id:0}).sort({shard:1})
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
mongos> db.chunks.find({ns:"movies.action"}, {shard:1, _id:0}).sort({shard:1})
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
mongos> db.chunks.find({ns:"movies.comedy"}, {shard:1, _id:0}).sort({shard:1})
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
mongos>
因此,集合块已经基于定义的标签和规则被重新分配(图 7-23 )。
图 7-23。
Distribution with tagging
带标签的 7.5.6.3 缩放
接下来,您将了解如何通过标记进行扩展。我们换个场景吧。让我们假设集合movies.action
需要两台服务器来存储它的数据。因为只有三个碎片,这意味着另外两个集合的数据需要移动到一个碎片中。
在这种情况下,您将更改碎片的标记。您将向 Shard0 添加标签“comedies ”,从 Shard2 中删除标签,并进一步向 Shard2 添加标签“actions”。
这意味着标记为“喜剧”的块将被移动到 Shard0,而标记为“动作”的块将被传播到 Shard2。
首先将集合movies.comedy
块移动到 Shard0,并从 Shard2:
mongos> sh.addShardTag("shard0000","comedies")
mongos> sh.removeShardTag("shard0002","comedies")
接下来,将标签“actions”添加到 Shard2,这样movies.action
块也分布在 Shard2 中:
mongos> sh.addShardTag("shard0002","actions")
一段时间后重新发出 find 命令将显示以下结果:
mongos> db.chunks.find({ns:"movies.drama"}, {shard:1, _id:0}).sort({shard:1})
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
mongos> db.chunks.find({ns:"movies.action"}, {shard:1, _id:0}).sort({shard:1})
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0001" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
{ "shard" : "shard0002" }
mongos> db.chunks.find({ns:"movies.comedy"}, {shard:1, _id:0}).sort({shard:1})
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
{ "shard" : "shard0000" }
mongos>
块已经被重新分配,反映了所做的更改(图 7-24 )。
图 7-24。
Tagging with scaling
7.5.6.4 多重标签
您可以将多个标签与碎片相关联。让我们给碎片添加两个不同的标签。
假设您想要基于磁盘分布写入。你有一个碎片有一个旋转磁盘,另一个有一个 SSD(固态硬盘)。您希望将 50%的写入重定向到使用 SSD 的碎片,将剩余的写入重定向到使用旋转磁盘的碎片。
首先,根据这些属性标记碎片:
mongos> sh.addShardTag("shard0001", "spinning")
mongos> sh.addShardTag("shard0002", "ssd")
mongos>
让我们进一步假设您有一个movies.action
集合的distribution
字段,您将使用它作为分片键。distribution
字段值介于 0 和 1 之间。接下来你要说“如果分发<5,把这个发给旋转盘。如果分配> = .5,发送到 SSD 因此,您将规则定义如下:
mongos>sh.addTagRange("movies.action", {distribution:MinKey} ,{distribution:.5} ,"spinning")
mongos>sh.addTagRange("movies.action" ,{distribution:.5} ,{distribution:MaxKey},"ssd")
mongos>
现在,发行版小于. 5 的文档将被写入旋转碎片,其他文档将被写入 SSD 磁盘碎片。
通过标记,您可以控制每个新添加的服务器将获得的负载类型。
7.5.7 在共享环境中导入数据时要记住的几点
以下是导入数据时需要记住的几点。
数据的 7.5.7.1 预分割
您可以使用下面的命令告诉 MongoDB 如何创建块,而不是让 MongoDB 选择创建块:
db.runCommand( { split : "practicalmongodb.mycollection" , middle : { shardkey : value } } );
张贴这个你也可以让 MongoDB 知道哪个块去哪个节点。
为此,您需要了解将要导入数据库的数据。这也取决于您要解决的用例以及您的应用如何读取数据。在决定将块放在哪里时,要记住数据局部性之类的东西。
7.5.7.2 决定块的大小
在决定块大小时,您需要记住以下几点:
If the size is too small, the data will be distributed evenly but it will end up having more frequent migrations, which will be an expensive operation at the mongos layer. If the size is large, it will lead to less migration, reducing the expense at the mongos layer, but you will end up with uneven data distribution.
7.5.7.3 选择了一把好的碎片钥匙
选择一个好的碎片键对于在碎片集群的节点之间良好地分配数据是非常重要的。
对碎片的监控
除了对其他 MongoDB 实例进行正常的监控和分析之外,分片集群还需要额外的监控,以确保其所有操作正常运行,并且数据在节点之间有效分布。在本节中,您将看到为了分片集群的正常运行应该进行哪些监控。
监控配置服务器
如您所知,配置服务器存储了分片集群的元数据。mongos 缓存数据并将请求路由到各自的碎片。如果配置服务器宕机,但有一个正在运行的 mongos 实例,对 shard 集群没有直接影响,它将在一段时间内保持可用。然而,您将无法执行诸如块迁移或重启新 mongos 之类的操作。从长远来看,配置服务器的不可用性会严重影响集群的可用性。为了确保集群保持平衡和可用,您应该监控配置服务器。
7.5.9.1 监控分片状态平衡和块分配
对于最有效的分片集群部署来说,需要将块均匀地分布在各个分片中。如您所知,这是由 MongoDB 使用后台进程自动完成的。您需要监控碎片状态,以确保流程有效运行。为此,您可以在 mongos mongo shell 中使用db.printShardingStatus()
或sh.status()
命令来确保该进程有效工作。
7.5.9.2 监控锁的状态
几乎在所有情况下,平衡器都会在完成其进程后自动释放其锁,但是您需要检查数据库的锁状态,以确保没有持久锁,因为这可能会阻碍未来的平衡,从而影响集群的可用性。从 mongos mongo 发出以下命令来检查锁的状态:
use config
db.locks.find()
7.6 生产集群架构
在本节中,您将了解生产集群体系结构。为了理解它,让我们考虑一个非常普通的社交网络应用的用例,其中用户可以创建一个朋友圈,并可以在整个组中共享他们的评论或图片。用户还可以评论或喜欢她朋友的评论或图片。用户在地理上是分散的。
应用要求所有评论跨地理位置立即可用;数据要冗余,这样用户的评论、帖子、图片才不会丢失;而且应该是高度可用的。因此,应用的生产集群应该包含以下组件:
At least two mongos instance, but you can have more as per need. Three config servers, each on a separate system. Two or more replica sets serving as shards. The replica sets are distributed across geographies with read concern set to nearest. See Figure 7-25.
图 7-25。
Production cluster architecture
现在让我们看看 MongoDB 生产部署中可能出现的故障场景及其对环境的影响。
场景 1
Mongos 变得不可用:mongos 发生故障的应用服务器将无法与集群通信,但这不会导致任何数据丢失,因为 mongos 不维护自己的任何数据。mongos 可以重启,重启时,它可以与配置服务器同步以缓存集群元数据,应用可以正常启动其操作(图 7-26 )。
图 7-26。
Mongos become unavailable
场景 2
副本集的一个 mongod 在碎片中变得不可用:因为您使用副本集来提供高可用性,所以没有数据丢失。如果主节点关闭,则选择一个新的主节点,而如果它是一个次节点,则断开连接并继续正常运行(图 7-27 )。
图 7-27。
One of the mongod of replica set is unavailable
唯一的区别是减少了数据的重复,使系统变得不那么脆弱,所以您应该并行检查 mongod 是否是可恢复的。如果是,应该恢复并重新启动,而如果是不可恢复的,您需要创建一个新的副本集并尽快替换它。
场景 3
如果其中一个碎片变得不可用:在这种情况下,碎片上的数据将不可用,但其他碎片将可用,因此不会停止应用。应用可以继续其读/写操作;但是,部分结果必须在应用中处理。同时,碎片应该尝试尽快恢复(图 7-28 )。
图 7-28。
Shard unavailable
场景 4
三个配置服务器中只有一个是可用的:在这种情况下,尽管集群将变成只读的,但它不会为任何可能导致集群结构变化的操作提供服务,从而导致元数据的变化,如块迁移或块分割。应尽快更换配置服务器,因为如果所有配置服务器都不可用,将导致集群无法运行(图 7-29 )。
图 7-29。
Only one config server available
7.7 摘要
在本章中,您讲述了核心流程和工具、独立部署、分片概念、复制概念和生产部署。您还了解了如何实现 HA。
在下一章中,您将看到数据是如何存储的,如何使用日志来写数据,GridFS 的用途,以及 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": "16
th
让我们进一步假设这个集合有以下两个索引:
{ "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 的局限性和不适合它的用例。
十一、MongoDB 限制
当开始使用一个新的数据库时,你也应该意识到它的局限性,以便更好地使用数据库
在这一章中,我们将列出 MongoDB 的局限性和不适合它的用例。
11.1 MongoDB 空间太大(适用于 MMAPv1)
先说磁盘空间的问题。MongoDB(带存储引擎 MMAPv1)空间太大;换句话说,数据目录文件比数据库的实际数据要大。
这是因为预先分配了数据文件。这是为了防止文件系统碎片而设计的。
数据目录中的文件命名为
- This option can be disabled by using the
-- noprealloc
option. However, it is not recommended to use it in the production environment. It should only be used to test and frequently call the small data set of drop database. - OP: If mongod is a member of the replica set, there will be a file named
oplog.rs
in the data directory. The file exists in the local database and is a pre-allocated set with an upper limit. In a 64-bit installation, the allocation of this file defaults to about 5% of disk space. - Journal: Log files are also included in the data directory, which stores the write operations on the disk before MongoDB applies the log files to the database.
- MongoDB pre-allocates 3GB of data for the log, which exceeds the actual database size, so it is not suitable for small-scale installation. The available solution to this problem is to use
–smallflags
in the command line flag or/etc/mongod.conf
file until you run in an environment with the required disk space. But this feature makes it unsuitable for small installation. - Empty record: when a document or collection is deleted, the space will never be returned to the operating system; On the contrary, MongoDB maintains a list of these empty records, which can be reused.
要回收这些被删除的空间,可以使用compact
或repairDatabase
选项,但是要注意这两个选项都需要额外的磁盘空间来运行。
Note
WiredTiger 存储引擎不存在这种限制。相反,由于数据文件的压缩,存储大小减少了 50%。此外,一旦集合被删除,磁盘空间将被自动回收,这与上面提到的 MMAPv1 存储引擎不同。
11.2 内存问题(适用于存储引擎 MMAPv1)
在 MongoDB 中,通过内存映射整个数据集来管理内存。它允许操作系统控制内存映射并分配最大数量的 RAM。结果是性能不是最佳的,并且不能有效地推断内存使用情况。
Indexes are memory-heavy; in other words, indexes take up lot of RAM. Since these are B-tree indexes, defining many indexes can lead to faster consumption of system resources. A consequence of this is that memory is allocated automatically when required. In a shared environment, it’s trickier to run the database. In general, as with all database servers, it’s best to run MongoDB on a dedicated server.
11.3 32 位与 64 位
MongoDB 有两个版本,32 位和 64 位。
由于 MongoDB 使用内存映射文件,所以 32 位版本仅限于存储大约 2GB 的数据。如果需要存储更多数据,应该使用 64 位版本。
从 3.0 版本开始,MongoDB 不再提供对 32 位版本的商业支持。此外,32 位版本的 MongoDB 不支持 WiredTiger 存储引擎。
11.4 BSON 文件
本节介绍了 BSON 文档的局限性。
- 大小限制:与其他数据库一样,文档中可以存储的内容也有限制。当前版本支持最大 16MB 的文档。此最大大小确保文档在传输时不会使用过多的 RAM 或带宽。
- 嵌套深度限制:在 MongoDB 中,BSON 文档不支持超过 100 层的嵌套。
- 字段名称:如果您存储 1000 个带有关键字“col1”的文档,那么该关键字在数据集中会存储很多次。尽管 MongoDB 支持任意文档,但实际上大多数字段名都是相同的。保持短字段名被认为是优化空间使用的一个好习惯。
11.5 名称空间限制
从名称空间的角度来看,请注意以下限制。
- 名称空间的长度:包括集合和数据库名称在内的每个名称空间的长度必须小于 123 个字节。
- 命名空间文件大小(适用于 MMAPv1 存储引擎):命名空间文件大小不能大于 2047MB。默认大小为 16MB 但是,这可以使用
nssize
选项进行配置。名称空间数量(适用于 MMAPv1 存储引擎):名称空间数量=(名称空间文件大小/628)。16MB 的名称空间文件将支持大约 24,000 个名称空间。
Note
WiredTiger 存储引擎不存在这样的限制。
11.6 索引限制
本节涵盖了 MongoDB 中索引的局限性。
- 索引大小:索引项不能大于 1024 字节。
- 每个集合的索引数量:每个集合最多允许 64 个索引。
- 索引名称长度:默认情况下,索引名称由字段名称和索引方向组成。包括命名空间(即数据库和集合名称)的索引名称不能超过 128 个字节。如果默认索引名变得太长,您可以显式地为
ensureIndex()
助手指定一个索引名。 - 分片集合中的唯一索引:只有当完整的分片键作为唯一索引的前缀时,才支持跨分片;否则,跨分片不支持唯一索引。在这种情况下,唯一性是跨整个键而不是单个字段强制实施的。
- 复合索引中的索引字段数:不能超过 31 个字段。
11.7 封顶集合限制-封顶集合中的最大文档数
如果 max 参数用于指定封顶集合中的最大文档数,则不能超过 232 个文档。但是,如果没有使用这样的参数,文档的数量就没有限制。
11.8 分片限制
分片是跨分片分割数据的机制。下面几节讨论了在处理分片时需要注意的限制。
11.8.1 分片早期避免任何问题
使用碎片键,数据被分割成块,然后自动分布在碎片中。但是,如果分片实现得太晚,会导致服务器变慢,因为块的分割和迁移需要时间和资源。
一个简单的解决方案是在达到估计容量的 80%之前,使用 MongoDB 云管理器(刷新时间、锁百分比、队列长度和故障是很好的度量)和 shard 等工具来监控您的 MongoDB 实例容量。
11.8.2 碎片密钥无法更新
一旦文档被插入到集合中,shard 键就不能被更新,因为 MongoDB 使用 shard 键来确定文档应该被路由到哪个 shard。如果您想要更改文档的碎片密钥,建议的解决方案是删除文档,并在更改完成后重新插入文档。
11.8.3 碎片收集限制
该集合应在达到 256GB 之前进行分片。
11.8.4 选择正确的分片密钥
选择一个正确的分片密钥是非常重要的,因为一旦选择了密钥,就不容易修改了。
Note
什么被认为是错误的碎片键完全取决于应用。假设应用是一个新闻提要;选择时间戳字段作为分片键将是一个错误的分片键,因为这将导致只从一个分片插入、查询和迁移数据,而不是从整个集群。如果需要更正 shard 键,通常使用的过程是转储和恢复集合。
11.9 安全限制
对于数据库来说,安全性是一个重要的问题。让我们从安全性的角度来看一下 MongoDB 的局限性。
11.9.1 默认情况下不进行身份验证
虽然默认情况下不启用身份验证,但它完全受支持,并且可以轻松启用。
11.9.2 进出 MongoDB 的流量不加密
默认情况下,进出 MongoDB 的连接是不加密的。在公共网络上运行时,考虑加密通信;否则它会对您的数据造成威胁。公共网络上的通信可以使用 MongoDB 的 SSL 支持版本进行加密,MongoDB 只有 64 位版本。
11.10 读写限制
以下部分涵盖了重要的限制。
区分大小写的查询
默认情况下,MongoDB 区分大小写。
例如,以下两个命令将返回不同的结果:db.books.find({name: 'PracticalMongoDB'})
和db.books.find({name: 'practicalmongodb'})
。您应该确保知道数据是以哪种情况存储的。虽然可以使用像db.books.find({name: /practicalmongodb/i})
这样的正则表达式搜索,但它们并不理想,因为它们相对较慢。
类型敏感字段
因为在 MongoDB 中没有针对文档的强制模式,所以它不知道您在犯错误。您必须确保数据使用了正确的类型。
没有加入
MongoDB 不支持连接。如果需要从多个集合中检索数据,则必须执行多个查询。但是,您可以重新设计模式,将相关数据放在一起,以便可以在一个查询中检索信息。
事务
MongoDB 只支持单文档原子性。因为写操作可以修改多个文档,所以这个操作不是原子的。但是,您可以使用隔离运算符隔离影响多个文档的写操作。
11.10.4.1 副本集限制-副本集成员的数量
副本集用于确保 MongoDB 中的数据冗余。一个成员充当主要成员,其余成员充当次要成员。由于 MongoDB 的投票方式,您必须使用奇数个成员。
这是因为一个节点需要多数票才能成为主要节点。如果您使用偶数个节点,您将在没有选择主要成员的情况下结束,因为没有一个成员拥有多数票。在这种情况下,副本集将变为只读。
你可以用仲裁者来打破这种联系。它们可以帮助支持故障转移并节省成本。要了解有关副本集功能的更多信息,请参考第 7 章。
11.11 MongoDB 不适用范围
MongoDB 不适合以下情况:
- 高度事务性的系统,如会计或银行系统。传统的 RDBMS 仍然更适合这种需要大量原子复杂物质的应用。
- 传统的商业智能应用,其中特定问题的 BI 数据库将生成高度优化的查询。对于这样的应用,数据仓库可能是更合适的选择。
- 需要复杂 SQL 查询的应用。
- MongoDB 不支持事务性操作,所以银行系统肯定不能使用它。
11.12 摘要
在这一章中,您了解了 MongoDB 的局限性和不适合它的用例。
在下一章中,我们将讨论如何使用 MongoDB。
十二、MongoDB 最佳实践
“开始使用 MongoDB 很容易,但是一旦您开始开发应用,您将会遇到需要最佳实践来实现特定用例的场景。”
在前面的章节中,您已经熟悉了 MongoDB。本章的目的是利用其他用户的经验概述一些众所周知的问题,同时也提供各种帮助您顺利使用 MongoDB 的方法。
如您所知,MongoDB 处理文档,使用 RAM 存储数据以增强性能,并使用复制和分片来进一步提供数据安全性和可伸缩性。
本章将介绍您应该了解的技巧,从部署策略到增强查询、数据安全性和一致性,再到监控。
12.1 部署
在决定部署策略时,请记住以下提示,以便正确确定硬件规模。这些提示还将帮助您决定是否使用分片和复制。
-
数据集大小:最重要的是确定当前和预期的数据集大小。这不仅让您可以为单个物理节点选择资源,还可以帮助您规划分片计划(如果有的话)。
-
数据重要性:第二重要的事情是确定数据的重要性,确定数据有多重要,以及您对任何数据丢失或数据滞后的容忍程度(尤其是在复制的情况下)。
-
内存大小:下一步是确定内存需求,并相应地管理 RAM。与其他面向数据的应用一样,当整个数据集可以驻留在内存中时,MongoDB 也可以工作得最好,从而避免任何类型的磁盘 I/O。页面错误是一个可以使用 MongoDB Cloud Manager 之类的监控工具来测量的指标。如果可能的话,您应该始终选择内存大于您的工作集大小的平台。如果大小超过了单个节点的内存,您应该考虑使用分片,以便可以增加可用的内存量。这最大化了整体部署的性能。
-
磁盘类型:如果速度不是主要考虑因素,或者如果数据集大于任何内存策略所能支持的范围,那么选择合适的磁盘类型就非常重要。IOPS(每秒输入/输出操作数)是选择磁盘类型的关键;IOPS 越高,MongoDB 的性能越好。如果可能的话,应该使用本地磁盘,因为网络存储会导致性能下降和高延迟。还建议在创建磁盘阵列时使用 RAID 10(只要可能)。
-
CPU:如果您预期使用 map reducing,那么时钟速度和可用的处理器就成为重要的考虑因素。当您在内存中运行大部分数据的 mongod 时,时钟速度也会对整体性能产生重大影响。在您希望最大化每秒操作数的情况下,您必须考虑在部署策略中包含一个具有高时钟/总线速度的 CPU。
-
Replication is used if high availability is one of the requirements. In any MongoDB deployment it should be standard to set up a replica set with at least three nodes.A 2x1 deployment is the most common configuration for replication with three nodes, where there are two nodes in one data center and a backup node in a secondary data center, as depicted in Figure 12-1.
图 12-1。
MongoDB 2*1 deployment
12 . 1 . 1 MongoDB 站点的硬件建议
以下内容仅旨在为 MongoDB 部署提供高级指导。实际的硬件配置取决于您的数据、可用性要求、查询、性能标准和所选硬件组件的功能。
- 内存:由于 MongoDB 大量使用内存来获得更好的性能,内存越多,性能越好。
- 存储:MongoDB 可以使用 SSD(固态硬盘)或本地附加存储。由于 MongoDB 的磁盘访问模式没有顺序属性,使用 SSD 可以让客户体验到显著的性能提升。使用 SSD 的另一个好处是,如果工作集不再适合内存,它们会稍微降低性能。大多数 MongoDB 部署应该使用 RAID-10。使用 WiredTiger 存储引擎时,由于性能问题,强烈建议使用 XFS 文件系统。此外,不要使用大页面,因为 MongoDB 使用默认的虚拟内存页面会表现得更好。
- CPU:由于带有 MMAPv1 存储引擎的 MongoDB 很少遇到需要大量内核的工作负载,因此最好使用时钟速度较快的服务器,而不是时钟速度较慢的多核服务器。然而,WiredTiger 存储引擎是受 CPU 限制的,因此使用多核服务器可以显著提高性能。
12.1.2 需要注意的几点
总结本节,在为 MongoDB 选择硬件时,请考虑以下要点:
A faster CPU clock speed and more RAM are important for productivity. Since MongoDB doesn’t perform high amounts of computation, increasing the number of cores helps but does not provide a high level of marginal return when using the MMAPv1 storage engine. Using SATA SSD and PCI (Peripheral Component Interconnect) provides good price/performance and good results. It’s more effective to spend on commodity SATA spinning drives. MongoDB on NUMA Hardware : This point is only applicable for mongod running in Linux and not for instances that run on Windows or other Unix-like systems. NUMA (non-uniform memory access) and MongoDB don’t work well together, so when running MongoDB on NUMA hardware, you need to disable NUMA for MongoDB and run with an interleave memory policy because NUMA causes a number of operational problems for MongoDB, including performance slowness for periods of time or high processor usage.
12.2 编码
获得硬件后,在使用数据库编码时,请考虑以下提示:
- 第一点是考虑用于给定应用需求的数据模型,并决定是嵌入还是引用或者两者的混合。关于这方面的更多信息,请查看第 tk 章。在快速性能和保证即时一致性之间有一个平衡,所以根据您的应用来决定。
- 避免导致文档大小无限制增长的应用模式。在 MongoDB 中,BSON 文档的最大大小是 16MB。应该避免让文档无限制增长的应用模式。例如,应用不应该以导致文档显著增长的方式更新文档。当文档大小超过分配的大小时,MongoDB 将重新定位文档。这个过程不仅耗时,而且是资源密集型的,并且会不必要地降低其他数据库操作的速度。此外,它还会导致存储使用效率低下。请注意,上述限制适用于 MMAPv1 存储引擎。使用 WiredTiger 时,每次更新都会重写文档。
例如,让我们考虑一个博客应用。在这个应用中,很难估计一篇博文会收到多少回复。应用被设置为只向用户显示评论的子集,比如最近的评论或前 10 条评论。在这种情况下,您应该创建一个引用模型,将每个响应或一组响应作为单独的文档进行维护,然后在文档中添加对博客文章的引用,而不是创建一个将博客文章和用户响应作为单个文档进行维护的嵌入式模型。通过这种方式,可以控制文档的无限制增长,如果遵循嵌入数据的第一种模型,就会发生这种情况。
- 你也可以为未来设计文档。尽管 MongoDB 提供了在需要时在文档中追加新字段的选项,但它有一个缺点。当引入新的字段时,可能会出现文档不适合当前可用空间的情况,导致 MongoDB 为文档寻找新的空间并将其移动到那里,这可能需要时间。因此,如果您知道结构,那么在开始时创建所有的字段总是很有效的,不管当时您是否有可用的数据。如上所述,空间将被分配给文档,只要有值,就只需要更新。这样做,MongoDB 将不必寻找空间;它只是更新输入的值,这要快得多。
- 您还可以根据需要创建预期大小的文档。这一点也是为了确保给文档分配足够的空间,任何进一步的增长都不会导致空间的跳跃。这可以通过使用一个垃圾字段来实现,该字段包含一个预期大小的字符串,最初插入文档,然后立即取消设置该字段:
> mydbcol.insert({"_id" : ObjectID(..),......, "tempField" : stringOfAnticipatedSize})
> mydbcol.update({"_id" : ...}, {"$unset" : {"tempField" : 1}})
- 当您知道并且总是知道您正在访问的字段的名称时,应该总是在这种情况下使用子文档。否则,使用数组。
- 如果您想要查询必须计算但在文档中没有显式显示的信息,最好的选择是在文档中显式显示这些信息。由于 MongoDB 被设计为只存储和检索数据,所以它不做任何计算。任何琐碎的计算都会推给客户端,从而导致性能问题。
- 此外,尽可能避免
$Where
,因为这是一个极其耗费时间和资源的操作。 - 设计文档时使用正确的数据类型。例如,数字只能存储为数字数据类型,而不能存储为字符串数据类型。使用字符串存储数据需要更多的空间,并且会影响可以对数据执行的操作。
- 另外需要注意的是,MongoDB 中的字符串是区分大小写的。因此,搜索“practicalMongoDB”不会找到“Practicalmongodb”。因此,在进行字符串搜索时,您可以执行以下操作之一:
- 以规范化的大小写格式存储数据。
- 搜索时使用带
/I
的正则表达式。 - 在聚合框架中使用
$toUpper
或$
toLower
。
- 使用您自己的唯一键作为
_id
将节省一点空间,如果您计划在键上建立索引,这将非常有用。然而,在决定使用自己的密钥作为_id
时,您需要记住以下几点:- 您必须确保密钥的唯一性。
- 此外,还要考虑键的插入顺序,因为插入顺序将决定使用多少 RAM 来维护这个索引。
- 根据需要检索字段。当每秒完成数百或数千个请求时,只获取需要的字段当然是有利的。
- GridFS 只用于存储大于单个文档所能容纳的数据,或者太大而无法在客户端一次加载的数据,比如视频。任何将被传输到客户机的东西都是 GridFS 的理想选择。
- 使用 TTL 删除文档。如果集合中的文档需要在预定义的时间段后删除,TTL 功能可用于在文档达到预定义的期限后自动删除文档。
假设您有一个维护包含用户和系统交互细节的文档的集合。这些文档有一个名为 lastActivity 的日期字段,用于跟踪用户和系统的交互。假设您有一个需求,要求您只需要维护用户会话一个小时。在这种情况下,您可以将字段lastActivity
的 TTL 设置为 3600 秒。后台线程将自动运行,并检查和删除空闲时间超过 3600 秒的文档。
- 如果您需要基于插入顺序的高吞吐量,请使用上限集合。在某些情况下,根据数据大小,您需要在系统中维护一个滚动的数据窗口。例如,有上限的集合可用于存储大容量系统的日志信息,以便快速检索最新的日志条目。
- 注意,如果不小心的话,MongoDB 的灵活模式可能会导致数据不一致。例如,如果没有正确更新,复制数据(嵌入文档)的能力会导致数据不一致,等等。所以检查数据的一致性非常重要。
- 尽管 MongoDB 可以处理无缝故障转移,但是根据良好的编码实践,应用应该编写得很好,能够处理任何异常并优雅地处理这种情况。
12.3 应用响应时间优化
一旦开始开发应用,最重要的需求之一就是要有一个可接受的响应时间。换句话说,应用应该立即响应。您可以使用以下提示进行优化:
- 尽可能避免磁盘访问和页面错误。主动计算应用需要处理的数据集大小,并添加更多内存以避免页面错误和磁盘读取。此外,对应用进行编程,使其主要访问内存中可用数据,这样就不会经常出现页面错误。
- 对查询的字段创建索引。如果在执行的过滤器上创建索引,那么索引在内存中的存储方式会减少内存消耗,从而对查询产生积极的影响。
- 如果与完整的文档结构相比,应用涉及返回几个字段的查询,则创建覆盖索引。
- 拥有一个可以被最大查询使用的复合索引还可以节省内存,因为不用在内存中加载多个索引,一个索引就足够了。
- 在正则表达式中使用尾随通配符可以从关联索引中获益。
- 尝试创建索引,从根本上减少可供选择的文档。字段“性别”的索引不如字段“电话号码”的索引有用
- 索引并不总是好的。您需要保持所用索引的最佳平衡。尽管应该创建索引来支持查询,但是也应该记住删除不再使用的索引,因为每个索引都有与插入/更新操作相关的成本。如果一个索引没有被使用但仍然存在,它会对整个数据库容量产生负面影响。这对于大量插入的工作负载尤其重要。
- 文档应该以一种层次化的方式设计,相关的东西被组合在一起,并在适当的时候被描述为层次结构。这将使 MongoDB 能够在不扫描整个文档的情况下找到所需的信息。
- 当应用 AND 操作符时,应该总是从小的结果集查询到大的结果集,因为这将导致查询少量的文档。如果您知道最具限制性的条件,则该条件应该首先出现。
- 使用或查询时,应该从较大的结果集移到较小的结果集,因为这将限制后续查询的搜索空间。
- 工作集应该适合内存。
- 由于 WiredTiger 存储引擎支持块级和索引级的压缩,因此可将其用于写入密集型和 I/O 密集型应用。
12.4 数据安全
您了解了在决定部署时需要牢记的事项;您还学习了一些重要的技巧来获得良好的性能。现在,让我们来看一些关于数据安全性和一致性的提示:
- 复制和日志记录是为数据安全提供的两种方法。通常,建议使用复制来运行生产设置,而不是使用单个服务器来运行。并且应该至少有一个服务器被记录日志。当不可能启用复制并且您在单台服务器上运行时,日志记录可以提供数据安全。tk 章解释了当启用日志记录时写操作是如何工作的。
- 在服务器崩溃的情况下,修复应该是恢复数据的最后手段。尽管运行修复后数据库可能不会损坏,但它不会包含所有数据。
- 在复制环境中,将
W
设置为大多数安全写入。这是为了确保写入被复制到副本集的大多数成员。虽然这将降低写入操作的速度,但写入是安全的。 - 发出命令时,请始终指定
wtimeout
和w
,以避免无限的等待时间。 - MongoDB 应该始终在一个可信的环境中运行,并制定规则来防止来自所有未知系统、机器或网络的访问。
12.5 管理
以下是一些管理提示:
- 对耐用服务器进行即时备份。要在启用日志记录的情况下备份数据库,可以拍摄文件系统快照或执行普通的 fsync+锁,然后转储。注意,你不能在没有 fsync 和锁定的情况下复制所有的文件,因为复制不是一个瞬间的操作。
- 应该使用 Repair 来压缩数据库,因为它基本上先执行 mongodump,然后执行 mongorestore,创建数据的干净副本,并在此过程中删除数据文件中的任何空“洞”。
- 数据库概要分析器由 MongoDB 提供。它记录所有数据库操作的细粒度信息。可以启用它来记录所有事件的信息,或者只记录持续时间超过可配置阈值(默认为 100 毫秒)的事件。
Note
探查器数据存储在有上限的集合中。与解析日志文件相比,查询这个集合可能更容易。
- 解释计划可用于查看查询是如何解决的。这包括使用哪个索引、返回多少文档、索引是否覆盖查询、扫描了多少索引条目以及查询返回结果所用的时间(毫秒)等信息。当查询在不到 1 毫秒的时间内得到解决时,解释计划显示为 0。当您调用解释计划时,它会丢弃旧的计划,并启动测试可用索引的过程,以确保使用可能的最佳计划。
12.6 复制延迟
复制滞后是监控副本集背后的主要管理问题。给定辅助节点的复制延迟是在主节点写入操作和在辅助节点复制操作的时间差。通常,复制滞后会自我修复,并且是短暂的。然而,如果它仍然很高,并继续上升,可能是系统有问题。您可能会关闭系统直到问题解决,或者可能需要手动干预来协调不匹配,或者您甚至可能会使用过时的数据运行系统。
以下命令可用于确定副本集的当前复制延迟:
testset:PRIMARY>
rs.printSlaveReplicationInfo()
此外,您可以使用rs.printReplicationInfo()
命令来填充缺失的部分:
testset:PRIMARY>
rs.printReplicationInfo()
MongoDB 云管理器还可以用来查看最近和历史的复制延迟信息。可以从每个辅助节点的“状态”选项卡中获得复制延迟图。
以下是一些有助于减少这一时间的提示:
- 在写入负载较重的情况下,您应该有一个与主节点一样强大的辅助节点,以便它可以跟上主节点,并且可以以相同的速率在辅助节点上应用写入。此外,您应该有足够的网络带宽,以便能够以创建 ops 的相同速率从主服务器检索 ops。
- 调整应用写入问题。
- 如果辅助节点用于索引构建,则可以计划在主节点上有少量写入活动时进行。
- 如果辅助节点用于进行备份,请考虑在不阻止的情况下进行备份。
- 检查复制错误。运行
rs.status()
并检查errmsg
字段。此外,可以检查辅助节点的日志文件中是否有任何现有的错误消息。
12.7 分片
当数据不再适合一个节点时,可以使用分片来确保数据均匀分布在群集中,并且操作不会因资源限制而受到影响。
- 选择一个好的分片密钥。
- 您必须在生产部署中使用三台配置服务器来提供冗余。
- 碎片收集在达到 256GB 之前。
12.8 监控
应该主动监控 MongoDB 系统以检测异常行为,以便采取必要的措施来解决问题。有几个工具可用于监控 MongoDB 部署。
MongoDB 开发者提供了一个名为 MongoDB Cloud Manager 的免费托管监控服务。MongoDB 云管理器提供了整个集群指标的仪表板视图。或者,您可以使用 nagios、SNMP 或 munin 来构建自己的工具。
MongoDB 还提供了几个工具,如 mongostat 和 mongotop,以深入了解性能。使用监控服务时,应密切关注以下事项:
- Op 计数器:包括插入、删除、读取、更新和游标使用。
- 常驻内存:应该始终关注分配的内存。这个计数器值应该总是低于物理内存。如果内存不足,由于页面错误和索引缺失,您将会遇到性能下降的问题。
- 工作集大小:为了获得良好的性能,活动的工作集应该适合内存,因此需要密切关注工作集。您可以优化查询,使工作集适合内存,或者在工作集预计增加时增加内存。
- 队列:在 MongoDB 3.0 发布之前,读写锁用于同步读取,独占访问用于写入。在这种情况下,您可能会在单个编写器后面结束队列,其中可能包含读/写查询。需要监控队列指标和锁定百分比。如果队列和锁百分比呈上升趋势,这意味着数据库中存在争用。将操作更改为批处理模式或更改数据模型会对并发性产生显著的积极影响。从 3.0 版开始,引入了集合级锁定(在 MMAPv1 存储引擎中)和文档级锁定(在 WiredTiger 存储引擎中)。这导致并发性的提高,其中在数据库级别将不需要具有独占访问的写锁。因此,从这个版本开始,您只需要测量队列指标。
- 每当应用出现问题时,CRUD 行为、索引模式和索引可以帮助您更好地理解应用的流程。
- 建议对完整大小的数据库(如生产数据库副本)运行整个性能测试,因为在处理实际数据时,性能特征通常会得到强调。这也让您能够避免在处理实际性能数据库时可能出现的令人不快的意外。
12.9 摘要
在本章中,我们提供了各种方法来帮助您使用 MongoDB。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)