【六】MongoDB管理之副本集

时间:2022-10-06 14:55:38

一、复制介绍

所谓的复制就是在多个主机之间同步数据的过程。

1、数据冗余及可用性

复制技术提供数据冗余及可用性,在不同的数据库服务器上使用多个数据副本,复制技术防止单个数据库服务器出现数据故障而出现数据丢失。通过设置从库,你能在上面进行灾难切换、数据备份、报表服务等。在某些应用场景下,你还能提高读的能力,客户端通过将读和写请求分发到不同的服务器上面。

2、MongoDB复制技术

副本集是一组共享相同数据集的mongod实例。当所有写请求发向主库,而其他从库从主库上应用这些操作,以保证所有成员数据的一致性。

副本集中只能有一个主库接收客户端的写请求。当主库接受写请求后,进行数据操作,这些操作都记录到操作日志中,称为oplog。从库复制这些oplog并应用这些操作以保证与主库的数据集一致。如果这时主库不可用,集群中一个合格的从库通过竞选接管成为新的主库。你也可以在复制集群中添加一个mongod实例,作为仲裁者存在,它并不维护任何数据,它存在的意义在于对集群中其他成员发来的竞选请求和心跳检测做出响应。它对服务器硬件要求并不高。它永远都作为仲裁者存在,无论主库down了,还是某个从库成为新主库。

1)异步复制

从库应用日志中数据操作,是一种异步的方式。

2)自动接管

当主库和其他成员通信过程中,超10秒还无法联通那么一个符合条件的从库通过竞选将成为新的主库。在mongodb3.2版本中,引入了新版本的复制协议,以减少接管的时间。

3)读操作

默认情况下,客户端的读请求是发往主库上的,但是可以设置将客户端的读请求发送到从库上,但是由于复制技术采用的异步方式,这意味着在从库上读到的数据有时会产生延迟,与主库数据出现不一致。

二、复制实践

1、副本集中成员

主要包括三个:主库、从库、仲裁者。在一个副本集群中,对成员个数的最低要求是:一个主库、一个从库、一个仲裁者。但是大多数实际应用中是一个主库、两个从库。在3.0版本中一个集群中最多可以达到50个成员,在3.2版本中可以有12个成员。

1)主库

在一个副本集群中,只能存在一个主库,接收所有写请求。MongoDB应用写操作到数据文件中并记录操作到日志文件oplog中。从库成员复制这些oplog日志并应用操作到他们的数据集中。在集群中,所有成员都能接收读请求,但是默认上应用程序的读请求直接被发送到主库上。当主库不可用了,这就触发了竞选,会在剩下的从库中选择一个新主库。

在某些情景下,会有两个节点有那么一瞬间认为他们自己是主库,但是他们最多只有一个能够完成写操作,它就是目前的主库,并且另外一个是前主库还没有觉察它被降级了,典型的由于是网络分区。当这种情况出现时,连接到前主库的客户端也许会察觉到过期数据,最后进行回滚。

2)从库

为了复制数据,从库采用异步的方式,复制主库上的oplog并应用日志中操作到自己的数据集。一个主从集群环境中可以存在多个从库。

  • Priority 0 Replica Set Members

当从库设置优先级为0,表示这个从库不能成为主库,因为优先级为0的成员无法参加竞选。

  • Hidden Replica Set Members

隐藏从库是对应用程序不可见的。前提必须是不能变成主库,也就是优先级为0

  • Delayed Replica Set Members

设置某个从库的数据延迟主库多久,主要用来防止人为错误时进行恢复使用的,也就是当做备份。前提是延迟从库必须优先级为0,且为隐藏从库。

3)仲裁者

它没有数据集并且不能成为主库,它的存在可以允许主从复制集群中成员数为奇数,因为它总有一个投票权。

2、副本集部署架构

复制集群的架构能够影响集群的容量及性能。标准的生产环境部署架构是一个具有三个成员的复制集群,能够很好的提供容错和冗余能力。一般而言,我们要避免复杂,凡事根据实际的应用需求设计集群架构。

