MongoDB 基础

MongoDB 是什么?

MongoDB 是一个基于 分布式文件存储 的开源 NoSQL 数据库系统,由 C++ 编写的。MongoDB 提供了 面向文档 的存储方式,操作起来比较简单和容易,支持“无模式”的数据建模,可以存储比较复杂的数据类型,是一款非常流行的 文档类型数据库

在高负载的情况下,MongoDB 天然支持水平扩展和高可用,可以很方便地添加更多的节点/实例,以保证服务性能和可用性。在许多场景下,MongoDB 可以用于代替传统的关系型数据库或键/值存储方式,皆在为 Web 应用提供可扩展的高可用高性能数据存储解决方案。

适用于OLTP

MongoDB 的存储结构是什么?

MongoDB 的存储结构区别于传统的关系型数据库,主要由如下三个单元组成:

  • 文档(Document) :MongoDB 中最基本的单元,由 BSON 键值对(key-value)组成,类似于关系型数据库中的行(Row)。
  • 集合(Collection) :一个集合可以包含多个文档,类似于关系型数据库中的表(Table)。
  • 数据库(Database) :一个数据库中可以包含多个集合,可以在 MongoDB 中创建多个数据库,类似于关系型数据库中的数据库(Database)。

也就是说,MongoDB 将数据记录存储为文档 (更具体来说是BSON 文档),这些文档在集合中聚集在一起,数据库中存储一个或多个文档集合。

SQL 与 MongoDB 常见术语对比 :

SQL MongoDB
表(Table) 集合(Collection)
行(Row) 文档(Document)
列(Col) 字段(Field)
主键(Primary Key) 对象 ID(Objectid)
索引(Index) 索引(Index)
嵌套表(Embeded Table) 嵌入式文档(Embeded Document)
数组(Array) 数组(Array)

文档

MongoDB 中的记录就是一个 BSON 文档,它是由键值对组成的数据结构,类似于 JSON 对象,是 MongoDB 中的基本数据单元。字段的值可能包括其他文档、数组和文档数组。

image-20240602163330369

文档的键是字符串。除了少数例外情况,键可以使用任意 UTF-8 字符。

  • 键不能含有 \0(空字符)。这个字符用来表示键的结尾。
  • .$ 有特别的意义,只有在特定环境下才能使用。
  • 以下划线_开头的键是保留的(不是严格要求的)。

BSON [bee·sahn] 是 Binary JSON的简称,是 JSON 文档的二进制表示,支持将文档和数组嵌入到其他文档和数组中,还包含允许表示不属于 JSON 规范的数据类型的扩展。有关 BSON 规范的内容,可以参考 bsonspec.org,另见BSON 类型。

根据维基百科对 BJSON 的介绍,BJSON 的遍历速度优于 JSON,这也是 MongoDB 选择 BSON 的主要原因,但 BJSON 需要更多的存储空间。

与 JSON 相比,BSON 着眼于提高存储和扫描效率。BSON 文档中的大型元素以长度字段为前缀以便于扫描。在某些情况下,由于长度前缀和显式数组索引的存在,BSON 使用的空间会多于 JSON。

image-20240602163349917

集合

MongoDB 集合存在于数据库中,没有固定的结构,也就是 无模式 的,这意味着可以往集合插入不同格式和类型的数据。不过,通常情况相爱插入集合中的数据都会有一定的关联性。

image-20240602163406051

集合不需要事先创建,当第一个文档插入或者第一个索引创建时,如果该集合不存在,则会创建一个新的集合。

集合名可以是满足下列条件的任意 UTF-8 字符串:

  • 集合名不能是空字符串""
  • 集合名不能含有 \0 (空字符),这个字符表示集合名的结尾。
  • 集合名不能以"system."开头,这是为系统集合保留的前缀。例如 system.users 这个集合保存着数据库的用户信息,system.namespaces 集合保存着所有数据库集合的信息。
  • 集合名必须以下划线或者字母符号开始,并且不能包含 $

数据库

数据库用于存储所有集合,而集合又用于存储所有文档。一个 MongoDB 中可以创建多个数据库,每一个数据库都有自己的集合和权限。

MongoDB 预留了几个特殊的数据库。

  • admin : admin 数据库主要是保存 root 用户和角色。例如,system.users 表存储用户,system.roles 表存储角色。一般不建议用户直接操作这个数据库。将一个用户添加到这个数据库,且使它拥有 admin 库上的名为 dbAdminAnyDatabase 的角色权限,这个用户自动继承所有数据库的权限。一些特定的服务器端命令也只能从这个数据库运行,比如关闭服务器。
  • local : local 数据库是不会被复制到其他分片的,因此可以用来存储本地单台服务器的任意 collection。一般不建议用户直接使用 local 库存储任何数据,也不建议进行 CRUD 操作,因为数据无法被正常备份与恢复。
  • config : 当 MongoDB 使用分片设置时,config 数据库可用来保存分片的相关信息。
  • test : 默认创建的测试库,连接 mongod 服务时,如果不指定连接的具体数据库,默认就会连接到 test 数据库。

数据库名可以是满足以下条件的任意 UTF-8 字符串:

  • 不能是空字符串""
  • 不得含有' '(空格)、.$/\\0 (空字符)。
  • 应全部小写。
  • 最多 64 字节。

数据库名最终会变成文件系统里的文件,这也就是有如此多限制的原因。

MongoDB 有什么特点?

  • 数据记录被存储为文档 :MongoDB 中的记录就是一个 BSON 文档,它是由键值对组成的数据结构,类似于 JSON 对象,是 MongoDB 中的基本数据单元。
  • 模式自由 :集合的概念类似 MySQL 里的表,但它不需要定义任何模式,能够用更少的数据对象表现复杂的领域模型对象。
  • 支持多种查询方式 :MongoDB 查询 API 支持读写操作 (CRUD)以及数据聚合、文本搜索和地理空间查询。
  • 支持 ACID 事务 :NoSQL 数据库通常不支持事务,为了可扩展和高性能进行了权衡。不过,也有例外,MongoDB 就支持事务。与关系型数据库一样,MongoDB 事务同样具有 ACID 特性。MongoDB 单文档原生支持原子性,也具备事务的特性。MongoDB 4.0 加入了对多文档事务的支持,但只支持复制集部署模式下的事务,也就是说事务的作用域限制为一个副本集内。MongoDB 4.2 引入了分布式事务,增加了对分片集群上多文档事务的支持,并合并了对副本集上多文档事务的现有支持。
  • 高效的二进制存储 :存储在集合中的文档,是以键值对的形式存在的。键用于唯一标识一个文档,一般是 ObjectId 类型,值是以 BSON 形式存在的。BSON = Binary JSON, 是在 JSON 基础上加了一些类型及元数据描述的格式。
  • 自带数据压缩功能 :存储同样的数据所需的资源更少。
  • 支持 mapreduce :通过分治的方式完成复杂的聚合任务。不过,从 MongoDB 5.0 开始,map-reduce 已经不被官方推荐使用了,替代方案是 聚合管道。聚合管道提供比 map-reduce 更好的性能和可用性。
  • 支持多种类型的索引 :MongoDB 支持多种类型的索引,包括单字段索引、复合索引、多键索引、哈希索引、文本索引、 地理位置索引等,每种类型的索引有不同的使用场合。
  • 支持 failover :提供自动故障恢复的功能,主节点发生故障时,自动从从节点中选举出一个新的主节点,确保集群的正常使用,这对于客户端来说是无感知的。
  • 支持分片集群 :MongoDB 支持集群自动切分数据,让集群存储更多的数据,具备更强的性能。在数据插入和更新时,能够自动路由和存储。
  • 支持存储大文件 :MongoDB 的单文档存储空间要求不超过 16MB。对于超过 16MB 的大文件,MongoDB 提供了 GridFS 来进行存储,通过 GridFS,可以将大型数据进行分块处理,然后将这些切分后的小文档保存在数据库中。

