【Flink】Flink中的时间和窗口

目录

    • 时间语义
    • 水位线 WaterMarker
      • 水位线生成
      • 水位线概括
      • 代码中生成水位线
      • 水位线的传递
    • 窗口
      • 窗口的概念
      • 窗口类型
      • 窗口API
      • 窗口函数
        • 增量聚合函数
        • 全窗口函数
        • 两种窗口函数结合
      • 其他函数
      • 3层保护,保证数据不丢失

时间语义

【Flink】Flink中的时间和窗口_第1张图片
涉及的3个时间:
1.事件真正发生的时间
2.进入Flink系统的时间(一般不关心)
3.Flink系统开始处理任务的时间

哪个时间语义更重要?
事件时间更重要,Flink1.12开始时间语义默认是事件时间。

水位线 WaterMarker

水位线生成

方式一:来一条数据,附带一条水位线。如果数据很多很密集,就给系统平添很多压力。
【Flink】Flink中的时间和窗口_第2张图片
方式二:周期性的产生一条水位线。如果数据很多很密集,这种方式不会增加太大的系统压力;如果数据很稀疏,虽然有时数据是空的,只是单纯的打一条水位线,但系统整体清闲,此时打水位线不会有压力。
【Flink】Flink中的时间和窗口_第3张图片

以上的假设是基于有序流,如果是乱序流,以最新数据的时间戳,和历史最大的时间戳比较,比历史最大时间戳还大,说明是个更新的时间戳;若小于历史最大时间戳,说明是个迟到数据。水位线只涨不跌。
【Flink】Flink中的时间和窗口_第4张图片
【Flink】Flink中的时间和窗口_第5张图片

如果到了w9,这个窗口如果关闭,那后面迟到的w8就会被漏掉。可以多等2s。
【Flink】Flink中的时间和窗口_第6张图片

水位线生成:
一般是周期性地生成水位线,对于乱序数据需要设置延迟时间。
延迟时间到底设置成多少,需要权衡。如果希望处理的更快、实时性更强,那就把延迟时间设置的短一点;如果需要非常准确,那就把延迟时间设置长一点。

水位线概括

水位线代表了当前的事件时间时钟。
而且可以在数据的时间戳基础上增加一些延时来保证不丢数据,这一点对于乱序流的正确处理非常重要。
水位线的真谛:在这之前的所有数据都到齐了,再也不会有比当前水位线时间戳更小的事件时间戳到来了
特性:

  1. 水位线是插入到数据流中的一个标记,可以认为是一个特殊数据
  2. 水位线主要内容是时间戳,表示当前事件时间的进展
  3. 水位线是基于数据的时间戳生成的
  4. 水位线的时间戳必须单调递增,以确保任务的事件时间时钟一直向前推进
  5. 水位线可以通过设置延时,保证正确处理乱序数据
  6. 一个水位线Watermark(t)表示在当前流中事件时间已经达到了时间戳t,这代表t之前的数据都到期了,之后流中不会出现时间戳小于等于t的数据。

代码中生成水位线

创建一个WatermarkStrategy,把它作为参数给到assignTimestampsAndWatermarks函数。
WatermarkStrategy中的核心是要有两个东西:
(1)watermarkGenerator,水位线生成器(里面有onEvent基于事件断电生成,和onPeriodicEmit基于周期生成)
(2)timestampAssigner,时间戳的提取器

针对有序流和乱序流,Flink内置了两种用法:

env.getConfig().setAutoWatermarkInterval(100);

// 离数据源越近越好
// 有序流的watermark生成==一般只有测试时用
dataStream.assignTimestampsAndWatermarks( 
	WatermarkStrategy.<Event>forMonotonousTimestamps()
	.withTimestampAssigner(new SerializableTimestampAssigner<Event>(){
		@Override
		public long extractTimestamp(Event element, long recordTimestamp){
			return element.timestamp; // 这里应该是一个毫秒数
		}
	})
);


// 乱序流的watermark生成
dataStream.assignTimestampsAndWatermarks( 
	WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(2)) // 最大延迟时间
	.withTimestampAssigner(new SerializableTimestampAssigner<Event>(){
		@Override
		public long extractTimestamp(Event element, long recordTimestamp){
			return element.timestamp; // 这里应该是一个毫秒数
		}
	})
);

自定义watermark生成器:

env.addSource(new ClickSource())
	.assignTimestampsAndWatermarks(new CustomWatermarkStrategy())
	.print();

用到的CustomWatermarkStrategy:
实现WatermarkStrategy接口,并实现两个方法:
(1)createTimestampAssigner方法,提取时间戳
(2)实现createWatermarkGenerator方法,生成水位线,处理乱序数据。
在createWatermarkGenerator方法中,需要构建一个新的对象用于返回:CustomBoundedOutOfOrdernessGenerator。
用到的CustomBoundedOutOfOrdernessGenerator:
实现WatermarkGenerator接口,在onEvent方法中更新时间戳,在onPeriodicEmit方法中周期性的发送时间戳。

水位线的传递

【Flink】Flink中的时间和窗口_第7张图片
水位线传递时,从上游传到下游,上游有两个分区,两个分区的水位线很可能不一致,这时下游要以较小的时间戳为准。

所以,下游得记录上游所有分区的最新水位线,从中选最小值作为自己的水位线,如果需要更新,那就更新自己的水位线,并把自己的水位线广播出去。

窗口

窗口的概念

同一时间有多个桶存在。当第一个窗口收集齐了,那就可以关闭第一个窗口了。
【Flink】Flink中的时间和窗口_第8张图片

窗口类型

  1. 按照驱动类型分类:时间窗口、计数窗口
    【Flink】Flink中的时间和窗口_第9张图片

  2. 按照窗口分配数据的规则分类:滚动窗口、滑动窗口、会话窗口、全局窗口
    滚动窗口:收尾相接
    【Flink】Flink中的时间和窗口_第10张图片
    滑动窗口:窗口本身的大小、与下一个窗口交叠多久
    【Flink】Flink中的时间和窗口_第11张图片
    会话窗口:会话超时时间
    【Flink】Flink中的时间和窗口_第12张图片
    全局窗口:keyby后得到的每个key分别统计,默认一直统计,什么时候触发计算还得自己定义触发器。Flink内部实现的计数窗口,底层就是全局窗口。
    【Flink】Flink中的时间和窗口_第13张图片

窗口API

看在调用窗口算子前,是否有keyby操作,有就是keyed后再开窗,没有就是直接开窗。

推荐keyby之后再开窗:只针对相同key内部进行处理

stream.keyBy(<key selector>)
	.window(<window assigner>)
	.aggregate(<window function>)

直接开窗:把所有数据搜集到一个分区中处理

stream.windowAll(...) 把当前的并行度强制变成1,极不推荐

窗口怎么开,长什么样,收集多少数:

stream.keyBy(data -> data.user)
	// 滚动事件时间窗口 
	.window(TumblingEventTimeWindows.of(Time.hours(1), offset)) 
	// 滑动事件时间窗口 
	.window(SlidingEventTimeWindows.of(Time.hours(1), Time.minutes(5), offset))
	// 事件时间会话窗口
	.window(EventTimeSessionWindows.withGap(Time.seconds(2)))
	// 滑动计数窗口
	.countWindow(size=10, slide=2) // 10个数统计一次,每隔2个数滑动一次
	// 滚动计数窗口
	.countWindow(size=10) // 10个数统计一次

窗口函数

收集到了数据后,窗口干什么事:
【Flink】Flink中的时间和窗口_第14张图片
根据处理的方式分为:增量聚合函数(流)、全窗口函数(批)。

增量聚合函数

分为归约函数ReduceFunction、AggregateFunction

reduce 输入是啥,输出也得是啥。

stream.map(map成最终要的样子)
	.keyBy(data -> data.user)
	.window(TumblingEventTimeWindows.of(Time.hours(1), offset)) 
	.reduce( new RecudceFunction(...));
