MongoDB学习(六):集群之复制集

目录

1 概念

2 配置

3 原理

1)oplog

2)心跳机制

4 客户端连接集群


MongoDB学习(一):安装&基础概念&数据类型&部分shell操作曾经提到过,MongoDB有复制集(副本集)和分片集的概念。

1 概念

复制集是主从机制的扩展与改进,例如:

  • 容灾机制:主节点(Primary)down之后,自动通过选举机制提升从节点(Secondary)为新的主节点,保证集群可用性
  • 一致性:事务提交需要经过50%以上节点确认方可成功,否则将回滚

好处有:

  • 高可用、高一致性
  • 防止误操作:主从节点数据同步有一定延时,通过数据冗余,可以一定程度上避免删库跑路
  • 负载均衡:通过将读操作均匀分配给各节点,避免过多请求冲击导致的宕机

复制集不适用于以下场景:

  • 硬件不足:例如数据量大于内存时,就必须使用硬盘上的虚拟内存进行数据交换,导致I/O下降,此时集群并不比单机快多少,最好先使用分片对数据进行分割
  • 写多读少:此时主节点不但要承担大量的写操作,还需要频繁进行数据同步,反而降低了效率
  • 持续读:由于从节点并不是实时同步数据的,要么读取的数据存在过期的风险,要么需要将每次写入的数据进行同步,导致较大的延时

关于选举方式等,可以参考Paxos算法。在MongoDB 3.0中,复制集最多支持50个节点。

2 配置

上面提到,复制集事务提交需要超过50%节点确认,因此推荐配置奇数个节点,最少3个(如果仅有2个节点,那么每个结点都不能down,这样的配置意义不大),这三个节点可以都存放数据(一主两从),也可以配置一个仲裁节点(一主一从)。仲裁节点的作用是,当集群由于网络问题出现了两个节点数相等的分区时,它可以强制进行选举以维持服务。

一个包括主节点、从节点和仲裁节点的复制集​​​​

我们在本地进行模拟。

首先创建三个文件夹:

root@Ubuntu:~# mkdir ~/primary
root@Ubuntu:~# mkdir ~/secondary
root@Ubuntu:~# mkdir ~/arbiter

然后基于这三个文件夹启动mongod:

 mongod --replSet test --dbpath ~/primary --port 9000
 mongod --replSet test --dbpath ~/secondary --port 9001
 mongod --replSet test --dbpath ~/arbiter --port 9002

然后连接主节点并初始化:

mongo --port 9000
> rs.initiate()
{
        "info2" : "no configuration specified. Using a default configuration for the set",
        "me" : "localhost:9000",
        "ok" : 1,
        "operationTime" : Timestamp(1549627807, 1),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1549627807, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

然后添加从节点和仲裁节点到复制集:

test:PRIMARY> rs.add("localhost:9001")
{
        "ok" : 1,
        "operationTime" : Timestamp(1549627990, 1),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1549627990, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}
test:PRIMARY> rs.addArb("localhost:9002")
{
        "ok" : 1,
        "operationTime" : Timestamp(1549627994, 1),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1549627994, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        }
}

addArb("localhost:9002")add("localhost:9002",{arbiterOnly:true})等价。

上述过程可以进行简化:

> config = {_id:"test",members:[]}
{ "_id" : "test", "members" : [ ] }
> config.members.push({_id:0,host:"localhost:9000"})
1
> config.members.push({_id:1,host:"localhost:9001"})
2
> config.members.push({_id:2,host:"localhost:9002",arbiterOnly:true})
3
> rs.initiate(config)

config.members的配置内容如下:

  • _id:不可重复、增长的数字,代表节点ID
  • host:节点url
  • arbiterOnly:是否为仲裁节点
  • priority:0~1000的数字,代表该节点被选举为主节点的可能性(例如某些节点所在机器性能较强,更适合作为主节点)
  • votes:代表该节点每次得票数,默认每次得1票
  • hidden:使得该成员信息不显示在isMaster命令的输出中,可结合buildIndexes使用,必须与slaveDelay合用
  • buildIndexes:该节点是否会建立索引,适用于永远不会成为主节点的节点(priority为0)
  • slaveDelay:该从节点落后主节点的秒数(也就是多久同步一次),适用于永远不会成为主节点的节点(priority为0)
  • tags:一个子文档,用来标记节点

此时可以用rs.status()函数查看复制集状态,或者使用db.isMaster()查看集群拓扑,也可以使用db.getReplicationInfo()查看一些基本信息。

下面试试数据能否在主从节点间同步:

# 主节点操作
test:PRIMARY> use test
switched to db test
test:PRIMARY> db.user.insert({name:"xiaoming",sex:"man",age:18,hobby:"programming"})
WriteResult({ "nInserted" : 1 })

# 从节点操作
test:SECONDARY> rs.slaveOk()
test:SECONDARY> use test
switched to db test
test:SECONDARY> db.user.find()
{ "_id" : ObjectId("5c5d73b3b2ba7f15c8992a29"), "name" : "xiaoming", "sex" : "man", "age" : 18, "hobby" : "programming" }

可以看到,向主节点插入的数据,在从节点上也可以读取到

下面我们尝试关闭主节点:

# 主节点操作
test:PRIMARY> db.shutdownServer()
server should be down...
2019-02-08T20:22:35.829+0800 I NETWORK  [thread1] trying reconnect to 127.0.0.1:9000 (127.0.0.1) failed
2019-02-08T20:22:40.354+0800 I NETWORK  [thread1] reconnect 127.0.0.1:9000 (127.0.0.1) failed failed
>

# 从节点日志
2019-02-08T20:22:36.609+0800 I REPL     [replexec-5] Member localhost:9001 is now in state PRIMARY

可以看到,从节点被自动提升为主节点了

假如想重新配置复制集(例如:添加一个新节点),可以使用reconfig函数:

test:PRIMARY> config = rs.conf()
…… // config内容
test:PRIMARY> …… //对config进行编辑
test:PRIMARY> rs.reconfig(config)

3 原理

复制集有两大基本机制:oplog和心跳机制。oplog的作用是进行数据复制,心跳机制用来保证可用性。

1)oplog

oplog实际是一个固定集合,存放在local数据库中。当进行写操作时,首先会把操作记录到主节点的oplog中,然后从节点使用长轮询机制读取oplog最新一条记录的时间戳和版本,假如主节点有了新数据,那么就根据自身和主节点的进度差读取数据,并进行数据同步(一次增量同步,同步完会更新自己的oplog)。

test:PRIMARY> use local
switched to db local
test:PRIMARY> show collections
me
oplog.rs
replset.election 
replset.minvalid //存放了复制集成员初始化同步的信息
replset.oplogTruncateAfterPoint
startup_log
system.replset //存储了复制集配置文档
system.rollback.id
test:PRIMARY> db.oplog.rs.findOne({op:"i"})
{
        "ts" : Timestamp(1549628166, 1),
        "t" : NumberLong(1),
        "h" : NumberLong("8489469240773869540"),
        "v" : 2,
        "op" : "i",
        "ns" : "config.system.sessions",
        "ui" : UUID("4f7df555-faf6-4f66-b663-0c4e4a8b3ca1"),
        "wall" : ISODate("2019-02-08T12:16:06.060Z"),
        "o" : {
                "_id" : {
                        "id" : UUID("c6bde58c-cb90-4e57-a782-93e7da659220"),
                        "uid" : BinData(0,"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=")
                },
                "lastUse" : ISODate("2019-02-08T12:16:06.060Z")
        }
}

这里我们查询了一次数据插入操作的日志。

  • ts:存储了记录的时间戳信息,由两个数字组成,第一个是本世代开始后的秒数,第二个是世代数
  • op:操作,i代表插入,u代表更新
  • ns:命名空间
  • o:代表了插入的文档的副本

由于oplog是一个固定集合,这意味着操作过多时,太早的操作日志会被“挤出去”,假如一个从节点下线了很长时间,重新上线后就很可能无法找到自己的同步点,这时就需要进行一次完整同步。

oplog默认50MB(32位系统),1GB 或 5%可用磁盘空间(64位系统),在3.0版本以后,可以自行调整大小:

mongod --replSet test --oplogSize 1024

单位为MB,上述命令设置oplog大小为1GB。

2)心跳机制

心跳机制就是每隔若干时间,复制集成员间进行互ping,如果能正常收到回复,说明对方存活。在rs.staus()输出中,可以看到每个节点上一次心跳检测的信息:

                {
                        "_id" : 2,
                        "name" : "localhost:9002",
                        "health" : 1,
                        "state" : 7,
                        "stateStr" : "ARBITER",
                        "uptime" : 1926,
                        "lastHeartbeat" : ISODate("2019-02-08T13:00:29.280Z"),
                        "lastHeartbeatRecv" : ISODate("2019-02-08T13:00:28.738Z"),
                        "pingMs" : NumberLong(0),
                        "lastHeartbeatMessage" : "",
                        "syncingTo" : "",
                        "syncSourceHost" : "",
                        "syncSourceId" : -1,
                        "infoMessage" : "",
                        "configVersion" : 5
                }

health字段中,1代表存活,0代表无响应(即该节点下线)

假如一个节点无响应,可能有以下情况:

  • 该节点是主节点:那么需要进行一次选举,挑选数据最新的从节点进行晋升,原主节点重新上线后调整为从节点并进行数据同步
  • 该节点不是主节点:基本无影响,集群继续运行,下线节点如果是从节点,上线后进行数据同步即可
  • 主节点存活但剩余节点不足一半:主节点降级为从节点

最后一条看起来很奇怪,却有其合理性:假如由于网络故障,导致集群出现了一大一小两个分区,那么大的那个(如果没有主节点)必然会通过选举产生新的主节点。如果此时旧主节点不降级,就会出现分叉。等到网络恢复后,将不得不丢弃掉分区期间,小分区节点新增的数据。

states代表了集群状态:

状态值 状态 描述
0 STARTUP 复制集正在与各节点通过ping进行协商
1 PRIMARY 该节点是主节点
2 SECONDARY 该节点是从节点
3 RECOVERING 该节点刚刚恢复上线,正在进行数据同步
4 FATAL 该节点可以连接,但不响应ping
5 STARTUP2 初始化过程中,数据同步的状态
6 UNKNOWN 无法通过网络连接的节点
7 ABITER 仲裁节点
8 DOWN 该节点已下线
9 ROLLBACK 该节点正在进行事务回滚
10 REMOVED 该节点已经退出复制集

4 客户端连接集群

Java 客户端连接MongoDB集群非常简单。由于驱动会自动识别节点身份,我们只要提供host列表即可

  • 字符串式:

      MongoClient mongoClient = MongoClients.create(
                  "mongodb://host1:27017,host2:27017,host3:27017");
    

    可以显式提供复制集名称 

      MongoClient mongoClient = MongoClients.create(
                  "mongodb://host1:27017,host2:27017,host3:27017/?replicaSet=myReplicaSet");
    
  • ConnectionString式:类似字符串式

      MongoClient mongoClient = MongoClients.create(
            new ConnectionString("mongodb://host1:27017,host2:27017,host3:27017"));
    
      MongoClient mongoClient = MongoClients.create(
          new ConnectionString("mongodb://host1:27017,host2:27017,host3:27017/?replicaSet=myReplicaSet"));
    
  • ClusterSetting+MongoClientSettings式:

      ClusterSettings clusterSettings = ClusterSettings.builder()
                                          .hosts(asList(
                                              new ServerAddress("host1", 27017),
                                              new ServerAddress("host2", 27017),
                                              new ServerAddress("host3", 27017)))
                                          .build();
    
      MongoClientSettings settings = MongoClientSettings.builder()
                                          .clusterSettings(clusterSettings).build();
    
      MongoClient mongoClient = MongoClients.create(settings);

之后的使用和连接单服务器一致。 

猜你喜欢

转载自blog.csdn.net/u010670411/article/details/86776378