MongoDB进化论

MongoDB被定义为分布式文档存储数据库,从定义上说,首先它要是个数据库,用于文档存储的,而后是支持分布式。现下,我们可以看下它是如何做到分布式文档存储的。


一、传统CRUD


1. Create: insert插入

MongoDB是以bson(类似json)为存储结构的数据库,所以可以像下面这样插入数据:


做插入时,不需要先创建表,直接插入即可。


2. Retrieve: find查询

MongoDB是以javascript语法操作的数据库,所以可以进行如下查询:


find({uid: 1})的意思翻译成sql就是: "select * from users where uid = 1",里面的"_id"字段是mongodb自动生成的。


3. Update: update更新


4. Delete: deleteOne



还有其他更详尽操作,请查询mongodb官网,这里不做介绍。


二、自举


1. 查看有哪些数据库


2. 现在用的哪个库


3. 选择数据库


4. 有哪些表


有了这些基础元信息后,就可以根据基本CRUD操作里面数据了。


三、复制集


1. 理论

有了基础数据存储能力后,如何保证其高可用呢?单机是不太稳的,所以一般做法如mysql主从复制,或redis的replica,可以把数据在多台机器上做备份,就不至于机器完蛋后数据也跟着完蛋了。再就是保证主节点挂掉后,能自动切换到其他节点提供服务。MongoDB一开始做了主从复制,但不支持自动切换机制,于是后来用Replica Set复制集替代了之前的主从结构。

MongoDB复制集中一般需要三种角色:Primary主节点、Secondaries复制节点、Arbiter投票节点。其中Arbiter节点不是必须的,只有当Secondaries数量为偶数时,才需要Arbiter。比如有两个复制节点,当主节点挂了之后,两个复制节点都想把自己提升为Primary,各投自己一票,陷入江局,这时候就需要Arbiter了。如果有奇数个从节点,第一次投票有可能还是每个节点各一票,但后续就能打破江局。(实际中偶数个节点一般也能打破江局。)


2. 部署

下面开始动手实操:

这里,开三台机器,172.18.1.1、172.18.1.2、172.18.1.3,在三台机器上执行:

mongod --replSet shit
在其中任意一台机器上开mongo的shell客户端,把三台机器加入replica set:

rs.initiate({_id: "shit", members: [
{_id: 1, host: "172.18.1.1:27017"},
{_id: 2, host: "172.18.1.2:27017"},
{_id: 3, host: "172.18.1.3:27017"}
]})
执行结果:

{ "ok" : 1 }
表示成功把三台机器加入同一复制集中。

查看复制集状态:

rs.status()
输出:

{
	"set" : "shit",
	"date" : ISODate("2017-10-06T05:11:01.269Z"),
	"myState" : 1,
	"term" : NumberLong(1),
	"heartbeatIntervalMillis" : NumberLong(2000),
	"optimes" : {
		"lastCommittedOpTime" : {
			"ts" : Timestamp(1507266657, 1),
			"t" : NumberLong(1)
		},
		"appliedOpTime" : {
			"ts" : Timestamp(1507266657, 1),
			"t" : NumberLong(1)
		},
		"durableOpTime" : {
			"ts" : Timestamp(1507266657, 1),
			"t" : NumberLong(1)
		}
	},
	"members" : [
		{
			"_id" : 1,
			"name" : "172.18.1.1:27017",
			"health" : 1,
			"state" : 1,
			"stateStr" : "PRIMARY",
			"uptime" : 453,
			"optime" : {
				"ts" : Timestamp(1507266657, 1),
				"t" : NumberLong(1)
			},
			"optimeDate" : ISODate("2017-10-06T05:10:57Z"),
			"electionTime" : Timestamp(1507266535, 1),
			"electionDate" : ISODate("2017-10-06T05:08:55Z"),
			"configVersion" : 1,
			"self" : true
		},
		{
			"_id" : 2,
			"name" : "172.18.1.2:27017",
			"health" : 1,
			"state" : 2,
			"stateStr" : "SECONDARY",
			"uptime" : 136,
			"optime" : {
				"ts" : Timestamp(1507266657, 1),
				"t" : NumberLong(1)
			},
			"optimeDurable" : {
				"ts" : Timestamp(1507266657, 1),
				"t" : NumberLong(1)
			},
			"optimeDate" : ISODate("2017-10-06T05:10:57Z"),
			"optimeDurableDate" : ISODate("2017-10-06T05:10:57Z"),
			"lastHeartbeat" : ISODate("2017-10-06T05:11:00.074Z"),
			"lastHeartbeatRecv" : ISODate("2017-10-06T05:11:00.822Z"),
			"pingMs" : NumberLong(0),
			"syncingTo" : "172.18.1.1:27017",
			"configVersion" : 1
		},
		{
			"_id" : 3,
			"name" : "172.18.1.3:27017",
			"health" : 1,
			"state" : 2,
			"stateStr" : "SECONDARY",
			"uptime" : 136,
			"optime" : {
				"ts" : Timestamp(1507266657, 1),
				"t" : NumberLong(1)
			},
			"optimeDurable" : {
				"ts" : Timestamp(1507266657, 1),
				"t" : NumberLong(1)
			},
			"optimeDate" : ISODate("2017-10-06T05:10:57Z"),
			"optimeDurableDate" : ISODate("2017-10-06T05:10:57Z"),
			"lastHeartbeat" : ISODate("2017-10-06T05:11:00.074Z"),
			"lastHeartbeatRecv" : ISODate("2017-10-06T05:11:00.822Z"),
			"pingMs" : NumberLong(0),
			"syncingTo" : "172.18.1.1:27017",
			"configVersion" : 1
		}
	],
	"ok" : 1
}
可以看到,一primary,两secondaries。

