数据库
首页 > 数据库> > MongoDB全方位知识图谱

MongoDB全方位知识图谱

作者:互联网

 

导语 | MongoDB是一个强大的分布式存储引擎,天然支持高可用、分布式和灵活设计。MongoDB的一个很重要的设计理念是:服务端只关注底层核心能力的输出,至于怎么用,就尽可能的将工作交个客户端去决策。这也就是MongoDB灵活性的保证,但是灵活性带来的代价就是使用成本的提升。与MySql相比,想要用好MongoDB,减少在项目中出问题,用户需要掌握的东西更多。本文致力于全方位的介绍MongoDB的理论和应用知识,目标是让大家可以通过阅读这篇文章之后能够掌握MongoDB的常用知识,具备在实际项目中高效应用MongoDB的能力。本文既有MongoDB基础知识也有相对深入的进阶知识,同时适用于对MonogDB感兴趣的初学者或者希望对MongoDB有更深入了解的业务开发者。

 

一、前言

 

以下是笔者在学习和使用MongoDB过程中总结的MongoDB知识图谱。本文将按照一下图谱中依次介绍MongoDB的一些核心内容。由于能力和篇幅有限,本文并不会对图谱中全部内容都做深入分析,后续将会针对特定条目做专门的分析。同时,如果图谱和内容中有错误或疏漏的地方,也请大家随意指正,笔者这边会积极修正和完善。

 

本文按照图谱从以下3个方面来介绍MongoDB相关知识:

 

 

 

 

图片

 

 

二、基础知识

 

MongoDB是基于文档的NoSql存储引擎。MongoDB的数据库管理由数据库、Collection(集合,类似MySql的表)、Document(文档,类似MySQL的行)组成,每个Document都是一个类JSON结构BSON结构数据。

 

MongoDB的核心特性是:No Schema、高可用、分布式(可平行扩展),另外MongoDB自带数据压缩功能,使得同样的数据存储所需的资源更少。本节将会依次介绍这些特性的基本知识,以及MongoDB是如何实现这些能力的。

 

(一)No Schema

 

MongoDB是文档型数据库,其文档组织结构是BSON(Binary Serialized Document Format) 是类JSON的二进制存储格式,数据组织和访问方式完全和JSON一样。支持动态的添加字段、支持内嵌对象和数组对象,同时它也对JSON做了一些扩充,如支持Date和BinData数据类型。正是BSON这种字段灵活管理能力赋予了Mongo的No Schema或者Schema Free的特性。

 

No Schema特性带来的好处包括:

 

 

 

 

MongoDB在提供No Schema特性基础上,提供了部分可选的Schema特性:Validation。其主要功能有包括:

 

 

上面的字段包含内嵌文档的,也就是说,你可以指定Document内任意一层JSON文件的字段属性。validator的值有两种,一种是简单的JSON Object,另一种是通过关键字$jsonSchema指定。以下是简单示例,想了解更多请参考官方文档:MongoDB JSON Schema详解