stream.keyBy(data -> data.user)
	.window(TumblingEventTimeWindows.of(Time.hours(1), offset)) 
	// 输入类型Event,ACC累加器状态:Tuple2<所有数的和,所有数的个数>,输出类型String 
	.aggregate( new AggregateFunction<Event, Tuple2<Long,Integer>, String>(){
		// 只调用1次
		@Override
		public Tuple2<Long,Integer> createAccumulator(){
			return Tuple2.of(0L, 0);
		}
		// 每来一个数据就调用1次,状态叠加,返回的是变化后的状态
		@Override
		public Tuple2<Long,Integer> add(Event value, Tuple2<Long,Integer> accumulator){
			return Tuple2.of(accumulator.f0 + value.timestamp, accumulator.f1 + 1);
		}
		// 获取结果(平均时间戳)
		@Override
		public String getResult(Tuple2<Long,Integer> accumulator){
			Timestamp timestamp = new Timestamp(accumulator.f0 / accumulator.f1);
			return timestamp.toString();
		}
		//在会话窗口中才会用到merge
		@Override
		public Tuple2<Long,Integer> merge(Tuple2<Long,Integer> a, Tuple2<Long,Integer> b){
			return Tuple2.of(a.f0 + b.f0, a.f1 + b.f1);
		}
	});
全窗口函数

WindowFunction(基本弃用)、ProcessWindowFunction。
等一个窗口内容都到齐了之后,才进行计算。
WindowFunction:

stream.keyBy().window().apply()

ProcessWindowFunction:

stream.keyBy().window().process()
两种窗口函数结合

正常使用增量聚合函数,将result传入全窗口函数的process函数中。
AggregateFunction和ProcessWindowFunction结合计算UV:

stream.keyBy(data -> data.user)
	.window(TumblingEventTimeWindows.of(Time.hours(1), offset)) 
	.aggregate(new UvAgg(), new UvCountResult()) // 两者结合
	.print();

// 自定义实现AggregateFunction,增量聚合计算uv
public static class UvAgg implement AggregateFunction<Event, HashSet<String>,Long>{
	// 只调用1次
	@Override
	public HashSet<String> createAccumulator(){
		return new HashSet<>();
	}
	// 每来一个数据就调用1次,状态叠加,返回的是变化后的状态
	@Override
	public HashSet<String> add(Event value, HashSet<String> accumulator){
		accumulator.add(value.user);
		return accumulator;
	}
	// 获取结果
	@Override
	public Long getResult(HashSet<String> accumulator){
		return (long)accumulator.size();
	}
	// 在会话窗口中才会用到merge
	@Override
	public HashSet<String> merge(HashSet<String> a, HashSet<String> b){
		return null;
	}
}

// 自定义实现ProcessWindowFunction,包装窗口信息输出
public static class UvCountResult extends ProcessWindowFunction<Long, String, Boolean, TimeWindow>{
	@Override
	public void process(Boolean aBoolean, Context context, Iterable<Long> elements, Collector out){
		Long start = context.window().getStart();
		Long end = context.window().getEnd();
		Long uv = elements.iterator().next();
		out.collect("窗口 " + new Timestamp(start) + " ~ " + new Timestamp(end) + " UV值为:"+uv);
	}
}

其他函数

1.允许延迟:

stream.keyBy()
	.window()
	.allowedLateness(Time.minutes(1))

2.侧输出流:

// 定义一个输出标签
OutputTag<Event> late = new OutputTag<Event>("late"){};

result = stream.keyBy()
		.window()
		.allowedLateness()
		.sideOutputLateData(late)
		.aggregate();

result.print("result");
result.getSideOutput(late).print("late");

3层保护,保证数据不丢失

(1)水位线,可以延后2s,相当于把表调慢2s;
(2)允许窗口多存活1min。到水位线后再等1min,再关闭窗口,相当于调慢的表到点后,开着车门慢慢前行,来的数据还能赶上这班车,快速得到一个近似正确的结果,1min不断更新这个结果,最终得到一个更加正确的结果;
(3)将迟到的数据放入侧输出流。有可能迟到的数据迟的太多了,窗口得早点关闭好释放资源,这些迟到太多的数据统一放到侧输出流。从测输出流中逐一读出事件,根据事件时间,推测它所属的窗口,到之前处理结果的表里面,找到之前窗口统计的结果,合并后,更新那个结果。

你可能感兴趣的:(Flink,flink)