下面介绍几种常用的架构:

1)具有三成员复制集群

复制集群最低要求需要有三个成员,在三成员架构中,分为一主两从和一主一从一仲裁者。

  • 一主两从模式:当主库不可用,两个从库通过竞选成为新主库
  • 一主一从一仲裁:当主库不可用,这个唯一从库将会成为新主库

下面我们就对常用的一主两从进行配置部署:

【实验环境】:

            主机IP                       主机名             端口             功能说明

192.168.245.129                node1             27017           primary

192.168.245.131                node2             27017           secodary

192.168.245.132                node3             27017           secodary

1.1)在三*立的主机上安装mongodb,请参考:http://www.cnblogs.com/mysql-dba/p/5033242.html

1.2)确保三台主机上的mongodb实例相互能够连接上。具体方法如下:

  • 在node1主机上进行:
mongo --host 192.168.245.131 --port
mongo --host 192.168.245.132 --port
  • 在node2主机上进行:
mongo --host 192.168.245.129 --port
mongo --host 192.168.245.132 --port
  • 在node3主机上进行:
mongo --host 192.168.245.129 --port
mongo --host 192.168.245.131 --port

1.3)启动所有主机上mongod服务,特别的需要加上--replSet "rs0" 参数,指定复制集群名称。

mongod --dbpath=/data/db --fork --logpath=/data/log/mongodb.log --replSet "rs0"    #一个集群上成员replSet名字必须一样

1.4)连接到mongo shell环境中,进行复制配置文件初始化:

rs.initiate()            #这个操作只需要在一台上进行,一般都是在主库上进行
{
    "info2" : "no configuration specified. Using a default configuration for the set",
    "me" : "node1:27017",
    "ok" : 1
}

1.5)确认复制集群的配置初始化

rs0:SECONDARY> rs.conf()
{
"_id" : "rs0",
"version" : ,
"protocolVersion" : NumberLong(),
"members" : [
{
"_id" : ,
"host" : "node1:27017",
"arbiterOnly" : false,
"buildIndexes" : true,
"hidden" : false,
"priority" : ,
"tags" : { },
"slaveDelay" : NumberLong(),
"votes" :
}
],
"settings" : {
"chainingAllowed" : true,
"heartbeatIntervalMillis" : ,
"heartbeatTimeoutSecs" : ,
"electionTimeoutMillis" : ,
"getLastErrorModes" : { },
"getLastErrorDefaults" : {
"w" : ,
"wtimeout" :
}
}
}

1.6)在主库上添加其他成员:

rs0:PRIMARY> rs.add("192.168.245.131")
{ "ok" : }
rs0:PRIMARY> rs.add("192.168.245.132")
{ "ok" : }

1.7)使用rs.status()方法查看这时集群的状态:

rs0:PRIMARY> rs.status()
{
"set" : "rs0",
"date" : ISODate("2015-12-06T01:31:03.278Z"),
"myState" : ,
"term" : NumberLong(),
"heartbeatIntervalMillis" : NumberLong(),
"members" : [
{
"_id" : ,
"name" : "node1:27017",
"health" : ,
"state" : ,
"stateStr" : "PRIMARY",
"uptime" : ,
"optime" : {
"ts" : Timestamp(, ),
"t" : NumberLong()
},
"optimeDate" : ISODate("2015-12-05T23:53:03Z"),
"electionTime" : Timestamp(, ),
"electionDate" : ISODate("2015-12-05T23:51:39Z"),
"configVersion" : ,
"self" : true
},
{
"_id" : ,
"name" : "192.168.245.131:27017",
"health" : ,
"state" : ,
"stateStr" : "SECONDARY",
"uptime" : ,
"optime" : {
"ts" : Timestamp(, ),
"t" : NumberLong()
},
"optimeDate" : ISODate("2015-12-05T23:53:03Z"),
"lastHeartbeat" : ISODate("2015-12-06T01:31:01.809Z"),
"lastHeartbeatRecv" : ISODate("2015-12-06T01:31:02.855Z"),
"pingMs" : NumberLong(),
"syncingTo" : "node1:27017",
"configVersion" :
},
{
"_id" : ,
"name" : "192.168.245.132:27017",
"health" : ,
"state" : ,
"stateStr" : "SECONDARY",
"uptime" : ,
"optime" : {
"ts" : Timestamp(, ),
"t" : NumberLong()
},
"optimeDate" : ISODate("2015-12-05T23:53:03Z"),
"lastHeartbeat" : ISODate("2015-12-06T01:31:01.598Z"),
"lastHeartbeatRecv" : ISODate("2015-12-06T01:31:01.121Z"),
"pingMs" : NumberLong(),
"configVersion" :
}
],
"ok" :
}

1.8)进行测试验证主从复制功能:

#在主库上插入一个文档
rs0:PRIMARY> db.testrp.insert({"name":"test replication"})
WriteResult({ "nInserted" : })
#到从库上查看,已经有了主库上的数据
rs0:SECONDARY> db.testrp.find({"name":"test replication"})
{ "_id" : ObjectId("5663906284c32afdaa84c21f"), "name" : "test replication" }
#将主库kill掉,看看从库能否会接管成新主库
rs0:PRIMARY> rs.status()
{
    "set" : "rs0",
    "date" : ISODate("2015-10-23T01:03:10.811Z"),
    "myState" : 1,
    "term" : NumberLong(2),
    "heartbeatIntervalMillis" : NumberLong(2000),
    "members" : [
        {
            "_id" : 0,
            "name" : "node1:27017",
            "health" : 0, #这时主库被kill了,所以为0
            "state" : 8,
            "stateStr" : "(not reachable/healthy)",
            "uptime" : 0,
            "optime" : {
                "ts" : Timestamp(0, 0),
                "t" : NumberLong(-1)
            },
            "optimeDate" : ISODate("1970-01-01T00:00:00Z"),
            "lastHeartbeat" : ISODate("2015-10-23T01:03:10.475Z"),
            "lastHeartbeatRecv" : ISODate("2015-10-23T01:02:30.456Z"),
            "pingMs" : NumberLong(0),
            "lastHeartbeatMessage" : "Connection refused",
            "configVersion" : -1
        },
        {
            "_id" : 1,
            "name" : "192.168.245.131:27017",
            "health" : 1,
            "state" : 1,
            "stateStr" : "PRIMARY", #这台通过竞选成为了新主库
            "uptime" : 6590,
            "optime" : {
                "ts" : Timestamp(1449365602, 4),
                "t" : NumberLong(2)
            },
            "optimeDate" : ISODate("2015-12-06T01:33:22Z"),
            "infoMessage" : "could not find member to sync from",
            "electionTime" : Timestamp(1449365602, 3),
            "electionDate" : ISODate("2015-12-06T01:33:22Z"),
            "configVersion" : 3,
            "self" : true
        },
        {
            "_id" : 2,
            "name" : "192.168.245.132:27017",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 2879,
            "optime" : {
                "ts" : Timestamp(1449365602, 4),
                "t" : NumberLong(2)
            },
            "optimeDate" : ISODate("2015-12-06T01:33:22Z"),
            "lastHeartbeat" : ISODate("2015-10-23T01:03:10.463Z"),
            "lastHeartbeatRecv" : ISODate("2015-10-23T01:03:09.290Z"),
            "pingMs" : NumberLong(0),
            "syncingTo" : "192.168.245.131:27017",
            "configVersion" : 3
        }
    ],
    "ok" : 1
}

2)四成员及以上的复制集群

mongodb允许在复制集群中添加更多的主机,当然需要考虑下面的问题:

  • 确保集群中有奇数个选举投票成员,如果有偶数个成员,那么可以部署一个仲裁成员,保证选举投票成员数量是奇数。
  • mongodb复制集群最多可以有50个成员,7个投票成员,如果已经达到7个投票成员了,如果再加入的主机必须为non-voting。
  • 集群中成员的位置。必须要保证集群成员数的绝大多数在一个数据中心,比如一共有5个成员,那么需要3个在同一数据中心。

