mongodb知识点汇总

 

 

关于本书
作者张友东,阿⾥云⾼级技术专家,主要关注分布式存储与数据库等技术领域,先后参与淘宝分布式⽂件系统TFS、阿
⾥云数据库(POLARDB、MySQL、MongoDB、Redis ...)等项⽬的开发⼯作,致⼒于让开发者⽤上最好的云数据库服
务。
点击这⾥体验 MongoDB 云数据库
本书是我在开发阿⾥云 MongoDB 云数据库过程中积累的源码开发、线上运维管理、⽤户最佳实践经验,所有的⽂章都
曾发布到 阿⾥云栖社区 以及 Mongoing 中⽂社区,近期有很多读者反馈⽂章中有部分图⽚⽆法显示,影响对⽂章的理
解,我特地将所有的⽂章进⾏分类整理成电⼦书,⽅便⼤家学习,如果你觉得本书对你有帮助,可以友情打赏⿎励⼀
下,也欢迎向我反馈问题。
邮箱 & ⽀付宝账号:[email protected]

mongoDB复制原理

 

MongoDB 知识点汇总
1、复制集简介
       MongdoDB复制集由一组Mongod实例组成,包含一个Primary 节点和多个Secondary节点,
    Mongodb Driver(客户端)的所有数据都写入Primary,Secondary 从Primary 同步写入的数据,
    以保持复制集内的所有成员存储相同的数据。
2、Primary 选举
   复制集通过replSetlnitiate 命令(或mongo shell 的rs.initate) 进行初始化,初始化后各个
   成员间开始发送心跳信息,并发起Primary选举操作,获得大多数成员投票的支持的节点,
   会成为Primary,其余节点成为Secondary。
   初始化复制集 
   config = {_id:"my_repl_set",members :[{_id: 0,host:"rs1.example.net:27017"}, \
   {_id:1,host: "rs2.example.net:27017"},{_id: 2,host:"rs3.example.net:27017"}]}
   
    rs.initiate(config)

    批注: 大多数指的是总数的二分之一加一(N/2 + 1)
    
3、特殊的Secondary
   正常情况下,复制集的Secondary 会参与Primary 选举(自身也可以被选为Primary),
   并从Primary 同步 ,以保证数据的一支持。
   Secondary 可以提供读服务,增加Secondary 节点可以提供复制集的读服务能力,同时提升复制集的可用性。
   另外Mongodb支持对复制集的Secondary 节点进行灵活的配置,以适应多种场景的需求。
   
   1、Arbiter
   Arbiter 节点只参与投票,不能被选为Primary,也不从Primary同步数据。只是提到投票的作用。
   2、Priority 0
   Priority 0 节点的选举优先级为0,不会被选举为Primary。
   3、vote0
   Mongodb 3.0 里,复制集成员最多50个,参与Primary 选举投票的最多7个,
   其他成员(Vote0)的属性必须设置为0,即不参与投票。
   4、Hidden
   Hidden 节点不能为选择为主(Primary为0),并且对Driver(客户端)不可见。因为Hidden节点不会接受Driver的请求,可以使用Hidden
   节点做一些数据备份、离线计算的任务,不会影响复制集的服务。
   5、Delayed
   Delayed 节点必须是hidden节点,并且其数据落后于Primary一段时间(
   可配置,比如1个小时)。因Delayed 节点数据比Primary 落后一段时间,
   如果出现误操作可以通过Delaryed节点的数据来恢复到之前的时间点。
   
