文章目录
1.窗口概述
Flink 是一种流式计算引擎,主要是来处理无界数据流的,数据源源不断、无穷无尽。想要更加方便高效地处理无界流,一种方式就是将无限数据切割成有限的“数据块”进行处理,这就是所谓的“窗口”(Window)。
在 Flink 中, 窗口就是用来处理无界流的核心。我们很容易把窗口想象成一个固定位置的“框”,数据源源不断地流过来,到某个时间点窗口该关闭了,就停止收集数据、触发计算并输出结果。例如,我们定义一个时间窗口,每 10 秒统计一次数据,那么就相当于把窗口放在那
里,从 0 秒开始收集数据;到 10 秒时,处理当前窗口内所有数据,输出一个结果,然后清空窗口继续收集数据;到 20 秒时,再对窗口内所有数据进行计算处理,输出结果;依次类推,
延迟2s 的窗口
在流处理应用中,数据是连续不断的,因此我们不可能等到所有数据都到了才开始处理。当然我们可以每来一个消息就处理一次,但是有时我们需要做一些聚合类的处理,例如:在过去的1分钟内有多少用户点击了我们的网页。在这种情况下,我们必须定义一个窗口,用来 收集最近一分钟内的数据,并对这个窗口内的数据进行计算。
聚合事件(例如 count、sum)在流上的工作方式与在批处理中不同。例如,不可能计算流中的所有元素,因为流通常是无限的(无界的)。相反,流上的聚合(count、sum 等)由窗口限定范 围,例如“过去 5 分钟内的计数”或“最后 100 个元素的总和”。也就是说,流数据的计算 可以把连续不断的数据按照一定的规则拆分成大量的片段,在片段内进行统计和计算。比如可以把一小时内的数据保存到一个小的数据库表里,然后对这部分数据进行计算和统计,这 时流计算是提供自动切割的一种机制-窗口。
窗口实际就是一个Bucket桶,
[例子]
(1)第一个数据时间戳为 2,判断之后创建第一个窗口[0, 10),并将 2 秒数据保存进去;
(2)后续数据依次到来,时间戳均在 [0, 10)范围内,所以全部保存进第一个窗口;
(3)11 秒数据到来,判断它不属于[0, 10)窗口,所以创建第二个窗口[10, 20),并将 11秒的数据保存进去。由于水位线设置延迟时为 2 秒,所以现在的时钟是 9 秒,第一个窗口也没有到关闭时间;
(4)之后又有 9 秒数据到来,同样进入[0, 10)窗口中;
(5)12 秒数据到来,判断属于[10, 20)窗口,保存进去。这时产生的水位线推进到了 10秒,所以 [0, 10)窗口应该关闭了。第一个窗口收集到了所有的 7 个数据,进行处理计算后输出结果,并将窗口关闭销毁;
(6)同样的,之后的数据依次进入第二个窗口,遇到 20 秒的数据时会创建第三个窗口[20, 30)并将数据保存进去;遇到 22 秒数据时,水位线达到了 20 秒,第二个窗口触发计算,输出结果并关闭。
这里需要注意的是,Flink 中窗口并不是静态准备好的,而是动态创建——当有落在这个窗口区间范围的数据达到时,才创建对应的窗口。另外,这里我们认为到达窗口结束时间时,窗口就触发计算并关闭,事实上“触发计算”和“窗口关闭”两个行为也可以分开。
2.窗口分类
- 时间窗口
- 计数窗口
2.1 时间窗口
时间窗口以时间点来定义窗口的开始(start)和结束(end),所以截取出的就是某一时间段的数据。到达结束时间时,窗口不再收集数据,触发计算输出结果,并将窗口关闭销毁。所以可以说基本思路就是“定点发车”
Flink 中有一个专门的类来表示时间窗口,名称就叫作 TimeWindow。这个类只有两个私有属性:start 和 end,表示窗口的开始和结束的时间戳,单位为毫秒。
private final long start;
private final long end;
2.2 计数窗口
计数窗口基于元素的个数来截取数据,到达固定的个数时就触发计算并关闭窗口。这相当于座位有限、“人满就发车”,是否发车与时间无关。每个窗口截取数据的个数,就是窗口的大小。
3.细分
根据分配数据的规则,窗口的具体实现可以分为 4 类:滚动窗口(Tumbling Window)、滑动窗口(Sliding Window)、会话窗口(Session Window),以及全局窗口(Global Window)。
3.1 滚动窗口
滚动窗口可以基于时间定义,也可以基于数据个数定义;需要的参数只有一个,就是窗口的大小(window size)。比如我们可以定义一个长度为 1 小时的滚动时间窗口,那么每个小时就会进行一次统计;或者定义一个长度为 10 的滚动计数窗口,就会每 10 个数进行一次统计。
小圆点表示流中的数据,我们对数据按照 userId 做了分区。当固定了窗口大小之后,所有分区的窗口划分都是一致的;窗口没有重叠,每个数据只属于一个窗口。
3.2 滑动窗口
当滑动步长小于窗口大小时,滑动窗口就会出现重叠,这时数据也可能会被同时分配到多个窗口中。
滚动窗口是特殊的滑动窗口, 相当于滚动的size = slidw
3.3 会话窗口
会话窗口的长度不固定,起始和结束时间也是不确定的,各个分区之间窗口没有任何关联。 会话窗口之间一定是不会重叠的,而
且会留有至少为 size 的间隔(session gap)。
3.4 全局窗口
全局窗口没有结束的时间点,所以一般在希望做更加灵活的窗口处理时自定义使用。Flink 中的计数窗口(Count Window),底层就是用全局窗口实现的。
这种窗口全局有效,会把相同 key 的所有数据都分配到同一个窗口中;说直白一点,就跟没分窗口一样。
4.窗口Api
- 按键分区 Keyed
- 非按键分区 Non-Keyed
4.1 按键分区窗口
stream.keyBy(...)
.window(...)
4.2 非按键分区
4.3 代码中窗口Api的调用
stream.keyBy(<key selector>)
.window(<window assigner>)
.aggregate(<window function>)
5.窗口分配器 Window Assigners
5.1 时间窗口
1> 滚动处理时间窗口
stream.keyBy(...)
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.aggregate(...)
2> 滑动处理时间窗口
stream.keyBy(...)
.window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.aggregate(...)
3> 处理时间会话窗口
stream.keyBy(...)
.window(ProcessingTimeSessionWindows.withGap(Time.seconds(10)))
.aggregate(...)
4> 滚动事件时间窗口
stream.keyBy(...)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.aggregate(...)
5> 滑动事件时间窗口
stream.keyBy(...)
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.aggregate(...)
6> 事件时间会话窗口
stream.keyBy(...)
.window(EventTimeSessionWindows.withGap(Time.seconds(10)))
.aggregate(...)
5.2 计数窗口
1> 滚动计数窗口
stream.keyBy(...)
.countWindow(10)
2> 滑动计数窗口
stream.keyBy(...)
.countWindow(10,3)
5.3 全局窗口
stream.keyBy(...)
.window(GlobalWindows.create());
6.窗口函数
- 增量窗口函数: ReduceFunction 和 AggregateFunction。
- 全量窗口函数: WindowFunction 和 ProcessWindowFunction。
6.1 增量函数
AggregateFunction
需求为计算出pv, pv去重通过hashset去重
public class WindowAggregateTest_PvUv {
// 增量聚合效率比全量高, 但是拿不到信息
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();
environment.setParallelism(1);
SingleOutputStreamOperator<Event> stream = environment.addSource(new ClickSource())
.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event event, long l) {
return event.timestamp;
}
})
);
stream.print("data");
stream.keyBy(data -> true)
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(2)))
.aggregate(new AvgPv()).print();
environment.execute();
}
// Long 表示pv, hashset去重uv
public static class AvgPv implements AggregateFunction<Event, Tuple2<Long, HashSet<String>>, Double>{
@Override
public Tuple2<Long, HashSet<String>> createAccumulator() {
return Tuple2.of(0L, new HashSet<>());
}
@Override
public Tuple2<Long, HashSet<String>> add(Event event, Tuple2<Long, HashSet<String>> longHashSetTuple2) {
// pv + 1, uv 加入hasheset
longHashSetTuple2.f1.add(event.user);
return Tuple2.of(longHashSetTuple2.f0 + 1, longHashSetTuple2.f1);
}
@Override
public Double getResult(Tuple2<Long, HashSet<String>> longHashSetTuple2) {
// 结束的时候输出pv/uv
return (double)longHashSetTuple2.f0 / longHashSetTuple2.f1.size();
}
@Override
public Tuple2<Long, HashSet<String>> merge(Tuple2<Long, HashSet<String>> longHashSetTuple2, Tuple2<Long, HashSet<String>> acc1) {
return null;
}
}
}
ReduceFunction
需求为算出10s内各个用户的访问数量
public class WindowTest {
public static void main(String[] args) throws Exception {
// 先keyBy 再 window 分配器 再 window 函数
StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();
environment.setParallelism(1);
SingleOutputStreamOperator<Event> stream = environment.addSource(new ClickSource())
.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event event, long l) {
return event.timestamp;
}
})
);
stream
.map(new MapFunction<Event, Tuple2<String, Long>>() {
@Override
public Tuple2<String, Long> map(Event event) throws Exception {
return Tuple2.of(event.user, 1L);
}
})
.keyBy(data -> data.f0)
// 滑动事件时间窗口: 滑动大小, 滑动步长
// .window(SlidingEventTimeWindows.of(Time.hours(1), Time.minutes(5)))
// 会话时间事件窗口
// .window(EventTimeSessionWindows.withGap(Time.seconds(5)))
// 滚动时间窗口, 事件
.window(TumblingEventTimeWindows.of(Time.seconds(10)))
// 窗口函数: 1.增量聚合函数 2.全量聚合函数
// 归约函数reduce, 聚合函数aggregate
// 1.WindowFunction 2.ProcessWindowFunction
.reduce(new ReduceFunction<Tuple2<String, Long>>() {
@Override
public Tuple2<String, Long> reduce(Tuple2<String, Long> stringLongTuple2, Tuple2<String, Long> t1) throws Exception {
return Tuple2.of(stringLongTuple2.f0, stringLongTuple2.f1 + t1.f1);
}
})
.print();
environment.execute();
}
}
6.2 全量函数
ProcessWindowFunction
public class WindowFunctionTest {
// 全增量 ProcessWindowFunction
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();
environment.setParallelism(1);
SingleOutputStreamOperator<Event> stream = environment.addSource(new ClickSource())
.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event event, long l) {
return event.timestamp;
}
})
);
stream.print("data");
stream.keyBy(data -> true)
.window(TumblingEventTimeWindows.of(Time.seconds(10)))
.process(new UvCountByWindow()).print();
environment.execute();
}
// 自定义ProcessWindowFunction , 输出一条统计信息
public static class UvCountByWindow extends ProcessWindowFunction<Event, String, Boolean, TimeWindow> {
@Override
public void process(Boolean aBoolean, Context context, Iterable<Event> elements, Collector<String> out) throws Exception {
HashSet<String> set = new HashSet<>();
for (Event element : elements) {
set.add(element.user);
}
Integer uv = set.size();
Long start = context.window().getStart();
Long end = context.window().getEnd();
out.collect("start: " + start + ", end: " + end + ", uv: " + uv);
}
}
}
7.TopN 实例
一般来说增量函数比全量函数效率高, 但是拿到的信息量有限, 所以一般用的都是两个一起参与使用
[TopN实例]
思路1: 采用一个窗口, 然后全部统计
public class TopN_ProcessAllWindowFunction {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();
environment.setParallelism(1);
SingleOutputStreamOperator<Event> stream = environment.addSource(new ClickSource())
.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event event, long l) {
return event.timestamp;
}
})
);
stream.map(data -> data.url)
.windowAll(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.aggregate(new UrlHashMapCountAgg(), new UrlAllWindowResult())
.print();
environment.execute();
}
// 自定义增量聚合函数
public static class UrlHashMapCountAgg implements AggregateFunction<String, HashMap<String, Long>, ArrayList<Tuple2<String, Long>>>{
@Override
public HashMap<String, Long> createAccumulator() {
return new HashMap<>();
}
@Override
public HashMap<String, Long> add(String s, HashMap<String, Long> stringLongHashMap) {
if (stringLongHashMap.containsKey(s)){
Long aLong = stringLongHashMap.get(s);
stringLongHashMap.put(s, aLong + 1);
}else {
stringLongHashMap.put(s, 1L);
}
return stringLongHashMap;
}
@Override
public ArrayList<Tuple2<String, Long>> getResult(HashMap<String, Long> stringLongHashMap) {
ArrayList<Tuple2<String, Long>> list = new ArrayList<>();
for (String s : stringLongHashMap.keySet()) {
list.add(Tuple2.of(s, stringLongHashMap.get(s)));
}
list.sort(new Comparator<Tuple2<String, Long>>() {
@Override
public int compare(Tuple2<String, Long> o1, Tuple2<String, Long> o2) {
// jiang
return o2.f1.intValue() - o1.f1.intValue();
}
});
return list;
}
@Override
public HashMap<String, Long> merge(HashMap<String, Long> stringLongHashMap, HashMap<String, Long> acc1) {
return null;
}
}
// 自定义全增量函数
public static class UrlAllWindowResult extends ProcessAllWindowFunction<ArrayList<Tuple2<String, Long>>, String, TimeWindow> {
@Override
public void process(Context context, Iterable<ArrayList<Tuple2<String, Long>>> elements, Collector<String> out) throws Exception {
ArrayList<Tuple2<String, Long>> list = elements.iterator().next();
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("-----------------------\n");
stringBuilder.append("窗口结束时间: " + new Timestamp(context.window().getEnd()) + "\n");
// 取list 前两个, 包装信息输出
for (int i = 0; i < 2; i++) {
Tuple2<String, Long> tuple2 = list.get(i);
StringBuilder info = stringBuilder.append("No" + (i + 1) + " " +
"url" + tuple2.f0 + " "
+ "访问量: " + tuple2.f1 + " " + "\n"
);
stringBuilder.append(info);
}
stringBuilder.append("-----------------------\n");
out.collect(stringBuilder.toString());
}
}
}
思路2: 采用先开窗, 然后统计TopN
public class TopNTest {
public static void main(String[] args) throws Exception {
// 10s 内的Top5
StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();
environment.setParallelism(1);
SingleOutputStreamOperator<Event> stream = environment.addSource(new ClickSource())
.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event event, long l) {
return event.timestamp;
}
})
);
// 1.按照url分组, 统计窗口内每个url的访问量
SingleOutputStreamOperator<UrlViewCount> urlCountStream = stream.keyBy(data -> data.url)
// 开窗, 滑动窗口
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
// 聚合 +1
.aggregate(new UrlViewCountExample.UrlViewCountAgg(), new UrlViewCountExample.UrlViewCountResult());
urlCountStream.print("url count");
// 2. 对于同一个窗口统计出访问量, 进行收集和排序
urlCountStream.keyBy(data -> data.windowEnd)
.process(new TopNProcessResult(2))
.print();
environment.execute();
}
public static class TopNProcessResult extends KeyedProcessFunction<Long, UrlViewCount, String>{
private Integer pageSize;
private ListState<UrlViewCount> listState;
public TopNProcessResult(Integer pageSize) {
this.pageSize = pageSize;
}
@Override
public void open(Configuration parameters) throws Exception {
listState = getRuntimeContext().getListState(new ListStateDescriptor<UrlViewCount>("url-count-list", UrlViewCount.class));
}
@Override
public void processElement(UrlViewCount urlViewCount, Context context, Collector<String> collector) throws Exception {
listState.add(urlViewCount);
context.timerService().registerEventTimeTimer(context.timestamp() + 1);
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
ArrayList<UrlViewCount> result = new ArrayList<>();
for (UrlViewCount urlViewCount : listState.get()) {
result.add(urlViewCount);
}
result.sort(new Comparator<UrlViewCount>() {
@Override
public int compare(UrlViewCount o1, UrlViewCount o2) {
return o2.count.intValue() - o1.count.intValue();
}
});
StringBuilder resultInfo = new StringBuilder();
resultInfo.append("==========================\n\n");
resultInfo.append("窗口结束时间: " + new Timestamp(ctx.getCurrentKey()) + "\n");
for (int i = 0; i < Math.min(pageSize, result.size()); i++) {
UrlViewCount urlViewCount = result.get(i);
resultInfo.append("No ").append(i + 1).append(":")
.append("url: ").append(urlViewCount.url)
.append("访问量: ").append(urlViewCount.count).append("\n");
}
resultInfo.append("==========================\n");
out.collect(resultInfo.toString());
}
}
}
public class UrlViewCountExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forMonotonousTimestamps()
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event element, long recordTimestamp) {
return element.timestamp;
}
}));
// 需要按照url分组,开滑动窗口统计
stream.keyBy(data -> data.url)
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
// 同时传入增量聚合函数和全窗口函数
.aggregate(new UrlViewCountAgg(), new UrlViewCountResult())
.print();
env.execute();
}
// 自定义增量聚合函数,来一条数据就加一
public static class UrlViewCountAgg implements AggregateFunction<Event, Long, Long> {
@Override
public Long createAccumulator() {
return 0L;
}
@Override
public Long add(Event value, Long accumulator) {
return accumulator + 1;
}
@Override
public Long getResult(Long accumulator) {
return accumulator;
}
@Override
public Long merge(Long a, Long b) {
return null;
}
}
// 自定义窗口处理函数,只需要包装窗口信息
public static class UrlViewCountResult extends ProcessWindowFunction<Long, UrlViewCount, String, TimeWindow> {
@Override
public void process(String url, Context context, Iterable<Long> elements, Collector<UrlViewCount> out) throws Exception {
// 结合窗口信息,包装输出内容
Long start = context.window().getStart();
Long end = context.window().getEnd();
// 迭代器中只有一个元素,就是增量聚合函数的计算结果
out.collect(new UrlViewCount(url, elements.iterator().next(), start, end));
}
}
}