作者简介:李世阳,SelectDB生态研发工程师,ApacheDorisContributor。我们在使用各种SQL引擎的时候,会遇到复杂的查询需求。一部分可以通过引擎自带的内置函数来解决,但是内置函数往往具有一定的通用性。在某些特殊场景下,内置功能可能无法满足需求。所以一般的SQL引擎都会提供UDF功能,方便用户编写自己的逻辑来满足需求。具体要求,ApacheDoris也不例外。在JavaUDF之前,ApacheDoris提供了原生UDF。由于是C++编写,执行效率高,速度较快,但在实际使用中也存在一些问题:与Doris代码耦合度高,需要自己打包编译Doris源码,只支持C++语言,UDF代码错误会影响Doris集群的稳定性对于只熟悉Hive、Spark等大数据组件的用户来说有一定的门槛。从上面可以看出,原生UDF的实现门槛比较高,存在一定的不稳定因素。那么有没有实现起来比较简单,使用门槛低,与Doris代码耦合度低的UDF呢?答案是肯定的。在2022年12月正式发布的ApacheDoris1.2.0版本中(https://github.com/apache/dor...),我们引入了新的JavaUDF和RemoteUDF功能,其中JavaUDF不仅可以满足以上要求,从方便和安全的角度给用户带来全新的体验:不熟悉C++?是否可以使用与Java代码相同的严格条件来实现您自己的UDF?只要有Jar包就可以用担心稳定性?JavaUDF错误只影响自身,对Doris的稳定性几乎没有影响。从旧的大数据平台迁移数据和UDF是否费时费力?JavaUDF与HiveUDF完全兼容,易于实现快速迁移......设计思路的大致步骤ApacheDoris的BE是用C++代码编写的。如果要在Doris中实现JavaUDF,必然需要调用JNI,这是不正确的。JNI调用会导致严重的性能问题。那么如何设计一个JavaUDF来解决这个问题呢?DorisJavaUDF是为向量化引擎设计的,其设计思路如下:首先,制定用户在创建UDF时必须遵循的一些规则。例如,一个UDF类必须有一个Evaluate方法并且必须是Public和Non-Static的。这些规则确保我们可以正确调用UDF。其次,Doris查询引擎执行一个新的Java函数调用,BE创建或复用一个JVM来调用真正的JavaUDF。为了隔离不同的UDF实例,选择使用不同的类加载器来加载UDF。最后,因为执行是向量化的,所以可以一次执行多行数据,只调用一次JNI,因为JNI开销由输入列中的所有行共享[1],这会给用户带来更好的性能经验。详细步骤熟悉Java的朋友应该知道,JVM在直接内存中的IO操作,即非堆区比堆区效率更高,所以DorisJavaUDF一般是对直接内存中的数据进行IO操作。UDF通常有以下几种情况:一般UDF(定长UDF)这里的基本思想是将地址直接指向输入缓冲区和输出缓冲区,Doris可以直接从给定的地址读写回数据,即它可以帮助Doris避免不必要的数据复制。InputBuffer和OutputBuffer都是JVM的堆外内存,可以通过JavaAPI直接操作。整体执行方式如下:UDAF(可变长度输出)对于通用UDF,输出大小和类型不变,所以需要的Buffer大小也确定了,而对于UDAF(可变长度输出),通用UDF(固定-lengthUDF)步骤将不再适用。因此,需要做如下改动:**在第一步分配一个初始缓冲区,当结果大于分配的初始缓冲区时跳到第三步,在第三步进行扩容。一直这样的话,我们再次重复上述步骤以分配新的缓冲区并继续对剩余行执行UDF,直到所有数据都已执行。执行过程如下图所示:通过上面的介绍,我们对DorisJavaUDF的实现有了基本的了解,那么在实际生产中如何使用JavaUDF呢?使用JavaUDFJavaUDF使用起来非常简单。JavaUDF注册到Doris后,Doris在执行过程中通过调用jar包来实现UDF逻辑。序列结构如下图所示:具体步骤:参考`doris/samples/doris-demo/java-udf-demo/src/main/java/org/apache/doris/udf/AddOne.java`文件编写UDF逻辑,你可以像HiveUDF一样在任何地方编写和打包,而不是绑定到Doris环境。AddOne.java文件内容如下://根据一个//或多个贡献者许可协议授权给Apache软件基金会(ASF)。有关版权所有权的其他信息,请参阅与此作品一起分发的通知文件//。ASF根据Apache许可证2.0版(“许可证”)向您//授予此文件的许可;除非符合//许可,否则您不得使用此文件。您可以在////http://www.apache.org/licenses/LICENSE-2.0////获得许可证副本,除非适用法律要求或书面同意,//根据许可证分发的软件在//“按原样”的基础上分发,没有任何//种类的明示或暗示的保证或条件。请参阅许可证下的//特定语言管理权限和限制//。packageorg.apache.doris.udf;importorg.apache.hadoop.hive.ql.exec.UDF;publicclassAddOneextendsUDF{公共整数评估(在整数值){返回值==null?空值:值+1;}}执行mvnpackaging命令mvncleanpackage创建UDFCREATEFUNCTIONjava_udf_name(int)RETURNSintPROPERTIES("file"="file:///path/to/your_jar_name.jar","symbol"="org.apache.doris.udf.AddOne","always_nullable"="true","type"="JAVA_UDF");使用创建的UDF创建表:CREATETABLEIFNOTEXISTStest.t1(`col_1`intNOTNULL)DISTRIBUTEDBYHASH(col_1)PROPERTIES("replication_num"="1");插入数据:insertintotest.t1values(1),(2);使用udf:MySQL[(none)]>selectcol_1,java_udf_name(col_1)ascol_2fromtest.t1;+------------+------------+|col_1|col_2||+------------+------------+|1|2||2|3||+----------+------------+到目前为止,多丽丝JavaUDF的创建和使用就完成了。它非常简单易用。注意事项首先需要确定BE节点是否配置了JAVA_HOME。如果没有配置环境变量,可以在be/bin/start_be.sh文件的第一行添加exportJAVA_HOME=/xxx/xxxUDF代码中必须包含以下信息(UDAF替换成对应的)导入org.apache.hadoop.hive.ql.exec.UDF;创建DorisjavaUDF的语句,其格式如下CREATEFUNCTIONname([,...])[RETURNS]rettypePROPERTIES(["key"="value"][,...])示例如下CREATEFUNCTIONjava_udf_name(int)RETURNSintPROPERTIES("file"="file:///path/to/your_jar_name.jar","always_nullable"="true","type"="JAVA_UDF");java_udf_name是创建的UDF的名称,可以更改,UDF名称不能与其他Doris函数Life重复。名字后面的`(int)`表示函数输入参数是int类型,`RETURNS`后面的`int`表示函数输出也是int类型;输入输出类型必须与Java代码中Evaluate函数的输入输出类型保持一致。PROPERTIESfile表示jar包在本机的路径,修改"/path/to/your_jar_name.jar"为jar包的绝对路径。如果是多机环境,也可以使用http表示的路径,例如"file"="http://${host}:${http_port}/${your_jar_file}"`可以使用简单启动http服务器的python命令:nohuppython-mSimpleHTTPServer12345>/dev/null2>&1Symbol可以参考Java代码中的package`always_nullable`来说明UDF返回结果是否可以出现NULL值.如果要对计算中出现的NULL值进行特殊处理,保证结果中不会返回NULL,可以设置为false,有利于提高整个查询计算过程的性能。#收益总结通过本文的介绍,了解了DorisJavaUDF的设计和使用,那么在实际应用中,DorisJavaUDF能给用户带来哪些收益呢?-熟悉Java的同学也可以快速开发Doris,简单易用,大大提高开发效率。-兼容HiveUDF,有效降低Hadoop数据迁移成本。-UDF代码错误不会影响到Doris,一定程度上保证了Doris更好更稳定的运行。-与Doris代码解耦,真正做到“WriteOnceRunAnywhere”-在执行效率上,JavaUDF完全向量化执行,一次执行多行数据,只调用一次JNI,结合堆外内存,零Copy等优化技术,用户在使用JavaUDF时,也可以获得与之前的C++UDF相同甚至更好的查询性能体验。#社区贡献如果你的UDF已经在很多场景得到应用,你可以将UDF贡献给ApacheDoris社区。贡献步骤可以参考:
