本文会主要讲三种udf:

  • ScalarFunction

  • TableFunction

  • AggregateFunction

用户自定义函数是非常重要的一个特征,因为他极大地扩展了查询的表达能力。本文除了介绍这三种udf之外,最后会介绍一个redis作为交互数据源的udf案例。

注册用户自定义函数

在大多数场景下,用户自定义函数在使用之前是必须要注册的。对于Scala的Table API,udf是不需要注册的。

调用TableEnvironment的registerFunction()方法来实现注册。Udf注册成功之后,会被插入TableEnvironment的function catalog,这样table API和sql就能解析他了。

1.Scalar Functions 标量函数

标量函数,是指返回一个值的函数。标量函数是实现将0,1,或者多个标量值转化为一个新值。

实现一个标量函数需要继承ScalarFunction,并且实现一个或者多个evaluation方法。标量函数的行为就是通过evaluation方法来实现的。evaluation方法必须定义为public,命名为eval。evaluation方法的输入参数类型和返回值类型决定着标量函数的输入参数类型和返回值类型。evaluation方法也可以被重载实现多个eval。同时evaluation方法支持变参数,例如:eval(String... strs)。

下面给出一个标量函数的例子。例子实现的是一个hashcode方法。

  1. public class HashCode extends ScalarFunction {
  2. private int factor = 12;
  3. public HashCode(int factor) {
  4. this.factor = factor;
  5. }
  6. public int eval(String s) {
  7. return s.hashCode() * factor;
  8. }
  9. }
  10. BatchTableEnvironment tableEnv = TableEnvironment.getTableEnvironment(env);
  11. // register the function
  12. tableEnv.registerFunction("hashCode", new HashCode(10));
  13. // use the function in Java Table API
  14. myTable.select("string, string.hashCode(), hashCode(string)");
  15. // use the function in SQL API
  16. tableEnv.sqlQuery("SELECT string, HASHCODE(string) FROM MyTable");

默认情况下evaluation方法的返回值类型是由flink类型抽取工具决定。对于基础类型及简单的POJOS是足够的,但是更复杂的类型,自定义类型,组合类型,会报错。这种情况下,返回值类型的TypeInformation,需要手动指定,方法是重载ScalarFunction#getResultType()。

下面给一个例子,通过复写ScalarFunction#getResultType(),将long型的返回值在代码生成的时候翻译成Types.TIMESTAMP。

  1. public static class TimestampModifier extends ScalarFunction {
  2. public long eval(long t) {
  3. return t % 1000;
  4. }
  5. public TypeInformation<?> getResultType(signature: Class<?>[]) {
  6. return Types.TIMESTAMP;
  7. }
  8. }

2.Table Functions 表函数

与标量函数相似之处是输入可以0,1,或者多个参数,但是不同之处可以输出任意数目的行数。返回的行也可以包含一个或者多个列。

为了自定义表函数,需要继承TableFunction,实现一个或者多个evaluation方法。表函数的行为定义在这些evaluation方法内部,函数名为eval并且必须是public。TableFunction可以重载多个eval方法。Evaluation方法的输入参数类型,决定着表函数的输入类型。Evaluation方法也支持变参,例如:eval(String... strs)。返回表的类型取决于TableFunction的基本类型。Evaluation方法使用collect(T)发射输出rows。

在Table API中,表函数在scala语言中使用方法如下:.join(Expression) 或者 .leftOuterJoin(Expression),在java语言中使用方法如下:.join(String) 或者.leftOuterJoin(String)。

  • Join操作算子会使用表函数(操作算子右边的表)产生的所有行进行(cross) join 外部表(操作算子左边的表)的每一行。
  • leftOuterJoin操作算子会使用表函数(操作算子右边的表)产生的所有行进行(cross) join 外部表(操作算子左边的表)的每一行,并且在表函数返回一个空表的情况下会保留所有的outer rows。

在sql语法中稍微有点区别:

  • cross join用法是LATERAL TABLE(<TableFunction>)。
  • LEFT JOIN用法是在join条件中加入ON TRUE。

下面的例子讲的是如何使用表值函数。

  1. // The generic type "Tuple2<String, Integer>" determines the schema of the returned table as (String, Integer).
  2. public class Split extends TableFunction<Tuple2<String, Integer>> {
  3. private String separator = " ";
  4. public Split(String separator) {
  5. this.separator = separator;
  6. }
  7. public void eval(String str) {
  8. for (String s : str.split(separator)) {
  9. // use collect(...) to emit a row
  10. collect(new Tuple2<String, Integer>(s, s.length()));
  11. }
  12. }
  13. }
  14. BatchTableEnvironment tableEnv = TableEnvironment.getTableEnvironment(env);
  15. Table myTable = ... // table schema: [a: String]
  16. // Register the function.
  17. tableEnv.registerFunction("split", new Split("#"));
  18. // Use the table function in the Java Table API. "as" specifies the field names of the table.
  19. myTable.join("split(a) as (word, length)").select("a, word, length");
  20. myTable.leftOuterJoin("split(a) as (word, length)").select("a, word, length");
  21. // Use the table function in SQL with LATERAL and TABLE keywords.
  22. // CROSS JOIN a table function (equivalent to "join" in Table API).
  23. tableEnv.sqlQuery("SELECT a, word, length FROM MyTable, LATERAL TABLE(split(a)) as T(word, length)");
  24. // LEFT JOIN a table function (equivalent to "leftOuterJoin" in Table API).
  25. tableEnv.sqlQuery("SELECT a, word, length FROM MyTable LEFT JOIN LATERAL TABLE(split(a)) as T(word, length) ON TRUE");

需要注意的是PROJO类型不需要一个确定的字段顺序。意味着你不能使用as修改表函数返回的pojo的字段的名字。

默认情况下TableFunction返回值类型是由flink类型抽取工具决定。对于基础类型及简单的POJOS是足够的,但是更复杂的类型,自定义类型,组合类型,会报错。这种情况下,返回值类型的TypeInformation,需要手动指定,方法是重载TableFunction#getResultType()。

下面的例子,我们通过复写TableFunction#getResultType()方法使得表返回类型是RowTypeInfo(String, Integer)。

  1. public class CustomTypeSplit extends TableFunction<Row> {
  2. public void eval(String str) {
  3. for (String s : str.split(" ")) {
  4. Row row = new Row(2);
  5. row.setField(0, s);
  6. row.setField(1, s.length);
  7. collect(row);
  8. }
  9. }
  10. @Override
  11. public TypeInformation<Row> getResultType() {
  12. return Types.ROW(Types.STRING(), Types.INT());
  13. }
  14. }

3.Aggregation Functions 聚合函数

用户自定义聚合函数聚合一张表(一行或者多行,一行有一个或者多个属性)为一个标量的值。
[图片上传失败...(image-f5e972-1542542047386)]
上图中是讲的一张饮料的表这个表有是那个字段五行数据,现在要做的是求出所有饮料的最高价。

聚合函数需要继承AggregateFunction。聚合函数工作方式如下:

  • 首先,需要一个accumulator,这个是保存聚合中间结果的数据结构。调用AggregateFunction函数的createAccumulator()方法来创建一个空accumulator.

  • 随后,每个输入行都会调用accumulate()方法来更新accumulator。一旦所有的行被处理了,getValue()方法就会被调用,计算和返回最终的结果。

对于每个AggregateFunction,下面三个方法都是比不可少的:

  1. createAccumulator()
  2. accumulate()
  3. getValue()

flink的类型抽取机制不能识别复杂的数据类型,比如,数据类型不是基础类型或者简单的pojos类型。所以,类似于ScalarFunction 和TableFunction,AggregateFunction提供了方法去指定返回结果类型的TypeInformation,用的是AggregateFunction#getResultType()。Accumulator类型用的是AggregateFunction#getAccumulatorType()。

除了上面的方法,还有一些可选的方法。有些方法是让系统更加高效的执行查询,另外的一些在特定的场景下是必须的。例如,merge()方法在会话组窗口(session group window)上下文中是必须的。当一行数据是被视为跟两个回话窗口相关的时候,两个会话窗口的accumulators需要被join。

AggregateFunction的下面几个方法,根据使用场景的不同需要被实现:

  • retract():在bounded OVER窗口的聚合方法中是需要实现的。
  • merge():在很多batch 聚合和会话窗口聚合是必须的。
  • resetAccumulator(): 在大多数batch聚合是必须的。

AggregateFunction的所有方法都是需要被声明为public,而不是static。定义聚合函数需要实现org.apache.flink.table.functions.AggregateFunction同时需要实现一个或者多个accumulate方法。该方法可以被重载为不同的数据类型,并且支持变参。

