三、 Hive 逻辑算子及其生成(中)

3.2 Hive主要算子功能实现

   3.2.1 TS、SEL以及FIL算子的实现

   1、TS算子的实现

   TS算子(TableScanOperator)在逻辑上表示table扫描功能,实际table数据的读取由底层的计算引擎完成。如在Map-Reduce框架中,table数据的读取由Hadoop提供的InputFormat类完成。TS内部实现仅仅将从底层计算引擎输入的数据输出给子节点处理。

   2、SEL算子的实现

   SEL算子(SelectOperator)实现了关系代数中投影运算,从输入的每条表记录中筛选指定的列进行计算。例如:

select a.c1+1 as o1,a.c2*2 as o2 from a

表a数据如下:

          表3-1 table a字段及数据

c1        

c2            

c3       

1                

2              

3                       

4              

5              

6                      

7            

8              

9                          

SEL算子从记录中提取c1,c2字段,然后计算select表达式的值,得到结果如下:

o1                    

o2                 

2                     

4                   

5                           

10                     

8                         

16                            

3、FIL算子的实现

FIL(FilterOperator)实现过滤运算,对应HiveQL的where语句。FIL对输入的记录进行过滤,将不符合where表达式的记录过滤掉,剩下的记录输出给子节点。
   以表3-1的数据为例,给定HiveQL语句:

select a.c1+1 as o1,a.c2*2 as o2 from awhere a.c1%2=1

   该语句生成的算子DAG如下:

三、 Hive 逻辑算子及其生成(中)_第1张图片

TS算子读取表a中的数据,输出给FIL节点,FIL算子将a.c1%2=1的记录输出给SEL算子,其他记录被过滤掉,SEL算子做投影运算,计算a.c1+1和a.c2*2的值,然后输出给FS算子,FS算子将结果输出到临时文件中,最终的计算结果如下:

o1               

o2                   

2                          

4                          

8                           

16                              

可见,a.c1等于4的记录被过滤掉,剩下的记录最终计算输出。

Hive在生成算子DAG时总是将FIL算子放在TS算子的后面,这样做的好处是可以提前过滤数据,减少后续算子处理的数据量。

3.2.2 RS算子的实现

   在分布式计算系统中,通常计算过程包括两个阶段:map阶段和reduce阶段。map阶段会将数据分割成若干个数据块,给每个数据块创建一个map任务处理该数据块,处理完成后将中间结果输出;接着创建若干个reduce任务,reduce任务会按照一定规则收集各map任务产生的中间结果,并排序,然后做下一步的处理。

以wordcount程序为例进行说明。wordcount是一个统计文件单词个数的程序。下图给出了wordcount的计算流程。

三、 Hive 逻辑算子及其生成(中)_第2张图片

                                 图3-4 wordcount计算过程

输入数据分两个数据块,分别对应两个map任务,map任务统计数据块中单词的个数,以(key,value)形式输出。reduce阶段,创建了2个reduce任务,分别采集map任务生成的中间数据,采集按照key.hashcode()%reduce_num的值进行;reduce0采集值为0的kv对,reduce1采集值为1的kv对。然后reudce按照key对数据进行排序,最后,对采集到的数据做求和运算,得到每个单词的数量。

reduce采集map中间数据并排序的过程称为shuffle。Hadoop和Spark平台各自实现了shuffle过程。Hive中RS算子(ReduceSinkOperator)则是对shuffle过程的逻辑抽象,它定义了shuffle过程的数据采集规则、排序方式并使用平台相关的OuputCollector对象输出数据。

在介绍RS算子之前,先介绍两个术语:数据分发(partition)VS数据采集。数据分发是从map端的角度来描述数据从map端到reduce端的流动,即数据由map端”主动”分发到了reduce端(实际情况则是数据由reduce端采集过去)。这两个概念事实上等价,一般情况下,我们使用数据分发这个术语。

   1)数据分发规则

   RS算子以(key,value)的形式输出数据,其中key使用HiveKey对象表示。Hive使用计算平台默认的数据分发规则—HashPartitoner进行数据分发。HashPartitioner使用key.hashcode()%reduce_num的值进行数据分发,数据被分发给对应编号的reduce任务(0对应reduce0,1对应reduce1,以此类推)。HiveKey中定义了数据分发的表达式列表(partitonEval),表达式列表的hashcode作为HiveKey的hashcode,计算代码如下:

for (int i = 0; i < partitionEval.length; i++) {
    Object o = partitionEval[i].evaluate(row);
    keyHashCode = keyHashCode * 31
        + ObjectInspectorUtils.hashCode(o, partitionObjectInspectors[i]);
  }
}

当partitionEval列表为空时,Hive将随机分发数据,这种情况下HiveKey的hashcode为随机值。这样可以确保数据分布的均匀性,防止出现数据倾斜。

partitionEval表达式列表由具体的HiveQL语句确定,对于group by语句,partitionEval表达式列表为group by表达式;对于join语句,partitionEval表达式为join on表达式。此外,还可以通过distribute by,cluster by语句指定partition表达式。