再回过头来看配置:

rs.conf()
输出:

{
	"_id" : "shit",
	"version" : 1,
	"protocolVersion" : NumberLong(1),
	"members" : [
		{
			"_id" : 1,
			"host" : "172.18.1.1:27017",
			"arbiterOnly" : false,
			"buildIndexes" : true,
			"hidden" : false,
			"priority" : 1,
			"tags" : {
				
			},
			"slaveDelay" : NumberLong(0),
			"votes" : 1
		},
		{
			"_id" : 2,
			"host" : "172.18.1.2:27017",
			"arbiterOnly" : false,
			"buildIndexes" : true,
			"hidden" : false,
			"priority" : 1,
			"tags" : {
				
			},
			"slaveDelay" : NumberLong(0),
			"votes" : 1
		},
		{
			"_id" : 3,
			"host" : "172.18.1.3:27017",
			"arbiterOnly" : false,
			"buildIndexes" : true,
			"hidden" : false,
			"priority" : 1,
			"tags" : {
				
			},
			"slaveDelay" : NumberLong(0),
			"votes" : 1
		}
	],
	"settings" : {
		"chainingAllowed" : true,
		"heartbeatIntervalMillis" : 2000,
		"heartbeatTimeoutSecs" : 10,
		"electionTimeoutMillis" : 10000,
		"catchUpTimeoutMillis" : 60000,
		"getLastErrorModes" : {
			
		},
		"getLastErrorDefaults" : {
			"w" : 1,
			"wtimeout" : 0
		},
		"replicaSetId" : ObjectId("59d70fdc605f3389c2eb5b8e")
	}
}
从该配置中,可以看出,"_id"就是刚才命令行里启动mongod时--replSet参数的值。如果"arbiterOnly"=true表示是投票节点,只投票,不存储数据。如果"hidden"为true表示该节点只用作备份数据,可投票,不竞选primary, 可读对客户端不可见(应用程序客户端用db.isMaster()命令读取所有节点)。如果"priority"=0则表示该节点不能做为primary,但是可读,即做读写分离时,从该节点读,同一replSet中priority最大者为primary。"heartbeatIntervalMillis"表示心跳间隔是2000ms,"heartbeatTimeoutSecs"表示心跳超时时间为10s,即每2s发一心跳包,如果10s内心跳超时,则认为该节点挂掉,开始新一轮选举,选出新的primary。

3. 复制测试
接下来,在primary里写一条数据:

db.users.insert({uid: 1, nick: "eachain"})
将mongo shell连到一个secondary上查看数据:

mongo --host 172.18.1.2
db.users.find()
Error: error: {
	"ok" : 0,
	"errmsg" : "not master and slaveOk=false",
	"code" : 13435,
	"codeName" : "NotMasterNoSlaveOk"
}
哦,出错了,从节点不可读,要先执行下面命令:

db.getMongo().setSlaveOk()

可以看到,primary上的数据已经到secondary上来了。


4. 宕机测试

现在把作为primary的172.18.1.1停掉,对,直接拔电源是最好的,模拟宕机嘛:

rs.status()

发现172.18.1.1已经不可达了。


172.18.1.2提升成了primary,172.18.1.3依旧是secondary。


5. 恢复测试

现在往该复制集中插入一条数据:

db.users.insert({uid: 2, nick: "yc"})
再把172.18.1.1启动起来:


发现172.18.1.1成了secondary,把mongo连到172.18.1.1上查看数据:

mongo 172.18.1.1:27017

数据仍是完整的。


6. 修改投票节点

