1、主从复制
主从复制是MongoDB最常用的复制方式,这种方式很灵活.可用于备份,故障恢复,读扩展等。最基本的设置方式就是建立一个主节点和一个或多个从节点,每个从节点要知道主节点的地址。
运行mongod --master就启动了主服务器;
运行mongod --slave --source master_address就启动了从服务器。
其中master_address是主节点的地址。
1.1、建立主节点
首先给主节点建立数据目录:
[root@gflinux102 ~]# mkdir -p /opt/mongo/master
[root@gflinux102 ~]# mongod --dbpath /opt/mongo/master/ --port 10000 --master
1.2、建立从节点
选择不同的目录和端口,并且用--source为从节点指明主节点的地址:
建立从节点slave1:
[root@gflinux102 data]# mkdir -p /opt/mongo/slave1
[root@gflinux102 data]# mongod --dbpath /opt/mongo/slave1/ --port 10001 --slave --source localhost:10000
建立从节点slave2
[root@gflinux102 ~]# mkdir -p /opt/mongo/slave2
[root@gflinux102 ~]# mongod --dbpath /opt/mongo/slave2 --port 10002 --slave --source localhost:10000
注意:所有从节点都从主节点复制内容,还没有从从节点复制的机制,原因是从节点并不保存自己的oplog。
一个集群中有多少个从节点并没有明确的限制,但是上千个从节点对单个主机节点发起查询也会让其负担过重,所以实际上,不超过12个从节点的集群就可以运转良好。
1.3、数据测试
主节点:
[root@gflinux102 ~]# mongo localhost:10000
MongoDB shell version: 2.6.6
connecting to: localhost:10000/test
......
> db.rgf.insert("name":"rgf","age":10,"sex":"male")
2015-02-11T11:20:36.909+0800 SyntaxError: Unexpected token :
> db.rgf.insert({"name":"rgf","age":10,"sex":"male"})
WriteResult({ "nInserted" : 1 })
> db.rgf.find()
{ "_id" : ObjectId("54daca943149616d6b6d9714"), "name" : "rgf", "age" : 10, "sex" : "male" }
slave1:
[root@gflinux102 ~]# mongo localhost:10001
MongoDB shell version: 2.6.6
connecting to: localhost:10001/test
Server has startup warnings:
......
> use test
switched to db test
> db.rgf.find()
{ "_id" : ObjectId("54daca943149616d6b6d9714"), "name" : "rgf", "age" : 10, "sex" : "male" }
slave2:
[root@gflinux102 ~]# mongo localhost:10002
MongoDB shell version: 2.6.6
connecting to: localhost:10002/test
......
> use test
switched to db test
> db.rgf.find()
{ "_id" : ObjectId("54daca943149616d6b6d9714"), "name" : "rgf", "age" : 10, "sex" : "male" }
>
主从数据同步。
1.4、主从复制选项
1 | --only | 在从节点上指定只复制特定某个数据库(默认复制所有数据库) |
2 | --slavedelay | 用在从节点上,当应用主节点的操作时增加延时(s)。这样就能轻松设置延时节点,这种节点对用户无意中删除重要文档或者插入垃圾数据等有防护作用,这些不良操作都会被复制到所有的从节点上,通过延时执行操作,可以有个恢复的时间差。 |
3 | --fastsync | 以主节点的数据快照为基础启动从节点。如果数据目录一开始是主节点的数据快照,从节点用这个选项启动要比做完整同步快很多。 |
4 | --autoresync | 如果从节点与主节点不同步了,则自动重新同步。 |
5 | --oplogSize | 主节点oplog的大小 |
1.5、添加及删除源
1.5.1添加节点
启动从节点时,可以用--source指定主节点,也可以在shell中配置这个这个源。
从节点启动示例:
[root@gflinux102 ~]# mongod --slave --dbpath /opt/mongo/slave --port 10001
这个功能也可以在shell中运行,并且更加灵活:
> use local
switched to db local
> db.sources.insert({"host":"localhost:10000"})
插入源后,可以看到插入的文档:
> use local
switched to db local
> db.sources.find()
{ "_id" : ObjectId("54dac7ca275148d655ca1792"), "host" : "localhost:10000"}
当完成同步,该文档即被更新。
> db.sources.find()
{ "_id" : ObjectId("54dac7ca275148d655ca1792"), "host" : "localhost:10000", "source" : "main", "syncedTo" : Timestamp(1423632078, 1) }
1.5.2删除节点
如果想更改从节点的配置,改为比如10.100.11.102:10000为源,则可以用insert和remove来完成。
> db.sources.insert({"host":"10.100.11.102:10000"})
> db.sources.remove({"host":"localhost:10000"})
可以看到,sources集合可以被当做普通集合进行操作,而且为管理节点提供了很大的灵活性。
2、副本集
副本集(Replica Set)就是有自动故障恢复功能的主从集群。主从集群和副本集最明显的区别是副本集没有固定的"主节点",整个集群会选举出一个"主节点",当其不能工作时则变更到其他节点。副本集总会有一个活跃节点(primary)和一个或多个备份节点(secondary)。副本集可以在活跃节点有问题时自动切换。当活跃节点不工作了,备份节点就会成为活跃节点;如果原来的活跃节点恢复了,它会成为新的活跃节点的备份节点。
2.1、初始化副本集
副本集不能用localhost地址作为成员,所以的找到机器的主机名:
[root@gflinux102 ~]# echo ${HOSTNAME}
gflinux102
2.1.1、创建数据目录
要为每一个服务器创建数据目录,选择端口:
[root@gflinux102 mongo]# mkdir -p /opt/mongo/node{1,2}
2.1.2、命名副本集
名字是为了易于与别的副本集区分,也是为了方便将整个集合视为一个整体,这里命名为"gf"。--replSet是让服务器知晓这个"gf"副本集中还有别的同伴。
[root@gflinux102 mongo]# mkdir -p /opt/mongo/node{1,2}
2.1.2、启动副本集
启动第一台:
[root@gflinux102 mongo]# mongod --dbpath /opt/mongo/node1 --port 10001 --replSet gf/gflinux102:10002
启动第二台:
[root@gflinux102 data]# mongod --dbpath /opt/mongo/node2/ --port 10002 --replSet gf/gflinux102:10001
如果想要启动第三台:
[root@gflinux102 data]# mongod --dbpath /opt/mongo/node3/ --port 10003 --replSet gf/gflinux102:10001
[root@gflinux102 data]# mongod --dbpath /opt/mongo/node2/ --port 10002 --replSet gf/gflinux102:10001,gflinux102:10002
副本集的亮点就是有自检测功能:在其中指定单台服务器后,Mongodb就会自动搜索并连接其他的节点。
启动了几台服务器之后,日志就会告诉你副本集没有进行初始化,因为还差最后一步:
2.1.2、shell中初始化副本集
在shell中,连接其中一个服务器,初始化命令只能执行一次:
[root@gflinux102 data]#mongo gflinux:10001/admin
>db.runCommand({"replSetInitiate":{
..."_id":"gf",
..."members":[
... {
... "_id":1,
... "host":"gflinux102:10001"
... },
... {
... "_id":2,
... "host":"gflinux102:10002"
... }
...]}})
{
"info":"Config now saved locally.Shoule come online in about a
minute.",
"ok": true
}
"_id":"gf" :副本集的名字
"members":[...] :副本集中的服务器列表,每个服务器文档至少有两个键。
"_id":N:每个服务器的唯一ID
2.1.3、副本集中的节点
任何时间,集群中只有一个活跃节点,其他的都是备份节点.活跃节点实际上是活跃服务器,指定的活跃节点可以随时间而改变.
有几种不同类型的节点可以存在与副本集中
standard 标准节点
这是常规节点,它存储一份完整的数据副本,参与选举投票有可能成为活跃节点
passive 被动结点
存储了完整的数据副本,参与投票,不能成为活跃节点
arbiter 仲裁者
仲裁者只能参与投票,不接收复制的数据,也不能成为活跃节点.
标准节点和被动节点之间的区别仅仅是数量的差别,每个参与节点(非仲裁)有优先权.
优先权按照优先值从大到小.
在节点配置中修改priority键,来配置标准节点或者被动节点.
>members.push({"_id":3,"host":"127.0.0.1:10002","priority":40})
默认优先级为1,可以是0-1000(含)
"arbiterOnly"键可以指定仲裁节点
>members.push({"_id":4,"host":"127.0.0.1:10003","arbiterOnly":true})
备份节点会从活跃节点抽取oplog,并执行操作,就像活跃备份系统中的备份服务器一样.活跃节点也会写操作
到自己的本地oplog.oplog中的操作包含严格递增的序号,这个序号来判定数据的时效性.
2.1.4、故障切换和活跃节点选举
如果活跃节点出现故障,其余节点会选一个新的活跃节点.选举过程可以由任何非活跃节点发起,新的活跃节点由副本集中的大多数选举产生.仲裁节点也参与选举,避免出现僵局.新的活跃节点将是优先级最高的节点,优先级相同这数据较新的节点获胜.
不论活跃节点何时变化,新的活跃节点的数据就被假定为系统的最新数据.对其他几点(原来的活跃节点)的操作都会回滚,即便是之前的活跃节点已经恢复工作了.为了完成回滚,所有节点连接新的活跃节点后重新同步。这些节点会查看自己的oplog,找出自重活跃节点没有的操作,然后向活跃节点请求这些操作影响的文档最新副本。正在执行重新同步的节点被视为恢复中,在完成这个过程之前不能成为活跃节点的候选者。
3、从服务器上的操作
从节点的主要作用是作为故障恢复机制,以防主节点数据丢失或者停止服务,除此之外,从节点可用做备份的数据源、扩展读取性能或进行数据处理。
3.1、读扩展
用MongoDB扩展读取的一种方式就是将查询放在从节点上。这样,主节点的负载就减轻了。一般说来,当负载是读取密集型时这是非常不错的方案。要是写密集型,则要考虑用自动分片来进行扩展。
使用从节点来扩展MongoDB的读取有个要点,就是数据复制并不同步,也就是说在主节点插入和更新数据后,有片刻从节点的数据并不是最新的,在考虑用查询从节点完成请求时这点非常重要。
扩展读取本身很简单,像往常一样设置主从复制,连接从服务器处理请求.唯一的技巧是有个特殊的查询选项,告诉从服务器是否可以处理请求(默认是不可以的)。这个选项是slaveOkay,所有的MongoDB驱动程序都提供了一种机制来设置它。有些驱动程序还提供工具使得请求分布到从节点的过程自动化,但这个过程随驱动程序的不同而不同.
3.2、用从节点做数据处理
从节点可以作为一种机制来减轻密集型处理的负载,或作为聚合,避免影响主节点的性能。用--master参数启动一个普通的从节点.同时使用--slave和--master有点矛盾,这意味着如果能对从节点进行写入,像往常一样查询,就把它作为一个普通的MongoDB主节点就行了,向从节点插入数据不会同步到主节点中。从节点还是会不断的从真正的主节点复制数据。这样,就可以对从节点执行阻塞操作也不影响主节点的性能。用这种技术的时候,一定要确保不能对正在复制主节点数据的从节点上的数据库进行写入。从节点不能恢复这些操作,就不能映射主节点.
诸如:
启动主节点:
[root@gflinux102 ~]# mongod --dbpath /opt/mongo/master/ --port 10000 --master
启动从节点:
[root@gflinux102 ~]# mongod --dbpath /opt/mongo/slave1/ --port 10001 --master --slave --source localhost:10000
在主节点插入数据:
> db.rgf.insert({"name":"xuan","age":10,"sex":"male"})
WriteResult({ "nInserted" : 1 })
> db.rgf.find()
{ "_id" : ObjectId("54daca943149616d6b6d9714"), "name" : "rgf", "age" : 10, "sex" : "male" }
{ "_id" : ObjectId("54db1db98b5be4a508f8006c"), "name" : "xuan", "age" : 10, "sex" : "male" }
在从节点查看数据同步状况:
> use test;
switched to db test
> db.rgf.find()
{ "_id" : ObjectId("54daca943149616d6b6d9714"), "name" : "rgf", "age" : 10, "sex" : "male" }
{ "_id" : ObjectId("54db1db98b5be4a508f8006c"), "name" : "xuan", "age" : 10, "sex" : "male" }
同步成功。现在在从节点插入数据:
> db.rgf.insert({"hello":"zhao","meinv":1,"ok":"yes"})
WriteResult({ "nInserted" : 1 })
> db.rgf.find()
{ "_id" : ObjectId("54daca943149616d6b6d9714"), "name" : "rgf", "age" : 10, "sex" : "male" }
{ "_id" : ObjectId("54db1db98b5be4a508f8006c"), "name" : "xuan", "age" : 10, "sex" : "male" }
{ "_id" : ObjectId("54db1e898d04c632fbf8982d"), "hello" : "zhao", "meinv" : 1, "ok" : "yes" }
>
再在主节点查看数据:
> db.rgf.find()
{ "_id" : ObjectId("54daca943149616d6b6d9714"), "name" : "rgf", "age" : 10, "sex" : "male" }
{ "_id" : ObjectId("54db1db98b5be4a508f8006c"), "name" : "xuan", "age" : 10, "sex" : "male" }
>
说明主节点并没有同步从节点的数据。
4、复制工作原理
复制至少需要两个服务器或节点。其中一个是主节点,负责处理客户端请求,其他都是从节点负责映射主节点的数据。主节点记录在其上执行的所有操作。从节点定期轮训主节点获得这些操作,然后对自己的数据副本执行这些操作。由于和主节点执行了相同的操作,从节点就能保持与主节点的数据同步。
4.1、操作日志
主节点的操作记录(operation log)称为oplog。oplog存储在一个特殊的数据库local中。oplog就在其中的oplog.$main集合里面。oplog中的每个文档都代表主节点上执行的一个操作。
使用MongoVUE并不能查看到oplog的日志。
> use local
switched to db local
> db.oplog.$main.find()
{ "ts" : Timestamp(1423623841, 11), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423623855, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423623865, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423623875, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423623885, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423623895, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423623905, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423623915, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423623925, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423623935, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423623945, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423623955, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423623965, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423623975, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423623985, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423623995, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423624005, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423624015, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423624025, 1), "op" : "n", "ns" : "", "o" : { } }
{ "ts" : Timestamp(1423624035, 1), "op" : "n", "ns" : "", "o" : { } }
Type "it" for more
> db.oplog.$main.findOne()
{ "ts" : Timestamp(1423623841, 11), "op" : "n", "ns" : "", "o" : { } }
"ts"
操作的时间戳。时间戳是一种内部类型,用于跟踪操作执行的时间,由4个字节的时间戳和4个自己的递增计数器构成。
"op"
操作类型,只有一个字节。("i"代表插入)
"ns"
执行操作的命名空间(集合名)
"o"
进一步执行要执行操作的文档,对插入来说,就是要插入的文档.
oplog只记录改变数据库状态的操作。如查询不会存储在oplog中,这是因为oplog只是作为从节点与
主节点保持数据同步的机制。
存储在oplog中的操作也不是完全和主节点的操作已于一样的.这些操作在存储之前先要做等幂变换,也就是说,这些操作可以在服务器端多次执行,只要顺序是对的,就不会有问题.如使用"$inc"执行的增加更新操作,会变成"$set"操作。
oplog存储在固定集合中,由于新操作也会存储在oplog中,他们会自动替换旧的操作。这样能保证oplog不能超过预先设定的大小。启动服务器时可以用--oplogsize指定这个大小,单位是MB.默认情况下,64位的实例将会使用oplog 5%的可用空间。这个空间将在local数据库中分配,并在服务器启动时预先分配。
4.2、同步
从节点第一次启动时,会对主节点数据进行完整的同步。从节点复制主节点上的每个文档、耗费的资源可想而知。同步完成后,从节点开始查询主节点的oplog并执行这些操作,以保证数据是最新的。
如果从节点的操作已经被主节点落下很远,从节点就跟不上同步。跟不上同步的从节点无法一直不断地追赶主节点,因为主节点oplog的所有操作都太新。从节点发生宕机或者疲于应付读取时就会出现这种情况,也会在执行完完整同步以后发生类似的事情,因为只要同步时间太长,同步完成后,oplog可能已经滚了一圈了。
从节点跟不上同步时,复制就会停下,从节点需要重新做完整的同步。方法如下:
方法一:
[root@gflinux102 master]# mongod --dbpath /opt/mongo/slave2 --port 10002 --autoresync --slave --source localhost:10000
方法二:
在从服务器上执行:
> db.runCommand({"resync":1})
{
"ok" : 0,
"errmsg" : "resync may only be run against the admin database.",
"code" : 13
}
重新同步的代价很高,应尽量避免,方法是配置足够大的oplog,配置足够大的oplog能存放相当长的时间操作记录,大的oplog会占用更大的磁盘空间,则需要权衡一下,默认的oplog大小是剩余磁盘空间的5%。
> db.runCommand({"isMaster":1})
{
"ismaster" : true,
"maxBsonObjectSize" : 16777216,
"maxMessageSizeBytes" : 48000000,
"maxWriteBatchSize" : 1000,
"localTime" : ISODate("2015-02-12T01:15:58.709Z"),
"maxWireVersion" : 2,
"minWireVersion" : 0,
"ok" : 1
}
4.3、复制状态和本地数据库
本地数据库用来存放所有内部复制状态,主节点和从节点都有,本地数据库的名字就是local,其内容不会被复制,这样就能确保一个MongoDB服务器只有一个本地数据库。如果有不想被复制的文档,也可以将其放在本地数据库的集合里面。
主节点上的复制状态还包括从节点的列表(从节点连接主节点时会执行hangshake命令进行握手),这个列表放在slaves集合中:
主节点:
> use local
switched to db local
> db.slaves.find()
{ "_id" : ObjectId("54dac7ca275148d655ca1791"), "config" : { "host" : "127.0.0.1:50665", "upgradeNeeded" : true }, "ns" : "local.oplog.$main", "syncedTo" : Timestamp(1423635099, 1) }
{ "_id" : ObjectId("54dac8e37f3db8e24904984c"), "config" : { "host" : "127.0.0.1:50666", "upgradeNeeded" : true }, "ns" : "local.oplog.$main", "syncedTo" : Timestamp(1423635099, 1) }
>
从节点:
从节点也在本地数据库中存放状态,在me集合中存放从节点的唯一标识符,在sources集合中存放源或节点的列表:
> use local;
switched to db local
> db.me.find()
{ "_id" : ObjectId("54dac7ca275148d655ca1791"), "host" : "gflinux102" }
> db.sources.find()
{ "_id" : ObjectId("54dac7ca275148d655ca1792"), "host" : "localhost:10000", "source" : "main", "syncedTo" : Timestamp(1423704382, 1) }
>
主节点和从节点都跟踪从节点的更新状态,这是通过存放在"syncedTo"中的时间戳来完成的。每次从节点查询主节点的oplog时,都会用"syncedTo"来确定哪些操作需要执行,或者查看是否已经跟不上同步了。
4.4、阻塞复制
开发者可以用gerLastError的"w"参数来确保数据的同步性。这里运行gerLastError会进入阻塞状态,,直到N个服务器复制了最新的写入操作为止
> db.runCommand({getLastError:1})
{
"lastOp" : Timestamp(1423646345, 1),
"connectionId" : 1,
"n" : 0,
"syncMillis" : 0,
"writtenTo" : null,
"err" : null,
"ok" : 1
}
如果没有N,或者小于2命令就会立刻返回,如果N等于2,主节点要等到至少一个从节点复制了上个操作才会响应命令。主节点本身也包括在N里面。主节点使用local。slaves中存放的"syncedTo"信息跟踪从节点的更新情。指定"w"选项后,还可以使用"wtimeout"选项,表示以毫秒为单位的超时,gerLastError就能在上一个操作复制到N个节点超时返回错误(默认情况下没有超时)
阻塞复制会导致写操作变慢,尤其"w"的值比较大时。实际上,对重要操作将其值设为2或3就能效率和安全兼备了。
5、管理
5.1、诊断
MongoDB包含很多有用的管理工具,用以查看复制的状态,比如查看主从同步信息:
主节点:
> db.printReplicationInfo();
configured oplog size: 50MB
log length start to end: 81491secs (22.64hrs)
oplog first event time: Wed Feb 11 2015 11:04:01 GMT+0800 (CST)
oplog last event time: Thu Feb 12 2015 09:42:12 GMT+0800 (CST)
now: Thu Feb 12 2015 09:42:16 GMT+0800 (CST)
>
这些信息是oplog的大小和oplog中操作的时间范围。例子中的oplog是50M,能放置大约22h的操作。oplog的长度至少要能满足一次完整的重新同步。否则,从节点就会发生滞后。
从节点:
> db.printSlaveReplicationInfo()
source: localhost:10000
syncedTo: Thu Feb 12 2015 09:41:22 GMT+0800 (CST)
122 secs (0.03 hrs) behind the primary
>
显示从节点的数据源列表,其中有数据滞后时间,本例为122s。
5.2、变更oplog的大小
若发现oplog大小不合适,最简单的做法就是停掉主节点,删除local数据库的文件,用新的设置(--oplogSize)重新启动。
5.2.1、停止主从节点
[root@gflinux102 master]# kill `ps -ef|grep mongod | grep -v grep |awk '{print $2}'`
5.2.2、启动主节点
[root@gflinux102 ~]# mongod --dbpath /opt/mongo/master/ --port 10000 --master --oplogSize 50M
Error parsing command line: in option 'oplogSize': invalid option value
try 'mongod --help' for more information
[root@gflinux102 ~]# mongod --dbpath /opt/mongo/master/ --port 10000 --master --oplogSize 50
注意:--oplogSize后的参数不带单位。
5.2.3、启动从节点
要使用参数--autoresync参数重启,否则,在手动重新同步时容易发生如下错误:
> db.runCommand({"resync":1})
2015-02-12T10:29:26.425+0800 Socket say send() errno:32 Broken pipe 127.0.0.1:10001
2015-02-12T10:29:26.426+0800 Error: socket exception [SEND_ERROR] for 127.0.0.1:10001 at src/mongo/shell/query.js:81
2015-02-12T10:29:26.429+0800 trying reconnect to localhost:10001 (127.0.0.1) failed
2015-02-12T10:29:26.430+0800 reconnect localhost:10001 (127.0.0.1) ok
>
在后台端看到如下信息:
2015-02-12T10:29:56.292+0800 [replslave] all sources dead: data too stale halted replication, sleeping for 5 seconds
2015-02-12T10:30:01.294+0800 [replslave] all sources dead: data too stale halted replication, sleeping for 5 seconds
所以建议如下启动:
[root@gflinux102 ~]# mongod --dbpath /opt/mongo/slave1/ --port 10001 --slave --source localhost:10000 --autoresync
5.3、复制中的认证
如果在复制中使用了认证,在主节点和从节点都需要在本地数据库添加用户,每个节点的用户名和口令都是相同的。本地数据库的用户类似admin中的用户,能够读写整个服务器。
从节点连接主节点时,会用存储在local.system.users中的用户进行认证。最先尝试"repl"用户,若没有此用户,则用local.system.users中的第一个可用用户。
在主节点和从节点分别配置如下:
> db.addUser("repl","123456");