4、数据同步
   primary 与secondary 之间通过oplog 来同步数据,primary上写操作完成后,
   会向local.oplog.rs集合中写入一条oplog,Secondary 不断的从Primary
   获取新的oplog并应用。
   因oplog的数据会不断增加,local.oplog.rs被设置为一个capped集合,当
   容量达到配置的上线时,会将最旧的数据删除掉。另外考虑到oplog在secondary 上可能重复应用,
   oplog必须具有幂等性,集重复应用也会得到相同的结果。
   如下oplog的格式,包含ts、h、op、ns、o等字段。
   {
   "ts" : Timestamp(1446011584, 2),
   "h" : NumberLong("1687359108795812092"),
   "v" : 2,
   "op" : "i",
   "ns" : "test.nosql",
   "o" : { "_id" : ObjectId("563062c0b085733f34ab4129"), "name" : "mongodb", "score" : "100" }
   }
   上述oplog里各个字段的含义如下:
   ts: 操作时间,当前timestamp + 计数器,计数器每秒都被重置
   n: 操作的全局唯一标识符
   v:oplog的版本信息
   op:操作类型
      i: 插入操作
      u: 更新操作
      d: 删除操作
      c: 执行操作
      n: 空操作,特殊用途
   ns:操作针对的集合
   o: 操作内容,如果是更新操作
   o2:操作查询条件,仅update操作包含该字段
   
   同步原理:
   Secondary 初次同步数据时,会先进行init sync,从Primary(或其他数据更新的secondary)
   同步全量数据,然后不断通过tailable cursor 从Primary 的local.oplog.rs 集合里查询最新
   的oplog并应用到自身。
   
   init sync 过程包含如下步骤:
   1、T1时间,从primary 同步所有数据库的数据(local除外),通过listDatabase + listCollections + cloneCollection
   命令组合完成,假设T2 时间完成所有操作。
   2、从Primary应用[T1 - T2]时间段内的oplog,可能部分操作已经包含在步骤1,但由于oplog的幂等性,可重复应用。
   3、根据Primary各集合的index设置,在Secondary上为相应的集合创建index(每个
   集合_id的index已在步骤1 中完成)。
   
   批注: oplog集合的大小应该根据DB规模及应用写入需求合理配置,配置太大,会造成存储空间浪费;
   配置太小,可能造成secondary的init sync一直无法成功。比如在步骤1里由于
   DB数据太多、并且oplog配置太小,导致oplog不足以存储【T1-T2]时间内的所有oplog,这就是secondary无法从primary上同步完整的数据集。
   
   4、修改复制集的配置
   当需要修改复制集时,比如增加成员、删除成员、或者修改成员配置(比如priorty、vote、
   hidden、delary等属性),可以通过replsetReconfig命令(rs.config()) 对复制集进行重新配置。不如将复制集的第2个
   成员priority设置为2,可以执行下面命令:
   
   cfg = rs.config();
   cfg.members[1].prority =2;
   rs.reconfig(cfg)
   
   5、细说Primary选举
   Primary选举除了复制集初始化时发生、还有如下场景
   1、复制集被reconfig
   2、secondary 节点检查到Primary宕机时,会触发新Primary的选举
   3、当有Primary节点主动降级时(stepDown)时,也出触发
   新的Primary选举。
   
   Primary 的选举受节点间心跳、优先级、最新的oplog时间等多种因素影响。
   
   节点心跳
   复制集成员间默认每2秒发送一次心跳信息,如果10s未收到某个节点的心跳信息,
   则认为该节点以宕机;如果宕机的节点为primary,secondary(前提是可以被priamry)
   会发起新的primary选举。
   
   节点优先级
   * 每个节点都倾向于投票给优先级最高的节点
   * 优先级为0的及诶但不会主动发起primary选举
   * 当Primary发现有优先级更改seconday,并且该secongary的数据落后在10s内,
     则primary会主动降级,让优先级更改的secondary 成为primary的机会。
     
     optime
     拥有最新optime(最近一条oplog的时间)的节点才能被选为主。
     
     网络分区
     只有大多数投票节点间保持网络连通,才有机会被选Primary;如果10s未收到某个节点的心跳信息,primary与大多数的节点断开连接,primary会主动降级为secondary。当发生网络分区时,可能在短时间内出现多个primary,故Driver在写入时,最好设置【大多数成功的策略】,这样即使出现多个primary,也只有一个primary能成功写入。
     
     
