在MongoDB副本集中,默认情况下只有primary能响应客户端读和写操作。由于副本集中有多个成员,我们可能想充分利用这些资源,如果读操作和写操作都能在不同服务器上完成,而且保持读写一致性,那将是一件美妙的事。由于副本集中只有一个primary,secondry的数据都是从primary或者其他secondary复制过来的,所以写操作是不大可能直接写到secondary上面的。但是写操作是可以在secondaries上完成的。还有一个问题,由于secondary数据是从primary复制过来的,所以最新的数据可能没法实时从secondary查询。因此,在考虑读写分离的时候,还需考虑数据的读写一致性。为实现这种平衡,MongoDb需要具备良好的读写机制,下面就来研究下它的读写机制。
WRITE CONCERN
MondbDB提供了不同读操作级别来满足不同应用:
1. Errors Ignored
即忽略所有错误,不向客户端返回结果:
sh1:PRIMARY> db.test.ensureIndex({name:1},{unique:true});
sh1:PRIMARY> db.test.getIndexes()
[ { "v" : 1, "key" : { "_id" : 1 }, "ns" : "test.test", "name" : "_id_" }, { "v" : 1, "key" : { "name" : 1 }, "unique" : true, "ns" : "test.test", "name" : "name_1" } ] sh1:PRIMARY> db.test.insert({name:1}) sh1:PRIMARY> db.test.insert({name:1}) E11000 duplicate key error index: test.test.$name_1 dup key: { : 1.0 }
毫无疑问,使用这种写方式是相当恶劣的。所以现在mongodb默认不是这种方式。(怎么调整,变成这种方式呢?
2. 不确认(Unacknowledged)
这种方式和错误忽略类似,但是客户端驱动器只能收到网络相关错误,而不知道真正的错误是什么。这种方式同样恶劣。
3. 确认型(nacknowledged)
在这种方式下,mongod会向客户端返回成功信息或者错误提示信息。这是现在mongodb默认的写方式,服务器用不带参数的getLastError方法向客户端返回错误信息,没有错误则返回null。
sh1:PRIMARY> db.test.insert({name:10000}); sh1:PRIMARY> db.test.insert({name:10000}); E11000 duplicate key error index: test.test.$name_1 dup key: { : 10000.0 } sh1:PRIMARY> db.getLastError() E11000 duplicate key error index: test.test.$name_1 dup key: { : 10000.0 }
4. Journaled
使用这种写方式,mongodb确认写入journal日志后才返回信息。
journalCommitInterval参数决定可日志提交间隔,默认是100ms,取值范围是2-300ms。启动时候使用--journalCommitInterval指定即可。这种方法一般少用,因为会很大程度上影响性能,除非十分考虑数据的安全性。
副本集下的写入方式
对于并发插入不大,但是频繁查询,或者数据安全性高的,需要重点关注写入的优化,保证在读写分离时数据读写实时,备份实时
副本集的写入方式(Replica Acknowledged)是项目的关注点:
getLastError的w参数可以决定副本集中写入的级别。我们可以用w参数指定必须要写入多少个成员才向客户端返回信息。
默认下的写操作只需要primary确认写入即可,也就是说w为1(这个1只包含primary)。如果这个参数过大,超过了非仲裁节点个数,将会产生严重问题。此外,可以设置wtime参数决定最大超时时间(默认是0),这个一般不需要。
例如,现在把参数设置为2看看吧:
sh1:PRIMARY> db.test.runCommand( { getLastError: 1, w: 2 } )(如何取消这个命令呢?)
在这种情况下,写入操作步骤如下:
1.primary上完成写操作
2.primary上记录一条oplog日志,日志中包含一个ts字段,值为写操作执行的时间,比如本例中记为t
3.客户端调用{getLastError:1, w:2}命令等待primary返回结果
4.secondary从primary拉取oplog,获取到刚才那一次写操作的日志
5.secondary按获取到的日志执行相应的写操作
6.执行完成后,secondary再获取新的日志,其向primary上拉取oplog的条件为{ts:{$gt:t}}
7.primary此时收到secondary的请求,了解到secondary在请求时间大于t的写操作日志,所以他知道操作在t之前的日志都已经成功执行了
8.这时候getLastError命令检测到primary与secondary都完成了这次写操作,于是 w:2 的条件满足了,返回给客户端成功执行这个命令,
可以使用调试命令观察secondary最后同步的一条日志信息:
sh1:SECONDARY> rs.debug.getLastOpWritten() { "ts" : { "t" : 1374154241, "i" : 1 }, "h" : NumberLong("7643447760057625437"), "v" : 2, "op" : "i", "ns" : "test.test", "o" : { "_id" : ObjectId("51e7ee01b5ed8bdc9436becf"), "name" : 5258 } }
db.test.runCommand( { getLastError: 1, w: 2 } )命令针对集合进行设置,我们可以在副本集级别设置默认配置:
如果没有手动设置getlasterror参数,可以对副本集进行如下配置(推荐这样进行):
cfg = rs.conf() cfg.settings = {} cfg.settings.getLastErrorDefaults = {w: "majority"} --majority,表示需要多数成员确认,成员不包括relay,hidden,arb。 rs.reconfig(cfg)
副本集中自定义写入方式
使用集合的tags属性,可以实现自定义方式
例如:决定集合中2个写入才算成功:
sh1:PRIMARY> rs.conf() { "_id" : "sh1", "version" : 30, "members" : [ { "_id" : 2, "host" : "192.168.69.44:10000", "priority" : 20, "tags" : { "area" : "huakong1", "user" : "hx1" } }, { "_id" : 3, "host" : "192.168.69.45:10000", "priority" : 5, "tags" : { "area" : "huakong2", "user" : "hx2" } }, { "_id" : 4, "host" : "192.168.69.46:10000", "priority" : 8, "tags" : { "area" : "huakong3", "user" : "hx3" } } ], "settings" : { "getLastErrorModes" : { "ok" : { "user" : 2 --要求有两个成员写入完成才算成功 } } } } sh1:PRIMARY> db.runCommand( { getLastError: 1, w: "ok" } ) { "n" : 0, "lastOp" : { "t" : 0, "i" : 0 }, "connectionId" : 8999, "wnote" : "no write has been done on this connection", "err" : null, "ok" : 1 }
指定某个secondary写入才算成功(单独打tags)
我们还可以自定义集合中哪些字段值必须写到几个成员才返回成功。
例如,三个成员tags分别是:
{ "d1": "1" }
{ "d1": "2", "d11": "3" }
{ "d1": "3" }
现在要指定第二个成员写入就算成功吧
cfg = rs.conf()
cfg.settings = { getLastErrorModes: { ok: { "d11": 1 } } }
rs.reconfig(cfg)
db.runCommand( { getLastError: 1, w: "ok" }
sh1:SECONDARY> db.test.find()
error: { "$err" : "not master and slaveOk=false", "code" : 13435 }
sh1:SECONDARY> db.getMongo().setSlaveOk()
sh1:SECONDARY> db.test.find()
{ "_id" : ObjectId("51e7e24aed8371d7b496c240"), "name" : 1 }
{ "_id" : ObjectId("51e7e5ccb5ed8bdc9436bec7"), "name" : 10000 }
{ "_id" : ObjectId("51e7edf4b5ed8bdc9436becc"), "name" : 5252 }
{ "_id" : ObjectId("51e7ee01b5ed8bdc9436becf"), "name" : 5258 }
READ REFERENCE
设置成从secondary读是有好处的。例如,在多数据中心的环境中,客户端可能到secondary延时更小。又如,有时候副本集中因为某种原因没了primary,也可以保证从secondary读取数据.当然最重要的还是读写分离,分担负载。
最简单的办法,实现在mongo shell单个连接从secondary查询
在单个连接上启用setSlaveOk()方法:
sh1:SECONDARY> db.test.find()
error: { "$err" : "not master and slaveOk=false", "code" : 13435 }
sh1:SECONDARY>db.getMongo().setSlaveOk()
sh1:SECONDARY> db.test.find()
{ "_id" :ObjectId("51e7e24aed8371d7b496c240"), "name" : 1 }
{ "_id" :ObjectId("51e7e5ccb5ed8bdc9436bec7"), "name" : 10000 }
副本集中,MondoDB支持五种读方式:
Primary(默认方式):只从primary读取,primary无法和tags联合使用。
primaryPreferred:primary优先方式,如果从primary查不到就从secondary查询。联合tags时候,如果无法从primary查询,那么可以根据tags依次看剩余的secondaries,没有成员满足tags就报错。
secondary:只从secondary查询。如果没有指定tags,客户端随机选择一个secondary成员。如果指定tags,客户端会依次从满足tags的secondary查询(如果有多个满足,也是随机选择一个)
secondaryPreferred:使用这个选项,如果没法从scondary查询,那就从primary查询。如果指定secondaris的tags,可以从满足tags的成员中随机读取一个。如果一个都不满足,则返回错误
nearest:从网络中ping延时最小的成员查询,可能从primary,也可能从secondary查询。如果指定tags,则从满足条件的tag中选择nearest。
例如,登录seconday后,选择优先从secondary读取:mongo shell中可以通过readPref()方法决定读取方式
sh1:SECONDARY>
db.getMongo().setReadPref('secondaryPreferred')
sh1:SECONDARY> db.test.find()
{ "_id" :ObjectId("51e7e24aed8371d7b496c240"), "name" : 1 }
{ "_id" :ObjectId("51e7e5ccb5ed8bdc9436bec7"), "name" : 10000 }
{ "_id" :ObjectId("51e7edf4b5ed8bdc9436becc"), "name" : 5252 }
{ "_id" :ObjectId("51e7ee01b5ed8bdc9436becf"), "name" : 5258 }
使用tag sets,指定查询优先级注意:tags无法与primary共用
db.getMongo().setReadPref([ 'secondaryPreferred',{ "area": "huakong3","user":"hx3"},{ "area": "huakong2" ,"user":"hx2"}{}] )
使用查询方式,客户端从()中的第一个tags开始查询。如果有问题则向第二个查询,最后的{}表示如果之前的tags都没匹配,那就随机查询吧。
客户端驱动器可以设置基于连接或者集合等的数据读取方式。
指定从secondary或者nereast查询时候,查询对象选择的快速性,合理性以及故障切换的及时性,是我们需要考虑的。
实际使用:
一般从seconary读写在一下场合比较适用:
1.报表统计和数据分析工作
2.历史查询
3.网络分区时考虑就近查询
对于实时查询和实时显示,如果多成员同时写入会带来性能问题,还是用primary查询比较好。如果不需要多成员同时写入,也能保证数据实时,也可启用从secondary查询。可以根据实际情况进行调整,实现最优化。
例如,现在副本集有四个个成员,一个primary,两个secondary,一个仲裁。现在有两个客户端程序连接副本集,我们可以让两个客户端连接不同的secondary。当然,如果secondary故障,那么就需要及时切换到另外一个secondary,如果两个secondary都故障了,实际上那个primary也会变成secondary,那就连剩余的那个secondary吧。
sh1:PRIMARY> rs.conf()
{
"_id" : "sh1",
"version" : 34,
"members" : [
{
"_id" : 2,
"host" : "192.168.69.44:10000",
"priority" : 20,
"tags" : {
"area" : "huakong1",
"user" : "hx1"
}
},
{
"_id" : 3,
"host" : "192.168.69.45:10000",
"priority" : 5,
"tags" : {
"area" : "huakong2",
"user" : "hx2"
}
},
{
"_id" : 4,
"host" : "192.168.69.46:10000",
"priority" : 8,
"tags" : {
"area" : "huakong3",
"user" : "hx3"
}
},
{
"_id" : 5,
"host" : "192.168.69.45:10001",
"arbiterOnly" : true
}
],
"settings" : {
"getLastErrorDefaults" : {
"w" : "2"
}
}
}
第一个客户端登陆:设置如下:
db.getMongo().setReadPref('secondaryPreferred',[ { "area": "huakong3" ,"user":"hx3"},{ "area": "huakong2" ,"user":"hx2"},{} ])
第二个客户端登录,设置如下
db.getMongo().setReadPref('secondaryPreferred',[ { "area": "huakong2" ,"user":"hx2"},{ "area": "huakong3" ,"user":"hx3"},{} ])
在副本集+分片中,读写分离对应用程序来讲是透明的,所有的配置可以在mongos上完成。副本集+分片上的读写分离稍后讨论。