MongoDB实战指南(七):MongoDB复制集之复制集工作机制
http://www.cnblogs.com/longshiyVip/p/5097336.html 概述了复制集,整体上对复制集有了个概念,但是复制集最重要的功能之——自动故障转移是怎么实现的呢?数据同步又是如何实现的?带着这两个问题,下面展开分析。
一. 数据同步
先利用mongo客户端登录到复制集的primary节点上。
>mongo --port 40000
查看实例上所有数据库
rs0:PRIMARY> show dbs
local 0.09375GB
可以看到只有一个local数据库,因为此时还没有在复制集上创建任何其它数据库,local数据库为复制集所有成员节点上默认创建的一个数据库。在primary节点上查看local数据上的集合:
rs0:PRIMARY> show collections
oplog.rs
slaves
startup_log
system.indexes
system.replset
如果是在secondary节点则local数据库上的集合与上面有点不同,secondary节点上没有slaves集合,因为这个集合保存的是需要从primary节点同步数据的secondary节点;secondary节点上会有一个me集合,保存了实例本身所在的服务器名称;secondary节点上还有一个minvalid集合,用于保存对数据库的最新操作的时间截。其它集合primary节点和secondary节点都有,其中startup_log集合表示的是mongod实例每一次的启动信息;system.indexes集合保存的是当前数据库(local)上的所有索引信息;system.replset集合保存的是复制集的成员配置信息,复制集上的命令rs.conf()实际上是从这个集合取的数据返回的。最后要介绍的集合是oplog.rs,这个可是重中之中。
mongoDB就是通过oplog.rs来实现复制集间数据同步的,为了分析数据的变化,先在复制集上的primary节点上创建一个数据库students,然后插入一条记录。
rs0:PRIMARY> use students switched to db students rs0:PRIMARY> db.scores.insert({"stuid":1,"subject":"math","score":99});
接着查看一下primary节点上oplog.rs集合的内容:
rs0:PRIMARY> use local switched to db local rs0:PRIMARY> db.oplog.rs.find();
返回记录中会多出一条下面这样的记录(里面还有几条记录是复制集初始化时创建的):
{ "ts" : { "t" : 1376838296, "i" : 1 }, "h" : NumberLong("6357586994520331181"), "v" : 2, "op" : "i", "ns" : "students.scores", "o" : { "_id" : ObjectId("5210e2 98d7b419b44afa58cc"), "stuid" : 1, "subject" : "math", "score" : 99 } }
里面有几个重要字段,其中"ts"表示是这条记录的时间截,"t"是秒数,"i"每秒操作的次数;字段"op"表示的是操作码,值为"i"表示的是insert操作;"ns"表示插入操作发生的命名空间,这里值为: "students.scores",由数据库和集合名构成;"o"表示的是此插入操作包含的文档对象;
当primary节点完成插入操作后,secondary节点为了保证数据的同步也会完成一些动作:
所有secondary节点检查自己的local数据上oplog.rs集合,找出最近的一条记录的时间截;接着它会查询primary节点上的oplog.rs集合,找出所有大于此时间截的记录;最后它将这些找到的记录插入到自己的oplog.rs集合中并执行这些记录所代表的操作;通过这三步策略,就能保证secondary节点上的数据与primary节点上的数据同步了。整个流程如下图所示:
复制集数据同步流程
查看一下secondary节点上的数据,证明上面的分析是正确的。
rs0:SECONDARY> show dbs local 0.09375GB students 0.0625GB
在secondary节点上新插入了一个数据库students。但是有一点要注意:现在还不能在secondary节点上直接查询students集合上的内容,默认情况下mongoDB的所有读写操作都是在primary节点上完成的,后面也会介绍通过设置从secondary节点上来读,这将引入一个新的主题,后面再分析。
关于oplog.rs集合还有一个很重要的方面,那就是它的大小是固定的,mongoDB这样设置也是有道理的,假如大小没限制,那么随着时间的推移,在数据库上的操作会逐渐累积,oplog.rs集合中保存的记录也会逐渐增多,这样会消耗大量的存储空间,同时对于某个时间点以前的操作记录,早已同步到secondary节点上,也没有必要一直保存这些记录,因此mongoDB将oplog.rs集合设置成一个capped类型的集合,实际上就是一个循环使用的缓冲区。
固定大小的oplog.rs会带来新的问题,考虑下面这种场景:假如一个secondary节点因为宕机,长时间不能恢复,而此时大量的写操作发生在primary节点上,当secondary节点恢复时,利用自己oplog.rs集合上最新的时间截去查找primary节点上的oplog.rs集合,会出现找不到任何记录。因为长时间不在线,primary节点上的oplog.rs集合中的记录早已全部刷新了一遍,这样就不得不手动重新同步数据了。因此oplog.rs的大小是很重要,在32位的系统上默认大小是50MB,在64位的机器上默认是5%的空闲磁盘空间大小,也可以在mongod启动命令中通过项—oplogSize设置其大小。
二. 故障转移
上面的介绍的数据同步相当于传统数据库中的备份策略,mongoDB在此基础还有自动故障转移的功能。在复制集概述那一节提到过心跳"lastHeartbeat"字段,mongoDB就是靠它来实现自动故障转移的。 mongod实例每隔2秒就向其它成员发送一个心跳包以及通过rs.staus()中返回的成员的”health”值来判断成员的状态。如果出现复制集中primary节点不可用了,那么复制集中所有secondary的节点就会触发一次选举操作,选出一个新的primary节点。如上所配置的复制集中如果primary节点宕机了,那么就会选举secondary节点成为primary节点,arbiter节点只是参与选举其它成员成为primary节点,自己永远不会成为primary节点。如果secondary节点有多个则会选择拥有最新时间截的oplog记录或较高权限的节点成为primary节点。oplog记录在前面复制集概述中已经描述过,关于复制集中节点权限配置的问题可在复制集启动的时候进行设置,也可以在启动后重新配置,这里先略过这一点,集中精力讨论故障转移。
如果是某个secondary节点失败了,只要复制集中还有其它secondary节点或arbiter节点存在,就不会发生重新选举primary节点的过程。
下面模拟两种失败场景:一是secondary节点的失败,然后过一段时间后重启(时间不能无限期,否则会导致oplog.rs集合严重滞后的问题,需要手动才能同步);二是primary节点失败,故障转移发生。
先分析第一种情况的测试,当前复制集的配置情况如下:
(1)
rs0:PRIMARY> rs.conf() { "_id" : "rs0", "version" : 3, "members" : [ { "_id" : 0, "host" : "Guo:40000" //primary节点 }, { "_id" : 1, "host" : "Guo:40001" //secondary节点 }, { "_id" : 2, "host" : "Guo:40002", //arbiter节点 "arbiterOnly" : true } ] }
(2)通过Kill掉secondary节点所在的mongod实例,模拟第一种故障情况,如下图所示:
模拟secondary节点故障
通过rs.status()命令查看复制集状态,secondary节点状态信息如下:
"_id" : 1, "name" : "Guo:40001", "health" : 0, "state" : 8, //表示成员已经down机 "stateStr" : "(not reachable/healthy)", "uptime" : 0, "optime" : { "t" : 1376838296, "i" : 1 }, "optimeDate" : ISODate("2013-08-18T15:04:56Z") (3)接着通过primary节点插入一条记录: rs0:PRIMARY> db.scores.insert({stuid:2,subject:"english",score:100}) (4)再次查看复制集状态信息rs.status(),可以看到primary成员节点上oplpog信息如下: "optime" : { "t" : 1376922730, "i" : 1 }, "optimeDate" : ISODate("2013-08-19T14:32:10Z"),
与上面down机的成员节点比较,optime已经不一样,primary节点上要新于down机的节点。
(5)重新启动Kill掉的节点
>mongod --config E:\mongodb-win32-i386-2.4.3\configs_rs0\rs0_1.conf
查询复制集状态信息rs.status(),观看节点"Guo:40001"的状态信息如下:
"_id" : 1, "name" : "GUO:40001", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 136, "optime" : { "t" : 1376922730, //与上面primary节点一致了 "i" : 1 }, "optimeDate" : ISODate("2013-08-19T14:32:10Z"),
说明secondary节点已经恢复,并且从primary节点同步到了最新的操作数据。进一步通过查询secondary节点上local数据库上的oplog.rs集合来进行验证,发现多了一条下面这样的记录:
{ "ts" : { "t" : 1376922730, "i" : 1 }, "h" : NumberLong("-451684574732211704"), "v" : 2, "op" : "i", "ns" : "students.scores", "o" : { "_id" : ObjectId("52122c 6a99c5a3ae472a6900"), "stuid" : 2, "subject" : "english", "score" : 100 } }
这正是在primary节点上插入的记录,再次证明数据确实同步过来了。
接下来测试第二种情况,假如primary节点故障,流程变化如下图所示:
模拟primary节点失败并恢复后
(1)将primary节点Kill掉。
查询复制集的状态信息rs.status()
"name" : "Guo:40000", "health" : 0, "state" : 8, "stateStr" : "(not reachable/healthy)"
字段"health"的值为0,说明原来的primary节点已经down机了。
"name" : "Guo:40001", "health" : 1, "state" : 1, "stateStr" : "PRIMARY"
字段"stateStr"值为"PRIMARY",说明原来secondary节点变成了primary节点。
(2)在新的primary节点上插入一条记录
rs0:PRIMARY> db.scores.insert({stuid:3,subject:"computer",score:99})
(3)重新恢复"Guo:40000"节点(原来的primary节点)
>mongod --config E:\mongodb-win32-i386-2.4.3\configs_rs0\rs0_0.conf
再次查看复制集状态rs.status()
"name" : "Guo:40000", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 33, "optime" : { "t" : 1376924110, "i" : 1 },
当"Guo:40000"实例被重新激活后,变成了secondary节点,oplog也被同步成最新的了。说明当primary节点故障时,复制集能自动转移故障,将其中一个secondary节点变为primary节点,读写操作继续在新的primary节点上进行。原来primary节点恢复后,在复制集中变成了secondary节点。
上面两中情况都得到了验证,但是有一点要注意,mongDB默认情况下只能在primary节点上进行读写操作,如下图所示:
默认的读写流程图
对于客户端应用程序来说,对复制集的读写操作是透明的,默认情况它总是在primary节点上进行。 mongoDB提供了很多种常见编程语言的驱动程序,驱动程序位于应用程序与mongod实例之间,应用程发起与复制集的连接,驱动程序自动选择primary节点。当primary节点失效,复制集发生故障转移时,复制集将先关闭与所有客户端的socket连接,驱动程序将返回一个异常,应用程序收到这个异常,这个时候需要应用程序开发人员去处理这些异常,同时驱动程序会尝试重新与primary节点建立连接(这个动作对应用程序来说是透明的)。假如这个时候正在发生一个读操作,在异常处理中你可以重新发起读数据命令,因为读操作不会改变数据库的数据;假如这个时候发生的是写操作,情况就变得微妙起来,如果是非安全模式下的写,就会产生不确定因素,写是否成功不确定,如果是安全模式,驱动程序会通过getlasterror命令知道哪些写操作成功了,哪些失败,驱动程序会返回失败的信息给应用程序,针对这个异常信息,应用程序可以决定怎样处置这个写操作,可以重新执行写操作,也可以直接给用户暴出这个错误。
三. 写关注
对于某些应用程序来说,写关注是重要的。它能判断哪些写操作成功写入了,哪些失败了,对于失败的操作,驱动程序能返回错误,由应用程序决定怎么处理。如果没有写关注,应用程序发送一个写操作到socket后,就不会管后面发送了什么情况,不知道是否成功写入数据库,这种情形对于日志类型的应用程序还是可以接受的,因为偶尔的写失败不会影响整个日志的监控情况;带有写关注的操作会等到数据库确认成功写入后才能返回,因此写关注会带来一点性能的损失。下面先分析复制集上写关注配置。
默认情况下复制集的写关注只针对primary节点,如下图所示:
默认写关注w:1
当应用程序发送一个写操作请求时,驱动程序会调用getLastError命令返回写操作的执行情况(这一动作对应用程序来说是透明的),getLastError命令会根据你配置的写关注选项来执行。写关注选项的配置是针对当前客户端与数据库的socket连接来说的,因此配置项需要通过应用程序传递给驱动程序。当然如果你没有传递任何选项参数给驱动程序,getLastError命令会根据你配置在复制集中默认配置local.system.replset.settings.getLastErrorDefaults来执行。getLastError命令的常用选项如下:
(1) 选项w
当取值为-1时,驱动程序不会使用写关注,忽略掉所有的网络或socket错误。
当取值为0时,驱动程序不会使用写关注,只返回网络和socket的错误。
当取值为1时,驱动程序使用写关注,但是只针对primary节点,这个配置项是复制集或单mongod实例的默认写关注配置。
当取值为整数且大于1时,写关注将针对复制集中n个节点,当客户端收到这些节点的反馈信息后,命令才返回给客户端继续执行。如下图所示:
写关注w:2的执行流程图
(2)选项wtimeout
指定写关注应在多长时间内返回,如果你没有指定这个值,复制集可能因为不确定因素导致应用程序的写操作一直阻塞。
下面通过一段代码对上面的描述做个回顾,在C#驱动程序下连接复制集并插入一条记录。
//实例化一个客户端的连接属性实例 MongoClientSettings clientSetting = new MongoClientSettings(); //设置属性准备Servers为要连接的复制集中的所有成员实例 List<MongoServerAddress> Servers = new List<MongoServerAddress>(); Servers.Add(new MongoServerAddress("Guo",40000)); Servers.Add(new MongoServerAddress("Guo",40001)); Servers.Add(new MongoServerAddress("Guo",40002)); clientSetting.Servers = Servers; clientSetting.ReplicaSetName = "rs0"; //设置属性复制集的名称 MongoClient client = new MongoClient(clientSetting);//根据设置的属性,实例化客户端 //得到一个与复制集连接的实例 MongoServer server = client.GetServer(); //获得一个与具体数据库连接对象,数据库名为students MongoDatabase mydb = server.GetDatabase("students"); //获得数据库中的表对象,即scores表 MongoCollection mydbTable = mydb.GetCollection("scores"); //准备一条数据,即声明一个文档对象 BsonDocument doc = new BsonDocument { {"stuid",5}, {"subject","sports"}, {"score",99} }; //将文档插入到数据库中 mydbTable.Insert(doc);
上面代码向复制集中插入一条数据,但是客户端的配置属性都是默认的,写关注w选项值为1,可以在C#的驱动程序中通过MongoClientSettings这个类来设置客户端的连接属性,包括写关注等;上面的代码也没有具体指定连接到哪个节点,但驱动程序会默认的选择primary节点;当primary节点宕机时,复制集重新选择出新的primary节点,驱动程序尝试重新连接新的primary节点并完成插入,这个动作对应用程序透明的。
四. 读参考
读参考是指MongoDB将客户端的读请求路由到复制集中指定的成员上,默认情况下读操作的请求被路由到复制集中的primary节点上,如下图所示:
从Primary节点上进行读取能够保证读到的数据是最新的,但是将读操作路由到其他secondary节点上去后,由于从primary节点同步数据到secondary节点会产生时间差,可能导致从secondary节点上读到的数据不是最新的。当然这对于实时性要求不是很高的绝大部分应用程序来说,并不是大问题。
关于读参考还有一点需要注意,因为每一个secondary节点都会从priary节点同步数据,所有secondary节点一般有相同的写操作流量,同时primary节点上的读操作流量也并没有减少,所以读参考并不能提高系统读写的容量。它最大的好处是能够使客户端的读请求路由到最佳的secondary节点上(如最近的节点),提高客户端的读效率,MongoDB驱动支持的读参考模式如下。
1. primary模式
这是默认的读操作模式,所有的读请求都路由到复制集中的primary节点上,如果primary节点故障了,读操作将会产生一个错误或者抛出一个异常。
2. primaryPrefered模式
在大多数模式下,读操作从primary节点上进行,如果primary节点故障无法读取,读操作将会路由到secondary节点上。
3. secondary模式
读操作只能从secondary节点上进行,如果没有可用的secondary节点,读操作将会产生错误或抛出异常。
4. secondaryPrefered模式
在大多数模式下,读操作从secondary节点上进行,但当复制集中只有一个primary时,读操作将用这个复制集的primary节点。
5. neares模式
读操作从最近的节点上进行,有可能是primary节点,也有可能是secondary节点,并不会考虑节点的类型。
五. 总结
当MongoDB向复制集中的primary节点写数据时,也会将写操作日志oplog写到primary节点所在的local数据库中,因此对复制集的写操作会产生两个锁,一个是集合数据所在的数据库上的锁,还有一个是local数据库上的写锁。
复制集中的secondary节点并不是实时地同步oplog日志,将数据的变化反应到secondary节点上,而是采取周期延迟批量写入的方式,secondary节点当应用写操作变化时,会锁在数据库,不允许读操作发生。