前面我们学习了mongo的环境搭建到文档基础操作,以及高级查询处理等常见操作,但是一直都是处理的单机服务器,在我们实际生产中,使用单机风险会很高,如果是服务崩溃了或者不可访问怎么办,那么至少有一段时间不可用,如果是硬件出了问题,那么数据可能还要转移到其他机器上。但是无论是什么样的故障都可能或多或少带来体验的问题,甚至可能造成数据丢失等风险。而MongoDB自身是支持复制集操作的,即将数据保存在多个服务上,副本集本身就是一组服务,其中一个是主服务,用于对外公开使用,处理客户端的请求,除此之外还有多个备份服务,如果主服务奔溃,那么则会通过选举算法选出一个新的主服务。
复制集初体验
由于复制集需要不止一个mongo实例,那么我们可以考虑在当前机器中启动多个mongo实例来模拟多个mongo服务器组建复制集。首先我们先把mongo.conf文件复制多份,用于不同的实例启动,由于不记得当前的每个配置的路径,我们的mongo是启动状态,因此我们可以通过查看配置找到其他的目录:
systemctl status mongod
可以看到当前mongo的启动参数:
* mongod.service - MongoDB Database Server
Loaded: loaded (/usr/lib/systemd/system/mongod.service; enabled; vendor preset: disabled)
Active: active (running) since �� 2021-01-12 23:35:12 CST; 1h 16min ago
Docs: https://docs.mongodb.org/manual
Process: 1276 ExecStart=/usr/bin/mongod $OPTIONS (code=exited, status=0/SUCCESS)
Process: 1268 ExecStartPre=/usr/bin/chmod 0755 /var/run/mongodb (code=exited, status=0/SUCCESS)
Process: 1263 ExecStartPre=/usr/bin/chown mongod:mongod /var/run/mongodb (code=exited, status=0/SUCCESS)
Process: 1260 ExecStartPre=/usr/bin/mkdir -p /var/run/mongodb (code=exited, status=0/SUCCESS)
Main PID: 1554 (mongod)
CGroup: /system.slice/mongod.service
`-1554 /usr/bin/mongod -f /etc/mongod.conf
最后面我们可以看到当前的mongo文件目录在/usr/bin/目录,而启动使用的配置文件则是在/etc目录下,现在我们去etc目录下,cp两份配置文件:
cp mongod.conf mongod2.conf;
cp mongod.conf mongod3.conf;
创建完毕以后,我们将配置文件中的log存储目录,以及数据库存储目录和启动端口pid文件修改,并且将启动的端口修改,其中需要注意一点,我们需要在配置文件中指定当前的replication参数,即指定复制集名称:
# mongod.conf
# for documentation of all options, see:
# http://docs.mongodb.org/manual/reference/configuration-options/
# where to write logging data.
systemLog:
destination: file
logAppend: true
#修改mongo日志名称为/var/log/mongodb/mongod2.log和/var/log/mongodb/mongod3.log
path: /var/log/mongodb/mongod.log
# Where and how to store data.
storage:
#修改mongo数据库存储的目录为/var/lib/mongo2和/var/lib/mongo3
dbPath: /var/lib/mongo
journal:
enabled: true
# engine:
# wiredTiger:
# how the process runs
processManagement:
fork: true # fork and run in background
#修改pid存放文件路径名称为/var/run/mongodb/mongod2.pid和/var/run/mongodb/mongod3.pid
pidFilePath: /var/run/mongodb/mongod.pid
timeZoneInfo: /usr/share/zoneinfo
# network interfaces
net:
#修改启动端口分别为27018和27019
port: 27020
bindIp: 0.0.0.0
#security:
#operationProfiling:
#replication:
replication:
replSetName: test-replication
全部修改完毕以后,我们需要检查一下数据库存放的目录和日志目录是否存在,由于日志是和原来的mongo进程使用的同一个目录,仅仅是文件名不同,因此不需要重新创建,而数据库目录/var/lib/mongo2和/var/lib/mongo3则是之前没有的,我们需要先创建出来,否则无法启动:
cd /var/lib;
mkdir mongo2/ mongo3/;
这些做完以后,我们再去启动两个mongo进程:
/usr/bin/mongod -f /etc/mongod2.conf;
/usr/bin/mongod -f /etc/mongod3.conf;
可以看到输出内容:
about to fork child process, waiting until server is ready for connections.
forked process: 25498
child process started successfully, parent exiting
代表mongo进程启动成功,这时候我们查询一下当前启动进程列表进行确认:
ps -ef|grep mongo
可以看到三个mongo进程已经存在:
mongod 1554 1 1 1��12 ? 00:01:11 /usr/bin/mongod -f /etc/mongod.conf
root 25498 1 1 1��12 ? 00:00:55 ./mongod -f /etc/mongod2.conf
root 25696 1 1 1��12 ? 00:00:54 ./mongod -f /etc/mongod3.conf
root 107395 2401 0 01:08 pts/0 00:00:00 grep --color=auto mongo
现在我们可以开始搭建复制集了,首先我们需要使用shell连接我们想要指定为主服务的mongo机器:
/usr/bin/mongo --port 28020;
接着我们初始化复制集:
rs.initiate({ _id:"test-replication", members:[{ _id:0, host:"192.168.1.130:27020" },{ _id:1, host:"192.168.1.130:27018" },{ _id:2, host:"192.168.1.130:27019" }] })
可以看到如下的响应:
{
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1610473584, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1610473584, 1)
}
代表当前复制集创建成功,我们查看当前复制集的状态:
rs.status();
可以看到一大堆关于复制集的内容,其中每一个服务的实例都在上面,这个时候我们可以看到,光标的显示已经变成了刚才设置的复制集的名称了:
test-replication:PRIMARY>
这里可以看到前面是复制集的名称,后面是代表当前实例的角色,在复制集中有PRIMARY
和SECONDARY
两种角色,其中PRIMARY
角色代表当前是复制集中的主服务,而SECONDARY
角色则代表当前的mongo服务实例是备份服务。
复制集写入数据/读取数据 关闭复制集
复制集启动完毕以后,我们可以尝试在主服务中写入数据,然后在备份服务中读取刚才主服务中写入的数据查看复制集是否正常工作
db.test.insert({"abc":"test"});
//在主服务中查看刚刚插入的数据
db.test.find();
{ "_id" : ObjectId("5ffde4aad555549b75c807f6"), "abc" : "test" }
//再去启动一个shell连接,连接到其中任意一个备份服务
/usr/bin/mongo --port 27018 --host 192.168.1.130
//需要注意的时候,当前的会话第一次连接备份服务器,需要先调用rs.slaveOk();进行初始化,否则默认情况下当前备份服务器会报错,errmsg:not master and slaveOk=false
test-replication:SECONDARY> rs.slaveOk();
//接着查询
use replication;
db.test.find();
//可以看到我们刚刚插入的数据已经能查询出来了
{ "_id" : ObjectId("5ffde4aad555549b75c807f6"), "abc" : "test" }
查看当前主服务节点是否为我们指定的28020端口节点:
rs.isMaster();
//响应结果为
{
"hosts" : [
"192.168.1.130:27020",
"192.168.1.130:27018",
"192.168.1.130:27019"
],
"setName" : "test-replication",
"setVersion" : 1,
"ismaster" : false,
"secondary" : true,
"primary" : "192.168.1.130:27020",
"me" : "192.168.1.130:27018",
"lastWrite" : {
"opTime" : {
"ts" : Timestamp(1610477545, 1),
"t" : NumberLong(1)
},
"lastWriteDate" : ISODate("2021-01-12T18:52:25Z"),
"majorityOpTime" : {
"ts" : Timestamp(1610477545, 1),
"t" : NumberLong(1)
},
"majorityWriteDate" : ISODate("2021-01-12T18:52:25Z")
},
"maxBsonObjectSize" : 16777216,
"maxMessageSizeBytes" : 48000000,
"maxWriteBatchSize" : 100000,
"localTime" : ISODate("2021-01-12T18:52:35.359Z"),
"logicalSessionTimeoutMinutes" : 30,
"connectionId" : 28,
"minWireVersion" : 0,
"maxWireVersion" : 8,
"readOnly" : false,
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1610477545, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1610477545, 1)
}
可以看到当前复制集的机器ip集合以及当前服务并非主服务节点("ismaster" : false),同时也可以看到主服务节点的ip信息为:("primary" : "192.168.1.130:27020")
故障转移
mongo的复制集自身特性就支持自动故障转移
功能,即如果我们使用过程中mongo的主节点挂了,导致不可用了,这个时候mongo就会从备份节点中选举一个成为最新的主节点,为了验证这个功能,我们选择将目前的主节点服务关闭:
ps -ef|grep mongo
//所有的mongo列表
root 3156 1 1 18:06 ? 00:04:45 /usr/bin/mongod -f /etc/mongod1.conf
root 3358 1 1 18:06 ? 00:05:01 /usr/bin/mongod -f /etc/mongod2.conf
root 3596 1 1 18:07 ? 00:04:58 /usr/bin/mongod -f /etc/mongod3.conf
可以看到mongod1.conf启动的服务pid为3156,我们将其关闭掉:
//模拟mongo服务异常奔溃(直接强制kill)
kill -9 3156
//再次查看当前的mongo服务列表
ps -ef|grep mongo
//可以看到现在只有两个了
root 3358 1 1 18:06 ? 00:05:11 /usr/bin/mongod -f /etc/mongod2.conf
root 3596 1 1 18:07 ? 00:05:06 /usr/bin/mongod -f /etc/mongod3.conf
root 97710 2414 0 23:22 pts/0 00:00:00 grep --color=auto mongo
我们现在随便连接这剩下两个的任何一个,去查看现在的集群主节点是哪个:
/usr/bin/mongo --host 192.168.1.130 --port 27018;
//这里比较幸运,随便登录了一个,正好是选出来的新的主节点
test-replication:PRIMARY> db.isMaster();
//这里前面的primary已经表明了当前节点是主服务,不过我们依然选择通过isMaster函数查看具体信息
{
"hosts" : [
"192.168.1.130:27020",
"192.168.1.130:27018",
"192.168.1.130:27019"
],
"setName" : "test-replication",
"setVersion" : 1,
"ismaster" : true,
"secondary" : false,
"primary" : "192.168.1.130:27018",
"me" : "192.168.1.130:27018",
"electionId" : ObjectId("7fffffff0000000000000004"),
"lastWrite" : {
"opTime" : {
"ts" : Timestamp(1610724217, 1),
"t" : NumberLong(4)
},
"lastWriteDate" : ISODate("2021-01-15T15:23:37Z"),
"majorityOpTime" : {
"ts" : Timestamp(1610724217, 1),
"t" : NumberLong(4)
},
"majorityWriteDate" : ISODate("2021-01-15T15:23:37Z")
},
"maxBsonObjectSize" : 16777216,
"maxMessageSizeBytes" : 48000000,
"maxWriteBatchSize" : 100000,
"localTime" : ISODate("2021-01-15T15:23:38.444Z"),
"logicalSessionTimeoutMinutes" : 30,
"connectionId" : 80,
"minWireVersion" : 0,
"maxWireVersion" : 8,
"readOnly" : false,
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1610724217, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1610724217, 1)
}
可以看到"primary" : "192.168.1.130:27018",现在的主服务节点已经变更为了27018端口,而如果我们正常使用过程中,其实是无感知的,因为连接的是整个复制集,并不知道其主节点崩溃以及变更操作,并且可以在很短的时间内完成转移主服务节点,继续对外提供服务。
修改复制集配置
在我们的复制集启动以后,正常运行过程中,往往是不会将整个复制集重启,那么如果不支持动态修改配置的话,对于复制集的维护势必很困难,不过还好,mongo自身考虑到了这一点,支持我们在运行中动态修改部分复制集的配置。
动态添加/减少副本服务
在使用过程中,我们可以随时根据情况调整复制集的大小,进行动态的新增或者删除节点服务,为了演示效果,我们来复制一份配置文件,改名为mongod4.conf,将日志文件以及数据存放目录,启动端口等进行修改,(别忘记检查这些目录是否存在,不存在要创建一下):
mkdir /var/lib/mongo4;
//启动新的mongo实例
/usr/bin/mongod -f /etc/mongod4.conf;
//检查是否已经启动成功
ps -ef|grep mongod;
//可以看到已经启动了
root 40069 1 6 00:27 ? 00:00:01 /usr/bin/mongod -f /etc/mongod4.conf
接着我们连接上主节点服务,开始添加刚刚启动的服务实例:
test-replication:PRIMARY> rs.add("192.168.1.130:27021");
//输出如下
{
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1610730168, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1610730168, 1)
}
//我们再次检查当前复制集的所有节点信息
test-replication:PRIMARY> rs.status();
//可以看到members中已经有我们刚刚指定的27021端口的mongo实例
{
"set" : "test-replication",
"date" : ISODate("2021-01-15T17:04:19.209Z"),
"myState" : 1,
"term" : NumberLong(4),
"syncingTo" : "",
"syncSourceHost" : "",
"syncSourceId" : -1,
"heartbeatIntervalMillis" : NumberLong(2000),
"majorityVoteCount" : 3,
"writeMajorityCount" : 3,
"optimes" : {
"lastCommittedOpTime" : {
"ts" : Timestamp(1610730258, 1),
"t" : NumberLong(4)
},
"lastCommittedWallTime" : ISODate("2021-01-15T17:04:18.456Z"),
"readConcernMajorityOpTime" : {
"ts" : Timestamp(1610730258, 1),
"t" : NumberLong(4)
},
"readConcernMajorityWallTime" : ISODate("2021-01-15T17:04:18.456Z"),
"appliedOpTime" : {
"ts" : Timestamp(1610730258, 1),
"t" : NumberLong(4)
},
"durableOpTime" : {
"ts" : Timestamp(1610730258, 1),
"t" : NumberLong(4)
},
"lastAppliedWallTime" : ISODate("2021-01-15T17:04:18.456Z"),
"lastDurableWallTime" : ISODate("2021-01-15T17:04:18.456Z")
},
"lastStableRecoveryTimestamp" : Timestamp(1610730238, 1),
"lastStableCheckpointTimestamp" : Timestamp(1610730238, 1),
"electionCandidateMetrics" : {
"lastElectionReason" : "electionTimeout",
"lastElectionDate" : ISODate("2021-01-15T15:22:27.376Z"),
"electionTerm" : NumberLong(4),
"lastCommittedOpTimeAtElection" : {
"ts" : Timestamp(1610724136, 1),
"t" : NumberLong(3)
},
"lastSeenOpTimeAtElection" : {
"ts" : Timestamp(1610724136, 1),
"t" : NumberLong(3)
},
"numVotesNeeded" : 2,
"priorityAtElection" : 1,
"electionTimeoutMillis" : NumberLong(10000),
"numCatchUpOps" : NumberLong(0),
"newTermStartDate" : ISODate("2021-01-15T15:22:27.386Z"),
"wMajorityWriteAvailabilityDate" : ISODate("2021-01-15T15:22:28.549Z")
},
"electionParticipantMetrics" : {
"votedForCandidate" : true,
"electionTerm" : NumberLong(3),
"lastVoteDate" : ISODate("2021-01-15T10:07:01.837Z"),
"electionCandidateMemberId" : 0,
"voteReason" : "",
"lastAppliedOpTimeAtElection" : {
"ts" : Timestamp(1610651352, 1),
"t" : NumberLong(2)
},
"maxAppliedOpTimeInSet" : {
"ts" : Timestamp(1610651352, 1),
"t" : NumberLong(2)
},
"priorityAtElection" : 1
},
"members" : [
{
"_id" : 0,
"name" : "192.168.1.130:27020",
"health" : 0,
"state" : 8,
"stateStr" : "(not reachable/healthy)",
"uptime" : 0,
"optime" : {
"ts" : Timestamp(0, 0),
"t" : NumberLong(-1)
},
"optimeDurable" : {
"ts" : Timestamp(0, 0),
"t" : NumberLong(-1)
},
"optimeDate" : ISODate("1970-01-01T00:00:00Z"),
"optimeDurableDate" : ISODate("1970-01-01T00:00:00Z"),
"lastHeartbeat" : ISODate("2021-01-15T17:04:18.412Z"),
"lastHeartbeatRecv" : ISODate("2021-01-15T15:22:16.398Z"),
"pingMs" : NumberLong(0),
"lastHeartbeatMessage" : "Error connecting to 192.168.1.130:27020 :: caused by :: Connection refused",
"syncingTo" : "",
"syncSourceHost" : "",
"syncSourceId" : -1,
"infoMessage" : "",
"configVersion" : -1
},
{
"_id" : 1,
"name" : "192.168.1.130:27018",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 25044,
"optime" : {
"ts" : Timestamp(1610730258, 1),
"t" : NumberLong(4)
},
"optimeDate" : ISODate("2021-01-15T17:04:18Z"),
"syncingTo" : "",
"syncSourceHost" : "",
"syncSourceId" : -1,
"infoMessage" : "",
"electionTime" : Timestamp(1610724147, 1),
"electionDate" : ISODate("2021-01-15T15:22:27Z"),
"configVersion" : 2,
"self" : true,
"lastHeartbeatMessage" : ""
},
{
"_id" : 2,
"name" : "192.168.1.130:27019",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 25032,
"optime" : {
"ts" : Timestamp(1610730248, 1),
"t" : NumberLong(4)
},
"optimeDurable" : {
"ts" : Timestamp(1610730248, 1),
"t" : NumberLong(4)
},
"optimeDate" : ISODate("2021-01-15T17:04:08Z"),
"optimeDurableDate" : ISODate("2021-01-15T17:04:08Z"),
"lastHeartbeat" : ISODate("2021-01-15T17:04:18.402Z"),
"lastHeartbeatRecv" : ISODate("2021-01-15T17:04:18.385Z"),
"pingMs" : NumberLong(0),
"lastHeartbeatMessage" : "",
"syncingTo" : "192.168.1.130:27018",
"syncSourceHost" : "192.168.1.130:27018",
"syncSourceId" : 1,
"infoMessage" : "",
"configVersion" : 2
},
{
"_id" : 3,
"name" : "192.168.1.130:27021",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 90,
"optime" : {
"ts" : Timestamp(1610730248, 1),
"t" : NumberLong(4)
},
"optimeDurable" : {
"ts" : Timestamp(1610730248, 1),
"t" : NumberLong(4)
},
"optimeDate" : ISODate("2021-01-15T17:04:08Z"),
"optimeDurableDate" : ISODate("2021-01-15T17:04:08Z"),
"lastHeartbeat" : ISODate("2021-01-15T17:04:18.403Z"),
"lastHeartbeatRecv" : ISODate("2021-01-15T17:04:17.661Z"),
"pingMs" : NumberLong(1),
"lastHeartbeatMessage" : "",
"syncingTo" : "192.168.1.130:27019",
"syncSourceHost" : "192.168.1.130:27019",
"syncSourceId" : 2,
"infoMessage" : "",
"configVersion" : 2
}
],
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1610730258, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1610730258, 1)
}
同理,我们也可以移除添加的实例节点:
test-replication:PRIMARY> rs.remove("192.168.1.130:27021");
//输出如下
{
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1610730771, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1610730771, 1)
}
这个时候我们再去检查复制集状态,可以看到27021端口的mongo已经被踢出了复制集。
需要注意的一点是,在我们做动态配置的时候,有可能会看到shell连接上提示一堆无法连接数据库等错误,这种是正常的,其原理是在我们修改动态配置的时候,复制集中的主节点会把所有的连接给强制关闭。然后将主节点退化成普通备份节点,用于方便接受配置,然后在接受完配置以后,会恢复为主节点的状态,一切即可照旧使用。
重新加载配置config/reconfig函数
在使用过程中,有时也会遇到需求,将一部分mongo的配置信息修改掉,假设我们这里获取到的复制集节点配置信息里的所有服务都是ip+端口号的方式,突然要我们修改为域名 + 端口的方式,那么这个时候我们最好的解决方式就是重新加载config:
//获取当前节点配置信息
rs.config();
//输出如下
{
"_id" : "test-replication",
"version" : 3,
"protocolVersion" : NumberLong(1),
"writeConcernMajorityJournalDefault" : true,
"members" : [
{
"_id" : 0,
"host" : "192.168.1.130:27020",
"arbiterOnly" : false,
"buildIndexes" : true,
"hidden" : false,
"priority" : 1,
"tags" : {
},
"slaveDelay" : NumberLong(0),
"votes" : 1
},
{
"_id" : 1,
"host" : "192.168.1.130:27018",
"arbiterOnly" : false,
"buildIndexes" : true,
"hidden" : false,
"priority" : 1,
"tags" : {
},
"slaveDelay" : NumberLong(0),
"votes" : 1
},
{
"_id" : 2,
"host" : "192.168.1.130:27019",
"arbiterOnly" : false,
"buildIndexes" : true,
"hidden" : false,
"priority" : 1,
"tags" : {
},
"slaveDelay" : NumberLong(0),
"votes" : 1
}
],
"settings" : {
"chainingAllowed" : true,
"heartbeatIntervalMillis" : 2000,
"heartbeatTimeoutSecs" : 10,
"electionTimeoutMillis" : 10000,
"catchUpTimeoutMillis" : -1,
"catchUpTakeoverDelayMillis" : 30000,
"getLastErrorModes" : {
},
"getLastErrorDefaults" : {
"w" : 1,
"wtimeout" : 0
},
"replicaSetId" : ObjectId("5ffde070640dd6d0d8040a36")
}
}
//我们来修改members中每一个host的信息
var config = rs.config();
config.members[0].host = "localhost:27020";
config.members[1].host = "localhost:27018";
config.members[2].host = "localhost:27019";
rs.reconfig(config);
即可完成重新配置的config,同理我们也可以通过此种方式添加或者删除一些新的节点
复制集必须知道的其他概念
在我们设计一个复制集的时候,有些概念我们必须知道,这些概念也会或多或少的影响我们最终设计复制集。
大多数节点可用
我们知道,mongo中复制集节点,在主节点出现故障或者无法响应以后,其他的副本集会进行选举,最终选出来一个新的主节点,但是我们要知道mongoDB的副本集是遵循一个大多数节点的概念的,即:
选择主节点时需要由大多数决定,主节点只有在得到大多数支持时才能继续作为主节点,写操作被复制到大多数成员时这个写操作就是安全的。
假设说这里我们有一个五个节点组成的复制集,但是主节点和其中两个备份节点因为故障导致宕机了,这个时候仅有两个备份节点可用,这种情况下,由于两个节点不满足大多数节点的原则,因此无法选出新的主节点,而是两个备份节点和三个不可用节点,整个复制集无法对外提供服务了。可能会提出质疑,这种设计会影响服务使用呀,为什么MongoDB会选择这种选举策略呢?我们想一下,三个不可用的节点服务,是真的宕机了吗?其实也未必,这种不可用的可能性有很多,比如网络故障,或者网络延迟等,都有可能导致这个问题的出现。而且我们可以想象一下,如果进行选举的话,大部分节点推举即可成为主节点,那么这种情况下,不可用的三个节点服务有可能互相能连接上,对于它们来说不可用的服务节点是另外两台,反过来也是一样,这个时候彼此之间都进行选举操作,如果没有大多数的限制的话,就有可能出现两边各自选举出了一个主节点,这样整个复制集就会出现多个主节点,如果是多个主节点我们在使用的时候就有可能出现,一个主节点刚刚写入,另一个主节点可以删除的现象,导致整个复制集的数据不一致的情况,也给开发以及数据同步带来了很大的困难,因此mongoDB选择了很多中间件都用的方案,主节点选举必须是大部分节点服务都同意才可以,防止出现多个主节点的情况。
仲裁节点
在mongoDB中,数据节点除了我们说的主节点服务和备份节点服务以外,还有一个角色--仲裁节点
,这个节点与其他两个完全不同。仲裁节点
既不会像备份节点一样,参与主节点的数据备份和数据同步,也不会和主节点一样,可以对外提供读写服务,实际上,仲裁节点
的作用只有一个,即参与投票选举新的主节点。
由于仲裁者并不需要履行传统mongod服务器的责任,所以可以将仲裁者作为轻量级进程,运行在配置比较差的服务器上,也可以在我们不需要多份备份的时候,节省服务器内存空间的时候使用,将部分服务节点变为仲裁节点。
仲裁节点的创建
我们想要给复制集中创建一个仲裁节点,有两种方式,第一种则是我们在启动复制集的时候,通过rs.initiate
函数将复制集节点信息传入作为复制集配置文件启动的方式,此种方式中,我们只需要在传入的config中指定需要设置为仲裁节点的服务信息,添加属性arbiterOnly:true
即可,例如:
rs.initiate({ _id:"test-replication", members:[{ _id:0, host:"192.168.1.130:27020" },{ _id:1, host:"192.168.1.130:27018" },{ _id:2, host:"192.168.1.130:27019",arbiterOnly:true}] })
这样我们启动的复制集节点的时候,27019端口的节点就会变成仲裁节点了,当然,如果我们复制集已经在运行过程中,这个时候我们需要添加一个仲裁节点的话,也可以使用rs.addArb
辅助函数添加一个仲裁节点,使用如下:
rs.addArb("192.168.1.130:27022");
当然此函数的作用与我们直接使用rs.add()
函数,在配置中指定arbiterOnly:true的效果是一样的
注意:仲裁节点会给我们开发带来一定的好处,比如当我们复制集中现有的节点是偶数的时候,可能这个时候选举无法选出主节点,或者防止出现两个节点选票一致的情况,这个时候我们可以添加一个仲裁节点上去,辅助完成选举,当然,在实际使用中最多仅允许出现一个仲裁节点,而且仲裁节点本身也不是一定就需要的,最好只在节点数在偶数的时候使用,如果是奇数情况下建议不要使用仲裁节点,因为假设当前是三个节点的复制集,如果加入了一个仲裁节点,那么就代表我们的复杂度提升了1/3,本来只要67%的可用就可以继续使用复制集,现在需要维持在75%可用。而且节点越多,选举的时间就会成指数的增长,因此mongoDB官方也有数量的上限,复制集最多不能超过五十个节点。
优先级与隐藏节点
如果说我们在初始化复制集之前,仅仅指定了多个mongo服务的信息,然后初始化整个复制集,接着我们去连接其中任意一个节点,这个时候如果是选举完了,可以看到当前的角色,正常情况下,我们会发现在没有其他配置的情况下,我们的主节点一般会是指定的复制集节点列表的第一个服务,当然这个和mongo的选举机制有关系,同时也和一个参数有关,即priority (优先级)
,在我们没有指定这个参数的时候,默认每个备份节点的优先级都是1,这也是为什么在都相同的情况下,不会出现其他节点成为主节点的原因。
这里需要知道一点,优先级的取值范围是 0-100,默认情况下的值为1,值越小理论上越不活跃,即被选为主节点的可能性也是越低,但是当我们把一个备份节点设置为0优先级的时候,就会导致这个节点永远是个备份节点,永远不可能被选为主节点,这种节点被称之为被动成员节点。因此我们也可以理解为,在数据等其他因素都一样的情况下,如果我们给某节点指定了更高的优先级,那么在复制集初始化以后(重新选举也是一样),会是优先级更大的先被选为主节点(只要超过大半的其他节点在选举中投票同意即可)。
指定优先级用法
我们在初始化复制集的时候可以显式指定优先级:
rs.initiate({ _id:"test-replication", members:[{ _id:0, host:"192.168.1.130:27020",priority : 100},{ _id:1, host:"192.168.1.130:27018" },{ _id:2, host:"192.168.1.130:27019" }] })
同样也可以使用rs.add
以及修改config等方式去指定优先级
隐藏节点
需要注意的是,客户端在访问复制集的时候,只会访问开放的主节点和备份节点,而如果节点是隐藏的,是不会被请求的,不管是读取还是写入请求都不会访问隐藏节点。而且绝大多数情况下,隐藏节点是不会被选举成为主节点的,但是自身是会参与投票选举的,将节点隐藏的首要条件是当前节点的优先级为0,并且不为主节点才可以进行隐藏
如果想要将某个节点设置为隐藏节点,我们可以将其自身的config添加 members[n].hidden=true
,当然在此之前我们也需要先将其的优先级设置为0,即members[n].priority=0
,如下:
var cfg = rs.conf();
cfg.members[0].priority = 0
cfg.members[0].hidden = true
rs.reconfig(cfg);
即可完成对1号节点的隐藏操作,这个时候我们再去使用rs.status()
或者db.isMaster()
函数查询,会发现第一个节点的信息已经查询不到了
延迟备份节点
在实际使用过程中,有时为了防止出现数据因为意外或者人为导致的损坏,有时候会选择定期备份的方式,但除了这种方式以外我们还可以给部分节点设置为延迟备份节点,只需要在配置中指定slaveDelay : <seconds>
即可,单位是秒,假设我们设置的是延迟3600s,当前时间是9.53分,那么被设置为延迟备份节点的服务,最新的数据则只有8.53之前的,修改方式如下:
var cfg = rs.conf();
cfg.members[0].priority = 0;
cfg.members[0].hidden = true;
cfg.members[0].slaveDelay = 3600;
rs.reconfig(cfg);
配置为延迟备份节点的服务优先级方面必须是0,由于此部分数据是延迟的,存在数据不准确的情况,一般不建议开放出去,通常也会配置为隐藏节点,防止被客户端请求访问到获取脏数据