MongoDB 适合什么应用场景?

MongoDB 的优势在于其数据模型和存储引擎的灵活性、架构的可扩展性以及对强大的索引支持。

选用 MongoDB 应该充分考虑 MongoDB 的优势,结合实际项目的需求来决定:

  • 随着项目的发展,使用类 JSON 格式(BSON)保存数据是否满足项目需求?MongoDB 中的记录就是一个 BSON 文档,它是由键值对组成的数据结构,类似于 JSON 对象,是 MongoDB 中的基本数据单元。
  • 是否需要大数据量的存储?是否需要快速水平扩展?MongoDB 支持分片集群,可以很方便地添加更多的节点(实例),让集群存储更多的数据,具备更强的性能。
  • 是否需要更多类型索引来满足更多应用场景?MongoDB 支持多种类型的索引,包括单字段索引、复合索引、多键索引、哈希索引、文本索引、 地理位置索引等,每种类型的索引有不同的使用场合。

MongoDB 存储引擎

MongoDB 支持哪些存储引擎?

存储引擎(Storage Engine)是数据库的核心组件,负责管理数据在内存和磁盘中的存储方式。

与 MySQL 一样,MongoDB 采用的也是 插件式的存储引擎架构 ,支持不同类型的存储引擎,不同的存储引擎解决不同场景的问题。在创建数据库或集合时,可以指定存储引擎。

插件式的存储引擎架构可以实现 Server 层和存储引擎层的解耦,可以支持多种存储引擎,如MySQL既可以支持B-Tree结构的InnoDB存储引擎,还可以支持LSM结构的RocksDB存储引擎。

在存储引擎刚出来的时候,默认是使用 MMAPV1 存储引擎,MongoDB4.x 版本不再支持 MMAPv1 存储引擎。

现在主要有下面这两种存储引擎:

  • WiredTiger 存储引擎 :自 MongoDB 3.2 以后,默认的存储引擎为 WiredTiger 存储引擎 。非常适合大多数工作负载,建议用于新部署。WiredTiger 提供文档级并发模型、检查点和数据压缩(后文会介绍到)等功能。
  • In-Memory 存储引擎 :In-Memory 存储引擎在 MongoDB Enterprise 中可用。它不是将文档存储在磁盘上,而是将它们保留在内存中以获得更可预测的数据延迟。

此外,MongoDB 3.0 提供了 可插拔的存储引擎 API ,允许第三方为 MongoDB 开发存储引擎,这点和 MySQL 也比较类似。

WiredTiger 基于 LSM Tree 还是 B+ Tree?

目前绝大部分流行的数据库存储引擎都是基于 B/B+ Tree 或者 LSM(Log Structured Merge) Tree 来实现的。对于 NoSQL 数据库来说,绝大部分(比如 HBase、Cassandra、RocksDB)都是基于 LSM 树,MongoDB 不太一样。

上面也说了,自 MongoDB 3.2 以后,默认的存储引擎为WiredTiger 存储引擎。在 WiredTiger 引擎官网上,我们发现 WiredTiger 使用的是 B+ 树作为其存储结构:

1
WiredTiger maintains a table's data in memory using a data structure called a B-Tree ( B+ Tree to be specific), referring to the nodes of a B-Tree as pages. Internal pages carry only keys. The leaf pages store both keys and values.

此外,WiredTiger 还支持 LSM(Log Structured Merge) 树作为存储结构,MongoDB 在使用WiredTiger 作为存储引擎时,默认使用的是 B+ 树。

如果想要了解 MongoDB 使用 B 树的原因,可以看看这篇文章:为什么 MongoDB 使用 B 树?。

为什么这么设计(Why’s THE Design)是一系列关于计算机领域中程序设计决策的文章,我们在这个系列的每一篇文章中都会提出一个具体的问题并从不同的角度讨论这种设计的优缺点、对具体实现造成的影响。如果你有想要了解的问题,可以在文章下面留言。

我们在这一系列前面的文章曾经分析过 为什么 MySQL 使用 B+ 树,有读者在文章下面留言,希望能出一个为什么 MongoDB 使用 B 树的对比文章,这是一个比较好的问题,MySQL 和 MongoDB 两种不同类型的数据库使用了相似却不同的数据结构,为什么 MySQL 选择使用 B+ 树而 MongoDB 使用 B 树呢?

概述

MongoDB 是一个通用的、面向文档的分布式数据库[^1],这是官方对 MongoDB 介绍。区别于传统的关系型数据库 MySQL、Oracle 和 SQL Server,MongoDB 最重要的一个特点就是**『面向文档』**,由于数据存储方式的不同,对外提供的接口不再是被大家熟知的 SQL,所以被划分成了 NoSQL,NoSQL 是相对 SQL 而言的,很多我们耳熟能详的存储系统都被划分成了 NoSQL,例如:Redis、DynamoDB[^2] 和 Elasticsearch 等。

image-20240602163456995

NoSQL 经常被理解成没有 SQL(Non-SQL)或者非关系型(Non-Relational)[^3],不过也有人将其理解成不只是 SQL(Not Only SQL)[^4],深挖这个词的含义和起源可能没有太多意义,这种二次解读很多时候都是为营销服务的,我们只需要知道 MongoDB 对数据的存储方式与传统的关系型数据库完全不同。

MongoDB 的架构与 MySQL 非常类似,它们底层都使用了可插拔的存储引擎以满足用户的不同需求,用户可以根据数据特征选择不同的存储引擎,最新版本的 MongoDB 使用了 WiredTiger 作为默认的存储引擎[^5]。

image-20240602163510771

作为 MongoDB 默认的存储引擎,WiredTiger 使用 B 树作为索引底层的数据结构,但是除了 B 树之外,它还支持 LSM 树作为可选的底层存储结构,LSM 树的全称是 Log-structured merge-tree,你可以在 MongoDB 中使用如下所示的命令创建一个基于 LSM 树的集合(Collection)[^6]:

1
db.createCollection(    "posts",    { storageEngine: { wiredTiger: {configString: "type=lsm"}}})

我们在这篇文章中不仅会介绍 MongoDB 的默认存储引擎 WiredTiger 为什么选择使用 B 树而不是 B+ 树,还会对 B 树和 LSM 树之间的性能和应用场景进行比较,帮助各位读者更全面地理解今天的问题。

设计

既然要比较两个不同数据结构与 B 树的差别,那么在这里我们将分两个小节分别介绍 B+ 树和 LSM 树为什么没有成为 WiredTiger 默认的数据结构:

  • 作为非关系型的数据库,MongoDB 对于遍历数据的需求没有关系型数据库那么强,它追求的是读写单个记录的性能;
  • 大多数 OLTP 的数据库面对的都是读多写少的场景,B 树与 LSM 树在该场景下有更大的优势;

上述的两个场景都是 MongoDB 需要面对和解决的,所以我们会在这两个常见场景下对不同的数据结构进行比较。

非关系型

我们在上面其实已经多次提到了 MongoDB 是非关系型的文档数据库,它完全抛弃了关系型数据库那一套体系之后,在设计和实现上就非常自由,它不再需要遵循 SQL 和关系型数据库的体系,可以更自由对特定场景进行优化,而在 MongoDB 假设的场景中遍历数据并不是常见的需求。

image-20240602163545596

MySQL 中使用 B+ 树是因为 B+ 树只有叶节点会存储数据,将树中的每一个叶节点通过指针连接起来就能实现顺序遍历,而遍历数据在关系型数据库中非常常见,所以这么选择是完全没有问题的[^7]。

MongoDB 和 MySQL 在多个不同数据结构之间选择的最终目的就是减少查询需要的随机 IO 次数,MySQL 认为遍历数据的查询是常见的,所以它选择 B+ 树作为底层数据结构,而舍弃了通过非叶节点存储数据这一特性,但是 MongoDB 面对的问题就不太一样了:

image-20240602163600570

虽然遍历数据的查询是相对常见的,但是【关键】MongoDB 认为查询单个数据记录远比遍历数据更加常见,由于 B 树的非叶结点也可以存储数据,所以查询一条数据所需要的平均随机 IO 次数会比 B+ 树少,使用 B 树的 MongoDB 在类似场景中的查询速度就会比 MySQL 快。这里并不是说 MongoDB 并不能对数据进行遍历,我们在 MongoDB 中也可以使用范围来查询一批满足对应条件的记录,只是需要的时间会比 MySQL 长一些。

1
SELECT * FROM comments WHERE created_at > '2019-01-01'

很多人看到遍历数据的查询想到的可能都是如上所示的范围查询,然而在关系型数据库中更常见的其实是如下所示的 SQL —— 查询外键或者某字段等于某一个值的全部记录:

1
SELECT * FROM comments WHERE post_id = 1

上述查询其实并不是范围查询,它没有使用 >< 等表达式,但是它却会在 comments 表中查询一系列的记录,如果 comments 表上有索引 post_id,那么这个查询可能就会在索引中遍历相应索引,找到满足条件的 comment,这种查询也会受益于 MySQL B+ 树相互连接的叶节点,因为它能减少磁盘的随机 IO 次数。

MongoDB 作为非关系型的数据库,它从集合的设计上就使用了完全不同的方法,如果我们仍然使用传统的关系型数据库的表设计思路来思考 MongoDB 中集合的设计,写出类似如上所示的查询会带来相对比较差的性能:

1
db.comments.find( { post_id: 1 } )

因为 B 树的所有节点都能存储数据,各个连续的节点之间没有很好的办法通过指针相连,所以上述查询在 B 树中性能会比 B+ 树差很多,但是这并不是一个 MongoDB 中推荐的设计方法,更合适的做法其实是使用嵌入文档,将 post 和属于它的所有 comments 都存储到一起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"_id": "...",
"title": "为什么 MongoDB 使用 B 树",
"author": "draven",
"comments": [
{
"_id": "...",
"content": "你这写的不行"
},
{
"_id": "...",
"content": "一楼说的对"
}
]
}

使用上述方式对数据进行存储时就不会遇到 db.comments.find( { post_id: 1 } ) 这样的查询了,我们只需要将 post 取出来就会获得相关的全部评论,这种区别于传统关系型数据库的设计方式是需要所有使用 MongoDB 的开发者重新思考的,这也是很多人使用 MongoDB 后却发现性能不如 MySQL 的最大原因 —— 使用的姿势不对。

进一步说明:

在传统的关系型数据库中,你可能会将帖子(post)和评论(comments)存储在两个不同的表中,并通过post_id将他们关联起来。当你想要获取一个帖子和它的所有评论时,你就需要执行一个类似于db.comments.find( { post_id: 1 } )的查询。然而,因为MongoDB使用的是B树,这种查询在性能上可能不如使用B+树的关系型数据库。

在MongoDB中,一种更好的设计方式是使用嵌入式文档(embedded documents)。你可以将一个帖子和它的所有评论都存储在一起,像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"_id": "...",
"title": "为什么 MongoDB 使用 B 树",
"author": "draven",
"comments": [
{
"_id": "...",
"content": "你这写的不行"
},
{
"_id": "...",
"content": "一楼说的对"
}
]
}

在这种情况下,你只需要取出帖子就能同时获取所有的评论,而不需要像在关系型数据库中那样进行额外的查询。这样就大大提高了查询效率,因此这是一种更推荐的设计方法。


有些读者到这里可能会有疑问了,既然 MongoDB 认为查询单个数据记录远比遍历数据的查询更加常见,那为什么不使用哈希作为底层的数据结构呢?

回答:如果我们使用哈希,那么对于所有单条记录查询的复杂度都会是 O(1),但是遍历数据的复杂度就是 O(n);如果使用 B+ 树,那么单条记录查询的复杂度是 O(log n),遍历数据的复杂度就是 O(log n) + X,这两种不同的数据结构一种提供了最好的单记录查询性能,一种提供了最好的遍历数据的性能,但是这都不能满足 MongoDB 面对的场景 —— 单记录查询非常常见,但是对于遍历数据也需要有相对较好的性能支持,哈希这种性能表现较为极端的数据结构往往只能在简单、极端的场景下使用。

读多写少

LSM 树是一个基于磁盘的数据结构,它设计的主要目的是为长期需要高频率写入操作的文件提供低成本的索引机制[^8]。无论是 B 树还是 B+ 树,向这些数据结构组成的索引文件中写入记录都需要执行的磁盘随机写,LSM 树的优化逻辑就是牺牲部分的读性能,将随机写转换成顺序写以优化数据的写入。

我们在这篇文章不会详细介绍为什么 LSM 树有着较好的写入性能,我们只是来分析为什么 WiredTiger 使用 B 树作为默认的数据结构。WiredTiger 对 LSM 树和 B 树的性能进行了读写吞吐量的基准测试[^9],通过基准测试得到了如下图所示的结果,从图中的结果我们能发现:

image-20240602164301739

  1. 在不限制写入的情况下;

    1. LSM 树的写入性能是 B 树的 1.5 ~ 2 倍;
    2. LSM 树的读取性能是 B 树的 1/6 ~ 1/3;
  2. 在限制写入的情况下;

    1. LSM 树的写入性能与 B 树的性能基本持平;
    2. LSM 树的读取性能是 B 树的 1/4 ~ 1/2;

在限制写入的情况下,每秒会写入 30,000 条数据,从这里的分析结果来看,无论那种情况下 B 树的读取性能是远好于 LSM 树的。对于大多数的 OLTP 系统来说,系统的查询会是写的很多倍,所以 LSM 树在写入方面的优异表现也没有办法让它成为 MongoDB 默认的数据格式。

解释:在"无限制写"的情况下,数据可以随时写入,没有任何限制。这种方式下,如果有大量的写入请求,那么写入性能会成为系统的瓶颈。在这种情况下,LSM树的写入性能通常会优于B树。

而"限制写"是指对数据写入进行一定的限制,这可能是通过限制写入速度或者限制同时写入的数据量等方式来实现的。在这种情况下,因为写入被限制,所以读取性能会变得更重要。在这种情况下,B树的读取性能通常会优于LSM树。

总结

应用场景永远都是系统设计时首先需要考虑的问题,作为 NoSQL 的 MongoDB,其目标场景就与更早的数据库就有着比较大的差异,我们来简单总结一下 MongoDB 最终选择使用 B 树的两个原因:

  • MySQL 使用 B+ 树是因为数据的遍历在关系型数据库中非常常见,它经常需要处理各个表之间的关系并通过范围查询一些数据;但是 MongoDB 作为面向文档的数据库,与数据之间的关系相比,它更看重以文档为中心的组织方式,所以选择了查询单个文档性能较好的 B 树,这个选择对遍历数据的查询也可以保证可以接受的时延;
  • LSM 树是一种专门用来优化写入的数据结构,它将随机写变成了顺序写显著地提高了写入性能,但是却牺牲了读的效率,这与大多数场景需要的特点是不匹配的,所以 MongoDB 最终还是选择读取性能更好的 B 树作为默认的数据结构;

MongoDB 聚合

MongoDB 聚合有什么用?

实际项目中,我们经常需要将多个文档甚至是多个集合汇总到一起计算分析(比如求和、取最大值)并返回计算后的结果,这个过程被称为 聚合操作

根据官方文档介绍,我们可以使用聚合操作来:

  • 将来自多个文档的值组合在一起。
  • 对集合中的数据进行的一系列运算。
  • 分析数据随时间的变化。

MongoDB 提供了哪几种执行聚合的方法?

MongoDB 提供了两种执行聚合的方法:

  • 聚合管道(Aggregation Pipeline) :执行聚合操作的首选方法。
  • 单一目的聚合方法(Single purpose aggregation methods) :也就是单一作用的聚合函数比如 count()distinct()estimatedDocumentCount()

绝大部分文章中还提到了 map-reduce 这种聚合方法。不过,从 MongoDB 5.0 开始,map-reduce 已经不被官方推荐使用了,替代方案是 聚合管道。聚合管道提供比 map-reduce 更好的性能和可用性。

MongoDB 聚合管道由多个阶段组成,每个阶段在文档通过管道时转换文档。每个阶段接收前一个阶段的输出,进一步处理数据,并将其作为输入数据发送到下一个阶段。

每个管道的工作流程是:

  1. 接受一系列原始数据文档
  2. 对这些文档进行一系列运算
  3. 结果文档输出给下一个阶段

image-20240602165207382

常用阶段操作符 :

操作符 简述
$match 匹配操作符,用于对文档集合进行筛选
$project 投射操作符,用于重构每一个文档的字段,可以提取字段,重命名字段,甚至可以对原有字段进行操作后新增字段
$sort 排序操作符,用于根据一个或多个字段对文档进行排序
$limit 限制操作符,用于限制返回文档的数量
$skip 跳过操作符,用于跳过指定数量的文档
$count 统计操作符,用于统计文档的数量
$group 分组操作符,用于对文档集合进行分组
$unwind 拆分操作符,用于将数组中的每一个值拆分为单独的文档
$lookup 连接操作符,用于连接同一个数据库中另一个集合,并获取指定的文档,类似于 populate

更多操作符介绍详见官方文档:https://docs.mongodb.com/manual/reference/operator/aggregation/

阶段操作符用于 db.collection.aggregate 方法里面,数组参数中的第一层。

1
db.collection.aggregate( [ { 阶段操作符:表述 }, { 阶段操作符:表述 }, ... ] )

下面是 MongoDB 官方文档中的一个例子:

1
2
3
4
5
6
db.orders.aggregate([
# 第一阶段:$match阶段按status字段过滤文档,并将status等于"A"的文档传递到下一阶段。
{ $match: { status: "A" } },
# 第二阶段:$group阶段按cust_id字段将文档分组,以计算每个cust_id唯一值的金额总和。
{ $group: { _id: "$cust_id", total: { $sum: "$amount" } } }
])

MongoDB 事务

我们在介绍 NoSQL 数据的时候也说过,NoSQL 数据库通常不支持事务,为了可扩展和高性能进行了权衡。不过,也有例外,MongoDB 就支持事务。

