MongoDB全量迁移断点续传功能学习与实现

1.    背景

       MongoDB是一个基于分布式文件存储的开源数据库系统,用户存储文本数据。MongoDB提供了一个面向文档的存储,操作起来比较简单容易,查询速度快等特点。数据迁移是数据库工作中经常见到的一个场景,比如扩容、备份、上云等需求,数据迁移包括全量迁移和增量迁移。在全量迁移的过程中,为了追求速度,提高迁移效率,通常使用高并发的方式,这种并发通常是行级别,而在迁移的过程中,由于经常因为某种原因导致迁移失败,比如网络断了,数据库连接满了,db宕机等等,当再次启动迁移任务的时候,通常希望从上一次失败的地方进行断点续传,这就对记录断点位置和处理断点的方式提出了一定的要求。本文中主要介绍如何支持mongodb的断点续传。由于本人对mongo也是小白,初次接触mongo数据库,如果文中有布正确的地方,欢迎大家指正。


2. 基础知识点

    Mongo中表称之为集合(collection),集中的一条记录称之为文档(document),一个文档中可以有多种数据类型(BOSN),其中文档是以key-value的形式存存储。故mongo的存储形式非常的灵活,在mongo也提供非常丰富的查询方式和表达式。在实际场景中,mongodb通常非常大量的数据和信息,因此对于mongo的扩容和迁移造成了一定的困难。尤其是在进行全量迁移的过程中,通常遇到一些非常棘手的问题,比如说如何对mongo的分片,并发迁移等等。本文主要针对mongo的分片和断点续传做一个详细的研究和提出一个可行的方案。

   在mongo迁移的过程,最原始的做法是利用一个查询db.collections.find(),把一个collection放在一个查询里面,然后通过流式的方式逐渐的查出数据,然后写入到目标库中。由于mongo本身就具备高效的查询效率,因此在使用过程中,对于几千万、几亿条数据的单个集合,如果不存在网络的问题的,速度会非常的快,通常能达到3wTPS/s的速度。但是如果遇到几十亿、上百亿、甚至更大的集合就会遇到一个很严重的问题,迁移到一半的时候,网络中断了,查询超时等种种原因,导致这个查询返回失败。那这个时候,我们就得重新开始迁移,没法断点续传,因为我们查询的顺序是mongo最原始的写入顺序,如果中间发生delete、update、insert等操作,那么就会丢失一部分数据。同时,这么大体量的数据放在一个分片里面,迁移的时间也会非常的漫长,那么对于后面的增量数据来说,可能就是个灾难。在这里我们经过研究,提出一个可行性的方案,支持mongodb的断点续传功能

2.1   mongo的主键字段_id特点

      mongodb数据库不像mysql、oracle等数据库,拥有表结构,字段、主键等约束信息。Mongodb的文档是灵活的,你可以无限制的添加key-value键值对,每个value的类型你也可以随意定义。因此在同一个key-value中,可以会存在多种多类型,这就导致了分片出现的困难。Mongo虽然没有主键的概念,但是拥有天然的主键字段_id,每插入一条document的时候,mongodb会自动添加一个字段_id, 这个字段全局是唯一的,因此可以当做主键来使用。该字段默认的类型的是ObjectId类型。但是在写入数据的时候,如果不指明_id字段,会默认是objectId,如果自己指定了这个值,则可以是任意类型,可以是int,long,double,string等各种类型。因此如果我们希望通过这个字段来进行分片,那么该字段的各种类型就是一个极大的麻烦,因为我们不知道一个collection中_id字段有多少中类型,如下根据mongo官方文档给出的类型。

 

类型

编号

别名

备注

double

1

double

 

string

2

string

 

object

3

object

 

array

4

array

 

binary data

5

bindata

 

undefined

6

undefined

过期

objected

7

objectId

 

Boolean

8

bool

 

date

9

date

 

Null

10

null

 

regular expression

11

regex

 

DBpointer

12

dbpointer

过期

javascrip

13

javascript

 

Symbol

14

symbol

过期

javaScript(with scope)

15

javascriptWithScope

 

32-bit integer

16

int

 

timestamp

17

timestamp

 

64-bit integer

18

long

 

decimal 128

19

decimal

3.4以上

min key

-1

minkey

 

max key

127

maxkey

 


这里我们最常用的类型是ObjectId(默认),integer和string类型,通过如下语句,可以查询出对应类型的值。MongoDB全量迁移断点续传功能学习与实现

MongoDB全量迁移断点续传功能学习与实现

MongoDB全量迁移断点续传功能学习与实现

对collection中插入几条数据,_id包含各种类型,integer,objectId,string。MongoDB全量迁移断点续传功能学习与实现

MongoDB全量迁移断点续传功能学习与实现MongoDB全量迁移断点续传功能学习与实现

MongoDB全量迁移断点续传功能学习与实现

通过语句db.user.find({"_id":{"$type":2}});可以指定具体的类型,查出相应的数据。

2.2  分片的基本原理和方法

根据上面mongo的_id的特点,该字段可以是21种类型中的任何类型,也可以在一个collection中,同时存在多种类型,因此我们要先把每个类型单独拆分出来,作为多个分片,并且取得每个分片的最小值,通过如下语句。

