文章目录

1.窗口概述

Flink 是一种流式计算引擎,主要是来处理无界数据流的,数据源源不断、无穷无尽。想要更加方便高效地处理无界流,一种方式就是将无限数据切割成有限的“数据块”进行处理,这就是所谓的“窗口”(Window)。

在 Flink 中, 窗口就是用来处理无界流的核心。我们很容易把窗口想象成一个固定位置的“框”,数据源源不断地流过来,到某个时间点窗口该关闭了,就停止收集数据、触发计算并输出结果。例如,我们定义一个时间窗口,每 10 秒统计一次数据,那么就相当于把窗口放在那
里,从 0 秒开始收集数据;到 10 秒时,处理当前窗口内所有数据,输出一个结果,然后清空窗口继续收集数据;到 20 秒时,再对窗口内所有数据进行计算处理,输出结果;依次类推,

Flink_窗口_Window_数据
延迟2s 的窗口
Flink_窗口_Window_数据_02
在流处理应用中,数据是连续不断的,因此我们不可能等到所有数据都到了才开始处理。当然我们可以每来一个消息就处理一次,但是有时我们需要做一些聚合类的处理,例如:在过去的1分钟内有多少用户点击了我们的网页。在这种情况下,我们必须定义一个窗口,用来 收集最近一分钟内的数据,并对这个窗口内的数据进行计算。

聚合事件(例如 count、sum)在流上的工作方式与在批处理中不同。例如,不可能计算流中的所有元素,因为流通常是无限的(无界的)。相反,流上的聚合(count、sum 等)由窗口限定范 围,例如“过去 5 分钟内的计数”或“最后 100 个元素的总和”。也就是说,流数据的计算 可以把连续不断的数据按照一定的规则拆分成大量的片段,在片段内进行统计和计算。比如可以把一小时内的数据保存到一个小的数据库表里,然后对这部分数据进行计算和统计,这 时流计算是提供自动切割的一种机制-窗口。
Flink_窗口_Window_ide_03
窗口实际就是一个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.窗口分类

  • 时间窗口
  • 计数窗口
    Flink_窗口_Window_设计原则_04

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 滚动窗口

Flink_窗口_Window_java_05
滚动窗口可以基于时间定义,也可以基于数据个数定义;需要的参数只有一个,就是窗口的大小(window size)。比如我们可以定义一个长度为 1 小时的滚动时间窗口,那么每个小时就会进行一次统计;或者定义一个长度为 10 的滚动计数窗口,就会每 10 个数进行一次统计。

小圆点表示流中的数据,我们对数据按照 userId 做了分区。当固定了窗口大小之后,所有分区的窗口划分都是一致的;窗口没有重叠,每个数据只属于一个窗口。

3.2 滑动窗口

Flink_窗口_Window_设计模式_06
当滑动步长小于窗口大小时,滑动窗口就会出现重叠,这时数据也可能会被同时分配到多个窗口中。

滚动窗口是特殊的滑动窗口, 相当于滚动的size = slidw

3.3 会话窗口

Flink_窗口_Window_设计模式_07
会话窗口的长度不固定,起始和结束时间也是不确定的,各个分区之间窗口没有任何关联。 会话窗口之间一定是不会重叠的,而
且会留有至少为 size 的间隔(session gap)。

3.4 全局窗口

Flink_窗口_Window_ide_08
全局窗口没有结束的时间点,所以一般在希望做更加灵活的窗口处理时自定义使用。Flink 中的计数窗口(Count Window),底层就是用全局窗口实现的。

这种窗口全局有效,会把相同 key 的所有数据都分配到同一个窗口中;说直白一点,就跟没分窗口一样。

4.窗口Api

  • 按键分区 Keyed
  • 非按键分区 Non-Keyed

4.1 按键分区窗口

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

4.2 非按键分区

stream.windowAll(...)

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;
}
}
}

Flink_窗口_Window_ide_09
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();

}
}

Flink_窗口_Window_数据_10

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));
}
}
}

Flink_窗口_Window_设计模式_11