与关系型数据库一样,MongoDB 事务同样具有 ACID 特性:

  • 原子性(Atomicity) :事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  • 一致性(Consistency):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
  • 隔离性(Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的。WiredTiger 存储引擎支持读未提交( read-uncommitted )、读已提交( read-committed )和快照( snapshot )隔离,MongoDB 启动时默认选快照隔离。在不同隔离级别下,一个事务的生命周期内,可能出现脏读、不可重复读、幻读等现象。
  • 持久性(Durability):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

MongoDB 单文档原生支持原子性,也具备事务的特性。当谈论 MongoDB 事务的时候,通常指的是 多文档 。MongoDB 4.0 加入了对多文档 ACID 事务的支持,但只支持复制集部署模式下的 ACID 事务,也就是说事务的作用域限制为一个副本集内。MongoDB 4.2 引入了 分布式事务 ,增加了对分片集群上多文档事务的支持,并合并了对副本集上多文档事务的现有支持。

根据官方文档介绍:

从 MongoDB 4.2 开始,分布式事务和多文档事务在 MongoDB 中是一个意思。分布式事务是指分片集群和副本集上的多文档事务。从 MongoDB 4.2 开始,多文档事务(无论是在分片集群还是副本集上)也称为分布式事务。

在大多数情况下,多文档事务比单文档写入会产生更大的性能成本。对于大部分场景来说, 非规范化数据模型(嵌入式文档和数组) 依然是最佳选择。也就是说,适当地对数据进行建模可以最大限度地减少对多文档事务的需求。

注意 :

  • 从MongoDB 4.2开始,多文档事务支持副本集和分片集群,其中:主节点使用WiredTiger存储引擎,同时从节点使用WiredTiger存储引擎或In-Memory存储引擎。在MongoDB 4.0中,只有使用WiredTiger存储引擎的副本集支持事务。
  • 在MongoDB 4.2及更早版本中,你无法在事务中创建集合。从 MongoDB 4.4 开始,您可以在事务中创建集合和索引。有关详细信息,请参阅 在事务中创建集合和索引。

MongoDB 数据压缩

借助 WiredTiger 存储引擎( MongoDB 3.2 后的默认存储引擎),MongoDB 支持对所有集合和索引进行压缩。压缩以额外的 CPU 为代价最大限度地减少存储使用。

默认情况下,WiredTiger 使用 Snappy 压缩算法(谷歌开源,旨在实现非常高的速度和合理的压缩,压缩比 3 ~ 5 倍)对所有集合使用块压缩,对所有索引使用前缀压缩。

除了 Snappy 之外,对于集合还有下面这些压缩算法:

  • zlib:高度压缩算法,压缩比 5 ~ 7 倍
  • Zstandard(简称 zstd):Facebook 开源的一种快速无损压缩算法,针对 zlib 级别的实时压缩场景和更好的压缩比,提供更高的压缩率和更低的 CPU 使用率,MongoDB 4.2 开始可用。

WiredTiger 日志也会被压缩,默认使用的也是 Snappy 压缩算法。如果日志记录小于或等于 128 字节,WiredTiger 不会压缩该记录。

MongoDB 索引

MongoDB 索引有什么用?

和关系型数据库类似,MongoDB 中也有索引。索引的目的主要是用来提高查询效率,如果没有索引的话,MongoDB 必须执行 集合扫描 ,即扫描集合中的每个文档,以选择与查询语句匹配的文档。如果查询存在合适的索引,MongoDB 可以使用该索引来限制它必须检查的文档数量。并且,MongoDB 可以使用索引中的排序返回排序后的结果。

虽然索引可以显著缩短查询时间,但是使用索引、维护索引是有代价的。在执行写入操作时,除了要更新文档之外,还必须更新索引,这必然会影响写入的性能。因此,当有大量写操作而读操作少时,或者不考虑读操作的性能时,都不推荐建立索引。

MongoDB 支持哪些类型的索引?

MongoDB 支持多种类型的索引,包括单字段索引、复合索引、多键索引、哈希索引、文本索引、 地理位置索引等,每种类型的索引有不同的使用场合。

  • 单字段索引: 建立在单个字段上的索引,索引创建的排序顺序无所谓,MongoDB 可以头/尾开始遍历。
  • 复合索引: 建立在多个字段上的索引,也可以称之为组合索引、联合索引。
  • 多键索引 :MongoDB 的一个字段可能是数组,在对这种字段创建索引时,就是多键索引。MongoDB 会为数组的每个值创建索引。就是说你可以按照数组里面的值做条件来查询,这个时候依然会走索引。
  • 哈希索引 :按数据的哈希值索引,用在哈希分片集群上。
  • 文本索引: 支持对字符串内容的文本搜索查询。文本索引可以包含任何值为字符串或字符串元素数组的字段。一个集合只能有一个文本搜索索引,但该索引可以覆盖多个字段。MongoDB 虽然支持全文索引,但是性能低下,暂时不建议使用。
  • 地理位置索引: 基于经纬度的索引,适合 2D 和 3D 的位置查询。
  • 唯一索引 :确保索引字段不会存储重复值。如果集合已经存在了违反索引的唯一约束的文档,则后台创建唯一索引会失败。
  • TTL 索引 :TTL 索引提供了一个过期机制,允许为每一个文档设置一个过期时间,当一个文档达到预设的过期时间之后就会被删除。

复合索引中字段的顺序有影响吗?

复合索引中字段的顺序非常重要,例如下图中的复合索引由{userid:1, score:-1}组成,则该复合索引首先按照userid升序排序;然后再每个userid的值内,再按照score降序排序。

image-20240602170041591

在复合索引中,按照何种方式排序,决定了该索引在查询中是否能被应用到。

走复合索引的排序:

1
2
db.s2.find().sort({"userid": 1, "score": -1})
db.s2.find().sort({"userid": -1, "score": 1})

不走复合索引的排序:

1
2
3
4
5
6
db.s2.find().sort({"userid": 1, "score": 1})
db.s2.find().sort({"userid": -1, "score": -1})
db.s2.find().sort({"score": 1, "userid": -1})
db.s2.find().sort({"score": 1, "userid": 1})
db.s2.find().sort({"score": -1, "userid": -1})
db.s2.find().sort({"score": -1, "userid": 1})

我们可以通过 explain 进行分析:

1
db.s2.find().sort({"score": -1, "userid": 1}).explain()

复合索引遵循左前缀原则吗?

MongoDB 的复合索引遵循左前缀原则 :拥有多个键的索引,可以同时得到所有这些键的前缀组成的索引,但不包括除左前缀之外的其他子集。比如说,有一个类似 {a: 1, b: 1, c: 1, ..., z: 1} 这样的索引,那么实际上也等于有了 {a: 1}{a: 1, b: 1}{a: 1, b: 1, c: 1} 等一系列索引,但是不会有 {b: 1} 这样的非左前缀的索引。

什么是 TTL 索引?

TTL 索引提供了一个过期机制,允许为每一个文档设置一个过期时间 expireAfterSeconds ,当一个文档达到预设的过期时间之后就会被删除。TTL 索引除了有 expireAfterSeconds 属性外,和普通索引一样。

数据过期对于某些类型的信息很有用,比如机器生成的事件数据、日志和会话信息,这些信息只需要在数据库中保存有限的时间。

TTL 索引运行原理

  • MongoDB 会开启一个后台线程读取该 TTL 索引的值来判断文档是否过期,但不会保证已过期的数据会立马被删除,因后台线程每 60 秒触发一次删除任务,且如果删除的数据量较大,会存在上一次的删除未完成,而下一次的任务已经开启的情况,导致过期的数据也会出现超过了数据保留时间 60 秒以上的现象。
  • 对于副本集而言,TTL 索引的后台进程只会在 Primary 节点开启,在从节点会始终处于空闲状态,从节点的数据删除是由主库删除后产生的 oplog 来做同步。

TTL 索引限制

  • TTL 索引是单字段索引。复合索引不支持 TTL。
  • _id字段不支持 TTL 索引。
  • 无法在上限集合(Capped Collection)上创建 TTL 索引,因为 MongoDB 无法从上限集合中删除文档。
  • 如果某个字段已经存在非 TTL 索引,那么在该字段上无法再创建 TTL 索引。

什么是覆盖索引查询?

根据官方文档介绍,覆盖查询是以下的查询:

  • 所有的查询字段是索引的一部分。
  • 结果中返回的所有字段都在同一索引中。
  • 查询中没有字段等于null

由于所有出现在查询中的字段是索引的一部分, MongoDB 无需在整个数据文档中检索匹配查询条件和返回使用相同索引的查询结果。因为索引存在于内存中,从索引中获取数据比通过扫描文档读取数据要快得多。

举个例子:我们有如下 users 集合:

1
2
3
4
5
6
7
8
{
"_id": ObjectId("53402597d852426020000002"),
"contact": "987654321",
"dob": "01-01-1991",
"gender": "M",
"name": "Tom Benzamin",
"user_name": "tombenzamin"
}

我们在 users 集合中创建联合索引,字段为 genderuser_name :

1
db.users.ensureIndex({gender:1,user_name:1})

现在,该索引会覆盖以下查询:

1
db.users.find({gender:"M"},{user_name:1,_id:0})

为了让指定的索引覆盖查询,必须显式地指定 _id: 0 来从结果中排除 _id 字段,因为索引不包括 _id 字段。

MongoDB 高可用

复制集群

什么是复制集群?

MongoDB 的复制集群又称为副本集群是一组维护相同数据集合的 mongod 进程

客户端连接到整个 Mongodb 复制集群,主节点机负责整个复制集群的写,从节点可以进行读操作,但默认还是主节点负责整个复制集群的读。主节点发生故障时,自动从从节点中选举出一个新的主节点,确保集群的正常使用,这对于客户端来说是无感知的。

通常来说,一个复制集群包含 1 个主节点(Primary),多个从节点(Secondary)以及零个或 1 个仲裁节点(Arbiter)。

  • 主节点 :整个集群的写操作入口,接收所有的写操作,并将集合所有的变化记录到操作日志中,即 oplog。主节点挂掉之后会自动选出新的主节点。
  • 从节点 :从主节点同步数据,在主节点挂掉之后选举新节点。不过,从节点可以配置成 0 优先级,阻止它在选举中成为主节点。
  • 仲裁节点 :这个是为了节约资源或者多机房容灾用,只负责主节点选举时投票不存数据,保证能有节点获得多数赞成票。

下图是一个典型的三成员副本集群:

image-20240602170958257

主节点与备节点之间是通过 oplog(操作日志) 来同步数据的。oplog 是 local 库下的一个特殊的 上限集合(Capped Collection) ,用来保存写操作所产生的增量日志,类似于 MySQL 中 的 Binlog。

上限集合类似于定长的循环队列,数据顺序追加到集合的尾部,当集合空间达到上限时,它会覆盖集合中最旧的文档。上限集合的数据将会被顺序写入到磁盘的固定空间内,所以,I/O 速度非常快,如果不建立索引,性能更好。

当主节点上的一个写操作完成后,会向 oplog 集合写入一条对应的日志,而从节点则通过这个 oplog 不断拉取到新的日志,在本地进行回放以达到数据同步的目的。

副本集最多有一个主节点。如果当前主节点不可用,一个选举会抉择出新的主节点。MongoDB 的节点选举规则能够保证在 Primary 挂掉之后选取的新节点一定是集群中数据最全的一个。

为什么要用复制集群?

  • 实现 failover :提供自动故障恢复的功能,主节点发生故障时,自动从从节点中选举出一个新的主节点,确保集群的正常使用,这对于客户端来说是无感知的。
  • 实现读写分离 :我们可以设置从节点上可以读取数据,主节点负责写入数据,这样的话就实现了读写分离,减轻了主节点读写压力过大的问题。MongoDB 4.0 之前版本如果主库压力不大,不建议读写分离,因为写会阻塞读,除非业务对响应时间不是非常关注以及读取历史数据接受一定时间延迟。

分片集群

什么是分片集群?

分片集群是 MongoDB 的分布式版本,相较副本集,分片集群数据被均衡的分布在不同分片中, 不仅大幅提升了整个集群的数据容量上限,也将读写的压力分散到不同分片,以解决副本集性能瓶颈的难题。

MongoDB 的分片集群由如下三个部分组成

image-20240602171729227

  • Config Servers:配置服务器,本质上是一个 MongoDB 的副本集,负责存储集群的各种元数据和配置,如分片地址、Chunks 等
  • Mongos:路由服务,不存具体数据,从 Config 获取集群配置讲请求转发到特定的分片,并且整合分片结果返回给客户端。
  • Shard:每个分片是整体数据的一部分子集,从MongoDB3.6版本开始,每个Shard必须部署为副本集(replica set)架构

解释:

首先,分片集群主要由三个部分组成:Config Servers,Mongos和Shard。

  1. Config Servers:它们是配置服务器,负责存储集群的元数据和配置信息。比如说,你的数据库里有哪些数据,这些数据该如何分布在不同的Shard上,这些信息都是由Config Servers来保存的。可以把它们看作是一本详尽的目录,帮助我们知道去哪里查找或者存储数据。
  2. Mongos:它是路由服务,它的主要职能就是把客户端的请求转发到对应的Shard上。比如说,你想要查询某个数据,这个请求首先会被发送到Mongos,Mongos会查阅Config Servers的信息,找到这个数据存储在哪个Shard上,然后把请求转发到那个Shard。Mongos本身不存储数据,它只是起到一个信息中转站的作用。
  3. Shard:它们是真正存储数据的地方。每个Shard存储数据库的一部分数据。这样,当数据量非常大的时候,我们可以把数据分布在多个Shard上,这样不仅可以提高数据的读写速度,还能有效地利用多台服务器的存储空间。

举个例子,假设你正在管理一个图书馆,那么Config Servers就像是图书馆的图书目录,记录了每本书在哪个书架上;Mongos就像是图书馆的管理员,当读者想要找一本书的时候,管理员会查阅目录,然后告诉读者这本书在哪个书架上;而Shard就像是图书馆的书架,真正存储了这些书。

为什么要用分片集群?

随着系统数据量以及吞吐量的增长,常见的解决办法有两种:垂直扩展和水平扩展。

垂直扩展通过增加单个服务器的能力来实现,比如磁盘空间、内存容量、CPU 数量等;水平扩展则通过将数据存储到多个服务器上来实现,根据需要添加额外的服务器以增加容量。

类似于 Redis Cluster,MongoDB 也可以通过分片实现 水平扩展 。水平扩展这种方式更灵活,可以满足更大数据量的存储需求,支持更高吞吐量。并且,水平扩展所需的整体成本更低,仅仅需要相对较低配置的单机服务器即可,代价是增加了部署的基础设施和维护的复杂性。

也就是说当你遇到如下问题时,可以使用分片集群解决:

  • 存储容量受单机限制,即磁盘资源遭遇瓶颈。
  • 读写能力受单机限制,可能是 CPU、内存或者网卡等资源遭遇瓶颈,导致读写能力无法扩展。

举例:

垂直扩展和水平扩展是处理大数据的两种常见解决方案。我来通俗易懂地解释一下这两种扩展方式。

  1. 垂直扩展:这就好比是把一台车加装更大的引擎、更宽敞的储物空间,或者更舒适的座椅来提高它的性能。在计算机世界里,垂直扩展就是通过提升单台服务器的硬件性能来处理更多的数据,比如增加更大的硬盘、更多的内存、或者更强大的CPU。这种方式的优点是操作相对简单,只需要购买更好的硬件就行,但缺点是硬件的提升总是有上限的,比如CPU的速度或者硬盘的大小总是有个上限。
  2. 水平扩展:这就像是买更多的车来承载更多的人。在计算机世界里,水平扩展就是通过增加更多的服务器来共同处理数据。比如一个大型的网站,可以将用户的请求分发到多台服务器上处理,这样就可以处理更多的用户请求。水平扩展的优点是理论上可以无限扩展,只要有足够的资源,就可以一直添加服务器。但是,这种方式需要有合理的数据分片和负载均衡策略,否则可能会导致数据不一致或者某些服务器过载。

举个例子,假设我们开一家快餐店。如果我们要提高服务能力,垂直扩展就好比是购买更快的炉子、更大的冰箱、更多的食材,这样我们可以更快地做出更多的食物。而水平扩展就好比是开更多的分店,每家分店都可以为客户提供服务,这样我们就可以服务更多的客户。

什么是分片键?

分片键(Shard Key) 是数据分区的前提, 从而实现数据分发到不同服务器上,减轻服务器的负担。也就是说,分片键决定了集合内的文档如何在集群的多个分片间的分布状况。

分片键就是文档里面的一个字段,但是这个字段不是普通的字段,有一定的要求:

  • 它必须在所有文档中都出现。
  • 它必须是集合的一个索引,可以是单索引或复合索引的前缀索引,不能是多索引、文本索引或地理空间位置索引。
  • MongoDB 4.2 之前的版本,文档的分片键字段值不可变。MongoDB 4.2 版本开始,除非分片键字段是不可变的 _id 字段,否则您可以更新文档的分片键值。MongoDB 5.0 版本开始,实现了实时重新分片(live resharding),可以实现分片键的完全重新选择。
  • 它的大小不能超过 512 字节。

解释

分片键在MongoDB中起到了非常重要的作用。它是决定数据如何在分布式数据库系统中进行分布的关键。简单来说,就好比是一本厚厚的字典,我们是按照字母顺序来决定每个单词在哪一页的,那么这个“字母顺序”就相当于是分片键。

以一个学生管理系统为例,如果我们把学生的学号设置为分片键,那么系统在存储学生信息的时候,就会根据学号的大小将数据分布到不同的服务器上。比如学号1-1000的学生信息存储在服务器A,学号1001-2000的学生信息存储在服务器B,以此类推。这样做的好处是,当我们想要查找某个学生的信息时,系统可以直接去对应的服务器上查找,大大提高了查询效率。

但是,分片键需要满足一些条件:

  1. 它必须在所有文档中都出现。这是因为分片键是用来决定数据如何分布的,如果有的文档没有这个字段,那系统就无法决定把这个文档存储到哪里。
  2. 它必须是集合的一个索引,可以是单索引或复合索引的前缀索引。
  3. 分片键的大小不能超过512字节。

这就是分片键的基本概念,希望对你有所帮助。

如何选择分片键?

选择合适的片键对 sharding 效率影响很大,主要基于如下四个因素(摘自分片集群使用注意事项 - - 腾讯云文档):

  • 取值基数 取值基数建议尽可能大,如果用小基数的片键,因为备选值有限,那么块的总数量就有限,随着数据增多,块的大小会越来越大,导致水平扩展时移动块会非常困难。例如:选择年龄做一个基数,范围最多只有100个,随着数据量增多,同一个值分布过多时,导致 chunck 的增长超出 chuncksize 的范围,引起 jumbo chunk,从而无法迁移,导致数据分布不均匀,性能瓶颈。
  • 取值分布 取值分布建议尽量均匀,分布不均匀的片键会造成某些块的数据量非常大,同样有上面数据分布不均匀,性能瓶颈的问题。
  • 查询带分片 查询时建议带上分片,使用分片键进行条件查询时,mongos 可以直接定位到具体分片,否则 mongos 需要将查询分发到所有分片,再等待响应返回。
  • 避免单调递增或递减 单调递增的 sharding key,数据文件挪动小,但写入会集中,导致最后一篇的数据量持续增大,不断发生迁移,递减同理。

综上,在选择片键时要考虑以上4个条件,尽可能满足更多的条件,才能降低 MoveChuncks 对性能的影响,从而获得最优的性能体验。

分片策略有哪些?

MongoDB 支持两种分片算法来满足不同的查询需求(摘自MongoDB 分片集群介绍 - 阿里云文档):

1、基于范围的分片

image-20240602172751440

MongoDB 按照分片键(Shard Key)的值的范围将数据拆分为不同的块(Chunk),每个块包含了一段范围内的数据。当分片键的基数大、频率低且值非单调变更时,范围分片更高效。

  • 优点:Mongos 可以快速定位请求需要的数据,并将请求转发到相应的 Shard 节点中。
  • 缺点:可能导致数据在 Shard 节点上分布不均衡,容易造成读写热点,且不具备写分散性。
  • 适用场景:分片键的值不是单调递增或单调递减、分片键的值基数大且重复的频率低、需要范围查询等业务场景。

2、基于 Hash 值的分片

image-20240602172837739

MongoDB 计算单个字段的哈希值作为索引值,并以哈希值的范围将数据拆分为不同的块(Chunk)。

  • 优点:可以将数据更加均衡地分布在各 Shard 节点中,具备写分散性。
  • 缺点:不适合进行范围查询,进行范围查询时,需要将读请求分发到所有的 Shard 节点。
  • 适用场景:分片键的值存在单调递增或递减、片键的值基数大且重复的频率低、需要写入的数据随机分发、数据读取随机性较大等业务场景。

除了上述两种分片策略,您还可以配置 复合片键 ,例如由一个低基数的键和一个单调递增的键组成。

分片数据如何存储?

Chunk(块) 是 MongoDB 分片集群的一个核心概念,其本质上就是由一组 Document 组成的逻辑数据单元。每个 Chunk 包含一定范围片键的数据,互不相交且并集为全部数据,即离散数学中划分的概念。

分片集群不会记录每条数据在哪个分片上,而是记录 Chunk 在哪个分片上一级这个 Chunk 包含哪些数据。

默认情况下,一个 Chunk 的最大值默认为 64MB(可调整,取值范围为 1~1024 MB。如无特殊需求,建议保持默认值),进行数据插入、更新、删除时,如果此时 Mongos 感知到了目标 Chunk 的大小或者其中的数据量超过上限,则会触发 Chunk 分裂。

image-20240602172927951

数据的增长会让 Chunk 分裂得越来越多。这个时候,各个分片上的 Chunk 数量可能会不平衡。Mongos 中的 均衡器(Balancer) 组件就会执行自动平衡,尝试使各个 Shard 上 Chunk 的数量保持均衡,这个过程就是 再平衡(Rebalance)。默认情况下,数据库和集合的 Rebalance 是开启的。

如下图所示,随着数据插入,导致 Chunk 分裂,让 AB 两个分片有 3 个 Chunk,C 分片只有一个,这个时候就会把 B 分配的迁移一个到 C 分片实现集群数据均衡。

image-20240602172941546

Balancer 是 MongoDB 的一个运行在 Config Server 的 Primary 节点上(自 MongoDB 3.4 版本起)的后台进程,它监控每个分片上 Chunk 数量,并在某个分片上 Chunk 数量达到阈值进行迁移。

Chunk 只会分裂,不会合并,即使 chunkSize 的值变大。

Rebalance 操作是比较耗费系统资源的,我们可以通过在业务低峰期执行、预分片或者设置 Rebalance 时间窗等方式来减少其对 MongoDB 正常使用所带来的影响。

Chunk 只会分裂,不会合并,即使 chunkSize 的值变大。

Rebalance 操作是比较耗费系统资源的,我们可以通过在业务低峰期执行、预分片或者设置 Rebalance 时间窗等方式来减少其对 MongoDB 正常使用所带来的影响。

Chunk 迁移原理是什么?

一、chunk简介

1.1 chunk是什么

MongoDB在Sharding模式下(对于Sharding不了解的可以参考shard介绍),通过Mongos向开启了shard分片的集合写入文档,这些文档会根据其shardKey分散到MongoDB的不同shard上。

image-20240602174141898

如上图,假设集合的shardKey x(以范围分片为例,哈希分片类似),写入该集合的文档根据其x的取值范围,会被分散到chunk1到chunk4中,每个chunk只包含特定范围的数据(比如chunk2就只包含x的取值在[-75, 25)范围内的文档),同一chunk的文档只会存储在一个shard上,一个shard可能包含多个chunk,chunk具体存储在哪个shard,由记录在config server中的路由信息决定。

1.2 chunk分裂

默认情况下,一个chunk的最大值默认为64MB(可调整),进行数据插入、更新、删除时,如果此时mongos感知到了目标chunk的大小超过上限,则会触发chunk分裂。chunk分裂动作为逻辑上的概念,它会对需要分裂的chunk进行分段,指定split point标记切割的位置

image-20240602174158563

解释:

例如,假设我们的分片键是学号,一个Chunk中存储了学号1-1000的学生信息,但是这个Chunk已经满了。这时,如果我们要插入更多的学生信息,系统就会选择一个分裂点,例如500,然后将学号501-1000的学生信息移动到新的Chunk,原来的Chunk就只存储学号1-500的学生信息了。

这样,我们就可以继续向数据库插入新的数据,而不需要担心存储空间不足的问题。同时,通过合理的分裂点选择,我们也可以保证数据在Chunk之间的均衡分布,提高查询效率。

1.3 chunk的迁移

MongoDB默认情况下会开启一个balancer模块用于定期检测各个shard上的chunk数量分布,当检测到各个shard上的chunk数量存在分布不均匀的情况时,就会触发chunk迁移。如下图,三个shard的chunk数量分别为3、3、1,此时balancer认为chunk数量分布不均,于是会将shard B上的chunk迁移一个到shard C上,这样三个shard的chunk数量分布最终就会变为3、2、2,分布更为均匀。

image-20240602174214629

上文提到的balancer通过特定规则来筛选出来需要进行迁移的chunk,这些规则具体是什么呢?当前,mongoDB会对以下三种类型的chunk进行迁移(优先级由高到底):

  • chunk属于正在进行排水(即draining,一般出现在shard删除,move primary等情况下,表示该chunk需要尽快被删除)的shard
  • chunk是否违反了zones的约束(zones的定义见此)
  • 如果不属于以上两种情况,则通过计算各个shard之间的chunks数量进行负载均衡,原则上balancer会让各个shard在满足zones约束的条件下尽可能均衡

选定了需要迁移的chunk后,balancer会选择当前shards中chunks数最少的一个作为迁移的目标。

chunk迁移原理

chunk迁移操作通过moveChunk命令发起,moveChunk命令即可以被balancer自动调用(balancer每隔10s扫描哪些chunk需要被迁移),也支持用户主动发起。迁移chunk的整个过程实际上就是一次两个shard进行数据交换的过程,发送chunk的一方称为发送方(donorShard),接收chunk的一方称为接收方(recipientShard)。发送方和接收方通过一系列精心设计的步骤来实现chunk的迁移。

image-20240602174250750

完成一次chunk迁移需要进行以下7个步骤:1)发送方发起迁移: configsvr向发送方请求进行指定chunk的迁移任务(同一时刻只能执行一个chunk迁移)。如果此时发现已有一个相同的chunk的迁移任务,跳过此次迁移,否则会新发起一个迁移任务。

