MongoDB Replica Sets + Sharding 实战

一、Replica Sets 复制集

MongoDB 支持在多个机器中通过异步复制达到故障转移和实现冗余。多机器中同一时刻只有一台是用于写操作。正是由于这个情况,为 MongoDB 提供了数据一致性的保障。担当Primary 角色的机器能把读操作分发给 slave。

MongoDB 高可用可用分两种:

  • Master-Slave 主从复制

只需要在某一个服务启动时加上–master参数,而另一个服务加上–slave与–source参数,即可实现同步。MongoDB 的最新版本已不再推荐此方案。

  • Replica Sets 复制集

MongoDB 在 1.6 版本对开发了新功能 replica set,这比之前的 replication 功能要强大一些,增加了故障自动切换和自动修复成员节点,各个 DB 之间数据完全一致,大大降低了维护成功。auto shard 已经明确说明不支持 replication paris,建议使用 replica set,replica set 故障切换完全自动。

3-node Replica Sets:

如果上图所示,Replica Sets 的结构非常类似一个集群。是的,你完全可以把它当成集群,因为它确实跟集群实现的作用是一样的,其中一个节点如果出现故障,其它节点马上会将业务接过来而无须停机操作。

1、部署 Replica Sets 

创建数据文件存储路径:

[root@localhost ~]# mkdir -p /data/data/r0 
[root@localhost ~]# mkdir -p /data/data/r1 
[root@localhost ~]# mkdir -p /data/data/r2

创建日志文件路径:

[root@localhost ~]# mkdir -p /data/log

创建主从 key 文件,用于标识集群的私钥的完整路径,如果各个实例的 key file 内容不一致,程序将不能正常用。

[root@localhost ~]# mkdir -p /data/key 
[root@localhost ~]# echo "this is rs1 super secret key" > /data/key/r0 
[root@localhost ~]# echo "this is rs1 super secret key" > /data/key/r1 
[root@localhost ~]# echo "this is rs1 super secret key" > /data/key/r2 
[root@localhost ~]# chmod 600 /data/key/r*

启动 3 个实例:

[root@localhost ~]# /Apps/mongo/bin/mongod --replSet rs1 --keyFile /data/key/r0 --fork --port 
28010 --dbpath /data/data/r0 --logpath=/data/log/r0.log --logappend 
all output going to: /data/log/r0.log 
forked process: 6573 
[root@localhost ~]# /Apps/mongo/bin/mongod --replSet rs1 --keyFile /data/key/r1 --fork --port 
28011 --dbpath /data/data/r1 --logpath=/data/log/r1.log --logappend 
all output going to: /data/log/r1.log 
forked process: 6580 
[root@localhost ~]# /Apps/mongo/bin/mongod --replSet rs1 --keyFile /data/key/r2 --fork --port 
28012 --dbpath /data/data/r2 --logpath=/data/log/r2.log --logappend 
all output going to: /data/log/r2.log 
forked process: 6585 
[root@localhost ~]#

配置及初始化 Replica Sets:

[root@localhost bin]# /Apps/mongo/bin/mongo -port 28010 
MongoDB shell version: 1.8.1 
connecting to: 127.0.0.1:28010/test 
> config_rs1 = {_id: 'rs1', members: [ 
... {_id: 0, host: 'localhost:28010', priority:1}, --成员 IP 及端口,priority=1 指 PRIMARY
... {_id: 1, host: 'localhost:28011'}, 
... {_id: 2, host: 'localhost:28012'}] 
... } 
{ 
     "_id" : "rs1", 
     "members" : [ 
         {
            "_id" : 0, 
             "host" : "localhost:28010" 
         }, 
         { 
             "_id" : 1, 
             "host" : "localhost:28011" 
         }, 
         { 
             "_id" : 2, 
             "host" : "localhost:28012" 
         } 
     ] 
} 
> rs.initiate(config_rs1); --初始化配置
{ 
     "info" : "Config now saved locally. Should come online in about a minute.", 
     "ok" : 1 
}

查看复制集状态:

> rs.status()
{ 
     "set" : "rs1", 
     "date" : ISODate("2018-05-31T09:49:57Z"), 
     "myState" : 1, 
     "members" : [ 
         { 
             "_id" : 0, 
             "name" : "localhost:28010",
             "health" : 1, --1 表明正常; 0 表明异常
             "state" : 1, -- 1 表明是 Primary; 2 表明是 Secondary; 
             "stateStr" : "PRIMARY", --表明此机器是主库
             "optime" : { 
                 "t" : 1338457763000, 
                 "i" : 1 
              }, 
             "optimeDate" : ISODate("2018-05-31T09:49:23Z"), 
             "self" : true 
         }, 
         { 
             "_id" : 1, 
             "name" : "localhost:28011",
             "health" : 1,
             "state" : 2, 
             "stateStr" : "SECONDARY", 
             "uptime" : 23, 
             "optime" : { 
                 "t" : 1338457763000, 
                 "i" : 1 
             }, 
             "optimeDate" : ISODate("2018-05-31T09:49:23Z"), 
             "lastHeartbeat" : ISODate("2018-05-31T09:49:56Z") 
         }, 
         { 
             "_id" : 2, 
             "name" : "localhost:28012",
             "health" : 1, 
             "state" : 2, 
             "stateStr" : "SECONDARY", 
             "uptime" : 23, 
             "optime" : { 
                 "t" : 1338457763000, 
                 "i" : 1 
             }, 
             "optimeDate" : ISODate("2018-05-31T09:49:23Z"), 
             "lastHeartbeat" : ISODate("2018-05-31T09:49:56Z") 
         } 
     ], 
     "ok" : 1 
} 
rs1:PRIMARY>

还可以用 isMaster 查看 Replica Sets 状态:

rs1:PRIMARY> rs.isMaster()
{ 
     "setName" : "rs1", 
     "ismaster" : true, 
     "secondary" : false, 
     "hosts" : [ 
         "localhost:28010", 
         "localhost:28012", 
         "localhost:28011" 
     ], 
     "maxBsonObjectSize" : 16777216, 
     "ok" : 1 
} 
rs1:PRIMARY>

