Mongo读写分离

在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上完成。副本集+分片上的读写分离稍后讨论。

 

 


上一篇:MongoDB 4.2 数据迁移与备份指南


下一篇:SQL Server Statistics in Always On Availability Groups