MongoDB全量迁移断点续传功能学习与实现

MongoDB全量迁移断点续传功能学习与实现MongoDB全量迁移断点续传功能学习与实现

MongoDB全量迁移断点续传功能学习与实现

对于每个类型都可以作为一个单独的分片,然后获取的每个分片的最小值,后面会用到这个最小值。

2.3 mongoDB的基本分片逻辑

       根据在mysql,oracle、sqlserver等数据库中得知,每个数据库都支持分页查询,比如mysql 中的limit函数、oracle中的rownum关键字,还有sqlserver中的top关键字等。当然mongo中也提供了相应的分页查询的函数,skip和limit的函数。利用这两个函数的结合可以得到你想要的一部分数据。比如,要得到第10到100条数据,那么可以:db.collection.find().skip(10).limit(100); 通过这种方式得到每个分片的数据。在研究的过程中发现,当数据量很大的时候,利用skip()和limit函数进行分页查询的时候发现速度非常的慢,sql执行时间越来越长,曾经做过统计统计一张80亿条数据的集合,利用find(_id>xxx).skip(200w).limit(1)返回的时候需要60s,而find(_id>xxx).skip(50w).limit(1)返回的时候则需要20s。如果把id>xxx的条件去掉,直接使用skip(m*n).limit(1)的话,则查询基本时候卡死。通过研究mongo的执行计划和查阅mongodb的官方文档发现,使用skip()函数,会进行全集合扫描,约到后面性能越来越差,随着数据量的增加也会越来越差,这就是一开始我们使用这个函数进行分片,在一些小的表上发现迁移性能越来越慢,而大表基本就卡死的原因。

MongoDB全量迁移断点续传功能学习与实现

MongoDB全量迁移断点续传功能学习与实现

    针对上面这个问题,我们去研究了mongo的字段_id,发现该字段拥有天然的索引结构,并且如果我们只是使用find(_id>xxx)的话,速度非常的快速,因此,我们采用了一种多线程的模式,不适用skip函数,直接使用_id默认的索引顺序。在测试的过程中发现,利用find(_id>xxx)的返回速度非常的快。而我们唯一要做的就是,一开始得到整个表的最小的_id,就是通过上面的语句db.user.find({"_id":{"$type":2}})拿到最小值,通常通过排序的方式find().sort(_id).limit(1)的方式得到最小值。通过执行计划发现,db.user.find({"_id":{"$type":2}}).limit(1)走的是索引结构,因此这个速度也是非常的快。

我们通过执行计划对比了skip函数的性能,skip()函数会跳过相应的条数,执行一下两条sql得到的结果是一样的

MongoDB全量迁移断点续传功能学习与实现

MongoDB全量迁移断点续传功能学习与实现

接下来,我们看下一下两条sql的执行计划

MongoDB全量迁移断点续传功能学习与实现

MongoDB全量迁移断点续传功能学习与实现

从上图中可以看到,sql跳过了1010条语句才能找到对应的值,接下来我们执行另外一条sql,看下起查询计划,可以看出第二条语句走了_id的索引。

MongoDB全量迁移断点续传功能学习与实现

MongoDB全量迁移断点续传功能学习与实现

       从这个查询计划中可以看出,该sql直接定位到对应的ObjectId=595b5b122a95218172eb57df 然后从该objectId的值开始跳过对应的10条记录,找到对应的值。由此可见,skip()函数是从当前的位置的起,开始跳过一定数量的数据,因此skip的方式不可取。


3. 实现原理

      通过对比上面的分析发现,如果使用skip和limit的函数,先对一张表进行分片,得到每个分片的最小值_id,然后多线程并发的去执行,这样的好处是,得到分片之后迁移速度会非常快,缺点是需要得到所有分片之后才能执行,但是获取分片的最小值是一个非常耗时的过程,通过对比发现,一张80亿条是数据表,每个分片200w和50w,每次执行返回的时间是60s和20s,这样的得到分片就是67个小时和88个小时,这是用户不能接受的。

针对上面的情况我们采用另外一种方式,单个线程的读+多个线程写的方式,那么得到整个collection的最小值的时间是1s左右,就可以开始迁移。这样虽然单个分片,但是整体的效果要好于先得到所有的分片在进行迁移。

MongoDB全量迁移断点续传功能学习与实现

MongoDB全量迁移断点续传功能学习与实现

      针对以上两种情况,本文做了一个整合,两种方式并发同时执行,一个线程不停的去切分,得到分片,然后将得到分片丢到queue里面,然后多个线程从queue里面取得到的相应的分片,这样的方式是极大的提升了查询效率。

MongoDB全量迁移断点续传功能学习与实现

MongoDB全量迁移断点续传功能学习与实现


4.总结

       通过这次对mongo的分片和断点续传的了解,也学习到了mongo的非常的多知识,当然这并不是最好的方式,但是目前可以解决大部分mongo迁移和支持断点续传的功能,今后将继续深入学习mongodb,进步的对分片和断点续传的方式进行优化。以上是这次学习对mongo的一点总结和经验,如果文中有不对的地方欢迎广大网友指正。