5、复制集的读写设置
    1、Read Preference
    默认情况下,复制集的所有读请求都发送到primary,Driver可通过设置Read 
    Preference 来讲读请求路由到其他的节点。
    × primary: 默认规则,所有读请求发到primary
    × primaryPreferred:Primary 优先,如果primary 不可达,请求secondary
    × secondary: 所有的读请求都发送到secondary
    × secondaryPreferred:secondary优先,当所有secondary不可达时,请求primary
    × nearest: 读请求发送到最近的可达节点上(通过ping探测得出最近的节点)
    
    2、Write concern
    默认情况下,primary完成写操作即返回,Driver可通过设置Read【WriteConcern]的成功的规则。
    如下的write concern 规则设置写必须在大多数节点上成功,超时时间为5s。
    默认情况下,Primary完成写操作即返回,Driver可通过设置[Write
Concern(https://docs.mongodb.org/manual/core/write-concern/)来设置写成功的规则。
    
   db.products.insert(
      {item:"envelopes", qty: 100,type: "clasp"},
      {writeConcern:{w:majority,wtimeout:5000}}
      )
    上面的设置方式是针对单个请求的,也可以修改副本集默认的
    writeConcern,这样就不用每个请求单独设置:
    cfg=rs.config()
    cfg.settings = {}
    cfg.settings.getLastErrorDefaults = {w: "majority",wtimeout: 5000}
    rs.reconfig(cfg)
    
    
    异常处理(rollback)
    当Primary宕机时,如果有数据未同步到Secondary,当primary重新加入时,如果新的primary上已经发生了写操作,则旧primary需要回滚部分操作,以保证数据集与
    与新的primary一致。旧primary将回滚的数据写到单独的rollback目录下,
    数据库管理员可根据需要使用mongorestore进行恢复。
    
    
5、MongoDB同步原理解析
   mongodb 副本集数据同步主要包含2个步骤:
   1、inital sync ,可以理解为全量同步
   2、replication 追加同步oplog,可以理解为增量同步
   本文是对mongodb 高可用复制集原理的补充,会详细介绍mongodb数据同步实现的原理。
   
   1、initial sync
   secondary 节点出现如下状况时,需要先进行全量同步
   1、oplog 为空
   2、local.replset.minvalid集合里_initialSyncFlog字段设置为true
   3、内存标记initialSyncRequested 设置为true
   
   这个3个场景分表对应
   1、新节点加入,无任何oplog,此时需先进行initial sync
   2、initial sync 开始时,会主动将_initialSyncFlag 字段设置为true,正常结束后在设置为false;
   如果节点重启时,发现_initialSyncFlag为true,说明上次全量同步中途失败了,此时应该重新进行initial sync
   3、当用户发送resync命令时,initialSyncRequested 会设置为true,此时会重新开始一次initial sync
   
    
   initial sync流程
   1、全量同步开始,设置minvalid集合的_initialSyncFlag
   2、获取同步源上最新oplog时间戳为T1
   3、全量同步集合数据(耗时)
   4、获取同步源上最新oplog时间戳为T2
   5、从服务器重新应用[T1,T2]范围内的所有oplog
   6、获取同步源上最新的oplog时间戳T3
   7、从服务器重新应用[t2,t3]范围内的所有oplog
   8、建立集合所有索引(耗时)
   9、获取同步源上最新oplog时间戳为T4
   10、重放[t3,t4]范围内的所有oplog
   11、全量同步结束,清除minvalid集合的_initialSyncFlag
   
   Replication
   initial sync结束后,接下来Secondary 就会不断拉取主上新产生的oplog并重放,这个过程在Secondary同步
   慢问题分析也介绍过,这里从另一个角度分析下。
   
    图1
    
    
    注意事项:
    × initial sync 单线程复制数据,效率比较低,生产环境应该尽量避免initial sync出现,需要合理配置oplog,按照默认5%的可用磁盘空间来设置oploag在绝大部分场景下都能满足需求,特殊的case可根据实际情形进行设置oplog的大小。
    × 新加入节点时,可以通过物理复制的方式来避免initial sync,将Primary 上的dbpath拷贝到新的节点,直接启动,这样效率更高。
    × 当secondary 上需要的oplog 在同步源上的已经滚掉时,secondary的同步将无法正常进行,会进入recovering的状态,
      需向secondary 主动发送resync 命令重新同步。3.2 版本目前有个bug。可能导致resync 不能呢个正常工作,必须强制(kill -9
      )后重启节点,详情参考server-24773
      × 生产环境,最好通过db.printSlaveReplicationInfo() 来监控主备同步滞后的情况,当secondary 落后太多时,要及时调查清楚原因。
      × 当secondary 同步滞后是因为主上并发写入太高导致,(db.serverStatus().metrics.repl.buffer.sizeBytes持续接近db.serverStatus().metrics.repl.buffer.maxSizeBytes),
      可以通过调整Secondary上replWrite并发线程数来提升同步速度。
      
6、MongoDB 索引原理
    1、 为什么需要索引?
    当你抱怨MongoDB 集合查询效率低的时候,可能你就需要考虑使用索引了,为了方便后续介绍,先科普一下mongodb里的
    索引机制(同样适用于其他的数据库比如mysql)。
    mongo-9552:PRIMARY> db.person.find()
{ "_id" : ObjectId("571b5da31b0d530a03b3ce82"), "name" : "jack", "age" : 19 }
{ "_id" : ObjectId("571b5dae1b0d530a03b3ce83"), "name" : "rose", "age" : 20 }
{ "_id" : ObjectId("571b5db81b0d530a03b3ce84"), "name" : "jack", "age" : 18 }
{ "_id" : ObjectId("571b5dc21b0d530a03b3ce85"), "name" : "tony", "age" : 21 }
{ "_id" : ObjectId("571b5dc21b0d530a03b3ce86"), "name" : "adam", "age" : 18 }
    
    当你往某个集合插入多个文档后,每个文档在经过底层的存储引擎持久化后,会有一个位置信息,通过这个位置信息,
    就能从存储引擎里读出该文档。比如mmapv1引擎里,位置信息是[文件id +文件内offset],在writedtiger存储引擎(一个kv存储引擎)
    里,位置信息是wiredtiger 在存储文档时生成的一个key,通过这个key能访问到对应的文档;
    为方便介绍统一用pos来代表位置信息。
    
    ⽐如上⾯的例⼦⾥, person 集合⾥包含插⼊了4个⽂档,假设其存储后位置信息如下(为⽅便描述,⽂档省去_id字段)
    位置信息⽂档
    pos1 {"name" : "jack", "age" : 19 }
    pos2 {"name" : "rose", "age" : 20 }
    pos3 {"name" : "jack", "age" : 18 }
    pos4 {"name" : "tony", "age" : 21}
    pos5 {"name" : "adam", "age" : 18}
    假设现在有个查询 db.person.find( {age: 18} ) , 查询所有年龄为18岁的⼈,这时需要遍历所有的⽂档(『全表扫
    描』),根据位置信息读出⽂档,对⽐age字段是否为18。当然如果只有4个⽂档,全表扫描的开销并不⼤,但如果集
    合⽂档数量到百万、甚⾄千万上亿的时候,对集合进⾏全表扫描开销是⾮常⼤的,⼀个查询耗费数⼗秒甚⾄⼏分钟都有
    
    如果想加上db.person.find({age:18}),就需要考虑对person表的age字段建立索引。
    db.person.createIndex({age:1}) // 按age字段创建升序索引
    批注: 1: 代表升序,-1: 代表降序
    
    建立索引后,mongodb 会额外存储一份按age字段升序排序的索引数据,索引结构类似如下,索引通常采用类似btree的
    结构持久化存储,以保证从索引里快速找出某个age值对应的位置信息,然后根据位置信息就能读取出对应的文档;
    
    简单的说,索引就是将文档安装某个(或某些)字段顺序组织起来,以便能根据该字段高效的查询。
    有了索引,至少能优化如下场景的效率:
    × 查询,比如查询年龄为18的所有人
    × 更新/删除, 将年为18的所有人的信息更新或删除,因为更新或删除,需要根据
    条件先查出所有符合条件的后进行更新/删除操作。查询时间短了,更新/删除,的时间也就缩短了。
    × 排序,将所有人的信息按照年龄排序,如果没有索引,需要全表扫描文档,然后在对扫描的结果进行排序。
      
    众所周知,mongodb默认会为插入的文档生成_id 字段(如果应用本身没有指定该字段),_id是稳定唯一标识,为了保证根据文档id快速查找文档,mongodb默认会为集合创建_id字段的索引。
    
    primary> db.person.getIndexes() //查询集合的索引信息
    {
    "ns" : "test.person", // 集合名
    "v" : 1, // 索引版本
    "key" : { // 索引的字段及排序⽅向
    "_id" : 1 // 根据_id字段升序索引
    },
    "name" : "_id_" // 索引的名称
    }
    ]
    
    MongoDB 索引类型
    MongoDB 支持多种类型的索引,包括单字段索引、符合索引、多key索引、文本索引等,
    每种类型的索引有不同的使用场合。
    
    单字段索引
    > db.person.createindex({age: 1})
    
    上述语句针对age创建单字段索引,能加速对age查询请求。{age:1} 代表升序,-1:
    也可以通过{age: -1}创建降序索引。升序降序对于单字段索引效果是一样的。
    
    复合索引
    复合索引是单字段索引的升级版本,它针对多个字段联合创建索引,先按照第一个字段排序,第一个字段相同的文档按第二个字段排序,以此类推,如下oplog的格式,包含ts、h、op、ns、o等字段。
    如下针对age、name这个字段创建一个复合索引。
    >db.person.createIndex({age:1,name: 1})
    上述索引对应的数据组织类似下表,与{age: 1}索引不同的时,当age字段相同时,在根据name字段进⾏排序,所以
     pos5对应的⽂档排在pos3之前。
     age,name 位置信息
     18,adam pos5
     18,jack pos3
     19,jack pos1
     20,rose pos2
     21,tony pos4
     复合索引能满足的查询场景比单字段索引更丰富,不光能满足多个字段字段组合起来的查询,
     比如db.person.find({age:18,name: 'jack'}),也能满足所有能匹配复合索引前缀的查询。所以类似db.preson.find({age:18})的查询也能通过该索引来加速;但db.person.find({name: javck}) 则无法使用该复合索引。
     如果经常需要根据name字段以及name和age的字段组合来查询,则应该创建如下的复合索引。
     >db.person.createIndex({name: 1,age: 1})
     
     除了查询的需求能够影响索引的顺序,字段的值分布也是一个重要的考量因素,即使person集合所有的查询都是【name和age】组合
     字段的顺序也是有影响的。
     age字段的取值很有限,即拥有相同age字段的文档会有很多;而name字段的取值则很丰富,拥有相同name字段的文档很少;
     显然先按照name字段查询,再在相同name的文档里查找age字段更为高效。
     
     多key索引
     当索引的字段为数组时,创建出的索引称为多key索引,多key索引会为数组的每个元素建立一条索引,比如person表
     加入一个habbit字段(数组)用于描述兴趣爱好,需要查询有相同兴趣爱好的人就可以利用habbit字段的多key索引。
     {"name" :"jack","age":19,habbit:["fooball","running"]}
     >db.person.createIndex({habbit:1}) //自动创建多key索引
     >db.person.find({habbit:"football"})
     
     其他类型索引
     
     哈希索引
     是指按照某个字段的hash值来建立索引,目前主要用于mongodb sharded cluster 的hash分片,
     hash 索引只能满足字段完全匹配的查询,不能满足范围查询等。
     
     地理位置索引
     能很好的解决O2O 的应用场景,比如[查找附近的美食]、[查找某个区域内的车站]等。
     
     文本索引
     文本索引能解决文本查找的需求,比如有一个博客文章集合,需要根据博客的内容来
     快速查找,则可以对博客内容建立文本索引。
     
     索引额外属性
     MongoDB 除了支持多种不同类型的索引,还能对索引定制一些特殊的属性。
     × 唯一索引(unique index): 保证索引对应的字段不会出现相同的值,比如 _id索引
     × TTL索引: 可以针对某个时间字段,指定文档的过期时间(经过指定时间后过期或在某个时间点过期)
     × 部分索引:只针对符合某个特定条件的文档建立索引,3.2 版本才支持该特性。
     × 稀疏索引:只针对存在索引字段的文档建立索引,可以看作是部分索引的一种特殊情况。
     
7、索引优化
    db profiling 
    MongoDB支持对DB的请求进行profiling,目前支持3种级别的profiling。
    × 0: 不开启profiling
    × 1: 将处理时间超过某个阈值(默认100ms)的请求都记录到DB下的system.profile集合(类似于mysql、redis的 slow log)
    × 2: 将所有的请求都记录到DB下的system.profile集合(生产环境慎用)
    
    通常,生产环境建议使用1级别的profiling,并根据自身配置合理的阈值,用于监测慢请求的情况,并及时的做索引优化。
    
    如果能在集合创建的时候就能【根据业务查询需求决定该创建哪些索引】,当然是最佳的选择;但是由于业务需求多变,要根据实际情况不断的进行优化。索引 并不是越多越好,集合的索引太多,会影响写入、更新的性能,
    每次写入都需要更新所有索引的数据;索引你system.profile里的慢请求可能是索引建立的不够导致,也可能是索引过多导致的。
    
    
    查询计划
    
    索引已经建立了,但查询还是很慢怎么破?这时就的深入的分析一下索引的使用情况了,可以通过查看详细的查询计划来决定如何优化。
    通过执行计划可以看出如下问题:
    1、根据某个/某些字段查询,但没有建立索引
    2、根据某个/某些字段查询,但建立了多个索引,执行查询时没有使用预期的索引。
    
    建立索引前,db.person.find({age: 18}) 必须执行collscan,即全表扫描。
    mongo-9552:PRIMARY> db.person.find({age: 18}).explain()
    {
    "queryPlanner" : {
    "plannerVersion" : 1,
    "namespace" : "test.person",
    "indexFilterSet" : false,
    "parsedQuery" : {
    "age" : {
    "$eq" : 18
    }
    },
    "winningPlan" : {
    "stage" : "COLLSCAN",
    "filter" : {
    "age" : {
    "$eq" : 18
    }
    },
    "direction" : "forward"
    },
    "rejectedPlans" : [ ]
    },
    "serverInfo" : {
    "host" : "localhost",
    "port" : 9552,
    "version" : "3.2.3",
    "gitVersion" : "b326ba837cf6f49d65c2f85e1b70f6f31ece7937"
    },
    "ok" : 1
    }
        
    建立索引后,通过查询计划可以看出,先进行IXSCAN(从索引中查找),然后FETCH,读取出满足条件的文档。
     mongo-9552:PRIMARY> db.person.find({age: 18}).explain()
    {
    "queryPlanner" : {
    "plannerVersion" : 1,
    "namespace" : "test.person",
    "indexFilterSet" : false,
    "parsedQuery" : {
    "age" : {
    "$eq" : 18
    }
    },
    "winningPlan" : {
    "stage" : "FETCH",
    "inputStage" : {
    "stage" : "IXSCAN",
    "keyPattern" : {
    "age" : 1
    },
    "indexName" : "age_1",
    "isMultiKey" : false,
    "isUnique" : false,
    "isSparse" : false,
    "isPartial" : false,
    "indexVersion" : 1,
    "direction" : "forward",
    "indexBounds" : {
    "age" : [
    "[18.0, 18.0]"
    ]
    }
    }
    },
    "rejectedPlans" : [ ]
    },
    "serverInfo" : {
    "host" : "localhost",
    "port" : 9552,
    "version" : "3.2.3",
    "gitVersion" : "b326ba837cf6f49d65c2f85e1b70f6f31ece7937"
    },
    "ok" : 1
    }    
            
    参考资料
    MongoDB索引介绍
    createIndex命令
    MongoDB Sharded Cluster
    唯⼀索引 (unique index)
    TTL索引
    部分索引 (partial index)
    稀疏索引(sparse index)
    database profiling        
        
        

你可能感兴趣的:(MongoDB)