一、副本集的复制原理
Oplog
MongoDB的复制功能是通过操作日志oplog实现的,操作日志包含了主节点的每一次写操作。oplog是主节点的local数据库中的一个固定集合。备份节点通过查询主节点的这个集合就可以知道需要进行复制的写操作。
每个备份节点都维护着自己的oplog,记录着每一次从主节点复制数据的操作。这样,每个成员都可以作为同步源提供给其他成员使用(也就是说一个从节点不仅可以通过主节点的oplog同步数据,也能通过其他从节点的oplog同步数据,主节点和从节点都可以成为同步源),默认是以主节点作为同步源。
备份节点从当前使用的同步源中获取需要执行的操作,然后在自己的数据集上执行这些操作,最后再将这些操作写入自己的oplog(先执行操作,再写入自己的oplog)。如果遇到某个操作失败的情况(只有当同步源的数据损坏或者数据与主节点不一致时才可能发生),那么备份节点就会停止从当前的同步源复制数据。
如上图所示,上面的3个链表(循环链表)就是oplog日志,里面的每一个元素就是一次写操作。
oplog中按顺序保存着所有执行过的写操作。每个成员都维护着一份自己的oplog,每个成员的oplog都应该跟主节点的oplog完全一致(尽管可能会有延迟)。
节点1以主节点作为同步源进行同步,他只会复制主节点的oplog的id号大于节点2的id号的操作。节点2则以节点1为同步源。
由于oplog大小是固定的,它只能保存特定数量的操作日志。但从MongoDB 4.0开始,oplog还是固定集合但可以其内容超过其配置的大小限制。
通常oplog使用空间的增长速度与系统处理写请求的速率近乎相同:如果主节点上每分钟处理了1 KB的写入请求,那么oplog很可能也会在一分钟内写入1 KB条操作日志。
但是有一些例外情况:如果单次请求能够影响到多个文档(比如删除多个文档或者是多文档更新),oplog中就会出现多条操作日志,每个受影响的文档都会对应oplog中的一条日志。因此,如果执行db.coll.remove()删除了1 000 000个文档,那么oplog中就会有1 000 000条操作日志,每条日志对应一个被删除的文档。如果执行大量的批量操作,oplog很快就会被填满。
同步流程
1. 初始 化同步
首先一个成员会选择其他的一个成员作为同步源,在local.me中为自己创建一个标识符,删除所有已存在的数据库,以一个全新的状态开始进行同步。在这个过程中,所有现有的数据都会被删除。
然后是克隆,将同步源的除local数据库的所有库的所有记录全部复制到本地(全量复制)。这通常是整个过程中最耗时的部分。克隆过程中的所有操作都会被记录到oplog中。
克隆完成后,会开始在从节点建立索引。
在全量复制和建立索引的过程中(由于这个过程会很久),同步源如果执行了新的操作,新成员是无法在这个过程中同步这些新操作的,只能在新成员完成了全量复制和建立索引之后才能同步这些新操作。这会导致新成员的数据同步速度赶不上同步源的变化速度,产生数据不一致问题。这个问题被称为脱节。而且在全量复制的过程中,读写请求会变慢,所以为了避免脱节和初始化复制带来的性能下降的影响,尽可能在夜深人静的时候完成这一步(在夜深人静的时候添加新节点)。
PS:在3.4版本以后,初始化同步会获取在数据复制期间新增的oplog记录,因此即使在初始化同步过程中同步源执行了新的操作,这些操作也能被同步,不过要确保oplog足够大。
当完成全初始化同步之后,备份节点就会开始同步同步源的oplog日志(相当于增量复制)。
2. 复制
从节点成员在初始化同步之后会不断地复制数据。从节点成员从同步源复制oplog ,并以异步的方式应用这些操作。从节点可以根据ping时间和其他成员复制状态的变化,按需来自动调整它们的同步源,但是从节点应避免从延迟成员和隐藏成员中同步数据。
如果一个从节点成员的参数members[n].buildIndexes 设置为true,它只能从其他参数buildIndexes设置为true的成员同步数据。参数buildIndexes设置为false的成员可以从任何其他节点同步数据。参数buildIndexes默认为true。
3. 陈旧的从节点
如果备份节点远远落后于同步源当前的操作(同步源的oplog已经优先了备份节点整整一轮以上,即同步源oplog与备份节点oplog已经没有任何重合的情况),那么这个备份节点就是陈旧的。
导致备份节点陈旧的原因:
由于网络原因导致备份节点较长时间内无法同步同步源。或者备份节点由于故障较长时间的停机。
由于读请求过于频繁导致同步不及时。
当一个备份节点陈旧之后,它会查看副本集中的其他成员的oplog,如果其他成员的oplog足够详细可以补足该备份节点拉(la)下的操作,就会从这个成员进行同步。如果任何一个成员的oplog都没有参考价值,那么这个备份节点的复制操作就会中止,需要重新进行完全同步(这样会消耗大量时间和额外的系统性能)。
为了避免陈旧备份节点的出现和重新完全同步,可以让主节点使用比较大的oplog固定集合保存足够多的操作日志。
4. 心跳
每个成员都需要知道其他成员的状态:哪个是主节点?哪个可以作为同步源?哪个挂掉了?为了能和其他成员通信,每个成员每隔2秒钟就会向其他成员发送一个心跳请求。心跳请求的信息量非常小,用于检查每个成员的状态。
发送的心跳请求会将自身的状态告诉其他成员,会有以下几种状态:
STARTUP
成员刚启动时处于这个状态。
STARTUP2
整个初始化同步过程都处于这个状态。在这个状态下,MongoDB会创建几个线程,用于处理复制和选举,然后就会切换到RECOVERING状态。
RECOVERING
这个状态表明成员运转正常,但是暂时还不能处理读取请求。脱节时,也会进入RECOVERING
状态。
ARBITER
仲裁者应该始终处于ARBITER状态。
DOWN
如果一个正常运行的成员变得不可达,它就处于DOWN状态(可能是由于故障停机或网络原因无法与其他成员通信)。
UNKNOWN
同DOWN
REMOVED
当成员被移出副本集时,它就处于这个状态。如果被移出的成员又被重新添加到副本集中,它就会回到“正常”状态。
ROLLBACK
成员正在进行数据回滚
FATAL
如果一个成员发生了不可挽回的错误,也不再尝试恢复正常的话,它就处于FATAL状态。
这时,通常应该重启服务器,进行重新同步。
5. 选举
当一个成员无法到达主节点时(如主节点故障或网络原因导致成员无法接收到主节点的心跳),它就会申请被选举为主节点。
希望被选举为主节点的成员,会向它能到达的所有成员发送通知。如果这个成员不符合候选人要求,如这个成员的数据落后于副本集,或者是已经有一个运行中的主节点。在这些情况下,其他成员不会允许其进行选举。
另外,副本集被重新配置也会导致主节点退位。
假如网络状况良好,“大多数”服务器也都在正常运行,那么选举过程是很快的。如果主节点不可用,2秒钟(之前讲过,心跳的间隔是2秒)之内就会有成员发现这个问题,然后会立即开始选举,整个选举过程只会花费几毫秒。
但是,实际情况可能不会这么理想:网络问题,或者是服务器过载导致响应缓慢,都可能触发选举。在这种情况下,心跳会在最多20秒之后超时。如果选举打成平局,每个成员都需要等待30秒才能开始下一次选举。所以,如果有太多错误发生的话,选举可能会花费几分钟的时间。
6. 回滚
如果主节点执行了一个写请求之后挂了,但是备份节点还没来得及复制这次操作,然后又选举出来了一个新的主节点,那么新选举出来的主节点就会漏掉这次写操作。
如下所示, A和B处于一个数据中心,CDE处于另一个数据中心,两个数据中心可能处于两个不同的网络环境:
A是主节点,在执行完126操作之后down掉,B同步成功了126,但CDE没有同步到126。
之后,由于DC1这个数据中心只有1个B节点而且由于网络原因B暂时无法和CDE通信,所以B不满足“大多数”的选举条件而无法晋升为主节点(假如B能与CDE通信,那么B会由于拥有最新数据而晋升主节点),于是C晋升为主节点,这个新的主节点会继续处理后续的写入操作。
网络恢复之后,左边数据中心的服务器就会C开始同步126之后的操作,但是无法找到这个操作。这种情况发生的时候,A和B会进入回滚(rollback)过程。回滚会将失败之前的126操作撤消,然后定位到AB与C共有的125操作,从125操作继续同步。
这些被回滚的数据并不会丢失,而是会被写入到一个.bson的文件并存到数据目录(/data/db)下的rollback目录中。如果回滚的是一个更新操作(例如126操作是更新了集合col1的id=10005的文档),那么更新后id为10005的文档写入到rollback目录的col1.bson中。然后从C复制id=10005的文档到A和B,然后继续同步C后面的126,127,128...的操作。
回滚过程中,A和B会进入RECOVERING状态,此时AB无法接受读写请求。可以将回滚的操作重新应用到当前主节点C,具体操作可以查阅资料。
如果要回滚的内容太多,MongoDB可能承受不了。如果要回滚的数据量大于300 MB,或者要回滚30分钟以上的操作,回滚就会失败。对于回滚失败的节点,必须要重新同步(全量复制)。
这种情况最常见的原因是原备份节点C远远落后于原主节点A,而这时主节点A却挂了。然后C点成为主节点,这个主节点与旧的主节点相比,缺少很多操作。
为了保证成员A不会在回滚中失败,最好的方式是保持备份节点CDE的数据尽可能最新。
二、客户端连接副本集
连接副本集与连接单台服务器非常像。在驱动程序中使用与MongoClient等价的对象,并且提供一个希望连接到的副本集种子列表。种子是副本集成员。并不需要将所有成员都列出来(虽然可以这么做):驱动程序连接到某个种子服务器之后,就能够得到其他成员的地址。一个常用的连接字符串如下所示:
"mongodb://server-1:27017,server-2:27017"
具体可以查看相关的驱动程序文档。
当主节点挂掉之后,驱动程序会尽快自动找到新的主节点。如果没有可达的主节点,应用程序就无法执行写操作。
通常,驱动程序没有办法判断某次操作是否在服务器崩溃之前成功处理,但是用户的应用程序可以自己实现相应的解决方案。比如,如果驱动程序发出插入{"_id" : 1}文档的请求之后收到主节点崩溃的错误,连接到新的主节点之后,可以查询主节点中是否有{"_id" : 1}这个文档然后决定是否重新插入。
将读请求发送到备份节点
默认情况下,驱动程序会将所有的读写请求都路由到主节点。但很有时候我们认为可以将读请求路由到从节点来减轻主节点处理高并发请求的压力(但这种情况下作者更推荐使用分片代替复制来解决服务器分担负载的问题)。
其实作者并不推荐将读请求路由到从节点,但实际上在某些特殊情况下我们还是会这么做。
如何设置从节点能够接收读请求呢?对于mongo客户端,我们设置 rs.secondaryOk() 即可。对于其他的应用程序客户端,如python、php、Nodejs和Java等,我们需要具体查看他们mongodb扩展和api的文档。
1. 出于一致性考虑(不推荐将读请求路由到从节点)
对一致性要求非常高的应用程序不应该从备份节点读取数据。
备份节点通常会落后主节点几毫秒,但是不能保证一定是这样。有时,由于加载问题、配置错误、网络故障等原因,备份节点可能会落后于主节点几分钟、几个小时甚至几天。
如果应用程序需要读取它自己的写操作(例如,先插入一个文档,然后再查询它),那么不应该将读请求发送给备份节点。否则的话,可能会出现应用程序成功执行了一次写操作,却读不到这个值的情况。要知道客户端发送请求的速度可能会比备份节点复制操作的速度要快。
为了能够始终将读请求发送给主节点,需要将读选项设置为Primary(或者不管它,默认就是Primary)。
2. 出于负载的考虑(不推荐将读请求路由到从节点)
许多用户会将读请求发送给备份节点,以便实现负载均衡。这种情况下,设置的从节点数应该在负载极限之上更多一些。例如你的服务器每秒只能处理10 000次查询,而你需要进行30 000次/秒的查询,可能就需要设置4个备份节点,并且让它们分担一些数据加载的工作。此时每个备份节点的负载是75%。
后来,某一个备份节点崩溃了。
现在剩余的每个成员的负载都是100%。如果需要恢复刚刚崩溃的成员,它就需要从其他成员处复制数据(全量复制),这就会导致其他成员过载。服务器过载经常导致性能变慢,副本集性能进一步降低,然后强制其他成员承担更多的负载,导致这些成员变得更慢,这是一个恶性死循环。
此时应该使用5台服务器,而不是4台,这样如果一台服务器崩溃,并不会导致副本集过载。但是,即使你的计划非常完美,仍然需要处理其他服务器负载过大的情况。
一个更好的选择是,使用分片作分布式负载。
3. 故障转移时保证查询可用(推荐将读请求路由到从节点)
在某些情况下,将读请求发送给备份节点是合理的。例如,你可能希望应用程序在主节点挂掉时(而新的主节点被选举出来前)仍然能够执行读操作(而且你并不在意读到的数据是否是最新的)。这是最常见的将读请求发给送备份节点的原因:失去主节的时,应用程序进入只读状态。这种读选项叫做主节点优先(primary preferred)。
primary preferred在主节点正常是会将所有读请求路由到主节点,在主机点down掉时将读请求路由到从节点。
primary是无论主节点是否正常都将读请求路由到主节点。
从备份节点读取数据有一个常见的参数是获得低延迟的数据。可以将读选项设置为Nearest,以便将请求路由到延迟最低的成员(根据驱动程序到副本集成员的ping时间最短的)。
如果应用程序能够接受任何陈旧程序的数据,那就可以使用Secondary或者Secondary preferred读选项。
Secondary始终会将读请求发送给备份节点。如果没有可用的备份节点,请求就会出错,而不是重新将读请求发送给主节点。对于不在乎数据新旧程度并且希望主节点只处理写请求的应用程序来说(读写分离),这是一种可行的方式。如果对于数据新旧程度有要求,不建议使用这种方式。
Secondary preferred会优先将读请求路由到可用的备份节点。如果备份节点都不可用,请求就会被发送到主节点。
有时,读负载与写负载完全不同:读到的数据与写入的数据是完全不同的。为了做离线处理,你可能希望创建很多索引,但是又不想将这些索引创建在主节点上。在这种情况下,可以设置一个与主节点拥有不同索引的备份节点。如果希望以这种方式使用备份节点,最好是使用驱动程序创建一个直接连接到目标备份节点的连接,而不是连接到副本集。
应该根据应用程序的实际需要选择合适的选项。
可以将多个选项组合在一起使用:如果某些读请求必须从主节点读取数据,那就对这些请求使用Primary选项。如果另一些读请求并不要求数据是最新的,那么可以对这些读请求使用Primary preferred选项。如果某些请求对低迟延的要求大过一致性要求,那么可以使用Nearest选项。
上面这些 Primary/Primary preferred/Nearest/Secondary/Secondary preferred 参数也可以在启动mongod服务时通过参数initialSyncSourceReadPreference参数设置(版本4.2.7中的新参数),不过该参数是用来指定选择同步源的策略的。
三、管理副本集
以单机模式启动成员
许多维护工作不能在备份节点上进行(因为要执行写操作),也不能在主节点上进行。此时我们需要将备份节点从集群中脱离出来变成单机模式后再进行维护,这需要重启备份节点所在机器的mongod服务。在以单机模式重启服务器之前,先看一下服务器的命令行参数。
db.serverCmdLineOpts(){
"argv": ["mongod", "-f", "/var/lib/mongod.conf"],
"parsed": {
"replSet": "mySet",
"port": "27017",
"dbpath": "/var/lib/db"
},
"ok": 1
}
重启的时候,一个是不使用replSet选项,二个是监听不同的端口,三个是保持dbpath
的值不变,这样副本集的其他成员就找不到它了。
当在这台服务器上执行完维护工作之后,可以以最原始的参数重新启动它。启动之后,它会自动与副本集中的其他成员进行同步,将维护期间落下的操作全部复制过来。
如果我们通过rs.remove()将需要维护的备份节点移除再进行维护的话,那么在维护完后执行rs.add()将节点添加会集群时会进行重新初始化同步,这样会造成较大的性能消耗和时间浪费。所以建议以单机模式启动需要维护的成员。
副本集配置
副本集配置总是以一个文档的形式保存在local.system.replSet集合中。副本集中所有成员的这个文档都是相同的。绝对不要使用update更新这个文档,应该使用rs辅助函数或者replSetReconfig命令修改副本集配置。
添加新成员
rs.add("spock:27017")
修改副本集成员的配置。修改副本集成员配置时,有几个限制需要注意:
不能修改成员的"_id"字段;
不能将接收rs.reconfig命令的成员(通常是主节点)的优先级设为0;
不能将仲裁者成员变为非仲裁者成员,反之亦然;
不能将"buildIndexes" : false的成员修改为"buildIndexes" : true;
需要注意的是,可以修改成员的"host"字段
1. 创建比较大的副本集
副本集最多只能拥有50个成员,其中只有7个成员拥有投票权。这是为了减少心跳请求的网络流量(每个成员都要向其他所有成员发送心跳请求)和选举花费的时间。
如果要创建7个以上成员的副本集,只有7个成员可以拥有投票权,需要将其他成员的投票数量设置为0(在添加节点时设置)
rs.add({"_id" : 7, "host" : "server-7:27017", "votes" : 0})
这样可以阻止这些成员在选举中投主动票,虽然它们仍然可以投否决票。
2. 修改成员状态
把主节点变为备份节点
可以使用stepDown
函数将主节点降级为备份节点:
rs.stepDown()
这个命令可以让主节点退化为备份节点,并维持60秒。如果这段时间内没有新的主节点被选举出来,这个节点就可以要求重新进行选举。如果希望主节点退化为备份节点并持续更长(或者更短)的时间,可以自己指定时间(以秒为单位):
rs.stepDown(600) // 10分钟
这里设置为10分钟不是说10分钟后它会重新变回主节点,而是说如果10分钟内没有节点被选举为主节点他才会重新进行选举。
注意,无法强制将某个成员变成主节点,除非对副本集做适当的配置(例如提高某台节点的优先级然后让主节点stepDown变为从节点)。
3. 阻止选举
如果需要对主节点做一些维护,但是不希望这段时间内将其他成员选举为主节点,那么可以在每个备份节点上执行freeze命令,以强制它们始终处于备份节点状态:
> rs.freeze(10000)
这个命令也会接受一个以秒表示的时间,表示在多长时间内保持备份节点状态。
维护完成之后,如果想“释放”其他成员,可以再次执行freeze命令,将时间指定为0即可:
> rs.freeze(0)
这样,其他成员就可以在必要时申请被选举为主节点。
也可以在主节点上执行rs.freeze(0),这样可以将退位的主节点重新变为主节点。
4. 使用维护模式
当在副本集成员上执行某个非常耗时的操作时,这个成员就人进入维护模式(maintenance mode):强制成员进入RECOVERING状态。有时,成员会自动进入维护模式,比如在成员上做压缩时。不会有读请求发送给进入了维护模式的成员,这个成员也不能再作为复制源。
也可以通过执行replSetMaintenanceMode命令强制一个成员进入维护模式。例如,下面这个脚本会自动检测成员是否落后于主节点30秒以上,如果是,就强制将这个成员转入维护模式:
function maybeMaintenanceMode() {
var local = db.getSisterDB("local");
// 如果成员不是备份节点(它可能是主节点或者已经处于维护状态),就直接返回
if (!local.isMaster().secondary) {
return;
}
// 查找这个成员最后一次操作的时间
var last = local.oplog.rs.find().sort({"$natural" : -1}).next();
var lastTime = last['ts']['t']; // 如果落后主节点30秒以上
if (lastTime < (new Date()).getTime()-30) {
db.adminCommand({"replSetMaintenanceMode" : true});
}
};
将成员从维护模式中恢复,可以使用如下命令:
> db.adminCommand({"replSetMaintenanceMode" : false});
5. 修改节点的复制源
MongoDB根据ping时间选择同步源。一个成员向另一个成员发送心跳请求,就可以知道心跳请求所耗费的时间。但是可能会出现这种情况,B会以A(主节点)作为复制源,C以B为复制源,形成一个A<-B<-C<-D...的复制链条。通常不太可能发生这样的情况,但是并非完全不可能。但这种情况通常是不可取的:复制链中的每个备份节点都要比它前面的备份节点稍微落后一点,复制链条的最后一个节点就会比主节点落后很多。
只要出现这种状况,可以用replSetSyncFrom(或者是它对应的辅助函数rs.syncFrom())命令修改成员的复制源进行修复。
连接到需要修改复制源的备份节点,运行这个命令,为其指定一个复制源:
> secondary.adminCommand({"replSetSyncFrom" : "server0:27017"})
可能要花费几秒钟的时间才能切换到新的复制源。
我们可以通过查看rs.status()的"syncingTo"字段查看一个节点的同步源。
当用replSetSyncFrom为成员指定一个并不比它领先的成员作为复制源时,系统会给出警告,但是仍然允许这么做。
6. 复制循环
A从B处同步数据,B从C处同步数据,C从A处同步数据,这就是一个复制循环。如果每个成员都是自动选取复制源,那么复制循环是不可能发生的。但是,使用replSetSyncFrom 强制为成员设置复制源时,就可能会出现复制循环。
复制循环带来的恶劣影响是复制循环中的成员(首先他们不可能是主节点,因为主节点不需要同步源)没有以主节点为同步源,无法复制新的写操作,数据会越来越落后。
可以禁用复制链,强制要求每个成员都从主节点进行复制,只需要将"allowChaining"
设置为false即可(如果不指定这个选项,默认是true)
> var config = rs.config()
> config.settings = config.settings || {} // 如果设置子对象不存在,就自动创建一个空的
> config.settings.allowChaining = false
> rs.reconfig(config)
将allowChaining设置为false之后,所有成员都会从主节点复制数据。如果主节点变得不可用,那么各个成员就会从其他备份节点处复制数据。
7. 计算延迟
延迟(lag)是指备份节点相对于主节点的落后程度,具体是主节点最后一次操作的时间戳与备份节点最后一次操作的时间戳的差。
可以使用rs.status()查看成员的复制状态,也可以通过在主节点上执行db.printReplicationInfo()(这个命令的输出信息中包括oplog相关信息),或者在备份节点上执行db.printSlaveReplicationInfo()快速得到一份摘要信息。
在备份节点上运行db.printSlaveReplicationInfo(),可以得到当前成员的复制源,以及当前成员相对复制源的落后程度等信息
db.printSlaveReplicationInfo();
source: server-0:27017
syncedTo: Tue Mar 30 2012 16:44:01 GMT-0400 (EDT)
= 12secs ago (0hrs)
这样就可以知道当前成员正在从哪个成员处复制数据。在这个例子中,备份节点比主节点落后12秒。
注意,在一个写操作非常少的系统中,有可能会造成延迟过大的幻觉。假设一小时执行一次写操作。主节点A执行了这次操作后,可能要50毫秒才能同步到从节点B,但是如果我们刚好在这50毫秒内查看延迟,就会发现延迟是1个小时。此时我们只需要多执行几次查看延迟就可以避免这个问题。
8. 调整oplog大小
可以将主节点的oplog长度看作维护工作的时间窗。如果主节点的oplog长度(我们一般以时间来描述oplog的长度)是一小时(也就是说一个小时的写操作就会将oplog覆盖掉1次),那么你就只有一小时的时间可以用于修复各种错误,不然的话备份节点可能会落后于主节点太多,导致不得不重新进行完全同步。在oplog被填满之前很难知道它的长度。
PS:我们可以通过db.printSlaveReplicationInfo()查看oplog的长度:
db.printReplicationInfo();
configured oplog size: 10.48576MB
log length start to end: 34secs (0.01hrs)
oplog first event time: Tue Mar 30 2010 16:42:57 GMT-0400 (EDT)
oplog last event time: Tue Mar 30 2010 16:43:31 GMT-0400 (EDT)
now: Tue Mar 30 2010 16:43:37 GMT-0400 (EDT)
其中log length start to end(即oplog的第一个文档和最后一个文档的生成时间差)就是oplog的长度。
没有办法在服务器运行期间调整oplog大小。但是,可以依次将每台服务器下线,调整它的oplog,然后重新把它添加到副本集中。记住,每一个可能成为主节点的服务器都应该拥有足够大的oplog,以预留足够的时间窗用于进行维护。
如果要增加oplog大小,可以按照如下步骤
先将当前服务器以单机模式启动(请参考之前的以单机模式启动的步骤)
> use local
> // op:"i"表示操作为Insert的文档
> var cursor = db.oplog.rs.find({"op" : "i"})
> var lastInsert = cursor.sort({"$natural" : -1}).limit(1).next() // $natural:-1表示按插入时间倒序排序。这里是获取最后一条插入操作。
> db.tempLastOp.save(lastInsert) // 将最后一条插入的同步操作存到tempLastOp集合中
> // 确保保存成功,这非常重要!
> db.tempLastOp.findOne()
//删除当前的oplog:
> db.oplog.rs.drop()
//创建一个新的oplog:
> db.createCollection("oplog.rs", {"capped" : true, "size" : 10000})
// 将最后一条操作记录写回oplog:
> var temp = db.tempLastOp.findOne()
> db.oplog.rs.insert(temp)
>> // 要确保插入成功
> db.oplog.rs.findOne()
最后,将当前服务器作为副本集成员重新启动。注意,由于这时它的oplog只有一条记录,所以在一段时间内无法知道该从节点的oplog的真实长度。另外,这个服务器现在也并不适合作为其他成员的复制源。
PS:重新调整oplog大小时需要先删除之前的oplog再创建新的oplog,因为我们无法直接改变现有oplog集合的大小。为什么要将最后一条oplog操作保留到新的oplog呢?前面我们说过,同步的时候是以从节点oplog的大于等于最后一条数据的id查询同步源的oplog日志来进行同步的。如果新的oplog没有这个最后一条同步操作的文档记录,就会重新进行一次完整同步。
通常不应该减小oplog的大小:即使oplog可能会有几个月那么长,但是通常总是有足够的硬盘空间来保存oplog,oplog并不会占用任何珍贵的资源(比如CPU或RAM)。
9. 从延迟备份节点中恢复
假设有人不小心删除了一个数据库,幸好你有一个延迟备份节点。现在,需要放弃其他成员的数据,明确将延迟备份节点指定为数据源。有几种方法可以使用。
下面介绍最简单的方法。
关闭所有其他成员。删除其他成员数据目录中的所有数据。确保每个成员(除了延迟备份节点)的数据目录都是空的。重启所有成员,然后它们会自动从延迟备份节点中复制数据。
这种方式非常简单,但是,在其他成员完成初始化同步之前,副本集中将只有一个成员可用(延迟备份节点)而且这个成员很可能会过载。
根据数据量的不同,第二种方式可能更好,也可能更差。
关闭所有成员,包括延迟备份节点。删除其他成员(除了延迟备份节点)的数据目录。将延迟备份节点的数据文件复制到其他服务器。重启所有成员。
注意,这样会导致所有服务器都与延迟备份节点拥有同样大小的oplog,这可能不是你想要的。
10. 创建索引
如果向主节点发送创建索引的命令,主节点会正常创建索引,然后备份节点在复制“创建索引”操作时也会创建索引。但是创建索引是一个需要消耗大量资源的操作,可能会导致成员不可用。如果所有备份节点都在同一时间开始创建索引,那么几乎所有成员都会不可用,一直到索引创建完成。
因此,可能你会希望每次只在一个成员上创建索引,以降低对应用程序的影响。如果要这么做,有下面几个步骤。
关闭一个备份节点服务器。
将这个服务器以单机模式启动。
在单机模式下创建索引。
索引创建完成之后,将服务器作为副本集成员重新启动。
对副本集中的每个备份节点重复上面的操作。
现在副本集的每个成员(除了主节点)都已经成功创建了索引。现在你有两个选择,应该根据自己的实际情况选择一个对生产系统影响最小的方式。
在主节点上创建索引。如果系统会有一段负载比较小的“空闲期”,那会是非常好的创建索引的时机。也可以修改读取首选项,在主节点创建索引期间,将读操作发送到备份节点上。
主节点创建索引时,备份节点仍然会复制这个操作,但是由于备份节点中已经有了同样的索引,实际上不会再次创建索引。
如果要创建唯一索引,需要先确保主节点中没有被插入重复的数据,或者应该首先为主节点创建唯一索引。否则,可能会有重复数据插入主节点,这会导致备份节点复制时出错,如果遇到这样的错误,备份节点会将自己关闭。你不得不以单机模式启动这台服务器,删除唯一索引,然后重新将其加入副本集。
在预算有限的情况下进行复制
如果预算有限,不能使用多台高性能服务器,可以考虑将备份节点只用于灾难恢复,这样的备份节点不需要太大的RAM和太好的CPU,也不需要太高的磁盘IO。这样,始终将高性能服务器作为主节点,比较便宜的服务器只用于备份,不处理任何客户端请求(将客户端配置为将全部读请求发送到主节点)。对于这样的备份节点,应该设置这些选项。
"priority" : 0
优先级为0的备份节点永远不会成为主节点。
"hidden" : true
将备份节点设为隐藏,客户端就无法将读请求发送给它了。
"buildIndexes" : false
这个选项是可选的,如果在备份节点上创建索引的话,会极大地降低备份节点的性能。如果不在备份节点上创建索引,那么从备份节点中恢复数据之后,需要重新创建索引。
"votes" : 0
在只有两台服务器的情况下,如果将备份节点的投票数设为0,那么当备份节点挂掉之后,主节点仍然会一直是主节点,不会因为达不到“大多数”的要求而退位。如果还有第三台服务器(即使它是你的应用服务器),那么应该在第三台服务器上运行一个仲裁者成员,而不是将第三台服务器的投票数量设为0。
更多关于mongdb复制和副本集的内容可以查看mongdb中文文档
https://docs.mongoing.com/replication
如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息
张柏沛IT技术博客 > 深入学习mongodb(六) mongodb副本集的复制原理、同步流程和选举机制(下)