本文由  网易云发布。

概述

出于对可扩展性和性能的考虑,UDF已变成大数据生态圈查询引擎的必备功能之一,无论是Calcite、Hive、Impala都对其进行支持,但是UDF的支持有利也有弊,好处在于它提供了对某些用户独有需求的支持,例如某些产品需要将表中的某字段使用自定义的方式解析成可读字段,例如需要实现特殊的聚合函数;它的弊端在于它对用户开发,这样对于恶意的用户可能执行非正常的逻辑,例如在函数中删除或者拷贝其它文件内容,从而对非授权数据造成破坏,因此对于一个SQL引擎来说,我们需要UDF的集中管理,所有用户自定义的UDF都需要管理员审查源代码,不允许普通用户自己上传UDF,从而避免意外的发生。

对于通常UDF的需求,个人觉得有两方面的需求:1、系统提供的函数完成不了的需求,或者需要使用系统函数进行拼凑才能完成的需求。2、使用当前系统提供的函数性能太差,需要做一些特别的优化。另外,对于UDF还分为两类:自定义函数(UDF)和自定义聚合函数(UDAF),前者会处理每一条输入的记录,转换成处理后的结果,类似于map的功能,后者对于多条记录进行聚合,输出聚合之后的值,类似于reduce的功能。

众所周知Impala使用了Java和C++实现(虽然大多数时候我们都说Impala是C++实现的,所以性能更好,但是它的SQL解析部分的确是Java实现的),Impala同样也支持两种语言的UDF,但是UDAF目前只能支持C++实现,本文分别介绍这些方法如何在

Impala中使用的。

目标

我们这里的example实现一个UDF和一个UDAF,分别实现如下的需求: 我们需要实现的UDF为int level(int),功能为根据value的值计算出距离该值最近的2的幂数的幂值,如果有多个值则取最大的。例如15,距离它最近的2的幂数为16,则level(15)=4, level(9)=3, level(12)=3或4则level(12)=4. 我们需要实现的UDAF为int sum_str(string),功能为计算name中出现的第一个整数的和值,如果该字符串不出现整数则为0,例如"abcd123ef"和"efdg23sd24"的和值为123+23=146. 这两个需求算是比较奇葩了吧,看看如何在impala中利用UDF和UDAF实现它们。

实现

Java版UDF

了解Impala的都知道,Impala可以直接使用Hive的metestore作为元数据库,同样Impala也可以直接使用Hive的UDF,所以对于之前奋斗在Hive第一线的同学们使用Impala有了不少亲切感,那么在这里就顺便温习一遍Hive的UDF使用流程吧。

使用Java实现的UDF必须继承org.apache.hadoop.hive.ql.exec.UDF类,然后在该类中实现名为evaluate的函数,猜测 Hive/Impala是根据函数名来找到具体的实现的,当然一个类里面也可以重载多个evaluate方法。该方法实现如下:

packagecom.netease.hive.udf;

importorg.apache.hadoop.hive.ql.exec.UDF;

publicclass LevelUDF extends UDF {

public Integer evaluate(Integer value) {          if(value == null)

return null;

double temp = value;           int cnt = 0;           int max = 1;

while(temp > 1) {                cnt ++;

temp /= 2;             max *= 2;

}

if(max - value > (value - max / 2))

cnt --;

return cnt;

}

public static void main(String[] args) {

System.out.println(newLevelUDF().evaluate(9));

}

}

然后编译成jar上传到HDFS上,Impala的自定义函数,无论是Java实现还是C++实现的都需要手动放到HDFS上,而非像Hive那样直接可以add jar命令,然后在impala shell中执行create function函数。

> hadoop fs-put ./udf.jar hdfs://namenode-or-nameservice/tmp/nrpt/

> create function level(int) returns intlocation 'hdfs://namenode-or-nameservice/tmp/nrpt/udf.jar' symbol='com.neteas

在impala中创建UDF必须指定参数和返回值,并且执行symbol为类名。根据参数和返回值在运行时查找该函数,如果参数和返回值和类中实现有差别则会出现运行时错误(create function还是能够成功的)。

