MongoDB主从复制架构原理和缺陷

master-slave架构中master节点负责数据的读写,slave没有写入权限只负责读取数据。
image.png
在主从结构中,主节点的操作记录成为oplog(operation log)。oplog存储在系统数据库local的oplog.$main集合中,这个集合的每个文档都代表主节点上执行的一个操作。从服务器会定期从主服务器中获取oplog记录,然后在本机上执行!对于存储oplog的集合,MongoDB采用的是固定集合,也就是说随着操作过多,新的操作会覆盖旧的操作!
主从结构没有自动故障转移功能,需要指定master和slave端,不推荐在生产中使用。
mongodb4.0后不再支持主从复制!
[main] Master/slave replication is no longer supported

复制集replica sets

什么是复制集

image.png
复制集是由一组拥有相同数据集的mongod实例做组成的集群。
复制集是一个集群,它是2台及2台以上的服务器组成,以及复制集成员包括Primary主节点,secondary从节点和投票节点。
复制集提供了数据的冗余备份,并在多个服务器上存储数据副本,提高了数据的可用性,保证数据的安全性。

为什么要使用复制集

  1. 高可用
    防止设备(服务器、网络)故障。
    提供自动failover 功能。
    技术来保证高可用
  2. 灾难恢复
    当发生故障时,可以从其他节点恢复,用于备份。
  3. 功能隔离
    我们可以在备节点上执行读操作,减少主节点的压力,比如:用于分析、报表,数据挖掘,系统任务等。

    复制集集群架构原理

    一个复制集中Primary节点上能够完成读写操作,Secondary节点仅能用于读操作。Primary节点需要记录所有改变数据库状态的操作,这些记录保存在 oplog 中,这个文件存储在 local 数据库,各个Secondary节点通过此 oplog 来复制数据并应用于本地,保持本地的数据与主节点的一致。oplog 具有幂等性,即无论执行几次其结果一致,这个比 mysql 的二进制日志更好用。
    oplog的组成结构

    {
      "ts" : Timestamp(1446011584, 2),
      "h" : NumberLong("1687359108795812092"),
      "v" : 2,
      "op" : "i",
      "ns" : "test.nosql",
      "o" : { "_id" : ObjectId("563062c0b085733f34ab4129"), "name" : "mongodb",
      "score" : "10"}
    }

    ts:操作时间,当前timestamp + 计数器,计数器每秒都被重置
    h:操作的全局唯一标识
    v:oplog版本信息
    op:操作类型
    i:插入操作
    u:更新操作
    d:删除操作
    c:执行命令(如createDatabase,dropDatabase)
    n:空操作,特殊用途
    ns:操作针对的集合
    o:操作内容
    o2:更新查询条件,仅update操作包含该字段

复制集数据同步分为初始化同步和keep复制同步。初始化同步指全量从主节点同步数据,如果Primary节点数据量比较大同步时间会比较长。而keep复制指初始化同步过后,节点之间的实时同步一般是增量同步。
初始化同步有以下两种情况会触发:
(1) Secondary第一次加入。
(2) Secondary落后的数据量超过了oplog的大小,这样也会被全量复制。

MongoDB的Primary节点选举基于心跳触发。一个复制集N个节点中的任意两个节点维持心跳,每个节点维护其他N-1个节点的状态。
image.png
心跳检测:
整个集群需要保持一定的通信才能知道哪些节点活着哪些节点挂掉。mongodb节点会向副本集中的其他节点每2秒就会发送一次pings包,如果其他节点在10秒钟之内没有返回就标示为不能访问。每个节点内部都会维护一个状态映射表,表明当前每个节点是什么角色、日志时间戳等关键信息。如果主节点发现自己无法与大部分节点通讯则把自己降级为secondary只读节点。
主节点选举触发的时机:

  • 第一次初始化一个复制集
  • Secondary节点权重比Primary节点高时,发起替换选举
  • Secondary节点发现集群中没有Primary时,发起选举
  • Primary节点不能访问到大部分(Majority)成员时主动降级

当触发选举时,Secondary节点尝试将自身选举为Primary。主节点选举是一个二阶段过程+多数派协议。