2)接收方发起chunk拷贝: 发送方进行迁移参数的校验,校验通过后,向接收方发送recvChunkStart命令,接收方进行一些传送文档数据的初始化工作后,会不断重复地向发送方发送migrateClone命令批量拉取chunk中的文档并将拉取的文档进行批量插入,即进行文档的全量拷贝。

3)接收方拉取增量修改: 文档全量拷贝完成后,接收方通过不断重复发送transferMods命令拉取chunk的增量修改(包括插入、更新、删除),并将其应用到自身。

4)发送方等待接收方chunk拷贝完成: 发送方不断向接收方发送recvChunkStatus命令查询文档增量同步是否完成或超时,当增量同步完成时,表示此时接受方已进入“steady”状态,可以进行接下来的流程。

5)发送方进入临界区: 一旦当接收方的文档数据同步完成,发送方就会进入临界区(critical section),此时发送方接下来的操作不可被打断,并且所有发送方的写操作将被挂起,直到发送方退出临界区。

6)接收方执行commit: 发送方进入临界区后,接下来会同步地调用recvChunkCommit命令给接收方,接收方再一次进行chunk文档的增量同步,同步完成后,向接收方返回同步完成的结果,接收方退出临界区。

7)configsvr执行commit: 接收方收到同步完成的结果后,向configsvr发送configsvrCommitChunkMigration命令,表示迁移完成。(configsvrCommitChunkMigration命令返回前,发送方的读操作会被挂起)

以上便为MongoDB进行chunk迁移的基本步骤,在下一节我们将会从源码层面对迁移流程的每一阶段代码做详细解读。

想看源码的点击下面网址详细查看!

转载自:万字详解,吃透 MongoDB!