现在,我不想让172.18.1.3上存数据了,让它只作为一个投票节点就好了,需要先把节点172.18.1.3摘掉,再添加进去:


指定它为投票节点。现在再看复制集状态:


去该节点上查看数据:


会报错,不是master或secondary。


7. 切换primary节点

个人还是偏爱172.18.1.1节点,想让它成为主节点呢。

cfg = rs.conf()
cfg.members[0].priority = 2
rs.reconfig(cfg)
查看状态:rs.status()



可以看到角色转换完成。即,更改priority配置后会重新选举primary。


8. 设置只备份节点

是的,我的172.18.1.1节点不会宕机的(蜜汁自信),我要把172.18.1.2设置成只备份节点。

cfg = rs.conf()
cfg.members[1].priority = 0
cfg.members[1].hidden = true
rs.reconfig(cfg)

用shell连上去,hidden节点是可读的,但应用程序客户端是不可见的,没有该节点。
当我们把配置改回去后:


节点172.18.1.2对应用程序又是可见的了。同时,根据"primary"字段也可知它是只读的(secondary只读)。

9. oplog与journal

主从复制,总是有延迟的,这时候就需要有一个窗口让从库跟上主库,像mysql中的binlog,redis中的in-memory backlog,都是把数据操作(不包括查询)记录下来,从库把操作拉到本地进行重放,达到数据一致。这种同步在mongodb中用oplog实现,它从本质上说,更像redis的in-memory backlog,它并不全部保存,超过限制内存后就会进行覆盖写了,而落后太多的(超出oplog记录范围)从库要从全复制开始了。设置oplog的大小要根据实际物理机器配置、业务写入量和速度等诸多因素权衡了。记得不要设置的太大导致OOM错误。

journal是存储引擎上的概念,与上面说的主从复制不是一个层面上的东西。对于mongodb的写操作不会马上落盘持久化,如果中途宕机,会导致部分数据丢失(默认配置为1分钟,即丢失1分钟内数据)。打开journal选项,可以记录每次写操作,在mongodb重启时,通过journal还原没有落盘的数据(默认journal落盘时间间隔为100ms)。和influxdb中记录cache的wal文件一样,记录最新还没有落盘的操作,重启时重新构造cache,这样不会因为宕机丢失数据。


四、分片


有了复制集后,可以保证存储的高可用了,但是总的存储量却没增大,所以下一步是如何扩大存储量,也就是mongodb的分片功能了。可以这样理解分片功能:将原来的一个复制集当成一个高可用的存储节点,然后由多个节点共同存储一份数据,达到存储量是所有节点存储量的总和。把复制集想象成raid1,一个高可用的mirror磁盘,把分片想象成raid0,一种快速写入并且存储量翻倍的磁盘结构。mongodb的分片结构是在复制集的基础上做的,所以mongodb的分片可以理解为raid0+1。


向多个分片中写数据,需要一个路由功能,负责写数据的扩散,和查询数据的汇总。该路由理论上可以由应用程序实现,但mongo官方想的比较周到,提供了mongos作为路由。mongos不存储数据,只做数据的搬运工,所以mongos是无状态,可根据需要水平扩展的。路由需要一些配置信息,这些配置信息存放在一个配置中心中,mongos的配置中心用的mongodb,这样所有的mongos可以共享同一份配置数据,一改全改,不会出现路由错误。


还是先准备三台机器:172.18.1.1、172.18.1.2、172.18.1.3,做三个复制集,一个复制集是一个分片,三个分片组成一个集群。如图:



1. 创建三个复制集

这里约定一下,mongos启用2000端口,shard server启用2001、2002和2003端口,config server启用3000端口。

在三台机器上分别执行下面命令:

mkdir -p /data/cfg/db /data/cfg/log
mkdir -p /data/sh1/db /data/sh1/log
mkdir -p /data/sh2/db /data/sh2/log
mkdir -p /data/sh3/db /data/sh3/log
mongod --replSet cfg --configsvr --port 3000 --dbpath /data/cfg/db --logpath /data/cfg/log/config.log --fork
mongod --replSet sh1 --shardsvr --port 2001 --dbpath /data/sh1/db --logpath /data/sh1/log/sh1.log --fork
mongod --replSet sh2 --shardsvr --port 2002 --dbpath /data/sh2/db --logpath /data/sh2/log/sh2.log --fork
mongod --replSet sh3 --shardsvr --port 2003 --dbpath /data/sh3/db --logpath /data/sh3/log/sh3.log --fork
连接到172.18.1.1:3000配置config server:

mongo 172.18.1.1:3000
rs.initiate({_id: "cfg", members: [
    {_id: 1, host: "172.18.1.1:3000"},
    {_id: 2, host: "172.18.1.2:3000"},
    {_id: 3, host: "172.18.1.3:3000"}
]})
连接到172.18.1.1:2001配置shard1:

mongo 172.18.1.1:2001
rs.initiate({_id: "sh1", members: [
    {_id: 1, host: "172.18.1.1:2001", priority: 2},
    {_id: 2, host: "172.18.1.2:2001"},
    {_id: 3, host: "172.18.1.3:2001", arbiterOnly: true}
]})
连接到172.18.1.2:2002配置shard2:

mongo 172.18.1.2:2002
rs.initiate({_id: "sh2", members: [
    {_id: 1, host: "172.18.1.1:2002", arbiterOnly: true},
    {_id: 2, host: "172.18.1.2:2002", priority: 2},
    {_id: 3, host: "172.18.1.3:2002"}
]})
连接到172.18.1.3:2003配置shard3:

mongo 172.18.1.3:2003
rs.initiate({_id: "sh3", members: [
    {_id: 1, host: "172.18.1.1:2003"},
    {_id: 2, host: "172.18.1.2:2003", arbiterOnly: true},
    {_id: 3, host: "172.18.1.3:2003", priority: 2}
]})
到三台机器上开启mongos:

mkdir -p /data/mgs/log
mongos --port 2000 --configdb cfg/172.18.1.1:3000,172.18.1.2:3000,172.18.1.3:3000 --logpath /data/mgs/log/mgs.log --fork
随便连接到一台mongos:

mongo 172.18.1.1:2000
添加分片:

db.adminCommand({addShard: "sh1/172.18.1.1:2001,172.18.1.2:2001,172.18.1.3:2001"})
db.adminCommand({addShard: "sh2/172.18.1.1:2002,172.18.1.2:2002,172.18.1.3:2002"})
db.adminCommand({addShard: "sh3/172.18.1.1:2003,172.18.1.2:2003,172.18.1.3:2003"})
设置数据库test(或其他什么database name)使用分片:

db.adminCommand({enableSharding: "test"})
为test库的users表设置使用分片:

db.adminCommand({shardCollection: "test.users", key: {uid: 1}})
现在可以看到:



2. 插入数据

插入十万条数据:

for (var i = 1; i <= 100000; i++) db.users.save({uid: 1000000+i, nick: "user_"+(1000000+i)})
……嗯,等会儿吧……
运行命令:

db.users.stats()
查看分片情况:




数量相差如此之大,什么鬼?
运行下面命令:

mongos> use config
switched to db config
mongos> db.chunks.find().pretty()
{
	"_id" : "test.users-uid_MinKey",
	"lastmod" : Timestamp(2, 0),
	"lastmodEpoch" : ObjectId("59d747692e1f99b345dde2c9"),
	"ns" : "test.users",
	"min" : {
		"uid" : { "$minKey" : 1 }
	},
	"max" : {
		"uid" : 1000002
	},
	"shard" : "sh2"
}
{
	"_id" : "test.users-uid_1000002.0",
	"lastmod" : Timestamp(3, 0),
	"lastmodEpoch" : ObjectId("59d747692e1f99b345dde2c9"),
	"ns" : "test.users",
	"min" : {
		"uid" : 1000002
	},
	"max" : {
		"uid" : 1000018
	},
	"shard" : "sh3"
}
{
	"_id" : "test.users-uid_1000018.0",
	"lastmod" : Timestamp(3, 1),
	"lastmodEpoch" : ObjectId("59d747692e1f99b345dde2c9"),
	"ns" : "test.users",
	"min" : {
		"uid" : 1000018
	},
	"max" : {
		"uid" : { "$maxKey" : 1 }
	},
	"shard" : "sh1"
}

发现范围太大了,嗯,我们的数据量太小了,一个trunk都装不满……。


MongoDB中分片方法中有三种:范围(正序、逆序),hashed,geo。上面我们用的是范围划分,而且数据量较小,所以大部分都落在了一个范围内,那当数据量小时能不能也分布均匀一些呢?当然可以,没错,用hashed。


下面删除了这张表,重建hashed索引:





现在看着是不是均匀了很多呢?


但是注意,当实际应用中,还是范围索引用的更多,mongodb的balancer可以自动均衡,当用到组合索引时,只能用范围分片了,而且更适合于查询操作。注意:hashed分片方法不能用组合索引,而且查询时不能用其它组合索引,比较坑。


接下来的问题是,如果数据量还是大,需要扩容,怎么办?这时候如果用范围分片的方法,就会很爽了,因为直接添加分片,就会往新分片上写数据了。


好了,总结下来,MongoDB在设计上还是很优秀的,把很多应用场景需要解决的问题,都提供了解决方案,值得推荐。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值