窗口
- 在数据流中,数据是连续、无界的,所以无法对一个没有终点的数据流进行全局的聚合计算
- 需要将无界流切割成一个个有界数据块,再进行计算,以实现类似批处理的聚合操作
分类
时间/数量
- 时间窗口:以时间为单位来划分窗口,如每分钟计算一次
- 计数窗口:按数据元素的数量来划分窗口,如每100条数据计算一次
触发形态
- 滚动窗口:固定大小,不重叠,无间隔,每个元素只属于一个窗口
- 滑动窗口:固定大小,可以重叠,有滑动间隔
- 会话窗口:大小不确定,通过活动间隙来划分,不重叠
- 全局窗口:把所有相同keyed数据划分到一个全局唯一的窗口中,默认永远不会触发,除非编写自定义触发器
窗口函数
Reduce
- 每来一条数据就更新一次结果
- 输入和输出的数据类型必须一致
- 第一条数据不会触发ReduceFunction,只会被缓存
- reduce(WaterSensor t1, WaterSensor t2)其中t1是缓存数据
windowed.reduce(new ReduceFunction<WaterSensor>() {
@Override
public WaterSensor reduce(WaterSensor t1, WaterSensor t2) throws Exception {
System.out.println("reducing:"+t1);
return new WaterSensor(t1.getId(), t1.getTs(), t1.getVal() + t2.getVal());
}
}).print();
Aggregate
- 比Reduce更灵活:可以自定义累加器类型和输出类型
- merge方法支持多字段聚合
windowed.aggregate(new AggregateFunction<WaterSensor, Integer, Integer>() {
@Override
public Integer createAccumulator() {
return 0;
}
@Override
public Integer add(WaterSensor waterSensor, Integer integer) {
return integer+waterSensor.getVal();
}
@Override
public Integer getResult(Integer integer) {
return integer;
}
@Override
public Integer merge(Integer integer, Integer acc1) {
return acc1+integer;
}
}).print();
全窗口函数
- 功能最强:可以访问窗口全部数据、状态、上下文;获取窗口元信息;输出多条结果
- 性能最低:必须缓存窗口中的所有数据
windowed.process(new ProcessWindowFunction<WaterSensor, Tuple3<String,String,Integer>, String, TimeWindow>() {
@Override
public void process(String s, ProcessWindowFunction<WaterSensor, Tuple3<String, String, Integer>, String, TimeWindow>.Context context, Iterable<WaterSensor> iterable, Collector<Tuple3<String, String, Integer>> collector) throws Exception {
long start = context.window().getStart()
long end = context.window().getEnd()
String windowStart = DateFormatUtils.format(start, "yyyy-MM-dd-HH:mm:ss")
String windowEnd = DateFormatUtils.format(end, "yyyy-MM-dd-HH:mm:ss")
Integer maxn=0
for (WaterSensor waterSensor : iterable) {
maxn=max(maxn,waterSensor.getVal())
}
collector.collect(Tuple3.of(windowStart,windowEnd,maxn))
}
}).print()
Aggregate / Reduce + ProcessWindowFunction组合
- ProcessWindowFunction的iterable中存储的是一条最终的聚合数据
windowed.aggregate(new AggregateFunction<WaterSensor, Integer, Integer>() {
@Override
public Integer createAccumulator() {
return 0;
}
@Override
public Integer add(WaterSensor waterSensor, Integer integer) {
return integer+ waterSensor.getVal();
}
@Override
public Integer getResult(Integer integer) {
return integer;
}
@Override
public Integer merge(Integer integer, Integer acc1) {
return null;
}
}, new ProcessWindowFunction<Integer, Tuple3<String,String,Integer>, String, TimeWindow>() {
@Override
public void process(String s, ProcessWindowFunction<Integer, Tuple3<String, String,Integer>, String, TimeWindow>.Context context, Iterable<Integer> iterable, Collector<Tuple3<String, String,Integer>> collector) throws Exception {
long start = context.window().getStart();
long end = context.window().getEnd();
Integer res=0;
for (Integer i : iterable) {
res=res+i;
}
String windowStart = DateFormatUtils.format(start, "yyyy-MM-dd-HH:mm:ss");
String windowEnd = DateFormatUtils.format(end, "yyyy-MM-dd-HH:mm:ss");
collector.collect(Tuple3.of(windowStart,windowEnd,res));
}
}).print();
水位线
- 在窗口的处理过程中,基于数据的时间戳自定义一个“逻辑时钟”,这个时钟的时间不会自动流逝,而是靠新到达数据的时间戳来推动。基于此,计算的过程可以完全不依赖处理时间,在一般实时流处理的场景中,事件事件可以基本与处理时间保持同步,同时保证了计算的正确。
- 在flink中用来衡量事件时间进展的标记被称为水位线。
- 理想状态下,数据按照生成的先后顺序进入流中,每一条数据都生成一个水位线。但是,当数据量非常大且同时涌入的数据时差非常小时,为了提高效率,可以每隔一段时间生成一个水位线。
- 在分布式系统中,数据在节点间传输。由于网络传输延迟,可能导致乱序。此时要生成水位线,可以对每个新到达的数据提取时间戳,并判断时间戳是否比之前的大,是则生成新的水位线。也就是说只有新到达数据的时间戳比当前时钟大才能推动时钟前进。当数据量很大时,为了提高效率,选择一段时间内到达数据中的最大时间戳,作为这段时间的水位线。
- 对于迟到数据,为了让窗口能正确收集,可以让时钟进度延迟一会(比如延迟2秒)。这种情况下,要生成的水位线的时间戳就是,当前数据最大时间戳减去2秒。
- 水位线表示当前流中所有数据的时间戳都小于等于水位线的时间戳。
forMonotonousTimestamps
SingleOutputStreamOperator<WaterSensor> assigned = mapped.assignTimestampsAndWatermarks(WatermarkStrategy.
<WaterSensor>forMonotonousTimestamps().
withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
@Override
public long extractTimestamp(WaterSensor waterSensor, long l) {
System.out.println(waterSensor + "at:" + l);
return waterSensor.getTs() * 1000L;
}
}));
forBoundedOutOfOrderness
SingleOutputStreamOperator<WaterSensor> assigned = mapped.assignTimestampsAndWatermarks(WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
@Override
public long extractTimestamp(WaterSensor waterSensor, long l) {
System.out.println(waterSensor + "at:" + l);
return waterSensor.getTs();
}
}));
传递
- 必须遵循最小水位线原则
- 下游算子的水位线是所有上游水位线的最小值
- 水位线只允许向前,不会倒退
空闲等待
- 当上游某个子任务长时间没有数据,并且其水位线是上游所有子任务的最小值,造成了下游水位线停滞,窗口被卡住的问题。flink引入空闲检测来解决这一问题。
- withIdleness方法可以设置空闲等待时间,超过该时间没有收到数据,相应子任务被判定为空闲,其水位线不再影响下游,直到收到数据
- 当在测试过程中遇到水位线停滞的问题时,可以先把测试代码的并行度设置为1,验证该问题是否为空闲分区引起
WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
@Override
public long extractTimestamp(WaterSensor waterSensor, long l) {
System.out.println(waterSensor + "at:" + l);
return waterSensor.getTs()*1000L;
}
})
.withIdleness(Duration.ofSeconds(20)));
允许迟到
- 可以配置allowedLateness方法来延迟窗口的关闭,窗口结束后,在允许迟到的时间内,到达的数据仍能被处理(例如可以触发计算并输出)
- 与窗口等待时间的区别:窗口等待是为了处理乱序的流,本该先到但是后到的数据也能被划分到窗口内,然后统一触发计算。允许迟到是为了处理窗口结束后才到达的数据,这些数据会马上触发计算。
keyed.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.allowedLateness(Time.seconds(2)).process(new ProcessWindowFunction<WaterSensor, Tuple3<String,String,Integer>, String, TimeWindow>() {
@Override
public void process(String s, ProcessWindowFunction<WaterSensor, Tuple3<String, String, Integer>, String, TimeWindow>.Context context, Iterable<WaterSensor> iterable, Collector<Tuple3<String, String, Integer>> collector) throws Exception {
long start = context.window().getStart()
long end = context.window().getEnd()
String windowStart = DateFormatUtils.format(start, "yyyy-MM-dd-HH:mm:ss")
String windowEnd = DateFormatUtils.format(end, "yyyy-MM-dd-HH:mm:ss")
Integer maxn=0
for (WaterSensor waterSensor : iterable) {
maxn=max(maxn,waterSensor.getVal())
}
collector.collect(Tuple3.of(windowStart,windowEnd,maxn))
}
}).print()
侧输出流处理迟到
- sideOutputLateData方法可以将关闭窗口后到达的数据发送到侧输出流
SingleOutputStreamOperator<Tuple3<String, String, Integer>> processed = keyed.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.allowedLateness(Time.seconds(2))
.sideOutputLateData(outputTag)
.process(new ProcessWindowFunction<WaterSensor, Tuple3<String, String, Integer>, String, TimeWindow>() {
@Override
public void process(String s, ProcessWindowFunction<WaterSensor, Tuple3<String, String, Integer>, String, TimeWindow>.Context context, Iterable<WaterSensor> iterable, Collector<Tuple3<String, String, Integer>> collector) throws Exception {
long start = context.window().getStart()
long end = context.window().getEnd()
String windowStart = DateFormatUtils.format(start, "yyyy-MM-dd-HH:mm:ss")
String windowEnd = DateFormatUtils.format(end, "yyyy-MM-dd-HH:mm:ss")
Integer maxn = 0
for (WaterSensor waterSensor : iterable) {
maxn = max(maxn, waterSensor.getVal())
}
collector.collect(Tuple3.of(windowStart, windowEnd, maxn))
}
})
processed.getSideOutput(outputTag).print()
processed.print()
合流
window join
- 对于两条已经分配WatermarkStrategy的数据流,先调用join方法连接,再设置window分配窗口,由此实现简单的双流联结窗口。
- 优点:语义清晰,易于理解;直接支持各种 WindowAssigner;事件时间驱动,支持乱序;
- 缺点:由于join匹配是笛卡尔积的形式,这种写法只适用于数据量小,窗口粒度的情况;不能使用更灵活的匹配逻辑;当匹配的两个数据刚好卡在窗口边缘两侧时会丢失
SingleOutputStreamOperator<String> assigned1 = ds1.assignTimestampsAndWatermarks(strategy)
SingleOutputStreamOperator<String> assigned2 = ds2.assignTimestampsAndWatermarks(strategy)
assigned1.join(assigned2).where(keySelector).equalTo(keySelector)
.window(TumblingEventTimeWindows.of(Time.seconds(15)))
.apply(new RichJoinFunction<String, String, Tuple3<String,String,Integer>>() {
@Override
public Tuple3<String,String,Integer> join(String s, String s2) throws Exception {
String[] water1 = s.split(" ")
String[] water2 = s2.split(" ")
return Tuple3.of(water1[1],water2[1],Integer.valueOf(water1[2])+Integer.valueOf(water2[2]))
}
}).print()
IntervalJoin
- 一条流中的任意数据以其时间戳为中心,从其下界点到上界点的闭区间,为该数据可以跟其它流匹配的范围
- 只支持事件时间
- 只支持keyed状态
- 可以通过设置sideoutput获取迟到数据
KeyedStream<String, String> ds11 = ds1.assignTimestampsAndWatermarks(strategy).keyBy(keySelector)
KeyedStream<String, String> ds22 = ds2.assignTimestampsAndWatermarks(strategy).keyBy(keySelector)
OutputTag<String>l=new OutputTag<>("left-late")
OutputTag<String>r=new OutputTag<>("right-late")
SingleOutputStreamOperator<Tuple3<String, String, Integer>> processed = ds11.intervalJoin(ds22)
.between(Time.seconds(-2), Time.seconds(2))
.sideOutputLeftLateData(l).sideOutputRightLateData(r)
.process(new ProcessJoinFunction<String, String, Tuple3<String, String, Integer>>() {
@Override
public void processElement(String s, String s2, ProcessJoinFunction<String, String, Tuple3<String, String, Integer>>.Context context, Collector<Tuple3<String, String, Integer>> collector) throws Exception {
String[] w1 = s.split(" ")
String[] w2 = s2.split(" ")
collector.collect(Tuple3.of(w1[1], w2[1], Integer.valueOf(w1[2]) + Integer.valueOf(w2[2])))
}
})
processed.print()
processed.getSideOutput(l).print()
processed.getSideOutput(r).print()