Dremel: 交互式分析Web数据

Dremel是一个可扩展的、交互式即时查询系统,用于分析只读的嵌套数据。Dremel可以对集群上的超大数据集进行交互式分析。Pig、Hive利用MapReduce执行查询,需要在多个MR作业间传递数据,相比之下,Dremel是就地执行查询的(MapReduce的瓶颈很有可能就是在Map和Reduce之间传递数据)。Dremel并不是用来取代MapReduce的,它可以和MapReduce互相补充,比如用于分析MapReduce的输出。

实现Dremel有2个问题:首先是通用的存储层,比如GFS,一个高性能的存储层对于就地查询是非常关键的;其次是存储格式,按列存储对于扁平的关系数据非常合适,运用到嵌套数据更加困难,必须要保留结构信息,并且能够按任意域的子集重构记录。

本文的主要内容就是如何按列存储嵌套数据模型。

1.嵌套数据模型


编程语言使用的数据结构,分布式系统交换的信息,结构化文档,等等,可以很自然地用嵌套结构表示。在Google,嵌套数据模型是大多数结构化数据处理的基础。Dremel使用的嵌套数据模型可用下面语句表示:



τ是一个原子类型或者一个记录类型。Dremel使用的原子类型包括整形、浮点数、字符串等;记录包括一个或多个域,每个域可以是原子类型或其它记录类型。Ai是记录的第i个域,每个域可以选择一个标签,标签*(repeated)表示重复域(可出现0或多次),标签?(optional)表示可选域(0或1次),无标签(required)则表示必要域(只出现一次)。





上图是论文里给出的例子,定义了一个记录类型Document表示网页,以及该记录的两个实例。域DocId是必要域;Links是可选域,Links内可以包含一系列的Backward和Forward,指向其它网页的DocId;每个文档可以有多个Name,表示可以用不同的URL访问该网页;每个Name可以有多个Language(Code-Country对),以及可选域Url。r1和r2是满足该格式的两个例子。 域的完整路径使用’.’连接,比如Name.Language.Country。

整个数据模型可以看成是一棵树,只有叶子结点才有值。

2.按列存储


按列存储有很多好处,比如更好压缩率,减少查询读取的数据量。按列存储关系数据很简单,但是按列存储嵌套数据很复杂,因为必须保存数据的结构信息。本小节解决记录的按列无损存储,快速编码,高效记录聚合等挑战。

2.1 重复级别和定义级别


数据本身不能告诉我们记录的结构。以Document的Name.Language.Code为例,按列存储后,连续两个Code的值,我们并不知道它们是属于同一个Name下的两个Language,还是不同的两个Name,如下图三个Code的关系。因此为了保存记录的结构信息,Dremel引入了Repetition Level和Definition Level概念。



仍然以Code为例,它在r1中出现了3次。’en-us’和’en’位于第一个Name,’en-gb’位于第三个Name。换句话说,’en’是在Language发生重复,’en-gb’是在Name发生重复。为了区分这些情况,Dremel为每个值分配一个重复级别。重复级别代表值是在哪一级发生重复。Name.Language.Code路径上有两个重复域,因此Code的重复级别可以是0、1、2,0表示开始新记录(或者理解为在记录一级重复)。根据定义,r1的三个Code的重复级别分别是0、2、1。注意r1的第二个Name里没有Code,为了区分’en-gb’是第二个Name还是第三个Name,必须’en’和’en-gb’之间插入NULL。

在Code的例子里,因为Code是Language的必要域,如果Code读取了一个NULL,我们就可以确定这个位置实际上是一个没有Language的Name。但是一般情况下,还需要额外信息。以Country为例,Country是Language的可选域,如果Country读出一个NULL,那么这个位置到底是缺了Language的Name,还是缺了Country的Language呢?

定义级别用于解决这个问题。定义级别表示这个值的完整路径上有几个域是可以未定义(可选域或重复域)但却已定义。对于非空的值,它的定义级别就等于完整路径上可选域和重复域的数量;对于NULL,得看它出现的位置上实际定义了几个可选域和重复域。以Country为例,Name.Language.Country的可选域或重复域有三个,所以对于非空值,它的定义域一定是3;而对于NULL,则要看缺席的原因,比如r1的第一个Name的第二个Language没有Country,定义级别为2,而第二个Name没有Country,定义级别为1。NULL告诉我们此处本可以有一个值,定义级别告诉我们缺席的原因。

r1和r2所有域的重复级别和定义级别如下图所示。论文的附录A提供了快速计算重复级别和定义级别的算法。


2.2 编码


列的存储都以block为单位,值经过压缩后,和对应的重复级别和定义级别一起被编码到block。好的编码方式可以尽可能去掉不必要的数据。

性质一:特定的一列里,非空值的定义级别都是相同的,而空值的定义级别肯定小于非空值的定义级别。

性质二:重复级别总是小于定义级别。

利用性质一,我们可以不存储非空值的定义级别和NULL值。根据性质二,定义级别为0意味着重复级别也是0,因此DocId的两个级别都不用存储。

2.3 记录的组装


前面讨论的是如何将记录按列存储,这一小节是如何将列重组为记录,这对面向记录的处理工具(比如MapReduce)是非常关键的。给定域的一个子集,Dremel将只重组该记录被选定的域。Dremel的思想是创建一个有限状态机(FSM),负责读取每个域的值和级别。FSM的每个状态对应一个域,状态的跳转由重复级别决定:每读取了一个值,Dremel查看下一个重复级别决定跳转到哪个状态。



上图显示了重组完整记录的有限状态机。开始的状态是DocId,每当读取一个DocId,FSM跳转到Links.Backward;根据重复级别的定义,1表示下一个Backward值属于同一个Links,因此重复读取,遇到0则表示Backward读完了,跳转到Links.Forward;Forward和Backward类似;跳转到Code后,情况更加复杂,因为Country和Code是兄弟,所以不论Code的重复级别为何值,都跳转到Country;当下一个Country的重复级别是2时,表明当前Language级别发生重复,跳转到Code;否则Language级别无重复,跳转到Name.Url;下一个Url的重复级别是1表明Name发生重复,跳转到Code;否则Name结束,记录读取完毕。完整的算法在论文附录B。

FSM的构造算法在论文附录C,基本的思路比较简单。假如现处于状态f,即读取域f的值,如果下一个重复级别是l,则回溯到级别为l的祖先结点,并且选择该祖先结点下的下一个(论文里说的是first leaf,个人认为有误)叶子结点n为跳转状态。因此我们得到了一个跳转(f, l)->n。以Name.Language.Country为例,假如读取的下一个重复级别为1,因为country的祖先中级别为1的是Name,而Name在Language后的下一个叶子结点是Url,因此得到跳转(Name.Language.Country, 1)->Name.Url(而按照原文first leaf的解释,跳转的目标应该是Name.Language.Code,显然是错误的)。

原文: Dremel: Interactive Analysis of Web-Scale Datasets. In VLDB'2010.

posted on 2013-09-10 16:22  OpenNaive  阅读(527)  评论(0编辑  收藏  举报

导航