涉及的3个时间:
1.事件真正发生的时间
2.进入Flink系统的时间(一般不关心)
3.Flink系统开始处理任务的时间
哪个时间语义更重要?
事件时间更重要,Flink1.12开始时间语义默认是事件时间。
方式一:来一条数据,附带一条水位线。如果数据很多很密集,就给系统平添很多压力。
方式二:周期性的产生一条水位线。如果数据很多很密集,这种方式不会增加太大的系统压力;如果数据很稀疏,虽然有时数据是空的,只是单纯的打一条水位线,但系统整体清闲,此时打水位线不会有压力。
以上的假设是基于有序流,如果是乱序流,以最新数据的时间戳,和历史最大的时间戳比较,比历史最大时间戳还大,说明是个更新的时间戳;若小于历史最大时间戳,说明是个迟到数据。水位线只涨不跌。
如果到了w9,这个窗口如果关闭,那后面迟到的w8就会被漏掉。可以多等2s。
水位线生成:
一般是周期性地生成水位线,对于乱序数据需要设置延迟时间。
延迟时间到底设置成多少,需要权衡。如果希望处理的更快、实时性更强,那就把延迟时间设置的短一点;如果需要非常准确,那就把延迟时间设置长一点。
水位线代表了当前的事件时间时钟。
而且可以在数据的时间戳基础上增加一些延时来保证不丢数据,这一点对于乱序流的正确处理非常重要。
水位线的真谛:在这之前的所有数据都到齐了,再也不会有比当前水位线时间戳更小的事件时间戳到来了
特性:
创建一个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方法中周期性的发送时间戳。
水位线传递时,从上游传到下游,上游有两个分区,两个分区的水位线很可能不一致,这时下游要以较小的时间戳为准。
所以,下游得记录上游所有分区的最新水位线,从中选最小值作为自己的水位线,如果需要更新,那就更新自己的水位线,并把自己的水位线广播出去。
同一时间有多个桶存在。当第一个窗口收集齐了,那就可以关闭第一个窗口了。
按照窗口分配数据的规则分类:滚动窗口、滑动窗口、会话窗口、全局窗口
滚动窗口:收尾相接
滑动窗口:窗口本身的大小、与下一个窗口交叠多久
会话窗口:会话超时时间
全局窗口:keyby后得到的每个key分别统计,默认一直统计,什么时候触发计算还得自己定义触发器。Flink内部实现的计数窗口,底层就是全局窗口。
看在调用窗口算子前,是否有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个数统计一次
收集到了数据后,窗口干什么事:
根据处理的方式分为:增量聚合函数(流)、全窗口函数(批)。
分为归约函数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");
(1)水位线,可以延后2s,相当于把表调慢2s;
(2)允许窗口多存活1min。到水位线后再等1min,再关闭窗口,相当于调慢的表到点后,开着车门慢慢前行,来的数据还能赶上这班车,快速得到一个近似正确的结果,1min不断更新这个结果,最终得到一个更加正确的结果;
(3)将迟到的数据放入侧输出流。有可能迟到的数据迟的太多了,窗口得早点关闭好释放资源,这些迟到太多的数据统一放到侧输出流。从测输出流中逐一读出事件,根据事件时间,推测它所属的窗口,到之前处理结果的表里面,找到之前窗口统计的结果,合并后,更新那个结果。