> select level(15);

+-------------------+

| default.level(15) |

+-------------------+

| 4                 |

+-------------------+

> select level(9);

+------------------+

| default.level(9) |

+------------------+

| 3                |

+------------------+

使用Java实现UDF是非常简单的 ,那么为什么还需要使用C++的实现呢,主要是性能的考虑,毕竟相同的逻辑使用C++实现无论是性能还是资源消耗都会比JAVA好得多,所以Impala官方是非常推崇使用C++实现UDF,并且声称性能会有10倍的提升。

C++实现UDF

使用C++实现UDF就不那么轻松了,首先要面临的就是系统库的差别,所以一般要求UDF开发机和Impala运行的机器使用相同的 Linux发行版,最好在其中的一台Impalad机器上开发,避免出现不可预测的问题,C++开发UDF需要首先下载impalaudf devel开发包和一下其他依赖的工具:

sudo yum install gcc-c++ cmakeboost-devel sudo yum install impala-udf-devel

g++、cmake工具一般开发机上都是有的,boost开发包需要手动安装,当然如果你的函数实现不需要boost也不需要安装这个,不过Impala本身都是使用boost开发的,不过使用boost库可以提高开发效率和性能,impala-udf-devel这个包在安装的时候发现 ubuntu和debain上根本找不到,不过不用担心,其实安装这些包主要就是把这个包下面的.h文件放到系统的include目录下,把.so 文件放到系统的lib下,只要有源码自己也可以编译。可以在https://github.com/laserson/impala-udf-devel下载它们的源码(就两个.h文件和一个.cc文件)。对于UDF开发更推荐这种方式,省的自己写CMakeList.txt文件了,clone到本地之后可以直接运行 cmake生成Makefile文件。

> git clonehttps://github.com/laserson/impala-udf-devel.git

> cd impala-udf-devel/ > cmake.

此时会发现出现了错误:

CMake Error atCMakeLists.txt:46 (add_library):

Cannot find source file:

my-udf-file-1.cc

打开CMakeList.txt文件会发现add_library(myudf SHARED my-udf-file-1.cc my-udf-file-2.cc)这一行,它表示根据my-udf-file-

1.cc和my-udf-file-2.cc文件生成一个动态链接库myudf,我们可以通过该一下这个配置然后编写自己的udf文件,我们把 CMakeList.txt文件的最后几行改为如下:

# Build the UDA/UDFs into a shared library.  You can have multiple UDFs per # file, and/orspecify multiple files here. add_library(level-udfSHARED level-udf.cc)

# The resulting LLVM IR module will have the same name asthe .cc file if (CLANG_EXECUTABLE)   COMPILE_TO_IR(level-udf.cc) endif(CLANG_EXECUTABLE)然后编辑代码level-udf.h(创建):

#ifndefLEVEL_UDF_H

#defineLEVEL_UDF_H

#include"udf/udf.h"

using namespace impala_udf;

IntVal level(FunctionContext*context, const IntVal& value);

#endif

编辑level-udf.cc文件(创建):

#include"level-udf.h"

using namespacestd;

IntVal level(FunctionContext* context,const IntVal& value) {    if(value.is_null)         returnIntVal::null();

int original = value.val;    double temp = (double) original;    int cnt = 0;     int max = 1;     while(temp > 1) {         cnt ++;         temp /= 2;         max *= 2;

}

if((max - original) > (original - max / 2))         cnt --;     return IntVal(cnt);

}

这里需要说明一下的是FunctionContext对象,这个对象是调用UDF函数之前由Impala自己创建的,它包含当前查询的id、查询执行的用户名、使用该对象分配内存,并且可以向Impala日志系统输出warning日志,或者直接输出一条error日志以结束该查询。而每一个函数的输入和输出是IntVal类型,这个是在udf.h中定义的一个类型,它实际上就是包含null的int类型,除了该类型之外还有

