目录
在MongoDB学习(一):安装&基础概念&数据类型&部分shell操作曾经提到过,MongoDB有复制集(副本集)和分片集的概念。
1 概念
复制集是主从机制的扩展与改进,例如:
- 容灾机制:主节点(Primary)down之后,自动通过选举机制提升从节点(Secondary)为新的主节点,保证集群可用性
- 一致性:事务提交需要经过50%以上节点确认方可成功,否则将回滚
- 该内容可以通过WriteConcern选项进行配置,详见MongoDB学习(二):CRUD操作、条件运算、分页操作、排序
好处有:
- 高可用、高一致性
- 防止误操作:主从节点数据同步有一定延时,通过数据冗余,可以一定程度上避免删库跑路
- 负载均衡:通过将读操作均匀分配给各节点,避免过多请求冲击导致的宕机
复制集不适用于以下场景:
- 硬件不足:例如数据量大于内存时,就必须使用硬盘上的虚拟内存进行数据交换,导致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);
之后的使用和连接单服务器一致。