一、架构

MongoDB的分片机制 mongodb分片原理_MongoDB的分片机制


在大数据领域常常提到的4V特征中,Volume(数据量大)是首当其冲被提及的。由于单机垂直扩展能力的局限,水平扩展的方式则显得更加的靠谱。MongoDB 自带了这种能力,可以将数据存储到多个机器上以提供更大的容量和负载能力。此外,同时为了保证数据的高可用,MongoDB 采用副本集的方式来实现数据复制。一个典型的MongoDB集群架构会同时采用分片+副本集的方式,如下图:

MongoDB的分片机制 mongodb分片原理_高可用_02

数据分片(Shards) 分片用于存储真正的集群数据,可以是一个单独的 Mongod实例,也可以是一个副本集。 生产环境下Shard一般是一个 Replica Set,以防止该数据片的单点故障。对于分片集合(sharded collection)来说,每个分片上都存储了集合的一部分数据(按照分片键切分),如果集合没有分片,那么该集合的数据都存储在数据库的 Primary Shard中。
(分片是逻辑概念,可以这样理解有3台机器,要部署两个分片,每个分片有一个副本集。
机器1部署Primary,机器2部署secondary, 机器3部署secondary,设置他们属于同一个分片1。
机器1部署Primary,机器2部署secondary, 机器3部署secondary,设置他们属于同一个分片2
)

配置服务器(Config Servers) 保存集群的元数据(metadata),包含各个Shard的路由规则,配置服务器由一个副本集(ReplicaSet)组成。

查询路由(Query Routers) Mongos是 Sharded Cluster 的访问入口,其本身并不持久化数据 。Mongos启动后,会从 Config Server 加载元数据,开始提供服务,并将用户的请求正确路由到对应的Shard。Sharding 集群可以部署多个 Mongos 以分担客户端请求的压力。

replica set集群特性

1. n个不同类型节点组成
2. 每个节点数据相同
3. 建议至少有一个primary和两个secondary节点
4. 集群中只能有一个primary节点
5. 写请求都通过primary节点
6. 支持自动故障恢复
7. primary节点不可用时,通过投票选举从secondary节点列表中选出primary节点,因此最好节点数量是奇数
8. secondary节点从primary节点通过oplog异步方式同步数据

shard的replica set的架构图:

MongoDB的分片机制 mongodb分片原理_mongodb_03


config servers的replica set的架构图:

MongoDB的分片机制 mongodb分片原理_副本集_04

集群机器配置说明

MongoDB的分片机制 mongodb分片原理_mongodb_05

节点类型
primary
主节点,负责集群数据维护,所有数据更新操作都通过主节点
secondary
从节点,从主节点复制数据,提供读请求响应
arbiter
选举节点,不保存数据,只参与primary投票选举

二、分片机制
下面的几个细节,对于理解和应用 MongoDB 的分片机制比较重要,所以有必要提及一下:

1. 数据如何切分

首先,基于分片切分后的数据块称为 chunk,一个分片后的集合会包含多个 chunk,每个 chunk 位于哪个分片(Shard) 则记录在 Config Server(配置服务器)上。Mongos 在操作分片集合时,会自动根据分片键找到对应的 chunk,并向该 chunk 所在的分片发起操作请求。

数据是根据分片策略来进行切分的,而分片策略则由 分片键(ShardKey)+分片算法(ShardStrategy)组成。

MongoDB 支持两种分片算法:

范围分片:

MongoDB的分片机制 mongodb分片原理_数据_06

如上图所示,假设集合根据x字段来分片,x的取值范围为[minKey, maxKey](x为整型,这里的minKey、maxKey为整型的最小值和最大值),将整个取值范围划分为多个chunk,每个chunk(默认配置为64MB)包含其中一小段的数据:如 Chunk1 包含x的取值在[minKey, -75)的所有文档,而Chunk2包含x取值在[-75, 25)之间的所有文档…

范围分片能很好的满足范围查询的需求,比如想查询x的值在[-30, 10]之间的所有文档,这时 Mongos 直接能将请求路由到 Chunk2,就能查询出所有符合条件的文档。 范围分片的缺点在于,如果 ShardKey 有明显递增(或者递减)趋势,则新插入的文档多会分布到同一个chunk,无法扩展写的能力,比如使用_id作为 ShardKey,而MongoDB自动生成的id高位是时间戳,是持续递增的。

哈希分片:

MongoDB的分片机制 mongodb分片原理_MongoDB的分片机制_07


Hash分片是根据用户的 ShardKey 先计算出hash值(64bit整型),再根据hash值按照范围分片的策略将文档分布到不同的 chunk。由于 hash值的计算是随机的,因此 Hash 分片具有很好的离散性,可以将数据随机分发到不同的 chunk 上。 Hash 分片可以充分的扩展写能力,弥补了范围分片的不足,但不能高效的服务范围查询,所有的范围查询要查询多个 chunk 才能找出满足条件的文档。

2. 如何保证均衡

如前面的说明中,数据是分布在不同的 chunk上的,而 chunk 则会分配到不同的分片上,那么如何保证分片上的 数据(chunk) 是均衡的呢?在真实的场景中,会存在下面两种情况:

A. 全预分配,chunk 的数量和 shard 都是预先定义好的,比如 10个shard,存储1000个chunk,那么每个shard 分别拥有100个chunk。此时集群已经是均衡的状态(这里假定)

