MongoDB分片迁移原理与源码
MongoDB架构
单节点
单个节点的MongoDB实例,具备MongoDB基本的功能和服务能力,不过缺乏数据冗余和高可用,以及横向扩展的能力,一般很少在实际生产环境中使用。
副本集
MongoDB的副本集,是指一组具有相同数据的mongod节点服务的集合。副本集架构可以实现数据冗余以及高可用。
一个基本的副本集架构如下:
一个副本集下,一主多备因为有用相同的数据,这种复制机制可以减少单节点下数据丢失的风险。
另外,副本集内的节点之间是通过异步复制oplog的方式,来实现节点之间数据的一致的。MongoDB的数据一致性是基于Raft协议改进实现的。
MongoDB复制流程与Raft协议有一些基本的差别,包括:
- 选举差异。MongoDB的节点可以设置优先级并设置了多种节点角色,Raft无此概念。MongoDB的副本集的心跳是节点两两互发的,而Raft是主节点发,备节点回复。MongoDB的主节点在不能收到大多数节点的心跳的时候,就会自动降级,防止出现多主和过期主,而Raft的主节点再收到更高任期的主节点心跳的时候才会降级。
- 日志复制。MongoDB的日志复制是异步过程,主节点收到写操作时,先在本地应用写,再写一个日志后,其他节点去拉取日志把写操作应用到本地节点,而Raft是写一个日志并复制到大多数节点,然后主节点再将写应用到本地后反馈给用户,之后告知其他节点应用写操作。MongoDB中的日志是备节点去主节点拉取,而Raft是主节点推到备节点。MongoDB通过Write concern帮助用户更好的选项配置数据写到哪些节点后返回给用户,而Raft就是日志复制到大多数节点后返回。
参考:
Raft协议图解
分片集群
副本集架构虽然提高了数据安全和系统可用性,但是并不能提高数据的容量和大数据量下的服务读写能力。基于分片集群架构的MongoDB,可以实现数据分布在多个不同节点上实现数据的横向扩展以支持大数据量,而同时可以提高服务的整体读写能力。
一个基本的分片集群架构如下:
分片集群架构通过将数据进行拆分,部署到不同的shard节点上,将读写压力进行了分摊,同时在增加分片的情况下,可以进一步提高整体服务的读写负载能力。
另外通过添加shard,可以不断提高整体服务的数据容量,实现数据的水平扩容。
最后分片可以提高服务整体的可用性,及时一个分片一部分数据出现问题,其他分片和数据也可以在一定程序下继续提供服务。
分片迁移
数据块管理
在分片集群下,MongoDB提供了分片键的概念,基于该键去进行数据的分布规则,可以基于hash,可以基于range。
但是不管基于hash还是range都可能导致数据分布不均匀,或者分片集群新增shard节点的时候,都需要对数据进行动态调整,以实现数据在各分片的均衡分布,以平衡各shard的服务。
为了解决分片集群中,集合数据分布不均匀的问题,MongoDB提供了balance功能,该功能可以在后台监测各个shard数据块(chunk)的情况,在满足条件的情况下,会将数据块从一个shard(数据库多)迁移到另外一个shard(数据块少),直到集合的数据块在各shard中分布均匀。
MongoDB是依据分片键来管理数据块,每块数据都是整个数据的一个子集。
当用户通过mongos访问MongoDB服务进行数据写入的时候,会根据分片键、分片策略等将数据路由到某一个分片,写入保存,生成一个个数据块。而有数据插入和更新导致数据块超过限制的时候,MongoDB会对数据块进行拆分(split chunk)。
MongoDB中默认的数据块大小是64M,该值可以增大或减少。更小的数据块会产生更频繁的数据迁移,可以实现数据更大程度的均衡;更大的数据块会减少迁移,但是存在数据不均衡的风险。
拆分数据块只发生在插入和更新时;如果调低快大小,有可能导致所有数据块都拆分成新的快;如果调高快大小,已有的数据块必须通过插入或修改的方式达到新的大小。拆分不能是未完成状态。
当数据块因为拆分越来越多后,一定条件下会触发move chunk。这个判断和检测是否需要迁移数据块的进程为balancer。balancer会定期检测不同分片的数据块信息,如果含有最多块的分片的块数比含有最少块的分片的块数超过一定大小,就会认为是不均衡的状态,需要进行迁移。
另外当有shard添加到分片集群中,也会发生不均衡(因为有shard块数为0);当要删除一个shard的时候,也会发生不均衡(因为要重新分配该shard上的数据块)。
块迁移流程
- 平衡器进程将move chunk的命令发送到迁移的源shard;
- 源shard使用一个内部move chunk命令开始移动。在迁移过程中,对该数据块的操作还路由到源shard,源shard负责对该数据块的写操作的处理;
- 目标shard创建所需要的索引;
- 目标shard开始请求数据块的文档并接收数据的拷贝;
- 再接收完数据块中最后一个文档,目标shard开始同步进程以确保迁移过程中对迁移文档的修改也同步过来了;
- 完全同步之后,源shard连接config服务器,使用数据块的新位置更新集群元数据;
- 再修改完元数据后,如果源shard上的chunk没有打开的游标了,源shard就会删除这些文档的旧拷贝。
注意:如果balancer需要操作其他块迁移从源shard,那么balancer不用等待这些旧文档删除,就可以立刻进行下一个块迁移操作。因为这些删除操作是异步的。
异步迁移块清理
要从一个分片迁移多个块,平衡器一次迁移一个块。但是,平衡器在开始下一个块迁移之前不会等待当前迁移流程的删除阶段完成。
如果存在大量块需要迁移的时候(比如新shard加入),可以不需要等待上一个chunk的删除,就可以进行下一个chunk迁移,提高整体迁移的速度。
MongoDB提供了一个参数去设置是否异步删除:_waitForDelete。迁移一个 chunk 数据以后,是否同步等待数据删除完毕;默认为 false, 由一个单独的线程异步删除旧数据。
由于块迁移流程的操作不能做到原子性,从在异步流程,如果在上述操作步骤4/5/6/7出现宕机或网络问题等问题导致迁移中断,都可能出现问题,导致数据不一致、孤儿文档等问题,这也是本文章主要关注的点。
未完,待续
参考文档
MongoDB官方文档
孤儿文档是怎样产生的(MongoDB orphaned document)
MongoDB疑难解析:为什么升级之后负载升高了?
由数据迁移至MongoDB导致的数据不一致问题及解决方案