3)基于地理位置的分布式复制架构

将一个复制集群部署到多个数据中心,能够保证系统的冗余并提高容错能力。即使有一个数据中心不可用了,另一个数据中心也可以继续提供服务。但是在另外的数据中心(这里我就称为第二数据中心)中的mongodb实例优先级必须设置为0,不让它接管主库。例如,下面是简单的基于地理位置的架构:

【六】MongoDB管理之副本集

如果主数据中心不可用,你可以手工从第二数据中心进行恢复数据集以最小的宕机时间。这也是为什么要保证一个数据中心中的成员数要超过大多数,这样才能保证投票数能够选择一个新主。

3、复制集群高可用

复制集群采用自动接管的方式保证系统的高可用。当主库不可用,会通过选举投票的方式选择一个新主。集群中各成员持有相同的数据集,但是其他方面都是独立的。在接管成为新主过程中,有些情况下接管进程也许会执行回滚。

  • 接管时如何选举的?

复制集群采用选举制来决定哪个成员成为主库。选举一般出现在两个时间点:一是复制集群初始化完成后;二是主库不可用时。在集群中,选举制是必不可少的独立操作,但是选举需要时间才能完成,在选举正在进行中,这个时候集群中没有主库且不能接收客户端的写请求,其他的成员这时都是只读状态(所以如果设置从库能读的话),系统的读服务不受影响。写服务会短暂无法操作。除非必要,Mongodb尽量避免选举发生。

影响投票条件:

心跳检测:每隔两秒,集群中成员会发送心跳(pings)到其他主机上,如果10秒内没有回应,那么认为该主机不可用。

优先级参数:优先级越高,越能够成为主库,为0时不能成为主库。在选举过程中,优先级低的成员有机会成为主库,如果出现这种情况,会继续投票选举,直到优先级高的成为主库。

网络分区:保证一个数据中心中的成员数要超过大多数

  • 在接管后回滚?

接管完成后,待原主库被修复重新加入集群中需要进行回滚写操作。只有当原主库接收了写操作且在宕机前从库没有成功应用时,回滚操作才是必须的操作。这样当它重新加入,才能够保证数据的一致性。

MongoDB努力避免回滚发生,因为这种情况很少出现,一般发生在网络分区的环境中。如果原主库在不可用之前写操作复制到其他任意从库上了且那个成员对于大多数成员是可访问的,那么将不会发生回滚。

【收集回滚数据】:

如果出现回滚,DBA必须决定是否应用这些回滚。MongoDB将回滚数据写到BSON文件中,具体位于数据目录的rollback/下,文件命名如下:

<database>.<collection>.<timestamp>.bson
records.accounts.--09T18--04.0.bson #比如这个文件名

在原主库进行了回滚并降级为从库后,DBA必须要应用这些回滚数据到新主库上。用bsondump可以读回滚文件内容,然后用mongorestore工具进行应用。

【避免回滚】:

对于一个复制集群,默认采用write concern {w: 1},如采用这个参数,那么在主库宕机后且写操作已复制到任一从库上时还是会进行回滚。为了避免这样,可以用w: majority write concern代替。

【回滚限制】:

mongd实例不会回滚超过300字节的数据,如果你需要回滚超过300字节的数据,那么必须手工介入。

4、副本集处理读写请求

1) 安全写级别(write concern)

在后台,无论mongodb运行在单独一台主机上还是在复制集群中,它对前端应用程序都是透明的。对于写操作,前端需要返回确认信息,是否真正写入成功了呢?对于复制集群中,默认的写确认仅仅发送到主库上,但是我们也可以修改以发送到更多的主机上。目前有两种方法:

  • 重载writeConcern方法:
db.products.insert(
{ item: "envelopes", qty : , type: "Clasp" },
{ writeConcern: { w: , wtimeout: } }
)