B. 非预分配,这种情况则比较复杂,一般当一个 chunk 太大时会产生分裂(split),不断分裂的结果会导致不均衡;或者动态扩容增加分片时,也会出现不均衡的状态。 这种不均衡的状态由集群均衡器进行检测,一旦发现了不均衡则执行 chunk数据的搬迁达到均衡。

MongoDB 的数据均衡器运行于 Primary Config Server(配置服务器的主节点)上,而该节点也同时会控制 Chunk 数据的搬迁流程。

MongoDB的分片机制 mongodb分片原理_副本集_08


3. 应用高可用应用节点可以通过同时连接多个 Mongos 来实现高可用,如下:

MongoDB的分片机制 mongodb分片原理_数据_09


选举

MongoDB 副本集通过 Raft 算法来完成主节点的选举,这个环节在初始化的时候会自动完成,如下面的命令:

MongoDB的分片机制 mongodb分片原理_副本集_10


initiate 命令用于实现副本集的初始化,在选举完成后,通过 isMaster()命令就可以看到选举的结果:

MongoDB的分片机制 mongodb分片原理_MongoDB的分片机制_11

心跳

在高可用的实现机制中,心跳(heartbeat)是非常关键的,判断一个节点是否宕机就取决于这个节点的心跳是否还是正常的。副本集中的每个节点上都会定时向其他节点发送心跳,以此来感知其他节点的变化,比如是否失效、或者角色发生了变化。利用心跳,MongoDB 副本集实现了自动故障转移的功能,如下图:

MongoDB的分片机制 mongodb分片原理_MongoDB的分片机制_12


默认情况下,节点会每2秒向其他节点发出心跳,这其中包括了主节点。 如果备节点在10秒内没有收到主节点的响应就会主动发起选举。此时新一轮选举开始,新的主节点会产生并接管原来主节点的业务。 整个过程对于上层是透明的,应用并不需要感知,因为 Mongos 会自动发现这些变化。如果应用仅仅使用了单个副本集,那么就会由 Driver 层来自动完成处理。

复制

主节点和备节点的数据是通过日志(oplog)复制来实现的,这很类似于 mysql 的 binlog。在每一个副本集的节点中,都会存在一个名为local.oplog.rs的特殊集合。 当 Primary 上的写操作完成后,会向该集合中写入一条oplog, 而 Secondary 则持续从 Primary 拉取新的 oplog 并在本地进行回放以达到同步的目的。

下面,看看一条 oplog 的具体形式:

{
	"ts": Timestamp(
		1446011584,
		2
	),
	"h": NumberLong(
		"1687359108795812092"
	),
	"v": 2,
	"op": "i",
	"ns": "test.nosql",
	"o": {
		"_id": ObjectId(
			"563062c0b085733f34ab4129"
		),
		"name": "mongodb",
		"score": "100"
	}
}
其中的一些关键字段有:

ts 操作的 optime,该字段不仅仅包含了操作的时间戳(timestamp),还包含一个自增的计数器值。
h 操作的全局唯一表示
v oplog 的版本信息
op 操作类型,比如 i=insert,u=update..
ns 操作集合,形式为 database.collection
o 指具体的操作内容,对于一个 insert 操作,则包含了整个文档的内容
MongoDB 对于 oplog 的设计是比较仔细的,比如:

oplog 必须保证有序,通过 optime 来保证。
oplog 必须包含能够进行数据回放的完整信息。
oplog 必须是幂等的,即多次回放同一条日志产生的结果相同。
oplog 集合是固定大小的,为了避免对空间占用太大,旧的 oplog 记录会被滚动式的清理。

一致性
一致性是一个复杂的话题,而一致性更多从应用角度上提出的,比如:

向系统写入一条数据,应该能够马上读到写入的这个数据。

在分布式架构的CAP理论以及许多延续的观点中提到,由于网络分区的存在,要求系统在一致性和可用性之间做出选择,而不能两者兼得。

MongoDB的分片机制 mongodb分片原理_副本集_13

在 MongoDB 中,这个选择是可以由开发者来定的。 MongoDB 允许客户端为其操作设定一定的级别或者偏好,包括:

read preference 读取偏好,可指定读主节点、读备节点,或者是优先读主、优先读备、取最近的节点
write concern 写关注,指定写入结果达到什么状态时才返回,可以为无应答(none)、应答(ack),或者是大多数节点完成了数据复制等等
read concern 读关注,指定读取的数据版本处于怎样的状态,可以为读本地、读大多数节点写入,或者是线性读(linearizable)等等。
使用不同的设定将会产生对于C(一致性)、A(可用性)的不同的抉择,比如:

将读偏好设置为 primary,此时读写都在主节点上。这保证了数据的一致性,但一旦主节点宕机会导致失败(可用性降低)
将读偏好设置为 secondaryPrefered,此时写主,优先读备,可用性提高了,但数据存在延迟(出现不一致)
将读写关注都设置为 majority(大多数),一致性提升了,但可用性也同时降低了(节点失效会导致大多数写失败)
关于这种权衡的讨论会一直存在,而 MongoDB 除了提供多样化的选择之外,其主要是通过复制、基于心跳的自动failover等机制来降低系统发生故障时产生的影响,从而提升整体的可用性。