flink窗口与水位线详解

一、窗口

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、设置一定的窗口允许迟到时间,只考虑大部分的迟到数据,极端小部分迟到很久的数据放到侧输出流中。

相关推荐
kaoa0001 小时前
Linux入门攻坚——89、Hadoop-1-架构及概念
大数据·linux·hadoop·分布式
RisunJan1 小时前
DeepSeek 全景技术指南:从混合推理架构到提示语工程实战(2026.09)
大数据·人工智能·架构
2503_931712481 小时前
具身机器人和人形机器人的区别是什么?两者是否有什么关系
大数据
长谷深风1111 小时前
Checkpoint设计的三大生死关
大数据·人工智能·ai·大模型·ai智能体·tool·aiagent
BYSJMG1 小时前
计算机毕业设计选题推荐:基于大数据的水质多指标关联分析与可视化的设计与实现,Spark 加 K-Means 做水质分群
大数据·hadoop·信息可视化·数据分析·spark·kmeans·课程设计
龙亘川2 小时前
数字化建设案例|构建议案履职一体化闭环,解决议案建议办理多主体协同难题
大数据·人工智能·科技·信息可视化·智慧城市
牛企老板俱乐部2 小时前
全国钻井泥浆材料选型避坑指南从井温地层与体系匹配切入分析
大数据
乐迪信息2 小时前
航道船舶逆行AI识别,港口安全智能告警系统
大数据·前端·人工智能·安全·计算机视觉·音视频
2601_962218472 小时前
万象生鲜系统订单全生命周期追踪实现生鲜企业订单业务数字化可视
大数据·运维·微服务·云原生·架构