在插入数据时重载writeConcern方法,将w的值改为2,表示写确认发送到集群中两台主机上包括主库。

writeConcern方法参数说明:

{ w: <value>, j: <boolean>, wtimeout: <number> }

w:表示写操作的请求确认发送到mongod实例个数或者指定tag的mongod实例。具体有以下几个值:

0:表示不用写操作确认;

1:表示发送到单独一个mongod实例,对于复制集群环境,就发送到主库上;

大于1:表示发送到集群中实例的个数,但是不能超过集群个数,否则出现写一直阻塞;

majority:v3.2版本中,发送到集群中大多数节点,包括主库,并且必须写到本地硬盘的日志文件中,才算这次写入是成功的。

<tag set>:表示发送到指定tag的实例上;

j:表示写操作是否已经写入日志文件中,是boolean类型的。

wtimeout:确认请求的超时数,比如w设置10,但是集群一共才9个节点,那么就一直阻塞在那,通过设置超时数,避免写确认返回阻塞。

当写入操作很多时,每次都需要重载writeConcern方法,太麻烦了,那么可以参考下面的方法,修改配置。

  • 修改复制集群的配置:
cfg = rs.conf()
cfg.settings = {}
cfg.settings.getLastErrorDefaults = { w: "majority", wtimeout: }
rs.reconfig(cfg)

2)读安全级别和读选项

这里有两个概念,必须弄明白。

读安全级别(readConcern),这个是v3.2版本新引进的,允许客户端设置合适的读隔离级别。如下:

readConcern: { level: <majority|local> }

默认情况下,mongodb选择local作为读级别,它能够读取主库上最新的数据但是在没有回滚情况下(可能主库不可用时进行回滚)。你可以选择majority级别,这时能够保证数据已经写到多个节点上并不会回滚。

为了设置安全级别,可以在启动实例是指定--enableMajorityReadConcern参数。或者将replication.enableMajorityReadConcern配置到文件中。下面这些命令支持readConcern:

读选项描述了mongodb读请求的访问路径,默认下客户端的读请求直接发送到主库。那么有时我想做读写分离或者减轻主库的读写压力时,就需要把读请求分流到其他secondary库上,这个时候读选项就派上用处了。共有五种读选项模式:

Read Preference Mode Description
primary 默认的,读请求发送到主库
primaryPreferred 在多数情况下, 从主库上读,只有主库不可用了,这时从secondary库读。
secondary 从secondary库读
secondaryPreferred 在多数情况下, 从secondary库上读,只有secondary库都不可用了,这时从主库读。
nearest 从副本集中网络延时最少的库上读,这时不分主从库了。

这些参数可以在应用程序连接mongodb时通过函数指定或在shell中通过函数指定。具体如下:

在shell中:

db.collection.find().readPref( { mode: 'nearest'} )

在java程序中,连接到整个副本集,它不关心具体哪一台机器是主还是从,所以这时几个参数就发挥作用了。

  List<ServerAddress> addresses = new ArrayList<ServerAddress>();
ServerAddress address1 = new ServerAddress("192.168.245.129" , 27017);
ServerAddress address2 = new ServerAddress("192.168.245.131" , 27017);
ServerAddress address3 = new ServerAddress("192.168.245.132" , 27017);
addresses.add(address1);
addresses.add(address2);
addresses.add(address3);
MongoClient client = new MongoClient(addresses);
DB db = client.getDB( "test" );
DBCollection coll = db.getCollection( "testdb" ); BasicDBObject object = new BasicDBObject();
object.append( "test2" , "testval2" ); //读操作从副本节点读取
ReadPreference preference = ReadPreference.secondary();
DBObject dbObject = coll.findOne(object, null , preference); System. out .println(dbObject);

读选项值为secondary的架构如下,也就是常说的读写分离,当然这种模式不能保证读到的数据是最新的,因为从库也许会有一定的延时。

【六】MongoDB管理之副本集

以上五种参数要根据应用需求来设置,不可盲目指定,一般建议如下:

  • 追求数据一致性最大化:

采用primary读选项和majority安全读级别,但是当主库不可用时,如果大多数成员也不可用时,就会报错。还可以禁用自动故障切换,这样会牺牲系统可用性。

  • 追求系统可用性最大化:

采用primaryPreferred读选项,但是会增加主库的读写压力。

  • 追求最小延时:为了总是读到延时小的节点,可以采用nearest。

3)读选项执行过程

当我们选择一个非主键的读选项时,mongodb驱动程序是通过如下几个过程决定从一个成员上读:

  • 收集可用的成员,分析他们的类型(主库,从库等)
  • 如果设置了标签集,排除不满足的成员
  • 判断哪些成员离客户端最近(网络延时短)
  • 在上面最近的成员中ping距离为毫秒的成员建立列表
  • 从这些列表主机中随机选择一个成员进行读操作

5、复制过程

副本集中的成员是连续不断的复制数据,首先,成员用initial sync铺捉数据集的变化,然后连续不断的记录和应用这些变化的数据。每个成员都把数据变化记录到oplog中,oplog实际上是个封顶集合。

1)副本集的oplog

oplog(operation log)是一种特别的封顶集合,当每次修改数据库数据时,它会滚动的记录数据变化的操作。MongoDB在主库上应用数据操作并记录到oplog中,然后从库复制oplog并以单线程方式应用这些操作。所有副本集成员在local.oplog.rs集合中都持有一份rplog的副本,以此来维护数据库的状态。所有副本集成员都会向其他成员发送心跳检测,任何成员都可以从其他成员那导入oplog条目。

  • oplog文件大小

当你第一次启动成员时,就默认指定了日志文件大小,一般是操作系统硬盘可用空间的5%,1G-50G.一般而言,这个文件大小都足够用了,但是你也可以修改这个文件大小,具体参考官方文档。

  • 需要大oplog的情况
#一次更新很多文档,oplog为了遵循幂等性,需要将多个更新转换为单独的操作,这个占用空间非常多。
#删除的数据等于插入的数量
#大量的原位更新,就是没有改变硬盘数据的大小,但是需要记录很多日志操作。
  • oplog状态
rs0:PRIMARY> rs.printReplicationInfo()
configured oplog size: .7419919967651MB
log length start to end: 6028secs (.67hrs)
oplog first event time: Sun Dec :: GMT+ (CST)
oplog last event time: Sun Dec :: GMT+ (CST)
now: Sat Oct :: GMT+ (CST)
rs0:PRIMARY>

2)数据同步

为了保持各副本成员拥有最新的数据,副本集里secondary成员通过sync或复制其他成员的数据。MongoDB提供两种方式进行secondary成员间的数据同步:

  • 初始化同步(Initial Sync)

初始化同步会拷贝所有的数据到另一个成员那里,一般当这个成员是新加入的没有数据或者有数据但是丢失了部分历史数据会使用初始化同步。当进行初始化同步操作时,MongoDB会:

、克隆源成员所有数据库,为了克隆数据库,mongod会查询所有源成员中的集合并将这些数据插入到源成员的副本中,包括建立_id索引。克隆进程只是克隆合法的数据,忽略无效的数据
、到目标成员上应用克隆的rplog副本
、给所有集合建立索引
  • 复制(replication)

当初始化同步完成后,成员会从初始同步源成员那连续的复制oplog,并采用异步的方式应用这些数据操作。一般情况下,都是从主库进行同步。当然,副本成员也会自动的改变源成员,为了能同步,两个成员之间members[n].buildIndexes参数值必须一样。另外,副本成员不会从延迟类型和隐藏类型成员那同步数据。

6、主从复制

对于新的应用,MongoDB建议采用副本集而不是主从复制。以前都是采用主从复制的,后来引入了副本集,这里就不介绍了,请查看官方文档。

注意:由于本人翻译水平有限,有时无法分开副本集和主从复制专业术语的区别,抱歉!本文中都是指副本集