# mongodb 集群高可用


<!--more-->


## 1. mongo主从架构

master-slave架构中master节点负责数据的读写，slave没有写入权限只负责读取数据。![img_13](https://img.zhaojq.top/20260727092026792.png "img_13")

> 在主从结构中，主节点的操作记录成为oplog（operation log）。
>
> **oplog**存储在系统数据库local的oplog.$main集合中，这个集合的每个文档都代表主节点上执行的一个操作。从服务器会定期从主服务器中获取oplog记录，然后在本机上执行！对于存储oplog的集合，MongoDB采用的是**固定集合**，也就是说随着操作过多，新的操作会覆盖旧的操作！

主从结构没有自动故障转移功能，需要指定master和slave端，不推荐在生产中使用。

mongodb4.0后不再支持主从复制！

## 2. 复制集replica sets

### 2.1 概念

![img_14](https://img.zhaojq.top/20260727092026984.png "img_14")

复制集是由一组拥有相同数据集的mongod实例做组成的集群。

复制集是一个集群，它是2台及2台以上的服务器组成，以及复制集成员包括**Primary主节点**,**secondary从节点** 和 **投票节点**。

复制集提供了数据的冗余备份，并在多个服务器上存储数据副本，提高了数据的可用性,保证数据的安全性。

### 2.2 为什么使用复制集

- 高可用

  防止设备（服务器、网络）故障。 

  提供自动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版本信息
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个节点的状态。

![img_15](https://img.zhaojq.top/20260727092027220.png "img_15")

> 心跳检测：
>
> 整个集群需要保持一定的通信才能知道哪些节点活着哪些节点挂掉。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]秒内的随机时间，之后再次尝试选举。

### 2.4 复制集搭建

![img_16](https://img.zhaojq.top/20260727092027429.png "img_16")

- 主节点配置

  ```properties
  # 主节点配置 
  dbpath=/data/mongo/
  bind_ip=0.0.0.0 
  port=37017 
  fork=true 
  logpath=/data/mongo/server1.log 
  #复制集名称, 自定义
  replSet=myCluster
  ```

- 从节点1配置

  ```properties
  dbpath=/data/mongo/
  bind_ip=0.0.0.0 
  port=37018
  fork=true 
  logpath=/data/mongo/server2.log 
  replSet=myCluster
  ```

- 从节点2配置

  ```properties
  dbpath=/data/mongo/
  bind_ip=0.0.0.0 
  port=37019
  fork=true 
  logpath=/data/mongo/server3.log 
  replSet=myCluster
  ```

- 初始化节点配置

  如果是不同服务器,记得关闭防火墙`systemctl stop firewalld.service`或开放端口

  启动三个节点, 然后进入任意一个节点,运行如下命令: 注意ip,port为自定义的

  ```json
  var cfg ={
      "_id":"myCluster",
      "protocolVersion":1,
      "members":[
          {
              "_id":1,
              "host":"10.211.55.11:37017",
              "priority":10
          },
          {
              "_id":2,
              "host":"10.211.55.12:37018"
          }
      ]
  }
  
  rs.initiate(cfg)
  rs.status()
  
  priority: 该参数为优先级, 刚initiate时会显示SECONDARY, 过一会儿就会显示PRIMARY
  ```

- 节点的动态增删

  ```
  增加节点 
  rs.add("10.211.55.13:37019") 
  删除slave 节点 
  rs.remove("10.211.55.13:37019")
  ```

- 数据演示

  进入主节点 ----- 插入数据 ------ 进入从节点验证

  注意:默认节点下从节点不能读取数据。调用 `rs.slaveOk() `解决

  为了保证高可用，在集群当中如果主节点挂掉后，会自动在从节点中选举一个重新做为主节点。

  节点说明:

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

- 高可用演示

  将primary节点停掉, 会发现sencodary会自动变成primary

### 2.5. 复制集成员参数配置

| 参数         | 类型   | 取值                          | 说明                                                         |
| ------------ | ------ | ----------------------------- | ------------------------------------------------------------ |
| _id          | 整数   | _id:0                         | 复制集标示                                                   |
| host         | 字符串 | host:"ip:port"                | 节点主机名                                                   |
| arbiterOnly  | 布尔   | arbiterOnly:true              | 是否为仲裁节点                                               |
| priority     | 整数   | priority=0\|1                 | 权重,默认1,是否有资格变为主节点,取值0-100,0代表永远不会变为主节点 |
| hidden       | 布尔   | hidden=true\|false,0\|1       | 隐藏,权重必须为0,才可以设置                                  |
| votes        | 整数   | votes=0\|1                    | 投票,是否为投票节点,0不投票, 1投票                           |
| slaveDelay   | 整数   | slaveDelay=3600               | 从库的延迟秒                                                 |
| buildIndexes | 布尔   | buildIndexes=true\|false,0\|1 | 主库的索引,从库也创建,_id索引无效                            |

```json
#例:
var cfg ={"_id":"lagouCluster", "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()
```

### 2.6 有仲裁节点复制集搭建

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

注入节点 执行 rs.addArb("IP:端口")



## 3. 分片集群 shard cluster

### 3.1 分片概念

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

### 3.2 为什么要分片

- 存储容量需求超出单机磁盘容量。 

- 活跃的数据集超出单机内存容量，导致很多请求都要从磁盘读取数据，影响性能。 

- IOPS超出单个MongoDB节点的服务能力，随着数据的增长，单机实例的瓶颈会越来越明显。 

- 副本集具有节点数量限制。 

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

### 3.3 分片的工作原理

![img_17](https://img.zhaojq.top/20260727092027613.png "img_17")

分片集群由以下3个服务组成： 

- Shards Server: 每个shard由一个或多个mongod进程组成，用于存储数据。 

- Router Server: 数据库集群的请求入口，所有请求都通过Router(mongos)进行协调，不需要在应用程 序添加一个路由选择器，Router(mongos)就是一个请求分发中心它负责把应用程序的请求转发到对应的 Shard服务器上。 

- Config Server: 配置服务器。存储所有数据库元信息（路由、分片）的配置。

**分片参数:** 

- 片键 shark key

  为了在数据集合中分配文档，MongoDB使用分片主键分割集合。

- 区块 chunk

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

- 分片策略

  - 范围分片

    ![img_18](https://img.zhaojq.top/20260727092027837.png "img_18")

    范围分片是基于分片主键的值切分数据，每一个区块将会分配到一个范围。
    范围分片适合满足在一定范围内的查找，例如查找X的值在[20,30)之间的数据，mongo路由根据Conﬁgserver中存储的元数据，可以直接定位到指定的shard的Chunk中。

    缺点:如果shardkey有明显递增（或者递减）趋势，则新插入的文档多会分布到同一个chunk，无法扩展写的能力。

  - hash分片（Hash based sharding）

    ![img_19](https://img.zhaojq.top/20260727092028013.png "img_19")

    Hash分片是计算一个分片主键的hash值，每一个区块将分配一个范围的hash值。

    Hash分片与范围分片互补，能将文档随机的分散到各个chunk，充分的扩展写能力，弥补了范围分片的不足，缺点是不能高效的服务范围查询，所有的范围查询要分发到后端所有的Shard才能找出满足条件的文档。

  - 组合片键 A+B(散列思想 不是直接hash)

    数据库中没有比较合适的片键供选择，或者是打算使用的片键基数太小（即变化少如星期只有7天可变化），可以选另一个字段使用组合片键，甚至可以添加冗余字段来组合。一般是粗粒度+细粒度进行组合。

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

### 3.4 分片集群的搭建过程 

![img_20](https://img.zhaojq.top/20260727092028219.png "img_20")

我们就拿一台虚拟机服务器,通过不同端口来模拟集群(如果电脑空间大,可以配置多台虚拟机,模拟真实集群环境), **ip: 10.211.55.11**

> Tips: 以下配置文件中的路径, 我们都配置到Mongo解压包目录下,使用相对路径, 便于区别不同分片集

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

   mongo目录下创建config目录,并添加以下3个节点配置文件和配置里需要的目录![img_21](https://img.zhaojq.top/20260727092028428.png "img_21")![img_22](https://img.zhaojq.top/20260727092028621.png "img_22")

   - 节点1: confifig-17017.conf

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

   - 节点2: confifig-17018.conf

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

   - 节点3: confifig-17019.conf

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

   - 启动配置节点

     ```shell
     ./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":"10.211.55.11:17017"}, 
     {"_id":2,"host":"10.211.55.11:17018"}, 
     {"_id":3,"host":"10.211.55.11:17019"}
     ] };
     
     rs.initiate(cfg)
     ```

2. 配置shard集群并启动

   **shard1**集群搭建37017到37019:![img_23](https://img.zhaojq.top/20260727092028807.png "img_23")![img_24](https://img.zhaojq.top/20260727092028970.png "img_24")

   - 三个节点配置如下

     ```properties
     dbpath=shard1/server1
     bind_ip=0.0.0.0 
     port=37017 
     fork=true 
     logpath=shard1/shard1-37017.log
     replSet=shard1 
     shardsvr=true
     ---------------------
     dbpath=shard1/server2
     bind_ip=0.0.0.0 
     port=37018 
     fork=true 
     logpath=shard1/shard1-37018.log
     replSet=shard1 
     shardsvr=true
     ---------------------
     dbpath=shard1/server3
     bind_ip=0.0.0.0 
     port=37019 
     fork=true 
     logpath=shard1/shard1-37019.log
     replSet=shard1 
     shardsvr=true
     ```

   - 启动配置节点
   
     ```shell
     ./bin/mongod -f shard1/shard1-37017.conf 
     ./bin/mongod -f shard1/shard1-37018.conf 
     ./bin/mongod -f shard1/shard1-37019.conf 
     ```
   
   - 进入其中一个进行集群配置 
   
     ```
     ./bin/mongo --port 37017
     
     var cfg ={"_id":"shard1", "protocolVersion" : 1, 
     "members":[ 
     {"_id":1,"host":"10.211.55.11:37017"}, 
     {"_id":2,"host":"10.211.55.11:37018"}, 
     {"_id":3,"host":"10.211.55.11:37019"}
     ] };
     
     rs.initiate(cfg)
     ```
   
   **shard2**集群搭建47017到47019:![img_23](https://img.zhaojq.top/20260727092028807.png "img_23")![img_25](https://img.zhaojq.top/20260727092029315.png "img_25")
   
   - 三个节点配置如下
   
     ```properties
     dbpath=shard2/server1
     bind_ip=0.0.0.0 
     port=47017 
     fork=true 
     logpath=shard2/shard2-47017.log
     replSet=shard2 
     shardsvr=true
     ---------------------
     dbpath=shard2/server2
     bind_ip=0.0.0.0 
     port=47018 
     fork=true 
     logpath=shard2/shard2-47018.log
     replSet=shard2 
     shardsvr=true
     ---------------------
     dbpath=shard2/server3
     bind_ip=0.0.0.0 
     port=47019 
     fork=true 
     logpath=shard2/shard2-47019.log
     replSet=shard2 
     shardsvr=true
     ```
   
   - 启动配置节点
   
     ```shell
     ./bin/mongod -f shard2/shard2-47017.conf 
     ./bin/mongod -f shard2/shard2-47018.conf 
     ./bin/mongod -f shard2/shard2-47019.conf 
     ```
   
   - 进入其中一个进行集群配置 
   
     ```
     ./bin/mongo --port 47017
     
     var cfg ={"_id":"shard2", "protocolVersion" : 1, 
     "members":[ 
     {"_id":1,"host":"10.211.55.11:47017"}, 
     {"_id":2,"host":"10.211.55.11:47018"}, 
     {"_id":3,"host":"10.211.55.11:47019"}
     ] };
     
     rs.initiate(cfg)
     ```
   
2. 路由节点配置和启动

   ![img_26](https://img.zhaojq.top/20260727092029507.png "img_26") ![img_27](https://img.zhaojq.top/20260727092029674.png "img_27")

   - router-27017.conf

     ```properties
     port=27017 
     bind_ip=0.0.0.0 
     fork=true 
     logpath=router/router.log
     configdb=configsvr/10.211.55.11:17017,10.211.55.11:17018,10.211.55.11:17019
     ```
   
   - 启动路由节点
   
     ```
     ./bin/mongos -f router/router-27017.conf
     ```
   
2. 路由中添加分片节点

   - 进入mongos

     ```
     ./bin/mongo --port 27017
     ```
   
   - 添加分片节点
   
     ```
     sh.status();
     ```
   
     ![img_28](https://img.zhaojq.top/20260727092029852.png "img_28")
   
     ```
     sh.addShard("shard1/10.211.55.11:37017,10.211.55.11:37018,10.211.55.11:37019");
     sh.addShard("shard2/10.211.55.11:47017,10.211.55.11:47018,10.211.55.11:47019");
     
     sh.status();
     ```
   
     ![img_29](https://img.zhaojq.top/20260727092030043.png "img_29")
   
5. 开启数据库和集合分片(指定片键)

   继续使用mongos完成分片开启和分片大小设置

   ```
   # 为数据库开启分片功能 
   sh.enableSharding("my_test") 
   
   为指定集合开启分片功能: 片键字段名如 name, 索引使用hash 
   片键: 1代表范围分片, hashed代表哈希分片
   sh.shardCollection("my_test.my_test_data",{"name":"hashed"})
   ```

6. 向集合中插入数据测试

   通过路由循环向集合中添加数

   ```
   use my_test; 
   for(var i=1;i<= 100;i++){ 
     db.my_test_data.insert(
     	{"name":"test"+i, salary:(Math.random()*20000).toFixed(2)}
     ); 
   }
   ```

7. 验证分片效果

   分别进入shard1和shard2查看数据














