Flink的窗口机制
6.1.1 窗口概述
窗口window是用来处理无限数据集的有限块。窗口就是把流切成了有限大小的多个存储桶bucket
流处理应用中,数据是连续不断的,因此我们不能等所有的数据来了才开始处理,当然也可以来一条数据,处理一条数据,但是有时候我们需要做一些聚合类的处理,例如:在过去的一分钟内有多少用户点击了网页。这种情况下,就适合定义一个窗口,用来收集最近一分钟内的数据,并对这个窗口的数据进行计算。
6.1.2 窗口分类
基于时间的窗口(时间驱动)
基于元素的窗口(数据驱动)
keyBy之前的窗口函数:sensorDS.windowAll(),并行度只能是1
keyBy之后的窗口函数:sensorDS.window() ★
老版本写法:1.12标记为过时,不建议使用
sensorKS.timeWindow(Time.seconds(3)); // 一个参数,表示滚动窗口
sensorKS.timeWindow(Time.seconds(5),Time.seconds(2)); // 两个参数,表示滑动窗口
新版本写法:
//一个参数,表示滚动窗口
sensorKS.window(TumblingProcessingTimeWindows.of(Time.seconds(3)));
//两个参数,表示滑动窗口
sensorKS.window(SlidingProcessingTimeWindows.of(Time.seconds(5),Time.seconds(2)));
(1)基于时间的窗口
①滚动时间窗口(Tumbling Windows)
keyBy()之前的窗口函数、keyBy()之后的窗口函数
滚动时间窗口,有固定的窗口时间大小。比如一个10s的滚动窗口,从当前窗口开始算,每10s启动一个新的窗口。
window()传入的对象叫窗口分配器。
public class Flink01_TumblingWindows {
public static void main(String[] args) throws Exception {
//1 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(2);
//2 加载数据
SingleOutputStreamOperator<WaterSensor> sensorDS = env
.socketTextStream("hadoop102", 9999)
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] dats = value.split(",");
return new WaterSensor(
dats[0],
Long.valueOf(dats[1]),
Integer.valueOf(dats[2])
);
}
});
//TODO 3 时间窗口 - 滚动窗口TumblingWindows
//keyBy之前开窗,api都带有 'all',所有数据进入到一个窗口(并且并行度为1)
// AllWindowedStream<WaterSensor, TimeWindow> sensorWS = sensorDS.windowAll(TumblingProcessingTimeWindows.of(Time.seconds(10)));
//TODO 时间窗口 - 一般keyBy之后使用 => KeyedStream.window(窗口类型.of(时间参数))
KeyedStream<WaterSensor, String> sensorKS = sensorDS.keyBy(sensor -> sensor.getId());
WindowedStream<WaterSensor, String, TimeWindow> sensorWS = sensorKS.window(TumblingProcessingTimeWindows.of(Time.seconds(3)));
//TODO 4 开窗后,数据的处理
//keyBy之后开窗,对数据的处理
sensorWS
.process(new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
@Override
public void process(String s, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
long start = context.window().getStart();
long end = context.window().getEnd();
out.collect("==============================================\n" +
"窗口为:[" + start + "," + end + ")\n" +
// "数据:" + elements + "\n" +
"数据条数=" + elements.spliterator().estimateSize() + "\n" +
"=======================================\n\n");
}
})
.print();
//keyBy之前开窗,对数据的处理process(new ProcessAllWindowFunction)
// sensorWS
// .process(new ProcessAllWindowFunction<WaterSensor, String, TimeWindow>() {
// @Override
// public void process(Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
// long start = context.window().getStart();
// long end = context.window().getEnd();
// out.collect("==============================================\n" +
// "窗口为:[" + start + "," + end + ")\n" +
"数据:" + elements + "\n" +
// "数据条数=" + elements.spliterator().estimateSize() + "\n" +
// "=======================================\n\n");
//
// }
// })
// .print();
env.execute();
}
}
②滑动时间窗口(Sliding Windows)
SlidingProcessingTimeWindows.of(参数1,参数2)
,参数1是窗口时间长度,参数2是滑动步长,(参数3是步幅)滑动窗口,也有一个固定的窗口大小,另外还有一个滑动步长。
- 当滑动步长 = 窗口大小的时候,和滚动窗口就一样了
- 当滑动步长 < 窗口大小的时候,窗口会重叠,这种情况会出现数据分配到多个窗口的情况了
例如:滑动窗口10s,滑动步长5s
public class Flink02_SlidingWindows {
public static void main(String[] args) throws Exception {
//1 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(2);
//2 加载数据
SingleOutputStreamOperator<WaterSensor> sensorDS = env
.socketTextStream("hadoop102", 9999)
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] dats = value.split(",");
return new WaterSensor(
dats[0],
Long.valueOf(dats[1]),
Integer.valueOf(dats[2])
);
}
});
KeyedStream<WaterSensor, String> sensorKS = sensorDS.keyBy(sensor -> sensor.getId());
//TODO 滑动时间窗口
WindowedStream<WaterSensor, String, TimeWindow> sensorWS = sensorKS.window(SlidingProcessingTimeWindows.of(Time.seconds(5), Time.seconds(3)));
sensorWS
.process(new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
@Override
public void process(String s, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
long start = context.window().getStart();
long end = context.window().getEnd();
out.collect("==============================================\n" +
"窗口为:[" + start + "," + end + ")\n" +
// "数据:" + elements + "\n" +
"数据条数=" + elements.spliterator().estimateSize() + "\n" +
"=======================================\n\n");
}
})
.print();
env.execute();
}
}
③会话窗口(Session Windows)
会话窗口:定义一个时间间隔Gap,如果达到时间间隔,没有新的数据来的时候,那么之前的数据就会划分为一个窗口。
- 静态Gap:时间间隔 不变
比如Gap=10,只要10s没有接收到数据,那么就关闭这个窗口,有新的数据来,会再开启一个新的窗口
只要10s内有新的数据来,那么这个10s会被刷新。- 动态Gap:时间间隔 动态改变
Gap是一个动态的值,也是达到这个动态的值,那么就关闭窗口,有新的数据来,会再开启一个新窗口
只要动态值内 有新的数据来,那么这个动态的值会被刷新。会话窗口没有固定的开启和关闭时间。在Flink内部, 每到达一个新的元素都会创建一个新的会话窗口, 如果这些窗口彼此相距比较定义的gap小, 则会对他们进行合并. 为了能够合并, 会话窗口算子需要合并触发器和合并窗口函数:
ReduceFunction
,AggregateFunction
, orProcessWindowFunction
静态Gap
public class Flink03_SessionWindows {
public static void main(String[] args) throws Exception {
//1 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(3);
//2 读取数据
SingleOutputStreamOperator<WaterSensor> sensorDS = env
.socketTextStream("hadoop102", 9999)
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] dats = value.split(",");
return new WaterSensor(
dats[0],
Long.valueOf(dats[1]),
Integer.valueOf(dats[2])
);
}
});
KeyedStream<WaterSensor, String> sensorKS = sensorDS.keyBy(sensor -> sensor.getId());
//3 TODO 会话窗口 - SessionWindow 静态Gap,固定会话时间间隔
WindowedStream<WaterSensor, String, TimeWindow> sensorWS = sensorKS.window(ProcessingTimeSessionWindows.withGap(Time.seconds(10)));
//4 TODO 开窗后对数据的处理
sensorWS
.process(new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
@Override
public void process(String s, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
long start = context.window().getStart();
long end = context.window().getEnd();
out.collect("==============================================\n" +
"窗口为:[" + start + "," + end + ")\n" +
// "数据:" + elements + "\n" +
"数据条数=" + elements.spliterator().estimateSize() + "\n" +
"=======================================\n\n");
}
})
.print();
env.execute();
}
}
动态Gap
public class Flink04_SessionWindows {
public static void main(String[] args) throws Exception {
//1 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(3);
//2 读取数据
SingleOutputStreamOperator<WaterSensor> sensorDS = env
.socketTextStream("hadoop102", 9999)
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] dats = value.split(",");
return new WaterSensor(
dats[0],
Long.valueOf(dats[1]),
Integer.valueOf(dats[2])
);
}
});
KeyedStream<WaterSensor, String> sensorKS = sensorDS.keyBy(sensor -> sensor.getId());
//TODO 3 时间窗口-SessionWindows - 动态Gap,动态会话间隔
WindowedStream<WaterSensor, String, TimeWindow> sensorWS = sensorKS.window(ProcessingTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor<WaterSensor>() {
@Override
public long extract(WaterSensor element) {
//TODO 动态:根据watersensor的ts字段去动态调整会话时间间隔
return element.getTs() * 1000L;
}
}));
//4 开窗后 对数据的处理
sensorWS
.process(new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
@Override
public void process(String s, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
long start = context.window().getStart();
long end = context.window().getEnd();
out.collect("==============================================\n" +
"窗口为:[" + start + "," + end + ")\n" +
// "数据:" + elements + "\n" +
"数据条数=" + elements.spliterator().estimateSize() + "\n" +
"=======================================\n\n");
}
})
.print();
env.execute();
}
}
④全局窗口(Global Windows)
分配相同key的所有元素进入到同一个Global Window(全局窗口)中,那么怎么判断窗口结束了?怎样重新启动一个新的窗口呢?
所以全局窗口需要自定义触发器。
其实上面的TrumblingWindows、SlidingWindows、SessionWindows都有自己的触发器。
//TODO 全局窗口
sensorKS
.window(GlobalWindows.create())
.trigger(Trigger类型)
(2)基于元素个数的窗口
按照指定的数据条数生成window,与时间无关。
①滚动窗口
countWindow(参数1)
滚动计数窗口,参数1是窗口每到达指定条数据,就会关闭这个窗口,开启一个新的窗口
②滑动窗口
countWindow(参数1,参数2)
滑动计数窗口,参数1是窗口大小,参数2是滑动步长,单位是数据的条数。
public class Flink06_CountWindow {
public static void main(String[] args) throws Exception {
//1 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(3);
//2 读取数据
SingleOutputStreamOperator<WaterSensor> sensorDS = env
.socketTextStream("hadoop102", 9999)
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] dats = value.split(",");
return new WaterSensor(
dats[0],
Long.valueOf(dats[1]),
Integer.valueOf(dats[2])
);
}
});
KeyedStream<WaterSensor, String> sensorKS = sensorDS.keyBy(sensor -> sensor.getId());
//TODO 计数窗口
// 窗口并不是以第一条数据为起始点的,而是有自己的划分逻辑
// 窗口的触发,依赖于滑动步长,每经过一个滑动步长,都有一个窗口关闭并输出
sensorKS
// .countWindow(5) //滚动窗口
.countWindow(5, 2) //滑动窗口
.sum("vc")
.print();
env.execute();
}
}
(3)Window Function
窗口函数就是对一个窗口内的数据的操作。
分为聚合函数:reduceFunction和AggregateFunction,来一条聚合一条,只在窗口关闭时才会输出
全窗口函数:ProcessWindowFunction,来一条保存一条,只有在窗口关闭的时候才聚合,输出结果
①ReduceFunction
注意:这里的reduce()是WindowedStream的;前面的reduce()是键控流KeyedStream的
public class Flink07_WindowFunction_ReduceFunction {
public static void main(String[] args) throws Exception {
//1 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(3);
//2 读取数据
SingleOutputStreamOperator<WaterSensor> sensorDS = env
.socketTextStream("hadoop102", 9999)
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] dats = value.split(",");
return new WaterSensor(
dats[0],
Long.valueOf(dats[1]),
Integer.valueOf(dats[2])
);
}
});
KeyedStream<WaterSensor, String> sensorKS = sensorDS.keyBy(sensor -> sensor.getId());
//3 开启滚动时间窗口
WindowedStream<WaterSensor, String, TimeWindow> sensorWS = sensorKS.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));
//4 TODO 增量函数ReduceFunction
SingleOutputStreamOperator<WaterSensor> resultDS = sensorWS.reduce(new ReduceFunction<WaterSensor>() {
@Override
public WaterSensor reduce(WaterSensor value1, WaterSensor value2) throws Exception {
System.out.println(value1 + " <=========> " + value2);
return new WaterSensor(value1.getId(), 1L, value1.getVc() + value2.getVc());
}
});
//5 打印结果
resultDS.print();
env.execute();
}
}
②AggregateFunction
AggregateFunction比ReduceFunction更加灵活。因为reduceFunction的输入和输出类型必须是一样的。
- AggregateFunction<泛型1,泛型2,泛型3>,泛型1是输入类型,泛型2是累加器结果类型,泛型3是输出类型。
- 需要重写四个方法:累加器的初始化、聚合逻辑、获取结果、会话窗口的合并窗口。
public class Flink08_WindowFunction_AggregateFunction {
public static void main(String[] args) throws Exception {
//1 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(3);
//2 读取数据
SingleOutputStreamOperator<WaterSensor> sensorDS = env
.socketTextStream("hadoop102", 9999)
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] dats = value.split(",");
return new WaterSensor(
dats[0],
Long.valueOf(dats[1]),
Integer.valueOf(dats[2])
);
}
});
KeyedStream<WaterSensor, String> sensorKS = sensorDS.keyBy(sensor -> sensor.getId());
//3 开启滚动时间窗口
WindowedStream<WaterSensor, String, TimeWindow> sensorWS = sensorKS.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));
//TODO 4 增量函数AggregateFunction
sensorWS.aggregate(new AggregateFunction<WaterSensor, Integer, String>() {
@Override
public Integer createAccumulator() {
//累加器的初始化
return 0;
}
@Override
public Integer add(WaterSensor value, Integer accumulator) {
//聚合逻辑
return accumulator + 1; //每来一条数据就+1
}
@Override
public String getResult(Integer accumulator) {
//获取结果
return accumulator.toString();
}
@Override
public Integer merge(Integer a, Integer b) {
//合并窗口,注意,只有会话窗口才会调用
return a + b;
}
})
.print();
env.execute();
}
}
③ProcessWindowFunction
全窗口函数-process(new ProcessWindowFunction)
public class Flink09_WindowFunction_ProcessFunction {
public static void main(String[] args) throws Exception {
//1 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(3);
//2 读取数据
SingleOutputStreamOperator<WaterSensor> sensorDS = env
.socketTextStream("hadoop102", 9999)
.map(new MapFunction<String, WaterSensor>() {
@Override
public WaterSensor map(String value) throws Exception {
String[] dats = value.split(",");
return new WaterSensor(
dats[0],
Long.valueOf(dats[1]),
Integer.valueOf(dats[2])
);
}
});
KeyedStream<WaterSensor, String> sensorKS = sensorDS.keyBy(sensor -> sensor.getId());
//3 开启滚动时间窗口
WindowedStream<WaterSensor, String, TimeWindow> sensorWS = sensorKS.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));
//TODO 4 全窗口函数
//泛型1输入类型,泛型2输出类型,泛型3分组key的类型,泛型4窗口类型
sensorWS.process(new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
@Override
public void process(String s, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
//参数1分组key,参数2上下文,参数3数据(可迭代类型,保存多个数据),参数4采集器
out.collect("key=" + s + "\n" +
"数据为:" + elements + "\n" +
"数据条数:" + elements.spliterator().estimateSize() + "\n" +
"窗口为:【" + context.window().getStart() + ", " + context.window().getEnd() + "】\n" +
"=======================================================\n\n");
}
})
.print();
env.execute();
}
}
6.1.3 窗口源码分析
以 事件时间的 滚动窗口为例
(1)窗口怎样划分?开始时间、结束时间?
1)开始时间 = timestamp - (timestamp - offset + windowSize) % windowSize
对 窗口长度 取整数倍(以1970年1月1日0点 为基准 => 因为是时间戳)
并不是以第一条数据 作为窗口的 起始点
2) 结束时间 = 开始时间 + 窗口长度
比如:
事件时间=1s,窗口大小为10s,那么窗口就是[0-10)
事件时间=11s,窗口大小为10s,那么窗口就是[10-21)
(2)窗口为什么是左闭右开?
属于窗口的最大时间戳: maxTimestamp = end - 1ms
=> [0,10) => 属于本窗口的最大时间戳为, 10s -1ms = 9999ms
=> 所以10s这条数据,不属于本窗口,所以是开区间
(3)窗口什么时候触发计算?
--事件时间触发器:EventTimeTrigger
watermark >= 窗口的最大时间戳 就fire触发计算
--窗口触发分析:
window.maxTimestamp() <= ctx.getCurrentWatermark()
watermark >= 窗口的最大时间戳 \
=> 比如,[0,10)的窗口,当 watermark >= 10s - 1ms = 9999ms时触发
=> 此时,watermark = 事件时间 - 等待3s - 1ms => 事件时间 = 13s时触发
(4)窗口的生命周期:什么时候创建的窗口?
4、窗口的生命周期: 什么时候创建的窗口?
1)属于本窗口的第一条数据来的时候,new出来的
2)属于本窗口的每条数据来的时候,都会new,但是集合是 singleton类型,不会重复存放 => 也就是说,只会有一个窗口对象
--详细解释:
return Collections.singletonList(new TimeWindow(start, start + size));
=> 每来一条数据,都会 new一个窗口对象
=> 将窗口对象,放入一个 SingletonList,是单例的,所以不会重复
=> 窗口结束时间 end = 计算出来的 start + 窗口长度
(5)窗口的生命周期:什么时候关闭窗口?
窗口结束时间 + 等待时间
5、窗口的生命周期: 什么时候关闭窗口?
窗口关闭时间 = 窗口最大时间戳 + allowedLateness
注意:如果 allowedLateness = 0(不设置),那么触发和关窗的时间一样
(6)详细分析过程
详细分析过程:
WindowOperator
=> assignWindows()
long start = TimeWindow.getWindowStartWithOffset(timestamp, (globalOffset + staggerOffset) % size, size);
=> timestamp - (timestamp - offset + windowSize) % windowSize
=> 1 - (1 -0 + 10) % 10 => start = 0s
=> 7 - (7 -0 + 10) % 10 => start = 0s
=> 12 - (12 - 0 + 10) % 10 => start = 10s
=> 这个算法,相当于,对 窗口长度 取整数倍(以1970年1月1日0点 为基准 => 因为是时间戳)
=> 并不是以第一条数据 作为窗口的 起始点
return Collections.singletonList(new TimeWindow(start, start + size));
=> 每来一条数据,都会 new一个窗口对象
=> 将窗口对象,放入一个 SingletonList,是单例的,所以不会重复
=> 窗口结束时间 end = 计算出来的 start + 窗口长度
// Gets the largest timestamp that still belongs to this window.
// 获取属于本窗口的 最大时间戳
public long maxTimestamp() {
return end - 1;
}
=> [0,10) => 属于本窗口的最大时间戳为, 10s -1ms = 9999ms => 所以10s这条数据,不属于本窗口,所以是开区间
=> long cleanupTime = window.maxTimestamp() + allowedLateness;
return cleanupTime >= window.maxTimestamp() ? cleanupTime : Long.MAX_VALUE;
=》 窗口关闭时间 = 窗口最大时间戳 + allowedLateness
窗口触发分析:
window.maxTimestamp() <= ctx.getCurrentWatermark()
watermark >= 窗口的最大时间戳 \java
=> 比如,[0,10)的窗口,当 watermark >= 10s - 1ms = 9999ms时触发
=> 此时,watermark = 事件时间 - 等待3s - 1ms => 事件时间 = 13s时触发
*/
6.2 Keyed & None-Keyed Windows
在用window前首先需要确认应该是在keyBy前的流上用,还是在keyBy后的流上用。
- 在keyBy前的流上用:流的并行度只能是1,所有的窗口逻辑只能在一个task上执行。
- 在keyBy后的流上用:窗口计算并行的运用在多个task上,每个分组都有自己的单独窗口。