AnyVal; BooleanVal; TinyIntVal;SmallIntVal; IntVal; BigIntVal; StringVal; TimestampVal这几种类型,和Impala中的类型一一对应,其中AnyVal是其他类型的父类,它可以标识该对象是否null,其他的基本类型可以通过val成员获取原始值(在不是null的情况下),StringVal可以通过ptr和len获取首地址和长度,TimestampVal则可以获取date和time_of_date分别获取日期和纳秒数(这样的话TimestampVal就可以包含Date了,况且Impala还不支持Date类型)。这里面没有使用boost库(大多数情况下是需要使用的),然后编译,链接生成动态链接库:

> cmake .

> make

执行完成之后在当前目录下产生了build目录(类似于maven构建之后的target目录),该目录下存在链接完成的liblevel-udf.so文件,库名为之前在CMakeList.txt文件中修改之后的名字。

然后按照之前的方式首先上传动态链接库到HDFS,然后create function。

> hadoop fs-put ./liblevel-udf.so hdfs://namenode-or-nameservice/tmp/nrpt/

> createfunction level_c(INT) returns int location 'hdfs://namenode-or-nameservice/tmp/nrpt/liblevel-udf.so'symbol='

使用C++函数时symbol执行函数名,通过该函数名可以在liblevel-udf.so中找到该函数。

> select level_c(12);

+---------------------+

|default.level_c(12) |

+---------------------+

| 4                   |

+---------------------+

> select level_c(9);

+--------------------+

|default.level_c(9) |

+--------------------+

| 3                  |

+--------------------+

至于C++实现和Java实现的性能对比,可以在实际的场景下使用不同的语言实现相同的逻辑,只要你的C++代码写的不是太挫,那么至少会有两倍以上的性能差别的,这里也推荐使用C++实现UDF,不过一定要注意内存泄漏、段错误等情况,如果实现不当可能到 impalad挂掉。对于 C++ 实现UDF的详细说明可以参考Impala官方文档: http://www.cloudera.com/documentation/archive/impala/2-x/2-1-x/topics/impala_udf.html,更多的C++实现的UDF实例可以参考https://github.com/cloudera/impala-udf-samples

C++实现UDAF

实现一个聚合函数需要不像简简单单的实现一个UDF一样,毕竟需要初始化环境,对于每一个输入记录进行聚合,这就要求它在整个执行过程中保持一个状态,例如计算SUM时需要保持一个sum变量记录已经处理的记录的和,这样的方式和reducer有所差别,它是通过对输入的值循环调用aggr函数,而reducer则是将这个列表作为输入处理。在Impala中实现一个UDAF需要实现下面这些函数:

INIT_FN:初始化操作,在查询执行时执行一次,例如清理计数器,分配缓存等。例如SUM时sum=0。 UPDATE_FN:在单机上执行的merge操作,对于每一个节点的每一条记录调用一次该函数,例如SUM时执行sum+= value。

MERGE_FN:节点之间的merge,通常逻辑上和UPDATE_FN操作类似,例如SUM时执行sum+=value。

SERIALIZE_FN:对于INIT、UPDATE、MERGE阶段的结果进行序列化,例如需要对包含指针的值使用它所执行的内容序列化,然后释放该指针。一般情况下不需要设置该函数。

FINALIZE_FN:最终获取结果的函数,只调用一次。

我们这里需要实现的是统计某一个string列中每一个成员第一次出现的整数的和,我们定义为int sum_first_int(string),这里不牵扯到指针的序列化,所以不需要实现SERIALIZE_FN函数。

该需求的实现sum-udaf.h(新建):

#ifndef_SUM_UDAF_H_

#define_SUM_UDAF_H_

#include"udf/udf.h"

using namespaceimpala_udf;

void init_func(FunctionContext*context, BigIntVal* result);

void update_func(FunctionContext*context, const StringVal& input, BigIntVal* result);

void merge_func(FunctionContext*context, const StringVal& input, BigIntVal* result);

BigIntVal finalize_func(FunctionContext*context, const BigIntVal& val);

#endif

实现了四个函数的定义,其中init_func函数传递了一个BigIntVal函数,它是由函数的返回值决定的,result参数作为保存聚合结果,update_func函数对于每一个输入的记录进行处理,input参数为该记录中该列的值,merge_func对于不同节点的聚合结果进行再聚合,finalize_func则是返回最终结果。可以看出每一个聚合组的状态信息保存在result参数中。

实现文件sum-udaf.cc(新建):

#include"sum-udaf.h"

#include<ctype.h>

void init_func(FunctionContext* context, BigIntVal* result) {

result->is_null = false; result->val = 0;

}

void update_func(FunctionContext*context, const StringVal& input, BigIntVal* result) {     if(input.is_null) return;     int cur = 0;     uint8_t *ptr = input.ptr;     int len = input.len;     int i = 0;

for(i = 0 ; i < len && !isdigit(ptr[i]); ++ i);     while(i < len &&isdigit(ptr[i])) {         cur = cur * 10 + (ptr[i] - '0');

++ i;

}

result->val += cur;

}

void merge_func(FunctionContext*context, const BigIntVal& input, BigIntVal* result) {     result->val += input.val;

}

BigIntVal finalize_func(FunctionContext*context, const BigIntVal& val) {    return val;

}

完成之后再修改CMakeList.txt文件,增加对sum-udaf.cc的编译和链接,然后执行cmake和make(同上),就可以生成新的.so链接库了。使用相同的方式将该库上传到HDFS然后再impala shell上创建聚合函数。

> createaggregate function sum_first_int(string) returns bigint

> location'hdfs://namenode-or-nameservice/tmp/nrpt/liblevel-udf.so' init_fn='init_func'update_fn='update_func' merg

此时可以使用该聚合函数了,如下:

> CREATETABLE default.test (id string);

> insertinto test values("abc1234edf"), ("12shd45"), ("dhhf"),("qwe3"), ("2016-09-01");

> select sum_first_int(id) fromtest;

+---------------------------+

|default.sum_first_int(id) |

+---------------------------+

| 3265                      |

+---------------------------+

总结

本文详细讲述了在Impala中实现自定义函数和自定义聚合函数的方式并给出实现实例以供参考,使用UDF和UDAF我们可以很轻易的实现特定需求的计算逻辑,也可以用性能更好的方式实现一些相对固定的需求,例如我们可以使用UDAF函数实现更好性能的 distinct count计算(利用hyoerloglog实现有误差的去重计数),但是开放的接口也可能存在一些安全性上的问题,给系统运维的也带来了一定的负担,所以对于自定义函数的引入也需要谨慎。

想要了解网易大数据,请戳这里网易大数据|专业的私有化大数据平台

了解 网易云 :
网易云官网:https://www.163yun.com/
新用户大礼包:https://www.163yun.com/gift
网易云社区:https://sq.163yun.com/

