Flink案例实战
一、案例业务背景
在传统业务进行报表类统计,经常需要对数据进行分类汇总之类业务的开发,在过去我们基于关系型数据库进行相应的功能实现,这样有如下一些缺点:
- 当需要分析的数据集很大时,严重增加查询的时效,更有严重的直接让数据库崩溃。
- 分析的结果通常是具有特定业务意义的,具有对历史数据所分析的结果不变性的特性,也就是说其结果本质只需要进行一次计算就好了,可是在传统实现方案中很难做到。
- 不具备实时性的特性。
- 其实所有的数据是具有事件的特性,在数据产生的源头我们就可以对其进行相应的分析,而传统失去数据具有事件的特性。
二、案例业务介绍
在这里我们将分析一批设备的实时电压电流的某一特定区间的电压与电流的最大值、最小值、平均的值。在这里,我们所分析的时间窗口设定5分钟。
三、环境准备
- Kafka-2.0.0
- MySQL-5.7.16
- Flink-1.10.1
- OS:MacOS
四、核心案例代码
A. 基于Table API & SQL 混合的方式
A.1. 案例代码
package org.example;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.io.jdbc.JDBCAppendTableSink;
import org.apache.flink.api.java.io.jdbc.JDBCAppendTableSinkBuilder;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.ProcessFunction;
import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.java.StreamTableEnvironment;
import org.apache.flink.table.descriptors.Rowtime;
import org.apache.flink.table.descriptors.Schema;
import org.apache.flink.types.Row;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;
/**
* Hello world!
*
* @author Bond(China)
*/
public class AppAnalysis {
public static void main(String[] args) {
// create stream environment
StreamExecutionEnvironment env = StreamEnvironmentBuilder.getExecutionEnvironment();
StreamTableEnvironment tableEnv = StreamEnvironmentBuilder.getStreamTableEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
// define data schema for source
Schema sourceSchema = new Schema();
sourceSchema.field("deviceId", DataTypes.STRING());
sourceSchema.field("voltage", DataTypes.DOUBLE());
sourceSchema.field("current", DataTypes.DOUBLE());
sourceSchema.field("state", DataTypes.DOUBLE());
sourceSchema.field("rowtime", DataTypes.TIMESTAMP(3))
.rowtime(new Rowtime().timestampsFromField("timestamp").watermarksPeriodicBounded(60));
SourceBuilder.newInstance()
.schema(sourceSchema)
.kafka(KafkaConfigBuilder.buildDefault())
.buildKafka("MeasureInfo");
// define operator sql
Table statisticTable = tableEnv.sqlQuery("SELECT deviceId, TUMBLE_START(`rowtime`, INTERVAL '5' MINUTE) as window_start, TUMBLE_END(`rowtime`, INTERVAL '5' MINUTE) as window_end, MAX(`current`) as maxCurrent, MIN(`current`) as minVoltage, AVG(`current`) as avgVoltage FROM MeasureInfo GROUP BY TUMBLE(`rowtime`, INTERVAL '5' MINUTE), deviceId");
DataStream<Tuple2<Boolean, Row>> statisticStream = tableEnv.toRetractStream(statisticTable, Types.ROW(Types.STRING, Types.SQL_TIMESTAMP, Types.SQL_TIMESTAMP, Types.DOUBLE, Types.DOUBLE, Types.DOUBLE));
// define sink to output
JDBCAppendTableSink statisticSink = new JDBCAppendTableSinkBuilder()
.setDBUrl("jdbc:mysql://localhost:3306/test?useSSL=false")
.setDrivername("com.mysql.jdbc.Driver")
.setUsername("root")
.setPassword("root")
.setBatchSize(1)
.setQuery("REPLACE INTO `test`.`statistic`(`deviceId`,`window_start`,`window_end`, `maxCurrent`,`minCurrent`,`avgCurrent`) VALUES(?,?,?,?,?,?)")
.setParameterTypes(new TypeInformation[]{Types.STRING, Types.SQL_TIMESTAMP, Types.SQL_TIMESTAMP, Types.DOUBLE, Types.DOUBLE, Types.DOUBLE})
.build();
// transfer Tuple data to row that is for convenient to save to mysql
OutputTag<Row> dataTag = new OutputTag<Row>("data") {
};
DataStream<Row> rowDataStream = statisticStream.process(new ProcessFunction<Tuple2<Boolean, Row>, Object>() {
@Override
public void processElement(Tuple2<Boolean, Row> data, Context context, Collector<Object> collector) throws Exception {
context.output(dataTag, data.f1);
}
}).getSideOutput(dataTag);
// output to mysql
rowDataStream.print();
statisticSink.emitDataStream(rowDataStream);
// execute job
try {
env.execute("iot-main-job");
} catch (Exception e) {
e.printStackTrace();
}
}
}
A.2. 代码分析
上面的代码从上到下主要是由以下几个部分所组成:
- 获取流式/批处理的运行环境
- 定义分析的数据来源(Source)
- 定义分析数据的具体算法(Operator)
- 定义分析结果的存储位置(Sink)
- 触发Job的执行
几乎所有的FLink程序都是由上面的部分所组成,这是组成一个Flink分析程序的基本组成。
A.3. 小结
- 使用这类混合的方式适合具有专业的程序员,并不适应数据分析人员
- 具有很强的灵活性,但每一次业务的修改都将触发其线上环境的程序包部署,运维成本高
- 代码比较复杂,需要熟悉Flink的各类API才能实现一个相当完美的Flink程序
B. 使用纯SQL的方式
B.1 案例代码
B.1.1 SQL代码<job.sql>
CREATE TABLE MeasureInfo (
`deviceId` STRING,
`voltage` DOUBLE,
`current` DOUBLE,
`state` INT,
`timestamp` TIMESTAMP(3),
WATERMARK FOR `timestamp` AS `timestamp` - INTERVAL '5' SECOND
) WITH (
'connector.type'='kafka',
'connector.version'='universal',
'connector.topic'='Bond-Device-Metric5',
'connector.startup-mode'='earliest-offset',
'format.type'='json',
'format.derive-schema'='true',
'update-mode'='append',
'connector.properties.0.key'='bootstrap.servers',
'connector.properties.0.value'='127.0.0.1:9092',
'connector.properties.1.key'='zookeeper.connect',
'connector.properties.1.value'='127.0.0.1:2181'
);
CREATE TABLE ResultInfo (
`deviceId` STRING,
`window_start` TIMESTAMP(3),
`window_end` TIMESTAMP(3),
`maxCurrent` DOUBLE,
`minCurrent` DOUBLE,
`avgCurrent` DOUBLE
) WITH (
'connector.type'='jdbc',
'connector.driver'='com.mysql.jdbc.Driver',
'connector.url'='jdbc:mysql://localhost:3306/test?useSSL=false',
'connector.username'='root',
'connector.password'='root',
'connector.table'='statistic',
'connector.write.flush.max-rows'='5',
'connector.write.flush.interval'='5'
);
INSERT INTO ResultInfo
SELECT
deviceId,
TUMBLE_START(`timestamp`, INTERVAL '5' MINUTE) as window_start,
TUMBLE_END(`timestamp`, INTERVAL '5' MINUTE) as window_end,
MAX(`current`) as maxCurrent,
MIN(`current`) as minVoltage,
AVG(`current`) as avgVoltage
FROM MeasureInfo
GROUP BY TUMBLE(`timestamp`, INTERVAL '5' MINUTE), deviceId;
B.1.1Java代码
package org.example;
import org.apache.commons.lang3.StringUtils;
import org.apache.flink.api.java.io.jdbc.JDBCTableSourceSinkFactory;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.java.StreamTableEnvironment;
import java.io.File;
/**
* Hello world!
*
* @author Bond(China)
*/
public class AppAnalysisPureSQL {
public static void main(String[] args) {
// create stream environment
StreamExecutionEnvironment env = StreamEnvironmentBuilder.getExecutionEnvironment();
StreamTableEnvironment tableEnv = StreamEnvironmentBuilder.getStreamTableEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
// take sql file & parse file
String path = AppAnalysisPureSQL.class.getClassLoader().getResource("").getPath();
String jobSql = FileUtils.read(new File(path + "/job.sql"));
// execute sql use stream env.
String[] sqls = jobSql.split(";");
for (String sql : sqls) {
if(StringUtils.isBlank(sql)){
continue;
}
tableEnv.sqlUpdate(sql);
}
// execute job
try {
env.execute("iot-main-job");
} catch (Exception e) {
e.printStackTrace();
}
}
}
B.2. 代码分析
上面的代码从上到下主要是由以下几个部分所组成:
- 获取流式/批处理的运行环境
- 获取Job的SQL文件中的内容
- 逐条执行每条SQL
- 触发Job的执行
上面是代码是将:定义数据源(Source)、定义数据分析(Operator)、定义数据输出(Sink)这三部分的工作转换到SQL文件,Java代码主要关注的是获取运行环境和触发Job的执行。
因此上面代码并不违背Flink程序组成的最基本原则。
B.3. 小结
- 使用这纯SQL方式适合于数据分析人员。
- 运维成本低,因为只需修改SQL就可以完成业务的修改。
- 代码比较简洁,只需要使用人员熟悉SQL就基本可以完成大部分的数据分析工作。
五、广泛应用的思考
- 建议使用纯SQL的方式
- 封装具有通用性Job编排服务,开放SQL编辑入口
- 运行 CI/CD 工具加速开发与运维过程
- 在上面纯SQL的方式中为起点,将SQL与程序分开,增加文件事件驱动+程序内部多线程运行的方式相结合,可适当增强业务变更与运维的灵活性。
- 在Flink程序+Spring Boot,让程序支持灵活接口的变更Job的任务信息,此种方式建议突离Flink Server的运行。相当于这样一个就是一个可独立运行的微服务的应用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sbORfqYD-1605887012168)(http://note.youdao.com/yws/res/7051/095BCD7F42894ED6BBF16E95338E2CA6)]
六、关键知识
- Flink基础概念、架构
- Table API & Stream API
- WaterMark & Window
- Flink SQL
七、其他深度概念
- CEP
- 有状态流处理