2、主从操作日志 oplog 

MongoDB 的 Replica Set 架构是通过一个日志来存储写操作的,这个日志就叫做”oplog”。

oplog.rs 是一个固定长度的 capped collection,它存在于”local”数据库中,用于记录 Replica Sets 操作日志。在默认情况下,对于 64 位的 MongoDB,oplog 是比较大的,可以达到 5%的磁盘空间。oplog 的大小是可以通过 mongod 的参数”—oplogSize”来改变 oplog 的日志大小。

Oplog 内容样例:

rs1:PRIMARY> use local 
switched to db local 
rs1:PRIMARY> show collections 
oplog.rs 
system.replset 
rs1:PRIMARY> db.oplog.rs.find()
{ "ts" : { "t" : 1338457763000, "i" : 1 }, "h" : NumberLong(0), "op" : "n", "ns" : "", "o" : { "msg" :"initiating set" } } 
{ "ts" : { "t" : 1338459114000, "i" : 1 }, "h" : NumberLong("5493127699725549585"), "op" : "i", "ns" : "test.c1", "o" : { "_id" : ObjectId("4fc743e9aea289af709ac6b5"), "age" : 29, "name" : "Tony" } } 
rs1:PRIMARY>

字段说明:

ts:某个操作的时间戳。

op:操作类型,如下:

  • i:insert 
  • d:delete 
  • u:update 

ns:命名空间,也就是操作的 collection name 。

o:document 的内容。

查看 master 的 oplog 元数据信息:

rs1:PRIMARY> db.printReplicationInfo()
configured oplog size: 47.6837158203125MB 
log length start to end: 1351secs (0.38hrs) 
oplog first event time: Thu May 31 2018 17:49:23 GMT+0800 (CST) 
oplog last event time: Thu May 31 2018 18:11:54 GMT+0800 (CST) 
now: Thu May 31 2018 18:21:58 GMT+0800 (CST) 
rs1:PRIMARY>

字段说明:

  • configured oplog size:配置的 oplog 文件大小;
  • log length start to end:oplog 日志的启用时间段;
  • oplog first event time:第一个事务日志的产生时间;
  • oplog last event time:最后一个事务日志的产生时间;
  • now:现在的时间;

查看 slave 的同步状态:

rs1:PRIMARY> db.printSlaveReplicationInfo()
source: localhost:28011 
 syncedTo: Thu May 31 2018 18:11:54 GMT+0800 (CST) 
 = 884secs ago (0.25hrs) 
source: localhost:28012 
 syncedTo: Thu May 31 2018 18:11:54 GMT+0800 (CST) 
 = 884secs ago (0.25hrs) 
rs1:PRIMARY>

字段说明:

  • source: 从库的 IP 及端口;
  • syncedTo: 目前的同步情况,延迟了多久等信息;

3、主从配置信息

在 local 库中不仅有主从日志 oplog 集合,还有一个集合用于记录主从配置信息system.replset。

rs1:PRIMARY> use local 
switched to db local 
rs1:PRIMARY> show collections 
oplog.rs 
system.replset 
rs1:PRIMARY> db.system.replset.find()
{ "_id" : "rs1", "version" : 1, "members" : [ 
     { 
         "_id" : 0, 
         "host" : "localhost:28010" 
     }, 
     { 
         "_id" : 1, 
         "host" : "localhost:28011" 
     }, 
     { 
         "_id" : 2, 
         "host" : "localhost:28012" 
     } 
] } 
rs1:PRIMARY>

从这个集合中可以看出,Replica Sets 的配置信息,也可以在任何一个成员实例上执行 rs.conf()来查看配置信息。

4、管理维护 Replica Sets 

1. 读写分离

有一些第三方的工具,提供了一些可以让数据库进行读写分离的工具。我们现在是否有一个疑问,从库要是能进行查询就更好了,这样可以分担主库的大量的查询请求。

先向主库中插入一条测试数据:

[root@localhost bin]# ./mongo --port 28010 
MongoDB shell version: 1.8.1 
connecting to: 127.0.0.1:28010/test 
rs1:PRIMARY> db.c1.insert({age:30}) 
db.c2rs1:PRIMARY> db.c1.find() 
{ "_id" : ObjectId("4fc77f421137ea4fdb653b4a"), "age" : 30 }

在从库进行查询等操作:

[root@localhost bin]# ./mongo --port 28011 
MongoDB shell version: 1.8.1 
connecting to: 127.0.0.1:28011/test 
rs1:SECONDARY> show collections 
Thu May 31 22:27:17 uncaught exception: error: { "$err" : "not master and slaveok=false", "code" : 13435 } 
rs1:SECONDARY>

当查询时报错了,说明是个从库且不能执行查询的操作。

让从库可以读,分担主库的压力:

rs1:SECONDARY> db.getMongo().setSlaveOk()
not master and slaveok=false 
rs1:SECONDARY> show collections 
c1 
system.indexes 
rs1:SECONDARY> db.c1.find() 
{ "_id" : ObjectId("4fc77f421137ea4fdb653b4a"), "age" : 30 } 
rs1:SECONDARY>

看来我们要是执行 db.getMongo().setSlaveOk(), 我们就可查询从库了。

2. 故障转移

复制集比传统的 Master-Slave 有改进的地方就是他可以进行故障的自动转移,如果我们停掉复制集中的一个成员,那么剩余成员会再自动选举出一个成员,做为主库。

例如:我们将 28010 这个主库停掉,然后再看一下复制集的状态。

杀掉 28010 端口的 MongoDB:

[root@localhost bin]# ps aux|grep mongod 
root 6706 1.6 6.9 463304 6168 Sl 21:49 0:26 
/Apps/mongo/bin/mongod --replSet rs1 --keyFile /data/key/r0 --fork --port 28010 
root 6733 0.4 6.7 430528 6044 ? Sl 21:50 0:06 
/Apps/mongo/bin/mongod --replSet rs1 --keyFile /data/key/r1 --fork --port 28011 
root 6747 0.4 4.7 431548 4260 ? Sl 21:50 0:06 
/Apps/mongo/bin/mongod --replSet rs1 --keyFile /data/key/r2 --fork --port 28012 
root 7019 0.0 0.7 5064 684 pts/2 S+ 22:16 0:00 grep mongod 
[root@localhost bin]# kill -9 6706

查看复制集状态:

[root@localhost bin]# ./mongo --port 28011 
MongoDB shell version: 1.8.1 
connecting to: 127.0.0.1:28011/test 
rs1:SECONDARY> rs.status() 
{ 
     "set" : "rs1", 
     "date" : ISODate("2018-05-31T14:17:03Z"), 
     "myState" : 2, 
     "members" : [ 
         { 
             "_id" : 0, 
             "name" : "localhost:28010",
             "health" : 0, 
             "state" : 1, 
             "stateStr" : "(not reachable/healthy)", 
             "uptime" : 0, 
             "optime" : { 
                 "t" : 1338472279000, 
                 "i" : 1 
             }, 
             "optimeDate" : ISODate("2018-05-31T13:51:19Z"), 
             "lastHeartbeat" : ISODate("2018-05-31T14:16:42Z"), 
             "errmsg" : "socket exception" 
         }, 
         { 
             "_id" : 1, 
             "name" : "localhost:28011",
             "health" : 1, 
             "state" : 2, 
             "stateStr" : "SECONDARY", 
             "optime" : {
                "t" : 1338472279000, 
                 "i" : 1 
             }, 
             "optimeDate" : ISODate("2018-05-31T13:51:19Z"), 
             "self" : true 
         }, 
         { 
             "_id" : 2, 
             "name" : "localhost:28018",
             "health" : 1, 
             "state" : 1, 
             "stateStr" : "PRIMARY", 
             "uptime" : 1528, 
             "optime" : { 
             "t" : 1338472279000, 
             "i" : 1 
         }, 
         "optimeDate" : ISODate("2018-05-31T13:51:19Z"), 
         "lastHeartbeat" : ISODate("2018-05-31T14:17:02Z") 
     } 
 ], 
 "ok" : 1 
} 
rs1:SECONDARY>

可以看到 28010 这个端口的 MongoDB 出现了异常,而系统自动选举了 28012 这个端口为主,所以这样的故障处理机制,能将系统的稳定性大大提高。

3. 增减节点

MongoDB Replica Sets 不仅提供高可用性的解决方案,它也同时提供负载均衡的解决方案,增减 Replica Sets 节点在实际应用中非常普遍,例如当应用的读压力暴增时,3 台节点的环境已不能满足需求,那么就需要增加一些节点将压力平均分配一下;当应用的压力小时,可以减少一些节点来减少硬件资源的成本;总之这是一个长期且持续的工作。

1)增加节点

官方给我们提了 2 个方案用于增加节点,一种是通过 oplog 来增加节点,一种是通过数据库快照(--fastsync)和 oplog 来增加节点,下面将分别介绍。

通过 oplog 增加节点:

配置并启动新节点 配置并启动新节点,启用启用 28013 这个端口给新的节点。

[root@localhost ~]# mkdir -p /data/data/r3
[root@localhost ~]# echo "this is rs1 super secret key" > /data/key/r3 
[root@localhost ~]# chmod 600 /data/key/r3 
[root@localhost ~]# /Apps/mongo/bin/mongod --replSet rs1 --keyFile /data/key/r3 --fork --port 28013 --dbpath /data/data/r3 --logpath=/data/log/r3.log --logappend
all output going to: /data/log/r3.log 
forked process: 10553 
[root@localhost ~]#

添加此新节点到现有的 Replica Sets:

rs1:PRIMARY> rs.add("localhost:28013") 
{ "ok" : 1 }

查看查看 Replica Sets 我们可以清晰的看到内部是如何添加 28013 这个新节点的。

步骤一:进行初始化

rs1: PRIMARY > rs.status() 
{ 
     "set" : "rs1", 
     "date" : ISODate("2018-05-31T12:17:44Z"), 
     "myState" : 1, 
     "members" : [ 
……
         { 
             "_id" : 3, 
             "name" : "localhost:28013", 
             "health" : 0, 
             "state" : 6, 
             "stateStr" : "(not reachable/healthy)", 
             "uptime" : 0, 
             "optime" : { 
                 "t" : 0, 
                 "i" : 0 
             }, 
             "optimeDate" : ISODate("1970-01-01T00:00:00Z"), 
             "lastHeartbeat" : ISODate("2018-05-31T12:17:43Z"), 
             "errmsg" : "still initializing" 
         } 
     ], 
     "ok" : 1 
}

步骤二:进行数据同步

rs1:PRIMARY> rs.status() 
{ 
     "set" : "rs1",
     "date" : ISODate("2018-05-31T12:18:07Z"), 
     "myState" : 1, 
     "members" : [ 
……
         { 
             "_id" : 3, 
             "name" : "localhost:28013", 
             "health" : 1, 
             "state" : 3, 
             "stateStr" : "RECOVERING", 
             "uptime" : 16, 
             "optime" : { 
                 "t" : 0, 
                 "i" : 0 
             }, 
             "optimeDate" : ISODate("1970-01-01T00:00:00Z"), 
             "lastHeartbeat" : ISODate("2018-05-31T12:18:05Z"), 
             "errmsg" : "initial sync need a member to be primary or secondary to do our initial sync" 
         } 
     ], 
     "ok" : 1 
}

 步骤三:初始化同步完成

rs1:PRIMARY> rs.status() 
{ 
     "set" : "rs1", 
     "date" : ISODate("2018-05-31T12:18:08Z"), 
     "myState" : 1, 
     "members" : [ 
……
         { 
             "_id" : 3, 
             "name" : "localhost:28013", 
             "health" : 1, 
             "state" : 3, 
             "stateStr" : "RECOVERING", 
             "uptime" : 17, 
             "optime" : { 
                 "t" : 1338466661000, 
                 "i" : 1 
             }, 
             "optimeDate" : ISODate("2018-05-31T12:17:41Z"),
             "lastHeartbeat" : ISODate("2018-05-31T12:18:07Z"), 
             "errmsg" : "initial sync done" 
         } 
     ], 
     "ok" : 1 
}

步骤四:节点添加完成,状态正常

rs1:PRIMARY> rs.status() 
{ 
     "set" : "rs1", 
     "date" : ISODate("2018-05-31T12:18:10Z"), 
     "myState" : 1, 
     "members" : [ 
……
         { 
             "_id" : 3, 
             "name" : "localhost:28013", 
             "health" : 1, 
             "state" : 2, 
             "stateStr" : "SECONDARY", 
             "uptime" : 19, 
             "optime" : { 
                 "t" : 1338466661000, 
                 "i" : 1 
             }, 
             "optimeDate" : ISODate("2018-05-31T12:17:41Z"), 
             "lastHeartbeat" : ISODate("2018-05-31T12:18:09Z") 
         } 
     ], 
     "ok" : 1 
}

验证数据已经同步过来了:

[root@localhost data]# /Apps/mongo/bin/mongo -port 28013 
MongoDB shell version: 1.8.1 
connecting to: 127.0.0.1:28013/test 
rs1:SECONDARY> rs.slaveOk() 
rs1:SECONDARY> db.c1.find()
{ "_id" : ObjectId("4fc760d2383ede1dce14ef86"), "age" : 10 } 
rs1:SECONDARY>

2)通过数据库快照(--fastsync) 和 oplog 增加节点

通过 oplog 直接进行增加节点操作简单且无需人工干预过多,但 oplog 是 capped collection,采用循环的方式进行日志处理,所以采用 oplog 的方式进行增加节点,有可能导致数据的不一致,因为日志中存储的信息有可能已经刷新过了。

不过没关系,我们可以通过数据库快照 (--fastsync)和 oplog 结合的方式来增加节点,这种方式的操作流程是,先取某一个复制集成员的物理文件来做为初始化数据,然后剩余的部分用 oplog 日志来追,最终达到数据一致性。

取某一个复制集成员的物理文件来做为初始化数据:

[root@localhost ~]# scp -r /data/data/r3 /data/data/r4
[root@localhost ~]# echo "this is rs1 super secret key" > /data/key/r4 
[root@localhost ~]# chmod 600 /data/key/r4

在取完物理文件后,在 c1 集中插入一条新文档,用于最后验证此更新也同步了。

rs1:PRIMARY> db.c1.find() 
{ "_id" : ObjectId("4fc760d2383ede1dce14ef86"), "age" : 10 } 
rs1:PRIMARY> db.c1.insert({age:20}) 
rs1:PRIMARY> db.c1.find() 
{ "_id" : ObjectId("4fc760d2383ede1dce14ef86"), "age" : 10 } 
{ "_id" : ObjectId("4fc7748f479e007bde6644ef"), "age" : 20 } 
rs1:PRIMARY>

启用启用 28014 这个端口给新的节点:

/Apps/mongo/bin/mongod --replSet rs1 --keyFile /data/key/r4 --fork --port 28014 --dbpath /data/data/r4 --logpath=/data/log/r4.log --logappend --fastsync

添加添加 28014 节点:

rs1:PRIMARY> rs.add("localhost:28014") 
{ "ok" : 1 }

验证数据已经同步过来了:

[root@localhost data]# /Apps/mongo/bin/mongo -port 28014 
MongoDB shell version: 1.8.1 
connecting to: 127.0.0.1:28014/test 
rs1:SECONDARY> rs.slaveOk() 
rs1:SECONDARY> db.c1.find() 
{ "_id" : ObjectId("4fc760d2383ede1dce14ef86"), "age" : 10 } 
{ "_id" : ObjectId("4fc7748f479e007bde6644ef"), "age" : 20 } 
rs1:SECONDARY>

3)减少节点

下面将刚刚添加的两个新节点 28013 和 28014 从复制集中去除掉,只需执行 rs.remove 指令就可以了,具体如下:

rs1:PRIMARY> rs.remove("localhost:28014")
{ "ok" : 1 } 
rs1:PRIMARY> rs.remove("localhost:28013")
{ "ok" : 1 }

查看复制集状态,可以看到现在只有 28010、28011、28012 这三个成员,原来的28013和28014 都成功去除了。

rs1:PRIMARY> rs.status() 
{ 
     "set" : "rs1", 
     "date" : ISODate("2018-05-31T14:08:29Z"), 
     "myState" : 1, 
     "members" : [ 
         { 
             "_id" : 0, 
             "name" : "localhost:28010", 
             "health" : 1, 
             "state" : 1, 
             "stateStr" : "PRIMARY", 
             "optime" : { 
                 "t" : 1338473273000, 
                 "i" : 1 
             }, 
             "optimeDate" : ISODate("2018-05-31T14:07:53Z"), 
             "self" : true 
         }, 
         { 
             "_id" : 1, 
             "name" : "localhost:28011", 
             "health" : 1, 
             "state" : 2, 
             "stateStr" : "SECONDARY", 
             "uptime" : 34, 
             "optime" : { 
                 "t" : 1338473273000, 
                 "i" : 1 
             }, 
             "optimeDate" : ISODate("2018-05-31T14:07:53Z"), 
             "lastHeartbeat" : ISODate("2018-05-31T14:08:29Z") 
         }, 
         { 
             "_id" : 2, 
             "name" : "localhost:28012", 
             "health" : 1, 
             "state" : 2,
             "stateStr" : "SECONDARY", 
             "uptime" : 34, 
             "optime" : { 
                 "t" : 1338473273000, 
                 "i" : 1 
             }, 
             "optimeDate" : ISODate("2018-05-31T14:07:53Z"), 
             "lastHeartbeat" : ISODate("2018-05-31T14:08:29Z") 
         } 
     ], 
     "ok" : 1 
} 
rs1:PRIMARY>

二、Sharding 分片

这是一种将海量的数据水平扩展的数据库集群系统,数据分表存储在 sharding 的各个节点上,使用者通过简单的配置就可以很方便地构建一个分布式 MongoDB 集群。

MongoDB 的数据分块称为 chunk。每个 chunk 都是 Collection 中一段连续的数据记录,通常最大尺寸是 200MB,超出则生成新的数据块。

要构建一个 MongoDB Sharding Cluster,需要三种角色:

  • Shard Server 

即存储实际数据的分片,每个 Shard 可以是一个 mongod 实例,也可以是一组 mongod 实例构成的 Replica Set。为了实现每个 Shard 内部的 auto-failover,MongoDB 官方建议每个 Shard 为一组 Replica Set。

  • Config Server 

为了将一个特定的 collection 存储在多个 shard 中,需要为该 collection 指定一个 shard key,例如{age: 1} ,shard key 可以决定该条记录属于哪个 chunk。Config Servers 就是用来存储:所有 shard 节点的配置信息、每个 chunk 的 shard key 范围、chunk 在各 shard 的分布情况、该集群中所有 DB 和 collection 的 sharding 配置信息。

  • Route Process 

这是一个前端路由,客户端由此接入,然后询问 Config Servers 需要到哪个 Shard 上查询或保存记录,再连接相应的 Shard 进行操作,最后将结果返回给客户端。客户端只需要将原本发给 mongod 的查询或更新请求原封不动地发给 Routing Process,而不必关心所操作的记录存储在哪个 Shard 上。

下面我们在同一台物理机器上构建一个简单的 Sharding Cluster,架构图如下:

  • Shard Server 1:20000; 
  • Shard Server 2:20001;
  • Config Server :30000;
  • Route Process:40000;

1、启动 Shard Server

mkdir -p /data/shard/s0 --创建数据目录
mkdir -p /data/shard/s1 
mkdir -p /data/shard/log --创建日志目录
/Apps/mongo/bin/mongod --shardsvr --port 20000 --dbpath /data/shard/s0 --fork --logpath /data/shard/log/s0.log --directoryperdb --启动 Shard Server 实例 1
/Apps/mongo/bin/mongod --shardsvr --port 20001 --dbpath /data/shard/s1 --fork --logpath /data/shard/log/s1.log --directoryperdb --启动 Shard Server 实例 2

2、启动 Config Server

mkdir -p /data/shard/config --创建数据目录
/Apps/mongo/bin/mongod --configsvr --port 30000 --dbpath /data/shard/config --fork --logpath /data/shard/log/config.log --directoryperdb --启动 Config Server 实例

3、启动 Route Process

/Apps/mongo/bin/mongos --port 40000 --configdb localhost:30000 --fork --logpath /data/shard/log/route.log --chunkSize 1 --启动 Route Server 实例

mongos 启动参数中,chunkSize 这一项是用来指定 chunk 的大小的,单位是 MB,默认大小为 200MB,为了方便测试 Sharding 效果,我们把 chunkSize 指定为 1MB。

4、配置 Sharding 

接下来,我们使用 MongoDB Shell 登录到 mongos,添加 Shard 节点。

[root@localhost ~]# /Apps/mongo/bin/mongo admin --port 40000 --此操作需要连接 admin 库
MongoDB shell version: 1.8.1 
connecting to: 127.0.0.1:40000/admin 
> db.runCommand({ addshard:"localhost:20000" }) --添加 Shard Server
{ "shardAdded" : "shard0000", "ok" : 1 } 
> db.runCommand({ addshard:"localhost:20001" }) 
{ "shardAdded" : "shard0001", "ok" : 1 } 
> db.runCommand({ enablesharding:"test" }) --设置分片存储的数据库
{ "ok" : 1 } 
> db.runCommand({ shardcollection: "test.users", key: { _id:1 }}) --设置分片的集合名称,且必须指定 Shard Key,系统会自动创建索引
{ "collectionsharded" : "test.users", "ok" : 1 } 
>

5、验证 Sharding 正常工作

我们已经对 test.users 表进行了分片的设置,下面我们们插入一些数据看一下结果。

> use test 
switched to db test 
> for (var i = 1; i <= 500000; i++) db.users.insert({age:i, name:"wangwenlong", addr:"Beijing", 
country:"China"}) 
> db.users.stats() 
{ 
     "sharded" : true, --说明此表已被 shard
     "ns" : "test.users", 
     "count" : 500000, 
     "size" : 48000000, 
     "avgObjSize" : 96, 
     "storageSize" : 66655232, 
     "nindexes" : 1, 
     "nchunks" : 43, 
     "shards" : { 
         "shard0000" : { --在此分片实例上约有 24.5M 数据
             "ns" : "test.users", 
             "count" : 254889, 
             "size" : 24469344, 
             "avgObjSize" : 96,
             "storageSize" : 33327616, 
             "numExtents" : 8, 
             "nindexes" : 1, 
             "lastExtentSize" : 12079360, 
             "paddingFactor" : 1, 
             "flags" : 1, 
             "totalIndexSize" : 11468800, 
             "indexSizes" : { 
                 "_id_" : 11468800 
             }, 
             "ok" : 1 
         }, 
         "shard0001" : { --在此分片实例上约有 23.5M 数据
             "ns" : "test.users", 
             "count" : 245111, 
             "size" : 23530656, 
             "avgObjSize" : 96, 
             "storageSize" : 33327616, 
             "numExtents" : 8, 
             "nindexes" : 1, 
             "lastExtentSize" : 12079360, 
             "paddingFactor" : 1, 
             "flags" : 1, 
             "totalIndexSize" : 10649600, 
             "indexSizes" : { 
                 "_id_" : 10649600 
             }, 
             "ok" : 1 
         } 
     }, 
     "ok" : 1 
} 
>

我们看一下磁盘上的物理文件情况:

[root@localhost bin]# ll /data/shard/s0/test --此分片实例上有数据产生
总计 262420 
-rw------- 1 root root 16777216 06-03 15:21 test.0
-rw------- 1 root root 33554432 06-03 15:21 test.1
-rw------- 1 root root 67108864 06-03 15:22 test.2
-rw------- 1 root root 134217728 06-03 15:24 test.3
-rw------- 1 root root 16777216 06-03 15:21 test.ns 
[root@localhost bin]# ll /data/shard/s1/test --此分片实例上有数据产生
总计 262420
-rw------- 1 root root 16777216 06-03 15:21 test.0
-rw------- 1 root root 33554432 06-03 15:21 test.1
-rw------- 1 root root 67108864 06-03 15:22 test.2
-rw------- 1 root root 134217728 06-03 15:23 test.3
-rw------- 1 root root 16777216 06-03 15:21 test.ns 
[root@localhost bin]#

看上述结果,表明 test.users 集合已经被分片处理了,但是通过 mongos 路由,我们并感觉不到是数据存放在哪个 shard 的 chunk 上的,这就是 MongoDB 用户体验上的一个优势,即对用户是透明的。

6、管理维护 Sharding 

列出所有的 Shard Server:

> db.runCommand({ listshards: 1 }) --列出所有的 Shard Server
{ 
     "shards" : [ 
         { 
             "_id" : "shard0000", 
             "host" : "localhost:20000" 
         }, 
         { 
             "_id" : "shard0001", 
             "host" : "localhost:20001" 
         } 
    ], 
     "ok" : 1 
}

查看 Sharding 信息:

> printShardingStatus() --查看 Sharding 信息
--- Sharding Status --- 
 sharding version: { "_id" : 1, "version" : 3 } 
 shards: 
     { "_id" : "shard0000", "host" : "localhost:20000" } 
     { "_id" : "shard0001", "host" : "localhost:20001" } 
 databases: 
     { "_id" : "admin", "partitioned" : false, "primary" : "config" } 
     { "_id" : "test", "partitioned" : true, "primary" : "shard0000" } 
 test.users chunks:shard0000 1 
     { "_id" : { $minKey : 1 } } -->> { "_id" : { $maxKey : 1 } } on : shard0000 { "t" : 1000, "i" : 0 } 
>

判断是否是 Sharding:

> db.runCommand({ isdbgrid:1 }) 
{ "isdbgrid" : 1, "hostname" : "localhost", "ok" : 1 } 
>

对现有的表进行 Sharding ,刚才我们是对表 test.users 进行分片了,下面我们将对库中现有的未分片的表 test.users_2 进行分片处理。

表最初状态如下,可以看出他没有被分片过:

> db.users_2.stats() 
{ 
     "ns" : "test.users_2", 
     "sharded" : false, 
     "primary" : "shard0000", 
     "ns" : "test.users_2", 
     "count" : 500000, 
     "size" : 48000016, 
     "avgObjSize" : 96.000032, 
     "storageSize" : 61875968, 
     "numExtents" : 11, 
     "nindexes" : 1, 
     "lastExtentSize" : 15001856, 
     "paddingFactor" : 1, 
     "flags" : 1, 
     "totalIndexSize" : 20807680, 
     "indexSizes" : { 
         "_id_" : 20807680 
     }, 
     "ok" : 1 
}

对其进行分片处理:

> use admin
switched to db admin 
> db.runCommand({ shardcollection: "test.users_2", key: { _id:1 }}) 
{ "collectionsharded" : "test.users_2", "ok" : 1 }

再次查看分片后的表的状态,可以看到它已经被我们分片了。

> use test 
switched to db test 
> db.users_2.stats() 
{ 
     "sharded" : true, 
     "ns" : "test.users_2", 
     "count" : 505462, 
…… 
     "shards" : { 
         "shard0000" : { 
             "ns" : "test.users_2", 
…… 
             "ok" : 1 
         }, 
         "shard0001" : { 
             "ns" : "test.users_2", 
…… 
             "ok" : 1 
         } 
     }, 
     "ok" : 1 
} 
>

新增 Shard Server :

刚才我们演示的是新增分片表,接下来我们演示如何新增 Shard Server ,启动一个新 Shard Server 进程。

[root@localhost ~]# mkdir /data/shard/s2 
[root@localhost ~]# /Apps/mongo/bin/mongod --shardsvr --port 20002 --dbpath /data/shard/s2 --fork --logpath /data/shard/log/s2.log --directoryperdb 
all output going to: /data/shard/log/s2.log 
forked process: 6772

配置新 Shard Server:

[root@localhost ~]# /Apps/mongo/bin/mongo admin --port 40000 
MongoDB shell version: 1.8.1
connecting to: 127.0.0.1:40000/admin 
> db.runCommand({ addshard:"localhost:20002" }) 
{ "shardAdded" : "shard0002", "ok" : 1 } 
> printShardingStatus() 
--- Sharding Status --- 
 sharding version: { "_id" : 1, "version" : 3 } 
 shards: 
     { "_id" : "shard0000", "host" : "localhost:20000" } 
     { "_id" : "shard0001", "host" : "localhost:20001" } 
     { "_id" : "shard0002", "host" : "localhost:20002" } --新增 Shard Server 
 databases: 
     { "_id" : "admin", "partitioned" : false, "primary" : "config" } 
     { "_id" : "test", "partitioned" : true, "primary" : "shard0000" } 
         test.users chunks: 
             shard0002 2 
             shard0000 21 
             shard0001 21 
             too many chunksn to print, use verbose if you want to force print 
         test.users_2 chunks: 
             shard0001 46 
             shard0002 1 
             shard0000 45 
             too many chunksn to print, use verbose if you want to force print

查看分片表状态,以验证新 Shard Server:

> use test 
switched to db test 
> db.users_2.stats() 
{ 
 "sharded" : true, 
 "ns" : "test.users_2", 
…… 
     "shard0002" : { --新的 Shard Server 已有数据
         "ns" : "test.users_2", 
         "count" : 21848, 
         "size" : 2097408, 
         "avgObjSize" : 96, 
         "storageSize" : 2793472, 
         "numExtents" : 5, 
         "nindexes" : 1, 
         "lastExtentSize" : 2097152,
         "paddingFactor" : 1, 
         "flags" : 1, 
         "totalIndexSize" : 1277952,
        "indexSizes" : { 
             "_id_" : 1277952 
         }, 
         "ok" : 1 
     } 
 }, 
 "ok" : 1 
} 
>

我们可以发现,当我们新增 Shard Server 后数据自动分布到了新 Shard 上,这是由 MongoDB内部自已实现的。

有些时候有于硬件资源有限,所以我们不得不进行一些回收工作,下面我们就要将刚刚启用的 Shard Server 回收,系统首先会将在这个即将被移除的 Shard Server 上的数据先平均分配到其它的 Shard Server 上。

然后最终在将这个 Shard Server 踢下线, 我们需要不停的调用db.runCommand({"removeshard" : "localhost:20002"});来观察这个移除操作进行到哪里了:

> use admin 
switched to db admin 
> db.runCommand({"removeshard" : "localhost:20002"}); 
{ 
     "msg" : "draining started successfully", 
     "state" : "started", 
     "shard" : "shard0002", 
     "ok" : 1 
} 
> db.runCommand({"removeshard" : "localhost:20002"}); 
{ 
     "msg" : "draining ongoing", 
     "state" : "ongoing", 
     "remaining" : { 
         "chunks" : NumberLong(44), 
         "dbs" : NumberLong(0) 
     }, 
     "ok" : 1 
} 
…… 
> db.runCommand({"removeshard" : "localhost:20002"}); 
{ 
     "msg" : "draining ongoing", 
     "state" : "ongoing",
     "remaining" : {
        "chunks" : NumberLong(1),
         "dbs" : NumberLong(0) 
     }, 
     "ok" : 1 
} 
> db.runCommand({"removeshard" : "localhost:20002"}); 
{ 
     "msg" : "removeshard completed successfully", 
     "state" : "completed",
     "shard" : "shard0002", 
     "ok" : 1 
} 
> db.runCommand({"removeshard" : "localhost:20002"}); 
{ 
     "assertion" : "can't find shard for: localhost:20002", 
     "assertionCode" : 13129, 
     "errmsg" : "db assertion failure", 
     "ok" : 0 
}

最终移除后,当我们再次调用 db.runCommand({"removeshard" : "localhost:20002"});的时候系统
会报错,已便通知我们不存在 20002 这个端口的 Shard Server 了,因为它已经被移除掉了。

接下来我们看一下表中的数据分布:

> use test 
switched to db test 
> db.users_2.stats() 
{ 
     "sharded" : true, 
     "ns" : "test.users_2", 
     "count" : 500000,
     "size" : 48000000, 
     "avgObjSize" : 96, 
     "storageSize" : 95203584, 
     "nindexes" : 1, 
     "nchunks" : 92, 
     "shards" : { 
         "shard0000" : { 
             "ns" : "test.users_2", 
             "count" : 248749,
             "size" : 23879904, 
             "avgObjSize" : 96, 
             "storageSize" : 61875968, 
             "numExtents" : 11, 
             "nindexes" : 1,
             "lastExtentSize" : 15001856, 
             "paddingFactor" : 1, 
             "flags" : 1, 
             "totalIndexSize" : 13033472, 
             "indexSizes" : { 
                 "_id_" : 13033472 
             }, 
             "ok" : 1 
         }, 
         "shard0001" : { 
             "ns" : "test.users_2", 
             "count" : 251251,
             "size" : 24120096, 
             "avgObjSize" : 96, 
             "storageSize" : 33327616, 
             "numExtents" : 8, 
             "nindexes" : 1, 
             "lastExtentSize" : 12079360, 
             "paddingFactor" : 1, 
             "flags" : 1, 
             "totalIndexSize" : 10469376, 
             "indexSizes" : { 
                 "_id_" : 10469376 
             }, 
             "ok" : 1 
         } 
     }, 
     "ok" : 1 
}

可以看出数据又被平均分配到了另外 2 台 Shard Server 上了,对业务没什么特别大的影响。

三、Replica Sets + Sharding 

MongoDB Auto-Sharding 解决了海量存储和动态扩容的问题,但离实际生产环境所需的高可
靠、高可用还有些距离,所以有了” Replica Sets + Sharding”的解决方案:

  • Shard:使用 Replica Sets,确保每个数据节点都具有备份、自动容错转移、自动恢复能力;
  • Config:使用 3 个配置服务器,确保元数据完整性;
  • Route:使用 3 个路由进程,实现负载平衡,提高客户端接入性能;

以下我们配置一个 Replica Sets + Sharding 的环境,架构图如下:

服务及开放的端口如下: 

Server A(192.168.3.231 ):

  • mongod shard1_1:27017;
  • mongod shard2_1:27018;
  • mongod config1:20000;
  • mongs1:30000;

Server B (192.168.3.232):

  • mongod shard1_2:27017; 
  • mongod shard2_2:27018;
  • mongod config2:20000;
  • mongs2:30000;

Server C (192.168.3.233):

  • mongod shard1_3:27017; 
  • mongod shard2_3:27018; 
  • mongod config3:20000; 
  • mongs3:30000;

1、创建数据目录

在 Server A 上:

[root@localhost bin]# mkdir -p /data/shard1_1 
[root@localhost bin]# mkdir -p /data/shard2_1
[root@localhost bin]# mkdir -p /data/config

在 Server B 上:

[root@localhost bin]# mkdir -p /data/shard1_2 
[root@localhost bin]# mkdir -p /data/shard2_2 
[root@localhost bin]# mkdir -p /data/config

在 Server C 上:

[root@localhost bin]# mkdir -p /data/shard1_3 
[root@localhost bin]# mkdir -p /data/shard2_3 
[root@localhost bin]# mkdir -p /data/config

2、配置 Replica Sets 

1. 配置 shard1 所用到的 Replica Sets 

在 Server A 上:

[root@localhost bin]# /Apps/mongo/bin/mongod --shardsvr --replSet shard1 --port 27017 --dbpath /data/shard1_1 --logpath /data/shard1_1/shard1_1.log --logappend --fork 
[root@localhost bin]# all output going to: /data/shard1_1/shard1_1.log 
forked process: 18923

在 Server B 上:

[root@localhost bin]# /Apps/mongo/bin/mongod --shardsvr --replSet shard1 --port 27017 --dbpath /data/shard1_2 --logpath /data/shard1_2/shard1_2.log --logappend --fork 
forked process: 18859 
[root@localhost bin]# all output going to: /data/shard1_2/shard1_2.log 
[root@localhost bin]#

在 Server C 上:

[root@localhost bin]# /Apps/mongo/bin/mongod --shardsvr --replSet shard1 --port 27017 --dbpath /data/shard1_3 --logpath /data/shard1_3/shard1_3.log --logappend --fork 
all output going to: /data/shard1_3/shard1_3.log 
forked process: 18768 
[root@localhost bin]#

用 mongo 连接其中一台机器的 27017 端口的 mongod,初始化 Replica Sets“shard1”,执行:

[root@localhost bin]# ./mongo --port 27017 
MongoDB shell version: 1.8.1 
connecting to: 127.0.0.1:27017/test 
> config = {_id: 'shard1', members: [
...        {_id: 0, host: '192.168.3.231:27017'}, 
...        {_id: 1, host: '192.168.3.232:27017'}, 
...         {_id: 2, host: '192.168.3.233:27017'}] 
...     } 
…… 
> rs.initiate(config)
{ 
     "info" : "Config now saved locally. Should come online in about a minute.", 
     "ok" : 1 
}

2. 配置 shard2 所用到的 Replica Sets 

在 Server A 上:

[root@localhost bin]# /Apps/mongo/bin/mongod --shardsvr --replSet shard2 --port 27018 --dbpath /data/shard2_1 --logpath /data/shard2_1/shard2_1.log --logappend --fork 
all output going to: /data/shard2_1/shard2_1.log 
[root@localhost bin]# forked process: 18993 
[root@localhost bin]#

在 Server B 上:

[root@localhost bin]# /Apps/mongo/bin/mongod --shardsvr --replSet shard2 --port 27018 --dbpath /data/shard2_2 --logpath /data/shard2_2/shard2_2.log --logappend --fork 
all output going to: /data/shard2_2/shard2_2.log 
forked process: 18923 
[root@localhost bin]#

在 Server C 上:

[root@localhost bin]# /Apps/mongo/bin/mongod --shardsvr --replSet shard2 --port 27018 --dbpath /data/shard2_3 --logpath /data/shard2_3/shard2_3.log --logappend --fork 
[root@localhost bin]# all output going to: /data/shard2_3/shard2_3.log 
forked process: 18824 
[root@localhost bin]#

用 mongo 连接其中一台机器的 27018 端口的 mongod,初始化 Replica Sets “shard2”,执行:

[root@localhost bin]# ./mongo --port 27018
MongoDB shell version: 1.8.1 
connecting to: 127.0.0.1:27018/test 
> config = {_id: 'shard2', members: [ 
...                     {_id: 0, host: '192.168.3.231:27018'},
...                     {_id: 1, host: '192.168.3.232:27018'}, 
...                     {_id: 2, host: '192.168.3.233:27018'}] 
...             } 
…… 
> rs.initiate(config)
{ 
         "info" : "Config now saved locally. Should come online in about a minute.", 
         "ok" : 1 
}

3、配置 3 台 Config Server 

在 Server A、B、C 上执行:

/Apps/mongo/bin/mongod --configsvr --dbpath /data/config --port 20000 --logpath /data/config/config.log --logappend --fork

4、配置 3 台 Route Process 

在 Server A、B、C 上执行:

/Apps/mongo/bin/mongos --configdb 192.168.3.231:20000,192.168.3.232:20000,192.168.3.233:20000 --port 30000 --chunkSize 1 --logpath /data/mongos.log --logappend --fork

5、配置 Shard Cluster 

连接到其中一台机器的端口 30000 的 mongos 进程,并切换到 admin 数据库做以下配置。

[root@localhost bin]# ./mongo --port 30000 
MongoDB shell version: 1.8.1 
connecting to: 127.0.0.1:30000/test 
> use admin 
switched to db admin 
>db.runCommand({addshard:"shard1/192.168.3.231:27017,192.168.3.232:27017,192.168.3.233:27017"}); 
{ "shardAdded" : "shard1", "ok" : 1 } 
>db.runCommand({addshard:"shard2/192.168.3.231:27018,192.168.3.232:27018,192.168.3.233:27018"});
{ "shardAdded" : "shard2", "ok" : 1 } 
>

激活数据库及集合的分片:

db.runCommand({ enablesharding:"test" }) 
db.runCommand({ shardcollection: "test.users", key: { _id:1 }})

6、验证 Sharding 正常工作

连接到其中一台机器的端口 30000 的 mongos 进程,并切换到 test 数据库,以便添加测试数据。

use test 
for(var i=1;i<=200000;i++) db.users.insert({id:i,addr_1:"Beijing",addr_2:"Shanghai"}); 
db.users.stats() 
{ 
     "sharded" : true, 
     "ns" : "test.users", 
     "count" : 200000, 
     "size" : 25600384, 
     "avgObjSize" : 128, 
     "storageSize" : 44509696, 
     "nindexes" : 2, 
     "nchunks" : 15, 
     "shards" : { 
         "shard0000" : { 
…… 
         }, 
         "shard0001" : { 
…… 
         } 
     }, 
     "ok" : 1 
}

可以看到Sharding搭建成功了,跟我们期望的结果一致。

猜你喜欢

转载自blog.csdn.net/qq_35029061/article/details/128646992