目录
前言
命令行注册UDF函数(Create Function xxx as "全限定类名")
语法分析
生成物理计划
执行物理计划进行函数注册
Select带有UDF函数的查询
继上个月开始了Apache IoTDB的源码贡献,闲来有空时,便会看看感兴趣模块的代码。这次主要跟大家分享一下自己对Apache IoTDB’s UDF相关源码的一些分析与理解。至于为什么选择阅读它,或者说为什么对它感兴趣,可能我觉得这也是一个软件扩展性的一个体现吧!好了,废话不多说,开始上菜~
注:再啰嗦一下哈!如果大家对Apache IoTDB的UDF的使用没有一个认识的话,建议最好可以看一下它的官方文档:UDF官方文档,我刚开始看的时候感觉有点难看,建议读者如果跟我一样的感受,最好是自己跟着写一个小Demo就清楚了,它github仓库代码也有例子。
IoTDB> create function example1 as "org.study.demo.UDTFExample"
Msg: The statement is executed successfully.
这句话背后到底发生了什么?
在server/src/main/java/org/apache/iotdb/db/qp/sql/IoTDBSqlVisitor.java里visitCreateFunction方法里将该语句转换成了内部的CreateFunctionOperator。
@Override
public Operator visitCreateFunction(IoTDBSqlParser.CreateFunctionContext ctx) {
CreateFunctionOperator createFunctionOperator =
new CreateFunctionOperator(SQLConstant.TOK_FUNCTION_CREATE);
createFunctionOperator.setUdfName(parseIdentifier(ctx.udfName.getText()));
createFunctionOperator.setClassName(parseStringLiteral(ctx.className.getText()));
return createFunctionOperator;
}
server/src/main/java/org/apache/iotdb/db/qp/logical/sys/CreateFunctionOperator.java
@Override
public PhysicalPlan generatePhysicalPlan(PhysicalGenerator generator)
throws QueryProcessException {
return new CreateFunctionPlan(udfName, className);
}
server/src/main/java/org/apache/iotdb/db/qp/executor/PlanExecutor.java
@Override
public boolean processNonQuery(PhysicalPlan plan)
throws QueryProcessException, StorageGroupNotSetException, StorageEngineException {
、、、
case CREATE_FUNCTION:
return operateCreateFunction((CreateFunctionPlan) plan);
、、、
}
private boolean operateCreateFunction(CreateFunctionPlan plan) throws UDFRegistrationException {
UDFRegistrationService.getInstance().register(plan.getUdfName(), plan.getClassName(), true);
return true;
}
由此可见关键就在于UDFRegistrationService的注册方法
server/src/main/java/org/apache/iotdb/db/query/udf/service/UDFRegistrationService.java
public void register(String functionName, String className, boolean writeToTemporaryLogFile)
throws UDFRegistrationException {
functionName = functionName.toUpperCase();
validateFunctionName(functionName, className); // 检查函数名是否合法,主要是跟内置的一些函数是否冲突[1]
checkIfRegistered(functionName, className); // 检查是否注册过
doRegister(functionName, className); // 注册,通过反射构造Method对象,放进ConcurrentHashMap中
tryAppendRegistrationLog(functionName, className, writeToTemporaryLogFile);
}
看到这,UDF注册已经没有密码了,就是根据用户传进的全限定类名进行反射,获得Method引用,保存起来用于后续UDF查询的调用。(详见代码注释)
其实还有个问题,就是Apache IoTDB如何知道到哪加载用户类呢?
这个其实类似一种约定,代码内部是写死的,用户要将写好的UDF类打包的jar放到指定目录ext/udf下,具体见下面的代码片段。
server/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java
/** External lib directory for UDF, stores user-uploaded JAR files */
private String udfDir =
IoTDBConstant.EXT_FOLDER_NAME + File.separator + IoTDBConstant.UDF_FOLDER_NAME;
node-commons/src/main/java/org/apache/iotdb/commons/conf/IoTDBConstant.java
public static final String EXT_FOLDER_NAME = "ext";
public static final String UDF_FOLDER_NAME = "udf";
SELECT s1, example1(s1), s2, example1(s2) FROM root.sg1.d1;
接下来就是带有UDF查询的SQL语句背后的执行机制,讲真的我觉得对于现在的我来说,也是好不容易看懂,但你真要我比较清晰地讲出来,可能得再过段时间了,再梳理梳理,这里先稍微简单讲讲吧。
基本思想,肯定后面会根据前面注册的那个Method引用在适当的地方进行对查询的数据进行转换。但在具体实现时,那位作者将其分成三层
InputLayer 2. TransformerLayer 3. OutputLayer
InputLayer: 一个原始的timeseries数据的scan的输入(多个用到该timeseries的数据也只需一个,避免重复资源浪费、效率等等吧);
TransformerLayer: 如果有UDF会进行转换,有的是直接透传
OutputLayer: 最终查询列的输出形式
上述的语句最终会转换成UDTFQueryOperator,然后会进一步进行处理,这里贴出一些关键代码给大家解解馋,敬请期待下文哈!如果大家实在等不了,可以自行阅读代码,如果有什么见解,欢迎与我讨论,共同进步~
@Override
public IntermediateLayer constructIntermediateLayer( // 构建中间层是进行UDF的调用
long queryId,
UDTFPlan udtfPlan,
RawQueryInputLayer rawTimeSeriesInputLayer,
Map expressionIntermediateLayerMap,
Map expressionDataTypeMap,
LayerMemoryAssigner memoryAssigner)
throws QueryProcessException, IOException {
if (!expressionIntermediateLayerMap.containsKey(this)) {
float memoryBudgetInMB = memoryAssigner.assign();
Transformer transformer;
if (isPlainAggregationFunctionExpression) {
transformer =
new TransparentTransformer( // 等于没有转换,相当于透传
rawTimeSeriesInputLayer.constructPointReader(
udtfPlan.getReaderIndexByExpressionName(toString())));
} else {
IntermediateLayer udfInputIntermediateLayer =
constructUdfInputIntermediateLayer(
queryId,
udtfPlan,
rawTimeSeriesInputLayer,
expressionIntermediateLayerMap,
expressionDataTypeMap,
memoryAssigner);
transformer =
constructUdfTransformer( // 执行了validate和beforeStart两步,根据相应的访问策略AccessStrategy返回对应的Transformer
queryId,
udtfPlan,
expressionDataTypeMap,
memoryAssigner,
udfInputIntermediateLayer);
}
expressionDataTypeMap.put(this, transformer.getDataType());
expressionIntermediateLayerMap.put(
this,
memoryAssigner.getReference(this) == 1
? new SingleInputColumnSingleReferenceIntermediateLayer(
this, queryId, memoryBudgetInMB, transformer)
: new SingleInputColumnMultiReferenceIntermediateLayer(
this, queryId, memoryBudgetInMB, transformer)); // 它的LayerPointReader是transformer
}
return expressionIntermediateLayerMap.get(this);
}
private UDFQueryTransformer constructUdfTransformer(
long queryId,
UDTFPlan udtfPlan,
Map expressionDataTypeMap,
LayerMemoryAssigner memoryAssigner,
IntermediateLayer udfInputIntermediateLayer)
throws QueryProcessException, IOException {
UDTFExecutor executor = udtfPlan.getExecutorByFunctionExpression(this);
executor.beforeStart(queryId, memoryAssigner.assign(), expressionDataTypeMap);
AccessStrategy accessStrategy = executor.getConfigurations().getAccessStrategy();
switch (accessStrategy.getAccessStrategyType()) {
case ROW_BY_ROW:
return new UDFQueryRowTransformer(udfInputIntermediateLayer.constructRowReader(), executor);
case SLIDING_SIZE_WINDOW:
case SLIDING_TIME_WINDOW:
return new UDFQueryRowWindowTransformer(
udfInputIntermediateLayer.constructRowWindowReader(
accessStrategy, memoryAssigner.assign()),
executor);
default:
throw new UnsupportedOperationException("Unsupported transformer access strategy");
}
}