flink窗口计算
1、时间概念
1.1、事件时间(Event Time)
事件时间是指每个事件或元素在其生产设备上产生的时间。该时间通常在它们进入Flink之前就已经嵌入在事件中,并且可以从每个事件中提取事件时间戳。有了事件时间,基于窗口的聚合(事件时间列上的一种特殊的分组和聚合),每个时间窗口是一个组,每一行数据可以属于多个窗口。
1.2、处理时间(Processing Time):
处理时间是指正在执行相应事件机器的系统时间。当流式程序按处理时间运行时,所有基于时间的操作都将使用数据处理机器的系统时间。在分布式和异步环境中,处理时间不能提供确定性,因为它容易受到记录到达系统的速度以及记录在系统内部操作之间流动的速度的影响。
2、窗口分类
2.1、window和windowAll
窗口是处理无限流的核心。窗口将流分成有限大小的“桶”,我们可以在其上应用算子计算。
window()和windowAll()都需要传入一个窗口分配器,负责分配事件到相应的窗口。
window()
方法:适用于KeyedStream,即通过keyBy对数据进行分组后的流。每个分组(Keyed Stream)可以独立进行窗口操作,因此可以并行处理,提高处理效率。具有相同Key的数据元素会被发送到同一个Task中进行处理。
windowAll()
方法:适用于DataStream,即未通过keyBy分组的流。所有数据都会发送到下游算子的单个实例上,因此其并行度为1,性能相对较差。
Flink窗口程序的大致骨架结构:
KeyedStream:
非KeyedStream:
代码示例:
// 1. 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 2. 读取数据源
DataStream<String> textStream = env.socketTextStream("localhost", 9999, "\n");
// 3. 数据转换
DataStream<Tuple2<String, Integer>> wordCountStream = textStream
.assignTimestampsAndWatermarks(MyWatermark.create())
// 对数据源的单词进行拆分,每个单词记为1,然后通过out.collect将数据发射到下游算子
.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
for (String word : value.split("\\s")) {
out.collect(new Tuple2<>(word, 1));
}
}
}
)
// 对单词进行分组
.keyBy(value -> value.f0)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
// 对某个组里的单词的数量进行滚动相加统计
.reduce((a, b) -> new Tuple2<>(a.f0, a.f1 + b.f1));
// 4. 数据输出。字节输出到控制台
wordCountStream.print("WindowWordCount01 ======= ").setParallelism(1);
// 5. 启动任务
env.execute(WindowWordCount01.class.getSimpleName());
DataStream<Tuple2<String, Integer>> wordCountStream = textStream
.assignTimestampsAndWatermarks(MyWatermark.create())
.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
for (String word : value.split("\\s")) {
out.collect(new Tuple2<>(word, 1));
}
}
}
)
.windowAll(TumblingEventTimeWindows.of(Time.seconds(5)))
.reduce((a, b) -> new Tuple2<>(a.f0, a.f1 + b.f1));
2.2、窗口可以分类
Flink的窗口可以分为滚动窗口、滑动窗口、会话窗口、全局窗口。
滚动窗口:滚动窗口分配器将每个元素分配给指定大小的窗口,滚动窗口具有固定的大小,并且不重叠。例如,指定大小为5分钟的滚动窗口,则每隔5分钟将启动一个新窗口。如下图:
代码示例:
DataStream<Tuple2<String, Integer>> wordCountStream = textStream
.assignTimestampsAndWatermarks(MyWatermark.create())
// 对数据源的单词进行拆分,每个单词记为1,然后通过out.collect将数据发射到下游算子
.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
for (String word : value.split("\\s")) {
out.collect(new Tuple2<>(word, 1));
}
}
}
)
// 对单词进行分组
.keyBy(value -> value.f0)
// 基于事件时间的滚动窗口
// .window(TumblingEventTimeWindows.of(Time.seconds(5)))
// 基于处理时间的滚动窗口
// .window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
// 对某个组里的单词的数量进行滚动相加统计
.reduce((a, b) -> new Tuple2<>(a.f0, a.f1 + b.f1));
**滑动窗口:**滑动窗口分配器将元素分配给固定长度的窗口。与滚动窗口类似,滑动窗口的大小由指定参数配置,但是增加了滑动步长(Slide)参数,相当于以指定步长不断向前滑动。
使用滑动窗口时,需要设置窗口大小和滑动步长两个参数。因此,如果滑动窗口的步长小于窗口大小,则滑动窗口可以重叠。这种情况下,元素被分配给多个窗口。如果滑动窗口的步长大于窗口大小时,有些元素可能会丢失。
例如,每隔5分钟需要对最近10分钟的数据进行计算,就可以设置窗口大小为10分钟,滑动步长为5分钟。这样,每隔5分钟就会得到一个窗口,其中包含最近10分钟内到达的数据。如下图:
代码示例:
DataStream<Tuple2<String, Integer>> wordCountStream = textStream
.assignTimestampsAndWatermarks(MyWatermark.create())
// 对数据源的单词进行拆分,每个单词记为1,然后通过out.collect将数据发射到下游算子
.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
for (String word : value.split("\\s")) {
out.collect(new Tuple2<>(word, 1));
}
}
}
)
// 对单词进行分组
.keyBy(value -> value.f0)
// 基于事件时间的滑动窗口
// .window(SlidingEventTimeWindows.of(Time.seconds(5), Time.seconds(1)))
// 基于处理时间的滑动窗口
// .window(SlidingProcessingTimeWindows.of(Time.seconds(5), Time.seconds(1)))
// 对某个组里的单词的数量进行滚动相加统计
.reduce((a, b) -> new Tuple2<>(a.f0, a.f1 + b.f1));
**会话窗口:**会话窗口分配器按活动会话对事件进行分组。与滚动窗口和滑动窗口相比,会话窗口不重叠且没有固定的开始和结束时间。相反,当会话窗口在一定时间段内未收到事件时,即发生不活动的间隙时,窗口将关闭。
**全局窗口:**全局窗口分配器将所有具有相同Key的事件分配给同一个全局窗口。由于全局窗口没有自然的窗口结束时间,因此使用全局窗口需要指定触发器。
3、窗口函数
事件被窗口分配器分配到窗口后,接下来需要指定想要在每个窗口上执行的计算函数(即窗口函数),以便对窗口内的数据进行处理。
3.1、增量计算和全量计算
Flink计算窗口数据分为增量计算和全量计算。Flink提供的窗口函数有ReduceFunction、AggregateFunction、ProcessWindowFunction。
增量计算:每当窗口中有新数据到达时,立即对新数据进行处理,并将结果累积或更新。它通过对新增数据进行增量更新,减少对整个窗口数据的重复扫描。比如ReduceFunction、AggregateFunction。
**全量计算:**全量计算是指在窗口结束时,对窗口内的所有数据进行一次性计算。它会收集整个窗口的数据,等待所有数据到达后统一处理。例如对整个窗口数据排序取TopN,比如ProcessWindowFunction。
3.2、函数功能
ReduceFunction
:适合简单聚合操作(如计数、求和等)。
AggregateFunction
:需要增量计算但逻辑复杂的场景(如平均值计算)。
ProcessWindowFunction
:需要访问窗口上下文或完整窗口数据的场景(如 Top N、全局排序)。
带增量聚合的processWindowFunction:由于ProcessWindowFunction是全量计算函数,如果既要获得窗口信息又要进行增量聚合,则可以将ProcessWindowFunction与ReduceFunction或AggregateFunction结合使用。
Flink算子apply和process都是全量聚合,即会等窗口的所有元素到达后做全量计算。
Flink算子reduce是增量聚合,即每来一个元素就聚合计算一次。
process和processWindowFunction使用,比如统计5秒钟内每个单词的次数,代码示例:
DataStream<Tuple2<String, Integer>> wordCountStream = textStream
.assignTimestampsAndWatermarks(MyWatermark.create())
// 对数据源的单词进行拆分,每个单词记为1,然后通过out.collect将数据发射到下游算子
.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
for (String word : value.split("\\s")) {
out.collect(new Tuple2<>(word, 1));
}
}
}
)
// 对单词进行分组
.keyBy(value -> value.f0)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
// 清除器,用于清除超过evictionSec前的数据。防止整个窗口的数据量过大
// 对某个组里的单词的数量进行滚动相加统计
.process(new ProcessWindowFunction<Tuple2<String, Integer>, Tuple2<String, Integer>, String, TimeWindow>() {
@Override
public void process(String key, Context context, Iterable<Tuple2<String, Integer>> elements, Collector<Tuple2<String, Integer>> out) throws Exception {
int sum = 0;
for (Tuple2<String, Integer> element : elements) {
sum += element.f1;
}
out.collect(new Tuple2<>(key, sum));
}
});
继续使用上面的例子,统计窗口内,所有输入的单词的数量,示例如下:
DataStream<Tuple2<String, Integer>> wordCountStream = textStream
.assignTimestampsAndWatermarks(MyWatermark.create())
// 对数据源的单词进行拆分,每个单词记为1,然后通过out.collect将数据发射到下游算子
.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
for (String word : value.split("\\s")) {
out.collect(new Tuple2<>(word, 1));
}
}
}
)
// 对单词进行分组
.keyBy(value -> value.f0)
.window(TumblingEventTimeWindows.of(Time.seconds(10)))
// 对某个组里的单词的数量进行增量相加
.reduce((value1, value2) -> new Tuple2<>(value1.f0, value1.f1 + value2.f1))
// 在进行总数合并
.process(new ProcessFunction<Tuple2<String, Integer>, Tuple2<String, Integer>>() {
@Override
public void processElement(Tuple2<String, Integer> value, ProcessFunction<Tuple2<String, Integer>, Tuple2<String, Integer>>.Context ctx, Collector<Tuple2<String, Integer>> out) throws Exception {
value.f1 += value.f1;
out.collect(new Tuple2<>("合计", value.f1));
}
});
4、触发器
触发器(Trigger)决定了一个窗口的数据何时被窗口函数处理。每个窗口都有一个默认的触发器。如果默认的触发器不满足你的需求,可以使用trigger()指定一个自定义触发器。抽象类Trigger定义了触发器的基本方法,允许触发器对不同的事件做出反应。
主要方法:
onElement()
:每当一个元素到达窗口时触发,用于定义元素触发逻辑。
onEventTime()
:当事件时间进展到特定时间点时触发,用于定义基于事件时间的触发逻辑。
onProcessingTime()
:当处理时间进展到特定时间点时触发,用于定义基于处理时间的触发逻辑。
onMerge()
:合并窗口(如会话窗口)时触发,用于处理合并逻辑。
clear()
:当窗口被清理时触发,用于释放窗口的资源。
枚举类型 TriggerResult:
CONTINUE
:不触发计算,继续接收数据。
FIRE
:触发窗口计算,但不清空窗口状态。
FIRE_AND_PURGE
:触发窗口计算并清空窗口状态。
PURGE
:不触发计算,直接清空窗口状态。
内置触发器:
EventTimeTrigger
:基于事件时间触发,默认用于基于事件时间的窗口;在水位线(watermark)到达窗口结束时间时触发。
ProcessingTimeTrigger
:基于处理时间触发,默认用于基于处理时间的窗口;当系统时间到达窗口结束时间时触发。
CountTrigger
:当窗口中的元素数量达到指定阈值时触发。
PurgingTrigger
:触发窗口计算后立即清除窗口中的所有数据,从而释放内存资源。
ContinuousProcessingTimeTrigger
:定期基于处理时间触发,例如每隔 1 秒触发一次。
ContinuousEventTimeTrigger
:定期基于事件时间触发,例如每隔 1 分钟触发一次。
比如自定义一个触发器,在全局窗口内,对于统计输出的单词是1的的场景进行触发计算,代码示例:
private static class MyTrigger<T, W extends Window> extends Trigger<T, W> {
private final ValueStateDescriptor<T> stateDesc;
private MyTrigger(TypeSerializer<T> stateSerializer) {
stateDesc = new ValueStateDescriptor<>("last-element", stateSerializer);
}
@Override
public TriggerResult onElement(T element, long timestamp, W window, TriggerContext ctx) throws Exception {
Tuple2<String, Integer> elementValue = (Tuple2<String, Integer>) element;
ValueState<T> lastElementState = ctx.getPartitionedState(stateDesc);
if (lastElementState.value() == null) {
lastElementState.update(element);
return TriggerResult.CONTINUE;
}
// 此处状态描述器ValueState可以不使用
Tuple2<String, Integer> lastValue = (Tuple2<String, Integer>) lastElementState.value();
if (elementValue.f0.equals("1")) {
lastElementState.update(element);
return TriggerResult.FIRE;
}
return TriggerResult.CONTINUE;
}
@Override
public TriggerResult onProcessingTime(long time, W window, TriggerContext ctx) throws Exception {
return TriggerResult.CONTINUE;
}
@Override
public TriggerResult onEventTime(long time, W window, TriggerContext ctx) throws Exception {
return TriggerResult.CONTINUE;
}
@Override
public void clear(W window, TriggerContext ctx) throws Exception {
ctx.getPartitionedState(stateDesc).clear();
}
}
5、清除器
除了触发器之外,Flink的窗口还允许使用evictor()方法指定一个可选的清除器(Evictor)。使用清除器允许在触发器触发后,窗口函数执行之前或之后,从窗口中删除元素。清除器Evictor是一个接口,有evictBefore()和evictAfter()两个方法。
接着使用上面的例子,全局窗口,每隔30S清理一次窗口内的数据,代码示例:
DataStream<Tuple2<String, Integer>> wordCountStream = textStream
.assignTimestampsAndWatermarks(MyWatermark.create())
// 对数据源的单词进行拆分,每个单词记为1,然后通过out.collect将数据发射到下游算子
.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
for (String word : value.split("\\s")) {
out.collect(new Tuple2<>(word, 1));
}
}
}
)
// 对单词进行分组
.keyBy(value -> value.f0)
.window(GlobalWindows.create())
// 清除器,用于清除超过evictionSec前的数据。防止整个窗口的数据量过大
.evictor(TimeEvictor.of(Time.of(30, TimeUnit.SECONDS)))
.trigger(new WindowWordCount02.MyTrigger(textStream.getType().createSerializer(env.getConfig())))
// 对某个组里的单词的数量进行滚动相加统计
.reduce(new ReduceFunction<Tuple2<String, Integer>>() {
@Override
public Tuple2<String, Integer> reduce(Tuple2<String, Integer> a, Tuple2<String, Integer> b) throws Exception {
return new Tuple2<>(a.f0, a.f1 + b.f1);
}
});
这个例子执行时,可以看到,非1
的字符串不会触发窗口计算,同时,清除器会清理30秒之前的数据,比如,某一次输入后,到了30秒之后继续输出,会重新开始统计。某一次输入后,没到30S就继续输入,会把最近的30秒内的窗口所有数据合并统计。比如下图: