1. window 概念
-
1.1 为什么要有窗口
- 实时流是源源不断的进行处理,为了得到一段流的处理后结果,此时就要把无限流转为有界流,此时Flink引入了窗口的概念
-
1.2 什么叫窗口
- 实时流上截取的一段流 就叫一个窗口
-
1.3 开窗的原理
- 将流数据发到有限大小的桶中进行分析.例如每小时的数据开窗,8~9的数据为一个桶 ,9~10点的数据为一个桶。那么8~9的数据来了 那么就会放在8~9的桶中。
2. window 类型
-
2.1 时间窗口
- 滚动时间窗口
- 窗口长度固定 且连续 不可以有重叠
- 滑动时间窗口
- 窗口长度固定 可以有重叠
- 会话窗口
- 一段时间没有接受到新数据 就会生成新的窗口 时间无对其
- 滚动时间窗口
-
2.2 计数窗口(根据窗口中key的个数来开窗)
- 滚动计数窗口
- 滑动计数窗口
3. 怎么新建窗口
- keyBy( ) 之后通过 timeWindows 和 countWindow方法 定义时间窗口和计数窗口
4. 新建窗口后的聚合方式
-
4.1 增量聚合函数
- 每条数据到来就进行计算,保存一个中间聚合状态,等到窗口结束 输出结果
ReduceFunction AggreateFunction 等
-
4.2 全窗口函数
- 先把窗口所有的数据收集起来,等到计算的时候遍历所有数据 可以拿到当前窗口的元数据信息(窗口的开始时间 结束时间 窗口的key)
ProcessWindownFunction / windowFunction
- 窗口元数据信息
@PublicEvolving
public class TimeWindow extends Window {
private final long start;
private final long end;
public long getStart() {
return start;
}
public long getEnd() {
return end;
}
@Override
public long maxTimestamp() {
return end - 1;
}
- 例子:
public class WindowTest1_TimeWindow {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// // 从文件读取数据
// DataStream<String> inputStream = env.readTextFile("D:\\Projects\\BigData\\FlinkTutorial\\src\\main\\resources\\sensor.txt");
// socket文本流
DataStream<String> inputStream = env.socketTextStream("localhost", 7777);
// 转换成SensorReading类型
DataStream<SensorReading> dataStream = inputStream.map(line -> {
String[] fields = line.split(",");
return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
});
// 开窗测试
// 1. 增量聚合函数
DataStream<Integer> resultStream = dataStream.keyBy("id")
// .countWindow(10, 2);
// .window(EventTimeSessionWindows.withGap(Time.minutes(1)));
// .window(TumblingProcessingTimeWindows.of(Time.seconds(15)))
.timeWindow(Time.seconds(15))
//输入类型(IN)、累加器类型(ACC)和输出类型(OUT)
.aggregate(new AggregateFunction<SensorReading, Integer, Integer>() {
@Override
public Integer createAccumulator() {
return 0;
}
@Override
public Integer add(SensorReading value, Integer accumulator) {
return accumulator + 1;
}
@Override
public Integer getResult(Integer accumulator) {
return accumulator;
}
@Override
public Integer merge(Integer a, Integer b) {
return a + b;
}
});
// 2. 全窗口函数
SingleOutputStreamOperator<Tuple3<String, Long, Integer>> resultStream2 = dataStream.keyBy("id")
.timeWindow(Time.seconds(15))
// .process(new ProcessWindowFunction<SensorReading, Object, Tuple, TimeWindow>() {
// })
.apply(new WindowFunction<SensorReading, Tuple3<String, Long, Integer>, Tuple, TimeWindow>() {
@Override
// tuple 为窗口的key
// window 为窗口元数据的包装
public void apply(Tuple tuple, TimeWindow window, Iterable<SensorReading> input, Collector<Tuple3<String, Long, Integer>> out) throws Exception {
String id = tuple.getField(0);
Long windowEnd = window.getEnd();
Integer count = IteratorUtils.toList(input.iterator()).size();
out.collect(new Tuple3<>(id, windowEnd, count));
}
});
// 3. 其它可选API
OutputTag<SensorReading> outputTag = new OutputTag<SensorReading>("late") {
};
SingleOutputStreamOperator<SensorReading> sumStream = dataStream.keyBy("id")
.timeWindow(Time.seconds(15))
// .trigger()
// .evictor()
.allowedLateness(Time.minutes(1))
.sideOutputLateData(outputTag)
.sum("temperature");
sumStream.getSideOutput(outputTag).print("late");
resultStream2.print();
env.execute();
}
}
- 增量函数和全窗口函数结合使用
ReduceFunction/AggregateFunction和ProcessWindowFunction结合使用,分配到某个窗口的元素将被提前聚合,而当窗口的trigger触发时,也就是窗口收集完数据关闭时,将会把聚合结果发送到ProcessWindowFunction中,这时Iterable参数将会只有一个值,就是前面聚合的值
input
.keyBy(...)
.timeWindow(...)
.reduce(
incrAggregator: ReduceFunction[IN],
function: ProcessWindowFunction[IN, OUT, K, W])
input
.keyBy(...)
.timeWindow(...)
.aggregate(
incrAggregator: AggregateFunction[IN, ACC, V],
windowFunction: ProcessWindowFunction[V, OUT, K, W])
5. 其它可选 API
- trigger() —— 触发器:定义 窗口什么时候关闭,触发计算并输出结果
- evitor() —— 移除器:定义移除某些数据的逻辑
- allowedLateness() —— 允许处理迟到的数据
设置窗口的延迟关闭的时间。
设置此目的:是为了hold住watermark没有hold住的迟到数据?
原理:如果没设置 那么到达窗口关闭时间 如果是全窗口那么触发计算,输出结果 如果是增量聚合 输出增量聚合的结果 销毁窗口。如果后面该窗口的数据 来了 那么就会被丢弃。为了避免这种现象,设置窗口延迟关闭时间,达到窗口关闭时间 输出聚合的结果,先不关闭窗口 在之后来了该窗口的数据,来一条聚合一条输出。
- sideOutputLateData() —— 将迟到的数据放入侧输出流
如果真有那种少量特别晚的数据 直接单独收集起来。特别处理
- getSideOutput() —— 获取侧输出流
6. Flink中的时间语义
-
6.1 Flink中的时间语义有哪些?
- 在 Flink 的流式处理中,会涉及到时间的不同概念
- Event time 数据中携带的时间
- Ingestion time 进入Flink系统的时候
- Processing time 处理时间
- 在 Flink 的流式处理中,会涉及到时间的不同概念
-
6.2 Flink中哪些时间比较重要呢?
- 在 Flink 的流式处理中,绝大部分的业务都会使用 eventTime,一般只在
eventTime 无法使用时,才会被迫使用 ProcessingTime 或者 IngestionTime。
- 在 Flink 的流式处理中,绝大部分的业务都会使用 eventTime,一般只在
-
6.3 设置Event Time
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment
// 从调用时刻开始给 env 创建的每一个 stream 追加时间特征
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
数据在网络中分布式传输 可能由于先发送的事件后到,所以在处理数据的时候可能会丢或者处理多余数据。例如一下图 以事件发生时间5s开一个窗口, 事件时间是5s的时候 准备关闭窗口 输出结果时,但是由于网络传输的原因 3 和2 的数据还没有接受。此时关闭窗口 那么就2 和 3 的窗口就不会被处理。
7. watermark
-
7.1 watermark是什么
- 是一个时间戳 表示小于这个时间戳的事件已经到达了
- 可以在数据源定义 也可以在任何算子中定义
- 是一个StreamElement 和普通数据一起在算子之间传递
- 触发窗口计算
什么时候会触发窗口计算
watermark=当前数据的最大事件时间-延迟时间 -
7.2 watermark 就一定能保证延迟的数据不影响最终结果吗?
假如窗口已经关闭了 此时又来了 属于该窗口的数据,那么此时数据将被丢弃掉 印象最终的结果。为了解决这种问题,所以可设置窗口延迟关闭时间,当窗户到达设置的窗口时间大小后,输出结果 但是并不关闭窗口,在一定的延迟时间内,如果有该窗口的数据 每来一条 处理一条。如果没有延迟窗口没有兜住,那么就将结果扔到侧输出流中.
-
7.3 watermark的特点
- watermark 必须单调递增,以确保任务的事件时间时钟在向前推进,而不是在后退
-
7.4 生成watermark的两种方式
- AssignerWithPeriodicWatermarks : 周期性生成watermark ,默认是每隔200毫秒生成一个watermark
// 可以自定义
env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime);
env.getConfig().setAutoWatermarkInterval(200L);
// 默认为200ms
public void setStreamTimeCharacteristic(TimeCharacteristic characteristic) {
this.timeCharacteristic = Preconditions.checkNotNull(characteristic);
if (characteristic == TimeCharacteristic.ProcessingTime) {
getConfig().setAutoWatermarkInterval(0);
} else {
getConfig().setAutoWatermarkInterval(200);
}
}
生成原理:
假如设置5s生成watermark 那么flink每隔5s会去调用getCurrentWatermark方法 判断在上一个watermark之后产生的数据中最大事件时间减延迟时间为当前watermark,如果当前watermark大于上一个生成的事件戳插入到数据中。否则不插入。
// 对于乱序数据 源码中生成watermark的方法
public final Watermark getCurrentWatermark() {
// this guarantees that the watermark never goes backwards.
// currentMaxTimestamp 上一个watermark之后最大的事件时间
long potentialWM = currentMaxTimestamp - maxOutOfOrderness;
if (potentialWM >= lastEmittedWatermark) {
lastEmittedWatermark = potentialWM;
}
return new Watermark(lastEmittedWatermark);
}
@Override
// 对于有序数据 watermark=currentTimestamp-1
// 延迟1毫秒
public final Watermark getCurrentWatermark() {
return new Watermark(currentTimestamp == Long.MIN_VALUE ? Long.MIN_VALUE : currentTimestamp - 1);
}
- AssignerWithPunctuatedWatermarks : 断点性生成,每个数据来了 都去提取数据中的时间戳 与上一个watermark比较,判断要不要把新生成的watermark插入到数据流中
DataStream<SensorReading> dataStream = inputStream.map(line -> {
String[] fields = line.split(",");
return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
})
// 升序数据根据数据提取事件时间 根据事件事件设置watermark 数据有顺序就不需要设置watemark
// .assignTimestampsAndWatermarks(new AscendingTimestampExtractor<SensorReading>() {
// @Override
// public long extractAscendingTimestamp(SensorReading element) {
// return element.getTimestamp() * 1000L;
// }
// })
// 乱序数据设置时间戳和watermark
.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<SensorReading>(Time.seconds(2)) {
@Override
public long extractTimestamp(SensorReading element) {
return element.getTimestamp() * 1000L;
}
});
OutputTag<SensorReading> outputTag = new OutputTag<SensorReading>("late") {
};