上一篇我们讲解了基本UDF的编写,这一节我们来看下UDAF[User Defined Aggregation Functions],自定义聚合函数,用来处理输入多行,输出一行的操作,类似MapReduce中Reduce操作。UDAF是需要在hive的sql语句和group by联合使用,hive的group by对于每个分组,只能返回一条记录。

概述

Hive是构建在Hadoop上的数据仓库,我们的sql语句最终都是要变成Mapreduce函数,只不过hive已经帮助我们写好并隐藏mapreduce ,给我们提供简洁的sql函数,所以我们要结合MapperCombinerReducer来帮助我们理解UDAF函数。在hadoop集群中有若干台机器,在不同的机器上MapperReducer任务独立运行。所以大体上来说,这个UDAF函数读取数据[mapper],聚集一堆mapper输出到部分聚集结果[combiner],并且最终创建一个最终的聚集结果[reducer]。因为我们跨域多个combiner进行聚集,所以我们需要保存部分聚集结果。在实现UDAF时候,Hive提供了两个重要的类org.apache.hadoop.hive.ql.udf.generic.AbstractGenericUDAFResolverorg.apache.hadoop.hive.ql.udf.generic.GenericUDAFEvaluator,我们先分析这两个类都做了什么工作,然后通过一个简单的例子来了解如何自定义UDAF。

UDAF重要的类及原理分析

AbstractGenericUDAFResolver

AbstractGenericUDAFResolver很简单,要覆盖实现getEvaluator方法,该方法会根据sql传递的参数数据格式指定调用哪个Evaluator进行处理

public GenericUDAFEvaluator getEvaluator(TypeInfo[] info) 
  throws SemanticException {
  throw new SemanticException(
    "This UDAF does not support the deprecated getEvaluator() method.");
}

GenericUDAFEvaluator

所有Evaluator必须继承抽象类org.apache.hadoop.hive.ql.udf.generic.GenericUDAFEvaluator。子类必须实现它的一些抽象方法,实现UDAF的逻辑。

ObjectInspector

ObjectInspector作用主要是解耦数据使用与数据格式,使得数据流在输入输出端切换不同的输入输出格式,不同的Operator上使用不同的格式,可以通过这个抽象类知道上游传递过来的参数类型,从而解耦。一个ObjectInspector对象本身并不包含任何数据,它只是提供***对数据的存储类型说明***和***对数据对象操作***的统一管理或者是代理。

public interface ObjectInspector extends Cloneable {
  String getTypeName();

  ObjectInspector.Category getCategory();

  public static enum Category {
    PRIMITIVE,
    LIST,
    MAP,
    STRUCT,
    UNION;

    private Category() {
    }
  }
}

Model

Model代表了UDAF在Mapreduce的各个阶段,定义如下:

public static enum Mode {
  // PARTIAL1: from original data to partial aggregation data: iterate() and t
  // erminatePartial() will be called.
  PARTIAL1,
  // PARTIAL2: from partial aggregation data to partial aggregation data: 
  // merge() and terminatePartial() will be called. 
  PARTIAL2,
  // FINAL: from partial aggregation to full aggregation: merge() and
  // terminate() will be called. 
  FINAL,
  // COMPLETE: from original data directly to full aggregation: iterate() and
  // terminate() will be called.
  COMPLETE
};

我们来详细分析下各个阶段:

  1. PARTIAL1是map阶段,从原始数据到数据聚合,将会调用iterateterminatePartial方法;
  2. PARTIAL2相当于map阶段的Combine,负责map端合并map的数据,将调用mergeterminatePartial方法;
  3. FINAL相当于reduce阶段,从部分聚合到完全聚合,将会调用mergeterminate方法;
  4. COMPLETE这个表示mapreduce中只有map没有reduce,直接在map端就得到结果了,从原始数据直接到完全聚合结果,将会调用iterateterminate方法。

所以,完整的UDAF逻辑是一个mapreduce过程,如果有mapper和reducer,就会经历PARTIAL1(mapper),FINAL(reducer),如果还有combiner,那就会经历PARTIAL1(mapper),PARTIAL2(combiner),FINAL(reducer)。而有一些情况下只有mapper,而没有reducer,所以就会只有COMPLETE阶段,这个阶段直接输入原始数据,出结果。

相关方法

实现UDAF,需要实现以下几个方法:

// 确定各个阶段输入输出参数的数据格式ObjectInspectors
public  ObjectInspector init(Mode m, ObjectInspector[] parameters) throws HiveException;
 
// 保存数据聚集结果的类
abstract AggregationBuffer getNewAggregationBuffer() throws HiveException;
 
// 重置聚集结果
public void reset(AggregationBuffer agg) throws HiveException;
 
// map阶段,迭代处理输入sql传过来的列数据
public void iterate(AggregationBuffer agg, Object[] parameters) throws HiveException;
 
// map与combiner结束返回结果,得到部分数据聚集结果
public Object terminatePartial(AggregationBuffer agg) throws HiveException;
 
// combiner合并map返回的结果,还有reducer合并mapper或combiner返回的结果。
public void merge(AggregationBuffer agg, Object partial) throws HiveException;
 
// reducer阶段,输出最终结果
public Object terminate(AggregationBuffer agg) throws HiveException;

UDAF开发步骤

Hive开发UDAF有两个步骤:

  1. 编写resolver类,resolver负责类型检查,操作符重载。类继承org.apache.hadoop.hive.ql.udf.generic.AbstractGenericUDAFResolverAbstractGenericUDAFResolver实现了org.apache.hadoop.hive.ql.udf.generic.GenericUDAFResolver2,方便统一接口。
  2. 第二个是编写evaluator类。evaluator真正实现UDAF的逻辑。通常来说,实现org.apache.hadoop.hive.ql.udf.generic.GenericUDAFEvaluator ,包括几个必须实现的抽象方法,这几个方法负责完成UDAF所需要处理的逻辑。

