Flink 实战之 Real-Time DateHistogram
- Flink 实战之 Real-Time DateHistogram
- Flink 实战之从 Kafka 到 ES
DateHistogram 用于根据日期或时间数据进行分桶聚合统计。它允许你将时间序列数据按照指定的时间间隔进行分组,从而生成统计信息,例如每小时、每天、每周或每月的数据分布情况。
Elasticsearch 就支持 DateHistogram 聚合,在关系型数据库中,可以使用 GROUP BY 配合日期函数来实现时间分桶。但是当数据基数特别大时,或者时间分桶较多时,这个聚合速度就非常慢了。如果前端想呈现一个时间分桶的 Panel,这个后端接口的响应速度将非常感人。
我决定用 Flink 做一个实时的 DateHistogram。
实验设计
场景就设定为从 Kafka 消费数据,由 Flink 做实时的时间分桶聚合,将聚合结果写入到 MySQL。
源端-数据准备
Kafka 中的数据格式如下,为简化程序,测试数据做了尽可能的精简:
testdata.json
{
"gid" : "001254500828905",
"timestamp" : 1620981709790
}
KafkaDataProducer 源端数据生成程序,要模拟数据乱序到达的情况:
package org.example.test.kafka;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
import java.util.Random;
import java.util.Scanner;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import com.alibaba.fastjson.JSONObject;
public class KafkaDataProducer {
private static final String TEST_DATA = "testdata.json";
private static final String GID = "gid";
private static final String TIMESTAMP = "timestamp";
// 定义日志颜色
public static final String reset = "\u001B[0m";
public static final String red = "\u001B[31m";
public static final String green = "\u001B[32m";
public static void main(String[] args) throws InterruptedException {
Properties props = new Properties();
String topic = "trace-2024";
props.put("bootstrap.servers", "127.0.0.1:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
Producer<String, String> producer = new KafkaProducer<String, String>(props);
InputStream inputStream = KafkaDataProducer.class.getClassLoader().getResourceAsStream(TEST_DATA);
Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8.name());
String content = scanner.useDelimiter("\\A").next();
scanner.close();
JSONObject jsonContent = JSONObject.parseObject(content);
int totalNum = 2000;
Random r = new Random();
for (int i = 0; i < totalNum; i++) {
// 对时间进行随机扰动,模拟数据乱序到达
long current = System.currentTimeMillis() - r.nextInt(60) * 1000;
jsonContent.put(TIMESTAMP, current);
producer.send(new ProducerRecord<String, String>(topic, jsonContent.toString()));
// wait some time
Thread.sleep(2 * r.nextInt(10));
System.out.print("\r" + "Send " + green + (i + 1) + "/" + totalNum + reset + " records to Kafka");
}
Thread.sleep(2000);
producer.close();
System.out.println("发送记录总数: " + totalNum);
}
}
目标端-表结构设计
MySQL 的表结构设计:
CREATE TABLE `flink`.`datehistogram` (
`bucket` varchar(255) PRIMARY KEY,
`count` bigint
);
bucket 列用于存储时间分桶,形如 [09:50:55 - 09:51:00],count 列用于存储对应的聚合值。
实现
maven 依赖:
<dependencies>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-runtime-web</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-jdbc</artifactId>
<version>3.1.2-1.17</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-statebackend-rocksdb</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
</dependencies>
BucketCount 类用于转换 Kafka 中的数据为时间分桶格式,并便于聚合:
package org.example.flink.data;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class BucketCount {
private long timestamp;
private String bucket;
private long count;
public BucketCount(long timestamp) {
this.timestamp = timestamp;
this.bucket = formatTimeInterval(timestamp);
this.count = 1;
}
public BucketCount(String bucket, long count) {
this.bucket = bucket;
this.count = count;
}
/**
* 将时间戳格式化为时间区间格式
*
* @param time
* @return 例如 [11:28:00 — 11:28:05]
*/
private String formatTimeInterval(long time) {
// 定义输出的日期时间格式
DateTimeFormatter outputFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
// 将时间戳转换为 LocalDateTime 对象
LocalDateTime dateTime = Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault()).toLocalDateTime();
// 提取秒数并计算区间开始时间
int seconds = dateTime.getSecond();
int intervalStartSeconds = (seconds / 5) * 5;
// 创建区间开始和结束时间的 LocalDateTime 对象
LocalDateTime intervalStartTime = dateTime.withSecond(intervalStartSeconds);
LocalDateTime intervalEndTime = intervalStartTime.plusSeconds(5);
// 格式化区间开始和结束时间为字符串
String startTimeString = intervalStartTime.format(outputFormatter);
String endTimeString = intervalEndTime.format(outputFormatter);
// 返回格式化后的时间区间字符串
return startTimeString + "-" + endTimeString;
}
// 省略Getter, Setter
}
RealTimeDateHistogram 类完成流计算:
package org.example.flink;
import java.time.Duration;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.connector.jdbc.JdbcConnectionOptions;
import org.apache.flink.connector.jdbc.JdbcExecutionOptions;
import org.apache.flink.connector.jdbc.JdbcSink;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer;
import org.apache.flink.contrib.streaming.state.EmbeddedRocksDBStateBackend;
import org.apache.flink.streaming.api.datastream.DataStreamSink;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.triggers.ProcessingTimeTrigger;
import org.example.flink.data.BucketCount;
import com.alibaba.fastjson.JSONObject;
public class RealTimeDateHistogram {
public static void main(String[] args) throws Exception {
// 1. prepare
Configuration configuration = new Configuration();
configuration.setString("rest.port", "9091");
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(configuration);
env.enableCheckpointing(2 * 60 * 1000);
// 使用rocksDB作为状态后端
env.setStateBackend(new EmbeddedRocksDBStateBackend());
// 2. Kafka Source
KafkaSource<String> source = KafkaSource.<String>builder()
.setBootstrapServers("127.0.0.1:9092")
.setTopics("trace-2024")
.setGroupId("group-01")
.setStartingOffsets(OffsetsInitializer.latest())
.setProperty("commit.offsets.on.checkpoint", "true")
.setValueOnlyDeserializer(new SimpleStringSchema())
.build();
DataStreamSource<String> sourceStream = env.fromSource(source, WatermarkStrategy.noWatermarks(),
"Kafka Source");
sourceStream.setParallelism(2); // 设置source算子的并行度为2
// 3. 转换为易于统计的BucketCount对象结构{ bucket: 00:00, count: 200 }
SingleOutputStreamOperator<BucketCount> mapStream = sourceStream
.map(new MapFunction<String, BucketCount>() {
@Override
public BucketCount map(String value) throws Exception {
JSONObject jsonObject = JSONObject.parseObject(value);
long timestamp = jsonObject.getLongValue("timestamp");
return new BucketCount(timestamp);
}
});
mapStream.name("Map to BucketCount");
mapStream.setParallelism(2); // 设置map算子的并行度为2
// 4. 设置eventTime字段作为watermark,要考虑数据乱序到达的情况
SingleOutputStreamOperator<BucketCount> mapStreamWithWatermark = mapStream
.assignTimestampsAndWatermarks(
WatermarkStrategy.<BucketCount>forBoundedOutOfOrderness(Duration.ofSeconds(60))
.withIdleness(Duration.ofSeconds(60))
.withTimestampAssigner(new SerializableTimestampAssigner<BucketCount>() {
@Override
public long extractTimestamp(BucketCount bucketCount, long recordTimestamp) {
// 提取eventTime字段作为watermark
return bucketCount.getTimestamp();
}
}));
mapStreamWithWatermark.name("Assign EventTime as Watermark");
// 5. 滚动时间窗口聚合
SingleOutputStreamOperator<BucketCount> windowReducedStream = mapStreamWithWatermark
.windowAll(TumblingEventTimeWindows.of(Time.seconds(5L))) // 滚动时间窗口
.trigger(ProcessingTimeTrigger.create()) // ProcessingTime触发器
.allowedLateness(Time.seconds(120)) // 数据延迟容忍度, 允许数据延迟乱序到达
.reduce(new ReduceFunction<BucketCount>() {
@Override
public BucketCount reduce(BucketCount bucket1, BucketCount bucket2) throws Exception {
// 将两个bucket合并,count相加
return new BucketCount(bucket1.getBucket(), bucket1.getCount() + bucket2.getCount());
}
});
windowReducedStream.name("Window Reduce");
windowReducedStream.setParallelism(1); // reduce算子的并行度只能是1
// 6. 将结果写入到数据库
DataStreamSink<BucketCount> sinkStream = windowReducedStream.addSink(
JdbcSink.sink("insert into flink.datehistogram(bucket, count) values (?, ?) "
+ "on duplicate key update count = VALUES(count);",
(statement, bucketCount) -> {
statement.setString(1, bucketCount.getBucket());
statement.setLong(2, bucketCount.getCount());
},
JdbcExecutionOptions.builder()
.withBatchSize(1000)
.withBatchIntervalMs(200)
.withMaxRetries(5)
.build(),
new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
.withUrl("jdbc:mysql://127.0.0.1:3306/flink")
.withUsername("username")
.withPassword("password")
.build())
);
sinkStream.name("Sink DB");
sinkStream.setParallelism(1); // sink算子的并行度只能是1
// 执行
env.execute("Real-Time DateHistogram");
}
}
几个关键点
window
Flink 的 window 将数据源沿着时间边界,切分成有界的数据块,然后对各个数据块进行处理。下图表示了三种窗口类型:
固定窗口(又名滚动窗口)
固定窗口在时间维度上,按照固定长度将无界数据流切片,是一种对齐窗口。窗口紧密排布,首尾无缝衔接,均匀地对数据流进行切分。滑动窗口
滑动时间窗口是固定时间窗口的推广,由窗口大小和窗口间隔两个参数共同决定。当窗口间隔小于窗口大小时,窗口之间会出现重叠;当窗口间隔等于窗口大小时,滑动窗口蜕化为固定窗口;当窗口间隔大于窗口大小时,得到的是一个采样窗口。与固定窗口一样,滑动窗口也是一种对齐窗口。会话窗口
会话窗口是典型的非对齐窗口。会话由一系列连续发生的事件组成,当事件发生的间隔超过某个超时时间时,意味着一个会话的结束。会话很有趣,例如,我们可以通过将一系列时间相关的事件组合在一起来分析用户的行为。会话的长度不能先验地定义,因为会话长度在不同的数据集之间永远不会相同。
EventTime
数据处理系统中,通常有两个时间域:
- 事件时间:事件发生的时间,即业务时间。
- 处理时间:系统发现事件,开始对事件进行处理的时间。
根据事件时间划分窗口 的方式在事件本身的发生时间备受关注时显得格外重要。下图所示为将无界数据根据事件时间切分成 1 小时固定时间窗口:
要特别注意箭头中所示的两个事件,两个事件根据处理时间所在的窗口,跟事件时间发生的窗口不是同一个。如果基于处理时间划分窗口的话,结果就是错的。只有基于事件时间进行计算,才能保证数据的正确性。
当然,天下没有免费的午餐。事件时间窗口功能很强大,但由于迟到数据的原因,窗口的存在时间比窗口本身的大小要长很多,导致的两个明显的问题是:
- 缓存:事件时间窗口需要存储更长时间内的数据。
- 完整性:基于事件时间的窗口,我们也不能判断什么时候窗口的数据都到齐了。Flink 通过 watermark,能够推断一个相对精确的窗口结束时间。但是这种方式并不能得到完全正确的结果。因此,Flink 还支持让用户能定义何时输出窗口结果,并且定义当迟到数据到来时,如何更新之前窗口计算的结果。
reduce
Reduce 算子基于 ReduceFunction 对集合中的元素进行滚动聚合,并向下游算子输出每次滚动聚合后的结果。常用的聚合方法如 average, sum, min, max, count 都可以使用 reduce 实现。
效果预览
从效果图中可以看出,Sum Panel 中的 stat value(为 DateHistogram 中每个 Bucket 对应值的加和)和 Kafka 端的数据跟进的非常紧,代表 Flink 的处理延迟非常低。向 Kafka 中总计压入的数据量和 Flink 输出的数据总数一致,代表数据的统计结果是准确的。此外,最近一段时间的柱状图都在实时变化,代表 Flink 对迟到的数据按照 EventTime 进行了准确处理,把数据放到了准确的 date bucket 中。
Flink 实战之 Real-Time DateHistogram的更多相关文章
- flink学习笔记-flink实战
说明:本文为<Flink大数据项目实战>学习笔记,想通过视频系统学习Flink这个最火爆的大数据计算框架的同学,推荐学习课程: Flink大数据项目实战:http://t.cn/EJtKh ...
- Flink实战| Flink+Redis实时防刷接口作弊
随着人口红利的慢慢削减,互联网产品的厮杀愈加激烈,大家开始看好下沉市场的潜力,拼多多,趣头条等厂商通过拉新奖励,购物优惠等政策率先抢占用户,壮大起来.其他各厂商也紧随其后,纷纷推出自己产品的极速版,如 ...
- 《大数据实时计算引擎 Flink 实战与性能优化》新专栏
基于 Flink 1.9 讲解的专栏,涉及入门.概念.原理.实战.性能调优.系统案例的讲解. 专栏介绍 扫码下面专栏二维码可以订阅该专栏 首发地址:http://www.54tianzhisheng. ...
- Flink 实战:如何解决生产环境中的技术难题?
大数据作为未来技术的基石已成为国家基础性战略资源,挖掘数据无穷潜力,将算力推至极致是整个社会面临的挑战与难题. Apache Flink 作为业界公认为最好的流计算引擎,不仅仅局限于做流处理,而是一套 ...
- Flink实战(1) - Apache Flink安装和示例程序的执行
在Windows上安装 从官方网站下载需要的二进制包 比如我下载的是flink-1.2.0-bin-hadoop2-scala_2.10.tgz,解压后进入bin目录 可以执行bat文件,也可以使用c ...
- Flink实战(六) - Table API & SQL编程
1 意义 1.1 分层的 APIs & 抽象层次 Flink提供三层API. 每个API在简洁性和表达性之间提供不同的权衡,并针对不同的用例. 而且Flink提供不同级别的抽象来开发流/批处理 ...
- Flink实战(七) - Time & Windows编程
0 相关源码 掌握Flink中三种常用的Time处理方式,掌握Flink中滚动窗口以及滑动窗口的使用,了解Flink中的watermark. Flink 在流处理工程中支持不同的时间概念. 1 处理时 ...
- Flink实战(八) - Streaming Connectors 编程
1 概览 1.1 预定义的源和接收器 Flink内置了一些基本数据源和接收器,并且始终可用.该预定义的数据源包括文件,目录和插socket,并从集合和迭代器摄取数据.该预定义的数据接收器支持写入文件和 ...
- Flink实战(102):配置(一)管理配置
来源:http://www.54tianzhisheng.cn/2019/03/28/flink-additional-data/ 前言 如果你了解 Apache Flink 的话,那么你应该熟悉该如 ...
- Flink实战学习资料
这份资料我已经看了一些,感觉挺不错的,在这里分享一下,有需要的可以购买学习.
随机推荐
- C#/.NET/.NET Core技术前沿周刊 | 第 2 期(2024年8.19-8.25)
前言 C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录.追踪C#/.NET/.NET Core领域.生态的每周最新.最实用.最有价值的技术文章.社区动态.优质项目和学习资源等. ...
- Jenkins 运行pipeline 报错:A Jenkins administrator will need to approve this script before it can be us
之前没有注意过这个问题,是因为之前运行pipeline时,默认勾选了"使用 Groovy 沙盒" 这次不小心取消了勾选导致,重新加上勾选即可
- 立体视觉 StereoVision
双目相机 原理 [深度相机系列三]深度相机原理揭秘--双目立体视觉 StereoVision--立体视觉(1) StereoVision--立体视觉(2) StereoVision--立体视觉(3) ...
- .NET 压缩/解压文件
本文为大家介绍下.NET解压/压缩zip文件.虽然解压缩不是啥核心技术,但压缩性能以及进度处理还是需要关注下,针对使用较多的zip开源组件验证,给大家提供技术选型 目前了解到的常用技术方案有Syste ...
- C# Dynamic 转换成 Dictionary,Dynamic 转换成 DataTable
部分软件开发的时候用到了 dynamic 类型,这个类型的数据不需要做其他处理的时候非常好用,但是需要对其中的数据调整的时候就不是那么好用了,这里提供两个常见的转换方式 Dynamic To Dict ...
- VS Code – Keyboard Shortcuts
前言 记入一些自己常用到的 Keyboard Shortcuts 和 Extensions. Keyboard Shortcuts undo redo 鼠标坐标:shift + left/right ...
- 【QT性能优化】QT性能优化之QT性能优化实战 QML优化 QT高性能 QT6系列视频课程 QT6 性能优化实战 QT高性能 QT原理源码 QML优化 GUI绘图原理源码
QT性能优化实战视频课程 QT6 Widgets高性能应用编程 1.课前考试 2.字符串优化(上) 3.字符串优化(下) 4.绘图优化(上) 5.绘图优化(下) 6.QT界面优化(上) 7.QT界面 ...
- Vue3——SVG 图标配置
1. SVG 图标配置 安装 SVG 依赖插件 vite-plugin-svg-icons npm i vite-plugin-svg-icons -D npm install fast-glob - ...
- WaterCloud:一套基于.NET 8.0 + LayUI的快速开发框架,完全开源免费!
前言 今天大姚给大家分享一套基于.NET 8.0 + LayUI的快速开发框架,项目完全开源.免费(MIT License)且开箱即用:WaterCloud. 可完全实现二次开发让开发更多关注业务逻辑 ...
- 了解final关键字在Java并发编程领域的作用吗?
在Java并发编程领域,final关键字扮演着一个至关重要的角色.虽然很多同学熟悉final用于修饰变量.方法和类的基本用法,但其在并发环境中的应用和原理却常常被忽视.final关键字不仅仅是一个简单 ...