MongoDB
2024-10-25 15:41:26 0 举报
AI智能生成
MongoDB是一款强大的非关系型数据库,以其高性能、高可用性和自动扩展性受到广泛关注。它使用JSON-like文档作为数据存储模型,为开发者提供了灵活、易用的数据模型。此外,MongoDB还具备丰富的功能,如索引、聚合、复制和分片等,为各种应用提供了良好的支持。MongoDB适用于Web应用、物联网、游戏、移动应用等多种场景,是现代应用开发中不可或缺的一部分。
作者其他创作
大纲/内容
简介
是什么?
MongoDB 是一个基于分布式文件存储的数据库,由 C++ 编写,可以为WEB 应用提供可扩展、 高性能、易部署的数据存储解决方案。
MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库中功能最丰富、 最像关系数据库的。在高负载的情况下,通过添加更多的节点,可以保证服务器性能。
MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库中功能最丰富、 最像关系数据库的。在高负载的情况下,通过添加更多的节点,可以保证服务器性能。
MongoDB是一个开源OLTP数据库,它灵活的文档模型(JSON)非常适合敏捷式开发、高可用和水平扩展的大数据应用
技术优势
MongoDB基于灵活的JSON文档模型,非常适合敏捷式的快速开发。与此同时,其与生俱来的高可用、高水平扩展能力使得它在处理海量、高并发的数据应用时颇具优势
JSON 结构和对象模型接近,开发代码量低
JSON的动态模型意味着更容易响应新的业务需求
复制集提供99.999%高可用(MongDB的优势: 原生的高可用)
分片架构支持海量数据和无缝扩容(MongDB的优势: 横向扩展能力)
MongoDB 应用场景
- 游戏场景,使用 MongoDB 存储游戏用户信息,用户的装备、积分等直接以内嵌文档的形式存储,方便查询、更新;
- 物流场景,使用 MongoDB 存储订单信息,订单状态在运送过程中会不断更新,以MongoDB 内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来;
- 社交场景,使用 MongoDB 存储存储用户信息,以及用户发表的朋友圈信息,通过地理位置索引实现附近的人、地点等功能;
- 物联网场景,使用 MongoDB 存储所有接入的智能设备信息,以及设备汇报的日志信息,并对这些信息进行多维度的分析;
- 视频直播,使用 MongoDB 存储用户信息、礼物信息等;
- 大数据应用,使用云数据库MongoDB作为大数据的云存储系统,随时进行数据提取分析,掌握行业动态。
MongoDB 和 RDBMS(关系型数据库)对比
尽管这些概念大多与SQL标准定义类似,但MongoDB与传统RDBMS仍然存在不少差异,包括:
- 半结构化:在一个集合中,文档所拥有的字段并不需要是相同的,而且也不需要对所用的字段进行声明。因此,MongoDB具有很明显的半结构化特点。除了松散的表结构,文档还可以支持多级的嵌套、数组等灵活的数据类型,非常契合面向对象的编程模型
- 弱关系:MongoDB没有外键的约束,也没有非常强大的表连接能力。类似的功能需要使用聚合管道技术来弥补
MongoDB 数据模型
BSON协议与数据类型
JSON
JSON(JavaScript Object Notation, JS对象简谱)即JavaScript对象表示法,它是JavaScript对象的一种文本表现形式。
作为一种轻量级的数据交换格式,JSON的可读性非常好,而且非常便于系统生成和解析,这些优势也让它逐渐取代了XML标准在Web领域的地位,当今许多流行的Web应用开发框架,如SpringBoot都选择了JSON作为默认的数据编/解码格式。
作为一种轻量级的数据交换格式,JSON的可读性非常好,而且非常便于系统生成和解析,这些优势也让它逐渐取代了XML标准在Web领域的地位,当今许多流行的Web应用开发框架,如SpringBoot都选择了JSON作为默认的数据编/解码格式。
JSON只定义了6种数据类型:
- string: 字符串
- number : 数值
- object: JS的对象形式,用{key:value}表示,可嵌套
- array: 数组,JS的表示方式[value],可嵌套
- rue/false: 布尔类型
- null: 空值
大多数情况下,使用JSON作为数据交互格式已经是理想的选择,但是JSON基于文本的解析效率并不是最好的,在某些场景下往往会考虑选择更合适的编/解码格式,一些做法如:
- 在微服务架构中,使用gRPC(基于Google的Protobuf)可以获得更好的网络利用率。
- 分布式中间件、数据库,使用私有定制的TCP数据包格式来提供高性能、低延时的计算能力。
BSON
BSON目前主要用于MongoDB数据库。BSON(Binary JSON)是二进制版本的JSON,其在性能方面有更优的表现。BSON在许多方面和JSON保持一致,其同样也支持内嵌的文档对象和数组结构。二者最大的区别在于JSON是基于文本的,而BSON则是二进制(字节流)编/解码的形式。在空间的使用上,BSON相比JSON并没有明显的优势。
MongoDB在文档存储、命令协议上都采用了BSON作为编/解码格式,主要具有如下优势:
- 类JSON的轻量级语义,支持简单清晰的嵌套、数组层次结构,可以实现模式灵活的文档结构。
- 更高效的遍历,BSON在编码时会记录每个元素的长度,可以直接通过seek操作进行元素的内容读取,相对JSON解析来说,遍历速度更快。
- 更丰富的数据类型,除了JSON的基本数据类型,BSON还提供了MongoDB所需的一些扩展类型,比如日期、二进制数据等,这更加方便数据的表示和操作。
BSON的数据类型
MongoDB中,一个BSON文档最大大小为16M,文档嵌套的级别不超过100
$type操作符
$type操作符基于BSON类型来检索集合中匹配的数据类型,并返回结果。
日期类型
MongoDB的日期类型使用UTC(Coordinated Universal Time)进行存储,也就是+0时区的时间
使用new Date与ISODate最终都会生成ISODate类型的字段(对应于UTC时间)
db.dates.insert([{data1:Date()},{data2:new Date()},{data3:ISODate()}])
db.dates.find().pretty()
db.dates.insert([{data1:Date()},{data2:new Date()},{data3:ISODate()}])
db.dates.find().pretty()
ObjectId生成器
MongoDB集合中所有的文档都有一个唯一的_id字段,作为集合的主键。在默认情况下,_id字段使用ObjectId类型,采用16进制编码形式,共12个字节。
- - 4字节 表示Unix时间戳(秒)。
- - 5字节 表示随机数(机器号[3字节]+进程号[2字节]唯一)。
- - 3字节 表示计数器(初始化时随机)。
大多数客户端驱动都会自行生成这个字段,比如MongoDB Java Driver会根据插入的文档是否包含_id字段来自动补充ObjectId对象。这样做不但提高了离散性,还可以降低MongoDB服务器端的计算压力。在ObjectId的组成中,5字节的随机数并没有明确定义,客户端可以采用机器号、进程号来实现
内嵌文档和数组
内嵌文档
数组
嵌套型的数组
数组元素可以是基本类型,也可以是内嵌的文档结构
{
tags:[
{tagKey:xxx,tagValue:xxxx},
{tagKey:xxx,tagValue:xxxx}
]
}
{
tags:[
{tagKey:xxx,tagValue:xxxx},
{tagKey:xxx,tagValue:xxxx}
]
}
这种结构非常灵活,一个很适合的场景就是商品的多属性表示
固定集合(capped collection)
固定集合(capped collection)是一种限定大小的集合,其中capped是覆盖、限额的意思。跟普通的集合相比,数据在写入这种集合时遵循FIFO原则。可以将这种集合想象为一个环状的队列,新文档在写入时会被插入队列的末尾,如果队列已满,那么之前的文档就会被新写入的文档所覆盖。通过固定集合的大小,我们可以保证数据库只会存储“限额”的数据,超过该限额的旧数据都会被丢弃
使用示例
优势
固定集合在底层使用的是顺序I/O操作,而普通集合使用的是随机I/O。顺序I/O在磁盘操作上由于寻道次数少而比随机I/O要高效得多,因此固定集合的写入性能是很高的。此外,如果按写入顺序进行数据读取,也会获得非常好的性能表现。
缺陷
1. 无法动态修改存储的上限,如果需要修改max或size,则只能先执行collection.drop命令,将集合删除后再重新创建。
2. 无法删除已有的数据,对固定集合中的数据进行删除将会得到如下错误:
3. 对已有数据进行修改,新文档大小必须与原来的文档大小一致,否则不允许更新:
4. 默认情况下,固定集合只有一个_id索引,而且最好是按数据写入的顺序进行读取。当然,也可以添加新的索引,但这会降低数据写入的性能。
5. 固定集合不支持分片,同时,在MongoDB 4.2版本中规定了事务中也无法对固定集合执行写操作。
使用场景
固定集合很适合用来存储一些“临时态”的数据。“临时态”意味着数据在一定程度上可以被丢弃。同时,用户还应该更关注最新的数据,随着时间的推移,数据的重要性逐渐降低,直至被淘汰处理。
一些适用的场景如下:
一些适用的场景如下:
- 系统日志,这非常符合固定集合的特征,而日志系统通常也只需要一个固定的空间来存放日志。在MongoDB内部,副本集的同步日志(oplog)就使用了固定集合。
- 存储少量文档,如最新发布的TopN条文章信息。得益于内部缓存的作用,对于这种少量文档的查询是非常高效的。
MongoDB保留的数据库
admin: 从权限的角度来看,这是"root"数据库。要是将一个用户添加到这个数据库,这个用户自动继承所有数据库的权限。一些特定的服务器端命令也只能从这个数据库运行,比如列出所有的数据库或者关闭服务器。
local: 这个数据永远不会被复制,可以用来存储限于本地单台服务器的任意集合
config: 当Mongo用于分片设置时,config数据库在内部使用,用于保存分片的相关信息。
如何考虑是否选择MongoDB?
没有某个业务场景必须要使用MongoDB才能解决,但使用MongoDB通常能让你以更低的成本解决问题。如果你不清楚当前业务是否适合使用MongoDB,可以通过做几道选择题来辅助决策。(只要有一项需求满足就可以考虑使用MongoDB,匹配越多,选择MongoDB越合适。)
MongoDB 命令
基础命令
集合数据操作(CRUD)
添加文档
单个
批量
查询文档
find
findOne
比较条件查询
db.集合名.find(条件)
指定条件查询
查询条件对照表
逻辑条件查询
查询逻辑运算符
- $lt: 存在并小于
- $lte: 存在并小于等于
- $gt: 存在并大于
- $gte: 存在并大于等于
- $ne: 不存在或存在但不等于
- $in: 存在并在指定数组中
- $nin: 不存在或不在指定数组中
- $or: 匹配两个或多个条件中的一个
- $and: 匹配全部条件
分页查询
db.集合名.find({条件}).sort({排序字段:排序方式})).skip(跳过的行数).limit(一页显示多少数据)
sort : 1 升序 -1 降序
正则表达式
更新文档
更新操作符
update命令的选项配置较多,为了简化使用还可以使用一些快捷命令
- updateOne:更新单个文档。
- updateMany:更新多个文档。
- replaceOne:替换单个文档。
replace语义
findAndModify命令
findAndModify兼容了查询和修改指定文档的功能,findAndModify只能更新单个文档
删除文档
remove
delete
返回被删除文档
集合聚合操作
聚合是MongoDB的高级查询语言,它允许我们通过转化合并由多个文档的数据来生成新的在单个文档 里不存在的文档信息。一般都是将记录按条件分组之后进行一系列求最大值,最小值,平均值的简单操 作,也可以对记录进行复杂数据统计,数据挖掘的操作。聚合操作的输入是集中的文档,输出可以是一 个文档也可以是多个文档。
单一作用聚合
提供了对常见聚合过程的简单访问,操作都从单个集合聚合文档
注意:在分片群集上,如果存在孤立文档或正在进行块迁移,则db.collection.count()没有查询谓词可能导致计数不准确。要避免这些情况,请在分片群集上使用 db.collection.aggregate()方法。
聚合管道(Aggregation Pipeline)
MongoDB 聚合框架(Aggregation Framework)是一个计算框架,它可以:
- 作用在一个或几个集合上;
- 对集合中的数据进行的一系列运算;
- 将这些数据转化为期望的形式;
管道(Pipeline)和阶段(Stage)
整个聚合运算过程称为管道(Pipeline),它是由多个阶段(Stage)组成的, 每个管道:
- 接受一系列文档(原始数据);
- 每个阶段对这些文档进行一系列运算;
- 结果文档输出给下一个阶段
常用的管道聚合阶段
$project
$match
$count
计数并返回与查询匹配的结果数
$group
accumulator操作符
$group阶段的内存限制为100M。默认情况下,如果stage超过此限制,$group将产生错误。但是,要允许处理大型数据集,请将allowDiskUse选项设置为true以启用$group操作以写入临时文件。
$unwind:将数组拆分为单独的文档
$limit
$skip
$sort
$lookup:关联查询
MapReduce 编程模型
Pipeline查询速度快于MapReduce,但是MapReduce的强大之处在于能够在多台Server上并行执行复杂的聚合逻辑。MongoDB不允许Pipeline的单个聚合操作占用过多的系统内存,如果一个聚合操作消耗20%以上的内存,那么MongoDB直接停止操作,并向客户端输出错误消息
MapReduce是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结 果合并成最终结果(REDUCE)。
从MongoDB 5.0开始,map-reduce操作已被弃用。聚合管道比映射-reduce操作提供更好的性能和可用性。Map-reduce操作可以使用聚合管道操作符重写,例如$group、$group、$merge等。
体系结构
MongoDB和 RDBMS(关系型数据库)对比
什么是BSON
数据类型
MongDB的保留库
admin
local
config
ObjectId类型
MongoDB索引
索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构,它是某个表 中一列或若干列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。索引的作 用相当于图书的目录,可以根据目录中的页码快速找到所需的内容。索引目标是提高数据库的查询效率,没有索引的话,查询会进行全表扫描(scan every document in a collection), 数据量 大时严重降低了查询效率。默认情况下Mongo在一个集合(collection)创建时,自动地对集合 的_id创建了唯一索引。
B+Tree就是一种常用的数据库索引数据结构,MongoDB采用B+Tree 做索引,索引创建在colletions上。MongoDB不使用索引的查询,先扫描所有的文档,再匹配符合条件的文档。 使用索引的查询,通过索引找到文档,使用索引能够极大的提升查询效率(回表)。
B+Tree就是一种常用的数据库索引数据结构,MongoDB采用B+Tree 做索引,索引创建在colletions上。MongoDB不使用索引的查询,先扫描所有的文档,再匹配符合条件的文档。 使用索引的查询,通过索引找到文档,使用索引能够极大的提升查询效率(回表)。
MongoDB底层索引结构
B+树
WiredTiger数据文件在磁盘的存储结构
- B+ Tree中的leaf page包含一个页头(page header)、块头(block header)和真正的数据(key/value),其中页头定义了页的类型、页中实际载荷数据的大小、页中记录条数等信息;块头定义了此页的checksum、块在磁盘上的寻址位置等信息。
- WiredTiger有一个块设备管理的模块,用来为page分配block。如果要定位某一行数据(key/value)的位置,可以先通过block的位置找到此page(相对于文件起始位置的偏移量),再通过page找到行数据的相对位置,最后可以得到行数据相对于文件起始位置的偏移量offsets。
索引分类
- 按照索引包含的字段数量,可以分为单键索引和组合索引(或复合索引)。
- 按照索引字段的类型,可以分为主键索引和非主键索引。
- 按照索引节点与物理记录的对应方式来分,可以分为聚簇索引和非聚簇索引,其中聚簇索引是指索引节点上直接包含了数据记录,而后者则仅仅包含一个指向数据记录的指针。
- 按照索引的特性不同,又可以分为唯一索引、稀疏索引、文本索引、地理空间索引等
索引设计原则
1、每个查询原则上都需要创建对应索引
2、单个索引设计应考虑满足尽量多的查询
3、索引字段选择及顺序需要考虑查询覆盖率及选择性
4、对于更新及其频繁的字段上创建索引需慎重
5、对于数组索引需要慎重考虑未来元素个数
6、对于超长字符串类型字段上慎用索引
7、并发更新较高的单个集合上不宜创建过多索引
索引类型
单键索引
普通索引
唯一索引
db.集合名.ensureIndex({"字段名":排序方式},{unique: true})
索引TTL
TTL索引是MongoDB中一种特殊的索引, 可以支持文档在一定时间之后自动过期删除,目前TTL索引 只能在单字段上建立,并且字段类型必须是日期类型。
组合索引
制作复合索引时要注意的重要事项包括:字段顺序与索引方向
db.集合名.createIndex( { "字段名1" : 排序方式, "字段名2" : 排序方式 } )
多键索引
在数组的属性上建立索引,MongoDB支持针对数组中每一个element创建索引,Multikey indexes支持strings,numbers和nested documents
复合索引是多个字段的组合,而多键索引则仅仅是在一个字段上出现了多键(multi key)。而实质上,多键索引也可以出现在复合字段上
地理空间索引(Geospatial Index)
2dsphere索引(球面上的点)
2d索引(平面上的点)
全文检索引
MongoDB提供了针对string内容的文本查询,Text Index支持任意属性值为string或string数组元素的 索引查询。注意:一个集合仅支持最多一个Text Index,中文分词不理想推荐ES。
db.集合.createIndex({"字段": "text"})
db.集合.find({"$text": {"$search": "coffee"}})
db.集合.find({"$text": {"$search": "coffee"}})
哈希索引 Hashed Index
针对属性的哈希值进行索引查询,当要使用Hashed index时,MongoDB能够自动的计算hash值,无需程序计算hash值。注:hash index仅支持等于查询,不支持范围查询(Hash是散列算法)
通配符索引 Wildcard Indexes
MongoDB的文档模式是动态变化的,而通配符索引可以建立在一些不可预知的字段上,以此实现查询的加速。MongoDB 4.2 引入了通配符索引来支持对未知或任意字段的查询。
注意事项
通配符索引不兼容的索引类型或属性
通配符索引是稀疏的,不索引空字段。因此,通配符索引不能支持查询字段不存在的文档。
通配符索引为文档或数组的内容生成条目,而不是文档/数组本身。因此通配符索引不能支持精确的文档/数组相等匹配。通配符索引可以支持查询字段等于空文档{}的情况
索引属性
唯一索引(Unique Indexes)
- 唯一性索引对于文档中缺失的字段,会使用null值代替,因此不允许存在多个文档缺失索引字段的情况。
- 对于分片的集合,唯一性约束必须匹配分片规则。换句话说,为了保证全局的唯一性,分片键必须作为唯一性索引的前缀字段。
部分索引(Partial Indexes)
部分索引仅对满足指定过滤器表达式的文档进行索引。通过在一个集合中为文档的一个子集建立索引,部分索引具有更低的存储需求和更低的索引创建和维护的性能成本。3.2新版功能。
部分索引提供了稀疏索引功能的超集,应该优先于稀疏索引。
唯一约束结合部分索引使用导致唯一约束失效的问题
注意:如果同时指定了partialFilterExpression和唯一约束,那么唯一约束只适用于满足筛选器表达式的文档。如果文档不满足筛选条件,那么带有惟一约束的部分索引不会阻止插入不满足惟一约束的文档。
注意:如果同时指定了partialFilterExpression和唯一约束,那么唯一约束只适用于满足筛选器表达式的文档。如果文档不满足筛选条件,那么带有惟一约束的部分索引不会阻止插入不满足惟一约束的文档。
案例
稀疏索引(Sparse Indexes)
索引的稀疏属性确保索引只包含具有索引字段的文档的条目,索引将跳过没有索引字段的文档。
特性: 只对存在字段的文档进行索引(包括字段值为null的文档)
特性: 只对存在字段的文档进行索引(包括字段值为null的文档)
如果稀疏索引会导致查询和排序操作的结果集不完整,MongoDB将不会使用该索引,除非hint()明确指定索引。
案例
TTL索引(TTL Indexes)
在一般的应用系统中,并非所有的数据都需要永久存储。例如一些系统事件、用户消息等,这些数据随着时间的推移,其重要程度逐渐降低。更重要的是,存储这些大量的历史数据需要花费较高的成本,项目中通常会对过期且不再使用的数据进行老化处理
通常的做法如下:
通常的做法如下:
- 方案一:为每个数据记录一个时间戳,应用侧开启一个定时器,按时间戳定期删除过期的数据。
- 方案二:数据按日期进行分表,同一天的数据归档到同一张表,同样使用定时器删除过期的表。
对于数据老化,MongoDB提供了一种更加便捷的做法:TTL(Time To Live)索引。TTL索引需要声明在一个日期类型的字段中,TTL 索引是特殊的单字段索引,MongoDB 可以使用它在一定时间或特定时钟时间后自动从集合中删除文档。
对集合创建TTL索引之后,MongoDB会在周期性运行的后台线程中对该集合进行检查及数据清理工作。除了数据老化功能,TTL索引具有普通索引的功能,同样可以用于加速数据的查询
TTL 索引不保证过期数据会在过期后立即被删除。文档过期和 MongoDB 从数据库中删除文档的时间之间可能存在延迟。删除过期文档的后台任务每 60 秒运行一次。因此,在文档到期和后台任务运行之间的时间段内,文档可能会保留在集合中
TTL 索引不保证过期数据会在过期后立即被删除。文档过期和 MongoDB 从数据库中删除文档的时间之间可能存在延迟。删除过期文档的后台任务每 60 秒运行一次。因此,在文档到期和后台任务运行之间的时间段内,文档可能会保留在集合中
可变的过期时间
TTL索引在创建之后,仍然可以对过期时间进行修改。这需要使用collMod命令对索引的定义进行变更
使用约束
TTL索引只能支持单个字段,并且必须是非_id字段。
TTL索引不能用于固定集合。
TTL索引无法保证及时的数据老化,MongoDB会通过后台的TTLMonitor定时器来清理老化数据,默认的间隔时间是1分钟。当然如果在数据库负载过高的情况下,TTL的行为则会进一步受到影响。
TTL索引对于数据的清理仅仅使用了remove命令,这种方式并不是很高效。因此TTL Monitor在运行期间对系统CPU、磁盘都会造成一定的压力。相比之下,按日期分表的方式操作会更加高效。
一般的日志存储?
- 日期分表
- 固定集合
- TTL索引
隐藏索引(Hidden Indexes)
隐藏索引对查询规划器不可见,不能用于支持查询。通过对规划器隐藏索引,用户可以在不实际删除索引的情况下评估删除索引的潜在影响。如果影响是负面的,用户可以取消隐藏索引,而不必重新创建已删除的索引。4.4新版功能
索引使用建议
1、为每一个查询建立合适的索引
这个是针对于数据量较大比如说超过几十上百万(文档数目)数量级的集合。如果没有索引MongoDB需要把所有的Document从盘上读到内存,这会对MongoDB服务器造成较大的压力并影响到其他请求的执行。
2、创建合适的复合索引,不要依赖于交叉索引
如果你的查询会使用到多个字段,MongoDB有两个索引技术可以使用:交叉索引和复合索引。交叉索引就是针对每个字段单独建立一个单字段索引,然后在查询执行时候使用相应的单字段索引进行索引交叉而得到查询结果。交叉索引目前触发率较低,所以如果你有一个多字段查询的时候,建议使用复合索引能够保证索引正常的使用
3、复合索引字段顺序:匹配条件在前,范围条件在后(Equality First, Range After)
前面的例子,在创建复合索引时如果条件有匹配和范围之分,那么匹配条件(sport: “marathon”) 应该在复合索引的前面。范围条件(age: <30)字段应该放在复合索引的后面。
4、尽可能使用覆盖索引(Covered Index)
建议只返回需要的字段,同时,利用覆盖索引来提升性能。
5、建索引要在后台运行
在对一个集合创建索引时,该集合所在的数据库将不接受其他读写操作。对大数据量的集合建索引,建议使用后台运行选项 {background: true}
6、避免设计过长的数组索引
数组索引是多值的,在存储时需要使用更多的空间。如果索引的数组长度特别长,或者数组的增长不受控制,则可能导致索引空间急剧膨胀。
索引Explain分析(TODO)
参数
queryPlanner:执行计划的详细信息,包括查询计划、集合信息、查询条件、最佳执行计划、查询方式和 MongoDB 服务信息等
executionStats
allPlansExecution
慢查询分析
1、开启内置的查询分析器,记录读写操作效率 db.setProfilingLevel(n,m)
2、查询监控结果 db.system.profile.find().sort({millis:-1}).limit(3)
3、分析慢速查询
4、解读explain结果
MongoDB应用
适用场景
网站数据:Mongo 非常适合实时的插入,更新与查询,并具备网站实时数据存储所需的复制及高度伸缩性。
缓存:由于性能很高,Mongo 也适合作为信息基础设施的缓存层。在系统重启之后,由Mongo 搭建的持久化缓存层可以避免下层的数据源过载。
大尺寸、低价值的数据:使用传统的关系型数据库存储一些大尺寸低价值数据时会比较浪费, 在此之前,很多时候程序员往往会选择传统的文件进行存储。
高伸缩性的场景:Mongo 非常适合由数十或数百台服务器组成的数据库,Mongo 的路线图中 已经包含对MapReduce 引擎的内置支持以及集群高可用的解决方案。
用于对象及JSON 数据的存储:Mongo 的BSON 数据格式非常适合文档化格式的存储及查询
行业具体应用场景
游戏场景,使用 MongoDB 存储游戏用户信息,用户的装备、积分等直接以内嵌文档的形式存储,方便查询、更新。
物流场景,使用 MongoDB 存储订单信息,订单状态在运送过程中会不断更新,以 MongoDB 内 嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来。
社交场景,使用 MongoDB 存储存储用户信息,以及用户发表的朋友圈信息,通过地理位置索引 实现附近的人、地点等功能。
物联网场景,使用 MongoDB 存储所有接入的智能设备信息,以及设备汇报的日志信息,并对这 些信息进行多维度的分析
直播,使用 MongoDB 存储用户信息、礼物信息等
操作MongoDB
Java
Spring
Springboot
Change Streams
Change Stream指数据的变化事件流,MongoDB从3.6版本开始提供订阅数据变更的功能。
Change Stream 是 MongoDB 用于实现变更追踪的解决方案,类似于关系数据库的触发器,但原理不完全相同
Change Stream 是 MongoDB 用于实现变更追踪的解决方案,类似于关系数据库的触发器,但原理不完全相同
Change Streams 的实现原理
Change Stream 是基于 oplog 实现的,提供推送实时增量的推送功能。它在 oplog 上开启一个 tailable cursor 来追踪所有复制集上的变更操作,最终调用应用中定义的回调函数。
被追踪的变更事件主要包括:
被追踪的变更事件主要包括:
- insert/update/delete:插入、更新、删除;
- drop:集合被删除;
- rename:集合被重命名;
- dropDatabase:数据库被删除;
- invalidate:drop/rename/dropDatabase 将导致 invalidate 被触发, 并关闭 change stream;
过滤变更事件
Change Stream会采用 "readConcern:majority" 这样的一致性级别,保证写入的变更不会被回滚。因此:
- 未开启 majority readConcern 的集群无法使用 Change Stream;
- 当集群无法满足 {w: “majority”} 时,不会触发 Change Stream(例如 PSA 架构 中的 S 因故障宕机)。
Change Streams 故障恢复
假设在一系列写入操作的过程中,订阅 Change Stream 的应用在接收到“写3”之后 于 t0 时刻崩溃,重启后后续的变更怎么办?
想要从上次中断的地方继续获取变更流,只需要保留上次变更通知中的 _id 即可。 Change Stream 回调所返回的的数据带有 _id,这个 _id 可以用于断点恢复。例如:
var cs = db.collection.watch([], {resumeAfter: <_id>})
即可从上一条通知中断处继续获取后续的变更通知。
var cs = db.collection.watch([], {resumeAfter: <_id>})
即可从上一条通知中断处继续获取后续的变更通知。
使用场景
- 跨集群的变更复制——在源集群中订阅 Change Stream,一旦得到任何变更立即写 入目标集群。
- 微服务联动——当一个微服务变更数据库时,其他微服务得到通知并做出相应的变更。
- 其他任何需要系统联动的场景。
案例 1.监控
用户需要及时获取变更信息(例如账户相关的表),ChangeStreams 可以提供监控功能,一旦相关的表信息发生变更,就会将变更的消息实时推送出去
案例 2.分析平台
例如需要基于增量去分析用户的一些行为,可以基于 ChangeStreams 把数据拉出来,推到下游的计算平台, 比如 类似 Flink、Spark 等计算平台等等。
案例 3.数据同步
基于 ChangeStreams,用户可以搭建额外的 MongoDB 集群,这个集群是从原端的 MongoDB 拉取过来的, 那么这个集群可以做一个热备份,假如源端集群发生网络不通等等之类的变故,备集群就可以接管服务。 还可以做一个冷备份,如用户基于 ChangeStreams 把数据同步到文件,万一源端数据库发生不可服务, 就可以从文件里恢复出完整的 MongoDB 数据库, 继续提供服务。(当然,此处还需要借助定期全量备份来一同完成恢复) 另外数据同步它不仅仅局限于同一地域,可以跨地域,从北京到上海甚至从中国到美国等等。
案例 4.消息推送
假如用户想实时了解公交车的信息,那么公交车的位置每次变动,都实时推送变更的信息给想了解的用 户,用户能够实时收到公交车变更的数据,非常便捷实用。
注意事项
- Change Stream 依赖于 oplog,因此中断时间不可超过 oplog 回收的最大时间窗
- 在执行 update 操作时,如果只更新了部分数据,那么 Change Stream 通知的也是增量部分;
- 删除数据时通知的仅是删除数据的 _id。
MongoShell 测试
变更字段说明
Change Streams 整合Spring Boot
如何抉择是否需要适用MongoDB?
MongoDB架构(TODO)
MongoDB逻辑结构
MongoDB 与 MySQL 中的架构相差不多,底层都使用了可插拔的存储引擎以满足用户的不同需要。用户可以根据程序的数据特征选择不同的存储引擎,在最新版本的 MongoDB 中使用了 WiredTiger 作为默认的存储引擎,WiredTiger 提供了不同粒度的并发控制和压缩机制,能够为不同种类的应用提供了最好的性能和存储率。
在存储引擎上层的就是 MongoDB 的数据模型和查询语言了,由于 MongoDB 对数据的存储与 RDBMS 有较大的差异,所以它创建了一套不同的数据模型和查询语言
在存储引擎上层的就是 MongoDB 的数据模型和查询语言了,由于 MongoDB 对数据的存储与 RDBMS 有较大的差异,所以它创建了一套不同的数据模型和查询语言
MongoDB的数据模型
描述数据模型
内嵌:内嵌的方式指的是把相关联的数据保存在同一个文档结构之中。MongoDB的文档结构允许一个字 段或者一个数组内的值作为一个嵌套的文档。
引用: 引用方式通过存储数据引用信息来实现两个不同文档之间的关联,应用程序可以通过解析这些数据引 用来访问相关数据。
如何选择数据模型
选择内嵌
1. 数据对象之间有包含关系 ,一般是数据对象之间有一对多或者一对一的关系 。
2. 需要经常一起读取的数据。
3. 有 map-reduce/aggregation 需求的数据放在一起,这些操作都只能操作单个 collection
2. 需要经常一起读取的数据。
3. 有 map-reduce/aggregation 需求的数据放在一起,这些操作都只能操作单个 collection
选择引用
1. 当内嵌数据会导致很多数据的重复,并且读性能的优势又不足于覆盖数据重复的弊端 。
2. 需要表达比较复杂的多对多关系的时候 。
3. 大型层次结果数据集 嵌套不要太深
2. 需要表达比较复杂的多对多关系的时候 。
3. 大型层次结果数据集 嵌套不要太深
MongoDB 存储引擎
- B-Tree:像Oracle、SQL Server、DB2、MySQL (InnoDB)和PostgreSQL这些传统的关系数据库依赖的底层存储引擎是基于B-Tree开发的;
- LSM Tree:像Cassandra、Elasticsearch (Lucene)、Google Bigtable、Apache HBase、LevelDB和RocksDB这些当前比较流行的NoSQL数据库存储引擎是基于LSM开发的。
存储引擎概述
存储引擎是MongoDB的核心组件,负责管理数据如何存储在硬盘和内存上。MongoDB支持的存储引擎有 MMAPv1 ,WiredTiger 和 InMemory
- InMemory存储引擎:用于将数据只存储在内存中,只将少量的元数据 (meta-data)和诊断日志(Diagnostic)存储到硬盘文件中,由于不需要Disk的IO操作,就能获取所需的数据,InMemory存储引擎大幅度降低了数据查询的延迟(Latency)。
- 从mongodb3.2开始默认的存储引擎是WiredTiger,3.2版本之前的默认存储引擎是MMAPv1,mongodb4.x版本不再支持MMAPv1存储引擎。
WiredTiger存储引擎优势
文档空间分配方式
WiredTiger使用的是B-Tree存储
MMAPV1采用的预分配,返回在将文档放进去,这个中间的差值需要借用padding的方式给补齐。 线性存储
并发级别
WiredTiger:文档级别锁
MMAPV1引擎:使用表级锁
数据压缩
snappy (默认) 和 zlib , 相比MMAPV1(无压缩) 空间节省数倍。
内存使用
WiredTiger 可以指定内存的使用大小。
MMAPV1使用的是系统的内存,不受限制。
MMAPV1使用的是系统的内存,不受限制。
Cache使用
WiredTiger 引擎使用了二阶缓存WiredTiger Cache,File System Cache来保证Disk上的数据的最终一 致性。
MMAPv1 只有journal 日志
WiredTiger引擎包含的文件和作用
- WiredTiger.basecfg:存储基本配置信息,与 ConfigServer有关系
- WiredTiger.lock: 定义锁操作
- table*.wt: 存储各张表的数据
- WiredTiger.wt:存储 table* 的元数据
- WiredTiger.turtle: 存储WiredTiger.wt的元数据
- journal:存储WAL(Write Ahead Log)
WiredTiger存储引擎实现原理
读请求
理想情况下,MongoDB可以提供近似内存式的读写性能。WiredTiger引擎实现了数据的二级缓存,第一层是操作系统的页面缓存,第二层则是引擎提供的内部缓存。
读取数据时的流程如下:
- 数据库发起Buffer I/O读操作,由操作系统将磁盘数据页加载到文件系统的页缓存区。
- 引擎层读取页缓存区的数据,进行解压后存放到内部缓存区。
- 在内存中完成匹配查询,将结果返回给应用。
MongoDB为了尽可能保证业务查询的“热数据”能快速被访问,其内部缓存的默认大小达到了内存的一半,该值由wiredTigerCacheSize参数指定,其默认的计算公式如下:
wiredTigerCacheSize=Math.max(0.5*(RAM-1GB),256MB)
wiredTigerCacheSize=Math.max(0.5*(RAM-1GB),256MB)
写请求
当数据发生写入时,MongoDB并不会立即持久化到磁盘上,而是先在内存中记录这些变更,之后通过CheckPoint机制将变化的数据写入磁盘。为什么要这么处理?主要有以下两个原因:
- 如果每次写入都触发一次磁盘I/O,那么开销太大,而且响应时延会比较大。
- 多个变更的写入可以尽可能进行I/O合并,降低资源负荷
WiredTiger的写操作会默认写入 Cache ,并持久化到 WAL (Write Ahead Log),每60s或Log文件达到2G 做一次 checkpoint (当然我们也可以通过在写入时传入 j: true 的参数强制 journal 文件的同步 , writeConcern { w: , j: , wtimeout: }) 产生快照文件。WiredTiger初始化时,恢复至最新的快照状态,然后再根据WAL 恢复数据,保证数据的完整性。
WAL是引擎内部的一个理念,到了外部就叫 journal,journal存储的就是WAL
WAL是引擎内部的一个理念,到了外部就叫 journal,journal存储的就是WAL
Cache是基于BTree的,节点是一个page,root page是根节点,internal page是中间索引节点,leaf page真正存储数据,数据以page为单位读写。
WiredTiger采用Copy on write的方式管理写操作 (insert、update、delete),写操作会先缓存在cache里,持久化时,写操作不会在原来的leaf page 上进行,而是写入新分配的page(copy的页),每次checkpoint都会产生一个新的root page(也会生成一个新的 snapshosts)
WiredTiger采用Copy on write的方式管理写操作 (insert、update、delete),写操作会先缓存在cache里,持久化时,写操作不会在原来的leaf page 上进行,而是写入新分配的page(copy的页),每次checkpoint都会产生一个新的root page(也会生成一个新的 snapshosts)
checkpoint流程
1. 对所有的table进行一次checkpoint,每个table的checkpoint的元数据更新至WiredTiger.wt
2. 对WiredTiger.wt进行checkpoint,将该table checkpoint的元数据更新至临时文件 WiredTiger.turtle.set
3. 将WiredTiger.turtle.set重命名为WiredTiger.turtle。
4. 上述过程如果中间失败,WiredTiger在下次连接初始化时,首先将数据恢复至最新的快照状态,然后根据WAL恢复数据,以保证存储可靠性。
Journaling
Journal是一种预写式日志(write ahead log)机制,主要用来弥补CheckPoint机制的不足。如果开启了Journal日志,那么WiredTiger会将每个写操作的redo日志写入Journal缓冲区,该缓冲区会频繁地将日志持久化到磁盘上。默认情况下,Journal缓冲区每100ms执行一次持久化。此外,Journal日志达到100MB,或是应用程序指定journal:true,写操作都会触发日志的持久化。 一旦MongoDB发生宕机,重启程序时会先恢复到上一个检查点,然后根据Journal日志恢复增量的变化。由于Journal日志持久化的间隔非常短,数据能得到更高的保障,如果按照当前版本的默认配置,则其在断电情况下最多会丢失100ms的写入数据
WiredTiger写入数据的流程
1. 应用向MongoDB写入数据(插入、修改或删除)。
2. 数据库从内部缓存中获取当前记录所在的页块,如果不存在则会从磁盘中加载(Buffer I/O)
3. WiredTiger开始执行写事务,修改的数据写入页块的一个更新记录表,此时原来的记录仍然保持不变。
4. 如果开启了Journal日志,则在写数据的同时会写入一条Journal日志(Redo Log)。该日志在最长不超过100ms之后写入磁盘
5. 数据库每隔60s执行一次CheckPoint操作,此时内存中的修改会真正刷入磁盘。
2. 数据库从内部缓存中获取当前记录所在的页块,如果不存在则会从磁盘中加载(Buffer I/O)
3. WiredTiger开始执行写事务,修改的数据写入页块的一个更新记录表,此时原来的记录仍然保持不变。
4. 如果开启了Journal日志,则在写数据的同时会写入一条Journal日志(Redo Log)。该日志在最长不超过100ms之后写入磁盘
5. 数据库每隔60s执行一次CheckPoint操作,此时内存中的修改会真正刷入磁盘。
- Journal日志的刷新周期可以通过参数 storage.journal.commitIntervalMs 指定,MongoDB 3.4及以下版本的默认值是50ms,而3.6版本之后调整到了100ms。由于Journal日志采用的是顺序I/O写操作,频繁地写入对磁盘的影响并不是很大。
- CheckPoint的刷新周期可以调整 storage.syncPeriodSecs 参数(默认值60s),在MongoDB 3.4及以下版本中,当Journal日志达到2GB时同样会触发CheckPoint行为。如果应用存在大量随机写入,则CheckPoint可能会造成磁盘I/O的抖动。在磁盘性能不足的情况下,问题会更加显著,此时适当缩短CheckPoint周期可以让写入平滑一些。
在数据库宕机时 , 为保证 MongoDB 中数据的持久性,MongoDB 使用了 Write Ahead Logging(WAL) 向磁盘上的 journal 文件预先进行写入。除了 journal 日志,MongoDB 还使用检查点(checkpoint)来保证数据的一致性,当数据库发生宕机时,我们就需要 checkpoint 和 journal 文件协作完成数据的恢复工作。
- 在数据文件中查找上一个检查点的标识符
- 在 journal 文件中查找标识符对应的记录
- 重做对应记录之后的全部操作
WT引擎: 数据结构
B-Tree数据结构 : B-Tree是为磁盘或其它辅助存储设备而设计的一种数据结构,目的是为了在查找数据的过程中减少磁盘I/O的次数
在整个B-Tree中,从上往下依次为Root结点、内部结点和叶子结点,每个结点就是一个Page,数据以Page为单位在内存和磁盘间进行调度,每个Page的大小决定了相应结点的分支数量,每条索引记录会包含一个数据指针,指向一条数据记录所在文件的偏移量。
如上图,假设每个结点100个分支,那么所有叶子结点合起来可以包含100万个键值(等于100100100)。通常情况下Root结点和内部结点的Page会驻留在内存中,所以查找一条数据可能只需2次磁盘I/O。但随着数据不断的插入、删除,会涉及到B-Tree结点的分裂、位置提升及合并等操作,因此维护一个B-Tree的平衡也是比较耗时的。
如上图,假设每个结点100个分支,那么所有叶子结点合起来可以包含100万个键值(等于100100100)。通常情况下Root结点和内部结点的Page会驻留在内存中,所以查找一条数据可能只需2次磁盘I/O。但随着数据不断的插入、删除,会涉及到B-Tree结点的分裂、位置提升及合并等操作,因此维护一个B-Tree的平衡也是比较耗时的。
WiredTiger数据文件在磁盘上的数据结构(B+树)
对于WiredTiger存储引擎来说,集合所在的数据文件和相应的索引文件都是按B-Tree结构来组织的,不同之处在于数据文件对应的B-Tree叶子结点上除了存储键名外(keys),还会存储真正的集合数据(values),所以数据文件的存储结构也可以认为是一种B+Tree,其整体结构如下图所示:
从上图可以看到,B+ Tree中的leaf page包含一个页头(page header)、块头(block header)和真正的数据(key/value),其中页头定义了页的类型、页中实际载荷数据的大小、页中记录条数等信息;块头定义了此页的checksum、块在磁盘上的寻址位置等信息。
WiredTiger有一个块设备管理的模块,用来为page分配block。如果要定位某一行数据(key/value)的位置,可以先通过block的位置找到此page(相对于文件起始位置的偏移量),再通过page找到行数据的相对位置,最后可以得到行数据相对于文件起始位置的偏移量offsets。由于offsets是一个8字节大小的变量,所以WiredTiger磁盘文件的大小,其最大值可以非常大(264bit)。
WiredTiger有一个块设备管理的模块,用来为page分配block。如果要定位某一行数据(key/value)的位置,可以先通过block的位置找到此page(相对于文件起始位置的偏移量),再通过page找到行数据的相对位置,最后可以得到行数据相对于文件起始位置的偏移量offsets。由于offsets是一个8字节大小的变量,所以WiredTiger磁盘文件的大小,其最大值可以非常大(264bit)。
WiredTiger内存上的基础数据结构
WiredTiger会按需将磁盘的数据以page为单位加载到内存,同时在内存会构造相应的B-Tree来存储这些数据。为了高效的支撑CRUD等操作以及将内存里面发生变化的数据持久化到磁盘上,WiredTiger也会在内存里面维护其它几种数据结构,如下图所示:
上图是WiredTiger在内存里面的大概布局图,通过它我们可梳理清楚存储引擎是如何将数据加载到内存,然后如何通过相应数据结构来支持查询、插入、修改操作的。
- 内存里面B-Tree包含三种类型的page,即rootpage、internal page和 leaf page,前两者包含指向其子页的page index指针,不包含集合中的真正数据,leaf page包含集合中的真正数据即keys/values和指向父页的home指针;
- 内存上的 leaf page会维护一个WT_ROW结构的数组变量,将保存从磁盘leaf page读取的keys/values值,每一条记录还有一个cell_offset变量,表示这条记录在page上的偏移量;
- 内存上的leaf page会维护一个WT_UPDATE结构的数组变量,每条被修改的记录都会有一个数组元素与之对应,如果某条记录被多次修改,则会将所有修改值以链表形式保存。
- 内存上的leaf page会维护一个WT_INSERT_HEAD结构的数组变量,具体插入的data会保存在WT_INSERT_HEAD结构中的WT_UPDATE属性上,且通过key属性的offset和size可以计算出此条记录待插入的位置;同时,为了提高寻找待插入位置的效率,每个WT_INSERT_HEAD变量以跳转链表的形式构成。
page的其它数据结构
对于一个面向行存储的leaf page来说,包含的数据结构除了上面提到的WT_ROW(keys/values)、WT_UPDATE(修改数据)、WT_INSERT_HEAD(插入数据)外,还有如下几种重要的数据结构:
WT_PAGE_MODIFY
保存page上事务、脏数据字节大小等与page修改相关的信息
read_gen
page的read generation值作为evict page时使用,具体来说对应page在LRU队列中的位置,决定page被evict server选中淘汰出去的先后顺序。
WT_PAGE_LOOKASIDE
page关联的lookasidetable数据。当对一个page进行reconcile时,如果系统中还有之前的读操作正在访问此page上修改的数据,则会将这些数据保存到lookasidetable;当page再被读时,可以利用lookasidetable中的数据重新构建内存page
WT_ADDR
page被成功reconciled后,对应的磁盘上块的地址,将按这个地址将page写到磁盘,块是最小磁盘上文件的最小分配单元,一个page可能有多个块。
checksum
page的校验和,如果page从磁盘读到内存后没有任何修改,比较checksum可以得到相等结果,那么后续reconcile这个page时,不会将这个page的再重新写入磁盘
WT引擎: Page生命周期
为什么要了解Page生命周期
数据以page为单位加载到cache、cache里面又会生成各种不同类型的page及为不同类型的page分配不同大小的内存、eviction触发机制和reconcile动作都发生在page上、page大小持续增加时会被分割成多个小page,所有这些操作都是围绕一个page来完成的
Page的生命周期
- 第一步:pages从磁盘读到内存;
- 第二步:pages在内存中被修改;
- 第三步:被修改的脏pages在内存被reconcile,完成后将discard这些pages
- 第四步:pages被选中,加入淘汰队列,等待被evict线程淘汰出内存;
- 第五步:evict线程会将“干净“的pages直接从内存丢弃(因为相对于磁盘page来说没做任何修改),将经过reconcile处理后的磁盘映像写到磁盘再丢弃“脏的”pages。
pages的状态是在不断变化的,因此,对于读操作来说,它首先会检查pages的状态是否为WT_REF_MEM,然后设置一个hazard指针指向要读的pages,如果刷新后,pages的状态仍为WT_REF_MEM,读操作才能继续处理。
与此同时,evict线程想要淘汰pages时,它会先锁住pages,即将pages的状态设为WT_REF_LOCKED,然后检查pages上是否有读操作设置的hazard指针,如有,说明还有线程正在读这个page则停止evict,重新将page的状态设置为WT_REF_MEM;如果没有,则pages被淘汰出去
与此同时,evict线程想要淘汰pages时,它会先锁住pages,即将pages的状态设为WT_REF_LOCKED,然后检查pages上是否有读操作设置的hazard指针,如有,说明还有线程正在读这个page则停止evict,重新将page的状态设置为WT_REF_MEM;如果没有,则pages被淘汰出去
Page的各种状态
针对一页page的每一种状态,详细描述如下:
- WT_REF_DISK: 初始状态,page在磁盘上的状态,必须被读到内存后才能使用,当page被evict后,状态也会被设置为这个。
- WT_REF_DELETED: page在磁盘上,但是已经从内存B-Tree上删除,当我们不在需要读某个leaf page时,可以将其删除。
- WT_REF_LIMBO: page的映像已经被加载到内存,但page上还有额外的修改数据在lookasidetable上没有被加载到内存。
- WT_REF_LOOKASIDE: page在磁盘上,但是在lookasidetable也有与此page相关的修改内容,在page可读之前,也需要加载这部分内容。
当对一个page进行reconcile时,如果系统中还有之前的读操作正在访问此page上修改的数据,则会将这些数据保存到lookasidetable;当page再被读时,可以利用lookasidetable中的数据重新构建内存page。
- WT_REF_LOCKED: 当page被evict时,会将page锁住,其它线程不可访问。
- WT_REF_MEM: page已经从磁盘读到内存,并且能正常访问。
- WT_REF_READING: page正在被某个线程从磁盘读到内存,其它的读线程等待它被读完,不需要重复去读。
- WT_REF_SPLIT: 当page变得过大时,会被split,状态设为WT_REF_SPLIT,原来指向的page不再被使用。
Page的大小参数
无论将数据从磁盘读到内存,还是从内存写到磁盘,都是以page为单位调度的,但是在磁盘上一个page到底多大?是否是最小分割单元?以及内存里面的各种page的大小对存储引擎的性能是否有影响?本节将围绕这些问题,分析与page大小相关的参数是如何影响存储引擎性能的。 总的来说,涉及到的关键参数和默认值如下表所示:
allocation_size
MongoDB磁盘文件的最小分配单元(由WiredTiger自带的块管理模块来分配),一个page的可以由一个或多个这样的单元组成;默认值是4KB,与主机操作系统虚拟内存页的大小相当,大多数场景下不需要修改这个值
memory_page_max
WiredTigerCache里面一个内存page随着不断插入修改等操作,允许增长达到的最大值,默认值为5MB。当一个内存page达到这个最大值时,将会被split成较小的内存pages且通过reconcile将这些pages写到磁盘pages,一旦完成写到磁盘,这些内存pages将从内存移除。
需要注意的是:split和reconcile这两个动作都需要获得page的排它锁,导致应用程序在此page上的其它写操作会等待,因此设置一个合理的最大值,对系统的性能也很关键。
需要注意的是:split和reconcile这两个动作都需要获得page的排它锁,导致应用程序在此page上的其它写操作会等待,因此设置一个合理的最大值,对系统的性能也很关键。
- 如果值太大,虽然spilt和reconcile发生的机率减少,但一旦发生这样的动作,持有排它锁的时间会较长,导致应用程序的插入或修改操作延迟增大;
- 如果值太小,虽然单次持有排它锁的时间会较短,但是会导致spilt和reconcile发生的机率增加。
internal_page_max
磁盘上internalpage的最大值,默认为4KB。随着reconcile进行,internalpage超过这个值时,会被split成多个pages。
这个值的大小会影响磁盘上B-Tree的深度和internalpage上key的数量,如果太大,则internalpage上的key的数量会很多,通过遍历定位到正确leaf page的时间会增加;如果太小,则B-Tree的深度会增加,也会影响定位到正确leaf page的时间。
这个值的大小会影响磁盘上B-Tree的深度和internalpage上key的数量,如果太大,则internalpage上的key的数量会很多,通过遍历定位到正确leaf page的时间会增加;如果太小,则B-Tree的深度会增加,也会影响定位到正确leaf page的时间。
leaf_page_max
磁盘上leaf page的最大值,默认为32KB。随着reconcile进行,leaf page超过这个值时,会被split成多个pages。
这个值的大小会影响磁盘的I/O性能,因为我们在从磁盘读取数据时,总是期望一次I/O能多读取一点数据,所以希望把这个参数调大;但是太大,又会造成读写放大,因为读出来的很多数据可能后续都用不上。
这个值的大小会影响磁盘的I/O性能,因为我们在从磁盘读取数据时,总是期望一次I/O能多读取一点数据,所以希望把这个参数调大;但是太大,又会造成读写放大,因为读出来的很多数据可能后续都用不上。
internal_key_max
internalpage上允许的最大key值,默认大小为internalpage初始值的1/10,如果超过这个值,将会额外存储。导致读取key时需要额外的磁盘I/O。
leaf_key_max
leaf page上允许的最大key值,默认大小为leaf page初始值的1/10,如果超过这个值,将会额外存储。导致读取key时需要额外的磁盘I/O。
leaf_value_max
leaf page上允许的最大value值(保存真正的集合数据),默认大小为leaf page初始值的1/2,如果超过这个值,将会额外存储。导致读取value时需要额外的磁盘I/
split_pct
内存里面将要被reconciled的 page大小与internal_page_max或leaf_page_max值的百分比,默认值为75%,如果内存里面被reconciled的page能够装进一个单独的磁盘page上,则不会发生spilt,否则按照该百分比值*最大允许的page值分割新page的大小。
Page无锁及压缩
WT引擎: checkpoint 原理
为什么要理解Checkpoint
一是将内存里面发生修改的数据写到数据文件进行持久化保存,确保数据一致性;
二是实现数据库在某个时刻意外发生故障,再次启动时,缩短数据库的恢复时间,WiredTiger存储引擎中的Checkpoint模块就是来实现这个功能的。
Checkpoint包含的关键信息
本质上来说,Checkpoint相当于一个日志,记录了上次Checkpoint后相关数据文件的变化。
一个Checkpoint包含关键信息如下图所示:
一个Checkpoint包含关键信息如下图所示:
root page
包含rootpage的大小(size),在文件中的位置(offset),校验和(checksum),创建一个checkpoint时,会生成一个新root page
allocated list pages
用于记录最后一次checkpoint之后,在这次checkpoint执行时,由WiredTiger块管理器新分配的pages,会记录每个新分配page的size,offset和checksum
discarded list pages
用于记录最后一次checkpoint之后,在这次checkpoint执行时,丢弃的不在使用的pages,会记录每个丢弃page的size,offset和checksum
available list pages
在这次checkpoint执行时,所有由WiredTiger块管理器分配但还没有被使用的pages;当删除一个之前创建的checkpoint时,它所附带的可用pages将合并到最新的这个checkpoint的可用列表上,也会记录每个可用page的size,offset和checksum
file size
在这次checkpoint执行后,磁盘上数据文件的大小
Checkpoint执行的完整流程
Checkpoint是数据库中一个比较耗资源的操作,何时触发执行以及以什么样的流程执行是本节要研究的内容,如下所述:
执行流程:
一个checkpoint典型执行流程如下图所述:
执行流程:
一个checkpoint典型执行流程如下图所述:
- 查询集合数据时,会打开集合对应的数据文件并读取其最新checkpoint数据;
- 集合文件会按checkponit信息指定的大小(file size)被 truncate 掉,所以系统发生意外故障,恢复时可能会丢失checkponit之后的数据(如果没有开启Journal);
- 在内存构造一棵包含root page的live tree,表示这是当前可以修改的checkpoint结构,用来跟踪后面写操作引起的文件变化;其它历史的checkpoint信息只能读,可以被删除;
- 内存里面的page随着增删改查被修改后,写入并需分配新的磁盘page时,将会从livetree中的available列表中选取可用的page供其使用。随后,这个新的page被加入到checkpoint的allocated列表中;
- 如果一个checkpoint被删除时,它所包含的allocated和discarded两个列表信息将被合并到最新checkpoint的对应列表上;任何不再需要的磁盘pages,也会将其引用添加到live tree的available列表中;
- 当新的checkpoint生成时,会重新刷新其allocated、available、discard三个列表中的信息,并计算此时集合文件的大小以及rootpage的位置、大小、checksum等信息,将这些信息作checkpoint元信息写入文件;
- 生成的checkpoint默认名称为WiredTigerCheckpoint,如果不明确指定其它名称,则新check point将自动取代上一次生成的checkpoint。
Checkpoint执行的触发时机
按一定时间周期:默认60s,执行一次checkpoint;
按一定日志文件大小:当Journal日志文件大小达到2GB(如果已开启),执行一次checkpoint;
任何打开的数据文件被修改,关闭时将自动执行一次checkpoint。
注意:checkpoint是一个相当重量级的操作,当对集合文件执行checkpoint时,会在文件上获得一个排它锁,其它需要等待此锁的操作,可能会出现EBUSY的错误
WT引擎:事务实现
WT引擎:缓存机制
为什么会需要理解eviction cache
从mongoDB 3.0版本引入WiredTiger存储引擎(以下称为WT)以来,一直有同学反应在高速写入数据时WT引擎会间歇性写挂起,有时候写延迟达到了几十秒,这确实是个严重的问题。引起这类问题的关键在于WT的 LRU cache 的设计模型,WT在设计LRU cache时采用分段扫描标记和hazardpointer的淘汰机制,在WT内部称这种机制叫 eviction cache 或者 WT cache,其设计目标是充分利用现代计算机超大内存容量来提高事务读写并发。在高速不间断写时内存操作是非常快的,但是内存中的数据最终必须写入到磁盘上,将页数据(page)由内存中写入磁盘上是需要写入时间,必定会和应用程序的高速不间断写产生竞争,这在任何数据库存储引擎都是无法避免的,只是由于WT利用大内存和写无锁的特性,让这种不平衡现象更加显著。下图是一位网名叫chszs同学对mongoDB 3.0和3.2版本测试高速写遇到的hang现象
从上图可以看出,数据周期性出现了hang现象,笔者在单独对WT进行高并发顺序写时遇到的情况和上图基本一致,有时候挂起长达20秒。针对此问题我结合WT源码和调试测试进行了分析,基本得出的结论如下:
要彻底弄清楚这几个问题,就需要对从WT引擎的eviction cache原理来剖析,通过分析原理找到解决此类问题的办法。先来看eviction cache是怎么实现的,为什么要这么实现。
- WT引擎的eviction cache实现时没有考虑 lru cache 的分级淘汰,只是通过扫描btree来标记,这使得它和一些独占式btree操作(例如:checkpoint)容易发生竞争。
- WTbtree的checkpoint机制设计存在bug,在大量并发写事务发生时,checkpoint需要很长时间才能完成,造成刷入磁盘的数据很大,写盘时间很长。容易引起cache 满而挂起所有的读写操作。
- WT引擎的redo log文件超过1GB大小后就会另外新建一个新的redo log文件来继续存储新的日志,在操作系统层面上新建一个文件的是需要多次I/O操作,一旦和checkpoint数据刷盘操作同时发生,所有的写也就挂起了。
要彻底弄清楚这几个问题,就需要对从WT引擎的eviction cache原理来剖析,通过分析原理找到解决此类问题的办法。先来看eviction cache是怎么实现的,为什么要这么实现。
eviction cache原理
eviction cache是一个LRU cache,即页面置换算法缓冲区,LRU cache最早出现的地方是操作系统中关于虚拟内存和物理内存数据页的置换实现,后被数据库存储引擎引入解决内存和磁盘不对等的问题。所以LRU cache主要是解决内存与数据大小不对称的问题,让最近将要使用的数据缓存在cache中,把最迟使用的数据淘汰出内存,这是LRU置换算法的基本原则。但程序代码是无法预测未来的行为,只能根据过去数据页的情况来确定,一般我们认为过去经常使用的数据比不常用的数据未来被访问的概率更高,很多LRU cache大部分是基于这个规则来设计。
WT的eviction cache也不例外的遵循了这个LRU原理,不过WT的eviction cache对数据页采用的是分段局部扫描和淘汰,而不是对内存中所有的数据页做全局管理。基本思路是一个线程阶段性的去扫描各个btree,并把btree可以进行淘汰的数据页添加到一个lru queue中,当queue填满了后记录下这个过程当前的btree对象和btree的位置(这个位置是为了作为下次阶段性扫描位置),然后对queue中的数据页按照访问热度排序,最后各个淘汰线程按照淘汰优先级淘汰queue中的数据页,整个过程是周期性重复。WT的这个evict过程涉及到多个eviction thread和hazard pointer技术。
WT的evict过程都是以page为单位做淘汰,而不是以K/V。这一点和memcache、redis等常用的缓存LRU不太一样,因为在磁盘上数据的最小描述单位是page block,而不是记录。
WT的evict过程都是以page为单位做淘汰,而不是以K/V。这一点和memcache、redis等常用的缓存LRU不太一样,因为在磁盘上数据的最小描述单位是page block,而不是记录。
eviction线程模型
WT在设计的时候采用一种叫做leader-follower的线程模型,模型示意图如下:
Leader thread负责周期性扫描所有内存中的btree索引树,将符合evict条件的page索引信息填充到eviction queue,当填充queue满时,暂停扫描并记录下最后扫描的btree对象和btree上的位置,然后对queue中的page按照事务的操作次数和访问次做一次淘汰评分,再按照评分从小到大做排序。也就是说最评分越小的page越容易淘汰。下个扫描阶段的起始位置就是上个扫描阶段的结束位置,这样能保证在若干个阶段后所有内存中的page都被扫描过一次,这是为了公平性。这里必须要说明的是一次扫描很可能只是扫描内存一部分btree对象,而不是全部,所以我对这个过程称为阶段性扫描(evict pass),它不是对整个内存中的page做评分排序。这个阶段性扫描的间隔时间是100ms,而触发这个evict pass的条件就是 WT cache管理的内存超出了设置的阈值,这个在后面的eviction cache管理的内存小节中详细介绍
在evict pass后,如果evction queue中有等待淘汰的page存在就会触发一个操作系统信号来激活follower thread来进行evict page工作。虽然evict pass的间隔时间通常是100毫秒,这里有个问题就是当WT cache的内存触及上限并且有大量写事务发生时,读写事务线程在事务开始时会唤醒leader thread和follower thread,这就会产生大量的操作系统上下文切换,系统性能急剧下降。好在WT-2.8版本修复了这个问题,leader follower通过抢锁来成为leader,通过多线程信号合并和周期性唤醒来follower,而且leader thread也承担evict page的工作,可以避免大部分的线程唤醒和上下文切换。是不是有点像Nginx的网络模型 ?
hazard pointer
hazard pointer是一个无锁并发技术,其应用场景是单个线程写和多个线程读的场景,大致的原理是这样的,每个读的线程设计一个与之对应的无锁数组用于标记这个线程引用的hazard pointer对象。读线程的步骤如下:
- 读线程在访问某个hazard pointer对象时,先将在自己的标记数组中标记访问的对象。
- 读线程在访问完毕某个hazard pointer对象时,将其对应的标记从标记数组中删除。
写线程的步骤大致是这样的,写线程如果需要对某个hazard pointer对象写时,先判断所有读线程是否标记了这个对象,如果标记了,放弃写。如果未标记,进行写。
Hazard pointer是怎样应用在WT中呢?我们这样来看待这个事情,把内存page的读写看做hazard pointer的读操作,把page从内存淘汰到磁盘上的过程看做hazard pointer的写操作,这样瞬间就能明白为什么WT在页的操作上可以不遵守The FIX Rules规则,而是采用无锁并发的页操作。要达到这种访问方式有个条件就是内存中page本身的结构要支持lock free访问。从上面的描述可以看出evict page的过程中首先要做一次hazard pointer写操作检查,而后才能进行page的reconcile和数据落盘。
hazard pointer并发技术的应用是整个WT存储引擎的关键,它关系到btree结构、internal page的构造、事务线程模型、事务并发等实现。Hazard pointer使得WT不依赖The Fix Rules规则,也让WT的btree结构更加灵活多变。
hazard pointer并发技术的应用是整个WT存储引擎的关键,它关系到btree结构、internal page的构造、事务线程模型、事务并发等实现。Hazard pointer使得WT不依赖The Fix Rules规则,也让WT的btree结构更加灵活多变。
eviction cache管理的内存
eviction cache与checkpoint之间的事
checkpoint的过程是将内存中所有的脏页(dirty page)同步刷入磁盘上并将redo log的重演位置设置到最后修改提交事务的redo log位置,相对于WT引擎来说,就是将eviction cache中的所有脏页数据刷入磁盘但并不将内存中的page淘汰出内存。这个过程其实和正常的evict过程是冲突的,而且checkpoint过程中需要更多的内存完成这项工作,这使得在一个高并发写的数据库中有可能出现挂起的状况发生。为了更好的理解整个问题的细节,我们先来看看WT checkpoint的原理和过程。
btree的checkpoint
WT引擎中的btree建立checkpoint过程还是比较复杂的,过程的步骤也比较多,而且很多步骤会涉及到索引、日志、事务和磁盘文件等。我以WT-2.7(mongoDB 3.2)版本为例子,checkpoint大致的步骤如下图
在上图中,其中绿色的部分是在开始checkpoint事务之前会将所有的btree的脏页写入文件OS cache中,如果在高速写的情况下,写的速度接近也reconcile的速度,那么这个过程将会持续很长时间,也就是说OS cache中会存在大量未落盘的数据。而且在WT中btree采用的 copy on write(写时复制) 和extent技术,这意味OS cache中的文件数据大部分是在磁盘上是连续存储的,那么在绿色框最后一个步骤会进行同步刷盘,这个时候如果OS cache的数据量很大就会造成这个操作长时间占用磁盘I/O。这个过程是会把所有提交的事务修改都进行reconcile落盘操作。
在上图的紫色是真正开始checkpoint事务的步骤,这里需要解释的是由于前面绿色步骤刷盘时间会比较长,在这个时间范围里会有新的写事务发生,也就意味着会新的脏页,checkpint必须把最新提交的事务修改落盘而且还要防止btree的分裂,这个时候就会获得btree的独占排他式访问,这时 eviction cache不能对这个btree上的页进行evict操作(在这种情况下是不是容易造成WT cache满而挂起读写事务?)。
PS:WT-2.8版本之后对checkpoint改动非常大,主要是针对上面两点做了拆分,防止读写事务挂起发生,但大体过程是差不多的。
在上图的紫色是真正开始checkpoint事务的步骤,这里需要解释的是由于前面绿色步骤刷盘时间会比较长,在这个时间范围里会有新的写事务发生,也就意味着会新的脏页,checkpint必须把最新提交的事务修改落盘而且还要防止btree的分裂,这个时候就会获得btree的独占排他式访问,这时 eviction cache不能对这个btree上的页进行evict操作(在这种情况下是不是容易造成WT cache满而挂起读写事务?)。
PS:WT-2.8版本之后对checkpoint改动非常大,主要是针对上面两点做了拆分,防止读写事务挂起发生,但大体过程是差不多的。
内存和磁盘I/O的权衡
引起写挂起问题的原因多种多样,但归根结底是因为内存和磁盘速度不对称的问题。因为WT的设计原则就是让数据尽量利用现代计算机的超大内存,可是内存中的脏数据在checkpoint时需要同步写入磁盘造成瞬间I/O很高,这是矛盾的。要解决这些问题个人认为有以下几个途径
1.、将MongoDB的WT版本升级到2.8,2.8版本对evict queue模型做了分级,尽量避免evict page过程中堵塞问题,2.8的checkpoint机制不在是分为预前刷盘和checkpoint刷盘,而是采用逐个对btree直接做checkpoint刷盘,缓解了OS cache缓冲太多的文件脏数据问题。
2、试试direct I/O或许会有不同的效果,WT是支持direct I/O模式。笔者试过direct I/O模式,让WT cache彻底接管所有的物理内存管理,写事务的并发会比MMAP模式少10%,但没有出现过超过1秒的写延迟问题。
3、尝试将WT cache设小点,大概设置成整个内存的1/4左右。这种做法是可以缓解OS cache中瞬间缓存太多文件脏数据的问题,但会引起WT cache频繁evict page和频繁的leader-follower线程上下文切换。而且这种机制也依赖于OS page cache的刷盘周期,周期太长效果不明显。
4、用多个磁盘来存储,redo log文件放在一个单独的机械磁盘上,数据放在单独一个磁盘上,避免redo log与checkpoint刷盘发生竞争。
5、有条件的话,换成将磁盘换成SSD吧。这一点比较难,mongoDB现在也大量使用在OLAP和大数据存储,而高速写的场景都发生这些场景,成本是个问题。如果是OLTP建议用SSD。
这些方法只能缓解读写事务挂起的问题,不能说彻底解决这个问题,WT引擎发展很快,开发团队正对WT eviction cache和checkpoint正在做优化,这个问题慢慢变得不再是问题,尤其是WT-2.8版本,大量的模型和代码优化都是集中在这个问题上。
MongoDB高可用
主从架构
master-slave架构中 master节点负责数据的读写,slave没有写入权限只负责读取数据。
在主从结构中,主节点的操作记录成为oplog(operation log)。oplog存储在系统数据库local的 oplog.$main集合中,这个集合的每个文档都代表主节点上执行的一个操作。从服务器会定期从主服务器中获取oplog记录,然后在本机上执行!
对于存储oplog的集合,MongoDB采用的是固定集合,也就是说随着操作过多,新的操作会覆盖旧的操作!
对于存储oplog的集合,MongoDB采用的是固定集合,也就是说随着操作过多,新的操作会覆盖旧的操作!
主从架构没有自动故障转移功能,mongodb4.0不在支持主从
复制集 replica sets
(OpLog + 心跳检测 + 选举机制)
(OpLog + 心跳检测 + 选举机制)
复制集集群架构原理
选举
MongoDB的Primary节点选举基于心跳触发。一个复制集N个节点中的任意两个节点维持心跳,每个节点维护其他N-1个节点的状态。
心跳检测
主节点选举触发的时机(集群中没有主节点)
复制集架构
- 复制集是由一组拥有相同数据集的mongod实例做组成的集群。
- 复制集是一个集群,它是2台及2台以上的服务器组成,以及复制集成员包括Primary主节点,secondary从节点 和投票(裁判)节点。
- 复制集提供了数据的冗余备份,并在多个服务器上存储数据副本,提高了数据的可用性,保证数据的安全性。
PSS 模式
PSS模式由一个主节点和两个备节点所组成,即Primary+Secondary+Secondary
此模式始终提供数据集的两个完整副本,如果主节点不可用,则复制集选择备节点作为主节点并继续正常操作。旧的主节点在可用时重新加入复制集
PSA模式
PSA模式由一个主节点、一个备节点和一个仲裁者节点组成,即Primary+Secondary+Arbiter
其中,Arbiter节点不存储数据副本,也不提供业务的读写操作。Arbiter节点发生故障不影响业务,仅影响选举投票。此模式仅提供数据的一个完整副本,如果主节点不可用,则复制集将选择备节点作为主节点。
为什么要使用复制集 ?
- 数据分发: 将数据从一个区域复制到另一个区域,减少另一个区域的读延迟
- 读写分离: 不同类型的压力分别在不同的节点上执行
- 异地容灾: 在数据中心故障时候快速切换到异地
- 功能隔离(负载均衡)我们可以在备节点上执行读操作,减少主节点的压力。 比如:用于分析、报表,数据挖掘,系统任务等。
经典三节点复制集环境搭建
看笔记
复制集成员角色
两个重要属性
属性一:Priority = 0
当 Priority 等于 0 时,它不可以被复制集选举为主,Priority 的值越高,则被选举为主的概率更大。通常,在跨机房方式下部署复制集可以使用该特性。假设使用了机房A和机房B,由于主要业务与机房A更近,则可以将机房B的复制集成员Priority设置为0,这样主节点就一定会是A机房的成员
属性二:Vote = 0
不可以参与选举投票,此时该节点的 Priority 也必须为 0,即它也不能被选举为主。由于一个复制集中最多只有7个投票成员,因此多出来的成员则必须将其vote属性值设置为0,即这些成员将无法参与投票。
节点角色
Primary:主节点,其接收所有的写请求,然后把修改同步到所有备节点。一个复制集只能有一个主节点,当主节点“挂掉”后,其他节点会重新选举出来一个主节点。
Secondary:备节点,与主节点保持同样的数据集。当主节点“挂掉”时,参与竞选主节点。分为以下三个不同类型
Hidden = false:正常的只读节点,是否可选为主,是否可投票,取决于 Priority,Vote 的值;
Hidden = true:隐藏节点,对客户端不可见, 可以参与选举,但是 Priority 必须为 0,即不能被提升为主。
由于隐藏节点不会接受业务访问,因此可通过隐藏节点做一些数据备份、离线计算的任务,这并不会影响整个复制集。
由于隐藏节点不会接受业务访问,因此可通过隐藏节点做一些数据备份、离线计算的任务,这并不会影响整个复制集。
Delayed :延迟节点,必须同时具备隐藏节点和Priority0的特性,会延迟一定的时间(SlaveDelay 配置决定)从上游复制增量,常用于快速回滚场景
Arbiter:仲裁节点,只用于参与选举投票,本身不承载任何数据,只作为投票角色。比如你部署了2个节点的复制集,1个 Primary,1个Secondary,任意节点宕机,复制集将不能提供服务了(无法选出Primary),这时可以给复制集添加⼀个 Arbiter节点,即使有节点宕机,仍能选出Primary。 Arbiter本身不存储数据,是非常轻量级的服务,当复制集成员为偶数时,最好加入⼀个Arbiter节点,以提升复制集可用性。
配置隐藏节点
配置延时节点
查看复制延迟
如果希望查看当前节点oplog的情况,则可以使用rs.printReplicationInfo()命令
复制集高可用
复制集选举
MongoDB的复制集选举使用Raft算法(https://raft.github.io/)来实现,选举成功的必要条件是大多数投票节点存活。在具体的实现中,MongoDB对raft协议添加了一些自己的扩展,这包括:
- 支持chainingAllowed链式复制,即备节点不只是从主节点上同步数据,还可以选择一个离自己最近(心跳延时最小)的节点来复制数据。
- 增加了预投票阶段,即preVote,这主要是用来避免网络分区时产生Term(任期)值激增的问题
- 支持投票优先级,如果备节点发现自己的优先级比主节点高,则会主动发起投票并尝试成为新的主节点。
一个复制集最多可以有50 个成员,但只有 7 个投票成员。这是因为一旦过多的成员参与数据复制、投票过程,将会带来更多可靠性方面的问题
当复制集内存活的成员数量不足大多数时,整个复制集将无法选举出主节点,此时无法提供写服务,这些节点都将处于只读状态。此外,如果希望避免平票结果的产生,最好使用奇数个节点成员,比如3个或5个。当然,在MongoDB复制集的实现中,对于平票问题已经提供了解决方案:
- 为选举定时器增加少量的随机时间偏差,这样避免各个节点在同一时刻发起选举,提高成功率
- 使用仲裁者角色,该角色不做数据复制,也不承担读写业务,仅仅用来投票
自动故障转移
在故障转移场景中,我们所关心的问题是:
- 备节点是怎么感知到主节点已经发生故障的?
- 如何降低故障转移对业务产生的影响?
一个影响检测机制的因素是心跳,在复制集组建完成之后,各成员节点会开启定时器,持续向其他成员发起心跳,这里涉及的参数为heartbeatIntervalMillis,即心跳间隔时间,默认值是2s。如果心跳成功,则会持续以2s的频率继续发送心跳;如果心跳失败,则会立即重试心跳,一直到心跳恢复成功。
另一个重要的因素是选举超时检测,一次心跳检测失败并不会立即触发重新选举。实际上除了心跳,成员节点还会启动一个选举超时检测定时器,该定时器默认以10s的间隔执行,具体可以通过electionTimeoutMillis参数指定:
另一个重要的因素是选举超时检测,一次心跳检测失败并不会立即触发重新选举。实际上除了心跳,成员节点还会启动一个选举超时检测定时器,该定时器默认以10s的间隔执行,具体可以通过electionTimeoutMillis参数指定:
- 如果心跳响应成功,则取消上一次的electionTimeout调度(保证不会发起选举),并发起新一轮electionTimeout调度。
- 如果心跳响应迟迟不能成功,那么electionTimeout任务被触发,进而导致备节点发起选举并成为新的主节点。
在MongoDB的实现中,选举超时检测的周期要略大于electionTimeoutMillis设定。该周期会加入一个随机偏移量,大约在10~11.5s,如此的设计是为了错开多个备节点主动选举的时间,提升成功率。
在electionTimeout任务中触发选举必须要满足以下条件:
- (1)当前节点是备节点。
- (2)当前节点具备选举权限。
- (3)在检测周期内仍然没有与主节点心跳成功。
业务影响评估
在复制集发生主备节点切换的情况下,会出现短暂的无主节点阶段,此时无法接受业务写操作。如果是因为主节点故障导致的切换,则对于该节点的所有读写操作都会产生超时。如果使用MongoDB 3.6及以上版本的驱动,则可以通过开启retryWrite来降低影响
- 如果主节点属于强制掉电,那么整个Failover过程将会变长,很可能需要在Election定时器超时后才被其他节点感知并恢复,这个时间窗口一般会在12s以内。然而实际上,对于业务呼损的考量还应该加上客户端或mongos对于复制集角色的监视和感知行为(真实的情况可能需要长达30s以上)。
- 对于非常重要的业务,建议在业务层面做一些防护策略,比如设计重试机制。
思考:如何优雅的重启复制集?
如果想不丢数据重启复制集,更优雅的打开方式应该是这样的:
- 逐个重启复制集里所有的Secondary节点
- 对Primary发送rs.stepDown()命令,等待primary降级为Secondary
- 重启降级后的Primary
复制集数据同步机制
复制集数据同步分为初始化同步(全量)和keep复制同步(增量)。初始化同步指全量从主节点同步数据,如果Primary 节点数据量比较大同步时间会比较长。而keep复制指初始化同步过后,节点之间的实时同步一般是增量同步。
初始化同步有以下两种情况会触发:
初始化同步有以下两种情况会触发:
- (1) Secondary第一次加入。
- (2) Secondary落后的数据量超过了oplog的大小,这样也会被全量复制。
MongoDB的Primary节点选举基于心跳触发。一个复制集N个节点中的任意两个节点维持心跳,每个节点维护其他N-1个节点的状态。
心跳检查
整个集群需要保持一定的通信才能知道哪些节点活着哪些节点挂掉。mongodb节点会向副本集中的其他节点每`2秒`就会发送一次pings包,如果其他节点在`10秒`钟之内没有返回就标示为不能访问。每个节点内部都会维护一个状态映射表,表明当前每个节点是什么角色、日志时间戳等关键信息。如果`主节点发现自己无法与大部分节点通讯则把自己降级为secondary只读节点。`
主节点选举触发的时机(集群中没有主节点)
- 第一次初始化一个复制集
- Secondary节点权重比Primary节点高时,发起替换选举(默认权重(优先级)是1,投票/隐藏节点 的权重是 0 )
- Secondary节点发现集群中没有Primary时,发起选举
- Primary节点不能访问到大部分(Majority)成员时主动降级
当触发选举时,Secondary节点尝试将自身选举为Primary。主节点选举是一个 二阶段过程+多数派协议。
oplog
在复制集架构中,主节点与备节点之间是通过oplog来同步数据的,这里的oplog是一个特殊的固定集合,当主节点上的一个写操作完成后,会向oplog集合写入一条对应的日志,而备节点则通过这个oplog不断拉取到新的日志,在本地进行回放以达到数据同步的目的。
什么是oplog
MongoDB oplog 是 Local 库下的一个固定集合,用来保存写操作所产生的增量日志(类似于 MySQL 中 的 Binlog)。
它是一个 Capped Collection(固定集合),即超出配置的最大值后,会自动删除最老的历史数据,MongoDB 针对 oplog 的删除有特殊优化,以提升删除效率。
主节点产生新的 oplog Entry,从节点通过复制 oplog 并应用来保持和主节点的状态一致;
Oplog Entry 数据样例
查看oplog
数据样例
ts 字段描述了oplog产生的时间戳,可称之为optime。optime是备节点实现增量日志同步的关键,它保证了oplog是节点有序的,其由两部分组成:
- 当前的系统时间,即UNIX时间至现在的秒数,32位
- 整数计时器,不同时间值会将计数器进行重置,32位
optime属于BSON的Timestamp类型,这个类型一般在MongoDB内部使用。既然oplog保证了节点级有序,那么备节点便可以通过轮询的方式进行拉取,这里会用到可持续追踪的游标(tailable cursor)技术。
每个备节点都分别维护了自己的一个offset,也就是从主节点拉取的最后一条日志的optime,在执行同步时就通过这个optime向主节点的oplog集合发起查询。为了避免不停地发起新的查询链接,在启动第一次查询后可以将cursor挂住(通过将cursor设置为tailable)。这样只要oplog中产生了新的记录,备节点就能使用同样的请求通道获得这些数据。tailable cursor只有在查询的集合为固定集合时才允许开启
oplog集合的大小
幂等性
每一条oplog记录都描述了一次数据的原子性变更,对于oplog来说,必须保证是幂等性的。也就是说,对于同一个oplog,无论进行多少次回放操作,数据的最终状态都会保持不变。某文档x字段当前值为100,用户向Primary发送一条{$inc: {x: 1}},记录oplog时会转化为一条{$set: {x: 101}的操作,才能保证幂等性
oplog的写入被放大,导致同步追不上——大数组更新
当数组非常大时,对数组的一个小更新,可能就需要把整个数组的内容记录到oplog里,我遇到一个实际的生产环境案例,用户的文档内包含一个很大的数组字段,1000个元素总大小在64KB左右,这个数组里的元素按时间反序存储,新插入的元素会放到数组的最前面($position: 0),然后保留数组的前1000个元素($slice: 1000)。
上述场景导致,Primary上的每次往数组里插入一个新元素(请求大概几百字节),oplog里就要记录整个数组的内容,Secondary同步时会拉取oplog并重放,Primary到Secondary同步oplog的流量是客户端到Primary网络流量的上百倍,导致主备间网卡流量跑满,而且由于oplog的量太大,旧的内容很快被删除掉,最终导致Secondary追不上,转换为RECOVERING状态。
上述场景导致,Primary上的每次往数组里插入一个新元素(请求大概几百字节),oplog里就要记录整个数组的内容,Secondary同步时会拉取oplog并重放,Primary到Secondary同步oplog的流量是客户端到Primary网络流量的上百倍,导致主备间网卡流量跑满,而且由于oplog的量太大,旧的内容很快被删除掉,最终导致Secondary追不上,转换为RECOVERING状态。
在文档里使用数组时,一定得注意上述问题,避免数组的更新导致同步开销被无限放大的问题。使用数组时,尽量注意:
1. 数组的元素个数不要太多,总的大小也不要太大
2. 尽量避免对数组进行更新操作
3. 如果一定要更新,尽量只在尾部插入元素,复杂的逻辑可以考虑在业务层面上来支持
1. 数组的元素个数不要太多,总的大小也不要太大
2. 尽量避免对数组进行更新操作
3. 如果一定要更新,尽量只在尾部插入元素,复杂的逻辑可以考虑在业务层面上来支持
复制延迟
由于oplog集合是有固定大小的,因此存放在里面的oplog随时可能会被新的记录冲掉。如果备节点的复制不够快,就无法跟上主节点的步伐,从而产生复制延迟(replication lag)问题。这是不容忽视的,一旦备节点的延迟过大,则随时会发生复制断裂的风险,这意味着备节点的optime(最新一条同步记录)已经被主节点老化掉,于是备节点将无法继续进行数据同步
为了尽量避免复制延迟带来的风险,我们可以采取一些措施,比如:
为了尽量避免复制延迟带来的风险,我们可以采取一些措施,比如:
增加oplog的容量大小,并保持对复制窗口的监视。
通过一些扩展手段降低主节点的写入速度。
优化主备节点之间的网络。
避免字段使用太大的数组(可能导致oplog膨胀【写入放大】)。
数据回滚
由于复制延迟是不可避免的,这意味着主备节点之间的数据无法保持绝对的同步。当复制集中的主节点宕机时,备节点会重新选举成为新的主节点。那么,当旧的主节点重新加入时,必须回滚掉之前的一些“脏日志数据”,以保证数据集与新的主节点一致。主备复制集合的差距越大,发生大量数据回滚的风险就越高。
对于写入的业务数据来说,如果已经被复制到了复制集的大多数节点,则可以避免被回滚的风险。应用上可以通过设定更高的写入级别(writeConcern:majority)来保证数据的持久性。这些由旧主节点回滚的数据会被写到单独的rollback目录下,必要的情况下仍然可以恢复这些数据。
对于写入的业务数据来说,如果已经被复制到了复制集的大多数节点,则可以避免被回滚的风险。应用上可以通过设定更高的写入级别(writeConcern:majority)来保证数据的持久性。这些由旧主节点回滚的数据会被写到单独的rollback目录下,必要的情况下仍然可以恢复这些数据。
当rollback发生时,MongoDB将把rollback的数据以BSON格式存放到dbpath路径下rollback文件夹中,BSON文件的命名格式如下:...bson
同步源选择
MongoDB是允许通过备节点进行复制的,这会发生在以下的情况中:
在 settings.chainingAllowed 开启的情况下,备节点自动选择一个最近的节点(ping命令时延最小)进行同步。settings.chainingAllowed选项默认是开启的,也就是说默认情况下备节点并不一定会选择主节点进行同步,这个副作用就是会带来延迟的增加,你可以通过下面的操作进行关闭
使用replSetSyncFrom 命令临时更改当前节点的同步源,比如在初始化同步时将同步源指向备节点来降低对主节点的影响
复制集成员的配置参数
举例:
分片集群 Shard Cluster
简介
分片(shard)是指在将数据进行水平切分之后,将其存储到多个不同的服务器节点上的一种扩展方式。分片在概念上非常类似于应用开发中的“水平分表”。不同的点在于,MongoDB本身就自带了分片管理的能力,对于开发者来说可以做到开箱即用。
为什么要分片
MongoDB复制集实现了数据的多副本复制及高可用,但是一个复制集能承载的容量和负载是有限的
1. 存储容量需求超出单机磁盘容量。
2. 活跃的数据集超出单机内存容量,导致很多请求都要从磁盘读取数据,影响性能。
3. IOPS超出单个MongoDB节点的服务能力,随着数据的增长,单机实例的瓶颈会越来越明显。
4. 副本集具有节点数量限制
1. 存储容量需求超出单机磁盘容量。
2. 活跃的数据集超出单机内存容量,导致很多请求都要从磁盘读取数据,影响性能。
3. IOPS超出单个MongoDB节点的服务能力,随着数据的增长,单机实例的瓶颈会越来越明显。
4. 副本集具有节点数量限制
MongoDB 分片集群架构
MongoDB 分片集群(Sharded Cluster)是对数据进行水平扩展的一种方式。MongoDB 使用分片集群来支持大数据集和高吞吐量的业务场景。在分片模式下,存储不同的切片数据的节点被称为分片节点,一个分片集群内包含了多个分片节点。当然,除了分片节点,集群中还需要一些配置节点、路由节点,以保证分片机制的正常运作
- 数据分片:分片用于存储真正的数据,并提供最终的数据读写访问。分片仅仅是一个逻辑的概念,它可以是一个单独的mongod实例,也可以是一个复制集。图中的Shard1、Shard2都是一个复制集分片。在生产环境中也一般会使用复制集的方式,这是为了防止数据节点出现单点故障。
- 配置服务器(Config Server):配置服务器包含多个节点,并组成一个复制集结构,对应于图中的ConfigReplSet。配置复制集中保存了整个分片集群中的元数据,其中包含各个集合的分片策略,以及分片的路由表等。
- 查询路由(mongos):mongos是分片集群的访问入口,其本身并不持久化数据。mongos启动后,会从配置服务器中加载元数据。之后mongos开始提供访问服务,并将用户的请求正确路由到对应的分片。在分片集群中可以部署多个mongos以分担客户端请求的压力
分片集群的搭建过程
具体看md笔记
分片策略
通过分片功能,可以将一个非常大的集合分散存储到不同的分片上(如图)
假设这个集合大小是1TB,那么拆分到4个分片上之后,每个分片存储256GB的数据。这个当然是最理想化的场景,实质上很难做到如此绝对的平衡。一个集合在拆分后如何存储、读写,与该集合的分片策略设定是息息相关的。在了解分片策略之前,我们先来介绍一下chunk。
假设这个集合大小是1TB,那么拆分到4个分片上之后,每个分片存储256GB的数据。这个当然是最理想化的场景,实质上很难做到如此绝对的平衡。一个集合在拆分后如何存储、读写,与该集合的分片策略设定是息息相关的。在了解分片策略之前,我们先来介绍一下chunk。
Chunk
chunk的意思是数据块,一个chunk代表了集合中的“一段数据”,例如,用户集合(db.users)在切分成多个chunk之后如图所示:
chunk所描述的是范围区间,例如,db.users使用了userId作为分片键,那么chunk就是userId的各个值(或哈希值)的连续区间。集群在操作分片集合时,会根据分片键找到对应的chunk,并向该chunk所在的分片发起操作请求,而chunk的分布在一定程度上会影响数据的读写路径,这由以下两点决定:
- chunk的切分方式,决定如何找到数据所在的chunk
- chunk的分布状态,决定如何找到chunk所在的分片
- Splitting:当一个chunk的大小超过配置中的chunk size时,MongoDB的后台进程会把这个chunk切分成更小的chunk,从而避免chunk过大的情况
- Balancing:在MongoDB中,balancer是一个后台进程,负责chunk的迁移,从而均衡各个shard server的负载,系统初始1个chunk,chunk size默认值64M,生产库上选择适合业务的chunk size是最好的。MongoDB会自动拆分和迁移chunks
分片集群的数据分布(shard节点)
- 使用chunk来存储数据
- 集群搭建完成之后,默认开启一个chunk,大小是64M,
- 存储需求超过64M,chunk会进行分裂,如果单位时间存储需求很大,设置更大的chunk
- chunk会被自动均衡迁移
chunksize的选择
chunk的分裂和迁移非常消耗IO资源;chunk分裂的时机:在插入和更新,读数据不会分裂。
chunksize的选择
小的chunksize:数据均衡是迁移速度快,数据分布更均匀。数据分裂频繁,路由节点消耗更多资源。
大的chunksize:数据分裂少。数据块移动集中消耗IO资源。通常100-200M
chunkSize 对分裂及迁移的影响
- MongoDB 默认的 chunkSize 为64MB,如无特殊需求,建议保持默认值;chunkSize 会直接影响到 chunk 分裂、迁移的行为。
- chunkSize 越小,chunk 分裂及迁移越多,数据分布越均衡;反之,chunkSize 越大,chunk 分裂及迁移会更少,但可能导致数据分布不均。
- chunkSize 太小,容易出现 jumbo chunk(即shardKey 的某个取值出现频率很高,这些文档只能放到一个 chunk 里,无法再分裂)而无法迁移;chunkSize 越大,则可能出现 chunk 内文档数太多(chunk 内文档数不能超过 250000 )而无法迁移。
- chunk 自动分裂只会在数据写入时触发,所以如果将 chunkSize 改小,系统需要一定的时间来将 chunk 分裂到指定的大小。
- chunk 只会分裂,不会合并,所以即使将 chunkSize 改大,现有的 chunk 数量不会减少,但 chunk 大小会随着写入不断增长,直到达到目标大小
分片算法
chunk切分是根据分片策略进行实施的,分片策略的内容包括分片键和分片算法
范围分片(range sharding)
范围分片能很好地满足范围查询的需求,比如想查询x的值在[-30,10]之间的所有文档,这时mongos直接将请求定位到chunk2所在的分片服务器,就能查询出所有符合条件的文档。范围分片的缺点在于,如果Shard Key有明显递增(或者递减)趋势,则新插入的文档会分布到同一个chunk,此时写压力会集中到一个节点,从而导致单点的性能瓶颈。一些常见的导致递增的Key如下:
- 时间值。
- ObjectId,自动生成的_id由时间、计数器组成。
- UUID,包含系统时间、时钟序列。
- 自增整数序列。
哈希分片(Hash sharding)
哈希分片会先事先根据分片键计算出一个新的哈希值(64位整数),再根据哈希值按照范围分片的策略进行chunk的切分。适用于日志,物联网等高并发场景
哈希分片与范围分片是互补的,由于哈希算法保证了随机性,所以文档可以更加离散地分布到多个chunk上,这避免了集中写问题。然而,在执行一些范围查询时,哈希分片并不是高效的。因为所有的范围查询都必然导致对所有chunk进行检索,如果集群有10个分片,那么mongos将需要对10个分片分发查询请求。哈希分片与范围分片的另一个区别是,哈希分片只能选择单个字段,而范围分片允许采用组合式的多字段作为分片键。
分片标签
MongoDB允许通过为分片添加标签(tag)的方式来控制数据分发。一个标签可以关联到多个分片区间(TagRange)。均衡器会优先考虑chunk是否正处于某个分片区间上(被完全包含),如果是则会将chunk迁移到分片区间所关联的分片,否则按一般情况处理。
分片标签适用于一些特定的场景。例如,集群中可能同时存在OLTP和OLAP处理,一些系统日志的重要性相对较低,而且主要以少量的统计分析为主。为了便于单独扩展,我们可能希望将日志与实时类的业务数据分开,此时就可以使用标签。
分片标签适用于一些特定的场景。例如,集群中可能同时存在OLTP和OLAP处理,一些系统日志的重要性相对较低,而且主要以少量的统计分析为主。为了便于单独扩展,我们可能希望将日志与实时类的业务数据分开,此时就可以使用标签。
分片数据按区域:Zone
在分片群集中可以基于分片键划分数据的区域(zone), 你可以将每个区域(zone)与集群中的一个或多个分片关联。
应用区域(zone)的一些常见部署模式如下:
- 将指定的数据放在指定的分片上。
- 确保最相关的数据驻留在地理上最靠近应用程序服务器的分片上。
- 根据分片硬件的硬件/性能将数据路由到分片。
下图说明了具有三个分片和两个区域的分片集群。 A区域代表下边界为1且上限为10的范围。B区域代表下边界为10且上限为20的范围。分片Alpha和Beta具有A区域。 分片Beta也具有B区。分片Charlie没有与之关联的区域。 群集处于稳定状态
分片键(ShardKey)的选择
分片键的基数(cardinality),取值基数越大越有利于扩展。
- 以性别作为分片键 :数据最多被拆分为 2 份
- 以月份作为分片键 :数据最多被拆分为 12 份
分片键的取值分布应该尽可能均匀。
业务读写模式,尽可能分散写压力,而读操作尽可能来自一个或少量的分片。
分片键应该能适应大部分的业务操作
分片键(ShardKey)的约束
ShardKey 必须是一个索引。非空集合须在 ShardCollection 前创建索引;空集合 ShardCollection 自动创建索引
4.4 版本之前
- - ShardKey 大小不能超过 512 Bytes;
- - 仅支持单字段的哈希分片键;
- - Document 中必须包含 ShardKey;
- - ShardKey 包含的 Field 不可以修改。
4.4 版本之后
- - ShardKey 大小无限制;
- - 支持复合哈希分片键;
- - Document 中可以不包含 ShardKey,插入时被当 做 Null 处理;
- - 为 ShardKey 添加后缀 refineCollectionShardKey 命令,可以修改 ShardKey 包含的 Field;
而在 4.2 版本之前,ShardKey 对应的值不可以修改;4.2 版本之后,如果 ShardKey 为非_ID 字段, 那么可以修改 ShardKey 对应的值。
数据均衡
均衡的方式
一种理想的情况是,所有加入的分片都发挥了相当的作用,包括提供更大的存储容量,以及读写访问性能。因此,为了保证分片集群的水平扩展能力,业务数据应当尽可能地保持均匀分布。这里的均匀性包含以下两个方面:
- 1. 所有的数据应均匀地分布于不同的chunk上。
- 2. 每个分片上的chunk数量尽可能是相近的。
手动均衡
一种做法是,可以在初始化集合时预分配一定数量的chunk(仅适用于哈希分片),比如给10个分片分配1000个chunk,那么每个分片拥有100个chunk。另一种做法则是,可以通过splitAt、moveChunk命令进行手动切分、迁移
自动均衡
开启MongoDB集群的自动均衡功能。均衡器会在后台对各分片的chunk进行监控,一旦发现了不均衡状态就会自动进行chunk的搬迁以达到均衡。其中,chunk不均衡通常来自于两方面的因素:
- 一方面,在没有人工干预的情况下,chunk会持续增长并产生分裂(split),而不断分裂的结果就会出现数量上的不均衡;
- 另一方面,在动态增加分片服务器时,也会出现不均衡的情况。自动均衡是开箱即用的,可以极大简化集群的管理工作。
chunk分裂
在默认情况下,一个chunk的大小为64MB,该参数由配置的chunksize参数指定。如果持续地向该chunk写入数据,并导致数据量超过了chunk大小,则MongoDB会自动进行分裂,将该chunk切分为两个相同大小的chunk。
chunk分裂是基于分片键进行的,如果分片键的基数太小,则可能因为无法分裂而会出现jumbo chunk(超大块)的问题。
例如,对db.users使用gender(性别)作为分片键,由于同一种性别的用户数可能达到数千万,分裂程序并不知道如何对分片键(gender)的一个单值进行切分,因此最终导致在一个chunk上集中存储了大量的user记录(总大小超过64MB)
jumbo chunk对水平扩展有负面作用,该情况不利于数据的均衡,业务上应尽可能避免。一些写入压力过大的情况可能会导致chunk多次失败(split),最终当chunk中的文档数大于1.3×avgObjectSize时会导致无法迁移。此外在一些老版本中,如果chunk中的文档数超过250000个,也会导致无法迁移
例如,对db.users使用gender(性别)作为分片键,由于同一种性别的用户数可能达到数千万,分裂程序并不知道如何对分片键(gender)的一个单值进行切分,因此最终导致在一个chunk上集中存储了大量的user记录(总大小超过64MB)
jumbo chunk对水平扩展有负面作用,该情况不利于数据的均衡,业务上应尽可能避免。一些写入压力过大的情况可能会导致chunk多次失败(split),最终当chunk中的文档数大于1.3×avgObjectSize时会导致无法迁移。此外在一些老版本中,如果chunk中的文档数超过250000个,也会导致无法迁移
自动均衡
MongoDB的数据均衡器运行于Primary Config Server(配置服务器的主节点)上,而该节点也同时会控制chunk数据的搬迁流程。
- 分片shard0在持续的业务写入压力下,产生了chunk分裂。
- 分片服务器通知Config Server进行元数据更新。
- Config Server的自动均衡器对chunk分布进行检查,发现shard0和shard1的chunk数差异达到了阈值,向shard0下发moveChunk命令以执行chunk迁移。
- shard0执行指令,将指定数据块复制到shard1。该阶段会完成索引、chunk数据的复制,而且在整个过程中业务侧对数据的操作仍然会指向shard0;所以,在第一轮复制完毕之后,目标shard1会向shard0确认是否还存在增量更新的数据,如果存在则继续复制。
- shard0完成迁移后发送通知,此时Config Server开始更新元数据库,将chunk的位置更新为目标shard1。在更新完元数据库后并确保没有关联cursor的情况下,shard0会删除被迁移的chunk副本。
- Config Server通知mongos服务器更新路由表。此时,新的业务请求将被路由到shard1。
迁移阈值
均衡器对于数据的“不均衡状态”判定是根据两个分片上的chunk个数差异来进行的
迁移速度
数据均衡的整个过程并不是很快,影响MongoDB均衡速度的几个选项如下:
- _secondaryThrottle:用于调整迁移数据写到目标分片的安全级别。如果没有设定,则会使用w:2选项,即至少一个备节点确认写入迁移数据后才算成功。从MongoDB 3.4版本开始,_secondaryThrottle被默认设定为false, chunk迁移不再等待备节点写入确认。
- _waitForDelete:在chunk迁移完成后,源分片会将不再使用的chunk删除。如果_waitForDelete是true,那么均衡器需要等待chunk同步删除后才进行下一次迁移。该选项默认为false,这意味着对于旧chunk的清理是异步进行的。
- 并行迁移数量:在早期版本的实现中,均衡器在同一时刻只能有一个chunk迁移任务。从MongoDB 3.4版本开始,允许n个分片的集群同时执行n/2个并发任务。
随着版本的迭代,MongoDB迁移的能力也在逐步提升。从MongoDB 4.0版本开始,支持在迁移数据的过程中并发地读取源端和写入目标端,迁移的整体性能提升了约40%。这样也使得新加入的分片能更快地分担集群的访问读写压力
MongoDB 安全认证
安全认证概述
MongoDB 默认是没有账号的,可以直接连接,无须身份验证。实际项目中肯定是要权限验证的,否则后果不堪设想。从2016年开始发生了多起MongoDB黑客赎金事件,大部分MongoDB安全问题暴露出 了安全问题的短板其实是用户,首先用户对于数据库的安全不重视,其次用户在使用过程中可能没有养成定期备份的好习惯,最后是企业可能缺乏有经验和技术的专业人员。所以对MongoDB进行安全认证 是必须要做的
用户相关操作
角色
数据库内置的角色
- read:允许用户读取指定数据库
- readWrite:允许用户读写指定数据库
- dbAdmin:允许用户在指定数据库中执行管理函数,如索引创建、删除,查看统计或访问 system.profile
- userAdmin:允许用户向system.users集合写入,可以找指定数据库里创建、删除和管理用户
- clusterAdmin:只在admin数据库中可用,赋予用户所有分片和复制集相关函数的管理权限
- readAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的读权限
- readWriteAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的读写权限
- userAdminAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的userAdmin权限
- dbAdminAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的dbAdmin权限
- root:只在admin数据库中可用。超级账号,超级权限
- dbOwner:库拥有者权限,即readWrite、dbAdmin、userAdmin角色的合体
各个类型用户对应的角色
- 数据库用户角色:read、readWrite
- 数据库管理角色:dbAdmin、dbOwner、userAdmin
- 集群管理角色:clusterAdmin、clusterManager、clusterMonitor、hostManager
- 备份恢复角色:backup、restore;
- 所有数据库角色:readAnyDatabase、readWriteAnyDatabase、userAdminAnyDatabase、 dbAdminAnyDatabase
- 超级用户角色:root
- 这里还有几个角色间接或直接提供了系统超级用户的访问(dbOwner 、userAdmin、 userAdminAnyDatabase)
单机安全认证实现流程(见md笔记)
分片集群安全认证(见md笔记)
MongoDB 事务
MongoDB 多文档事务
在MongoDB中,对单个文档的操作是原子的。由于可以在单个文档结构中使用内嵌文档和数组来获得数据之间的关系,而不必跨多个文档和集合进行范式化,所以这种单文档原子性避免了许多实际场景中对多文档事务的需求。
对于那些需要对多个文档(在单个或多个集合中)进行原子性读写的场景,MongoDB支持多文档事务。而使用分布式事务,事务可以跨多个操作、集合、数据库、文档和分片使用。
MongoDB 虽然已经在 4.2 开始全面支持了多文档事务,但并不代表大家应该毫无节制地使用它。相反,对事务的使用原则应该是:能不用尽量不用。 通过合理地设计文档模型,可以规避绝大部分使用事务的必要性。
对于那些需要对多个文档(在单个或多个集合中)进行原子性读写的场景,MongoDB支持多文档事务。而使用分布式事务,事务可以跨多个操作、集合、数据库、文档和分片使用。
MongoDB 虽然已经在 4.2 开始全面支持了多文档事务,但并不代表大家应该毫无节制地使用它。相反,对事务的使用原则应该是:能不用尽量不用。 通过合理地设计文档模型,可以规避绝大部分使用事务的必要性。
使用事务的原则
无论何时,事务的使用总是能避免则避免;
模型设计先于事务,尽可能用模型设计规避事务;
不要使用过大的事务(尽量控制在 1000 个文档更新以内);
当必须使用事务时,尽可能让涉及事务的文档分布在同一个分片上,这将有效地提高效率;
MongoDB对事务支持
使用方法
writeConcern
writeConcern 决定一个写操作落到多少个节点上才算成功。MongoDB支持客户端灵活配置写入策略(writeConcern),以满足不同场景的需求
语法格式:{ w: <value>, j: <boolean>, wtimeout: <number> }
w: 数据写入到number个节点才向用客户端确认
{w: 0} :对客户端的写入不需要发送任何确认,适用于性能要求高,但不关注正确性的场景
{w: 1} :默认的writeConcern,数据写入到Primary就向客户端发送确认
{w: “majority”}: 数据写入到副本集大多数成员后向客户端发送确认,适用于对数据安全性要求比较高的场景,该选项会降低写入性能
j: 写入操作的journal持久化后才向客户端确认
默认为 {j: false},如果要求Primary写入持久化了才向客户端确认,则指定该选项为true
wtimeout: 写入超时时间,仅w的值大于1时有效
当指定 {w: }时,数据需要成功写入number个节点才算成功,如果写入过程中有节点故障,可能导致这个条件一直不能满足,从而一直不能向客户端发送确认结果,针对这种情况,客户端可设置wtimeout选项来指定超时时间,当写入过程持续超过该时间仍未结束,则认为写入失败
注意事项
- 虽然多于半数的 writeConcern 都是安全的,但通常只会设置 majority,因为这是等待写入延迟时间最短的选择;
- 不要设置 writeConcern 等于总节点数,因为一旦有一个节点故障,所有写操作都将失败;
- writeConcern 虽然会增加写操作延迟时间,但并不会显著增加集群压力,因此无论是否等待,写操作最终都会复制到所有节点上。设置 writeConcern 只是让写操作等待复制后再返回而已;
- 应对重要数据应用 {w: “majority”},普通数据可以应用 {w: 1} 以确保最佳性能。
在读取数据的过程中我们需要关注以下两个问题:
- 从哪里读?
- 什么样的数据可以读?
readPreference
从哪里读 ?
readPreference决定使用哪一个节点来满足正在发起的读请求。可选值包括:
- primary: 只选择主节点,默认模式;
- primaryPreferred:优先选择主节点,如果主节点不可用则选择从节点;
- secondary:只选择从节点;
- secondaryPreferred:优先选择从节点, 如果从节点不可用则选择主节点;
- nearest:根据客户端对节点的 Ping 值判断节点的远近,选择从最近的节点读取。
readPreference 场景举例
- 用户下订单后马上将用户转到订单详情页——primary/primaryPreferred。因为此时从节点可能还没复制到新订单;
- 用户查询自己下过的订单——secondary/secondaryPreferred。查询历史订单对 时效性通常没有太高要求;
- 生成报表——secondary。报表对时效性要求不高,但资源需求大,可以在从节点单独处理,避免对线上用户造成影响;
- 将用户上传的图片分发到全世界,让各地用户能够就近读取——nearest。每个地区的应用选择最近的节点读取数据。
readPreference 配置
从节点读测试
扩展:Tag
readPreference 只能控制使用一类节点。Tag 则可以将节点选择控制到一个或几个节点。考虑以下场景:
- 一个 5 个节点的复制集;
- 3 个节点硬件较好,专用于服务线上客户;
- 2 个节点硬件较差,专用于生成报表;
可以使用 Tag 来达到这样的控制目的:
- 为 3 个较好的节点打上 {purpose: "online"};
- 为 2 个较差的节点打上 {purpose: "analyse"};
- 在线应用读取时指定 online,报表读取时指定 analyse。
注意事项
指定 readPreference 时也应注意高可用问题。例如将 readPreference 指定 primary,则发生故障转移不存在 primary 期间将没有节点可读。如果业务允许,则应选择 primaryPreferred;
使用 Tag 时也会遇到同样的问题,如果只有一个节点拥有一个特定 Tag,则在这个节点失效时将无节点可读。这在有时候是期望的结果,有时候不是。例如:
如果报表使用的节点失效,即使不生成报表,通常也不希望将报表负载转移到其他节点上,此时只有一个节点有报表 Tag 是合理的选择;
如果线上节点失效,通常希望有替代节点,所以应该保持多个节点有同样的 Tag;
Tag 有时需要与优先级、选举权综合考虑。例如做报表的节点通常不会希望它成为主节点,则优先级应为 0
readConcern
在 readPreference 选择了指定的节点后,readConcern 决定这个节点上的数据哪些是可读的,类似于关系数据库的隔离级别。可选值包括
- available:读取所有可用的数据;
- local:读取所有可用且属于当前分片的数据;
- majority:读取在大多数节点上提交完成的数据;
- linearizable:可线性化读取文档,仅支持从主节点读;
- snapshot:读取最近快照中的数据,仅可用于多文档事务;
readConcern: local 和 available
在复制集中 local 和 available 是没有区别的,两者的区别主要体现在分片集上
考虑以下场景
一个 chunk x 正在从 shard1 向 shard2 迁移;
整个迁移过程中 chunk x 中的部分数据会在 shard1 和 shard2 中同时存在,但源分片 shard1仍然是chunk x 的负责方:
所有对 chunk x 的读写操作仍然进入 shard1;
config 中记录的信息 chunk x 仍然属于 shard1;
此时如果读 shard2,则会体现出 local 和 available 的区别:
local:只取应该由 shard2 负责的数据(不包括 x);
available:shard2 上有什么就读什么(包括 x);
注意事项
虽然看上去总是应该选择 local,但毕竟对结果集进行过滤会造成额外消耗。在一些无关紧要的场景(例如统计)下,也可以考虑 available;
MongoDB <=3.6 不支持对从节点使用 {readConcern: "local"};
从主节点读取数据时默认 readConcern 是 local,从从节点读取数据时默认readConcern 是 available(向前兼容原因)
readConcern: majority
只读取大多数据节点上都提交了的数据。考虑如下场景:
1. 集合中原有文档 {x: 0};
2. 将x值更新为 1;
1. 集合中原有文档 {x: 0};
2. 将x值更新为 1;
如果在各节点上应用 {readConcern: "majority"} 来读取数据:
考虑 t3 时刻的 Secondary1,此时
- 对于要求 majority 的读操作,它将返回 x=0;
- 对于不要求 majority 的读操作,它将返回 x=1;
如何实现?
节点上维护多个 x 版本(MVCC 机制),MongoDB 通过维护多个快照来链接不同的版本:
节点上维护多个 x 版本(MVCC 机制),MongoDB 通过维护多个快照来链接不同的版本:
- 每个被大多数节点确认过的版本都将是一个快照;
- 快照持续到没有人使用为止才被删除;
readConcern: majority 与脏读
MongoDB 中的回滚:
- 写操作到达大多数节点之前都是不安全的,一旦主节点崩溃,而从节点还没复制到该次操作,刚才的写操作就丢失了;
- 把一次写操作视为一个事务,从事务的角度,可以认为事务被回滚了。
所以从分布式系统的角度来看,事务的提交被提升到了分布式集群的多个节点级别的“提交”,而不再是单个节点上的“提交”。
在可能发生回滚的前提下考虑脏读问题:
如果在一次写操作到达大多数节点前读取了这个写操作,然后因为系统故障该操作回滚了,则发生了脏读问题;使用 {readConcern: "majority"} 可以有效避免脏读
在可能发生回滚的前提下考虑脏读问题:
如果在一次写操作到达大多数节点前读取了这个写操作,然后因为系统故障该操作回滚了,则发生了脏读问题;使用 {readConcern: "majority"} 可以有效避免脏读
如何安全的读写分离
考虑如下场景:
1. 向主节点写入一条数据
2. 立即从从节点读取这条数据
思考: 如何保证自己能够读到刚刚写入的数据?
readConcern: linearizable
只读取大多数节点确认过的数据。和 majority 最大差别是保证绝对的操作线性顺序
- 在写操作自然时间后面的发生的读,一定可以读到之前的写
- 只对读取单个文档时有效;
- 可能导致非常慢的读,因此总是建议配合使用 maxTimeMS
readConcern: snapshot
{readConcern: “snapshot”} 只在多文档事务中生效。将一个事务的 readConcern 设置为 snapshot,将保证在事务中的读
- 不出现脏读;
- 不出现不可重复读;
- 不出现幻读。
因为所有的读都将使用同一个快照,直到事务提交为止该快照才被释放
事务隔离级别
事务完成前,事务外的操作对该事务所做的修改不可访问
如果事务内使用 {readConcern: “snapshot”},则可以达到可重复读 Repeatable Read
事务超时
在执行事务的过程中,如果操作太多,或者存在一些长时间的等待,则可能会产生如下异常
原因在于,默认情况下MongoDB会为每个事务设置1分钟的超时时间,如果在该时间内没有提交,就会强制将其终止。该超时时间可以通过transactionLifetimeLimitSecond变量设定
事务写机制
MongoDB 的事务错误处理机制不同于关系数据库:
- 当一个事务开始后,如果事务要修改的文档在事务外部被修改过,则事务修改这个文档时会触发 Abort 错误,因为此时的修改冲突了。 这种情况下,只需要简单地重做事务就可以了;
- 如果一个事务已经开始修改一个文档,在事务以外尝试修改同一个文档,则事务以外的修改会等待事务完成才能继续进行。
写冲突测试
注意事项
可以实现和关系型数据库类似的事务场景
必须使用与 MongoDB 4.2 兼容的驱动;
事务默认必须在 60 秒(可调)内完成,否则将被取消;
涉及事务的分片不能使用仲裁节点;
事务会影响 chunk 迁移效率。正在迁移的 chunk 也可能造成事务提交失败(重试 即可);
多文档事务中的读操作必须使用主节点读;
readConcern 只应该在事务级别设置,不能设置在每次读写操作上。
小节
- available:读取所有可用的数据
- local:读取所有可用且属于当前分片的数据,默认设置
- majority:数据读一致性的充分保证,可能你最需要关注的
- linearizable:增强处理 majority 情况下主节点失联时候的例外情况
- snapshot:最高隔离级别,接近于关系型数据库的Serializable
MongoDB建模调优
MongoDB的开发规范
(1)命名原则。数据库、集合命名需要简单易懂,数据库名使用小写字符,集合名称使用统一命名风格,可以统一大小写或使用驼峰式命名。数据库名和集合名称均不能超过64个字符。
(2)集合设计。对少量数据的包含关系,使用嵌套模式有利于读性能和保证原子性的写入。对于复杂的关联关系,以及后期可能发生演进变化的情况,建议使用引用模式。
(3)文档设计。避免使用大文档,MongoDB的文档最大不能超过16MB。如果使用了内嵌的数组对象或子文档,应该保证内嵌数据不会无限制地增长。在文档结构上,尽可能减少字段名的长度,MongoDB会保存文档中的字段名,因此字段名称会影响整个集合的大小以及内存的需求。一般建议将字段名称控制在32个字符以内。
(4)索引设计。在必要时使用索引加速查询。避免建立过多的索引,单个集合建议不超过10个索引。MongoDB对集合的写入操作很可能也会触发索引的写入,从而触发更多的I/O操作。无效的索引会导致内存空间的浪费,因此有必要对索引进行审视,及时清理不使用或不合理的索引。遵循索引优化原则,如覆盖索引、优先前缀匹配等,使用explain命令分析索引性能。
(5)分片设计。对可能出现快速增长或读写压力较大的业务表考虑分片。分片键的设计满足均衡分布的目标,业务上尽量避免广播查询。应尽早确定分片策略,最好在集合达到256GB之前就进行分片。如果集合中存在唯一性索引,则应该确保该索引覆盖分片键,避免冲突。为了降低风险,单个分片的数据集合大小建议不超过2TB。
(6)升级设计。应用上需支持对旧版本数据的兼容性,在添加唯一性约束索引之前,对数据表进行检查并及时清理冗余的数据。新增、修改数据库对象等操作需要经过评审,并保持对数据字典进行更新。
(7)考虑数据老化问题,要及时清理无效、过期的数据,优先考虑为系统日志、历史数据表添加合理的老化策略。
(8)数据一致性方面,非关键业务使用默认的WriteConcern:1(更高性能写入);对于关键业务类,使用WriteConcern:majority保证一致性(性能下降)。如果业务上严格不允许脏读,则使用ReadConcern:majority选项。
(9)使用update、findAndModify对数据进行修改时,如果设置了upsert:true,则必须使用唯一性索引避免产生重复数据。
(10)业务上尽量避免短连接,使用官方最新驱动的连接池实现,控制客户端连接池的大小,最大值建议不超过200。
(11)对大量数据写入使用Bulk Write批量化API,建议使用无序批次更新。
(12)优先使用单文档事务保证原子性,如果需要使用多文档事务,则必须保证事务尽可能小,一个事务的执行时间最长不能超过60s。
(13)在条件允许的情况下,利用读写分离降低主节点压力。对于一些统计分析类的查询操作,可优先从节点上执行。
(14)考虑业务数据的隔离,例如将配置数据、历史数据存放到不同的数据库中。微服务之间使用单独的数据库,尽量避免跨库访问。
(15)维护数据字典文档并保持更新,提前按不同的业务进行数据容量的规划。
MongoDB调优
三大导致MongoDB性能不佳的原因:
1. 慢查询
2. 阻塞等待
3. 硬件资源不足
1,2通常是因为模型/索引设计不佳导致的
排查思路:按1-2-3依次排查
1. 慢查询
2. 阻塞等待
3. 硬件资源不足
1,2通常是因为模型/索引设计不佳导致的
排查思路:按1-2-3依次排查
影响MongoDB性能的因素
MongoDB数据建模小案例(见笔记)
MongoDB性能监控工具(见笔记)
性能问题排查参考案例
MongoDB高级集群架构
两地三中心
全球多写集群架构
备份恢复
mongoexport/mongoimport 导入/导出的是JSON格式,而 mongodump/mongorestore 导入/导出的是BSON格式
JSON可读性强但体积较大,BSON则是二进制文件,体积小但对人类几乎没有可读性。
在一些mongodb版本之间,BSON格式可能会随版本不同而有所不同,所以不同版本之间用mongodump/mongorestore可能不会成功,具体要看版本之间的兼容性。当无法使用BSON进行跨版本的数据迁移的时候,使用JSON格式即mongoexport/mongoimport是一个可选项。跨版本的mongodump/mongorestore并不推荐,实在要做请先检查文档看两个版本是否兼容(大部分时候是的)。
JSON虽然具有较好的跨版本通用性,但其只保留了数据部分,不保留索引,账户等其他基础信息。使用时应该注意。
在一些mongodb版本之间,BSON格式可能会随版本不同而有所不同,所以不同版本之间用mongodump/mongorestore可能不会成功,具体要看版本之间的兼容性。当无法使用BSON进行跨版本的数据迁移的时候,使用JSON格式即mongoexport/mongoimport是一个可选项。跨版本的mongodump/mongorestore并不推荐,实在要做请先检查文档看两个版本是否兼容(大部分时候是的)。
JSON虽然具有较好的跨版本通用性,但其只保留了数据部分,不保留索引,账户等其他基础信息。使用时应该注意。
JSON格式:mongoexport/mongoimport
JSON可读性强但体积较大,JSON虽然具有较好的跨版本通用性,但其只保留了数据部分,不保留索引,账户等其他基础信息。
BSON格式:mongoexport/mongoimport
BSON则是二进制文件,体积小但对人类几乎没有可读性
系统设计:模式构建(见笔记)
0 条评论
下一页