关系型API

  • 关系型API
  • 为什么需要关系型API?
  • Calcite
  • 关系API概述
  • 程序结构
  • 获取运行时
  • 获取Table运行时
  • 表注册
  • 输入表注册
  • 输出表注册
  • TableSource和TableSink
  • TableSource
  • TableSink
  • 定义查询
  • 相互转换
  • 将DataStream或DataSet转换为表
  • 将表转换为DataStream或DataSet
  • 将表转换为DataStream
  • 将表转换为DataSet
  • 动态表概述
  • 流式关系代数
  • 动态表
  • 持续查询
  • 动态表到流的转换


关系型API

Flink AsyncFunction写入_API

为什么需要关系型API?

DataStream或DataSet需要开发任意掌握Java或Scala语言,并精通两套API。随着流处理应用的广泛推广以及Flink在该领域的快速增长,Flink社区决定推出一套同时支持流处理与批处理、并且更简单的API,以降低开发门槛,满足更多用户的使用需求,关系型API由此产生。

关系型API是 Table API (提供类似LINQ(集成查询语言)语法的API)和 SQL(支持标准SQL查询)(是Table API的更高层抽象)的统称。

具有以下优点:

  1. 它是声明式的,用户不需要提供计算的实现细节,系统决定如何计算。
  2. 关系型API需要响应的执行引擎,用于翻译SQL,因此我们可在引擎层全局优化查询,而不是在编程语言层优化程序。DataStream和DataSe应用程序很难做到这一点。
  3. SQL的普及程度远远高于编程语言,尤其是在数据分析领域。

Calcite

Flink没有重复造轮子,是根据SQL解析与优化框架Calcite构建关系型API。

Apach Calcite是为不同计算平台和数据源提供统一动态数据管理服务的高层框架,Calcite在各种数据源上构建标准的SQL语言,并提供多种查询优化方案。此外,Calcite也适合用于流处理场景。

Calcite的目标是为不同计算平台和数据源提供统一的查询引擎(一种方案适应所有需求场景,one size fits all),并以类似SQL查询语言访问不同的数据源。

Apach Calcite执行SQL查询的主要步骤如下:

  1. 将SQL解析成未经校验额抽象语法树(AST Abstract Syntax Tree),抽象语法树是和语言无关的形式,这和交叉编译的第一步类似。
  2. Validate:验证AST,主要验证SQL语句是否合法,验证后结果为RelNode树。
  3. Optimize:优化RelNode树并生成物理执行计划。
  4. Execute:将物理执行计划转换为特定平台的执行代码,如Flink的DataStream应用程序。

关系API概述

关系型API是以库的形式提供的,在Maven工程中需要加入以下依赖:

<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-table-planner_2.11</artifactId>
  <version>1.8.0</version>
</dependency>

此外,根据使用的编程语言,需要获取对关系型API的支持:

<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-table-api-java-bridge_2.11</artifactId>
  <version>1.8.0</version>
</dependency>

<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-table-api-scala-bridge_2.11</artifactId>
  <version>1.8.0</version>
</dependency>

在内部,部分表生态系统是用Scala实现的。因此,请确保为批处理和流处理应用程序添加以下依赖项:

<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-streaming-scala_2.11</artifactId>
  <version>1.8.0</version>
</dependency>

程序结构

关系型API统一了DataStream API和DataSet API,而且Table API和SQL屏蔽了批处理和流处理的细节,因此Table API和SQL的应用程序结构是一致的。基于关系型API的数据处理应用程序应包括以下6个步骤:

获取运行时

  • StreamingExecutionEnvironment:对应流处理
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
  • ExecutionEnvironment:对应批处理
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();

获取Table运行时

关系型API统一了DataStream API与DataSet API,因此TableEnvironment根据DataStream和DataSet分别定义了获取Table运行时的方法:

  • StreamTableEnvironment:对应流处理
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env)
  • BatchTableEnvironment:对应批处理
BatchTableEnvironment tableEev = BatchTableEnvironment.create(env);

作为关系API的核心对象,TableEnvironment承担以下任务:

  • 负责表的管理。目录管理Schema的元数据和命名空间。
  • SQL查询。Flink规定统一查询中的表不能定义在不同的运行时环境中。
  • UDF管理
  • 将DataStream或DataSet转换成表
  • 管理StreamTableEnvironment对象和BatchTableEnvironment对象的引用。

统一批处理API和流处理API是以增加架构复杂度为代价的,并且关系型API是构建在DataStream API和DataSet API之上的,而不是重复造轮子。

表注册

TableEnvironment维护按照名称进行注册的表目录。有两种类型的表,输入表和输出表:

  • 输入表:输入表可以在表API和SQL查询中引用,并提供输入数据。
  • 输出表:输出表可用于将表API或SQL查询的结果发送到外部系统。

为什么区分输入表和输出表:

  1. Flink引擎以pull的方式获取外部数据,但不支持数据更新到Source,这是符合数据分析哲学的。
  2. 应用程序的目标是获取关系代数的运算结果,但大多数情况下,Schema中并没有结果对应的数据结构。

因此这里的表和关系型数据库中的视图类似。

输入表注册
  1. 基于已有的表实例
// get a StreamTableEnvironment, works for BatchTableEnvironment equivalently
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

// Table is the result of a simple projection query 
Table projTable = tableEnv.scan("X").select(...);

// register the Table projTable as table "projectedX"
tableEnv.registerTable("projectedTable", projTable);
  1. 基于外部Source
// get a StreamTableEnvironment, works for BatchTableEnvironment equivalently
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

// create a TableSource
TableSource csvSource = new CsvTableSource("/path/to/file", ...);

// register the TableSource as table "CsvTable"
tableEnv.registerTableSource("CsvTable", csvSource);
  1. 基于外部catalog
// get a StreamTableEnvironment, works for BatchTableEnvironment equivalently
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

// create an external catalog
ExternalCatalog catalog = new InMemoryExternalCatalog();

// register the ExternalCatalog catalog
tableEnv.registerExternalCatalog("InMemCatalog", catalog);

4.基于DataStream或DataSet

// get StreamTableEnvironment
// registration of a DataSet in a BatchTableEnvironment is equivalent
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

DataStream<Tuple2<Long, String>> stream = ...

// register the DataStream as Table "myTable" with fields "f0", "f1"
tableEnv.registerDataStream("myTable", stream);

// register the DataStream as table "myTable2" with fields "myLong", "myString"
tableEnv.registerDataStream("myTable2", stream, "myLong, myString");
输出表注册

由TableSink注册:

// get a StreamTableEnvironment, works for BatchTableEnvironment equivalently
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

// create a TableSink
TableSink csvSink = new CsvTableSink("/path/to/file", ...);

// define the field names and types
String[] fieldNames = {"a", "b", "c"};
TypeInformation[] fieldTypes = {Types.INT, Types.STRING, Types.LONG};

// register the TableSink as table "CsvSinkTable"
tableEnv.registerTableSink("CsvSinkTable", fieldNames, fieldTypes, csvSink);

TableSource和TableSink

TableSource
tableEnvironment
  // 连接外部数据源
  .connect(...)
  // 解析数据源格式
  .withFormat(...)
  // 定义Schema
  .withSchema(...)
  // 输出表的更新方式
  .inAppendMode()
  // 注册表
  .registerTableSource("MyTable")
TableSink
tableEnvironment
  // 连接外部数据源
  .connect(...)
  // 解析数据源格式
  .withFormat(...)
  // 定义Schema
  .withSchema(...)
  // 输出表的更新方式
  .inAppendMode()
  // 注册表
  .registerTableSink("MyTable")

Kafks示例:

tableEnvironment
  // declare the external system to connect to
  .connect(
    new Kafka()
      .version("0.10")
      .topic("test-input")
      .startFromEarliest()
      .property("zookeeper.connect", "localhost:2181")
      .property("bootstrap.servers", "localhost:9092")
  )

  // declare a format for this system
  .withFormat(
    new Avro()
      .avroSchema(
        "{" +
        "  \"namespace\": \"org.myorganization\"," +
        "  \"type\": \"record\"," +
        "  \"name\": \"UserMessage\"," +
        "    \"fields\": [" +
        "      {\"name\": \"timestamp\", \"type\": \"string\"}," +
        "      {\"name\": \"user\", \"type\": \"long\"}," +
        "      {\"name\": \"message\", \"type\": [\"string\", \"null\"]}" +
        "    ]" +
        "}"
      )
  )

  // declare the schema of the table
  .withSchema(
    new Schema()
      .field("rowtime", Types.SQL_TIMESTAMP)
        .rowtime(new Rowtime()
          .timestampsFromField("timestamp")
          .watermarksPeriodicBounded(60000)
        )
      .field("user", Types.LONG)
      .field("message", Types.STRING)
  )

  // specify the update-mode for streaming tables
  .inAppendMode()

  // register as source, sink, or both and under a name
  .registerTableSource("MyUserTable");

定义查询

Table API 与 SQL查询的格式是不同的:

  • Table API
// scan registered Orders table
Table orders = tableEnv.scan("Orders");
// compute revenue for all customers from France
Table revenue = orders
  .filter("cCountry === 'FRANCE'")
  .groupBy("cID, cName")
  .select("cID, cName, revenue.sum AS revSum");
  • SQL
Table revenue = tableEnv.sqlQuery(
    "SELECT cID, cName, SUM(revenue) AS revSum " +
    "FROM Orders " +
    "WHERE cCountry = 'FRANCE' " +
    "GROUP BY cID, cName"
  );

相互转换

应用程序可将DataStream或DataSet转换成Table,以可将Table转换成DataStream或DataSet。

将DataStream或DataSet转换为表
// get StreamTableEnvironment
// registration of a DataSet in a BatchTableEnvironment is equivalent
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

DataStream<Tuple2<Long, String>> stream = ...

// Convert the DataStream into a Table with default fields "f0", "f1"
Table table1 = tableEnv.fromDataStream(stream);

// Convert the DataStream into a Table with fields "myLong", "myString"
Table table2 = tableEnv.fromDataStream(stream, "myLong, myString");
将表转换为DataStream或DataSet

表可以转换为DataStream或DataSet。通过这种方式,可以对Table API或SQL的结果运行自定义DataStream或DataSet程序。

将表转换为DataStream或DataSet时,需要指定DataStream或DataSet的数据类型(要将表的行转换成的数据类型)。通常最方便的转换类型是Row。下面的列表概述了不同选项的特性:

  • Row:字段由位置、任意数量的字段、支持null值、没有类型安全访问。
  • POJO:字段按名称映射(POJO字段必须命名为Table字段),任意数量的字段,支持null值,类型安全访问。
  • Case Class:字段按位置映射,不支持null值,类型安全访问。
  • Tuple:字段按位置映射,限制为22(Scala)或25(Java)字段,不支持null值,类型安全访问。
  • Atomic Type:Table必须具有单个字段,不支持null值,类型安全访问。
将表转换为DataStream

一个Table是流媒体查询的结果将动态更新,随着新记录到达查询的输入流,它也在发生变化。因此DataStream转换这种动态查询需要对表的更新进行编码。

将Table转换为DataStream有两种模式:

Append Mode(追加模式):只有当动态表通过INSERT修改时,才可以使用此模式,它只进行追加不会对以前发出的结果进行修改。
Retract Mode(重复模式):这种模式总是可以使用。它使用boolean定义是插入还是删除操作。

// get StreamTableEnvironment. 
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

// Table with two fields (String name, Integer age)
Table table = ...

// convert the Table into an append DataStream of Row by specifying the class
DataStream<Row> dsRow = tableEnv.toAppendStream(table, Row.class);

// convert the Table into an append DataStream of Tuple2<String, Integer> 
//   via a TypeInformation
TupleTypeInfo<Tuple2<String, Integer>> tupleType = new TupleTypeInfo<>(
  Types.STRING(),
  Types.INT());
DataStream<Tuple2<String, Integer>> dsTuple = 
  tableEnv.toAppendStream(table, tupleType);

// convert the Table into a retract DataStream of Row.
//   A retract stream of type X is a DataStream<Tuple2<Boolean, X>>. 
//   The boolean field indicates the type of the change. 
//   True is INSERT, false is DELETE.
DataStream<Tuple2<Boolean, Row>> retractStream = 
  tableEnv.toRetractStream(table, Row.class);
将表转换为DataSet
// get BatchTableEnvironment
BatchTableEnvironment tableEnv = BatchTableEnvironment.create(env);

// Table with two fields (String name, Integer age)
Table table = ...

// convert the Table into a DataSet of Row by specifying a class
DataSet<Row> dsRow = tableEnv.toDataSet(table, Row.class);

// convert the Table into a DataSet of Tuple2<String, Integer> via a TypeInformation
TupleTypeInfo<Tuple2<String, Integer>> tupleType = new TupleTypeInfo<>(
  Types.STRING(),
  Types.INT());
DataSet<Tuple2<String, Integer>> dsTuple = 
  tableEnv.toDataSet(table, tupleType);

动态表概述

流式关系代数

SQL的本质是关系代数,而关系代数是定义在有界数据集上的,因此将关系代数运算移植到DataSet上不会遇到理论上的障碍。而移植到DataStream则不易理解:D阿塔Stream处理的对象是无限延伸且在时间轴上动态变化的数据集,而流式关系代数理论并不能建立在数据库技术的基础之上,否则流处理会失去意义。

流处理和关系代数间的区别:

  • 执行机制不同:根据CAP理论,可用性是有时限要求的,因此关系型数据库必须及时响应;而流处理是要一直运行下去,没有结束点的。
  • 计算结果的形式不同:SQL查询的结果解构事先知晓,并大小有限的;流处理则一直更新计算结果,并以流的形式发送出去,其结构和大小是未知的。

动态表

之所以称为动态表(Dynamic Table),是因为相对于关系型数据库的表,动态表的内容是不可预知且持续变化的,且相对于关系型表的查询,在动态表上的查询会产生持续查询(Continuous Query),这符合Flink流式数据处理编程模型:应用程序被翻译成计算图,其逻辑结构是固定的,数据处理任务是持续 的。

在DataStream上执行SQL查询,处理过程如下:

  1. 将DataStream转换成动态表
  2. 在动态表上定义持续查询
  3. 将持续查询的实时结果转换成动态表
  4. 将使用动态表表示的查询结果转换成DataStream,这样程序可借助DataStream API进一步转换查询结果。

    下面以用户点击行为分析为例进行说明:
[
  user:  VARCHAR,   // 用户名
  cTime: TIMESTAMP, // 点击时间
  url:   VARCHAR    // 点击界面的URL
]

为了使用关系查询处理DataStream,必须将其转换为动态表。从概念上讲,DataStream的每个记录都被解释为动态表的一条数据。

用户点击行为的动态表形式如图,下图显示了单击事件流(左侧)如何转换为表(右侧)。随着插入更多的单击流记录,结果表将不断增长:

Flink AsyncFunction写入_SQL_02

持续查询

接下来,分析每个用户的累计点击量,SQL语句如下所示:

SELECT USER,COUNT(URL) AS CNT FROM CLIKS GROUP BY USER

Flink会根据记录被观察的顺序持续执行SQL查询,生成动态变化的表,即表的行数会动态变化,某行的内容也会发生改变。

Flink AsyncFunction写入_SQL_03


窗口是流处理的重要机制,动态表不会丢弃这个流式数据处理的优良功能。以一小时长度开事件时间窗口,数据保存到.cvs文件中,在每个窗口内分析用户的累计点击次数,结果如图:

Flink AsyncFunction写入_SQL_04

动态表到流的转换

动态表可以像常规数据库表一样通过插入、更新和删除不断地进行修改。它可能是一个只有一行的表(不断更新)、一个只进行插入而不进行更新和删除修改的表,或者介于两者之间的任何东西。
当将动态表转换为流或将其写入外部系统时,需要对这些更改进行编码。Flink的Table API和SQL支持三种方式来编码动态表的更改:

  • Append-only stream:只有进行插入操作时动态表才会通过发出插入的行将其转换为流。
  • Retract stream:Retract stream包含两种类型的消息,insert和delete。通过将INSERT编码为add消息,DELETE编码为retract消息,UPDATE编码为已更新(前一行)的retract消息,以及add消息编码为已更新(新)行,动态表被转换为retract流。下图显示了将动态表转换为收缩流的过程。
  • Upsert stream:upsert stream包含两种类型的消息,upsert消息和delete消息。转换为upsert stream的动态表需要惟一键(可以是复合键)。通过将INSERT和UPDATE编码为upsert消息,将DELETE编码为DELETE消息,将具有唯一键的动态表转换为流。为了正确应用消息,流消费操作符需要知道惟一的key属性。与Retract stream的主要区别在于更新是用单个消息编码的,因此更高效。下图显示了将动态表转换为upsert stream的过程。