MongoDB副本集及分片

MongoDB集群有副本集主从复制两种模式,不过主从模式在MongoDB 3.6已经彻底废弃,今天主要探讨副本集的搭建和使用,以及分片。

副本集介绍

副本集(Replica Set)即副本的集合,在MongoDB中通过先定义一个副本集合,然后将多个节点(副本)加入到这个集合中。简单来说就是集群中包含了多份数据,保证主节点挂掉,备节点能够继续提供数据服务,实现MongoDB的数据备份及高可用。

副本集具有以下特征

  • N 个节点的集群
  • 任何节点可作为主节点
  • 所有写入操作都在主节点上
  • 自动故障转移
  • 自动恢复

副本集搭建

条件有限,我们在单机上,通过三个不同的MongoD线程来搭副本集。

节点IP端口
主节点127.0.0.127017
副本节点1127.0.0.127018
副本节点2127.0.0.127019

主节点配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 指定数据库路径
dbpath=/usr/local/mongodb/data/db
# 使用追加的方式写日志
logpath=/usr/local/mongodb/log/mongodb.log
# 使用追加的方式写日志
logappend = true
# 绑定服务IP
bind_ip=127.0.0.1
# 服务器端口
port = 27017
# 以守护进程的方式运行MongoDB,创建服务器进程
fork = true
# PID File 的完整路径
pidfilepath=/usr/local/mongodb/var/mongod.pid
# 不启用验证
noauth=true
# 最大同时连接数,默认2000
maxConns=2000
# 同步复制的日志大小设置,单位MB
oplogSize=10
# 副本集名称
replSet=rs0

副本节点的配置和主节点的基本一致,需要修改一下数据库/日志/PID路径和端口号,副本集名称需一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 指定数据库路径
dbpath=/usr/local/mongodb/node/2/data/db
# 使用追加的方式写日志
logpath=/usr/local/mongodb/node/2/log/mongodb.log
# 使用追加的方式写日志
logappend = true
# 绑定服务IP
bind_ip=127.0.0.1
# 服务器端口
port = 27018
# 以守护进程的方式运行MongoDB,创建服务器进程
fork = true
# PID File 的完整路径
pidfilepath=/usr/local/mongodb/var/mongod2.pid
# 不启用验证
noauth=true
# 最大同时连接数,默认2000
maxConns=2000
# 副本集
replSet=rs0

依次启动三个mongod进程:

1
2
3
4
gitlib@devops:/usr/local/mongodb$ ps -aux | grep mongod
root 14293 0.8 2.3 1588812 92700 ? Sl 08:06 0:01 bin/mongod -f mongod.conf
root 14652 3.5 2.2 1583180 89364 ? Sl 08:08 0:00 bin/mongod -f mongod2.conf
root 14723 6.4 2.2 1583180 89172 ? Sl 08:08 0:00 bin/mongod -f mongod3.conf

在主节点中,先使用rs.initiate()方法进行副本集初始化操作,再使用rs.add()方法来添加副本集的成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
> rs.initiate()
{
"info2" : "no configuration specified. Using a default configuration for the set",
"me" : "127.0.0.1:27017",
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1569457173, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1569457173, 1)
}
rs0:OTHER> rs.add('127.0.0.1:27018');
{
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1569457214, 2),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1569457214, 2)
}
rs0:PRIMARY> rs.add('127.0.0.1:27019');
{
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1569457219, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1569457219, 1)
}
rs0:PRIMARY>