2)排序

分布式计算引擎对RS算子输出的(key,value)按照key进行排序。RS中使用keyEval列表存放需要排序的表达式,keyEval被序列化后以二进制形式存放在HiveKey对象中。HiveKey注册Comparator对象,定义排序规则:按照字典序方式对二进制数据进行升序排序。RS使用BinarySortableSerDe对(key,value)进行序列化。BinarySortableSerDe是一种可以排序二进制序列化实现,默认按照升序排序。当需要降序排列时BinarySortableSerDe会先对序列化后的二进制进行反序运算,再按照反序结果进行排序,最后反序列化时再进行一次反序运算,从而实现降序排列。见下面例子:

三、 Hive 逻辑算子及其生成(中)_第3张图片

对输入数据0001~1000进行降排序,BinarySortableSerDe先反序运算(通过异或运算符^实现),在按照Comparator默认的升序规则字典排序,反序列化时,再进行反序运算,最终得到降序排列的结果。

RS的keyEval列表也是由具体的HiveQL语句确定。对于group by语句,keyEval为group by表达式;对于join语句,keyEval为join表达式。

3.2.3 GBY算子的实现

       GBY算子(GroupByOperator)实现了groupby运算。groupby是sql语句的基本运算功能,通过对数据按照特定字段分组聚合,对分组进行聚合运算。

3.2.3.1 数据聚合方式

Hive中实现数据聚合的方式有两种:hash聚合和sort聚合。hash聚合是基于hash表的聚合方式,将group by key相同的表达式记录聚合在一起,进行聚合运算。这种聚合方式不需要shuffle过程,减少shuffle过程中的网络数据传输。缺点是只能对数据做局部聚合,无法对整个数据进行聚合,一般用于map端分组聚合运算,减少map端的数据输出,类似于map-reduce的combine操作。 sort聚合方式是通过RS算子对数据进行分组排序(partiton和sort),所有groupby key相同的记录被分成一组,然后对每组数据进行聚合运算,这种方式可以得到聚合函数的最终结果。Hive中group by运算都会包含sort聚合方式,hash聚合方式则可以通过配置hive.map.aggr开启/关闭该功能,默认开启。

3.2.3.2 GBY算子实现分析

前文,我们介绍了Hive的数据聚合方式。下面,我们将具体介绍GBY算子的实现,首先介绍GBY算子的主要数据结构,然后介绍GBY算子的两种实现方式:基于hash聚合的GBY和基于sort的GBY。

(1)GBY算子主要数据结构

 GBY算子主要数据结构如下:

ExprNodeEvaluator[] keyFields;
ExprNodeEvaluator[][] aggregationParameterFields;
boolean[] aggregationIsDistinct

其中:

keyFields:groupby key数组,包含group by表达式和distinct表达式。如语句select id,count(distinctname) from a group by id;keyFields为id和name。ExprNodeEvaluator类用于计算表达式的值。

aggregationParameterFields:二维数组,表示第i个聚合函数的第j个参数表达式。

aggregationIsDistinct:bool数组,表示第i个聚合函数是否含有distinct关键字。

GBY算子的两种实现均在GroupByOperator类中,上面是这两种实现的公共数据结构。

(2)基于hash聚合的GBY实现

 基于hash聚合的GBY算子通过hash方式实现数据聚合并计算聚合函数的局部值。除了上面介绍的公共数据结构,与该实现方式相关的数据结构还有如下:

HashMap hashAggregations;
long numRowsInput;
long numRowsHashTbl;
float minReductionHashAggr;

hashAggregations是一张hash表,key是group by key的值,封装成KeyWrap对象(group by key的值通过keyFields表达式计算得到);value是每个聚合函数局部聚合结果,聚合结果用对象AggregationBuffer表示。聚合函数的计算是一个迭代过程,AggregationBuffer对象存放每次聚合函数的迭代结果。

numRowsInput用于记录处理的记录条数,numRowsHashTble记录hash表(key,value)条数,minReductionHashAggr是一个Hive配置参数(hive.map.aggr.hash.min.reduction),表示hash表的最小散列因子,默认值为0.5。散列因子计算公式为:numRowsHashTble/numRowsInput,即hash表数据条数/总记录条数。散列因子越大,说明记录中key大部分都不相同,hash表的聚合度很低。极端情况下,散列因子为1,这时记录中的所有key都不相同,hash表没有起到任何聚合作用。Hive中,当hash表的散列因子大于配置的最小值时,表示hash表的聚合作用太弱,这时会取消hash聚合方式。

hash聚合的GBY数据计算流程(代码在process方法中)如下:

三、 Hive 逻辑算子及其生成(中)_第4张图片

        图3-5 hash GBY计算流程

1)GBY的父节点输出一条记录给GBY,GBY先计算散列因子,判断散列因子是否小于设置的最小值。如果大于,放弃hashgroupby,否则执行下一步。

