MongoDB 集群
优质博文:IT-BLOG-CN
一、高可用架构
高可用性HA(High Availability)
指的是缩短因正常运维或者非预期故障而导致的停机时间,提高系统可用性。
高可用量化衡量标准: 引出一个SLA
的概念。SLA
是Service Level Agreement
(服务等级协议)的缩写。SLA
就是用来量化可用性的协议,在双方认可的前提条件下,服务提供商与用户间定义的一种双方认可的协定。SLA
是判定服务质量的重要指标。
SLA 是怎么量化的?其实就是按照停服时间进行计算?举个例子:
1 年 = 365 天 = 8760 小时
99.9 停服时间:8760 * 0.1% = 8760 * 0.001 = 8.76小时
99.99 停服时间:8760 * 0.0001 = 0.876 小时 = 52.6 分钟
99.999 停服时间:8760 * 0.00001 = 0.0876 小时 = 5.26分钟
也就是说,如果一家公有云厂商提供对象存储的服务,SLA
协议指明提供5
个9
的高可用服务,那就要保证一年的时间内对象存储的停服时间少于5.26
分钟,如果超过这个时间,就算违背了SLA
协议,可以找公有云提出赔偿。
前面我们说过,无论是数据的高可靠,还是组件的高可用全都是一个解决方案:冗余。我们通过多个组件和备份导致对外提供一致性和不中断的服务。冗余是根本,但是怎么来使用冗余则各有不同。
以下我们就按照不同的冗余处理策略,可以总结出MongoDB
几个特定的模式,这个也是通用性质的架构,在其他的分布式系统也是常见的。
我们从Mongo
的三种高可用模式逐一介绍,这三种模式也代表了通用分布式系统下高可用架构的进化史,分别是Master-Slave
,Replica Set
,Sharding
模式。
二、副本集群
MongoDB
副本集架构通过部署多个服务器存储数据副本来达到高可用的能力,每一个副本集实例由一个Primary
节点和一个或多个 Secondary
节点组成。Primary
角色是通过整个集群共同选举出来的,每个服务都可能成为Primary
,每个服务最开始只是Secondary
,而这个选举过程完全自动,不需要人为参与。
Primary
节点: 负责处理客户端的读写请求。每个副本集架构实例中只能有一个Primary
节点。
读请求默认是发到
Primary
节点处理,如果需要故意转发到Secondary
需要客户端修改一下配置(注意:是客户端配置,决策权在客户端)。
Secondary
节点: 通过定期轮询Primary
节点的oplog
(操作日志)复制Primary
节点的数据,类似于mysql
的binlog
。保证数据与Primary
节点一致。在 Primary
节点故障时,多个Secondary
节点通过选举成为新的Primary
节点,保障高可用。在每一个副本集的节点中,都会存在一个名为local.oplog.rs
的特殊集合。 当Primary
上的写操作完成后,会向该集合中写入一条oplog
,而Secondary
则持续从Primary
拉取新的oplog
并在本地进行回放以达到同步的目的。
下面,看看一条oplog
的具体形式:
{
"ts" : Timestamp(1446017544, 2), #该字段不仅仅包含了操作的时间戳`timestamp`,还包含一个自增的计数器值。
"h" : NumberLong("1327359106895543098"), # 操作的全局唯一表示
"v" : 2, #oplog 的版本信息
"op" : "i", # 操作类型,比如 i=insert,u=update..
"ns" : "test.nosql", #操作集合,形式为 database.collection
"o" : { "_id" : ObjectId("543478v0b085733f34ab7698"), "name" : "mongodb", "score" : "100" } #指具体的操作内容,对于一个 insert 操作,则包含了整个文档的内容
}
MongoDB
对于oplog
的设计是比较仔细的,比如:
oplog
必须保证有序,通过optime
来保证。
oplog
必须包含能够进行数据回放的完整信息。
oplog
必须是幂等的,即多次回放同一条日志产生的结果相同。
oplog
集合是固定大小的,为了避免对空间占用太大,旧的oplog
记录会被滚动式的清理。
Secondary
相互有心跳,Secondary
可以作为数据源,Replica
可以是一种链式的复制模式。
Arbiter
仲裁者: 不存储数据,不会被选为主,只进行选主投票。使用Arbiter
可以减轻在减少数据的冗余备份,又能提供高可用的能力。
副本集的系统架构图如下:
MongoDB
的Replica Set
副本集模式主要有以下几个特点:
【1】数据多副本,在故障的时候,可以使用完的副本恢复服务。注意:这里是故障自动恢复;
【2】读写分离,读的请求分流到副本上,减轻主Primary
的读压力;
【3】节点直接互有心跳,可以感知集群的整体状态;
读写分离只能增加集群"读"的能力,对于写负载非常高的情况却无能为力。对此需求,使用分片集群并增加分片,或者提升数据库节点的磁盘
IO
、CPU
能力可以取得一定效果。
优缺点: 可用性大大增强,因为故障时自动恢复,主节点故障,立马就能选出一个新的Primary
节点。但是有一个要注意的点:每两个节点之间互有心跳,这种模式会导致节点的心跳几何倍数增大,单个Replica Set
集群规模不能太大,一般来讲最大不要超过50
个节点。参与投票节点数要是奇数,因为偶数会导致脑裂,也就是投票数对等的情况,无法选出Primary
。
选举
MongoDB
副本集通过Raft
算法链接来完成主节点的选举,这个环节在初始化的时候会自动完成,如下面的命令:
config = {
_id : "my_replica_set",
members : [
{_id : 0, host : "rs1.example.net:27017"},
{_id : 1, host : "rs2.example.net:27017"},
{_id : 2, host : "rs3.example.net:27017"},
]
}
rs.initiate(config)
initiate
命令用于实现副本集的初始化,在选举完成后,通过isMaster()
命令就可以看到选举的结果:
> db.isMaster()
{
"hosts" : [
"192.168.126.41:27030",
"192.168.126.42:27030",
"192.168.126.43:27030"
],
"setName" : "myReplSet",
"setVersion" : 1,
"ismaster" : true,
"secondary" : false,
"primary" : "192.168.126.41:27030",
"me" : "192.168.126.41:27030",
"electionId" : ObjectId("7fffffff0000000000000001"),
"ok" : 1
}
心跳
在高可用的实现机制中,心跳heartbeat
是非常关键的,判断一个节点是否宕机就取决于这个节点的心跳是否还是正常的。副本集中的每个节点上都会定时向其他节点发送心跳,以此来感知其他节点的变化,比如是否失效、或者角色发生了变化。利用心跳,MongoDB
副本集实现了自动故障转移的功能。
默认情况下,节点会每2
秒向其他节点发出心跳,这其中包括了主节点。 如果备节点在10
秒内没有收到主节点的响应就会主动发起选举。
此时新一轮选举开始,新的主节点会产生并接管原来主节点的业务。整个过程对于上层是透明的,应用并不需要感知,因为Mongos
会自动发现这些变化。
如果应用仅仅使用了单个副本集,那么就会由Driver
层来自动完成处理。
三、分发集群
MongoDB
分片集群Sharded Cluster
架构在副本集的基础上,通过多组复制集群的组合,实现数据的横向扩展。每一个分片集群实例由 mongos
节点、config server
、shard
节点等组件组成,解决大数据量问题。水平扩容扩展的容量仅需要根据需要添加其他服务器,这比垂直扩容一台高端硬件的机器成本还低,代价就是软件的基础结构要支持,部署维护要复杂。
纵向优化的方案非常容易到达物理极限,横向优化则对个体要求不高,而是群体发挥效果(但是对软件架构提出更高的要求)。
代理层mongos
节点: 这是个无状态的组件,纯粹是路由功能。负责接收所有客户端应用程序的连接查询请求,并将请求路由到集群内部对应的分片上,同时会把接收到的响应拼装起来返回到客户端。您可以拥有多个mongos
节点实现负载均衡及故障迁移。每一个分片集群实例可支持3
个-32
个mongos
节点。应用节点可以通过同时连接多个Mongos
来实现高可用,当然,连接高可用的功能是由Driver
实现的。
配置中心config server
节点: 代理层是无状态的模块,数据层的每一个Shard
是各自独立的,那总要有一个集群统配管理的地方,这个地方就是配置中心。负责存储集群和Shard
节点的元数据信息,如集群的节点信息、分片数据的路由信息、每个Shard
里大概存储了多少数据量等。配置中心存储的就是集群拓扑,管理的配置信息。这些信息也非常重要,所以也不能单点存储,所以配置中心也是一个Replica Set
集群,数据也是多副本的。ConfigServer
节点规格固定为1核2GB
,磁盘空间为20GB
,默认3
副本集,不可变更配置。
数据层shard
节点: 负责将数据分片存储在多个服务器上。其实数据层就是由一个个Replica Set
集群组成。这样的一个Replica Set
我们就叫做 Shard
。您可以拥有多个Shard
节点来横向扩展实例的数据存储和读写并发能力。每一个分片集群实例可支持2
个-20
个Shard
节点。
Sharding 模式怎么存储数据
我们说过,纵向优化是对硬件使用者最友好的,横向优化则对硬件使用者提出了更高的要求,也就是说软件架构要适配。
单Shard
集群是有限的,但Shard
数量是无限的,Mongo
理论上能够提供近乎无限的空间,能够不断的横向扩容。那么现在唯一要解决的就是怎么去把用户数据存到这些Shard
里?
首先,要选一个字段(或者多个字段组合也可以)用来做Key
,这个Key
可以是你任意指定的一个字段。我们现在就是要使用这个Key
来,通过某种策略算出发往哪个Shard
上。这个策略叫做:Sharding Strategy
,也就是分片策略。
分片切分后的数据块称为
chunk
,一个分片后的集合会包含多个chunk
,每个chunk
位于哪个分片Shard
则记录在Config Server
(配置服务器)上。分片策略则由分片键ShardKey
+分片算法ShardStrategy
组成。
我们把Sharding Key
作为输入,按照特点的Sharding Strategy
计算出一个值,值的集合形成了一个值域,我们按照固定步长去切分这个值域,每一个片叫做Chunk
,每个Chunk
出生的时候就和某个Shard
绑定起来,这个绑定关系存储在配置中心里。
所以,我们看到MongoDB
的用Chunk
再做了一层抽象层,隔离了用户数据和Shard
的位置,用户数据先按照分片策略算出落在哪个Chunk
上,由于Chunk
某一时刻只属于某一个Shard
,所以自然就知道用户数据存到哪个Shard
了。
Sharding
模式下数据写入过程:
Sharding
模式下数据读取过程:
通过上图我们也看出来了,mongos
作为路由模块其实就是寻路的组件,写的时候先算出用户key
属于哪个Chunk
,然后找出这个Chunk
属于哪个Shard
,最后把请求发给这个Shard
,就能把数据写下去。读的时候也是类似,先算出用户key
属于哪个Chunk
,然后找出这个Chunk
属于哪个Shard
,最后把请求发给这个Shard
,就能把数据读上来。
实际情况下,mongos
不需要每次都和Config Server
交互,大部分情况下只需要把Chunk
的映射表cache
一份在mongos
的内存,就能减少一次网络交互,提高性能。
为什么要多一层 Chunk 这个抽象?
为了灵活,因为一旦是用户数据直接映射到Shard
上,那就相当于是用户数据和底下的物理位置绑定起来了,这个万一Shard
空间已经满了,怎么办?
存储不了呀,又不能存储到其他地方去。有同学就会想了,那我可以把这个变化的映射记录下来呀,记录下来理论上行得通,但是每一个用户数据记录一条到Shard
的映射,这个量级是非常大的,实际中没有可行性。
而现在多了一层Chunk
空间,就灵活了。用户数据不再和物理位置绑定,而是只映射到Chunk
上就可以了。如果某个Shard
数据不均衡,那么可以把Chunk
空间分裂开,迁走一半的数据到其他Shard
,修改下Chunk
到Shard
的映射,Chunk
到Shard
的映射条目很少,完全Hold
住,并且这种均衡过程用户完全不感知。
讲回Sharding Strategy
是什么?本质上Sharding Strategy
是形成值域的策略而已,MongoDB
支持两种Sharding Strategy
:
【1】Hashed Sharding
的方式
【2】Range Sharding
的方式
Hashed Sharding
: 把Key
作为输入,输入到一个Hash
函数中,计算出一个整数值,值的集合形成了一个值域,我们按照固定步长去切分这个值域,每一个片叫做Chunk
,这里的Chunk
则就是整数的一段范围而已。
假设集合根据x
字段来分片,x
的取值范围为[minKey
, maxKey
](x
为整型,这里的minKey
、maxKey
为整型的最小值和最大值),将整个取值范围划分为多个chunk
,每个chunk
(默认配置为64M
B)
优点: 计算速度快、均衡性好,纯随机;
缺点: 正因为纯随机,排序列举的性能极差,比如你如果按照name
这个字段去列举数据,你会发现几乎所有的Shard
都要参与进来;
Range Sharding
: Range
的方式本质上是直接用Key
本身来做值,形成的Key Space
。
如上图例子,Sharding Key
选为name
这个字段,对于test_0
,test_1
,test_2
这样的key
排序就是挨着的,所以就全都分配在一个Chunk
里。
这
3
条Docuement
大概率是在一个Chunk
上,因为我们就是按照Name
来排序的。
优点: 对排序列举场景非常友好,因为数据本来就是按照顺序依次放在Shard
上的,排序列举的时候,顺序读即可,非常快速;
缺点: 容易导致热点,举个例子,如果Sharding Key
都有相同前缀,那么大概率会分配到同一个Shard
上,就盯着这个Shard
写,其他Shard
空闲的很,却帮不上忙;
为什么说Sharding
模式不仅是容量问题得到解决,可用性也进一步提升?
因为Shard(Replica Set)
集群个数多了,即使一个或多个Shard
不可用,Mongo
集群对外仍可以 提供读取和写入服务。因为每一个 Shard
都有一个Primary
节点,都可以提供写服务,可用性进一步提升。
如何保证均衡
数据是分布在不同的chunk
上的,而chunk
则会分配到不同的分片上,那么如何保证分片上的 数据chunk
是均衡的呢?
在真实的场景中,会存在下面两种情况:
【1】全预分配: chunk
的数量和shard
都是预先定义好的,比如10
个shard
,存储1000
个chunk
,那么每个shard
分别拥有100
个chunk
。此时集群已经是均衡的状态(这里假定)
【2】非预分配: 这种情况则比较复杂,一般当一个chunk
太大时会产生分裂split
,不断分裂的结果会导致不均衡;或者动态扩容增加分片时,也会出现不均衡的状态。 这种不均衡的状态由集群均衡器进行检测,一旦发现了不均衡则执行chunk
数据的搬迁达到均衡。
MongoDB
的数据均衡器运行于Primary Config Server
配置服务器的主节点上,而该节点也同时会控制Chunk
数据的搬迁流程。
对于数据的不均衡是根据两个分片上的Chunk
个数差异来判定的,阈值对应表如下:
Number of chuncks | Migration Threshold |
---|---|
< 20 | 2 |
20 - 79 | 4 |
> 80 | 8 |
MongoDB 的数据迁移对集群性能存在一定影响,这点无法避免,目前的规避手段只能是将 均衡窗口 对齐到业务闲时段。 |
参考文献:链接