MongoDB权威指南 第九章 复制 MongoDB的复制功能很重要,尤其是现在的存储引擎还不支持单击持久性。不仅可以用复制来应对故障切换,数据集成,还可以做读扩展,热备份或作为离线批处理的数据源。 9.1 主从复制 主从复制是MongoDB最常用的复制方式。可用于备份,故障恢复和读扩展等。 基本就是搭建一个主节点和一个或多个从节点,每个从节点需要知道主节点的地址。运行mongod --master启动主服务器。运行mongod --slave --source master_address启动从服务器。 [root@test02 ~]# mongod --fork --dbpath /data/node2 --logpath /data/mongodb.log --port 10001 --logappend --master 从节点选择不同的目录和端口,并且用--source为从节点指明主节点的地址。 [root@test02 ~]# mongod --fork --dbpath /data/node3 --logpath /data/mongodb2.log --port 10002 --logappend --slave --source localhost:10001 所有的从节点都是从主节点复制信息,目前还不能从节点到从节点复制机制,原因是从节点没有自己的oplog。 一个集群中从节点没有明确的限制,但是多个节点对单点主机发起的查询也是吃不消的,不超过12个节点的集群可以良好运转。 9.1.1 选项 --only 在从节点上指定只复制特定某个数据库(默认复制所有数据库)。 --slavedelay 用在从节点上,当应用主节点的操作时增加延迟。这样可以轻松设置延时从节点了,这样的节点对于用户无意间删除重要数据或插入垃圾数据起到防护作用。通过延缓操作,可以有个恢复时间差。 --fastsync 以主节点的数据快照为基础启动从节点。如果数据目录一开始时主节点的数据快照,从节点用这个选项启动要比做完整同步块很多。 --autoresync 如果主节点和从节点不同步,可以自动同步了。 --oplogsuze 主节点oplog的大小(单位是MB)。 9.1.2 添加以及删除源 cat >> /etc/hosts <<EOF 192.168.27.212 test02 192.168.27.213 test03 192.168.27.214 test01 EOF 启动从节点时可以用--source指定主节点,也可以在shell中配置这个源。 [root@test02 ~]# mongod --fork --dbpath /data/node3 --logpath /data/mongodb.log --port 10003 --logappend --slave 将192.168.27.212:10001作为源插入到从节点上。 > db.sources.insert({ "host" : "192.168.27.212:10001"}); 立即查询会得到插入的文档: > use local switched to db local > db.sources.find(); { "_id" : ObjectId("530be5049ab1ad709cfe66b7"), "host" : "test02:10001" 当同步完成后,文档更新: > db.sources.find(); { "_id" : ObjectId("530bf0ab058022d91574c79c"), "host" : "test02:10001", "source" : "main", "syncedTo" : Timestamp(1393291443, 1), "dbsNextPass" : { "foo" : true, "test" : true } } 9.2 副本集 副本集就是有自动故障恢复功能的主从集群。主从集群和副本集最为明显的区别就是副本集没有固定的主节点:整个集群会选举出一个主节点,当其不能工作时,则变更到其它节点。副本集总会有一个活跃节点和一个或多个备份节点。 副本集最好的优点就是全自动化的。 mongod --fork --dbpath /data/node2 --logpath /data/mongodb.log --port 10001 --logappend --replSet myrepl/test03:10002 mongod --fork --dbpath /data/node3 --logpath /data/mongodb.log --port 10002 --logappend --replSet myrepl/test02:10001 副本集的亮点是自检测功能:在其中指定单台服务器后,MongoDB会自动搜索并连接其余的节点。 启动几台服务器后,日志会告诉你副本集没有初始化。需要在shell中初始化副本集。 连接任意一个服务器。初始化命令只执行一次: > db.runCommand({"replSetInitiate" : { ... "_id" : "myrepl", ... "members" : [ ... { ... "_id" : 1, ... "host" : "test02:10001" ... }, ... { ... "_id" : 2, ... "host" : "test03:10002" ... } ... ]}}) { "startupStatus" : 4, "info" : "myrepl/test03:10002", "ok" : 0, "errmsg" : "all members and seeds must be reachable to initiate set" } "_id" : "myrepl" 副本集的名称 "members" : [...] 副本集中的服务器列表,每个服务器至少两个键。 "_id" : N 每个服务器唯一的ID "host" : hostname 这个键指定服务器主机 或者: config = {"_id" : "myrepl", "members" : [ {"_id" : 0, "host" : "test02:10001"}, {"_id" : 1, "host" : "test03:10002"} ]} rs.initiate(config); rs.status(); myrepl:SECONDARY> rs.status(); { "set" : "myrepl", "date" : ISODate("2014-02-25T02:17:39Z"), "myState" : 2, "syncingTo" : "test03:10002", "members" : [ { "_id" : 0, "name" : "test02:10001", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 968, "optime" : Timestamp(1393294457, 1), "optimeDate" : ISODate("2014-02-25T02:14:17Z"), "errmsg" : "syncing to: test03:10002", "self" : true }, { "_id" : 1, "name" : "test03:10002", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 48, "optime" : Timestamp(1393294457, 1), "optimeDate" : ISODate("2014-02-25T02:14:17Z"), "lastHeartbeat" : ISODate("2014-02-25T02:17:38Z"), "lastHeartbeatRecv" : ISODate("2014-02-25T02:17:39Z"), "pingMs" : 1, "syncingTo" : "test02:10001" } ], "ok" : 1 } 如果这时候把primary节点停掉,在secondary节点执行写操作,就会发生如下错误提示: myrepl:SECONDARY> db.test.insert({name : "baobao"}); not master 如果只有2台Mongodb,配置复制集群还不够安全,需要1个外在角色调整各个节点的角色。 standard:常规节点,存储一份完整的数据副本,参与选举投票,可能称为活跃节点。 passive:存储完整的数据副本,参与投票,不能成为活跃节点。 arbiter:仲裁者只负责投票,不接受复制数据,也不能成为活跃节点。 当Primary宕掉后,可以通过Arbiter在Secodarys中选举一个Primary节点,避免单点故障。 可以增加一个仲裁节点,只负责仲裁,不做数据存储。 mongod --fork --dbpath /data/node1 --logpath /data/mongodb.log --port 10003 --logappend --replSet myrepl/test02:10001,test03:10002 myrepl:PRIMARY> rs.addArb("test01:10003"); { "ok" : 1 } 查看各节点的状态: myrepl:PRIMARY> rs.status(); { "set" : "myrepl", "date" : ISODate("2014-02-25T02:30:26Z"), "myState" : 1, "members" : [ { "_id" : 0, "name" : "test02:10001", "health" : 1, "state" : 1, "stateStr" : "PRIMARY", "uptime" : 1735, "optime" : Timestamp(1393295409, 1), "optimeDate" : ISODate("2014-02-25T02:30:09Z"), "self" : true }, { "_id" : 1, "name" : "test03:10002", "health" : 1, "state" : 2, "stateStr" : "SECONDARY", "uptime" : 204, "optime" : Timestamp(1393295409, 1), "optimeDate" : ISODate("2014-02-25T02:30:09Z"), "lastHeartbeat" : ISODate("2014-02-25T02:30:26Z"), "lastHeartbeatRecv" : ISODate("2014-02-25T02:30:24Z"), "pingMs" : 1, "syncingTo" : "test02:10001" }, { "_id" : 2, "name" : "test01:10003", "health" : 1, "state" : 6, "stateStr" : "UNKNOWN", "uptime" : 17, "lastHeartbeat" : ISODate("2014-02-25T02:30:25Z"), "lastHeartbeatRecv" : ISODate("1970-01-01T00:00:00Z"), "pingMs" : 1, "lastHeartbeatMessage" : "still initializing" } ], "ok" : 1 } 对比三个节点对自身节点性质的判断: myrepl:PRIMARY> db.isMaster(); { "setName" : "myrepl", "ismaster" : true, "secondary" : false, "hosts" : [ "test03:10002", "test02:10001" ], "arbiters" : [ "test01:10003" ], "primary" : "test03:10002", "me" : "test03:10002", "maxBsonObjectSize" : 16777216, "maxMessageSizeBytes" : 48000000, "localTime" : ISODate("2014-02-25T02:32:29.760Z"), "ok" : 1 } myrepl:SECONDARY> db.isMaster(); { "setName" : "myrepl", "ismaster" : false, "secondary" : true, "hosts" : [ "test02:10001", "test03:10002" ], "arbiters" : [ "test01:10003" ], "primary" : "test03:10002", "me" : "test02:10001", "maxBsonObjectSize" : 16777216, "maxMessageSizeBytes" : 48000000, "localTime" : ISODate("2014-02-25T02:33:50.144Z"), "ok" : 1 } myrepl:SECONDARY> db.isMaster(); { "setName" : "myrepl", "ismaster" : false, "secondary" : true, "hosts" : [ "test02:10001", "test03:10002" ], "arbiters" : [ "test01:10003" ], "primary" : "test03:10002", "me" : "test02:10001", "maxBsonObjectSize" : 16777216, "maxMessageSizeBytes" : 48000000, "localTime" : ISODate("2014-02-25T02:33:50.144Z"), "ok" : 1 } 在节点配置中修改priority键,来配置成标准节点或者被动节点。 默认优先级为1,可以是0~1000. "arbiterOnly"键可以指定仲裁节点。 备份节点会从活跃节点抽取oplog,并执行操作,就像活跃备份系统中的备份服务器一样。活跃节点也会写操作到自己的本地oplog,这样就能成为活跃节点了。oplog中的操作也包括严格递增的序号。通过序号判断数据的时效性。 9.2.3 故障切换和活跃节点的选举 如果活跃节点坏了,其他节点会选一个新的活跃节点。新的活跃节点由副本集中的大多数选举出来。仲裁节点只负责投票,避免出现僵局。新的节点是优先级最高的节点。 活跃节点使用心跳来跟踪集群中多少节点对其可见,如果不超过半数,则活跃节点自动降为备份节点。可以防止活跃节点一直不放权。 无论活跃节点何时变化,新活跃节点的数据被假定为系统的最新数据。其他节点的操作都会回滚,所有节点连接新的活跃节点后要重新同步。这些节点会查看自己的oplog,找出其中活跃节点没有执行过的操作,然后向活跃节点请求这些操作影响的文档的最新副本。 正在执行重新同步的节点被视为恢复中,在完成这个过程前,不能成为活跃节点候选者。 9.3 在从服务器上执行操作 从节点的主要作用是作为故障恢复机制,以防止主节点数据丢失或者停止服务。 可以在从节点做备份的数据源。也可以用来扩展读取性能,或者进行数据处理。 9.3.1 读扩展 用MongoDB扩展读取的一种方式就是将查询放在从节点上,减轻主节点的负载。当负载是读密集型时这样非常不错。当是写密集型时,需要用自动分片来扩展。 使用从节点来扩展MongoDB的读取有个要点,就是数据复制并不同步,就是在主节点插入或更新数据口,有片刻从节点的数据不是最新的。 扩展读取需要打开一个特殊选项slaveOkey,告诉从服务器是否可以处理请求。 如果直接在secondary上操作,会发生如下错误: myrepl:SECONDARY> db.test.find(); error: { "$err" : "not master and slaveOk=false", "code" : 13435 } 需要告知Mongodb集群,从哪台机器上进行读操作: myrepl:SECONDARY> rs.slaveOk(); myrepl:SECONDARY> db.test.find(); { "_id" : ObjectId("530bfc79eee2c2ce39f9cd95"), "name" : "caoqing" } { "_id" : ObjectId("530bfd8f3627cb16c15dcb32"), "name" : "xiaobao" } 9.3.2 用从节点做数据处理 从节点的另外一个服务就是作为一种机制来减轻密集型处理的负载,或作为聚合,避免影响主节点的性能。用--master启动一个普通的从节点,同时使用--master和--slave矛盾。这意味着如果能对从节点进行写入,像平常一样查询,就把它作为一个主节点。从节点还是会不断的从主节点复制数据。这样就可以对从节点执行阻塞操作而不影响主节点的性能。 从节点第一次启动时不能有正在复制的数据库,如果有,数据库就不能完成同步,只能更新。 用这种技术要保证不能对正在复制主节点数据的从节点上的数据库执行写入。从节点不能恢复这些操作,就不能正确的映射主节点。 9.4 工作原理 MongoDB的复制至少需要两台服务器或者节点,其中一个主节点,负责处理客户端请求,其他的都是从节点,负责映射主节点的数据。主节点记录在其上的所有操作。 从节点定期轮询主节点获取这些操作,然后对数据副本执行这些操作。由于和主节点执行了相同的操作,从节点就能保持和主节点的数据同步。 9.4.1 oplog 主节点的操作记录成为polog(operation log)。oplog存储在一个特殊的数据库里,成为local。oplog就在其中的oplog.$main集合里面。oplog的每个文档都代表主节点执行的一个操作。 myrepl:PRIMARY> db.oplog.$main.help(); 查看oplog的内容: myrepl:PRIMARY> use local; switched to db local myrepl:PRIMARY> show collections; me oplog.rs replset.minvalid slaves startup_log system.indexes system.replset myrepl:PRIMARY> db.oplog.rs.find(); { "ts" : Timestamp(1393294283, 1), "h" : NumberLong(0), "v" : 2, "op" : "n", "ns" : "", "o" : { "msg" : "initiating set" } } { "ts" : Timestamp(1393294457, 1), "h" : NumberLong("-8949844291534979055"), "v" : 2, "op" : "i", "ns" : "test.test", "o" : { "_id" : ObjectId("530bfc79eee2c2ce39f9cd95"), "name" : "caoqing" } } { "ts" : Timestamp(1393294735, 1), "h" : NumberLong("677282438107403253"), "v" : 2, "op" : "i", "ns" : "test.test", "o" : { "_id" : ObjectId("530bfd8f3627cb16c15dcb32"), "name" : "xiaobao" } } { "ts" : Timestamp(1393295409, 1), "h" : NumberLong("5171944912929102944"), "v" : 2, "op" : "n", "ns" : "", "o" : { "msg" : "Reconfig set", "version" : 2 } } myrepl:PRIMARY> 文档包含的键如下: ts 操作的时间戳。时间戳是一种内部类型,用于跟踪操作执行的时间。有4字节的时间戳和4字节的递增计数器构成。 op 操作类型,只有1字节代码。 ns 执行操作的命名空间。 o 进一步指定要执行操作的文档。 oplog只记录改变数据库状态的操作。oplog只是作为从节点和主节点保持数据同步的机制。 存储在oplog里的操作不是完全和主节点的操作一模一样的。这些操作在存储之前先做等幂变换,这些操作可以在从服务器端多次执行,只要顺序是对的,就不会有问题。 oplog在固定集合中,不能保证oplog不超过预先设定的大小。需要在创建mongodb服务时指定--oplogSize,参数指定oplog的大小。 一般64bit-linux,分配5%的剩余空间,单位为MB。 9.4.2 同步 从节点第一次启动时,会对主节点数据进行完整的同步。从节点复制主节点上的每一个数据,耗费资源大。同步完成后,从节点查询主节点的oplog,并执行这些操作,保证数据是最新的。 如果从节点的操作被主节点落下太远了,从节点就跟不上同步了,从节点发生宕机或者疲于应付读取时,就会出现这种情况,也会在执行完完整同步后出现这种情况,因为oplog可能已经回滚一圈了。 从节点跟不上同步后,复制就会停下,从节点需要重新做完整的同步。可以用{"resync" : 1}命令手动执行同步,也可以在启动从节点是使用--autoresync选项让其自动同步。重新同步代价高昂,尽量避免,方法就是配置足够大的oplog。 9.4.3 复制状态和本地数据库 本地数据库用来存放所有内部复制状态,主节点和从节点都有。本地数据就是local,其内容不会被复制。可以确保一盒MongoDB数据库只有一个本地数据库。 本地数据库不限于存放MongoDB的内部状态。如果有不想复制的文档,也可以放在本地数据库的集合里。 主节点上的复制状态还包括从节点上的列表。这个列表存放在slaves集合中: myrepl:PRIMARY> db.slaves.find(); { "_id" : ObjectId("530bfbdc911eb0ac3bf2aa8b"), "config" : { "_id" : 1, "host" : "test03:10002" }, "ns" : "local.oplog.rs", "syncedTo" : Timestamp(1393295409, 1) } 从节点也在本地数据库中存放状态。在me集合中存放从节点的唯一标识符,在sources集合中存放源或节点的列表。 myrepl:SECONDARY> db.me.find(); { "_id" : ObjectId("530bfbdc911eb0ac3bf2aa8b"), "host" : "test03" } 主节点和从节点都跟踪从节点的更新状况,这个是通过存放在"syncedTO"中的时间戳来完成的。 9.4.4 阻塞复制 开发者可以使用getLastrror的'w'参数来确保数据的同步性。运行getLastError会进入阻塞状态,直到N个服务器复制了最新的写入操作为止。 检查本连接的上一次数据库操作的错误。 myrepl:PRIMARY> db.runCommand("getlasterror") { "n" : 0, "lastOp" : Timestamp(0, 0), "connectionId" : 3525, "err" : null, "ok" : 1 } 指定"w"选项后,可以使用"wtimeout"选项,表示以毫秒为单位的超时。 阻塞复制会导致写操作明显变慢,尤其是"w"的值比较大时。 9.5 管理 9.5.1 管理 MongoDB包含很多有用的管理工具,用以查看复制的状态。 通过 db.printReplicationInfo()命令查看oplog状态。 myrepl:PRIMARY> db.printReplicationInfo(); configured oplog size: 997.7892578125001MB log length start to end: 1126secs (0.31hrs) oplog first event time: Tue Feb 25 2014 10:11:23 GMT+0800 (CST) oplog last event time: Tue Feb 25 2014 10:30:09 GMT+0800 (CST) now: Wed Feb 26 2014 02:07:23 GMT+0800 (CST) 输出信息包括oplog日志的大小,操作日志记录的起始时间。 查看从库同步状态。 myrepl:PRIMARY> db.printSlaveReplicationInfo(); source: test03:10002 syncedTo: Tue Feb 25 2014 10:30:09 GMT+0800 (CST) = 56533 secs ago (15.7hrs) source: test01:10003 no replication info, yet. State: ARBITER 输出信息包括从库的主机名,port信息等。 9.5.2 变更oplog的大小 如果发现oplog大小不合适,最简单的方法就是停掉主节点,删除local数据库的文件,用心的设置重新启动。 # rm -rf /data/node2/local* 为大型的oplog预分配空间非常耗费时间,且可能导致主节点停机时间增加,尽可能的手动预分配数据文件。 9.5.3 复制的认证问题 如果在复制中使用了认证,还需要做些配置,使得从节点可以访问俄主节点的数据。在主节点和从节点都需要在本地数据库增加用户,每个节点的用户名和口令相同。 从节点连接到主节点是,会用存储在local.system.users中的用户认证。最先尝试"repl"用户,如果没有,则用local.system.users中的第一个可用用户。