flink-基于 Table API 实现实时报表

Apache Flink提供了一个表API作为批处理和流处理的统一的关系API,也就是说,查询在无界、实时流或有界的批处理数据集上以相同的语义执行,并产生相同的结果。 Flink中的Table API通常用于简化数据分析、数据管道和ETL应用程序的定义。 

在这个例子中,将学习如何构建实时仪表板,以按帐户跟踪金融交易。 管道将从Kafka读取数据,并通过Grafana将结果写入MySQL可视化。  

可能遇到的问题:

如果在Windows上运行docker和你的数据生成器容器启动失败,那么请确保你正在使用正确的shell。 例如,table-walkthrough_data-generator_1容器的docker-entrypoint.sh需要bash。 如果不可用,它将抛出standard_init_linux错误。 执行:211:exec用户进程导致“没有这样的文件或目录”。 一个解决办法是在docker-entrypoint.sh的第一行将shell切换到sh。

所需的配置文件可以在flink-playground存储库中找到。 下载完成后,在IDE中打开项目flink-playground/table-walkthrough,并导航到文件spenreport。   

 1 EnvironmentSettings settings = EnvironmentSettings.inStreamingMode();
 2 TableEnvironment tEnv = TableEnvironment.create(settings);
 3 
 4 tEnv.executeSql("CREATE TABLE transactions (\n" +
 5     "    account_id  BIGINT,\n" +
 6     "    amount      BIGINT,\n" +
 7     "    transaction_time TIMESTAMP(3),\n" +
 8     "    WATERMARK FOR transaction_time AS transaction_time - INTERVAL '5' SECOND\n" +
 9     ") WITH (\n" +
10     "    'connector' = 'kafka',\n" +
11     "    'topic'     = 'transactions',\n" +
12     "    'properties.bootstrap.servers' = 'kafka:9092',\n" +
13     "    'format'    = 'csv'\n" +
14     ")");
15 
16 tEnv.executeSql("CREATE TABLE spend_report (\n" +
17     "    account_id BIGINT,\n" +
18     "    log_ts     TIMESTAMP(3),\n" +
19     "    amount     BIGINT\n," +
20     "    PRIMARY KEY (account_id, log_ts) NOT ENFORCED" +
21     ") WITH (\n" +
22     "   'connector'  = 'jdbc',\n" +
23     "   'url'        = 'jdbc:mysql://mysql:3306/sql-demo',\n" +
24     "   'table-name' = 'spend_report',\n" +
25     "   'driver'     = 'com.mysql.jdbc.Driver',\n" +
26     "   'username'   = 'sql-demo',\n" +
27     "   'password'   = 'demo-sql'\n" +
28     ")");
29 
30 Table transactions = tEnv.from("transactions");
31 report(transactions).executeInsert("spend_report");

代码解析:

执行环境 

前两行设置了TableEnvironment。 表环境是您为作业设置属性、指定是编写批处理应用程序还是流应用程序以及创建源的方式。 此演练将创建一个使用流执行的标准表环境。  

EnvironmentSettings settings = EnvironmentSettings.inStreamingMode();
TableEnvironment tEnv = TableEnvironment.create(settings);

注册table

接下来,在当前编目中注册表,您可以使用这些表连接到外部系统,以便读写批处理和流数据。 表源提供对存储在外部系统中的数据的访问,例如数据库、键值存储、消息队列或文件系统。 表汇向外部存储系统发送表。 根据源和接收器的类型,它们支持不同的格式,如CSV、JSON、Avro或Parquet。  

tEnv.executeSql("CREATE TABLE transactions (\n" +
     "    account_id  BIGINT,\n" +
     "    amount      BIGINT,\n" +
     "    transaction_time TIMESTAMP(3),\n" +
     "    WATERMARK FOR transaction_time AS transaction_time - INTERVAL '5' SECOND\n" +
     ") WITH (\n" +
     "    'connector' = 'kafka',\n" +
     "    'topic'     = 'transactions',\n" +
     "    'properties.bootstrap.servers' = 'kafka:9092',\n" +
     "    'format'    = 'csv'\n" +
     ")");

注册两个表; 一个事务输入表,一个支出报告输出表。 事务(transactions)表允许我们读取信用卡事务,其中包含帐户ID (account_id)、时间戳(transaction_time)和美元金额(amount)。 这个表是一个包含了CSV数据的Kafka主题事务的逻辑视图。  

tEnv.executeSql("CREATE TABLE spend_report (\n" +
    "    account_id BIGINT,\n" +
    "    log_ts     TIMESTAMP(3),\n" +
    "    amount     BIGINT\n," +
    "    PRIMARY KEY (account_id, log_ts) NOT ENFORCED" +
    ") WITH (\n" +
    "    'connector'  = 'jdbc',\n" +
    "    'url'        = 'jdbc:mysql://mysql:3306/sql-demo',\n" +
    "    'table-name' = 'spend_report',\n" +
    "    'driver'     = 'com.mysql.jdbc.Driver',\n" +
    "    'username'   = 'sql-demo',\n" +
    "    'password'   = 'demo-sql'\n" +
    ")");

第二个表spend_report存储聚合的最终结果。 它的底层存储是MySql数据库中的一个表。  

查询  

配置了环境并注册了表之后,就可以开始构建第一个应用程序了。 在TableEnvironment中,可以从输入表中读取数据,然后使用executeInsert将这些结果写入输出表。 报告功能是实现业务逻辑的地方。 它目前尚未实现。  

Table transactions = tEnv.from("transactions");
report(transactions).executeInsert("spend_report");

Flink的一个独特属性是,它跨批处理和流提供一致的语义。 这意味着您可以在静态数据集上以批处理模式开发和测试应用程序,并将其作为流应用程序部署到生产中。  

 

现在有了Job设置的框架,就可以添加一些业务逻辑了。 目标是构建一个报告,显示每个帐户在一天中每个小时的总花费。 这意味着时间戳列需要从毫秒到小时的粒度进行四舍五入。  

业务处理

Flink支持用纯SQL或使用Table API开发关系应用程序。 Table API是受SQL启发的流畅DSL,可以用Python、Java或Scala编写,并支持强大的IDE集成。 就像SQL查询一样,Table程序可以根据键选择所需的字段和分组。 这些特性,以及像floor和sum这样的内置函数,都可以编写这个报告。  

public static Table report(Table transactions) {
    return transactions.select(
            $("account_id"),
            $("transaction_time").floor(TimeIntervalUnit.HOUR).as("log_ts"),
            $("amount"))
        .groupBy($("account_id"), $("log_ts"))
        .select(
            $("account_id"),
            $("log_ts"),
            $("amount").sum().as("amount"));
}

用户定义函数  

Flink包含数量有限的内置函数,有时需要使用用户定义的函数对其进行扩展。 如果没有预先定义楼层,您可以自己实现它。  

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;

import org.apache.flink.table.annotation.DataTypeHint;
import org.apache.flink.table.functions.ScalarFunction;

public class MyFloor extends ScalarFunction {

    public @DataTypeHint("TIMESTAMP(3)") LocalDateTime eval(
        @DataTypeHint("TIMESTAMP(3)") LocalDateTime timestamp) {

        return timestamp.truncatedTo(ChronoUnit.HOURS);
    }
}

 

 

然后快速地将其集成到应用程序中。  

public static Table report(Table transactions) {
    return transactions.select(
            $("account_id"),
            call(MyFloor.class, $("transaction_time")).as("log_ts"),
            $("amount"))
        .groupBy($("account_id"), $("log_ts"))
        .select(
            $("account_id"),
            $("log_ts"),
            $("amount").sum().as("amount"));
}

该查询使用事务表中的所有记录,计算报告,并以一种高效、可伸缩的方式输出结果。 使用该实现运行测试将通过。  

添加窗口函数

基于时间的数据分组是数据处理中的一种典型操作,特别是在处理无限流时。 基于时间的分组称为窗口,Flink提供了灵活的窗口语义。 最基本的窗口类型称为Tumble窗口,它有固定的大小,并且桶不重叠。  

public static Table report(Table transactions) {
    return transactions
        .window(Tumble.over(lit(1).hour()).on($("transaction_time")).as("log_ts"))
        .groupBy($("account_id"), $("log_ts"))
        .select(
            $("account_id"),
            $("log_ts").start().as("log_ts"),
            $("amount").sum().as("amount"));
}

这将您的应用程序定义为基于时间戳列使用一个小时的滚动窗口。 因此,时间戳为2019-06-01 01:23:47的行被放入2019-06-01 01:00:00窗口。  

 基于时间的聚合是惟一的,因为与其他属性不同,时间通常在连续流应用程序中向前移动。 与楼层和UDF不同,窗口函数是内在的,它允许运行时应用额外的优化。 在批处理上下文中,窗口提供了一个方便的API,通过时间戳属性将记录分组。  

 使用该实现运行测试也将通过。  

再一次,流式计算!

这就是一个功能齐全、有状态的分布式流应用程序! 查询不断地消耗来自Kafka的事务流,计算每小时的开销,并在它们准备好时立即发出结果。 因为输入是无界的,所以查询会一直运行,直到手动停止为止。 因为Job使用基于时间窗口的聚合,所以当框架知道不再有记录到达某个特定窗口时,Flink可以执行特定的优化,比如状态清理。  

 表游乐场是完全dockerized的,并且可以作为流应用程序在本地运行。 该环境包含一个Kafka主题,一个连续数据生成器,MySql和Grafana。  

从表遍历文件夹中启动docker-compose脚本。  

 

 查询mysql结果

$ docker-compose exec mysql mysql -Dsql-demo -usql-demo -pdemo-sql

mysql> use sql-demo;
Database changed

mysql> select count(*) from spend_report;
+----------+
| count(*) |
+----------+
|      110 |
+----------+

Finally, go to Grafana to see the fully visualized result!

 项目pom依赖

<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-api-java</artifactId>
<version>1.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-sql-connector-kafka_2.12</artifactId>
<version>1.12.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-jdbc_2.12</artifactId>
<version>1.12.0</version>
<scope>provided</scope>
</dependency>
部署执行注意事项
1,需将相关依赖jar包上传至flink安装目录lib文件夹下,或项目打包时将依赖包一起打包
2,如依赖jar包有上传,但还是出现classNotFound异常,可以修改flink/bin目录下的config.sh的如下变量值,新增依赖jar包目录
INTERNAL_HADOOP_CLASSPATHS="${HADOOP_CLASSPATH}:${HADOOP_CONF_DIR}:${YARN_CONF_DIR}
修改为
INTERNAL_HADOOP_CLASSPATHS="${HADOOP_CLASSPATH}:${HADOOP_CONF_DIR}:${YARN_CONF_DIR}:/opt/flink-1.12.0/lib/"
3,kafka消息格式为csv格式:5,1,2022-02-02 00:00:00

posted on 2022-01-20 15:29  yuechuan  阅读(529)  评论(0)    收藏  举报