自己动手写Impala UDF的更多相关文章

  1. 【原创】自己动手写控件----XSmartNote控件

    一.前面的话 在上一篇博文自己动手写工具----XSmartNote [Beta 3.0]中,用到了若干个自定义控件,其中包含用于显示Note内容的简单的Label扩展控件,用于展示标签内容的labe ...

  2. 【原创】自己动手写工具----XSmartNote [Beta 3.0]

    一.前面的话 在动笔之前,一直很纠结到底要不要继续完成这个工具,因为上次给它码代码还是一年多之前的事情,参考自己动手写工具----XSmartNote [Beta 2.0],这篇博文里,很多园友提出了 ...

  3. 【原创】自己动手写工具----XSmartNote [Beta 2.0]

    一.前面的话 在上一篇自己动手写工具----XSmartNote中,我简单介绍了这个小玩意儿的大致界面和要实现的功能,看了一下园子里的评论,评价褒贬不一,有人说“现在那么多云笔记的工具”,“极简版ev ...

  4. 【原创】自己动手写工具----签到器[Beta 2.0]

    一.前面的话 上一篇中基本实现了简单的签到任务,但是不够灵活.在上一篇自己动手写工具----签到器的结尾中,我设想了几个新增功能来提高工具的灵活程度,下面把新增功能点列出来看看: (1)新增其他的进程 ...

  5. 自己动手写ORM的感受

    之前看到奋斗前辈和时不我待前辈的自己动手写ORM系列博客,感觉讲解的通俗易懂,清晰透彻.作为一个菜鸟,闲来也想着自己写一个ORM,一来加深自己对 ORM的理解,以求对EF,NHibernate等ROM ...

  6. 自己动手写插件底层篇—基于jquery移动插件实现

    序言 本章作为自己动手写插件的第一篇文章,会尽可能的详细描述一些实现的方式和预备知识的讲解,随着知识点积累的一点点深入,可能到了后期讲解也会有所跳跃.所以,希望知识点不是很扎实的读者或者是初学者,不要 ...

  7. Python - 动手写个ORM

    Python - 动手写个ORM 任务: 模拟简单的ORM - Object Relational Mapping 为model添加create方法 代码很简单,直接上 字段类型类 class Fie ...

  8. 【转】自己动手写SC语言编译器

    自序 编译原理与技术的一整套理论在整个计算机科学领域占有相当重要的地位,学习它对程序设计人员有很大的帮助.我们考究历史会发现那些人人称颂的程序设 计大师都是编译领域的高手,像写出BASIC语言的BIL ...

  9. 自己动手写CPU之第九阶段(8)——MIPS32中的LL、SC指令说明

    将陆续上传新书<自己动手写CPU>,今天是第47篇. 9.7 ll.sc指令实现思路 9.7.1 实现思路 这2条指令都涉及到訪问链接状态位LLbit,能够将LLbit当做寄存器处理,ll ...

随机推荐

  1. Linux查看系统负载(CPU和MEM考虑)

    查看占用CPU最高的10个进程 [tidb@:vg_adn_tidbCkhsTest:| head mysql ? Sl Nov22 : /usr/local/mysql/bin/mysqld --b ...

  2. 阿里八八Alpha阶段Scrum(12/12)

    今日进度 俞鋆: 和前端对接,完成了注册接口的对接写了一部分数据同步接口. 李嘉群: 完成了注册界面与服务器的对接 黄梅玲: json数据解析并在listview中显示(还未完成) 王国超: 今天开始 ...

  3. Django商城项目笔记No.15用户部分-用户中心邮箱验证

    首先增加前端页面success_verify_email.html页面 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitiona ...

  4. MPT树详解

    目录 MPT树定义 MPT树的作用是什么? 前缀树与默克尔树 前缀树 默克尔树 三种节点类型 MPT中的Merkle HP编码 官方表示形式 相关MPT树 参考目录 @ MPT树定义 一种经过改良的. ...

  5. PyQt5---firstwindow

    # -*- coding:utf-8 -*- ''' Created on Sep 13, 2018 @author: SaShuangYiBing ''' import sys from PyQt5 ...

  6. saxbuilder用法

    xml为我们在网络交换数据带来很大方便,在java中可以使用saxbuilder来操作xml格式文件,下面介绍一下saxbuilder的常用方法. import org.jdom.Document; ...

  7. SVG绘制图形

    一.SVG介绍 1.SVG指可伸缩矢量图片 2.SVG用来定义用于网络的基于矢量的图形 3.SVG使用XML格式定义图形 4.SVG图像在放大或改变尺寸的情况下其图形质量不会有损失 5.SVG是万维网 ...

  8. 关于requests.exceptions.SSLError: HTTPSConnectionPool

    问题: requests.exceptions.SSLError: HTTPSConnectionPool(host='mall.christine.com.cn', port=443): Max r ...

  9. SDOI 2018 R2 游记

    一篇真正的“游记”. DAY -3 下午: 今天老师批准了我去省选划水的请假要求. 批准之后感觉学习非常有动力,顺便忽悠别的同学和我一起去,然而wzx是唯一一个表示可以考虑一下的同学,其他同学直接一口 ...

  10. BZOJ3879:SvT(后缀数组,单调栈,ST表)

    Description (我并不想告诉你题目名字是什么鬼) 有一个长度为n的仅包含小写字母的字符串S,下标范围为[1,n]. 现在有若干组询问,对于每一个询问,我们给出若干个后缀(以其在S中出现的起始 ...