为了计算加权平均值,累加器需要存储已累积的所有数据的加权和及计数。在栗子中定义一个WeightedAvgAccum类作为accumulator。尽管,retract(), merge(), 和resetAccumulator()方法在很多聚合类型是不需要的,这里也给出了栗子。

  1. /**
  2. * Accumulator for WeightedAvg.
  3. */
  4. public static class WeightedAvgAccum {
  5. public long sum = 0;
  6. public int count = 0;
  7. }
  8. /**
  9. * Weighted Average user-defined aggregate function.
  10. */
  11. public static class WeightedAvg extends AggregateFunction<Long, WeightedAvgAccum> {
  12. @Override
  13. public WeightedAvgAccum createAccumulator() {
  14. return new WeightedAvgAccum();
  15. }
  16. @Override
  17. public Long getValue(WeightedAvgAccum acc) {
  18. if (acc.count == 0) {
  19. return null;
  20. } else {
  21. return acc.sum / acc.count;
  22. }
  23. }
  24. public void accumulate(WeightedAvgAccum acc, long iValue, int iWeight) {
  25. acc.sum += iValue * iWeight;
  26. acc.count += iWeight;
  27. }
  28. public void retract(WeightedAvgAccum acc, long iValue, int iWeight) {
  29. acc.sum -= iValue * iWeight;
  30. acc.count -= iWeight;
  31. }
  32. public void merge(WeightedAvgAccum acc, Iterable<WeightedAvgAccum> it) {
  33. Iterator<WeightedAvgAccum> iter = it.iterator();
  34. while (iter.hasNext()) {
  35. WeightedAvgAccum a = iter.next();
  36. acc.count += a.count;
  37. acc.sum += a.sum;
  38. }
  39. }
  40. public void resetAccumulator(WeightedAvgAccum acc) {
  41. acc.count = 0;
  42. acc.sum = 0L;
  43. }
  44. }
  45. // register function
  46. StreamTableEnvironment tEnv = ...
  47. tEnv.registerFunction("wAvg", new WeightedAvg());
  48. // use function
  49. tEnv.sqlQuery("SELECT user, wAvg(points, level) AS avgPoints FROM userScores GROUP BY user");

4.udf的最佳实践经验

4.1 Table API和SQL

代码生成器内部会尽可能多的尝试使用原生值。用户定义的函数可能通过对象创建、强制转换(casting)和拆装箱((un)boxing)引入大量开销。因此,强烈推荐参数和返回值的类型定义为原生类型而不是他们包装类型(boxing class)。Types.DATE 和Types.TIME可以用int代替。Types.TIMESTAMP可以用long代替。

建议用户自定义函数使用java编写而不是scala编写,因为scala的类型可能会有不被flink类型抽取器兼容。

4.2 用Runtime集成UDFs

有时候udf需要获取全局runtime信息或者在进行实际工作之前做一些设置和清除工作,比如,打开数据库链接和关闭数据库链接。Udf提供了open()和close()方法,可以被复写,功能类似Dataset和DataStream API的RichFunction方法。

Open()方法是在evaluation方法调用前调用一次。Close()是在evaluation方法最后一次调用后调用。Open()方法提共一个FunctionContext,FunctionContext包含了udf执行环境的上下文,比如,metric group,分布式缓存文件,全局的job参数。

通过调用FunctionContext的相关方法,可以获取到相关的信息:

  • getMetricGroup()并行子任务的指标组;
  • getCachedFile(name)分布式缓存文件的本地副本;
  • getJobParameter(name, defaultValue)给定key全局job参数;

给出的例子就是通过FunctionContext在一个标量函数中获取全局job的参数。主要是实现获取redis的配置,然后简历redis链接,实现redis的交互的过程。

  1. import org.apache.flink.table.functions.FunctionContext;
  2. import org.apache.flink.table.functions.ScalarFunction;
  3. import redis.clients.jedis.Jedis;
  4. public class HashCode extends ScalarFunction {
  5. private int factor = 12;
  6. Jedis jedis = null;
  7. public HashCode() {
  8. super();
  9. }
  10. @Override
  11. public void open(FunctionContext context) throws Exception {
  12. super.open(context);
  13. String redisHost = context.getJobParameter("redis.host","localhost");
  14. int redisPort = Integer.valueOf(context.getJobParameter("redis.port","6379"));
  15. jedis = new Jedis(redisHost,redisPort);
  16. }
  17. @Override
  18. public void close() throws Exception {
  19. super.close();
  20. jedis.close();
  21. }
  22. public HashCode(int factor) {
  23. this.factor = factor;
  24. }
  25. public int eval(int s) {
  26. s = s % 3;
  27. if(s == 2)
  28. return Integer.valueOf(jedis.get(String.valueOf(s)));
  29. else
  30. return 0;
  31. }
  32. }
  33. ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
  34. BatchTableEnvironment tableEnv = TableEnvironment.getTableEnvironment(env);
  35. // set job parameter
  36. Map<String,String> hashmap = new HashMap<>();
  37. hashmap.put("redis.host","localhost");
  38. hashmap.put("redis.port","6379");
  39. ParameterTool parameter = ParameterTool.fromMap(hashmap);
  40. exeEnv.getConfig().setGlobalJobParameters(parameter);
  41. // register the function
  42. tableEnv.registerFunction("hashCode", new HashCode());
  43. // use the function in Java Table API
  44. myTable.select("string, string.hashCode(), hashCode(string)");
  45. // use the function in SQL
  46. tableEnv.sqlQuery("SELECT string, HASHCODE(string) FROM MyTable");

作者:林夕_Yume
链接:https://www.jianshu.com/p/5dc2cab91c78

Flink UDF的更多相关文章

  1. flink 有状态udf 引起血案一

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/rlnLo2pNEfx9c/article/details/83422587 场景 近期在做一个画像的 ...

  2. 从"UDF不应有状态" 切入来剖析Flink SQL代码生成

    从"UDF不应有状态" 切入来剖析Flink SQL代码生成 目录 从"UDF不应有状态" 切入来剖析Flink SQL代码生成 0x00 摘要 0x01 概述 ...

  3. [源码分析]从"UDF不应有状态" 切入来剖析Flink SQL代码生成 (修订版)

    [源码分析]从"UDF不应有状态" 切入来剖析Flink SQL代码生成 (修订版) 目录 [源码分析]从"UDF不应有状态" 切入来剖析Flink SQL代码 ...

  4. Flink - state

      public class StreamTaskState implements Serializable, Closeable { private static final long serial ...

  5. Flink - Juggling with Bits and Bytes

    http://www.36dsj.com/archives/33650 http://flink.apache.org/news/2015/05/11/Juggling-with-Bits-and-B ...

  6. Flink资料(8) -- Flink代码贡献的指导及准则

    本文翻译自Contributing Code ----------------------------------------- Apache Flink是由自愿的代码贡献者维护.优化及扩展的.Apa ...

  7. Flink 核心技术浅析(整理版)

    1. Flink简介 Apache Flink是一个面向分布式数据流处理和批量数据处理的开源计算平台,它能够基于同一个Flink流执行引擎(streaming dataflow engine),提供支 ...

  8. Apache Flink系列(1)-概述

    一.设计思想及介绍 基本思想:“一切数据都是流,批是流的特例” 1.Micro Batching 模式 在Micro-Batching模式的架构实现上就有一个自然流数据流入系统进行攒批的过程,这在一定 ...

  9. 追源索骥:透过源码看懂Flink核心框架的执行流程

    li,ol.inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-bottom:20px}dt, ...

随机推荐

  1. Swift类和结构

    类和结构有非常多的共同点: 定义属性存储数据 定义方法执行功能处理 定义下标,通过下标訪问他们的值 初始化他们的状态 通过扩展(Extension)扩展其功能 遵守协议(Protocol).协议提供一 ...

  2. JDK源代码学习系列03----StringBuffer+StringBuilder

                         JDK源代码学习系列03----StringBuffer+StringBuilder 因为前面学习了StringBuffer和StringBuilder的父类 ...

  3. JBoss 7/WildFly Domain 模式怎样配置 Server 启动的 JVM 參数

    本文演示JBoss 7/WildFly Domain 模式怎样配置 Server 启动的 JVM 參数: 例如以下编辑Domain 模式配置文件 domain/configuration/domain ...

  4. cocos2d-x 源代码分析 总文件夹

    这篇博客用来整理与cocos2d-x相关的工作,仅仅要有新的分析.扩展或者改动,都会更改此文章. 祝大家愉快~ 1.源代码分析 1.CCScrollView源代码分析 http://blog.csdn ...

  5. Linux性能测试 netstat命令

    功能说明:Netstat用于显示与IP.TCP.UDP和ICMP协议相关的统计数据,一般用于检验本机各端口的网络连接情况.语 法:netstat [-acCeFghilMnNoprstuvVwx][- ...

  6. 细数Windows 的那些小技巧!

    以下整理自知乎 Windows 有哪些你相见恨晚的技巧?和Quora(英文版) What are some secret tricks you should know about Windows? 等 ...

  7. aspnetboilerplate && .net core 使用原生sql

    利用aspnetboilerplate提供的工具类IDbContextProvider private readonly IDbContextProvider<XXXDbContext> ...

  8. redis 从0 到 1 键值相关命令 服务器相关命令

    keys * 获取所有的key   忽略其数据类型 数据为空   返回(empty list or set) keys a* .*b 获取以a开头 或者 以b结尾的key 返回(empty list ...

  9. jquery动态创建小广告

    <!DOCTYPE html><html><head><meta http-equiv="Content-Type" content=&q ...

  10. telerik ChartGrid浅谈

    在最近接触的项目中,有很多都是以Chart图表的方式呈现出来的,关于telerik Chart的使用,有几个小点跟大家分享一下. 1:本例子使用的Chart的命名空间为 xmlns:telerik=h ...