第一阶段:
检测自身是否有被选举的资格,如果符合资格会向其它节点发起本节点是否有选举资格的FreshnessCheck,进行同僚仲裁
第二阶段:
发起者向集群中存活节点发送Elect(选举)请求,仲裁者收到请求的节点会执行一系列合法性检查,如果检查通过,则仲裁者(一个复制集中最多50个节点 其中只有7个具有投票权)给发起者投一票。
pv0通过30秒选举锁防止一次选举中两次投票。
pv1使用了terms(一个单调递增的选举计数器)来防止在一次选举中投两次票的情况。
多数派协议:
发起者如果获得超过半数的投票,则选举通过,自身成为Primary节点。获得低于半数选票的原因,除了常见的网络问题外,相同优先级的节点同时通过第一阶段的同僚仲裁并进入第二阶段也是一个原因。因此,当选票不足时,会sleep[0,1]秒内的随机时间,之后再次尝试选举。

复制集搭建

image.png

  1. 主节点配置 mongo_37017.conf

    # 主节点配置
    dbpath=/data/mongo/data/server1
    bind_ip=0.0.0.0
    port=37017
    fork=true
    logpath=/data/mongo/logs/server1.log
    replSet=mgCluster
  2. 从节点1配置 mongo_37018.conf

    dbpath=/data/mongo/data/server2
    bind_ip=0.0.0.0
    port=37018
    fork=true
    logpath=/data/mongo/logs/server2.log
    replSet=mgCluster
  3. 从节点2配置 mongo_37019.conf

    dbpath=/data/mongo/data/server3
    bind_ip=0.0.0.0
    port=37019
    fork=true
    logpath=/data/mongo/logs/server3.log
    replSet=mgCluster
  4. 初始化节点配置
    启动三个节点,然后进入任意一个节点,运行如下命令:

    var cfg ={"_id":"mgCluster",
             "protocolVersion" : 1,
             "members":[
                 {"_id":1,"host":"192.168.211.133:37017","priority":10},
                 {"_id":2,"host":"192.168.211.133:37018"}
             ]
          }
    rs.initiate(cfg)
    rs.status()
  5. 节点的动态增删
    增加节点

    rs.add("192.168.211.133:37019")

    删除slave节点

    rs.remove("192.168.211.133:37019")
  6. 复制集操作演示
    进入主节点 ----- 插入数据 ------ 进入从节点验证
    注意:默认节点下从节点不能读取数据。调用 rs.slaveOk() 解决
    为了保证高可用,在集群当中如果主节点挂掉后,会自动在从节点中选举一个,重新做为主节点。

    /*查看节点状态*/
    rs.status()

    节点说明:
    PRIMARY 节点: 可以查询和新增数据
    SECONDARY 节点:只能查询,不能新增,基于priority 权重可以被选为主节点
    ARBITER 节点: 不能查询数据和新增数据,不能变成主节点

    复制集成员的配置参数

参数字段类型说明取值说明
_id整数_id:0复制集中的标示
host字符串host:"主机:端口"节点主机名
arbiterOnly布尔值arbiterOnly:true是否为仲裁(裁判)节点
priority(权重)整数priority=0或1默认1,是否有资格变成主节点,取值范围0-1000,0永远不会变成主节点
hidden布尔值hidden=true或false,0或1隐藏,权重必须为0,才可以设置
votes整数votes= 0或1投票,是否为投票节点,0 不投票,1投票
slaveDelay整数slaveDelay=3600从库的延迟多少秒
buildIndexes布尔值buildIndexes=true或false,0或1主库的索引,从库也创建,_id索引无效

举例:

var cfg ={"_id":"mgCluster",
          "protocolVersion" : 1,
          "members":[
                {"_id":1,"host":"192.168.211.133:37017","priority":10},
                {"_id":2,"host":"192.168.211.133:37018","priority":0},
                {"_id":3,"host":"192.168.211.133:37019","priority":5},
                {"_id":4,"host":"192.168.211.133:37020","arbiterOnly":true}
          ]
         };
/*重新装载配置,并重新生成集群节点。*/
rs.reconfig(cfg)
/*重新查看集群状态*/
rs.status()

有仲裁节点复制集搭建

和上面的配置步骤相同,只是增加了一个特殊的仲裁节点
注入节点执行

rs.addArb("IP:端口")
rs.addArb("192.168.211.133:37020")

分片集群 Shard Cluster

什么是分片