2)计算该记录的group by key的值,通过keyEvals表达式数组进行计算,并封装成KeyWrap对象。如果groupby key的值不在hashAggregations表中,将该KeyWrap对象放入到hashAggregations表中,value值为初始化的AggregationBuffer数组。

3)计算所有聚合函数的局部聚合值。调用聚合函数的iterator()方法,对每条记录进行迭代,计算聚合局部值。注意,如果聚合函数包含distinct参数,那么只有当参数值发生变化的情况下才会对记录做迭代计算,相同的参数只计算一次。例如:count(distinct name)对于相同的name只调用一次count的iterator()方法。

4)更新hashAggregations表。实际上,步骤(3)中计算结果直接保存在AggregationBuffer对象中,AggregationBuffer在初始化时已经存入hash表中,因此,这一步不需要额外操作。

(3)基于sort的GBY实现

 基于sort的GBY通过sort聚合方式实现数据分组,并计算每组数据上的聚合函数的最终聚合结果。除了(1)中介绍的公共数据结构,还包含主要数据结构如下:

KeyWrapper currentKeys;
KeyWrapper newKeys;
AggregationBuffer[] aggregations;
Object[][] aggregationsParametersLastInvoke;

其中currentKeys表示当前处理的分组的groupby key的值(这里不包含distinct表达式),newKeys表示为每条记录新建的groupby key的值。aggregations存放每条记录处理后聚合函数的计算结果。aggregationsParametersLastInvoke存放上条记录处理的各个聚合函数的参数值。

基于sort GBY算子的数据计算流程如下:

三、 Hive 逻辑算子及其生成(中)_第5张图片

       图3-5 hash GBY计算流程

1) 父节点输入一条记录给GBY算子,调用keyEvals表达式计算该记录的groupby key的值,存放到newKeys。

   2)比较currentKeys和当前newKeys是否变化。由于sort GBY处理的记录按照groupby key排序,如果发生变化,说明当前分组已经结束,这是输出当前分组的计算结果,否则下一步。

   3)更新聚合函数的值。调用聚合函数的merge()方法,合并当前groupby key的局部聚合值。

  3.2.3.3 wordcount GBY实现

   下面以wordcount为例子,介绍GBY的实现过程。

   给定HiveQL语句:

select id,count(word) from a group by id

table a的数据如下表:

id                                

word                       

0

a

1

b

0

c

 

0

d

1

e

1

f

table a包含id和word两个字段,表中数据分成两个数据块,每个数据块3条数据。HiveQL语句按照id分组统计word的数量。该语句对应的算子DAG如下:

三、 Hive 逻辑算子及其生成(中)_第6张图片

下图给出了该DAG的计算过程,map端包含两个map任务,分别处理对应数据分块中的数据,处理流程如下:TS算子顺序读取表记录,输出给SEL算子;SEL算子从记录中帅选id和word字段输出给hash based GBY算子(SEL算子筛选过程图中省略);GBY算子统计数据块内各id的单词个数。RS算子将各map任务的中间结果输出,然后按照group by key,即id排序。排序后,id相同的记录分成一组,输出给sort based GBY进行处理;GBY算子计算每个分组内单词总和,最后通过FS算子输出结果。

三、 Hive 逻辑算子及其生成(中)_第7张图片

                                图3-6 wordcount数据处理流程

3.2.4 JOIN算子介绍

JOIN算子(JoinOperator)实现了多张表的join运算。

对于给定HiveQL语句:

select a.*,b.*,c.* from a join b on a.id=b.id join c onb.id=c.id

其算子DAG如图3-7所示。

三、 Hive 逻辑算子及其生成(中)_第8张图片

                                        图3-7 join语句算子DAG

DAG图中RS算子左边有三个分支,分别对应三张表a,b,c的运算。其中FIL算子用于过滤掉id为null的记录;RS算子按照id进行数据分发(partition)和排序(sort),并对数据进行打标(tag)。数据打标指对每条记录添加一个tag标示,用于区分数据来自那张表。tag标是一个整数,例如表a的记录会加上tag标记0,表b的记录会加上tag标1,表c则是2,依次递增。JOIN算子对RS分组排序后的数据进行Join运算。数据的具体处理流程如下

图3-8所示:

三、 Hive 逻辑算子及其生成(中)_第9张图片

                                      图3-8 join数据处理流程

表a,b,c的数据分别经过TS,FIL算子进行扫描、过滤处理,然后通

过RS算子进行数据分发和排序。RS算子按照join on表达式字段进行数据分发和排序并给每条记录打标(tag)。这里按照id进行数据分发和排序,表a,b,c的记录分别添加0,1,2做为tag。在reduce端,排序后的数据按照id进行分组,每组数据中包含多张表的数据,不同表的数据根据tag来区分。JOIN算子对分组中多张表的数据做笛卡尔乘积,得到每个id的join结果。











你可能感兴趣的:(hive)