GenericUDF使用详解

7,207 阅读9分钟

前言

大数据开发中总会遇到某些特殊或复杂的数据处理场景,靠Hive自带的函数堆叠也无法实现功能,这时候就需要我们自己去实现可以在Hive中嵌入的自定义数据处理函数——UDF函数。UDF函数按照继承类可以分为2种,一个是UDF,一个是GenericUDF,GenericUDF的开发会比UDF复杂一些,所以我们一般在以下几种场景下考虑使用GenericUDF:

  1. 传参情况复杂,比如某UDF要传参数有多种数量或多种类型的情况,在UDF中支持这种场景我们需要实现N个不同的evaluate()方法分别对应N种场景的传参,在GenericUDF我们只需在一个方法内加上判断逻辑,对不同的输入路由到不同的处理逻辑上即可。还有比如某UDF参数既要支持String list参数,也要支持Integer list参数。你可能认为我们只要继续多重载方法就好了,但是Java不支持同一个方法重载参数只有泛型类型不一样,所以该场景只能用GenericUDF。
  2. 需要传非Writable的或复杂数据类型作为参数。比如嵌套数据结构,传入Map的key-value中的value为list数据类型,或者比如数据域数量不确定的Struct结构,都更适合使用GenericUDF在运行时捕获数据的内部构造。
  3. 该UDF被大量、高频地使用,所以从收益上考虑,会尽可能地优化一切可以优化的地方,则GenericUDF相比UDF在operator中避免了多次反射转化的资源消耗(后面会细讲),更适合被考虑。
  4. 该UDF函数功能未来预期的重构、扩展场景较多,需要做得足够可扩展,则GenericUDF在这方面更优秀。

涉及知识点

反射

Java可以在程序运行时,动态加载类并获取类的详细信息,从而操作类或对象的属性和方法,即是反射,本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。相比于先创建对象再得到clas对象信息的一般声明方法,反射支持在运行期间动态地加载某些类,因而多见于各种通用框架的配置里。反射也会消耗一定的系统资源,因此当不需要时不要使用反射。UDF使用反射机制来传递参数对象。

泛型

通过解耦类或方法与所使用的类之间的约束,实现了“参数化类型”的概念,使代码可以应用于多种类型,这即是泛型。在GenericUDF里体现为所有参数对象都以Object类被传递。

ObjectInspector

Java的ObjectInspector类,用于帮助Hive了解复杂对象的内部架构,也支持通过创建特定的ObjectInspector对象替代创建具体类对象,来在内存中储存某类对象的信息。
在UDF中,ObjectInspector用于帮助Hive引擎在将HQL转成MR Job的时候确定输入和输出的数据类型。Hive语句会生成MapReduce Job执行,所以使用的是Hadoop数据格式,不是编写UDF的Java的数据类型,比如Java的int在Hadoop为IntWritable,String在Hadoop为Text格式,所以我们需要将UDF内的Java数据类型转成正确的Hadoop数据类型以支持Hive将HQL生成MapReduce Job。

序列化/反序列化

OSI七层协议模型中,展现层(Presentation Layer)的主要功能是把应用层的数据结构或对象(Java中的Object类)转换成一段连续的二进制串(Java中可以理解为byte array),或者反过来,把(在序列化过程中所生成的)二进制串转换成应用层的对象–这两个功能就是序列化和反序列化。当UDF函数处理逻辑涉及多步操作时,就涉及把数据序列化以传输,再反序列化以处理的操作。

Deferred Object

两个特征,一个是lazy-evaluation,即到需要时才会执行计算(这里指创建和给对象赋值),典型例子是Iterator类,只在需要的时候才执行计算并返回下一个元素;一个是short-circuiting,即一个逻辑在执行运算前就能知道结果,所以在需要返回结果时会直接返回结果退出,避免不必要的计算,例子比如(false && xxx && xxx)该判断逻辑不会执行后面的运算,因为结果一定是false。

GenericUDF代码逻辑层——外

UDF是用户自定义的Hive函数,用于执行在Hive原生函数中无法实现的处理逻辑。 data_types.png org.apache.hadoop.hive.ql.udf.generic.GenericUDF API提供了一个通用的接口将任何数据类型的对象当作泛型Object去调用和输出(你甚至可以把传入Struct对象当Map使用,只要你提供对应的ObjectInspector),该方法需要我们重载GenericUDF的initialize()evaluate()方法,接下来会解释其执行逻辑。

执行流程

  1. 当Hive解析query时,会得到传入UDF参数的参数类型,并调用initialize()方法。针对该UDF的每个参数该方法都会收到一个对应的ObjectInspector参数,且该方法必须返回一个ObjectInspector表示返回值类型。通过调用该方法,Hive知道该UDF将返回什么数据类型,因此可以继续解析query。
  2. 对于Hive的每行记录,我们在initialize()方法内读取ObjectInspector参数,并执行传参的数量和数据类型的检查,正确时才进行计算;
  3. evaluate()方法中,我们使用initialize()方法里收到的ObjectInspector去读evaluate()方法接收的参数,即一串泛型Object(实际是DeferredObject),ObjectInspector解析Object并转成具体类型的对象执行数据处理,最后输出结果。

代码示例

class ComplexUDFExample extends GenericUDF {
  // 0. ObjectInspector,通常以成员变量的形式被创建
  ListObjectInspector listOI; 
  StringObjectInspector elementOI;
  @Override
  public ObjectInspector initialize(ObjectInspector[] arguments) throws UDFArgumentException {
    // 1. 检查该记录是否传过来正确的参数数量
    if (arguments.length != 2) {
      throw new UDFArgumentLengthException("arrayContainsExample only takes 2 arguments: List<T>, T");
    }
    // 2. 检查该条记录是否传过来正确的参数类型
    ObjectInspector a = arguments[0];
    ObjectInspector b = arguments[1];
    if (!(a instanceof ListObjectInspector) || !(b instanceof StringObjectInspector)) {
      throw new UDFArgumentException("first argument must be a list / array, second argument must be a string");
    }
    // 3. 检查通过后,将参数赋值给成员变量ObjectInspector,为了在evaluate()中使用
    this.listOI = (ListObjectInspector) a;
    this.elementOI = (StringObjectInspector) b;
    
    // 4. 检查数组是否包含字符串,是否为字符串数组
    if(!(listOI.getListElementObjectInspector() instanceof StringObjectInspector)) {
      throw new UDFArgumentException("first argument must be a list of strings");
    }
    
    // 5. 用工厂类生成用于表示返回值的ObjectInspector
    return PrimitiveObjectInspectorFactory.javaBooleanObjectInspector;
  }
  
  @Override
  public Object evaluate(DeferredObject[] arguments) throws HiveException {
    
    // get the list and string from the deferred objects using the object inspectors
    List<String> list = (List<String>) this.listOI.getList(arguments[0].get());
    String arg = elementOI.getPrimitiveJavaObject(arguments[1].get());
    
    // check for nulls
    if (list == null || arg == null) {
      return null;
    }
    
    // see if our list contains the value we need
    for(String s: list) {
      if (arg.equals(s)) return new Boolean(true);
    }
    return new Boolean(false);
  }

  @Override
  public String getDisplayString(String[] arg0) {
    return "arrayContainsExample()"; // this should probably be better
  }

开发详细说明

现实中的业务开发肯定远没有上面例子上的那么简单,实际开发中对于ObjectInspector的使用和DeferredObject的转化还是比较有门道的。

ObjectInspector

创建ObjectInspector时,不要用new的方式创建,用工厂模式去创建以保证相同类型的ObjectInspector只有一个实例,且同一个ObjectInspector可以在代码中多处被使用。创建的例子:

ObjectInspectorFactory.getStandardListObjectInspector(ObjectInspector listElementObjectInspector))

因为每个类型只有一个ObjectInspector,我们一般在类的开头就声明整个代码中会使用到的ObjectInspector,在后续用this.ObjectInspector.xx来使用它。ObjectInspector类一般有2个方法,一个用于获得一个特定类型的ObjectInspector,多见于初始化ObjectInspector时;一个用于将传入泛型Object转化为特定类型的Object,多见于 evaluate()方法内。 ObjectInspector类下的各类继承类:

  • PrimitiveObjectInspector:Java的String和numerical类型都属于primitive types
  • ListObjectInspector:对应Hive的array
  • MapObjectInspector:对应Hive的map
  • StructObjectInspector:对应Hive的Structs
  • StandardListObjectInspector:相比ListObjectInspector支持更多方法,支持更多类型,同时支持java.util.List和java.util.Map,使用List表示Hive的Struct和Array类型,使用Map表示Hive的Map类型
  • StandardStructObjectInspector:包含域的复杂对象,提供方法去访问域,这些方法可以通过读ObjectInspector类的源码了解
  • StandardMapObjectInspector ObjectInspector创建实例的方法:StandardListObjectInspector.getList(inputObject), MapObjectInspector.getMap(object), ObjectInspecotor.get(object),等等 。
    ObjectInspector访问对象内部值的方法:StandardListObjectInspector.getListLength(object), StandardListObjectInspector.getListElement(input_list, list_size)。
    如果你集群的Hive版本低于1.3.0,你在集群调试UDF的时候可能遇到org.apache.hadoop.hive.serde2.lazybinary.LazyBinaryArray cannot be cast to [Ljava.lang.Object问题,这个问题可能需要加一行下面的代码以兼容解决:
Object input_obj = arguments[0].get();
Object input_list = (input_obj instanceof LazyBinaryArray) ? ((LazyBinaryArray) input_obj).getList() : stdListInspector.getList(input_obj);

DeferredObject转换

对于如何将传入了的泛型DeferredObject转换成我们可以执行的Java类型,一般有两种方法,一个是直接转换,一个是使用Converter。
方法1,比如我们获得的DeferredObject从Hive代码可知是Text格式,则我们可以选择直接操作Text对象,即将参数泛型object放到Text对象里,Text resultText = new Text(object);,或者当做String来处理:String resultText = object.toString();
方法2,在initialize()方法创建一个ObjectInspectorConverters对象,在evaluate()方法用该对象实施将泛型object转为具体类型的操作。

//在initialize()方法:
ObjectInspectorConverters.Converter objectInspectorConverters;
objectInspectorConverters = ObjectInspectorConverters.getConverter(arguments[0], PrimitiveObjectInspectorFactory.writableStringObjectInspector);
...

//在evaluate()方法:
Text t = (Text) this.objectInspectorConverters.convert(arguments[0].get());
...

在输出数据对象时,我们还需要把对象转成writable类型,比如Integer要被转成IntWritable,可以直接用new新对象装旧对象的方法IntWritable intWritable = new IntWritable(object);。当需要把ObjectInspector从primitive转成writable时,可以使用下面的方法转化:

ObjectInspector before;
  after = ObjectInspectorUtils.getStandardObjectInspector(before, ObjectInspectorCopyOption.WRITABLE)

注意,UDF的类型检查只能在运行时检查,无法在编译期间检查。

从容器ObjectInspector获得元素对象

以List为例,假设我们要从input_list中获得泛型object,并以具体类型存储到自定义的tmp_list中。

//根据input_list长度创建临时list
Object tmp_list = stdListInspector.create(currSize);
List input_list = listInspectorList.getList(args[i].get());
//遍历input_list,将元素以泛型object取出,并用ObjectInspectorUtils转成java类型
for (int j = 0; j < input_list.size(); ++j) {
    Object genObj = input_list.get(j);
    Object stdObj = ObjectInspectorUtils.copyToStandardObject(genObj, listInspectorList.getListElementObjectInspector(), ObjectInspectorUtils.ObjectInspectorCopyOption.JAVA);
//将转型后的元素放入tmp_list
    stdListInspector.set(tmp_list, lastIdx + j, stdObj);
    }
    lastIdx += input_list.size();

或者也可以

    Object input_obj = arguments[0].get();
    for (; list_size < stdListInspector.getListLength(input_list); list_size++) {
    tmp_list.add(stdListInspector.getListElement(input_list, list_size).toString()); 
    }

GenericUDF算子执行层——内

UDF/UDAF/UDTF都会把代码转化成一个MapReduce Job,把代码中的各种逻辑执行转化成一个个Map任务或Reduce任务,并将这些operator组成一个operator树。GenericUDF的执行逻辑是:当Hive解析调用UDF函数的query时,会将参数对象(可能是某个字段,也可能是某个常量,甚至可能是另一个UDF函数的返回值)连同对应的ObjectInspector传入UDF函数,这些内容先传入MapReduce Job的第一个operator,该operator会调用initializeOp()方法,该方法接收到传入参数对象Object和对应ObjectInspector,并解析代码知道自己需要输出什么数据结构,将对象object和处理后应该输出的参数ObjectInspector传给下一个operator,即child opertor。child operator接收上一个operator的参数对象和ObjectInspector以及parentId,再往下传,如此反复直到遍历出整个opertor树。其中数据对象在读写或各个operator传输间需要被序列化以传输,也要被反序列化成具体对象以执行代码逻辑,GenericUDF将参数对象在传输过程中的对象类型都置为Object,避免了没必要的序列化和反序列化(不需要转化成具体对象时却转化了)的资源消耗。

测试及使用

GenericUDF测试

GenericUDF当然也支持本地测试,但是本地测试无法模拟Hive中从Hadoop数据类型转到Java数据类型的场景,只能测试evaluate()的逻辑是否无问题。
以containsString为例,测试流程是,先创建一个类的对象new ComplexUDFExample(),再手动生成参数需要的ObjectInspector并传入initialize()方法。在本地初始化完成后,通过新建DeferredObject数组来装载要测试的参数数据,并使用Assert.assertEquals()对比返回值和预期是否一致。

public class ComplexUDFExampleTest {
  
  @Test
  public void testComplexUDFReturnsCorrectValues() throws HiveException {
    
    // set up the models we need
    ComplexUDFExample example = new ComplexUDFExample();
    ObjectInspector stringOI = PrimitiveObjectInspectorFactory.javaStringObjectInspector;
    ObjectInspector listOI = ObjectInspectorFactory.getStandardListObjectInspector(stringOI);
    JavaBooleanObjectInspector resultInspector = (JavaBooleanObjectInspector) example.initialize(new ObjectInspector[]{listOI, stringOI});
    
    // create the actual UDF arguments
    List<String> list = new ArrayList<String>();
    list.add("a");
    list.add("b");
    list.add("c");
    
    // test our results
    
    // the value exists
    Object result = example.evaluate(new DeferredObject[]{new DeferredJavaObject(list), new DeferredJavaObject("a")});
    Assert.assertEquals(true, resultInspector.get(result));
    
    // the value doesn't exist
    Object result2 = example.evaluate(new DeferredObject[]{new DeferredJavaObject(list), new DeferredJavaObject("d")});
    Assert.assertEquals(false, resultInspector.get(result2));
    
    // arguments are null
    Object result3 = example.evaluate(new DeferredObject[]{new DeferredJavaObject(null), new DeferredJavaObject(null)});
    Assert.assertNull(result3);
  }
}

注册UDF函数

1.将开发好自定义UDF函数的项目打包成JAR包,并上传至服务器指定地址
2.创建临时UDF函数,指向JAR包地址
create temporary function my_lower as 'com.example.hive.udf.Lower' USING JAR 'hdfs:///path/to/jar';
3.创建UDF函数,指向JAR包地址(注意,永久UDF启用在某些集群配置下需要重启hiveserver才能使用)
create function my_db.my_lower as 'com.example.hive.udf.Lower' USING JAR 'hdfs:///path/to/jar';
4.使用UDF函数
hive> select my_lower(title), sum(freq) from titles group by my_lower(title);
5.删除UDF函数 DROP FUNCTION [IF EXISTS] function_name;
注意:重名UDF函数会导致报错,命名前先试试是否已有该命名

后面有时间的话,会写一篇GenericUDAF的开发笔记。

参考资料

如果英文水平可以的话,推荐把参考资料都看一遍,本人习得之后用自己语言讲述的知识一定没有原文档的清楚和详细。
blog.matthewrathbone.com/2013/08/10/…
blog.csdn.net/czw698/arti…
blog.dataiku.com/2013/05/01/…
cwiki.apache.org/confluence/…