到此,MongoDB副本集部署完成,我们可以通过rs.status()命令查看副本集状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
gitlib@devops:~$ mongo 127.0.0.1:27018
rs0:SECONDARY> rs.status()
{
"set" : "rs0",
"date" : ISODate("2019-09-26T12:09:48.818Z"),
"myState" : 2,
"term" : NumberLong(1),
"syncingTo" : "127.0.0.1:27017",
"syncSourceHost" : "127.0.0.1:27017",
"syncSourceId" : 0,
"heartbeatIntervalMillis" : NumberLong(2000),
"optimes" : {
"lastCommittedOpTime" : {
"ts" : Timestamp(1569499786, 1),
"t" : NumberLong(1)
},
"lastCommittedWallTime" : ISODate("2019-09-26T12:09:46.038Z"),
"readConcernMajorityOpTime" : {
"ts" : Timestamp(1569499786, 1),
"t" : NumberLong(1)
},
"readConcernMajorityWallTime" : ISODate("2019-09-26T12:09:46.038Z"),
"appliedOpTime" : {
"ts" : Timestamp(1569499786, 1),
"t" : NumberLong(1)
},
"durableOpTime" : {
"ts" : Timestamp(1569499786, 1),
"t" : NumberLong(1)
},
"lastAppliedWallTime" : ISODate("2019-09-26T12:09:46.038Z"),
"lastDurableWallTime" : ISODate("2019-09-26T12:09:46.038Z")
},
"lastStableRecoveryTimestamp" : Timestamp(1569499726, 1),
"lastStableCheckpointTimestamp" : Timestamp(1569499726, 1),
"members" : [
{
"_id" : 0,
"name" : "127.0.0.1:27017",
"ip" : "127.0.0.1",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 42574,
"optime" : {
"ts" : Timestamp(1569499786, 1),
"t" : NumberLong(1)
},
"optimeDurable" : {
"ts" : Timestamp(1569499786, 1),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2019-09-26T12:09:46Z"),
"optimeDurableDate" : ISODate("2019-09-26T12:09:46Z"),
"lastHeartbeat" : ISODate("2019-09-26T12:09:47.119Z"),
"lastHeartbeatRecv" : ISODate("2019-09-26T12:09:47.667Z"),
"pingMs" : NumberLong(0),
"lastHeartbeatMessage" : "",
"syncingTo" : "",
"syncSourceHost" : "",
"syncSourceId" : -1,
"infoMessage" : "",
"electionTime" : Timestamp(1569457173, 2),
"electionDate" : ISODate("2019-09-26T00:19:33Z"),
"configVersion" : 3
},
{
"_id" : 1,
"name" : "127.0.0.1:27018",
"ip" : "127.0.0.1",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 43284,
"optime" : {
"ts" : Timestamp(1569499786, 1),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2019-09-26T12:09:46Z"),
"syncingTo" : "127.0.0.1:27017",
"syncSourceHost" : "127.0.0.1:27017",
"syncSourceId" : 0,
"infoMessage" : "",
"configVersion" : 3,
"self" : true,
"lastHeartbeatMessage" : ""
},
{
"_id" : 2,
"name" : "127.0.0.1:27019",
"ip" : "127.0.0.1",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 42569,
"optime" : {
"ts" : Timestamp(1569499786, 1),
"t" : NumberLong(1)
},
"optimeDurable" : {
"ts" : Timestamp(1569499786, 1),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2019-09-26T12:09:46Z"),
"optimeDurableDate" : ISODate("2019-09-26T12:09:46Z"),
"lastHeartbeat" : ISODate("2019-09-26T12:09:47.646Z"),
"lastHeartbeatRecv" : ISODate("2019-09-26T12:09:47.036Z"),
"pingMs" : NumberLong(0),
"lastHeartbeatMessage" : "",
"syncingTo" : "127.0.0.1:27018",
"syncSourceHost" : "127.0.0.1:27018",
"syncSourceId" : 1,
"infoMessage" : "",
"configVersion" : 3
}
],
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1569499786, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1569499786, 1)
}

副本集高可用

集群中的各节点还会通过传递心跳信息来检测各自的健康状况。当主节点故障时,多个从节点会触发一次 新的选举操作,并选举其中的一个成为新的主节点(通常谁的优先级更高,谁就是新的主节点),心跳信息默认每 2 秒传递一次。

MongoDB

客户端连接到副本集后,不关心具体哪一台机器是否挂掉。主服务器负责整个副本集的读写,副本集定期同步数据备份。一旦主节点挂掉,副本节点就会选举一个新的主服务器。这一切对于应用服务器不需要关心。

MongoDB

我们可以通过关闭主节点,测试是否会选举新的主节点:

1
2
3
4
5
6
7
8
9
gitlib@devops:~$ ps -aux | grep mongod
root 14293 0.6 2.5 1888584 99504 ? Sl 08:06 4:39 bin/mongod -f mongod.conf
root 14652 0.6 2.6 1923896 102200 ? Sl 08:08 4:59 bin/mongod -f mongod2.conf
root 14723 0.6 2.5 1886124 98984 ? Sl 08:08 4:47 bin/mongod -f mongod3.conf
gitlib@devops:~$ sudo kill -9 14293
[sudo] password for zhoufei:
zhoufei@devops:~$ ps -aux | grep mongod
root 14652 0.6 2.6 1932092 102200 ? Sl 08:08 4:59 bin/mongod -f mongod2.conf
root 14723 0.6 2.5 1894320 99064 ? Sl 08:08 4:47 bin/mongod -f mongod3.conf

我们直接kill掉主节点,进入节点1,看一下当前节点是否是主节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gitlib@devops:~$ mongo 127.0.0.1:27018
rs0:SECONDARY> rs.isMaster()
{
"hosts" : [
"127.0.0.1:27017",
"127.0.0.1:27018",
"127.0.0.1:27019"
],
"setName" : "rs0",
"setVersion" : 3,
"ismaster" : false,
"secondary" : true,
"primary" : "127.0.0.1:27019",
"me" : "127.0.0.1:27018",
...

可以看到当主节点(127.0.0.1:27017)挂掉之后,主节点自动切换到从节点2(127.0.0.1:27019)上。

副本集选举机制

副本集中的从节点在主节点挂掉后通过心跳机制检测到后,就会在集群内发起主节点的选举机制,自动选举出一位新的主服务器。

副本集包括三种节点:主节点从节点仲裁节点

  • 主节点负责处理客户端请求,读、写数据, 记录在其上所有操作的oplog;
  • 从节点定期轮询主节点获取这些操作,然后对自己的数据副本执行这些操作,从而保证从节点的数据与主节点一致。默认情况下,从节点不支持外部读取,但可以设置,副本集的机制在于主节点出现故障的时候,余下的节点会选举出一个新的主节点,从而保证系统可以正常运行。
  • 仲裁节点不复制数据,仅参与投票。由于它没有访问的压力,比较空闲,因此不容易出故障。由于副本集出现故障的时候,存活的节点必须大于副本集节点总数的一半,否则无法选举主节点,或者主节点会自动降级为从节点,整个副本集变为只读。因此,增加一个不容易出故障的仲裁节点,可以增加有效选票,降低整个副本集不可用的风险。仲裁节点可多于一个。也就是说只参与投票,不接收复制的数据,也不能成为活跃节点。

官方推荐MongoDB副本节点最少为3台, 建议副本集成员为奇数,最多12个副本节点,最多7个节点参与选举。限制副本节点的数量,主要是因为一个集群中过多的副本节点,增加了复制的成本,反而拖累了集群的整体性能。 太多的副本节点参与选举,也会增加选举的时间。而官方建议奇数的节点,是为了避免脑裂 的发生。

选举过程

副本集的选举过程大致如下:

  1. 得到每个服务器节点的最后操作时间戳。每个 mongodb都有oplog机制会记录本机的操作,方便和主服务器进行对比数据是否同步还可以用于错误恢复。
  2. 如果集群中大部分服务器down机了,保留活着的节点都为secondary状态并停止,不选举了。
  3. 如果集群中选举出来的主节点或者所有从节点最后一次同步时间看起来很旧了,停止选举等待人来操作。
  4. 如果上面都没有问题就选择最后操作时间戳最新(保证数据是最新的)的服务器节点作为主节点。

    MongoDB 同步延迟问题

在MongoDB中,所有写操作都会产生 oplog,oplog 是每修改一条数据都会生成一条,如果你采用一个批量update命令更新了 N 多条数据,那么oplog 会有很多条,而不是一条。所以同步延迟就是写操作在主节点上执行完后,从节点还没有把 oplog 拿过来再执行一次。而这个写操作的量越大,主节点与从节点的差别也就越大,同步延迟也就越大了。

分片

当MongoDB存储海量的数据时,一台机器可能不足以存储数据,也可能不足以提供可接受的读写吞吐量。这时我们就可以通过在多台机器上分割数据,使得数据库系统能存储和处理更多的数据。

分片集群结构分布:

MongoDB

三个主要组件:

  • Shard:数据存储位置,以chunk为单位存数据,实际生产环境中一个shard server角色可由几台机器组个一个replica set承担,防止主机单点故障;
  • Config Server:mongod实例,存储了整个ClusterMetadata,其中包括 chunk信息,默认需要配置3个Config Server节点;
  • Query Routers:(Mongos) 前端路由,客户端由此接入,且让整个集群看上去像单一数据库,前端应用可以透明使用。

Mongos本身并不持久化数据,Sharded Cluster所有的元数据都会存储到Config Server,而用户的数据会议分散存储到各个shard。Mongos启动后,会从配置服务器加载元数据,开始提供服务,将用户的请求正确路由到对应的碎片。

Mongos的路由功能

  • 当数据写入时,MongoDB Cluster根据分片键设计写入数据。
  • 当外部语句发起数据查询时,MongoDB根据数据分布自动路由至指定节点返回数据。

分片部署

条件有限,我们还是在单机上,用不同MongoDB线程来部署分片。

节点IP端口
Shard Server 1127.0.0.127020
Shard Server 2127.0.0.127021
Shard Server 3127.0.0.127022
Shard Server 4127.0.0.127023
Config Server127.0.0.127100
Router Server127.0.0.140000

分片服务器

Shard Server和普通Mongod程序一样,不同的是需要在配置文件中添加shardsvr=true标记为Shard Server,配置参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 指定数据库路径
dbpath=/usr/local/mongodb/share/1/data/db
# 使用追加的方式写日志
logpath=/usr/local/mongodb/share/1/log/mongodb.log
# 使用追加的方式写日志
logappend = true
# 绑定服务IP
bind_ip=127.0.0.1
# 服务器端口
port = 27020
# 以守护进程的方式运行MongoDB,创建服务器进程
fork = true
# PID File 的完整路径
pidfilepath=/usr/local/mongodb/var/mongod27020.pid
# 不启用验证
noauth=true
# 最大同时连接数,默认2000
maxConns=2000
# 同步复制的日志大小设置,单位MB
oplogSize=10
# 设置为shared server
shardsvr=true

以上配置复制4份,修改一下数据库路径/日志路径/服务器IP和端口/PID路径,启动4个Shard Server:

1
2
3
4
sudo bin/mongod -f shard1.conf
sudo bin/mongod -f shard2.conf
sudo bin/mongod -f shard3.conf
sudo bin/mongod -f shard4.conf

配置服务器

4.0版本的MongoDB中配置服务器(Config Server)需要设置副本集,同时设置configsvr=true,配置参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 指定数据库路径
dbpath=/usr/local/mongodb/share/5/data/db
# 使用追加的方式写日志
logpath=/usr/local/mongodb/share/5/log/mongodb.log
# 使用追加的方式写日志
logappend = true
# 绑定服务IP
bind_ip=127.0.0.1
# 服务器端口
port = 27100
# 以守护进程的方式运行MongoDB,创建服务器进程
fork = true
# PID File 的完整路径
pidfilepath=/usr/local/mongodb/var/mongod27100.pid
# 不启用验证
noauth=true
# 最大同时连接数,默认2000
maxConns=2000
# 同步复制的日志大小设置,单位MB
oplogSize=10
# 配置为config server
configsvr=true
# 副本集名称
replSet=rs0

启动Config Server,并初始化副本集:

1
2
3
sudo bin/mongod -f shard-config.conf
mongo 127.0.0.1:27100
> rs.initiaze()

新版本MongoDB建议设置多个Config Server,采用副本集形式设置集群,为了搭建方便,这里我们只采用单个Config Server。

路由服务器

Router Server不存放数据,配置参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 使用追加的方式写日志
logpath=/usr/local/mongodb/share/6/log/mongodb.log
# 使用追加的方式写日志
logappend = true
# 绑定服务IP
bind_ip=127.0.0.1
# 服务器端口
port = 4000
# 以守护进程的方式运行MongoDB,创建服务器进程
fork = true
# PID File 的完整路径
pidfilepath=/usr/local/mongodb/var/mongod4000.pid
# 设置监听的config服务器
configdb=rs0/127.0.0.1:27100

启动Router Server,路由服务器是由mongos命令启动,与分片服务器及配置服务器不同。

1
sudo bin/mongos -f shard-router.conf

启动后,需要通过sh.addShard()命令添加分片服务器:

1
2
3
4
sh.addShard('127.0.0.1:27020')
sh.addShard('127.0.0.1:27021')
sh.addShard('127.0.0.1:27022')
sh.addShard('127.0.0.1:27023')

配置完成后,可以通过sh.status()命令,查看分片情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
mongos> sh.status()
--- Sharding Status ---
sharding version: {
"_id" : 1,
"minCompatibleVersion" : 5,
"currentVersion" : 6,
"clusterId" : ObjectId("5d8ddd1d94796dc650e29f67")
}
shards:
{ "_id" : "shard0000", "host" : "127.0.0.1:27020", "state" : 1 }
{ "_id" : "shard0001", "host" : "127.0.0.1:27021", "state" : 1 }
{ "_id" : "shard0002", "host" : "127.0.0.1:27022", "state" : 1 }
{ "_id" : "shard0003", "host" : "127.0.0.1:27023", "state" : 1 }
active mongoses:
"4.2.0" : 1
autosplit:
Currently enabled: yes
balancer:
Currently enabled: yes
Currently running: no
Failed balancer rounds in last 5 attempts: 0
Migration Results for the last 24 hours:
No recent migrations
databases:
{ "_id" : "config", "primary" : "config", "partitioned" : true }
config.system.sessions
shard key: { "_id" : 1 }
unique: false
balancing: true
chunks:
shard0000 1
{ "_id" : { "$minKey" : 1 } } -->> { "_id" : { "$maxKey" : 1 } } on : shard0000 Timestamp(1, 0)
有用就打赏一下作者吧!