一、窗口
1、概念
flink流式计算是一种被设计用于处理无限数据集的数据处理引擎,而无限数据集是指一种不断增长的本质上无限的数据集,而 窗口(window) 是一种切割无限数据为有限块进行处理的手段。窗口并不是静态准备好的,而是动态创建的,当有落在这个窗口区间范围的数据到达时,才创建对应的窗口。
2、窗口分类
时间窗口: 时间到了就触发窗口计算
计数窗口: 窗口内的数据条数达到阈值就触发计算
会话窗口: 以数据的间隔时间(活跃度)为驱动,按 "会话超时" 切分数据流 ------ 当某段时间内没有新数据到达,就认为当前会话结束,关闭窗口并触发计算。窗口没有固定长度。
滚动窗口: 窗口大小固定,窗口和窗口之间无缝衔接、互不重叠,每条数据只会属于一个窗口。
滑动窗口: 窗口大小固定,但可以设置滑动步长,每经过一个步长都有一个窗口触发输出。窗口和窗口之间可以重叠,每条数据可能属于多个窗口。
全局窗口: 所有数据(同一个 key 的)都被分到同一个全局窗口里,默认永远不会关闭,也不会自动触发计算,必须配合自定义触发器(Trigger)才能触发。
3、窗口分配器类型与窗口函数Reduce
java
public class WindowReduceDemo{
public static void main(String[] args) throws Exception {
//创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<String> socketdata = env.socketTextStream("node-1",8888);
SingleOutputStreamOperator<WaterSensor> data = socketdata.map(new WaterSensorMapFunction());
KeyedStream<WaterSensor,String> dataKS = data .keyBy(x -> x.getId());
/*指定窗口分配器:指定用哪一种窗口 --- 时间or计数? 滚动,滑动,会话?
* 1.没有keyBy的窗口,窗口内的所有数据进入到同一个子任务,并行度只能为1
* data.windowAll()
* 2.有keyBy的窗口,每个key上都定义了一组窗口,各自独立的进行统计计算
* 基于时间的
* dataKS.window(TumblingProcessingTimeWindows.of(Time.seconds(10))); //滚动窗口,窗口长度为10s
* dataKS.window(SlidingProcessingTimeWindows.of(Time.seconds(10),Time.seconds(2)));//滑动窗口,窗口长度为10s,滑动步长2s
* dataKS.window(ProcessingTimeSessionWindows.withGap(Time.seconds(5)));//会话窗口,超时时间为间隔5s
* 基于计数的
* dataKS.countWindow(5) //滚动窗口,窗口长度为5个元素
* dataKS.countWindow(5,2) //滑动窗口,窗口长度为5个元素,滑动步长为2个元素
* dataKS.window(GlobalWindows.create()) //全局窗口,计数窗口的底层就是用的这个,需要自定义的时候才会用
*/
WindowedStream<WaterSensor,String,TimeWindow> dataWS = dataKS.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));
//窗口函数:增量聚合reduce,第一条数据来不进入reduce方法
SingleOutputStreamOperator<WaterSensor> reduce = dataWS.reduce(
new ReduceFunction<WaterSensor>(){
@Override
public WaterSensor reduce(WaterSensor value1 , WaterSensor value2) throws Exception {
return new WaterSensor(value1.id,value2.ts,value1.vc+value2.vc);
}
}
);
reduce.print();
env.execute();
}
}
public class WaterSensorMapFunction implements MapFunction<String,WaterSensor>{
@Override
public WaterSensor map(String value) throws Exception {
String[] datas = value.split(",");
return new WaterSensor(datas[0],Long.valueOf(datas[1]),Integer.valueOf(datas[2]));
}
}
4、窗口函数Aggregate
java
public class WindowAggregateDemo{
public static void main(String[] args) throws Exception {
//创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<String> socketdata = env.socketTextStream("node-1",8888);
SingleOutputStreamOperator<WaterSensor> data = socketdata.map(new WaterSensorMapFunction());
KeyedStream<WaterSensor,String> dataKS = data .keyBy(x -> x.getId());
//窗口分配器
WindowedStream<WaterSensor,String,TimeWindow> dataWS = dataKS.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));
/*窗口函数:增量聚合Aggregate
* 1.属于本窗口的第一条数据来,创建窗口,创建累加器
* 2.来一条计算一条,调用一次add方法
* 3.窗口输出时调用一次getresult方法
* 4.输入,中间累加器,输出类型可以不一样,非常灵活
*/
SingleOutputStreamOperator<String> aggregate = dataWS.aggregate(
/*类型一:输入数据的类型
* 类型二:累加器的类型,存储的中间的计算结果的类型
* 类型三:输出的类型
*/
new AggregateFunction<WaterSensor,Integer,String>(){
//创建累加器,初始化累加器
@Override
public Integer createAccumulator() {
return 0;
}
//聚合逻辑
@Override
public Integer add(WaterSensor value,Integer accumulator) {
return accumulator + value.getVc();
}
//获取最终结果,窗口触发时输出
@Override
public String getResult(Integer accumulator) {
return accumulator.toString();
}
@Override
public Integer merge(Integer a,Integer b) {
//只有会话窗口才会用到
return null;
}
}
);
aggregate.print();
env.execute();
}
}
5、全窗口函数
java
public class WindowProcessDemo{
public static void main(String[] args) throws Exception {
//创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<String> socketdata = env.socketTextStream("node-1",8888);
SingleOutputStreamOperator<WaterSensor> data = socketdata.map(new WaterSensorMapFunction());
KeyedStream<WaterSensor,String> dataKS = data .keyBy(x -> x.getId());
WindowedStream<WaterSensor,String,TimeWindow> dataWS = dataKS.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));
//全窗口函数process
SingleOutputStreamOperator<String> process = dataWS.process(
new ProcessWindowFunction<WaterSensor,String,String,TimeWindow>(){
/* 全窗口函数在窗口触发时才会调用一次,统一计算窗口的所有数据
* 1.s:分组的key
* 2.context 上下文
* 3.elements 存的数据
* 4.out 采集器
*/
@Override
public void process(String s , Context context , Iterable<WaterSensor> elements , Collector<String> out) throws Exception {
//上下文可以拿到window对象,还有侧输出流等等
long startTs = context.window().getStart();
long endTs = context.window().getEnd();
String windowStart = DateFormatUtils.format(startTs,"yyyy-MM-dd HH:mm:ss.SSS");
String windowEnd = DateFormatUtils.format(endTs,"yyyy-MM-dd HH:mm:ss.SSS");
long count = elements.spliterator().estimateSize();
out.collect("key=" + s + "的窗口[" + windowStart + "," + windowEnd + ")包括" + count + "条数据=>" + elements.toString());
}
}
);
process.print();
env.execute();
}
}
6、增量聚合与全窗口函数结合
java
public class WindowAggregateAndProcessDemo{
public static void main(String[] args) throws Exception {
//创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<String> socketdata = env.socketTextStream("node-1",8888);
SingleOutputStreamOperator<WaterSensor> data = socketdata.map(new WaterSensorMapFunction());
KeyedStream<WaterSensor,String> dataKS = data .keyBy(x -> x.getId());
//窗口分配器
WindowedStream<WaterSensor,String,TimeWindow> dataWS = dataKS.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));
/* 增量聚合aggregate/reduce + 全窗口Process
* 1.增量聚合来一条处理一条
* 2.窗口触发时增量聚合结果只有一条传递给全窗口函数
* 3.经过全窗口函数处理包装后输出
* 优点:
* 1.来一条计算一条,存储中间的计算结果,占用空间少
* 2.可以通过上下文实现灵活的功能
*/
SingleOutputStreamOperator<String> result = dataWS.aggregate(new MyAgg(),new MyProcess);
result.print();
env.execute();
}
public static class MyAgg implements AggregateFunction<WaterSensor,Integer,String>{
@Override
public Integer createAccumulator() {
return 0;
}
@Override
public Integer add(WaterSensor value,Integer accumulator) {
return accumulator + value.getVc();
}
@Override
public String getResult(Integer accumulator) {
return accumulator.toString();
}
@Override
public Integer merge(Integer a,Integer b) {
//只有会话窗口才会用到
return null;
}
}
public static class MyProcess extends ProcessWindowFunction<String,String,String,TimeWindow> {
@Override
public void process(String s , Context context , Iterable<String> elements , Collector<String> out) throws Exception {
long startTs = context.window().getStart();
long endTs = context.window().getEnd();
String windowStart = DateFormatUtils.format(startTs,"yyyy-MM-dd HH:mm:ss.SSS");
String windowEnd = DateFormatUtils.format(endTs,"yyyy-MM-dd HH:mm:ss.SSS");
long count = elements.spliterator().estimateSize();
out.collect("key=" + s + "的窗口[" + windowStart + "," + windowEnd + ")包括" + count + "条数据=>" + elements.toString());
}
}
}
7、窗口触发时机&划分&生命周期
1、窗口什么时候触发输出
时间进展 >= 窗口的最大时间戳(end - 1ms)
2、窗口是怎么划分的
start = 向下取整,取窗口长度的整数倍
end = start + 窗口长度
窗口左闭右开 ->属于本窗口的最大时间戳 = end - 1ms
3、窗口的生命周期
创建:属于本窗口的第一条数据来的时候,现new的,放入一个singleton单例的集合中。
销毁(关窗):时间进展 >= 窗口的最大时间戳(end - 1ms) + 允许迟到的时间(默认0)。
二、水位线
1、时间语义
1、事件时间:数据产生的时间(时间戳Timestamp)
2、处理时间:数据真正被处理的时间
2、什么是水位线
用来衡量事件时间 进展的标记,水位线可以看作一条特殊的数据记录,它是插入到数据流中的一个标记点,主要内容就是一个时间戳,用来指示当前的事件时间。
有序流中的水位线:
理想状态每条数据生成一个水位线,实际应用中如果数据量非常大,为了提高效率一般每隔一段时间生成一个水位线。
乱序流中的水位线:
在插入新的水位线时,要判断一下时间戳是否比之前的大,否的话就不再生成新的水位线。只有数据的时间戳比当前水位线大,才能推动时钟前进。如果数据量大的话,同样可以周期性的生成水位线,只需要保存一下之前所有数据中的最大时间戳,作为新的水位线
3、水位线特性
1、是插入到数据流中的一个标记,可以认为是一个特殊的数据
2、主要的内容是一个时间戳,用来表示当前事件时间的进展
3、是基于数据的时间戳生成的
4、水位线的时间戳必须单调递增,以确保任务的事件时间时钟一直向前推进
5、可以通过设置延迟,来保证正确的处理乱序数据
6、一个水位线watermark(t),表示在当前流中事件时间已经达到了时间戳t,代表t之前的数据都到齐了,之后不会再出现时间戳小于t的数据
4、有序流设置水位线
java
public class WatermarMonokDemo{
public static void main(String[] args) throws Exception {
//创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<String> socketdata = env.socketTextStream("node-1",8888);
SingleOutputStreamOperator<WaterSensor> data = socketdata.map(new WaterSensorMapFunction());
//指定Watermark策略
WatermarkStrategy<WaterSensor> watermarkStrategy = WatermarkStrategy
//升序的Watermark,没有等待时间
.<WaterSensor>forMonotonousTimestamps()
//指定时间戳分配器,从数据中提取
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>(){
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp){
//返回时间戳,要毫秒
return element.getTs() * 1000L;
}
});
SingleOutputStreamOperator<WaterSensor> dataWithWatermark = data.assignTimestampsAndWatermarks(watermarkStrategy );
KeyedStream<WaterSensor,String> dataKS = dataWithWatermark.keyBy(x -> x.getId());
//使用事件时间语义的窗口,watermark才会起作用
WindowedStream<WaterSensor,String,TimeWindow> dataWS = dataKS.window(TumblingEventTimeWindows.of(Time.seconds(10)));
//全窗口函数process
SingleOutputStreamOperator<String> process = dataWS.process(
new ProcessWindowFunction<WaterSensor,String,String,TimeWindow>(){
@Override
public void process(String s , Context context , Iterable<WaterSensor> elements , Collector<String> out) throws Exception {
//上下文可以拿到window对象,还有侧输出流等等
long startTs = context.window().getStart();
long endTs = context.window().getEnd();
String windowStart = DateFormatUtils.format(startTs,"yyyy-MM-dd HH:mm:ss.SSS");
String windowEnd = DateFormatUtils.format(endTs,"yyyy-MM-dd HH:mm:ss.SSS");
long count = elements.spliterator().estimateSize();
out.collect("key=" + s + "的窗口[" + windowStart + "," + windowEnd + ")包括" + count + "条数据=>" + elements.toString());
}
}
);
process.print();
env.execute();
}
}
5、乱序流设置水位线
java
public class WatermarkOutOfOrdernessDemo{
public static void main(String[] args) throws Exception {
//创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStreamSource<String> socketdata = env.socketTextStream("node-1",8888);
SingleOutputStreamOperator<WaterSensor> data = socketdata.map(new WaterSensorMapFunction());
//指定Watermark策略
WatermarkStrategy<WaterSensor> watermarkStrategy = WatermarkStrategy
//乱序的Watermark,有等待时间3s
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
//指定时间戳分配器,从数据中提取
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>(){
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp){
//返回时间戳,要毫秒
return element.getTs() * 1000L;
}
});
SingleOutputStreamOperator<WaterSensor> dataWithWatermark = data.assignTimestampsAndWatermarks(watermarkStrategy );
KeyedStream<WaterSensor,String> dataKS = dataWithWatermark.keyBy(x -> x.getId());
//使用事件时间语义的窗口,watermark才会起作用
WindowedStream<WaterSensor,String,TimeWindow> dataWS = dataKS.window(TumblingEventTimeWindows.of(Time.seconds(10)));
//全窗口函数process
SingleOutputStreamOperator<String> process = dataWS.process(
new ProcessWindowFunction<WaterSensor,String,String,TimeWindow>(){
@Override
public void process(String s , Context context , Iterable<WaterSensor> elements , Collector<String> out) throws Exception {
//上下文可以拿到window对象,还有侧输出流等等
long startTs = context.window().getStart();
long endTs = context.window().getEnd();
String windowStart = DateFormatUtils.format(startTs,"yyyy-MM-dd HH:mm:ss.SSS");
String windowEnd = DateFormatUtils.format(endTs,"yyyy-MM-dd HH:mm:ss.SSS");
long count = elements.spliterator().estimateSize();
out.collect("key=" + s + "的窗口[" + windowStart + "," + windowEnd + ")包括" + count + "条数据=>" + elements.toString());
}
}
);
process.print();
env.execute();
}
}
/*内置的Watermark的生成原理
* 1.都是周期生成的:默认200ms
* 2.有序流:Watermark = 当前最大事件时间 - 1ms
* 3.乱序流:Watermark = 当前最大事件时间 - 延迟时间 - 1ms
*/
6、水位线传递
多并行度下水位线的传递:接收到上游多个水位线,则取最小的那个,然后广播发送给下游所有子任务。
7、水位线设置空闲等待
在多个上游并行任务中,如果有其中一个没有数据,由于当前task是以上游并行任务中最小的那个作为当前任务的事件时钟,就会导致当前task的水位线无法推进,导致窗口无法触发,所以可以设置空闲等待时间,超过这个时间,就会把没有数据的任务剔除掉,不在参与到比较最小值中。
java
//指定Watermark策略
WatermarkStrategy<WaterSensor> watermarkStrategy = WatermarkStrategy
//乱序的Watermark,有等待时间3s
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
//指定时间戳分配器,从数据中提取
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>(){
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp){
//返回时间戳,要毫秒
return element.getTs() * 1000L;
}
})
//设置空闲等待时间5s
.withIdleness(Duration.ofSeconds(5));
8、设置窗口允许迟到
java
//在窗口后面设置推迟两秒关窗
WindowedStream<WaterSensor,String,TimeWindow> dataWS = dataKS
.window(TumblingEventTimeWindows.of(Time.seconds(10)))
//当Watermark进展到10s后会正常输出结果,但是并没有关窗,这时后面如果有窗口内的迟到数据到来会再次触发计算并输出,输出的结果也会和前面的结果累加,来一条计算输出一次。直到Watermark进展到12s,彻底关窗
.allowedLateness(Time.seconds(2));
9、关窗后的迟到数据放入侧输出流
java
OutputTag<WaterSensor> lateTag = new OutputTag<>("late-data",Types.POJO(WaterSensor.class));
//在窗口后面设置推迟两秒关窗
WindowedStream<WaterSensor,String,TimeWindow> dataWS = dataKS
.window(TumblingEventTimeWindows.of(Time.seconds(10)))
.allowedLateness(Time.seconds(2))
//设置侧输出流,放入关窗后的迟到数据
.sideOutputLateData(lateTag);
//获取侧输出流数据
dataWS.getSideOutput(lateTag).printToErr("关窗后的迟到数据");
10、如果Watermark等待3s,窗口允许延迟关窗2s,为什么不直接Watermark等待5s或者窗口允许延迟关窗5s?
1、Watermark等待时间不会设置太久-->太久会导致计算延迟久
2、窗口允许迟到,是对大部分迟到数据的处理,尽量让结果准确,如果设置太久,那么会导致频繁的计算输出
综上,设置策略:
1、Watermark等待时间不会很大,一般是秒级,在乱序和延迟间取舍
2、设置一定的窗口允许迟到时间,只考虑大部分的迟到数据,极端小部分迟到很久的数据放到侧输出流中。