文章目录
- Hadoop
- Flink(SQL相关后面专题补充)
- 1.
- 2. Flink SQL API State TTL 的过期机制是 onCreateAndUpdate 还是 onReadAndWrite?
- 3. watermark 到底是干啥的?应用场景?
- 4. 一个flink任务中可以既有事件时间窗口,又有处理时间窗口吗?
- 5.
- 6. Flink 提交作业的流程?以及与yarn的互动?
- 7. Operator Chains了解吗?
- 8. 10个int以数组的形式保存,保存在什么状态好?VlaueState还是 ListState?存在哪个的性能比较好?
- 9. 一个窗口,现在只取第一帧和最后一帧,如何实现?
- 10. 背压的原理?解决办法?
- 11. 遇到状态放不下的场景怎么办?
- 12. 使用flink统计订单表的GMV(商品交易总额),如果mysql中的数据出现错误,之后在mysql中做数据的修改操作,那么flink程序如何保证GMV的正确性,你们是如何解决?
- 13. 开窗函数有哪些?
- 14. 没有数据流的时候,窗口存在吗
- 15. 1小时的滚动窗口,一小时处理一次的压力比较大想让他5分钟处理一次.怎么办?
- 16. 两个流先后顺序不确定,到达的间隔也不确定,如何拼接成宽表?
- 17. 为什么使用维表?什么情况下使用?
- 18. Flink维表关联怎么做的?
- 19. 针对上面的MapState方案我想实现定期的线程更新避免无法感知到维表的数据变化要如何实现?
- 20. flink 1.17了解吗,有哪些新特性?
- 21. Flink 中 Table 和 DataStream 是如何互相转换的?
- 22. Flink 中 怎么消费kafka?如何感知消费堆积?
- 23. 详细介绍下 KeyedProcessFunction
- 24. 为什么推荐使用Application Mode集群运行模式?
- 25. 为什么使用选择flink?
- 26. Flink SQL 滚动&滑动窗口Demo
- 27. 简单介绍下Flink on k8s
- 28. Flink 内置的窗口分配器
- 29. Flink 如何处理迟到的数据?
- 30. Flink task 算子之间如何交换数据?网络传输中的内存管理?
- 31. Flink 中 SQL 是如何应用于流处理的?
- 32. 介绍下 TableEnvironment
- 33. Flink SQL 中表的概念
- 34. 在 pdd 这种发补贴券的场景下,希望可以在发的补贴券总金额超过 1w 元时,及时报警出来,来帮助控制预算,防止发的太多。
- 35. Flink SQL 的时间属性
- 36. Flink SQL 中支持的 4 种窗口的运算
- 37. Flink SQL 总共提供了 4 种函数:
- 38. Flink SQL 性能调优
- 39. Flink SQL 架构 & Blink Planner
- 40. Flink SQL 是如何转化为 JobGraph 的?经历了几个阶段?
- 41. 如果下级存储不支持事务,Flink 怎么保证 exactly-once
- 42. Flink SQL 计算用户分布,根据QQ 等级变化明细数据(time,uid,level)求出当前每个等级的用户数
- 43. 如何开启mini-batch
- 44. Flink SQL 计算 DAU:用户心跳日志(uid,time,type)。计算分 Android,iOS 的 DAU,最晚一分钟输出一次当日零点累计到当前的结果
- 45. Flink里面异步IO代码具体怎么写?
- 46. Flink 中状态 TTL 的原理机制?
- 47. Flink SQL 的 State 使用?TTL?
- 48. 实现计算小时级别,天级别的用户PV、UV(CUMULATE!!!)
- 49. Flink SQL 滚动窗口介绍
- 50. Flink SQL TVF 实现一个简单的滑动窗口拼接Demo
- 51. 简单介绍下非对齐checkpoint
- 52. Kafka+Flink的实时数仓方案有什么问题?如何优化?
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
Hadoop
1. 请说下HDFS读写流程
略
https://www.yuque.com/wangzhiwuimportbigdata/da20y0/ekwkul#Bb3na
2. Secondary NameNode了解吗?他的工作机制是怎样的?
略
3. Secondary NameNode 不能恢复NameNode的全部数据,那如何保证NameNode数据存储安全?
略
4.
5.
6.
7.
8.
Flink(SQL相关后面专题补充)
1.
2. Flink SQL API State TTL 的过期机制是 onCreateAndUpdate 还是 onReadAndWrite?
- 结论:Flink SQL API State TTL 的过期机制目前只支持 onCreateAndUpdate,DataStream API 两个都支持。
- 剖析:
- onCreateAndUpdate:是在创建State和更新State时【更新StateTTL】
- onReadAndWrite:是在访问State和写入State时【更新StateTTL】
- 实际踩坑场景:Flink SQL Deduplicate 写法,row_number partition by user_id
order by proctimeasc,此SQL最后生成的算子只会在第一条数据来的时候更新
state,后续访问不会更新stateTTL,因此state会在用户设置的stateTTL时间之后过期。
3. watermark 到底是干啥的?应用场景?
- 标识flink任务的事件时间进度,从而能够推动事件时间窗口的触发、计算
- 解决事件时间窗口的乱序问题
4. 一个flink任务中可以既有事件时间窗口,又有处理时间窗口吗?
结论:一个 Flink 任务可以同时有事件时间窗口,又有处理时间窗口。
两个角度说明:
- 我们其实没有必要把一个Flink任务和某种特定的时间语义进行绑定。对于事件时间窗口来说,我们只要给它watermark,能让watermark一直往前推进,让事件时间窗口能够持续触发计算就行。对于处理时间来说更简单,只要窗口算子按照本地时间按照固定的时间间隔进行触发就行。无论哪种时间窗口,主要满足时间窗口的触发条件就行。
- Flink的实现上来说也是支持的。Flink是使用一个叫做TimerService的组件来管理
timer的,我们可以同时注册事件时间和处理时间的timer,Flink会自行判断timer是否满足触发条件,如果是,则回调窗口处理函数进行计算。
5.
6. Flink 提交作业的流程?以及与yarn的互动?
略
7. Operator Chains了解吗?
为了更高效地分布式执行,Flink 会尽可能地将operator的subtask链接(chain)在一起形成task。每个task在一个线程中执行。
将operators链接成task是非常有效的优化:它能减少线程之间的切换,减少消息的序列化/反序列化,减少数据在缓冲区的交换,减少了延迟的同时提高整体的吞吐量。这就是Operator Chains(算子链)。
8. 10个int以数组的形式保存,保存在什么状态好?VlaueState还是 ListState?存在哪个的性能比较好?
ValueState[Array[Int]] update形式。
ListState[Int]:add形式添加。
对于操控来说ListState方便取值与更改。
按键分区状态(Keyed State)选择ValueState ListState。
算子状态(Operator State)选择ListState。
9. 一个窗口,现在只取第一帧和最后一帧,如何实现?
如果使用process全量聚合函数:
package com.herobin.flink.common;
import org.apache.flink.api.common.functions.MapFunction;
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.source.SourceFunction;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import org.apache.flink.streaming.api.windowing.time.Time;
public class FlinkDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);
DataStream<Integer> sourceStream = env.addSource(new SimpleSourceFunction());
sourceStream
.map(new MapFunction<Integer, Tuple2<String, Integer>>() {
@Override
public Tuple2<String, Integer> map(Integer value) throws Exception {
return Tuple2.of("Key", value);
}
}) // 为了使用 keyed window,我们需要提供一个 key,这里简单地给所有元素赋予相同的 key
.keyBy(value -> value.f0)
.timeWindow(Time.minutes(1))
.process(new MyProcessWindowFunction())
.print();
env.execute("Flink Demo");
}
public static class MyProcessWindowFunction extends ProcessWindowFunction<Tuple2<String, Integer>, String, String, TimeWindow> {
@Override
public void process(String key, Context context, Iterable<Tuple2<String, Integer>> elements, Collector<String> out) {
Integer first = null;
Integer last = null;
for (Tuple2<String, Integer> element : elements) {
if (first == null) {
first = element.f1;
}
last = element.f1;
}
out.collect("Window: " + context.window() + " First: " + first + " Last: " + last);
}
}
static class SimpleSourceFunction implements SourceFunction<Integer> {
private volatile boolean isRunning = true;
private int counter = 0;
@Override
public void run(SourceContext<Integer> ctx) throws Exception {
while(isRunning && counter < Integer.MAX_VALUE) {
synchronized (ctx.getCheckpointLock()) {
ctx.collect(counter++);
}
Thread.sleep(1000L); // 每秒生成一个整数
}
}
@Override
public void cancel() {
isRunning = false;
}
}
}
输出:
1> Window: TimeWindow{start=1694865120000, end=1694865180000} First: 0 Last: 28
1> Window: TimeWindow{start=1694865180000, end=1694865240000} First: 29 Last: 8
如果使用reduce窗口聚合函数,可以将数据转化为一个Tuple3类型的结果输出,f0 为 key,f1 为最早的数据,f2 为最晚的数据。(可以将上有数据转为tuple2<时间戳, 数据>的格式,结果的 f1 就等于 value1.f0 < value2.f0 ? value1.f1 : value2.f1
)。
public class FlinkDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<Tuple3<Integer, Long, Long>> sourceStream = env.addSource(new SimpleSourceFunction())
.map(value -> new Tuple3<>(value, System.currentTimeMillis(), System.currentTimeMillis()));
sourceStream
.keyBy(0)
.timeWindow(Time.minutes(5))
.reduce(new MyReduceFunction())
.print();
env.execute("Flink Demo");
}
public static class MyReduceFunction implements ReduceFunction<Tuple3<Integer, Long, Long>> {
@Override
public Tuple3<Integer, Long, Long> reduce(Tuple3<Integer, Long, Long> value1, Tuple3<Integer, Long, Long> value2) {
return new Tuple3<>(
value1.f0, // 取任意一个数值,因为它们都是相同的
Math.min(value1.f1, value2.f1), // 取最早的时间
Math.max(value1.f2, value2.f2) // 取最晚的时间
);
}
}
}
10. 背压的原理?解决办法?
略
11. 遇到状态放不下的场景怎么办?
有时候需要求uv,内存或者状态中存过多数据,导致压力巨大,这个时候可以结合 Redis或者布隆过滤器来去重。
注意:布隆过滤器存在非常小的误判几率,不能判断某个元素一定百分之百存在,所以只能用在允许有少量误判的场景,不能用在需要100%精确判断存在的场景。
12. 使用flink统计订单表的GMV(商品交易总额),如果mysql中的数据出现错误,之后在mysql中做数据的修改操作,那么flink程序如何保证GMV的正确性,你们是如何解决?
CDC 动态捕捉MySQL数据变化,实时处理后数据入湖-Hudi,MOR 机制 快速对下游可见。
另:一般也会有离线Job来恢复和完善实时数据。
13. 开窗函数有哪些?
- Flink SQL:
- 待补充
- Flink Stream:
- ReduceFunction、AggregateFunction:窗口不维护原始数据,只维护中间结果。每次基于中间结果和增量数据进行聚合
- ProcessWindowFunction:维护全部原始数据,窗口触发时进行全量聚合
14. 没有数据流的时候,窗口存在吗
不存在,没有数据,窗口不产生
15. 1小时的滚动窗口,一小时处理一次的压力比较大想让他5分钟处理一次.怎么办?
自定义触发器,4个方法,一个Close三个用于控制计算和输出 ??? 如何实现???
分段窗口:可以将1小时的窗口分解为多个小窗口(如,12个5分钟的窗口),并在每个小窗口上进行计算。然后,你可以使用windowAll和reduce操作将这些小窗口的结果组合起来,形成原本1小时窗口的结果。
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.assigners.GlobalWindows;
import org.apache.flink.streaming.api.windowing.triggers.CountTrigger;
public class FlinkDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<Tuple2<String, Integer>> sourceStream = env.addSource(new SimpleSourceFunction())
.map(value -> new Tuple2<>("Key", value));
DataStream<Tuple2<String, Integer>> smallWindowStream = sourceStream
.keyBy(0)
.timeWindow(Time.minutes(5))
.reduce(new MyReduceFunction());
smallWindowStream
.keyBy(0)
.window(GlobalWindows.create())
.trigger(CountTrigger.of(12)) // 每当有12个小窗口的结果到达时,就触发全局窗口的计算
.reduce(new MyReduceFunction())
.print();
env.execute("Flink Demo");
}
public static class MyReduceFunction implements ReduceFunction<Tuple2<String, Integer>> {
@Override
public Tuple2<String, Integer> reduce(Tuple2<String, Integer> value1, Tuple2<String, Integer> value2) {
return new Tuple2<>(
value1.f0, // 取任意一个key,因为它们都是相同的
value1.f1 + value2.f1 // 对小窗口的结果进行累加
);
}
}
}
16. 两个流先后顺序不确定,到达的间隔也不确定,如何拼接成宽表?
Flink 提供了几种处理这种情况的方法:
-
CoGroup: CoGroup 会将两个流中相同 key 的元素放在一起,然后你可以定义如何处理这两个元素。它可以处理到达时间不确定的问题。
-
Interval Join: Interval Join 可以处理到达间隔不确定的问题,你可以定义一个时间范围,如果第二个流中的元素在这个时间范围内到达,那么就将这两个元素 join 在一起。
-
使用 State 和 Process Function: 你可以使用 State(例如 ValueState)来存储已经到达的元素,然后在 Process Function 中判断另一个流的元素是否到达,如果到达了就进行 join。
-
使用 Table API 或 SQL 的 Temporal Table Function: Temporal Table Function 可以定义一个历史表,当另一个流的元素到达时,可以查询历史表中的状态,然后进行 join。这种方法更适合处理流和静态表(或者另一个流的历史数据)的 join。
具体选择哪种方法取决于你的具体需求和场景。
17. 为什么使用维表?什么情况下使用?
在一些数据量较小,且变化不大的场景下使用维表(如省份信息关联查询拼接)
18. Flink维表关联怎么做的?
1、async io
2、broadcast
3、async io + cache
4、open方法中读取,然后定时线程刷新,缓存更新是先删除,之后再来一条之后再负责写入缓存
在 Flink 中,常见的维表关联方案主要有以下几种:
-
MapState
MapState 可以用来存储 Key-Value 对,Key 是维度表的主键,Value 是维度表的其他字段。当主流数据到达时,可以通过主键在 MapState 中查询到相关的维度信息。
public class DimensionJoinFunction extends KeyedProcessFunction<Long, Tuple2<Long, String>, String> { private transient MapState<String, String> dimensionState; @Override public void open(Configuration parameters) throws Exception { MapStateDescriptor<String, String> descriptor = new MapStateDescriptor<>("dimensionState", Types.STRING, Types.STRING); dimensionState = getRuntimeContext().getMapState(descriptor); } @Override public void processElement(Tuple2<Long, String> value, Context ctx, Collector<String> out) throws Exception { String dimensionInfo = dimensionState.get(value.f0.toString()); out.collect(value.f1 + " " + dimensionInfo); } }
-
Broadcast State
Broadcast State 可以广播维度表数据到所有 Task,并保存在状态中。当主流数据到达时,可以在状态中查询到相关的维度信息。这种方法适合维度表较小且更新频率较高的情况。
public class DimensionJoinFunction extends BroadcastProcessFunction<Tuple2<Long, String>, Tuple2<Long, String>, String> { private transient BroadcastState<Long, String> dimensionState; @Override public void processElement(Tuple2<Long, String> value, ReadOnlyContext ctx, Collector<String> out) throws Exception { String dimensionInfo = ctx.getBroadcastState(descriptor).get(value.f0); out.collect(value.f1 + " " + dimensionInfo); } @Override public void processBroadcastElement(Tuple2<Long, String> value, Context ctx, Collector<String> out) throws Exception { dimensionState = ctx.getBroadcastState(descriptor); dimensionState.put(value.f0, value.f1); } }
-
异步 I/O
当维度表数据量较大且更新频率较高时,可以使用异步 I/O 从外部数据库中查询维度数据。这种方式可以有效避免状态过大的问题,但会引入一定的延迟。
public class AsyncDimensionJoinFunction extends RichAsyncFunction<Tuple2<Long, String>, String> { private transient DataSource dataSource; @Override public void open(Configuration parameters) throws Exception { dataSource = ...; // 初始化数据源 } @Override public void asyncInvoke(Tuple2<Long, String> input, ResultFuture<String> resultFuture) throws Exception { try (Connection connection = dataSource.getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM dimension_table WHERE id = ?")) { statement.setLong(1, input.f0); ResultSet resultSet = statement.executeQuery(); if (resultSet.next()) { resultFuture.complete(Collections.singleton(input.f1 + " " + resultSet.getString("info"))); } else { resultFuture.complete(Collections.singleton(input.f1)); } } } }
以上三种方案的选择取决于你的具体需求和场景,如数据大小,更新频率等因素。
19. 针对上面的MapState方案我想实现定期的线程更新避免无法感知到维表的数据变化要如何实现?
对于这个需求,你可以在 Flink 任务中开启一个定期的 Timer,然后在 onTimer 方法中更新 MapState。这样就可以定期获取维度表的最新数据。
以下是基本步骤:
-
在 open() 方法中启动一个定期的 Timer。
@Override public void open(Configuration parameters) throws Exception { // 创建 MapState 描述符 MapStateDescriptor<String, String> descriptor = new MapStateDescriptor<>("dimensionState", Types.STRING, Types.STRING); dimensionState = getRuntimeContext().getMapState(descriptor); // 启动定期的 Timer long interval = 60 * 1000L; // 定期间隔,例如每分钟更新一次 TimerService timerService = getRuntimeContext().getTimerService(); timerService.registerProcessingTimeTimer(timerService.currentProcessingTime() + interval); }
-
在 onTimer() 方法中更新 MapState。
@Override public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception { // 从外部系统获取维度表的最新数据 Map<String, String> latestData = fetchLatestData(); // 更新 MapState for (Map.Entry<String, String> entry : latestData.entrySet()) { dimensionState.put(entry.getKey(), entry.getValue()); } // 启动下一个定期的 Timer long interval = 60 * 1000L; // 定期间隔,例如每分钟更新一次 TimerService timerService = getRuntimeContext().getTimerService(); timerService.registerProcessingTimeTimer(timerService.currentProcessingTime() + interval); }
-
在 processElement() 方法中,你可以使用最新的 MapState。
@Override public void processElement(Tuple2<Long, String> value, Context ctx, Collector<String> out) throws Exception { String dimensionInfo = dimensionState.get(value.f0.toString()); out.collect(value.f1 + " " + dimensionInfo); }
这样,你就可以定期从外部系统获取维度表的最新数据,并更新到 MapState 中。在处理元素时,总是使用最新的维度信息。
20. flink 1.17了解吗,有哪些新特性?
- Flink SQL批处理支持 Delete 和 Update API,外部存储系统可以实现行级删除更新
- Checkpoint改进:提升了速度和稳定性。非对齐的ck稳定性提升较大,可用于生产环境。(老版本UC会导致过多小文件,导致HDFS namenode负载过高)
- Hive兼容:之前只支持流模式下的文件合并,1.17开始批模式下也能合并,大大减少小文件数量
- 火焰图:1.17支持了针对subTask级别的火焰图(之前是taskManager维度的)
21. Flink 中 Table 和 DataStream 是如何互相转换的?
Table 和 DataStream之间的转换是通过 StreamTableEnvironment 的 toDataStream 和 fromDataStream 完成的。1.13时只有流任务支持,批任务不支持,1.14开始,flink将流批处理统一到了 StreamTableEnvironment中,因此可以做Table和DataStream的相互转换了。
22. Flink 中 怎么消费kafka?如何感知消费堆积?
自定义反序列化方式,将kafka数据反序列化为tuple2类型:
package com.herobin.flink.common.base;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.connectors.kafka.KafkaDeserializationSchema;
import org.apache.kafka.clients.consumer.ConsumerRecord;
/**
* 自定义反序列化模式
* 反序列化模式描述了如何将 Kafka ConsumerRecords 转换为 Flink 处理的数据类型
* 通常常用的为 SimpleStringSchema 即直接已字符串解析的方式解析
* 这个自定义会讲kafka数据解析为Tuple2格式
* f0: 时间戳
* f1: value
*/
public class TimeDeserializationSchema implements KafkaDeserializationSchema<Tuple2<Long, ConsumerRecord<byte[], byte[]>>> {
@Override
public boolean isEndOfStream(Tuple2<Long, ConsumerRecord<byte[], byte[]>> nextElement) {
return false;
}
@Override
public Tuple2<Long, ConsumerRecord<byte[], byte[]>> deserialize(ConsumerRecord<byte[], byte[]> record) throws Exception {
return Tuple2.of(record.timestamp(),record);
}
@Override
public TypeInformation<Tuple2<Long, ConsumerRecord<byte[], byte[]>>> getProducedType() {
return TypeInformation.of(new TypeHint<Tuple2<Long, ConsumerRecord<byte[], byte[]>>>() {
@Override
public TypeInformation<Tuple2<Long, ConsumerRecord<byte[], byte[]>>> getTypeInfo() {
return super.getTypeInfo();
}
});
}
}
23. 详细介绍下 KeyedProcessFunction
KeyedProcessFunction 和 ProcessFunction 为同一包下,都是AbstractRichFunction抽象类的抽象实现。都支持processElement和ontimer方法,区别只是应用于Keyed Stream还是Stream。
KeyedProcessFunction用来操作KeyedStream。KeyedProcessFunction会处理流的每一个元素,输出为0个、1个或者多个元素。所有的Process Function都继承自RichFunction接口,所以都有open()、close()和getRuntimeContext()等方法。而KeyedProcessFunction[KEY, IN, OUT]还额外提供了两个方法:
-
processElement(v: IN, ctx: Context, out: Collector[OUT])
: 流中的每一个元素都会调用这个方法,调用结果将会放在Collector数据类型中输出。Context可以访问元素的时间戳,元素的key,以及TimerService时间服务。Context还可以将结果输出到别的流(side outputs)。 -
onTimer(timestamp: Long, ctx: OnTimerContext, out: Collector[OUT])
:是一个回调函数。当之前注册的定时器触发时调用。参数timestamp为定时器所设定的触发的时间戳。Collector为输出结果的集合。OnTimerContext和processElement的Context参数一样,提供了上下文的一些信息,例如firing trigger的时间信息(事件时间或者处理时间)。
24. 为什么推荐使用Application Mode集群运行模式?
Session和Per-Job流程如下:
- 下载依赖到client节点本地
- 在client中执行main()方法,生成jobGraph对象
- 将jobGraph和Dependencies一起提交给集群运行
- 等待Job运行结果
问题 :
- 消耗带宽(下载依赖包,提交JobGraph和依赖包)
- 生成JobGraph需要消耗CPU资源
- 任务多的时候客户端压力增加
Application Mode集群运行模式:
- 每个Application对应一个JM,可以运行多个Job
- client无需上传jobGraph和依赖包,仅负责提交job的提交和管理
- main()方法运行在JM,JobGraph的生成在集群上完成,客户端压力降低
总结:有效降低带宽消耗可客户端负载,实现了任务组之间的资源隔离和组内job的资源共享
25. 为什么使用选择flink?
1、storm 和 spark 都是批处理的概念,spark streaming也是微批的概念,而flink是真正的流处理,并且做到了流批一体。
2、spark 任务failover时虽然也能从ck恢复,但是数据会被重复处理,不能做到“只处理一次”语义。
3、flink支持天然反压机制,而spark需要额外的子组件来监控反压。
正是因为Flink Table & SQL的加入,Filnk在某种程度上做到了事实上的批流一体。
26. Flink SQL 滚动&滑动窗口Demo
滚动窗口:(实现统计每个用户每天的订单数量)
SELECT
userId,
TUMBLE_START(timeLine, INTERVAL '1' day) as winStart,
SUM(amout)
FROM Orders
GROUP BY
userId, TUMBLE(timeLine, INTERNAL '1' DAY);
滑动窗口:(间隔一小时计算一次过去一天的每个商品的销量)
SELECT
productId,
SUM(amount)
FROM Orders
GROUP BY
productId,
HOP(rowTime, INTERNAL '1' HOUR, INTERNAL '1' DAY)
27. 简单介绍下Flink on k8s
28. Flink 内置的窗口分配器
窗口分配器将会根据事件的事件时间或者处理时间来将事件分配到对应的窗口中去。窗口包含开始时间和结束时间这两个时间戳(左闭右开)。
所有的窗口分配器都包含一个默认的触发器:
- 对于事件时间:当水位线超过窗口结束时间,触发窗口的求值操作。
- 对于处理时间:当机器时间超过窗口结束时间,触发窗口的求值操作。
默认情况下,滚动窗口会和1970-01-01-00:00:00.000对齐,例如一个1小时的滚动窗口将会定义以下开始时间的窗口:00:00:00,01:00:00,02:00:00,等等。
如12:50启动的flink作业,有一小时的开窗,那窗口为[12:00, 13:00) [13:00, 14:00)… 即十分钟后就会触发窗口计算。
29. Flink 如何处理迟到的数据?
迟到的元素是指当这个元素来到时,这个元素所对应的窗口已经计算完毕了(也就是说水位线已经没过窗口结束时间了)。这说明迟到这个特性只针对事件时间。
DataStream API提供了三种策略来处理迟到元素
- 直接抛弃迟到的元素(默认)
- 将迟到的元素发送到另一条流中去
private static OutputTag<String> output = new OutputTag<String>("late-readings"){}; stream = env.add(source) .assignTimestampsAndWatermarks(...) .keyBy() .timeWindow(Time.seconds(5)) .sideOutputLateData(output) // 迟到数据输出到侧边流 .process(.....) // 正常处理 stream.getSideOutput(output).print(); // 获取侧边流
- 可以更新窗口已经计算完的结果,并发出计算结果。
- 这里指的就是依赖allowed lateness设置的可延迟时间,兼容一段延迟范围,延迟范围在其中的数据也会被计算,其实就是watermark会比事件时间少对应的范围差值。
.keyBy(r -> r.f0) .timeWindow(Time.seconds(5)) .allowedLateness(Time.seconds(5)) // 这里指定可延迟五秒!!! .process(new UpdateWindowResult())
30. Flink task 算子之间如何交换数据?网络传输中的内存管理?
Task 算子之间在网络层面上传输数据,使用的是 Buffer,申请和释放由 Flink自行管理,实现类为 NetworkBuffer。1 个 NetworkBuffer 包装了 1 个MemorySegment。同时继承了 AbstractReferenceCountedByteBuf,是 Netty 中的抽象类。
BufferPool 用来管理 Buffer,包含 Buffer 的申请、释放、销毁、可用 Buffer 通知等,实现类是 LocalBufferPool,每个 Task 拥有自己的 LocalBufferPool。
BufferPoolFactory 用 来 提 供 BufferPool 的 创 建 和 销 毁 , 唯 一 的 实 现 类 是NetworkBufferPool , 每 个 TaskManager 只 有 一 个 NetworkBufferPool 。 同一个TaskManager 上的 Task 共享 NetworkBufferPool,在 TaskManager 启动的时候创建并分配内存。
简单总结:inputBuffer/outputBuffer 向 task内部的 LocalBufferPool 申请内存块segment,不足再向 TM 中的 NetWorkBufferPool 申请,达到最大申请值的时候无法继续申请,触发反压。
详细如下:
网络上传输的数据会写到 Task 的 InputGate(IG)中,经过 Task 的处理后,再由 Task写到 ResultPartition(RS) 中。(感觉可以简单理解为inputBuffer 和 outputBuffer)。
- TM启动时候会初始化 NetworkEnvironment 对象,TM 中所有与网络相关的东西都由该类来管理(如 Netty 连接),其中就包括 NetworkBufferPool(默认占TM进程内存的10%)。根据配置, Flink 会 在 NetworkBufferPool 中 生 成 一 定 数 量 ( 默 认 2048 ) 的 内 存 块MemorySegment。
- Task启动时,会为 inputBuffer 和 outputBuffer 分别启动一个 LocalBufferPool 并设置可申请的内存块数量,此时并没有真正分配内存,只需需要的时候才分配。这样可以让 NetWorkBufferPool 中的内存尽可能多,实时按需分配,系统可以更轻松地应对瞬时压力。
- Task执行过程中,input/output buffer 会向 LocalBufferPool 申请内存,其中若没有可用的内存块且已申请的数量还没到池子上限,则会向 NetworkBufferPool 申请,如果达到了申请上限,或 NetworkBufferPool 也没有内存可分时,会级联向上反馈反压。(方式为阻塞在请求内存块的地方,达到暂停写入的目的)。
- 一个内存块被消费完成之后,会将内存块还给 LocalBufferPool 。如果 LocalBufferPool 中当前申请的数量超过了池子容量(每个task中的可申请容量是动态的,会由于新注册的 Task 导致该池子容量变小,总数是确定的,task越多,单task可申请的内存块越小),则LocalBufferPool 会将该内存块回收给 NetworkBufferPool。如果没超过池子容量,则会继续留在池子中,减少反复申请的开销。
31. Flink 中 SQL 是如何应用于流处理的?
主要是通过 动态表 和 连续查询两种技术方案。
-
动态表:输入流映射为SQL动态输入表(静态表:数据固定不变的,天/小时粒度的分区,动态表:随时间实时变化的)。
-
连续查询:不断的消费动态输入表来持续更新动态结果表的数据。
物化视图:也是一条SQL查询,和虚拟视图VIEW的区别是物化视图会缓存查询的结果,因此再访问时不需要重新计算。在流处理中,就需要进行实时视图维护,一旦视图的数据源表更新视图结果也同步更新保证数据是最新的。这种实时视图维护的技术就叫做连续查询。
连续查询对结果的输出方式有两种:
1. 插入结果表
2. 更新结果表
总结:动态表和连续查询两项技术在一条流SQL中的执行过程总共包含了三个步骤:
1、第一步:将数据输入流转换为SQL中的动态输入表。这里的转化其实就是指将输入流映射(绑定)为一个动态输入表。
2、第二步:在动态输入表上执行一个连续查询,然后生成一个新的动态结果表。
3、第三步:生成的动态结果表被转换回数据输出流。
32. 介绍下 TableEnvironment
TableEnvironment 在 SQL API 中的地位和 StreamExecutionEnvironment 在 DataStream 中的地位是一样的,都是包含了一个 Flink 任务运行时的所有上下文环境信息。
TableEnvironment 包含的功能如下:
- Catalog 管理:Catalog 可以理解为 Flink 的 MetaStore,类似 Hive MetaStore 对在 Hive 中的地位,关于 Flink Catalog 的详细内容后续进行介绍。
- 表管理:在 Catalog 中注册表
- SQL 查询:就像 DataStream 中提供了 addSource、map、flatmap 等接口
- UDF 管理:注册用户定义函数(标量函数:一进一出、表函数:一进多出、聚合函数:多进一出)
- UDF 扩展:加载可插拔 Module(Module 可以理解为 Flink 管理 UDF 的模块,是可插拔的,可以让小伙伴萌自定义 Module,去支持奇奇怪怪的 UDF 功能)
- DataStream 和 Table(Table\SQL API 的查询结果)之间进行转换:目前 1.17 版本的只有流任务支持,批任务不支持。1.14 支持批任务。
创建方法:
- 通过EnvironmentSettings 创建 TableEnvironment
// 1. 就是设置一些环境信息 EnvironmentSettings settings = EnvironmentSettings .newInstance() .inStreamingMode() // 声明为流任务 //.inBatchMode() // 声明为批任务 .build(); // 2. 创建 TableEnvironment TableEnvironment tEnv = TableEnvironment.create(settings);
- 通过已有的 StreamExecutionEnvironment 创建 TableEnvironment
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);
33. Flink SQL 中表的概念
一个表的全名(标识)会由三个部分组成:Catalog 名称.数据库名称.表名称。如果 Catalog 名称或者数据库名称没有指明,就会使用当前默认值 default。
表可以是常规的(外部表 TABLE),也可以是虚拟的(视图 VIEW)。
- 外部表 TABLE:描述的是外部数据,例如文件(HDFS)、消息队列(Kafka)等。依然拿离线 Hive SQL 举个例子,离线中一个表指的是 Hive 表,也就是所说的外部数据。
- 视图 VIEW:从已经存在的表中创建,视图一般是一个 SQL 逻辑的查询结果。对比到离线的 Hive SQL 中,在离线的场景(Hive 表)中 VIEW 也都是从已有的表中去创建的。其实 Flink 也是一样的。
临时表:表(视图、外部表)可以是临时的,并与单个 Flink session(可以理解为 Flink 任务运行一次就是一个 session)的生命周期绑定。
永久表:需要外部 Catalog(例如 Hive Metastore)来持久化表的元数据。一旦永久表被创建,它将对任何连接到这个 Catalog 的 Flink session 可见且持续存在,直至从 Catalog 中被明确删除。
34. 在 pdd 这种发补贴券的场景下,希望可以在发的补贴券总金额超过 1w 元时,及时报警出来,来帮助控制预算,防止发的太多。
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.table.api.Table;
import org.apache.flink.types.Row;
import org.apache.flink.util.Collector;
import flink.examples.FlinkEnvUtils;
import flink.examples.FlinkEnvUtils.FlinkEnv;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class AlertExample {
public static void main(String[] args) throws Exception {
FlinkEnv flinkEnv = FlinkEnvUtils.getStreamTableEnv(args);
// 1. pdd 发补贴券流水数据
String createTableSql = "CREATE TABLE source_table (\n"
+ " id BIGINT,\n" -- 补贴券的流水 id
+ " money BIGINT,\n" -- 补贴券的金额
+ " row_time AS cast(CURRENT_TIMESTAMP as timestamp_LTZ(3)),\n"
+ " WATERMARK FOR row_time AS row_time - INTERVAL '5' SECOND\n"
+ ") WITH (\n"
+ " 'connector' = 'datagen',\n"
+ " 'rows-per-second' = '1',\n"
+ " 'fields.id.min' = '1',\n"
+ " 'fields.id.max' = '100000',\n"
+ " 'fields.money.min' = '1',\n"
+ " 'fields.money.max' = '100000'\n"
+ ")\n";
// 2. 计算总计发放补贴券的金额
String querySql = "SELECT UNIX_TIMESTAMP(CAST(window_end AS STRING)) * 1000 as window_end, \n"
+ " window_start, \n"
+ " sum(money) as sum_money,\n" -- 补贴券的发放总金额
+ " count(distinct id) as count_distinct_id\n"
+ "FROM TABLE(CUMULATE(\n"
+ " TABLE source_table\n"
+ " , DESCRIPTOR(row_time)\n"
+ " , INTERVAL '5' SECOND\n"
+ " , INTERVAL '1' DAY))\n"
+ "GROUP BY window_start, \n"
+ " window_end";
flinkEnv.streamTEnv().executeSql(createTableSql);
Table resultTable = flinkEnv.streamTEnv().sqlQuery(querySql);
// 3. 将金额结果转为 DataStream,然后自定义超过 1w 的报警逻辑
flinkEnv.streamTEnv()
.toDataStream(resultTable, Row.class)
.flatMap(new FlatMapFunction<Row, Object>() {
@Override
public void flatMap(Row value, Collector<Object> out) throws Exception {
long l = Long.parseLong(String.valueOf(value.getField("sum_money")));
if (l > 10000L) {
log.info("报警,超过 1w");
}
}
});
flinkEnv.env().execute();
}
}
35. Flink SQL 的时间属性
三种时间在生产环境的使用频次 事件时间(SQL 常用) > 处理时间(SQL 几乎不用,DataStream 少用) > 摄入时间(不用)。
CREATE TABLE DDL 指定时间戳的方式:
CREATE TABLE user_actions (
user_name STRING,
data STRING,
user_action_time TIMESTAMP(3),
-- 使用下面这句来将 user_action_time 声明为事件时间,并且声明 watermark 的生成规则,即 user_action_time 减 5 秒
-- 事件时间列的字段类型必须是 TIMESTAMP 或者 TIMESTAMP_LTZ 类型
WATERMARK FOR user_action_time AS user_action_time - INTERVAL '5' SECOND
) WITH (
...
);
SELECT TUMBLE_START(user_action_time, INTERVAL '10' MINUTE), COUNT(DISTINCT user_name)
FROM user_actions
-- 然后就可以在窗口算子中使用 user_action_time
GROUP BY TUMBLE(user_action_time, INTERVAL '10' MINUTE);
CREATE TABLE DDL 指定时间戳的方式:
CREATE TABLE user_actions (
user_name STRING,
data STRING,
-- 使用下面这句来将 user_action_time 声明为处理时间
user_action_time AS PROCTIME()
) WITH (
...
);
SELECT TUMBLE_START(user_action_time, INTERVAL '10' MINUTE), COUNT(DISTINCT user_name)
FROM user_actions
-- 然后就可以在窗口算子中使用 user_action_time
GROUP BY TUMBLE(user_action_time, INTERVAL '10' MINUTE);
36. Flink SQL 中支持的 4 种窗口的运算
- 滚动窗口(TUMBLE)
- 滑动窗口(HOP)
- Session 窗口(SESSION)
- 渐进式窗口(CUMULATE)
37. Flink SQL 总共提供了 4 种函数:
- 临时性系统内置函数
- 系统内置函数
- 临时性 Catalog 函数(例如:Create Temporary Function)
- Catalog 函数(例如:Create Function)
38. Flink SQL 性能调优
主要包含以下四种优化:
- (常用)MiniBatch 聚合:unbounded group agg 中,可以使用 minibatch 聚合来做到微批计算、访问状态、输出结果,避免每来一条数据就计算、访问状态、输出一次结果,从而减少访问 state 的时长(尤其是 Rocksdb)提升性能。
- (常用)两阶段聚合:类似 MapReduce 中的 Combiner 的效果,可以先在 shuffle 数据之前先进行一次聚合,减少 shuffle 数据量
- (不常用)split 分桶:在 count distinct、sum distinct 的去重的场景中,如果出现数据倾斜,任务性能会非常差,所以如果先按照 distinct key 进行分桶,将数据打散到各个 TM 进行计算,然后将分桶的结果再进行聚合,性能就会提升很大
- (常用)去重 filter 子句:在 count distinct 中使用 filter 子句于 Hive SQL 中的 count(distinct if(xxx, user_id, null)) 子句,但是 state 中同一个 key 会按照 bit 位会进行复用,这对状态大小优化非常有用
MiniBatch 聚合如何解决上述问题:其核心思想是将一组输入的数据缓存在聚合算子内部的缓冲区中。当输入的数据被触发处理时,每个 key 只需要访问一次状态后端,这样可以大大减少访问状态的时间开销从而获得更好的吞吐量。但是,其会增加一些数据产出的延迟,因为它会缓冲一些数据再去处理。因此如果你要做这个优化,需要提前做一下吞吐量和延迟之间的权衡,但是大多数情况下,buffer 数据的延迟都是可以被接受的。所以非常建议在 unbounded agg 场景下使用这项优化。
39. Flink SQL 架构 & Blink Planner
Flink 1.9 版本引入了新的 Blink Planner,将批 SQL 处理作为流 SQL 处理的特例,尽量对通用的处理和优化逻辑进行抽象和复用,通过 Flink 内部的 Stream Transformation API 实现流 & 批的统一处理,替代原 Flink Planner 将流 & 批区分处理的方式。
40. Flink SQL 是如何转化为 JobGraph 的?经历了几个阶段?
主要经历如下几个阶段:
- 将 SQL文本 / TableAPI 代码转化为逻辑执行计划(Logical Plan)
- Logical Plan 通过优化器优化为物理执行计划(Physical Plan)
- 通过代码生成技术生成 Transformations 后进一步编译为可执行的 JobGraph 提交运行
41. 如果下级存储不支持事务,Flink 怎么保证 exactly-once
端到端的 exactly-once 对 sink 要求比较高,具体实现主要有幂等写入和事务性写入两种方式。
幂等写入的场景依赖于业务逻辑,更常见的是用事务性写入。而事务性写入又有预写日志(WAL)和两阶段提交(2PC)两种方式。
如果外部系统不支持事务,那么可以用预写日志的方式,把结果数据先当成状态保存,然后在收到 checkpoint 完成的通知时,一次性写入 sink 系统。
42. Flink SQL 计算用户分布,根据QQ 等级变化明细数据(time,uid,level)求出当前每个等级的用户数
类似将相同的数据进行分区,按照时间倒排取第一条的思想(partition by uid order by time desc)
-- 如果需要可以打开mini-batch
select
level
, count(1) as uv
, max(time) as time
from (
select
uid
, level
, time
, row_number() over (partition by uid order by time desc) rn
from source
) tmp
where rn =1
group by
level
43. 如何开启mini-batch
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.TableEnvironment;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.api.config.ExecutionConfigOptions;
public class FlinkSQLMiniBatchDemo {
public static void main(String[] args) throws Exception {
// 创建 StreamExecutionEnvironment
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 创建 Blink Planner 的 TableEnvironment
EnvironmentSettings settings = EnvironmentSettings.newInstance().useBlinkPlanner().inStreamingMode().build();
StreamTableEnvironment tEnv = StreamTableEnvironment.create(env, settings);
// 开启 MiniBatch
tEnv.getConfig().getConfiguration().set(ExecutionConfigOptions.TABLE_EXEC_MINIBATCH_ENABLED, true);
// 设置 MiniBatch 大小为 5000,意味着每处理 5000 条记录后执行一次 MiniBatch 操作
tEnv.getConfig().getConfiguration().set(ExecutionConfigOptions.TABLE_EXEC_MINIBATCH_ALLOW_LATENCY, Duration.ofSeconds(5));
// 创建表、查询、插入等操作
// ...
// 执行作业
env.execute("Flink SQL MiniBatch Demo");
}
}
44. Flink SQL 计算 DAU:用户心跳日志(uid,time,type)。计算分 Android,iOS 的 DAU,最晚一分钟输出一次当日零点累计到当前的结果
实现方式:Deduplicate
select
platform
, count(1) as dau
, max(time) as time
from (
select
uid
, platform
, time
, row_number() over (partition by uid, platform, time / 24 / 3600 / 1000 order by time desc) rn
from source
) tmp
where rn = 1
group by
platform
45. Flink里面异步IO代码具体怎么写?
- 定义一个AsyncFunction用于实现请求的分发
- 定义一个callback回调函数,该函数用于取出异步请求的返回结果,并将返回的结果传递给ResultFuture .
- 对DataStream使用Async操作。
import org.apache.flink.api.common.functions.RichAsyncFunction;
import org.apache.flink.api.common.functions.RuntimeContext;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.AsyncDataStream;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.async.ResultFuture;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AsyncIODemo {
public static void main(String[] args) throws Exception {
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 输入数据流
DataStream<String> input = env.fromElements("element1", "element2", "element3");
// 异步函数,用于发送请求并获取结果
RichAsyncFunction<String, String> asyncFunction = new RichAsyncFunction<String, String>() {
private transient ExecutorService executor;
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
executor = Executors.newFixedThreadPool(10);
}
@Override
public void close() throws Exception {
super.close();
executor.shutdown();
}
@Override
public void asyncInvoke(String input, ResultFuture<String> resultFuture) throws Exception {
// 使用线程池发送异步请求,模拟异步 I/O 操作
CompletableFuture.supplyAsync(() -> {
// 这里应该是实际的异步 I/O 操作,例如查询外部数据库
return "Result of " + input;
}, executor).thenAccept((String result) -> {
// 请求完成时回调
resultFuture.complete(Collections.singletonList(result));
});
}
};
// 调用 AsyncDataStream.unorderedWait() 方法进行异步数据请求
DataStream<String> result = AsyncDataStream.unorderedWait(
input, asyncFunction,
1000L, TimeUnit.MILLISECONDS, // 超时时间
100); // 进行中的异步请求的最大数量
result.print();
env.execute("Async I/O Example");
}
}
AsyncDataStream包含两种输出模式: 有序和无序,分别对应静态方法 orderedWait与unorderedWait。
46. Flink 中状态 TTL 的原理机制?
在 Flink 中设置 State TTL,就会有一个过期时间戳,具体实现时,Flink 会把时间戳字段和具体数据字段存储作为同级存储到 State 中。
举个例子,我要将一个 String 存储到 State 中时:
- 没有设置 State TTL 时,则直接将 String 存储在 State 中
- 如果设置 State TTL 时,则 Flink 会将 <String, Long> 存储在 State 中,其中 Long 为时间戳,用于判断是否过期。
State 过期的 4 种删除策略:
- lazy 删除策略(默认):就是在访问 State 的时候根据时间戳判断是否过期,如果过期则主动删除 State 数据
- full snapshot cleanup 删除策略:从状态恢复(checkpoint、savepoint)的时候采取做过期删除,但是不支持 rocksdb 增量 ck
- incremental cleanup 删除策略:访问 state 的时候,主动去遍历一些 state 数据判断是否过期,如果过期则主动删除 State 数据
- rocksdb compaction cleanup 删除策略:rockdb 做 compaction 的时候遍历进行删除。仅仅支持 rocksdb
47. Flink SQL 的 State 使用?TTL?
其实 Flink SQL 发明出来就是为了屏蔽窗口、状态这些底层的东西的。
但是我们在使用 Flink SQL 时,70% 以上的场景都是不得不去关注 State 的!
举个 Flink SQL 的例子,下这个 SQL 用于计算每个 sessionId 的点击量:
SELECT
sessionId
, COUNT(*)
FROM clicks
GROUP BY
sessionId;
当 sessionId 为 1 亿时,或许还能够正常运行,但是 sessionId 为 10 亿时,State 将会变得很大,我们就不得不考虑是否要设置 State TTL 以防止无限增大的 State。
问题:哪些场景的 Flink SQL 会常常去考虑 State TTL 呢?
答案:相信大家通过上面的案例之后也能总结出来了。其实就是 unbounded Flink SQL 常常会考虑到,因为这类 Flink SQL 的 State 只会越变越大,如果没有设置合理的 State TTL 的话,任务可能会由于大 State 导致磁盘压力大,任务卡住。
如何给Flink SQL状态设置TTL?
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.TableEnvironment;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.api.config.ExecutionConfigOptions;
import org.apache.flink.table.api.config.TableConfigOptions;
public class FlinkSQLStateTTL {
public static void main(String[] args) throws Exception {
// 创建 StreamExecutionEnvironment
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 创建 Blink Planner 的 TableEnvironment
EnvironmentSettings settings = EnvironmentSettings.newInstance().useBlinkPlanner().inStreamingMode().build();
StreamTableEnvironment tEnv = StreamTableEnvironment.create(env, settings);
// 设置状态的 TTL
tEnv.getConfig().getConfiguration().set(ExecutionConfigOptions.TABLE_EXEC_STATE_TTL, Duration.ofHours(1)); // 设置状态的 TTL 为1小时
// 开启状态的 TTL
tEnv.getConfig().getConfiguration().set(TableConfigOptions.TABLE_DYNAMIC_TABLE_OPTIONS_STATE_RETENTION, "ON");
// 创建表、查询、插入等操作
// ...
// 执行作业
env.execute("Flink SQL State TTL Demo");
}
}
48. 实现计算小时级别,天级别的用户PV、UV(CUMULATE!!!)
DataStream:
- 如果数据量不是特别大,或者虽然数据量大,但是key的数据量可控,直接用窗口聚合的方式,存放在mapState<Uid, UidCount>中,processFunction中 processElement将数据聚合到状态中,timer方式触发输出。mapState加上TTL,onReadAndUpdate。
- 使用redis,分组 key 为业务数据(如微博ID)+ 时间format(如天级则为yyyyMMdd),然后存放在redis的Hash类型中,hash 的 key 为 用户ID,value 为 用户访问次数。触发方式为timer触发,其实思想和上面一样,就是引入了外部存储。
Flink SQL:(使用 cumulate )
如果是一分钟:1min tumble window(count distinct 实际是 MapState)
如果是一小时:1day cumulate window(count distinct 实际是 MapState)
重点引出介绍 cumulate!!!!!!!!!!!!!!
渐进式窗口(CUMULATE)
渐进式窗口定义(1.13 只支持 Streaming 任务):渐进式窗口在其实就是 固定窗口间隔内提前触发的的滚动窗口,其实就是 Tumble Window + early-fire
的一个事件时间的版本。例如,从每日零点到当前这一分钟绘制累积 UV,其中 10:00 时的 UV 表示从 00:00 到 10:00 的 UV 总数。
渐进式窗口可以认为是首先开一个最大窗口大小的滚动窗口,然后根据用户设置的触发的时间间隔将这个滚动窗口拆分为多个窗口,这些窗口具有相同的窗口起点和不同的窗口终点。
应用场景:周期内累计 PV,UV 指标(如每天累计到当前这一分钟的 PV,UV)。这类指标是一段周期内的累计状态。
实际案例:每天的截止当前分钟的累计 money(sum(money)),去重 id 数(count(distinct id))。每天代表渐进式窗口大小为 1 天,分钟代表渐进式窗口移动步长为分钟级别。举例如下:
明细输入数据:
预期经过渐进式窗口计算的输出数据:
转化为折线图长这样:
可以看到,其特点就在于,每一分钟的输出结果都是当天零点累计到当前的结果。
渐进式窗口目前只有 Windowing TVF 方案支持:
-- 数据源表
CREATE TABLE source_table (
-- 用户 id
user_id BIGINT,
-- 用户
money BIGINT,
-- 事件时间戳
row_time AS cast(CURRENT_TIMESTAMP as timestamp(3)),
-- watermark 设置
WATERMARK FOR row_time AS row_time - INTERVAL '5' SECOND
) WITH (
'connector' = 'datagen',
'rows-per-second' = '10',
'fields.user_id.min' = '1',
'fields.user_id.max' = '100000',
'fields.price.min' = '1',
'fields.price.max' = '100000'
);
-- 数据汇表
CREATE TABLE sink_table (
window_end bigint,
window_start bigint,
sum_money BIGINT,
count_distinct_id bigint
) WITH (
'connector' = 'print'
);
-- 数据处理逻辑
insert into sink_table
SELECT
UNIX_TIMESTAMP(CAST(window_end AS STRING)) * 1000 as window_end,
window_start,
sum(money) as sum_money,
count(distinct id) as count_distinct_id
FROM TABLE(CUMULATE( -- !!!!!这里声明!!!!!
TABLE source_table
, DESCRIPTOR(row_time)
, INTERVAL '60' SECOND
, INTERVAL '1' DAY))
GROUP BY
window_start,
window_end
可以看到 Windowing TVF 滚动窗口的写法就是把 cumulate window 的声明写在了数据源的 Table 子句中,即 TABLE(CUMULATE(TABLE source_table, DESCRIPTOR(row_time), INTERVAL ‘60’ SECOND, INTERVAL ‘1’ DAY)),其中包含四部分参数:
第一个参数 TABLE source_table
声明数据源表;
第二个参数 DESCRIPTOR(row_time)
声明数据源的时间戳;
第三个参数 INTERVAL '60' SECOND
声明渐进式窗口触发的渐进步长为 1 min。
第四个参数 INTERVAL '1' DAY
声明整个渐进式窗口的大小为 1 天,到了第二天新开一个窗口重新累计。
渐进式窗口语义和滚动窗口类似
TVF 是 Table-valued Function 的缩写,即表值函数
49. Flink SQL 滚动窗口介绍
滚动窗口定义:滚动窗口将每个元素指定给指定窗口大小的窗口。滚动窗口具有固定大小,且不重叠。
应用场景:常见的按照一分钟对数据进行聚合,计算一分钟内 PV,UV 数据。
实际案例:简单且常见的分维度分钟级别同时在线用户数、总销售额
关于滚动窗口,在 1.13 版本之前和 1.13 及之后版本有两种 Flink SQL 实现方式,分别是:
- Group Window Aggregation(1.13 之前只有此类方案,此方案在 1.13 及之后版本已经标记为废弃,不推荐小伙伴萌使用)
- Windowing TVF(1.13 及之后建议使用 Windowing TVF,TVF 是 Table-valued Function 的缩写,即表值函数)
Group Window Aggregation 方案(支持 Batch\Streaming 任务):
-- 数据源表
CREATE TABLE source_table (
-- 维度数据
dim STRING,
-- 用户 id
user_id BIGINT,
-- 用户
price BIGINT,
-- 事件时间戳
row_time AS cast(CURRENT_TIMESTAMP as timestamp(3)),
-- watermark 设置
WATERMARK FOR row_time AS row_time - INTERVAL '5' SECOND
) WITH (
'connector' = 'datagen',
'rows-per-second' = '10',
'fields.dim.length' = '1',
'fields.user_id.min' = '1',
'fields.user_id.max' = '100000',
'fields.price.min' = '1',
'fields.price.max' = '100000'
)
-- 数据汇表
CREATE TABLE sink_table (
dim STRING,
pv BIGINT,
sum_price BIGINT,
max_price BIGINT,
min_price BIGINT,
uv BIGINT,
window_start bigint
) WITH (
'connector' = 'print'
)
-- 数据处理逻辑
insert into sink_table
select
dim,
count(*) as pv,
sum(price) as sum_price,
max(price) as max_price,
min(price) as min_price,
-- 计算 uv 数
count(distinct user_id) as uv,
UNIX_TIMESTAMP(CAST(tumble_start(row_time, interval '1' minute) AS STRING)) * 1000 as window_start
from source_table
group by
dim,
tumble(row_time, interval '1' minute)
可以看到 Group Window Aggregation
滚动窗口的 SQL 语法就是把 tumble window
的声明写在了 group by
子句中,即 tumble(row_time, interval '1' minute)
,第一个参数为事件时间的时间戳;第二个参数为滚动窗口大小。
Window TVF 方案(1.13 只支持 Streaming 任务):
-- 数据源表
CREATE TABLE source_table (
-- 维度数据
dim STRING,
-- 用户 id
user_id BIGINT,
-- 用户
price BIGINT,
-- 事件时间戳
row_time AS cast(CURRENT_TIMESTAMP as timestamp(3)),
-- watermark 设置
WATERMARK FOR row_time AS row_time - INTERVAL '5' SECOND
) WITH (
'connector' = 'datagen',
'rows-per-second' = '10',
'fields.dim.length' = '1','fields.user_id.min' = '1',
'fields.user_id.max' = '100000',
'fields.price.min' = '1',
'fields.price.max' = '100000'
)
-- 数据汇表
CREATE TABLE sink_table (
dim STRING,
pv BIGINT,
sum_price BIGINT,
max_price BIGINT,
min_price BIGINT,
uv BIGINT,
window_start bigint
) WITH (
'connector' = 'print'
)
-- 数据处理逻辑
insert into sink_table
SELECT
dim,
UNIX_TIMESTAMP(CAST(window_start AS STRING)) * 1000 as window_start,
count(*) as pv,
sum(price) as sum_price,
max(price) as max_price,
min(price) as min_price,
count(distinct user_id) as uv
FROM TABLE(TUMBLE(
TABLE source_table
, DESCRIPTOR(row_time)
, INTERVAL '60' SECOND))
GROUP BY window_start,
window_end,
dim
可以看到 Windowing TVF
滚动窗口的写法就是把 tumble window
的声明写在了数据源的 Table 子句中,即 TABLE(TUMBLE(TABLE source_table, DESCRIPTOR(row_time), INTERVAL '60' SECOND))
,包含三部分参数。
第一个参数 TABLE source_table
声明数据源表;
第二个参数 DESCRIPTOR(row_time)
声明数据源的时间戳;
第三个参数 INTERVAL '60' SECOND
声明滚动窗口大小为 1 min。
SQL 语义:
由于离线没有相同的时间窗口聚合概念,这里就直接说实时场景 SQL 语义,假设 Orders 为 kafka,target_table 也为 Kafka,这个 SQL 生成的实时任务,在执行时,会生成三个算子:
- 数据源算子(From Order):连接到 Kafka topic,数据源算子一直运行,实时的从 Order Kafka 中一条一条的读取数据,然后一条一条发送给下游的 窗口聚合算子
- 窗口聚合算子(TUMBLE 算子):接收到上游算子发的一条一条的数据,然后将每一条数据按照时间戳划分到对应的窗口中(根据事件时间、处理时间的不同语义进行划分),上述案例为事件时间,事件时间中,滚动窗口算子接收到上游的 Watermark 大于窗口的结束时间时,则说明当前这一分钟的滚动窗口已经结束了,将窗口计算完的结果发往下游算子(一条一条发给下游 数据汇算子)
- 数据汇算子(INSERT INTO target_table):接收到上游发的一条一条的数据,写入到 target_table Kafka 中
这个实时任务也是 24 小时一直在运行的,所有的算子在同一时刻都是处于 running 状态的。
注意:
事件时间中滚动窗口的窗口计算触发是由 Watermark 推动的。
50. Flink SQL TVF 实现一个简单的滑动窗口拼接Demo
SELECT
L.num as L_Num
, L.id as L_Id
, R.num as R_Num
, R.id as R_Id
, L.window_start
, L.window_end
FROM (
SELECT *
FROM TABLE(TUMBLE(TABLE LeftTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES))
) L
FULL JOIN (
SELECT *
FROM TABLE(TUMBLE(TABLE RightTable, DESCRIPTOR(row_time), INTERVAL '5' MINUTES))
) R
ON L.num = R.num
AND L.window_start = R.window_start
AND L.window_end = R.window_end;
51. 简单介绍下非对齐checkpoint
应对场景:
- 反压时,buffer 中缓存了大量的数据,导致 barrier 流动缓慢,对于 barrier 已经到达的 channel,由于需要对齐其他 channel 的 barrier,其上游数据处理会被阻塞住;
- barrier 将要很久才能流动到 sink,这导致 checkpoint 的完成时间很长。一个完成时间很长的 checkpoint 意味着,其在完成的那个时刻就已经过时了。
- 因为一个过度使用的 pipeline 是相当脆弱的,这可能会引起一个恶性循环:超时的 checkpoint、任务崩溃、恢复到一个相当过时的 checkpoint、反压更为严重、checkpoint 更为厉害,这样的一个恶性循环会使得任务几乎毫无进展;
优势在于:
- 即使 operator 的 barrier 没有全部到达,数据处理也不会被阻塞;
- checkpoint 耗时将极大减少,即使对于单输入的 operator 也是如此;
- 即使在不稳定的环境中,任务也会有更多的进展,因为从更新的 checkpoint 恢复能够避免许多重复计算;
- 更快的扩缩容;
非对齐 checkpoint 最根本的思想就是将缓冲的数据当做算子状态的一部分,该机制仍会使用 barrier,用来触发 checkpoint。
非对齐 checkpoint 也存在缺点:
- 需要保存 buffer 数据,状态大小会膨胀,磁盘压力大。尤其是当集群 IO 存在瓶颈时,该问题更为明显
- 恢复时需要额外恢复 buffer 数据,作业重新拉起的耗时可能会很长;
52. Kafka+Flink的实时数仓方案有什么问题?如何优化?
主要指的是实时数仓1.0 架构有哪些问题,2.0是怎么做的来解决的这些问题。
- Kafka无法支持海量数据存储。对于海量数据量的业务线来说,Kafka一般只能存储非常短时间的数据,比如最近一周,甚至最近一天;
- Kafka无法支持高效的OLAP查询。大多数业务都希望能在DWD\DWS层支持即席查询的,但是Kafka无法非常友好地支持这样的需求;
- 无法复用目前已经非常成熟的基于离线数仓的数据血缘、数据质量管理体系。需要重新实现一套数据血缘、数据质量管理体系;
- Lambad架构维护成本很高。很显然,这种架构下数据存在两份、schema不统一、 数据处理逻辑不统一,整个数仓系统维护成本很高;
- Kafka不支持update/upsert。目前Kafka仅支持append。实际场景中在DWS轻度汇聚层很多时候是需要更新的,DWD明细层到DWS轻度汇聚层一般会根据时间粒度以及维度进行一定的聚合,用于减少数据量,提升查询性能。假如原始数据是秒级数据,聚合窗口是1分钟,那就有可能产生某些延迟的数据经过时间窗口聚合之后需要更新之前数据的需求。这部分更新需求无法使用Kafka实现。
解决方案:大数据架构的批流一体建设
实时数仓2.0
无论是业务SQL使用上的统一还是计算引擎上的统一,都是批流一体的一个方面。除此之外,批流一体还有一个最核心的方面,那就是存储层面上的统一。在这个方面业界也有一些走在前面的技术,比如最近一段时间开始流行起来的数据湖三剑客-- delta/hudi/iceberg,就在往这个方向走。存储一旦能够做到统一,上述数据仓库架构就会变成如下模样(以Iceberg数据湖作为统一存储为例),称为实时数仓2.0:
2.0只是先做到了存储层面的统一,计算还是流批分离的。不多做介绍。。。。。。
基于Flink/数据湖的3.0架构如下图: