- 从节点每个上面的数据都是对数据库全量拷贝,从节点压力会不会过大?
- 数据压力大到机器支撑不了的时候能否做到自动扩展?
在系统早期,数据量还小的时候不会引起太大的问题,但是随着数据量持续增多,后续迟早会出现一台机器硬件瓶颈问题的。而mongodb主打的就是海量数据架构,他不能解决海量数据怎么行!不行!“分片”就用这个来解决这个问题。
传统数据库怎么做海量数据读写?其实一句话概括:分而治之。上图看看就清楚了,如下 taobao岳旭强在infoq中提到的 架构图:
上图中有个TDDL,是taobao的一个数据访问层组件,他主要的作用是SQL解析、路由处理。根据应用的请求的功能解析当前访问的sql判断是在哪个业务数据库、哪个表访问查询并返回数据结果。具体如图:
说了这么多传统数据库的架构,那Nosql怎么去做到了这些呢?mysql要做到自动扩展需要加一个数据访问层用程序去扩展,数据库的增加、删除、备份还需要程序去控制。一但数据库的节点一多,要维护起来也是非常头疼的。不过mongodb所有的这一切通过他自己的内部机制就可以搞定!顿时石化了,这么牛X!还是上图看看mongodb通过哪些机制实现路由、分片:
从图中可以看到有四个组件:mongos、config server、shard、replica set。
mongos,数据库集群请求的入口,所有的请求都通过mongos进行协调,不需要在应用程序添加一个路由选择器,mongos自己就是一个请求分发中心,它负责把对应的数据请求请求转发到对应的shard服务器上。在生产环境通常有多mongos作为请求的入口,防止其中一个挂掉所有的mongodb请求都没有办法操作。
config server,顾名思义为配置服务器,存储所有数据库元信息(路由、分片)的配置。mongos本身没有物理存储分片服务器和数据路由信息,只是缓存在内存里,配置服务器则实际存储这些数据。mongos第一次启动或者关掉重启就会从 config server 加载配置信息,以后如果配置服务器信息变化会通知到所有的 mongos 更新自己的状态,这样 mongos 就能继续准确路由。在生产环境通常有多个 config server 配置服务器,因为它存储了分片路由的元数据,这个可不能丢失!就算挂掉其中一台,只要还有存货, mongodb集群就不会挂掉。
shard,这就是传说中的分片了。上面提到一个机器就算能力再大也有天花板,就像军队打仗一样,一个人再厉害喝血瓶也拼不过对方的一个师。俗话说三个臭皮匠顶个诸葛亮,这个时候团队的力量就凸显出来了。在互联网也是这样,一台普通的机器做不了的多台机器来做,如下图:
一台机器的一个数据表 Collection1 存储了 1T 数据,压力太大了!在分给4个机器后,每个机器都是256G,则分摊了集中在一台机器的压力。也许有人问一台机器硬盘加大一点不就可以了,为什么要分给四台机器呢?不要光想到存储空间,实际运行的数据库还有硬盘的读写、网络的IO、CPU和内存的瓶颈。在mongodb集群只要设置好了分片规则,通过mongos操作数据库就能自动把对应的数据操作请求转发到对应的分片机器上。在生产环境中分片的片键可要好好设置,这个影响到了怎么把数据均匀分到多个分片机器上,不要出现其中一台机器分了1T,其他机器没有分到的情况,这样还不如不分片!
replica set,上两节已经详细讲过了这个东东,怎么这里又来凑热闹!其实上图4个分片如果没有 replica set 是个不完整架构,假设其中的一个分片挂掉那四分之一的数据就丢失了,所以在高可用性的分片架构还需要对于每一个分片构建 replica set 副本集保证分片的可靠性。生产环境通常是 2个副本 + 1个仲裁。
说了这么多,还是来实战一下如何搭建高可用的mongodb集群:
首先确定各个组件的数量,mongos 3个, config server 3个,数据分3片 shard server 3个,每个shard 有一个副本一个仲裁也就是 3 * 2 = 6 个,总共需要部署15个实例。这些实例可以部署在独立机器也可以部署在一台机器,我们这里测试资源有限,只准备了 3台机器,在同一台机器只要端口不同就可以,看一下物理部署图:
架构搭好了,安装软件!
- 1、准备机器,IP分别设置为: 192.168.0.136、192.168.0.137、192.168.0.138。
- 2、分别在每台机器上建立mongodb分片对应测试文件夹。
1
2
|
mkdir
-p
/data/mongodbtest
|
- 3、下载mongodb的安装程序包
1
|
wget http:
//fastdl
.mongodb.org
/linux/mongodb-linux-x86_64-2
.4.8.tgz
|
1
2
|
tar
xvzf mongodb-linux-x86_64-2.4.8.tgz
|
- 4、分别在每台机器建立mongos 、config 、 shard1 、shard2、shard3 五个目录。
因为mongos不存储数据,只需要建立日志文件目录即可。
1
2
|
mkdir
-p
/data/mongodbtest/mongos/log
|
1
2
|
mkdir
-p
/data/mongodbtest/config/data
|
1
2
|
mkdir
-p
/data/mongodbtest/config/log
|
1
2
|
mkdir
-p
/data/mongodbtest/mongos/log
|
1
2
|
mkdir
-p
/data/mongodbtest/shard1/data
|
1
2
|
mkdir
-p
/data/mongodbtest/shard1/log
|
1
2
|
mkdir
-p
/data/mongodbtest/shard2/data
|
1
2
|
mkdir
-p
/data/mongodbtest/shard2/log
|
1
2
|
mkdir
-p
/data/mongodbtest/shard3/data
|
1
2
|
mkdir
-p
/data/mongodbtest/shard3/log
|
- 5、规划5个组件对应的端口号,由于一个机器需要同时部署 mongos、config server 、shard1、shard2、shard3,所以需要用端口进行区分。
这个端口可以自由定义,在本文 mongos为 20000, config server 为 21000, shard1为 22001 , shard2为22002, shard3为22003.
- 6、在每一台服务器分别启动配置服务器。
1
|
/data/mongodbtest/mongodb-linux-x86_64-2
.4.8
/bin/mongod
--configsvr --dbpath
/data/mongodbtest/config/data
--port 21000 --logpath
/data/mongodbtest/config/log/config
.log --fork
|
- 7、在每一台服务器分别启动mongos服务器。
1
|
/data/mongodbtest/mongodb-linux-x86_64-2
.4.8
/bin/mongos
--configdb 192.168.0.136:21000,192.168.0.137:21000,192.168.0.138:21000 --port 20000 --logpath
/data/mongodbtest/mongos/log/mongos
.log --fork
|
- 8、配置各个分片的副本集。
1
2
|
/data/mongodbtest/mongodb-linux-x86_64-2
.4.8
/bin/mongod
--shardsvr --replSet shard1 --port 22001 --dbpath
/data/mongodbtest/shard1/data
--logpath
/data/mongodbtest/shard1/log/shard1
.log --fork --nojournal --oplogSize 10
|
为了快速启动并节约测试环境存储空间,这里加上 nojournal 是为了关闭日志信息,在我们的测试环境不需要初始化这么大的redo日志。同样设置 oplogsize是为了降低 local 文件的大小,oplog是一个固定长度的 capped collection,它存在于”local”数据库中,用于记录Replica Sets操作日志。注意,这里的设置是为了测试!
1
2
|
/data/mongodbtest/mongodb-linux-x86_64-2
.4.8
/bin/mongod
--shardsvr --replSet shard2 --port 22002 --dbpath
/data/mongodbtest/shard2/data
--logpath
/data/mongodbtest/shard2/log/shard2
.log --fork --nojournal --oplogSize 10
|
1
2
|
/data/mongodbtest/mongodb-linux-x86_64-2
.4.8
/bin/mongod
--shardsvr --replSet shard3 --port 22003 --dbpath
/data/mongodbtest/shard3/data
--logpath
/data/mongodbtest/shard3/log/shard3
.log --fork --nojournal --oplogSize 10
|
分别对每个分片配置副本集,深入了解副本集参考本系列前几篇文章。
任意登陆一个机器,比如登陆192.168.0.136,连接mongodb
1
2
|
/data/mongodbtest/mongodb-linux-x86_64-2
.4.8
/bin/mongo
127.0.0.1:22001
|
1
2
3
4
5
6
7
|
config = { _id:
"shard1"
, members:[
{_id:0,host:
"192.168.0.136:22001"
},
{_id:1,host:
"192.168.0.137:22001"
},
{_id:2,host:
"192.168.0.138:22001"
,arbiterOnly:
true
}
]
}
|
1
2
|
/data/mongodbtest/mongodb-linux-x86_64-2
.4.8
/bin/mongo
127.0.0.1:22002
|
1
2
3
4
5
6
7
|
config = { _id:
"shard2"
, members:[
{_id:0,host:
"192.168.0.136:22002"
},
{_id:1,host:
"192.168.0.137:22002"
},
{_id:2,host:
"192.168.0.138:22002"
,arbiterOnly:
true
}
]
}
|
1
2
|
/data/mongodbtest/mongodb-linux-x86_64-2
.4.8
/bin/mongo
127.0.0.1:22003
|
1
2
3
4
5
6
7
|
config = { _id:
"shard3"
, members:[
{_id:0,host:
"192.168.0.136:22003"
},
{_id:1,host:
"192.168.0.137:22003"
},
{_id:2,host:
"192.168.0.138:22003"
,arbiterOnly:
true
}
]
}
|
- 9、目前搭建了mongodb配置服务器、路由服务器,各个分片服务器,不过应用程序连接到 mongos 路由服务器并不能使用分片机制,还需要在程序里设置分片配置,让分片生效。
1
2
|
/data/mongodbtest/mongodb-linux-x86_64-2
.4.8
/bin/mongo
127.0.0.1:20000
|
1
2
|
db.runCommand( { addshard :
"shard1/192.168.0.136:22001,192.168.0.137:22001,192.168.0.138:22001"
});
|
如里shard是单台服务器,用 db.runCommand( { addshard : “[: ]” } )这样的命令加入,如果shard是副本集,用db.runCommand( { addshard : “replicaSetName/[:port][,serverhostname2[:port],…]” });这样的格式表示 。
1
2
|
db.runCommand( { addshard :
"shard2/192.168.0.136:22002,192.168.0.137:22002,192.168.0.138:22002"
});
|
1
2
|
db.runCommand( { addshard :
"shard3/192.168.0.136:22003,192.168.0.137:22003,192.168.0.138:22003"
});
|
1
2
|
db.runCommand( { listshards : 1 } );
|
#内容输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
{
"shards" : [
{
"_id" : "shard1",
"host" : "shard1/192.168.0.136:22001,192.168.0.137:22001"
},
{
"_id" : "shard2",
"host" : "shard2/192.168.0.136:22002,192.168.0.137:22002"
},
{
"_id" : "shard3",
"host" : "shard3/192.168.0.136:22003,192.168.0.137:22003"
}
],
"ok" : 1
}
|
因为192.168.0.138是每个分片副本集的仲裁节点,所以在上面结果没有列出来。
- 10、目前配置服务、路由服务、分片服务、副本集服务都已经串联起来了,但我们的目的是希望插入数据,数据能够自动分片,就差那么一点点,一点点。。。
连接在mongos上,准备让指定的数据库、指定的集合分片生效。
1
2
|
db.runCommand( { enablesharding :
"testdb"
});
|
1
2
|
db.runCommand( { shardcollection :
"testdb.table1"
,key : {
id
: 1} } )
|
我们设置testdb的 table1 表需要分片,根据 id 自动分片到 shard1 ,shard2,shard3 上面去。要这样设置是因为不是所有mongodb 的数据库和表 都需要分片!
- 11、测试分片配置结果。
1
2
|
/data/mongodbtest/mongodb-linux-x86_64-2
.4.8
/bin/mongo
127.0.0.1:20000
|
1
2
3
|
for
(var i = 1; i <= 100000; i++)
db.table1.save({
id
:i,
"test1"
:
"testval1"
});
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
{
"sharded"
:
true
,
"ns"
:
"testdb.table1"
,
"count"
:
100000
,
"numExtents"
:
13
,
"size"
:
5600000
,
"storageSize"
:
22372352
,
"totalIndexSize"
:
6213760
,
"indexSizes"
: {
"_id_"
:
3335808
,
"id_1"
:
2877952
},
"avgObjSize"
:
56
,
"nindexes"
:
2
,
"nchunks"
:
3
,
"shards"
: {
"shard1"
: {
"ns"
:
"testdb.table1"
,
"count"
:
42183
,
"size"
:
0
,
...
"ok"
:
1
},
"shard2"
: {
"ns"
:
"testdb.table1"
,
"count"
:
38937
,
"size"
:
2180472
,
...
"ok"
:
1
},
"shard3"
: {
"ns"
:
"testdb.table1"
,
"count"
:
18880
,
"size"
:
3419528
,
...
"ok"
:
1
}
},
"ok"
:
1
}
|
可以看到数据分到3个分片,各自分片数量为: shard1 “count” : 42183,shard2 “count” : 38937,shard3 “count” : 18880。已经成功了!不过分的好像不是很均匀,所以这个分片还是很有讲究的,后续再深入讨论。
- 12、java程序调用分片集群,因为我们配置了三个mongos作为入口,就算其中哪个入口挂掉了都没关系,使用集群客户端程序如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public
class
TestMongoDBShards {
public
static
void
main(String[] args) {
try
{
List<ServerAddress> addresses =
new
ArrayList<ServerAddress>();
ServerAddress address1 =
new
ServerAddress(
"192.168.0.136"
,
20000
);
ServerAddress address2 =
new
ServerAddress(
"192.168.0.137"
,
20000
);
ServerAddress address3 =
new
ServerAddress(
"192.168.0.138"
,
20000
);
addresses.add(address1);
addresses.add(address2);
addresses.add(address3);
MongoClient client =
new
MongoClient(addresses);
DB db = client.getDB(
"testdb"
);
DBCollection coll = db.getCollection(
"table1"
);
BasicDBObject object =
new
BasicDBObject();
object.append(
"id"
,
1
);
DBObject dbObject = coll.findOne(object);
System. out .println(dbObject);
}
catch
(Exception e) {
e.printStackTrace();
}
}
}
|
整个分片集群搭建完了,思考一下我们这个架构是不是足够好呢?其实还有很多地方需要优化,比如我们把所有的仲裁节点放在一台机器,其余两台机器承担了全部读写操作,但是作为仲裁的192.168.0.138相当空闲。让机器3 192.168.0.138多分担点责任吧!架构可以这样调整,把机器的负载分的更加均衡一点,每个机器既可以作为主节点、副本节点、仲裁节点,这样压力就会均衡很多了,如图:
当然生产环境的数据远远大于当前的测试数据,大规模数据应用情况下我们不可能把全部的节点像这样部署,硬件瓶颈是硬伤,只能扩展机器。要用好mongodb还有很多机制需要调整,不过通过这个东东我们可以快速实现高可用性、高扩展性,所以它还是一个非常不错的Nosql组件。
再看看我们使用的mongodb java 驱动客户端 MongoClient(addresses),这个可以传入多个mongos 的地址作为mongodb集群的入口,并且可以实现自动故障转移,但是负载均衡做的好不好呢?打开源代码查看:
它的机制是选择一个ping 最快的机器来作为所有请求的入口,如果这台机器挂掉会使用下一台机器。那这样。。。。肯定是不行的!万一出现双十一这样的情况所有请求集中发送到这一台机器,这台机器很有可能挂掉。一但挂掉了,按照它的机制会转移请求到下台机器,但是这个压力总量还是没有减少啊!下一台还是可能崩溃,所以这个架构还有漏洞!不过这个文章已经太长了,后续解决吧。
参考:
http://docs.mongodb.org/manual/core/sharding-introduction/
Posted in mongodb, 技术, 架构.
在上一篇文章《搭建高可用mongodb集群(二)—— 副本集》 介绍了副本集的配置,这篇文章深入研究一下副本集的内部机制。还是带着副本集的问题来看吧!
- 副本集故障转移,主节点是如何选举的?能否手动干涉下架某一台主节点。
- 官方说副本集数量最好是奇数,为什么?
- mongodb副本集是如何同步的?如果同步不及时会出现什么情况?会不会出现不一致性?
- mongodb的故障转移会不会无故自动发生?什么条件会触发?频繁触发可能会带来系统负载加重?
Bully算法 mongodb副本集故障转移功能得益于它的选举机制。选举机制采用了Bully算法,可以很方便从分布式节点中选出主节点。一个分布式集群架构中一般都有一个所谓的主节点,可以有很多用途,比如缓存机器节点元数据,作为集群的访问入口等等。主节点有就有吧,我们干嘛要什么Bully算法?要明白这个我们先看看这两种架构:
-
指定主节点的架构,这种架构一般都会申明一个节点为主节点,其他节点都是从节点,如我们常用的mysql就是这样。但是这样架构我们在第一节说了整个集群如果主节点挂掉了就得手工操作,上架一个新的主节点或者从从节点恢复数据,不太灵活。
-
不指定主节点,集群中的任意节点都可以成为主节点。mongodb也就是采用这种架构,一但主节点挂了其他从节点自动接替变成主节点。如下图:
好了,问题就在这个地方,既然所有节点都是一样,一但主节点挂了,怎么选择出来下一个节点是谁来做为主节点呢?这就是Bully算法解决的问题。
那什么是Bully算法,Bully算法是一种协调者(主节点)竞选算法,主要思想是集群的每个成员都可以声明它是主节点并通知其他节点。别的节点可以选择接受这个声称或是拒绝并进入主节点竞争。被其他所有节点接受的节点才能成为主节点。节点按照一些属性来判断谁应该胜出。这个属性可以是一个静态ID,也可以是更新的度量像最近一次事务ID(最新的节点会胜出)。详情请参考NoSQL数据库分布式算法的协调者竞选还有维基百科的解释 。
选举 那mongodb是怎进行选举的呢?官方这么描述:
We use a consensus protocol to pick a primary. Exact details will be spared here but that basic process is:
- get maxLocalOpOrdinal from each server.
- if a majority of servers are not up (from this server’s POV), remain in Secondary mode and stop.
- if the last op time seems very old, stop and await human intervention.
- else, using a consensus protocol, pick the server with the highest maxLocalOpOrdinal as the Primary.
大致翻译过来为使用一致协议选择主节点。基本步骤为:
- 得到每个服务器节点的最后操作时间戳。每个mongodb都有oplog机制会记录本机的操作,方便和主服务器进行对比数据是否同步还可以用于错误恢复。
- 如果集群中大部分服务器down机了,保留活着的节点都为 secondary状态并停止,不选举了。
- 如果集群中选举出来的主节点或者所有从节点最后一次同步时间看起来很旧了,停止选举等待人来操作。
- 如果上面都没有问题就选择最后操作时间戳最新(保证数据是最新的)的服务器节点作为主节点。
这里提到了一个一致协议(其实就是bully算法),这个和数据库的一致性协议还是有些区别,一致协议主要强调的是通过一些机制保证大家达成共识;而一致性协议强调的是操作的顺序一致性,比如同时读写一个数据会不会出现脏数据。一致协议在分布式里有一个经典的算法叫“Paxos算法”,后续再介绍。
上面有个问题,就是所有从节点的最后操作时间都是一样怎么办?就是谁先成为主节点的时间最快就选谁。
选举触发条件 选举不是什么时刻都会被触发的,有以下情况可以触发。
- 初始化一个副本集时。
- 副本集和主节点断开连接,可能是网络问题。
- 主节点挂掉。
选举还有个前提条件,参与选举的节点数量必须大于副本集总节点数量的一半,如果已经小于一半了所有节点保持只读状态。
日志将会出现:
can't see a majority of the set, relinquishing primary
主节点挂掉能否人为干预?答案是肯定的。
- 可以通过replSetStepDown命令下架主节点。这个命令可以登录主节点使用
db.adminCommand({replSetStepDown : 1})
如果杀不掉可以使用强制开关
db.adminCommand({replSetStepDown : 1, force : true})
或者使用 rs.stepDown(120)也可以达到同样的效果,中间的数字指不能在停止服务这段时间成为主节点,单位为秒。
- 设置一个从节点有比主节点有更高的优先级。
先查看当前集群中优先级,通过rs.conf()命令,默认优先级为1是不显示的,这里标示出来。
{
"_id" : "rs0",
"version" : 9,
"members" : [
{
"_id" : 0,
"host" : "192.168.1.136:27017" },
{
"_id" : 1,
"host" : "192.168.1.137:27017" },
{
"_id" : 2,
"host" : "192.168.1.138:27017" }
]
}
我们来设置,让id为1的主机可以优先成为主节点。
cfg = rs.conf()
cfg.members[0].priority = 1
cfg.members[1].priority = 2
cfg.members[2].priority = 1
rs.reconfig(cfg)
然后再执行rs.conf()命令查看优先级已经设置成功,主节点选举也会触发。
{
"_id" : "rs0",
"version" : 9,
"members" : [
{
"_id" : 0,
"host" : "192.168.1.136:27017" },
{
"_id" : 1,
"host" : "192.168.1.137:27017",
"priority" : 2
},
{
"_id" : 2,
"host" : "192.168.1.138:27017" }
]
}
如果不想让一个从节点成为主节点可以怎么操作?
a、使用rs.freeze(120)冻结指定的秒数不能选举成为主节点。
b、按照上一篇设置节点为Non-Voting类型。
-
当主节点不能和大部分从节点通讯。把主机节点网线拔掉,嘿嘿:)
优先级还可以这么用,如果我们不想设置什么hidden节点,就用secondary类型作为备份节点也不想让他成为主节点怎么办?看下图,共三个节点分布在两个数据中心,数据中心2的节点设置优先级为0不能成为主节点,但是可以参与选举、数据复制。架构还是很灵活吧!
奇数 官方推荐副本集的成员数量为奇数,最多12个副本集节点,最多7个节点参与选举。最多12个副本集节点是因为没必要一份数据复制那么多份,备份太多反而增加了网络负载和拖慢了集群性能;而最多7个节点参与选举是因为内部选举机制节点数量太多就会导致1分钟内还选不出主节点,凡事只要适当就好。这个“12”、“7”数字还好,通过他们官方经过性能测试定义出来可以理解。具体还有哪些限制参考官方文档《 MongoDB Limits and Thresholds 》。 但是这里一直没搞懂整个集群为什么要奇数,通过测试集群的数量为偶数也是可以运行的,参考这个文章http://www.itpub.net/thread-1740982-1-1.html。后来突然看了一篇stackoverflow的文章终于顿悟了,mongodb本身设计的就是一个可以跨IDC的分布式数据库,所以我们应该把它放到大的环境来看。
假设四个节点被分成两个IDC,每个IDC各两台机器,如下图。但这样就出现了个问题,如果两个IDC网络断掉,这在广域网上很容易出现的问题,在上面选举中提到只要主节点和集群中大部分节点断开链接就会开始一轮新的选举操作,不过mongodb副本集两边都只有两个节点,但是选举要求参与的节点数量必须大于一半,这样所有集群节点都没办法参与选举,只会处于只读状态。但是如果是奇数节点就不会出现这个问题,假设3个节点,只要有2个节点活着就可以选举,5个中的3个,7个中的4个。。。
心跳 综上所述,整个集群需要保持一定的通信才能知道哪些节点活着哪些节点挂掉。mongodb节点会向副本集中的其他节点每两秒就会发送一次pings包,如果其他节点在10秒钟之内没有返回就标示为不能访问。每个节点内部都会维护一个状态映射表,表明当前每个节点是什么角色、日志时间戳等关键信息。如果是主节点,除了维护映射表外还需要检查自己能否和集群中内大部分节点通讯,如果不能则把自己降级为secondary只读节点。
同步,副本集同步分为初始化同步和keep复制。初始化同步指全量从主节点同步数据,如果主节点数据量比较大同步时间会比较长。而keep复制指初始化同步过后,节点之间的实时同步一般是增量同步。初始化同步不只是在第一次才会被处罚,有以下两种情况会触发:
- secondary第一次加入,这个是肯定的。
- secondary落后的数据量超过了oplog的大小,这样也会被全量复制。
那什么是oplog的大小?前面说过oplog保存了数据的操作记录,secondary复制oplog并把里面的操作在secondary执行一遍。但是oplog也是mongodb的一个集合,保存在local.oplog.rs里,但是这个oplog是一个capped collection也就是固定大小的集合,新数据加入超过集合的大小会覆盖。所以这里需要注意,跨IDC的复制要设置合适的oplogSize,避免在生产环境经常产生全量复制。oplogSize 可以通过–oplogSize设置大小,对于linux 和windows 64位,oplog size默认为剩余磁盘空间的5%。
同步也并非只能从主节点同步,假设集群中3个节点,节点1是主节点在IDC1,节点2、节点3在IDC2,初始化节点2、节点3会从节点1同步数据。后面节点2、节点3会使用就近原则从当前IDC的副本集中进行复制,只要有一个节点从IDC1的节点1复制数据。
设置同步还要注意以下几点:
- secondary不会从delayed和hidden成员上复制数据。
- 只要是需要同步,两个成员的buildindexes必须要相同无论是否是true和false。buildindexes主要用来设置是否这个节点的数据用于查询,默认为true。
- 如果同步操作30秒都没有反应,则会重新选择一个节点进行同步。
到此,本章前面提到的问题全部解决了,不得不说mongodb的设计还真是强大!
后续继续解决上一节这几个问题:
- 主节点挂了能否自动切换连接?目前需要手工切换。
- 主节点的读写压力过大如何解决?
还有这两个问题后续解决:
- 从节点每个上面的数据都是对数据库全量拷贝,从节点压力会不会过大?
- 数据压力大到机器支撑不了的时候能否做到自动扩展?
Posted in mongodb, 技术.
在上一篇文章《搭建高可用MongoDB集群(一)——配置MongoDB》 提到了几个问题还没有解决。
- 主节点挂了能否自动切换连接?目前需要手工切换。
- 主节点的读写压力过大如何解决?
- 从节点每个上面的数据都是对数据库全量拷贝,从节点压力会不会过大?
- 数据压力大到机器支撑不了的时候能否做到自动扩展?
这篇文章看完这些问题就可以搞定了。NoSQL的产生就是为了解决大数据量、高扩展性、高性能、灵活数据模型、高可用性。但是光通过主从模式的架构远远达不到上面几点,由此MongoDB设计了副本集和分片的功能。这篇文章主要介绍副本集:
mongoDB官方已经不建议使用主从模式了,替代方案是采用副本集的模式,点击查看 ,如图:
那什么是副本集呢?打魔兽世界总说打副本,其实这两个概念差不多一个意思。游戏里的副本是指玩家集中在高峰时间去一个场景打怪,会出现玩家暴多怪物少的情况,游戏开发商为了保证玩家的体验度,就为每一批玩家单独开放一个同样的空间同样的数量的怪物,这一个复制的场景就是一个副本,不管有多少个玩家各自在各自的副本里玩不会互相影响。 mongoDB的副本也是这个,主从模式其实就是一个单副本的应用,没有很好的扩展性和容错性。而副本集具有多个副本保证了容错性,就算一个副本挂掉了还有很多副本存在,并且解决了上面第一个问题“主节点挂掉了,整个集群内会自动切换”。难怪mongoDB官方推荐使用这种模式。我们来看看mongoDB副本集的架构图:
由图可以看到客户端连接到整个副本集,不关心具体哪一台机器是否挂掉。主服务器负责整个副本集的读写,副本集定期同步数据备份,一但主节点挂掉,副本节点就会选举一个新的主服务器,这一切对于应用服务器不需要关心。我们看一下主服务器挂掉后的架构:
副本集中的副本节点在主节点挂掉后通过心跳机制检测到后,就会在集群内发起主节点的选举机制,自动选举一位新的主服务器。看起来很牛X的样子,我们赶紧操作部署一下!
官方推荐的副本集机器数量为至少3个,那我们也按照这个数量配置测试。
1、准备两台机器 192.168.1.136、192.168.1.137、192.168.1.138。 192.168.1.136 当作副本集主节点,192.168.1.137、192.168.1.138作为副本集副本节点。
2、分别在每台机器上建立mongodb副本集测试文件夹
1
2
3
4
5
6
7
8
|
mkdir
-p
/data/mongodbtest/replset
mkdir
-p
/data/mongodbtest/replset/data
cd
/data/mongodbtest
|
3、下载mongodb的安装程序包
1
|
wget http:
//fastdl
.mongodb.org
/linux/mongodb-linux-x86_64-2
.4.8.tgz
|
注意linux生产环境不能安装32位的mongodb,因为32位受限于操作系统最大2G的文件限制。
1
2
|
tar
xvzf mongodb-linux-x86_64-2.4.8.tgz
|
4、分别在每台机器上启动mongodb
1
|
/data/mongodbtest/mongodb-linux-x86_64-2
.4.8
/bin/mongod
--dbpath
/data/mongodbtest/replset/data
--replSet repset
|
可以看到控制台上显示副本集还没有配置初始化信息。
1
2
|
Sun Dec 29 20:12:02.953 [rsStart] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)
Sun Dec 29 20:12:02.953 [rsStart] replSet info you may need to run replSetInitiate -- rs.initiate() in the shell -- if that is not already done
|
5、初始化副本集
在三台机器上任意一台机器登陆mongodb
1
2
3
4
|
/data/mongodbtest/mongodb-linux-x86_64-2
.4.8
/bin/mongo
use admin
|
#定义副本集配置变量,这里的 _id:”repset” 和上面命令参数“ –replSet repset” 要保持一样。
1
2
3
4
5
|
config = { _id:
"repset"
, members:[
... {_id:0,host:
"192.168.1.136:27017"
},
... {_id:1,host:
"192.168.1.137:27017"
},
... {_id:2,host:
"192.168.1.138:27017"
}]
... }
|
#输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
{
"_id" : "repset",
"members" : [
{
"_id" : 0,
"host" : "192.168.1.136:27017"
},
{
"_id" : 1,
"host" : "192.168.1.137:27017"
},
{
"_id" : 2,
"host" : "192.168.1.138:27017"
}
]
}
|
#输出成功
1
2
3
4
|
{
"info" : "Config now saved locally. Should come online in about a minute.",
"ok" : 1
}
|
#查看日志,副本集启动成功后,138为主节点PRIMARY,136、137为副本节点SECONDARY。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
Sun Dec 29 20:26:13.842 [conn3] replSet replSetInitiate admin command received from client
Sun Dec 29 20:26:13.842 [conn3] replSet replSetInitiate config object parses ok, 3 members specified
Sun Dec 29 20:26:13.847 [conn3] replSet replSetInitiate all members seem up
Sun Dec 29 20:26:13.848 [conn3] ******
Sun Dec 29 20:26:13.848 [conn3] creating replication oplog of size: 990MB...
Sun Dec 29 20:26:13.849 [FileAllocator] allocating new datafile /data/mongodbtest/replset/data/local.1, filling with zeroes...
Sun Dec 29 20:26:13.862 [FileAllocator] done allocating datafile /data/mongodbtest/replset/data/local.1, size: 1024MB, took 0.012 secs
Sun Dec 29 20:26:13.863 [conn3] ******
Sun Dec 29 20:26:13.863 [conn3] replSet info saving a newer config version to local.system.replset
Sun Dec 29 20:26:13.864 [conn3] replSet saveConfigLocally done
Sun Dec 29 20:26:13.864 [conn3] replSet replSetInitiate config now saved locally. Should come online in about a minute.
Sun Dec 29 20:26:23.047 [rsStart] replSet I am 192.168.1.138:27017
Sun Dec 29 20:26:23.048 [rsStart] replSet STARTUP2
Sun Dec 29 20:26:23.049 [rsHealthPoll] replSet member 192.168.1.137:27017 is up
Sun Dec 29 20:26:23.049 [rsHealthPoll] replSet member 192.168.1.136:27017 is up
Sun Dec 29 20:26:24.051 [rsSync] replSet SECONDARY
Sun Dec 29 20:26:25.053 [rsHealthPoll] replset info 192.168.1.136:27017 thinks that we are down
Sun Dec 29 20:26:25.053 [rsHealthPoll] replSet member 192.168.1.136:27017 is now in state STARTUP2
Sun Dec 29 20:26:25.056 [rsMgr] not electing self, 192.168.1.136:27017 would veto with 'I don't think 192.168.1.138:27017 is electable'
Sun Dec 29 20:26:31.059 [rsHealthPoll] replset info 192.168.1.137:27017 thinks that we are down
Sun Dec 29 20:26:31.059 [rsHealthPoll] replSet member 192.168.1.137:27017 is now in state STARTUP2
Sun Dec 29 20:26:31.062 [rsMgr] not electing self, 192.168.1.137:27017 would veto with 'I don't think 192.168.1.138:27017 is electable'
Sun Dec 29 20:26:37.074 [rsMgr] replSet info electSelf 2
Sun Dec 29 20:26:38.062 [rsMgr] replSet PRIMARY
Sun Dec 29 20:26:39.071 [rsHealthPoll] replSet member 192.168.1.137:27017 is now in state RECOVERING
Sun Dec 29 20:26:39.075 [rsHealthPoll] replSet member 192.168.1.136:27017 is now in state RECOVERING
Sun Dec 29 20:26:42.201 [slaveTracking] build index local.slaves { _id: 1 }
Sun Dec 29 20:26:42.207 [slaveTracking] build index done. scanned 0 total records. 0.005 secs
Sun Dec 29 20:26:43.079 [rsHealthPoll] replSet member 192.168.1.136:27017 is now in state SECONDARY
Sun Dec 29 20:26:49.080 [rsHealthPoll] replSet member 192.168.1.137:27017 is now in state SECONDARY
|
#输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
{
"set" : "repset",
"date" : ISODate("2013-12-29T12:54:25Z"),
"myState" : 1,
"members" : [
{
"_id" : 0,
"name" : "192.168.1.136:27017",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 1682,
"optime" : Timestamp(1388319973, 1),
"optimeDate" : ISODate("2013-12-29T12:26:13Z"),
"lastHeartbeat" : ISODate("2013-12-29T12:54:25Z"),
"lastHeartbeatRecv" : ISODate("2013-12-29T12:54:24Z"),
"pingMs" : 1,
"syncingTo" : "192.168.1.138:27017"
},
{
"_id" : 1,
"name" : "192.168.1.137:27017",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 1682,
"optime" : Timestamp(1388319973, 1),
"optimeDate" : ISODate("2013-12-29T12:26:13Z"),
"lastHeartbeat" : ISODate("2013-12-29T12:54:25Z"),
"lastHeartbeatRecv" : ISODate("2013-12-29T12:54:24Z"),
"pingMs" : 1,
"syncingTo" : "192.168.1.138:27017"
},
{
"_id" : 2,
"name" : "192.168.1.138:27017",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 2543,
"optime" : Timestamp(1388319973, 1),
"optimeDate" : ISODate("2013-12-29T12:26:13Z"),
"self" : true
}
],
"ok" : 1
}
|
整个副本集已经搭建成功了。
6、测试副本集数据复制功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
mongo 127.0.0.1
use
test
;
往testdb表插入数据。
> db.testdb.insert({
"test1"
:
"testval1"
})
/data/mongodbtest/mongodb-linux-x86_64-2
.4.8
/bin/mongo
192.168.1.136:27017
repset:SECONDARY> use
test
;
repset:SECONDARY> show tables;
|
#输出
1
|
Sun Dec 29 21:50:48.590 error: { "$err" : "not master and slaveOk=false", "code" : 13435 } at src/mongo/shell/query.js:128
|
1
2
3
4
5
|
repset:SECONDARY> db.getMongo().setSlaveOk();
repset:SECONDARY> db.testdb.
find
();
|
1
2
|
#输出
{ "_id" : ObjectId("52c028460c7505626a93944f"), "test1" : "testval1" }
|
7、测试副本集故障转移功能
先停掉主节点mongodb 138,查看136、137的日志可以看到经过一系列的投票选择操作,137 当选主节点,136从137同步数据过来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
Sun Dec 29 22:03:05.351 [rsBackgroundSync] replSet sync source problem: 10278 dbclient error communicating with server: 192.168.1.138:27017
Sun Dec 29 22:03:05.354 [rsBackgroundSync] replSet syncing to: 192.168.1.138:27017
Sun Dec 29 22:03:05.356 [rsBackgroundSync] repl: couldn't connect to server 192.168.1.138:27017
Sun Dec 29 22:03:05.356 [rsBackgroundSync] replSet not trying to sync from 192.168.1.138:27017, it is vetoed for 10 more seconds
Sun Dec 29 22:03:05.499 [rsHealthPoll] DBClientCursor::init call() failed
Sun Dec 29 22:03:05.499 [rsHealthPoll] replset info 192.168.1.138:27017 heartbeat failed, retrying
Sun Dec 29 22:03:05.501 [rsHealthPoll] replSet info 192.168.1.138:27017 is down (or slow to respond):
Sun Dec 29 22:03:05.501 [rsHealthPoll] replSet member 192.168.1.138:27017 is now in state DOWN
Sun Dec 29 22:03:05.511 [rsMgr] not electing self, 192.168.1.137:27017 would veto with '192.168.1.136:27017 is trying to elect itself but 192.168.1.138:27017 is already primary and more up-to-date'
Sun Dec 29 22:03:07.330 [conn393] replSet info voting yea for 192.168.1.137:27017 (1)
Sun Dec 29 22:03:07.503 [rsHealthPoll] replset info 192.168.1.138:27017 heartbeat failed, retrying
Sun Dec 29 22:03:08.462 [rsHealthPoll] replSet member 192.168.1.137:27017 is now in state PRIMARY
Sun Dec 29 22:03:09.359 [rsBackgroundSync] replSet syncing to: 192.168.1.137:27017
Sun Dec 29 22:03:09.507 [rsHealthPoll] replset info 192.168.1.138:27017 heartbeat failed, retrying
|
查看整个集群的状态,可以看到138为状态不可达。
1
2
3
|
/data/mongodbtest/mongodb-linux-x86_64-2
.4.8
/bin/mongo
192.168.1.136:27017
repset:SECONDARY> rs.status();
|
#输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
{
"set" : "repset",
"date" : ISODate("2013-12-29T14:28:35Z"),
"myState" : 2,
"syncingTo" : "192.168.1.137:27017",
"members" : [
{
"_id" : 0,
"name" : "192.168.1.136:27017",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 9072,
"optime" : Timestamp(1388324934, 1),
"optimeDate" : ISODate("2013-12-29T13:48:54Z"),
"self" : true
},
{
"_id" : 1,
"name" : "192.168.1.137:27017",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 7329,
"optime" : Timestamp(1388324934, 1),
"optimeDate" : ISODate("2013-12-29T13:48:54Z"),
"lastHeartbeat" : ISODate("2013-12-29T14:28:34Z"),
"lastHeartbeatRecv" : ISODate("2013-12-29T14:28:34Z"),
"pingMs" : 1,
"syncingTo" : "192.168.1.138:27017"
},
{
"_id" : 2,
"name" : "192.168.1.138:27017",
"health" : 0,
"state" : 8,
"stateStr" : "(not reachable/healthy)",
"uptime" : 0,
"optime" : Timestamp(1388324934, 1),
"optimeDate" : ISODate("2013-12-29T13:48:54Z"),
"lastHeartbeat" : ISODate("2013-12-29T14:28:35Z"),
"lastHeartbeatRecv" : ISODate("2013-12-29T14:28:23Z"),
"pingMs" : 0,
"syncingTo" : "192.168.1.137:27017"
}
],
"ok" : 1
}
|
再启动原来的主节点 138,发现138 变为 SECONDARY,还是137 为主节点 PRIMARY。
1
2
3
4
5
6
7
8
|
Sun Dec 29 22:21:06.619 [rsStart] replSet I am 192.168.1.138:27017
Sun Dec 29 22:21:06.619 [rsStart] replSet STARTUP2
Sun Dec 29 22:21:06.627 [rsHealthPoll] replset info 192.168.1.136:27017 thinks that we are down
Sun Dec 29 22:21:06.627 [rsHealthPoll] replSet member 192.168.1.136:27017 is up
Sun Dec 29 22:21:06.627 [rsHealthPoll] replSet member 192.168.1.136:27017 is now in state SECONDARY
Sun Dec 29 22:21:07.628 [rsSync] replSet SECONDARY
Sun Dec 29 22:21:08.623 [rsHealthPoll] replSet member 192.168.1.137:27017 is up
Sun Dec 29 22:21:08.624 [rsHealthPoll] replSet member 192.168.1.137:27017 is now in state PRIMARY
|
8、java程序连接副本集测试。三个节点有一个节点挂掉也不会影响应用程序客户端对整个副本集的读写!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
public
class
TestMongoDBReplSet {
public
static
void
main(String[] args) {
try
{
List<ServerAddress> addresses =
new
ArrayList<ServerAddress>();
ServerAddress address1 =
new
ServerAddress(
"192.168.1.136"
,
27017
);
ServerAddress address2 =
new
ServerAddress(
"192.168.1.137"
,
27017
);
ServerAddress address3 =
new
ServerAddress(
"192.168.1.138"
,
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"
);
coll.insert(object);
DBCursor dbCursor = coll.find();
while
(dbCursor.hasNext()) {
DBObject dbObject = dbCursor.next();
System. out.println(dbObject.toString());
}
}
catch
(Exception e) {
e.printStackTrace();
}
}
}
|
目前看起来支持完美的故障转移了,这个架构是不是比较完美了?其实还有很多地方可以优化,比如开头的第二个问题:主节点的读写压力过大如何解决?常见的解决方案是读写分离,mongodb副本集的读写分离如何做呢?
看图说话:
常规写操作来说并没有读操作多,所以一台主节点负责写,两台副本节点负责读。
1、设置读写分离需要先在副本节点SECONDARY 设置 setSlaveOk。
2、在程序中设置副本节点负责读操作,如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public
class
TestMongoDBReplSetReadSplit {
public
static
void
main(String[] args) {
try
{
List<ServerAddress> addresses =
new
ArrayList<ServerAddress>();
ServerAddress address1 =
new
ServerAddress(
"192.168.1.136"
,
27017
);
ServerAddress address2 =
new
ServerAddress(
"192.168.1.137"
,
27017
);
ServerAddress address3 =
new
ServerAddress(
"192.168.1.138"
,
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);
}
catch
(Exception e) {
e.printStackTrace();
}
}
}
|
读参数除了secondary一共还有五个参数:primary、primaryPreferred、secondary、secondaryPreferred、nearest。
primary:默认参数,只从主节点上进行读取操作;
primaryPreferred:大部分从主节点上读取数据,只有主节点不可用时从secondary节点读取数据。
secondary:只从secondary节点上进行读取操作,存在的问题是secondary节点的数据会比primary节点数据“旧”。
secondaryPreferred:优先从secondary节点进行读取操作,secondary节点不可用时从主节点读取数据;
nearest:不管是主节点、secondary节点,从网络延迟最低的节点上读取数据。
好,读写分离做好我们可以数据分流,减轻压力解决了“主节点的读写压力过大如何解决?”这个问题。不过当我们的副本节点增多时,主节点的复制压力会加大有什么办法解决吗?mongodb早就有了相应的解决方案。
看图:
其中的仲裁节点不存储数据,只是负责故障转移的群体投票,这样就少了数据复制的压力。是不是想得很周到啊,一看mongodb的开发兄弟熟知大数据架构体系,其实不只是主节点、副本节点、仲裁节点,还有Secondary-Only、Hidden、Delayed、Non-Voting。
Secondary-Only:不能成为primary节点,只能作为secondary副本节点,防止一些性能不高的节点成为主节点。
Hidden:这类节点是不能够被客户端制定IP引用,也不能被设置为主节点,但是可以投票,一般用于备份数据。
Delayed:可以指定一个时间延迟从primary节点同步数据。主要用于备份数据,如果实时同步,误删除数据马上同步到从节点,恢复又恢复不了。
Non-Voting:没有选举权的secondary节点,纯粹的备份数据节点。
到此整个mongodb副本集搞定了两个问题:
- 主节点挂了能否自动切换连接?目前需要手工切换。
- 主节点的读写压力过大如何解决?
还有这两个问题后续解决:
- 从节点每个上面的数据都是对数据库全量拷贝,从节点压力会不会过大?
- 数据压力大到机器支撑不了的时候能否做到自动扩展?
做了副本集发现又一些问题:
- 副本集故障转移,主节点是如何选举的?能否手动干涉下架某一台主节点。
- 官方说副本集数量最好是奇数,为什么?
- mongodb副本集是如何同步的?如果同步不及时会出现什么情况?会不会出现不一致性?
- mongodb的故障转移会不会无故自动发生?什么条件会触发?频繁触发可能会带来系统负载加重
参考:
http://cn.docs.mongodb.org/manual/administration/replica-set-member-configuration/
http://docs.mongodb.org/manual/reference/connection-string/
http://www.cnblogs.com/magialmoon/p/3268963.html