分片(sharding)是MongoDB用来将大型集合水平分割到不同服务器(或者复制集)上所采用的方法。不需要功能强大的大型计算机就可以存储更多的数据,处理更大的负载。

为什么要分片

  1. 存储容量需求超出单机磁盘容量。
  2. 活跃的数据集超出单机内存容量,导致很多请求都要从磁盘读取数据,影响性能。
  3. IOPS超出单个MongoDB节点的服务能力,随着数据的增长,单机实例的瓶颈会越来越明显。
  4. 副本集具有节点数量限制。

垂直扩展:增加更多的CPU和存储资源来扩展容量。
水平扩展:将数据集分布在多个服务器上。水平扩展即分片。

分片的工作原理

image.png
分片集群由以下3个服务组成:
Shards Server: 每个shard由一个或多个mongod进程组成,用于存储数据。
Router Server: 数据库集群的请求入口,所有请求都通过Router(mongos)进行协调,不需要在应用程序添加一个路由选择器,Router(mongos)就是一个请求分发中心它负责把应用程序的请求转发到对应的Shard服务器上。
Config Server: 配置服务器。存储所有数据库元信息(路由、分片)的配置。

片键(shard key)
为了在数据集合中分配文档,MongoDB使用分片主键分割集合。

区块(chunk)
在一个shard server内部,MongoDB还是会把数据分为chunks,每个chunk代表这个shard server内部一部分数据。MongoDB分割分片数据到区块,每一个区块包含基于分片主键的左闭右开的区间范围。

分片策略

  • 范围分片(Range based sharding)
    image.png
    范围分片是基于分片主键的值切分数据,每一个区块将会分配到一个范围。
    范围分片适合满足在一定范围内的查找,例如查找X的值在[20,30)之间的数据,mongo 路由根据Config server中存储的元数据,可以直接定位到指定的shard的Chunk中。
    缺点: 如果shard key有明显递增(或者递减)趋势,则新插入的文档多会分布到同一个chunk,无法扩展写的能力。
  • hash分片(Hash based sharding)
    image.png
    Hash分片是计算一个分片主键的hash值,每一个区块将分配一个范围的hash值。
    Hash分片与范围分片互补,能将文档随机的分散到各个chunk,充分的扩展写能力,弥补了范围分片的不足,缺点是不能高效的服务范围查询,所有的范围查询要分发到后端所有的Shard才能找出满足条件的文档。
  • 组合片键 A + B(散列思想 不能是直接hash)
    数据库中没有比较合适的片键供选择,或者是打算使用的片键基数太小(即变化少如星期只有7天可变化),可以选另一个字段使用组合片键,甚至可以添加冗余字段来组合。一般是粗粒度+细粒度进行组合。

合理的选择shard key
无非从两个方面考虑,数据的查询和写入,最好的效果就是数据查询时能命中更少的分片,数据写入时能够随机的写入每个分片,关键在于如何权衡性能和负载。

分片集群的搭建过程

image.png

  1. 配置并启动 config 节点集群
    节点1 config-17017.conf

    # 数据库文件位置
    dbpath=config/config1
    #日志文件位置
    logpath=config/logs/config1.log
    # 以追加方式写入日志
    logappend=true
    # 是否以守护进程方式运行
    fork = true
    bind_ip=0.0.0.0
    port = 17017
    # 表示是一个配置服务器
    configsvr=true
    #配置服务器副本集名称
    replSet=configsvr

    节点2 config-17018.conf

    # 数据库文件位置
    dbpath=config/config2
    #日志文件位置
    logpath=config/logs/config.log
    # 以追加方式写入日志
    logappend=true
    # 是否以守护进程方式运行
    fork = true
    bind_ip=0.0.0.0
    port = 17018
    # 表示是一个配置服务器
    configsvr=true
    #配置服务器副本集名称
    replSet=configsvr

    节点3 config-17019.conf

    # 数据库文件位置
    dbpath=config/config3
    #日志文件位置
    logpath=config/logs/config3.log
    # 以追加方式写入日志
    logappend=true
    # 是否以守护进程方式运行
    fork = true
    bind_ip=0.0.0.0
    port = 17019
    # 表示是一个配置服务器
    configsvr=true
    #配置服务器副本集名称
    replSet=configsvr

    启动配置节点

    ./bin/mongod -f config/config-17017.conf
    ./bin/mongod -f config/config-17018.conf
    ./bin/mongod -f config/config-17019.conf

    进入任意节点的mongo shell并添加配置节点集群,注意use admin

    ./bin/mongo --port 17017
    use admin
    
    var cfg ={"_id":"configsvr",
           "members":[
               {"_id":1,"host":"192.168.211.133:17017"},
               {"_id":2,"host":"192.168.211.133:17018"},
               {"_id":3,"host":"192.168.211.133:17019"}
           ]
          };
    
    rs.initiate(cfg)
  2. 配置shard集群
    shard1集群搭建37017到37019

    dbpath=shard/shard1/shard1-37017
    bind_ip=0.0.0.0
    port=37017
    fork=true
    logpath=shard/shard1/shard1-37017.log
    replSet=shard1
    shardsvr=true
    
    dbpath=shard/shard1/shard1-37018
    bind_ip=0.0.0.0
    port=37018
    fork=true
    logpath=shard/shard1/logs/shard1-37018.log
    replSet=shard1
    shardsvr=true
    
    dbpath=shard/shard1/shard1-37019
    bind_ip=0.0.0.0
    port=37019
    fork=true
    logpath=shard/shard1/logs/shard1-37019.log
    replSet=shard1
    shardsvr=true

    启动每个 mongod 然后进入其中一个进行集群配置

    var cfg ={"_id":"shard1",
           "protocolVersion" : 1,
           "members":[
               {"_id":1,"host":"192.168.211.133:37017"},
               {"_id":2,"host":"192.168.211.133:37018"},
               {"_id":3,"host":"192.168.211.133:37019"}
           ]
          };
    
    rs.initiate(cfg)
    
    rs.status()

    shard2集群搭建47017到47019

    dbpath=shard/shard2/shard2-47017
    bind_ip=0.0.0.0
    port=47017
    fork=true
    logpath=shard/shard2/logs/shard2-47017.log
    replSet=shard2
    shardsvr=true
    
    dbpath=shard/shard2/shard2-47018
    bind_ip=0.0.0.0
    port=47018
    fork=true
    logpath=shard/shard2/logs/shard2-47018.log
    replSet=shard2
    shardsvr=true
    
    dbpath=shard/shard2/shard2-47019
    bind_ip=0.0.0.0
    port=47019
    fork=true
    logpath=shard/shard2/logs/shard2-47019.log
    replSet=shard2
    shardsvr=true

    启动每个 mongod 然后进入其中一个进行集群配置

    var cfg ={"_id":"shard2",
           "protocolVersion" : 1,
           "members":[
               {"_id":1,"host":"192.168.211.133:47017"},
               {"_id":2,"host":"192.168.211.133:47018"},
               {"_id":3,"host":"192.168.211.133:47019"}
           ]
          };
    
    rs.initiate(cfg)
    
    rs.status()
  3. 配置和启动路由节点
    route-27017.conf

    port=27017
    bind_ip=0.0.0.0
    fork=true
    logpath=route/logs/route.log
    configdb=configsvr/192.168.211.133:17017,192.168.211.133:17018,192.168.211.133:17019

    启动路由节点使用 mongos (注意不是mongod)

    ./bin/mongos -f route/route-27017.conf
  4. mongos(路由)中添加分片节点
    进入路由mongos

    ./bin/mongo --port 27017
    sh.status()
    
    sh.addShard("shard1/192.168.211.133:37017,192.168.211.133:37018,192.168.211.133:37019");
    
    sh.addShard("shard2/192.168.211.133:47017,192.168.211.133:47018,192.168.211.133:47019");
    
    sh.status()
  5. 开启数据库和集合分片(指定片键)
    继续使用mongos完成分片开启和分片大小设置

    /*为数据库开启分片功能*/
    sh.enableSharding("mg")
    /*为指定集合开启分片功能*/
    sh.shardCollection("mg.mg_datas",{"片键字段名,如name":索引说明})
  6. 向集合中插入数据测试
    通过路由循环向集合中添加数

    use mg;
    
    for(var i=1;i<= 1000;i++){
      db.mg_datas.insert({"name":"test"+i, salary:(Math.random()*20000).toFixed(2)});
    }
  7. 验证分片效果
    分别进入 shard1 和 shard2 中的数据库进行验证

标签: 集群, MongoDB

评论已关闭