Sum实现分析

我们来看下Hive内置的Sum函数是如何实现的。

GenericUDAFSum

根据上面分析的我们首先要实现AbstractGenericUDAFResolvergetEvaluator方法,根据数据类型选择不同的Evaluator

  1. 如果是BYTE,SHORT,INT,LONG类型,就是用GenericUDAFSumLong
  2. 如果是TIMESTAMP,FLOAT,DOUBLE,STRING,VARCHAR,CHAR类型,就是用GenericUDAFSumDouble
  3. 如果是DECIMAL类型,就是用GenericUDAFSumHiveDecimal
  4. 如果是其他类型则抛出异常。
public class GenericUDAFSum extends AbstractGenericUDAFResolver {
  @Override
  public GenericUDAFEvaluator getEvaluator(TypeInfo[] parameters)
    throws SemanticException {
    if (parameters.length != 1) {
      throw new UDFArgumentTypeException(parameters.length - 1, "Exactly one argument is expected.");
    }

    if (parameters[0].getCategory() != ObjectInspector.Category.PRIMITIVE) {
      throw new UDFArgumentTypeException(
        0, "Only primitive type arguments are accepted but "
        + parameters[0].getTypeName() + " is passed.");
    }
    
    switch (((PrimitiveTypeInfo) parameters[0]).getPrimitiveCategory()) {
      case BYTE:
      case SHORT:
      case INT:
      case LONG:
        return new GenericUDAFSumLong();
      case TIMESTAMP:
      case FLOAT:
      case DOUBLE:
      case STRING:
      case VARCHAR:
      case CHAR:
        return new GenericUDAFSumDouble();
      case DECIMAL:
        return new GenericUDAFSumHiveDecimal();
      case BOOLEAN:
      case DATE:
      default:
        throw new UDFArgumentTypeException(
          0, "Only numeric or string type arguments are accepted but "
          + parameters[0].getTypeName() + " is passed.");
    }
  }
}

可以看出来根据不同的类型选择了不同处理类,我们来分析下GenericUDAFSumLong的实现

GenericUDAFSumLong

GenericUDAFSumLong是用来对BYTE,SHORT,INT,LONG类型求和的UDAF,有两个变量,inputOI是输入的数据,result是结果数据,初始化是对这两个参数的初始化,另外定义了SumLongAgg来存储中间结果,里面包含了sum值和是否为空,我们自行实现时候可以自定义自己的AggBuffer。

public static class GenericUDAFSumLong extends GenericUDAFEvaluator {
  private PrimitiveObjectInspector inputOI;
  private LongWritable result;

  @Override
  public ObjectInspector init(Mode m, ObjectInspector[] parameters) throws HiveException {
    assert (parameters.length == 1);
    super.init(m, parameters);
    result = new LongWritable(0);
    inputOI = (PrimitiveObjectInspector) parameters[0];
    return PrimitiveObjectInspectorFactory.writableLongObjectInspector;
  }

  /** class for storing double sum value. */
  @AggregationType(estimable = true)
  static class SumLongAgg extends AbstractAggregationBuffer {
    boolean empty;
    long sum;
    @Override
    public int estimate() { return JavaDataModel.PRIMITIVES1 + JavaDataModel.PRIMITIVES2; }
  }

  @Override
  public AggregationBuffer getNewAggregationBuffer() throws HiveException {
    SumLongAgg result = new SumLongAgg();
    reset(result);
    return result;
  }

  @Override
  public void reset(AggregationBuffer agg) throws HiveException {
    SumLongAgg myagg = (SumLongAgg) agg;
    myagg.empty = true;
    myagg.sum = 0;
  }
}

另外它实现了Mode阶段的不同函数,我们来一一分析。

iter

iterate()函数读取到每行中列的数值,然后合并到缓存数据结构中。

@Override
public void iterate(AggregationBuffer agg, Object[] parameters) throws HiveException {
  assert (parameters.length == 1);
  try {
    merge(agg, parameters[0]);
  } catch (NumberFormatException e) {
    if (!warned) {
      warned = true;
      LOG.warn(getClass().getSimpleName() + " "
               + StringUtils.stringifyException(e));
    }
  }
}

merge

merge函数增加部分聚集总数到AggregationBuffer中。

@Override
public void merge(AggregationBuffer agg, Object partial) throws HiveException {
  if (partial != null) {
    SumLongAgg myagg = (SumLongAgg) agg;
    myagg.sum += PrimitiveObjectInspectorUtils.getLong(partial, inputOI);
    myagg.empty = false;
  }
}

terminatePartial

terminatePartial()函数直接调用terminate,返回AggregationBuffer中的内容,这里产生了最终结果。

@Override
public Object terminatePartial(AggregationBuffer agg) throws HiveException {
  return terminate(agg);
}

terminate

terminate()函数返回AggregationBuffer中的内容,这里产生了最终结果。

@Override
public Object terminate(AggregationBuffer agg) throws HiveException {
  SumLongAgg myagg = (SumLongAgg) agg;
  if (myagg.empty) {
    return null;
  }
  result.set(myagg.sum);
  return result;
}

总结

本文讲解了UDAF,介绍了重要的类以及UDAF的运行过程,最后通过分析sum的UDAF实现,来讲解了一个UDAF实现的具体步骤,但是如果想要更好理解UDAF的运行过程,还是需要看一看avg的实现,avg UDAF会对hive的运行流程要控制的更加精细,并判断当前运行的Mode做一定的逻辑处理,我们后面在分析。