(https://www.docs4dev.com/docs/zh/mongodb/v3.6/reference/reference-operator-query-jsonSchema.html)

 

方式一:

 

db.createCollection("saky_test_validation",{validator:  {    $and:[      {name:{$type: "string"}},      {status:{$in:["INIT","DEL"]}}]  }})

 

方式二:

 

db.createCollection("saky_test_validation", {   validator: {      $jsonSchema: {         bsonType: "object",         required: [ "name", "status", ],         properties: {            name: {               bsonType: "string",               description: "must be a string and is required"            },            status: {               enum: [ "INIT", "DEL"],                description: "can only be one of the enum values and is required"            }} }})

 

 

(二)MongoDB的高可用

 

高可用是MongoDB最核心的功能之一,相信很多同学也是因为这一特性才想深入了解它的。那么本节就来说下MongoDB通过哪些方式来实现它的高可用,然后给予这些特性我们可以实现什么程度的高可用。    

 

相信一旦提到高可用,浮现在大家脑海里会有如下几个问题:

 

 

 

 

那么,带着这些问题,我们继续看下去,看完大家应该会对这些问题有所了解了。

 

 

 

MongoDB高可用的基础是复制集群,复制集群本质来说就是一份数据存多份,保证一台机器挂掉了数据不会丢失。一个副本集至少有3个节点组成:

 

 

 

 

从上面的节点类型可以看出,一个三节点的复制集群可能是PSS或者PSA结构。PSA结构优点是节约成本,但是缺点是Primary挂掉之后,一些依赖 majority(多数)特性的写功能出问题,因此一般不建议使用。

 

复制集群确保数据一致性的核心设计是:

 

 

 

 

 

从上面4点我们可以得出 MongoDB 高可用的如下结论:

 

 

 

 

 

 

 

从上一小节发现,MongoDB的高可用机制在不同的场景表现是不一样的。实际上,MongoDB提供了一整套的机制让用户根据自己业务场景选择不同的策略。这里要说的就是MongoDB的读写策略,根据用户选取不同的读写策略,你会得到不同程度的数据可靠性和一致性保障。这些对业务开放者非常重要,因为你只有彻底掌握了这些知识,才能根据自己的业务场景选取合适的策略,同时兼顾读写性能和可靠性。

 

Write Concern——写策略    

 

控制服务端一次写操作在什么情况下才返回客户端成功,由两个参数控制:

 

 

 

Read Preference——读策略    

 

控制客户端从什么节点读取数据,默认为primary,具体参数及含义:

 

 

 

 

 

 

更多信息可参考MongoDB 官方文档

(https://www.mongodb.com/docs/v4.0/reference/read-preference/index.html?_ga=2.71414227.1531435120.1648536327-1778944104.1630835426)

 

Read Concern Level——读级别

 

这是一个非常有意思的参数,也是最不容易理解的异常参数。它主要控制的是读到的数据是不是最新的、是不是持久的,最新的和持久的是一对矛盾,最新的数据可能会被回滚,持久的数据可能不是最新的,这需要业务根据自己场景的容忍度做决策,前提是你的先知道有哪些,他们代表什么意义:

 

 

 

 

 

 

为了便于理解local和majority,这里引用一下MongoDB官网上的一张 WriteConcern=majority时写操作的过程图:

 

图片

 

通过这张图可以看出,不同节点在不同阶段看待同一条数据满足的level是不同的:

 

图片

 

 

(三)MongoDB的可扩展性——分片集群

 

水平扩展是MongoDB的另一个核心特性,它是MongoDB支持海量数据存储的基础。MongoDB天然的分布式特性使得它几乎可无限的横向扩展,你再也不用为MySQL分库分表的各种繁琐问题操碎心了。当然,我们这里不讨论MongoDB和其它存储引擎的对比,这个以后专门写下,这里只关注分片集群相关信息。

 

 

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

图片

 

其实分片集群的架构看起来和很多支持海量存储的设计很像,本质上都是将存储分片,然后在前面挂一个proxy做请求路由。但是,MongoDB的分片集群有个非常重要的特性是其它数据库没有的,这个特性就是数据均衡。数据分片一个绕不开的话题就是数据分布不均匀导致不同分片负载差异巨大,不能最大化利用集群资源。

 

MongoDB的数据均衡的实现方式是:






 

关于chunk更加深入的知识会在后面进阶知识里面讲解,这里就不展开了。

 

 

 

MongoDB支持两种分片算法来满足不同的查询需求:



 

区间分片示例:

图片

hash分片示例:

图片

从上面两张图可以看出:





 

 

(四)数据压缩

 

MongoDB的另外一个比较重要的特性是数据压缩,MongoDB会自动把客户数据压缩之后再落盘,这样就可以节省存储空间。MongoDB的数据压缩算法有多种:





 

现在推荐的MongoDB版本是4.0,在这个版本下推荐使用snappy算法,虽然zlib有更高的压缩比,但是读写会有一定的性能波动,不适合核心业务,但是比较适合流水、日志等场景。

 

 

三、应用接入

 

在掌握第一部分的基础上,基本上对MongoDB有一个比较直观的认识了,知道它是什么,有什么优势,适合什么场景。在此基础上,我们基本上已经可以判定MongoDB是否适合自己的业务了。如果适合,那么接下来就需要考虑怎么将其应用到业务中。在此之前,我们还得先对MongoDB的性能有个大致的了解,这样才能根据业务情况选取合适的配置。

 

(一)基本性能测试

 

在使用MongoDB之前,需要对其功能和性能有一定的了解,才能判定是否符合自己的业务场景,以及需要注意些什么才能更好的使用。笔者这边对其做了一些测试,本测试是基于自己业务的一些数据特性,而且这边使用的是分片集群。因此有些测试项不同数据会有差异,如压缩比、读写性能具体值等。但是也有一些是共性的结论,如写性能随数据量递减并最终区域平稳。

 

 

对比了同样数据在Mongo和MySQL下压缩比对比,可以看出snapy算法大概是MySQL的3倍,zlib大概是6倍。

 

图片

 

 

分片集群写性能在测试之后得到如下结论,这里分片是4核8G的配置:





图片

 

 

分片集群的读分为三年种情况:按shardkey查询、按索引查询、其他查询。下面这些测试数据都是在单分片2亿以上的数据,这个时候cache已经不能完全换成业务数据了,如果数据量很小,数据全在cache这个性能应该会很好。

 

 

 

 

Mongos有点特殊情况要注意的,就是客户端请求会到哪个Mongos是通过客户端ip的hash值决定的,因此同一个客户端所有请求一定会到同一个 Mongos,如果客户端过少的时候还会出现Mongos负载不均问题。

 

 

(二)分片选择

 

在了解了MongoDB的基本性能数据之后,就可以根据自己的业务需求选取合适的配置了。如果是分片集群,其中最重要的就是分片选取,包括:

 

 

 

 

关于前面两点,其实在知道各种性能参数之后就很简单了,前人已经总结出了相关的公式,我这里就简单把图再贴一下。

 

图片

 

 

(三)spring-data-mongo

 

MonogDB官方提供了各种语言的Client,这些Client是对mongo原始命令的封装。笔者这边是使用的java,因此并未直接使用MongoDB官方的客户端,而是经过二次封装之后的spring-data-mongo。好处是可以不用他关心底层的设计如连接管理、POJO转换等。

 

 

spring-data-mongo的使用方式非常简单。

 

第一步:引入jar包

 

        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-data-mongodb</artifactId>        </dependency>

 

第二步:ymal配置

 

spring:  data:    mongodb:      host: {{.MONGO_HOST}}      port: {{.MONGO_PORT}}      database: {{.MONGO_DB}}      username: {{.MONGO_USER}}      password: {{.MONGO_PASS}}

 

这里有个两个要注意:



 

关于配置,跟多的可以在IDEA里面搜索MongoAutoConfiguration查看源码,具体就是这个类:

org.springframework.boot.autoconfigure.mongo.MongoProperties

 

关于自己初始化MongoTemplate的方式是:

 

@Configurationpublic class MyMongoConfig {    @Primary    @Bean    public MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory,  MongoConverter mongoConverter){        MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory,mongoConverter);        mongoTemplate.setWriteConcern(WriteConcern.MAJORITY);        return mongoTemplate;    }}

 

第三步:使用MongoTemplate

 

在完成上面这些之后,就可以在代码里面注入MongoTemplate,然后使用各种增删改查接口了。

 

 

 

MongoDB Client的批量操作有两种方式:

 

 

 

bulkOps的方式会比另外一种方式在性能上低一些。

 

这两种方式到引擎层面具体执行时都是一条条语句单独执行,它们有一个很重要的参数:ordered,这个参数的作用是控制批量操作在引擎内最终执行时是并行的还是穿行的。其默认值是true。

 

 

 

显然,false模式下插入耗时会低一些,但是MongoTemplate的insertAll 函数是在内部写死的true。因此,如果想用false模式,需要自己继承MongoTemplate然后重写里面的insertDocumentList方法。

 

 public class MyMongoTemplate extends MongoTemplate {    @Override    protected List<Object> insertDocumentList(String collectionName, List<Document> documents) {                .........                InsertManyOptions options = new InsertManyOptions();                options = options.ordered(false);  // 要自己初始化一个这对象,然后设置为false                long begin = System.currentTimeMillis();                if (writeConcernToUse == null) {                    collection.insertMany(documents, options); // options这里默认是null                } else {              collection.withWriteConcern(writeConcernToUse).insertMany(documents,options);                }                return null;            });            return MappedDocument.toIds(documents);    }

 

 

 

因为MongoDB真的将太多自主性交给的客户端来决策,因此如果对其了解不够,真的会很容易踩坑。这里例举一些常见的坑,避免大家遇到。

 

预分片

 

这个问题的常见表现就是:为啥我的数据分布很随机了,但是分片集群的MongoDB插入性能还是这么低?

 

首先我们说下预分片是什么,预分片就是提前把shard key的空间划分成若干段,然后把这些段对应的chunk创建出来。那么,这个和插入性能的关系是什么呢?

 

我们回顾下前面说到的chunk知识,其中有两点需要注意:



 

那么,很明显,问题就是出在这了,chunk分裂和chunk迁移都是比较耗资源的,必然就会影响插入性能。

 

因此,如果提前将个分片上的chunk创建好,就能避免频繁的分裂和迁移chunk,进而提升插入性能。预分片的设置方式为:

 

 sh.shardCollection("saky_db.saky_table", {"_id": "hashed"}, false,{numInitialChunks:8192*分片数})

 

numInitialChunks的最大值为8192*分片数

 

内存排序

 

这个是一个不容易被注意到的问题,但是使用MongoDB时一定要注意的就是避免任何查询的内存操作,因为用MongoDB的很多场景都是海量数据,这个情况下任何内存操作的成本都可能是非常高昂甚至会搞垮数据库的,当然MongoDB为了避免内存操作搞垮它,是有个阈值,如果需要内存处理的数据超过阈值它就不会处理并报错。

 

继续说内存排序问题,它的本质是索引问题。MongoDB的索引都是有序的,正序或者逆序。如果我们有一个Collection里面记录了学生信息,包括年龄和性别两个字段。然后我们创建了这样一个复合索引:

 

{gender: 1, age: 1} // 这个索引先按性别升序排序,相同的再按年龄升序排序

 

当这个时候,如果你排序顺序是下面这样的话,就会导致内存排序,如果数据两小到没事,如果非常大的话就会影响性能。避免内存排序就是要查询的排序方式要和索引的相同。

 

{gender: 1, age: -1} // 这个索引先按性别升序排序,相同的再按年龄降序排序

 

链式复制

 

链式复制是指副本集的各个副本在复制数据时,并不是都是从Primary节点拉oplog,而是各个节点排成一条链,依次复制过去。

 

优点:避免大量Secondary从Primary拉oplog,影响Primary的性能。

 

缺点:如果WriteConcern=majority,那么链式复制会导致写操作耗时更长。

 

因此,是否开启链式复制就是一个成本与性能的平衡,默认是开启链式复制的:

 

 

 

链式复制关闭时,节点数据复制对Primary节点性能影响程度目前没有专业测试过,因此不能评判到底开启还是关闭好,这边数据库同学从他们的经验来建议是关闭,因此我这边是关闭的,如果有用到MongoDB的可以考虑关掉。

 

 

四、进阶知识

 

接下来终于到了最重要的部分了,这部分将讲解一些MongoDB的一些高级功能和底层设计。虽然不了解这些也能使用,但是如果想用好MongoDB,这部分知识是必须掌握的。

 

(一)存储引擎Wired Tiger

 

说到MongoDB最重要的知识,其存储引擎Wired Tiger肯定是要第一个说的。因为MongoDB的所有功能都是依赖底层存储引擎实现的,掌握了存储引擎的核心知识,有利于我们理解MongoDB的各种功能。存储引擎的核心工作是管理数据如何在磁盘和内存上读写,从MongoDB 3.2开始支持多种存储引擎:Wired Tiger,MMAPv1和In-Memory,其中默认为Wired Tiger。

 

 

B+Tree

 

存储引擎最核心的功能就是完成数据在客户端-内存-磁盘之间的交互。客户端是不可控的,因此如何设计一个高效的数据结构和算法,实现数据快速在内存和磁盘间交互就是存储引擎需要考虑的核心问题。目前大多少流行的存储引擎都是基于B/B+Tree和LSM(Log Structured Merge) Tree来实现,至于他们的优势和劣势,以及各种适用的场景,暂时超出了笔者的能力,后面到是有兴趣去研究一下。

 

Oracle、SQL Server、DB2、MySQL (InnoDB) 这些传统的关系数据库依赖的底层存储引擎是基于B+ Tree开发的;而像Cassandra、Elasticsearch (Lucene)、Google Bigtable、Apache HBase、LevelDB和RocksDB这些当前比较流行的NoSQL数据库存储引擎是基于LSM开发的。MongoDB虽然是NoSQL的,但是其存储引擎Wired Tiger 却是用的B+Tree,因此有种说法是MongoDB是最接近SQL的NoSQL存储引擎。好了,我们这里知道Wired Tiger的存储结构是B+Tree就行了,至于什么是B+Tree,它有些啥优势网都有很多文章,这里就不在赘述了。

 

Page

 

Wired Tiger在内存和磁盘上的数据结构都B+Tree,B+的特点是中间节点只有索引,数据都是存在叶节点。Wired Tiger管理数据结构的基本单元Page。

图片

上图是Page在内存中的数据结构,是一个典型的B+ Tree,Page上有3个重要的list WT_ROW、WT_UPDATE、WT_INSERT。这个Page的组织结构和Page的3个list对后面理解cache、checkpoint等操作很重要:





 

上面说了Page的基本结构,接下来再看下Page的生命周期和状态扭转,这个生命周期和Wired Tiger的缓存息息相关。

 

图片

Page在磁盘和内存中的整个生命周期状态机如上图:








 

其中两个比较重要的过程是reconcile和evict。

 

其中reconcile发生在checkpoint的时候,将内存中Page的修改转换成磁盘需要的B+ Tree结构。前面说了Page的WT_UPDATE和WT_UPDATE列表存储了数据被加载到内存之后的修改,类似一个内存级的oplog,而数据在磁盘中时显然不可能是这样的结构。因此reconcile会新建一个Page来将修改了的数据做整合,然后原Page就会被discarded,新page会被刷新到磁盘,同时加入LRU队列。

 

evict是内存不够用了或者脏数据过多的时候触发的,根据LRU规则淘汰内存 Page到磁盘。

 

 

 

MongoDB不是内存数据库,但是为了提供高效的读写操作存储引擎会最大化的利用内存缓存。MongoDB的读写性能都会随着数据量增加到了某个点出现近乎断崖式跌落最终趋于稳定。这其中的根本原因就是内存是否能cover住全部的数据,数据量小的时候是纯内存读写,性能肯定非常好,当数据量过大时就会触发内存和磁盘间数据的来回交换,导致性能降低。所以,如果在使用MongoDB时,如果发现自己某些操作明显高于常规,那么很大可能是它触发了磁盘操作。

 

接下来说下MongoDB的存储引擎Wired Tiger是怎样利用内存cache的。首先,Wired Tiger会将整个内存划分为3块:




 

内存分配大小一般是不建议改的,除非你确实想把自己全部数据放到内存,并且主够的引擎知识。

 

引擎cache和文件系统cache在数据结构上是不一样的,文件系统cache是直接加载的内存文件,是经过压缩的数据,可以占用更少的内存空间,相对的就是数据不能直接用,需要解压;而引擎中的数据就是前面提到的B+ Tree,是解压后的,可以直接使用的数据,占有的内存会大一些。

 

Evict

 

就算内存再大它与磁盘间的差距也是数据量级的差异,随着数据增长也会出现内存不够用的时候。因此内存管理一个很重要的操作就是内存淘汰evict。内存淘汰时机由eviction_target(内存使用量)和eviction_dirty_target(内存脏数据量)来控制,而内存淘汰默认是有后台的evict线程控制的。但是如果超过一定阈值就会把用户线程也用来淘汰,会严重影响性能,应该避免这种情况。用户线程参与evict的原因,一般是大量的写入导致磁盘IO抗不住了,需要控制写入或者更换磁盘。

 

图片

 

前面说过,MongoDB的读写都是操作的内存,因此必须要有一定的机制将内存数据持久化到磁盘,这个功能就是Wired Tiger的checkpoint来实现的。checkpoint实现将内存中修改的数据持久化到磁盘,保证系统在因意外重启之后能快速恢复数据。checkpoint本身数据也是会在每次checkpoint执行时落盘持久化的。

图片

 

一个checkpoint 就是一个内存B+ Tree,其结构就是前面提到的Page组成的树,它有几个重要的字段:

 

 

 

 

图片

checkpoint的大致流程入上图所述:


在系统启动或者集合文件打开时,从磁盘加载最新的checkpoint。
根据checkpoint的file size truncate文件。因为只有checkpoint确认的数据才是真正持久化的数据,它后面的数据可能是最新checkpoint之后到宕机之间的数据,不能直接用,需要通过Journal日志来回放。
根据checkpoint构建内存的B+Tree。
数据库run起来之后,各种修改操作都是操作checkpoint的B+Tree,并且会checkpoint会有专门的list来记录这些修改和新增的page。
在60s一次的checkpoint执行时,会创建新的checkpoint,并且将旧的 checkpoint数据合并过来。然后执行reconcile将修改的数据刷新到磁盘,并删除旧的checkpoint。这时候会清空allocated,discarded里面的 page,并且将空闲的page加到available里面。

 

 

(二)Chunk

 

Chunk为啥要单独出来说一下呢,因为它是MongoDB分片集群的一个核心概念,是使用和理解分片集群读写实现的最基础的概念。

 

 

首先,说下chunk是什么,chunk本质上就是由一组Document组成的逻辑数据单元。它是分片集群用来管理数据存储和路由的基本单元。具体来说就是,分片集群不会记录每条数据在哪个分片上,这不现实,它只会记录哪一批(一个chunk)数据存储在哪个分片上,以及这个chunk包含哪些范围的数据。而数据与chunk之间的关联是有数据的shard key的分片算法f(x)的值是否在chunk的起始范围来确定的。

 

前面说过,分片集群的chunk信息是存在Config里面的,而Config本质上是一个复制集群。如果你创建一个分片集群,那么你默认会得到两个库,admin和config,其中config库对应的就是分片集群架构里面的Config。其中的包含一个Collection chunks里面记录的就是分片集群的全部chunk信息,具体结构如下图:

 

图片

 

chunk的几个关键属性:







 

 

 

chunk是分片集群管理数据的基本单元,本身有一个大小,那么随着chunk内的数据不断新增,最终大小会超过限制,这个时候就需要把chunk拆分成2个,这个就chunk的分裂。

 

chunk的大小不能太大也不能太小。太大了会导致迁移成本高,太小了有会触发频繁分裂。因此它需要一个合理的范围,默认大小是64M,可配置的取值范围是1M~1024M。这个大小一般来说是不用专门配置的,但是也有特例:

 

 

 

导致chunk分裂有两个条件,达到任何一个都会触发:

 

 

 

 

 

MongoDB一个区别于其他分布式数据库的特性就是自动数据均衡。

 

chunk分裂是MongoDB保证数据均衡的基础:数据的不断增加,chunk不断分裂,如果数据不均匀就会导致不同分片上的chunk数目出现差异,这就解决了分片集群的数据不均匀问题发现。然后就可以通过将chunk从数据多的分片迁移到数据少的分片来实现数据均衡,这个过程就是rebalance。

 

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

图片

执行rebalance是有几个前置条件的:

 



图片

rebalance为了尽快完成数据迁移,其设计是尽最大努力迁移,因此是非常消耗系统资源的,在系统配置不高的时候会影响系统正常业务。因此,为了减少其影响需要:




 

 

(三)一致性/高可用

 

分布式系统必须要面对的一个问题就是数据的一致性和高可用,针对这个问题有一个非常著名的理论就是CAP理论。CAP理论的核心结论是:一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。关于CAP理论在网上有非常多的论述,这里也不赘述。

 

CAP理论提出了分布式系统必须面临的问题,但是我们也不可能因为这个问题就不用分布式系统。因此,BASE(Basically Available基本可用、Soft state软状态、Eventually consistent最终一致性)理论被提出来了。BASE理论是在一致性和可用性上的平衡,现在大部分分布式系统都是基于 BASE理论设计的,当然MongoDB也是遵循此理论的。

 

 

MongoDB为了保证可用性和分区容错性,采用的是副本集的方式,这种模式就必须要解决的一个问题就是怎样快速在系统启动和Primary发生异常时选取一个合适的主节点。这里潜在着多个问题:





 

Raft协议

 

MongoDB的选举算法是基于Raft协议的改进,Raft协议将分布式集群里面的节点有3种状态:




 

节点的状态变化是:正常情况下只有一个leader和多个flower,当leader挂掉了,那么flower里面就会有部分节点成为candidate参与竞选。当某个candidate竞选成功之后就成为新的leader,而其他candidate回到flower状态。具体状态机如下:

图片

Raft协议中有两个核心RPC协议分别应用在选举阶段和正常阶段:



 

投票规则

 

Raft协议规定了在选举阶段的投票规则:



 

选举过程

 

一轮完整的选举过程包含如下内容:






 

catchup(追赶)

 

以上就是目前掌握的MongoDB的选举机制,其中有个问题暂时还未得到解答,就是最后一个,怎样确保选出的Primary是最合适的那一个。因为,从前面的协议来看,存在一个逻辑bug:由于flower转换成candidate是随机并行的,再加上先到先得的投票机制会导致选出一个次优的节点成为Primary。但是这一点应该是笔者自己掌握知识不够,应该是有相关机制保证的,怀疑是通过节点优先级实现的。这点也和相关同学确认过,因此这里暂定此问题不存在,等深入学习这里的细节之后补充其设计和实现。

 

针对Raft协议的这个问题,下来查询了一些资料,结论是:



 

 

 

MongoDB的主从同步机制是确保数据一致性和可靠性的重要机制。其同步的基础是oplog,类似MySQL的binlog,但是也有一些差异,oplog虽然叫log但并不是一个文件,而是一个集合(Collection)。同时由于 oplog 的并行写入,存在尾部乱序和空洞现象,具体来说就是oplog里面的数据顺序可能是和实际数据顺序不一致,并且存在时间的不连续问题。为了解决这个问题,MongoDB采用的是混合逻辑时钟(HLC)来解决的,HLC不止解决乱序和空洞问题,同时也是用来解决分布式系统上事务一致性的方案。

主从同步的本质实际上就是,Primary节点接收客户端请求,将更新操作写到oplog,然后Secondary从同步源拉取oplog并本地回放,实现数据的同步。

 

同步源选取

 

同步源是指节点拉取oplog的源节点,这个节点不一定是Primary,链式复制模式下就可能是任何节点。节点的同步源选取是一个非常复杂的过程,大致上来说是:





 

在同步源选取时有些特殊情况:




 

oplog拉取和回放

 

整个拉取和回放的逻辑非常复杂,这里根据自己的理解简化说明,如果想了解更多知识可以参考《MongoDB复制技术内幕》

 

节点有一个专门拉取oplog的线程,通过Exhausted cursor从同步源拉取 oplog。拉取下来之后,并不会执行回放执行,而是会将其丢到一个本地的阻塞队列中。

 

然后有多个具体的执行线程,从阻塞队列中取出oplog并执行。在取出过程中,同一个Collection的oplog一定会被同一个线程取出执行,线程会尽可能的合并连续的插入命令。

 

整个回放的执行过程,大致为先加锁,然后写本店oplog,然后将oplog刷盘(WAL机制),最后更新自己的最新opTime。

 

 

(四)索引

 

索引对任何数据库而言都是非常重要的一个功能。数据库支持的索引类型,决定的数据库的查询方式和应用场景。而正确的使用索引能够让我们最大化的利用数据库性能,同时避免不合理的操作导致的数据库问题,最常见的问题就是CPU或内存耗尽。

 

 

MongoDB的索引和MySql的索引有点不一样,它的索引在创建时必须指定顺序(1:升序,-1:降序),同时所有的集合都有一个默认索引 _id,这是一个唯一索引,类似MySql的主键。

 

MongoDB支持的索引类型有:







 

 

 

索引功能强大,但是也有很多限制,使用索引时一定要注意一些问题。

 

复合索引

 

复合索引有几个问题需要注意:

 



 

后台创建索引

 

在对一个已经拥有较大数据集的Collection创建索引时,建议通过创建命令参数指定后台创建,不会阻塞命令和意外中断。但是,在后台创建多个索引时,不能命令执行完就接着下一个。因为是后台创建,命令行虽然推出了,但是索引还没创建完。这个时候如果同事输入多个创建索引命令,会因为大量的写操作和数据复制导致系统cpu耗尽。这个时候需要观察系统监控,确定第一个索引创建完了再执行下一个。

 

 

explain是MongoDB的查询计划工具,和MySql的explain功能相同,都是用来分析一条语句的索引使用情况、影响行数、执行时间等。

 

explain有三种参数分别对应结果输出的三部分数据:




 

explain是一个非常有用的工具,建议在一个数据量较大的数据库上开发新功能时,一定要用explain分析一下自己的语句是否合理、索引是否合理,避免在项目上线之后出现问题。

 

 

 鸣谢

https://mp.weixin.qq.com/s/bhXPnLotUoQYJI61eORCfA

 

 

标签:MongoDB,chunk,全方位,图谱,内存,分片,数据,节点
来源: https://www.cnblogs.com/yakniu/p/16358494.html