窗口概述
在流处理应用中,数据是连续不断的,因此我们不可能等到所有数据都到了才开始处理。当然我们可以每来一个消息就处理一次,但是有时我们需要做一些聚合类的处理,例如:在过去的1分钟内有多少用户点击了我们的网页。在这种情况下,我们必须定义一个窗口,用来收集最近一分钟内的数据,并对这个窗口内的数据进行计算。
流式计算是一种被设计用于处理无限数据集的数据处理引擎,而无限数据集是指一种不断增长的本质上无限的数据集,而Window窗口是一种切割无限数据为有限块进行处理的手段。
在Flink中, 窗口(window)是处理无界流的核心. 窗口把流切割成有限大小的多个"存储桶"(bucket), 我们在这些桶上进行计算.
窗口的分类
窗口分为2类:
- 基于时间的窗口(时间驱动)
- 基于元素个数的(数据驱动)
WindowAssigner
WindowAssigner 是一个抽象类,是所有窗口策略父类。
public abstract class WindowAssigner implements Serializable {}
WindowAssigner下所有的子类
SlidingProcessingTimeWindows
(org.apache.flink.streaming.api.windowing.assigners)
按照处理时间进行窗口滑动
of
的重载方法
- of(Time size, Time slide)
size:窗口长度
slide:滑动步长 - of(Time size, Time slide, Time offset)
offset:时区
BaseAlignedWindowAssigner
(org.apache.flink.streaming.api.windowing.assigners)
TumblingEventTimeWindows
(org.apache.flink.streaming.api.windowing.assigners)
TumblingTimeWindows
(org.apache.flink.streaming.api.windowing.assigners)
MergingWindowAssigner
(org.apache.flink.streaming.api.windowing.assigners)
DynamicProcessingTimeSessionWindows
(org.apache.flink.streaming.api.windowing.assigners)
ProcessingTimeSessionWindows
(org.apache.flink.streaming.api.windowing.assigners)
按照处理时间划分session,该方式为静态gap。
withGap :窗口间隔时长,gap时间内的会划为一个窗口,这是一个合并的操作,这也说明为啥是MergingWindowAssigner
子类的原因。
DynamicEventTimeSessionWindows
(org.apache.flink.streaming.api.windowing.assigners)
EventTimeSessionWindows
(org.apache.flink.streaming.api.windowing.assigners)
TumblingProcessingTimeWindows
(org.apache.flink.streaming.api.windowing.assigners)
按照处理时间进行窗口滚动
of
的重载方法
- of(Time size)
size:窗口长度 - of(Time size, Time offset)
offset:配置时区 - of(Time size, Time offset, WindowStagger windowStagger)
windowStagger:窗口交错
SlidingEventTimeWindows
(org.apache.flink.streaming.api.windowing.assigners)
SlidingTimeWindows
(org.apache.flink.streaming.api.windowing.assigners)
GlobalWindows
(org.apache.flink.streaming.api.windowing.assigners)
Time
public static Time of(long size, TimeUnit unit) {
return new Time(size, unit);
}
/** Creates a new {@link Time} that represents the given number of milliseconds. */
public static Time milliseconds(long milliseconds) {
return of(milliseconds, TimeUnit.MILLISECONDS);
}
/** Creates a new {@link Time} that represents the given number of seconds. */
public static Time seconds(long seconds) {
return of(seconds, TimeUnit.SECONDS);
}
/** Creates a new {@link Time} that represents the given number of minutes. */
public static Time minutes(long minutes) {
return of(minutes, TimeUnit.MINUTES);
}
/** Creates a new {@link Time} that represents the given number of hours. */
public static Time hours(long hours) {
return of(hours, TimeUnit.HOURS);
}
/** Creates a new {@link Time} that represents the given number of days. */
public static Time days(long days) {
return of(days, TimeUnit.DAYS);
}
WindowStagger
public enum WindowStagger {
/** Default mode, all panes fire at the same time across all partitions. */
ALIGNED {
@Override
public long getStaggerOffset(final long currentProcessingTime, final long size) {
return 0L;
}
},
/**
* Stagger offset is sampled from uniform distribution U(0, WindowSize) when first event
* ingested in the partitioned operator.
*/
RANDOM {
@Override
public long getStaggerOffset(final long currentProcessingTime, final long size) {
return (long) (ThreadLocalRandom.current().nextDouble() * size);
}
},
/**
* When the first event is received in the window operator, take the difference between the
* start of the window and current procesing time as the offset. This way, windows are staggered
* based on when each parallel operator receives the first event.
*/
NATURAL {
@Override
public long getStaggerOffset(final long currentProcessingTime, final long size) {
final long currentProcessingWindowStart =
TimeWindow.getWindowStartWithOffset(currentProcessingTime, 0, size);
return Math.max(0, currentProcessingTime - currentProcessingWindowStart);
}
};
public abstract long getStaggerOffset(final long currentProcessingTime, final long size);
}
基于时间的窗口
时间窗口包含一个开始时间戳(包括)和结束时间戳(不包括), 这两个时间戳一起限制了窗口的尺寸.
在代码中, Flink使用TimeWindow这个类来表示基于时间的窗口. 这个类提供了key查询开始时间戳和结束时间戳的方法, 还提供了针对给定的窗口获取它允许的最大时间戳的方法(maxTimestamp())。
时间窗口可以理解成一个桶,装有各种各样的元素,
相同的元素会存放到同一个窗口(如下图),统计窗口05的数据,其实每个元素都有05的窗口。
注意:(a,4),表示有 4个a,而不是a的个数为4。
时间窗口又分4种:
1.滚动窗口(Tumbling Windows)
滚动窗口有固定的大小, 窗口与窗口之间不会重叠也没有缝隙.比如,如果指定一个长度为5分钟的滚动窗口, 当前窗口开始计算, 每5分钟启动一个新的窗口。
滚动窗口能将数据流切分成不重叠的窗口,每一个事件只能属于一个窗口。
- 没有间隙
- 没有重叠
- 窗口长度
public static void main(String[] args) throws Exception {
Configuration config=new Configuration();
config.setInteger("rest.port",8081); // 配置固定端口
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(config);
DataStreamSource source = env.socketTextStream("mydocker", 9999);
// 扁平化
SingleOutputStreamOperator> flatMap = source.flatMap((FlatMapFunction>) (value, out) -> {
Arrays.stream(value.split(" ")).forEach(s -> out.collect(Tuple2.of(s, 1L)));
}).returns(Types.TUPLE(Types.STRING, Types.LONG));
// 聚合
KeyedStream, String> keyBy = flatMap.keyBy(s -> s.f0);
// 设置窗口为5秒
WindowedStream, String, TimeWindow> window = keyBy.window(TumblingProcessingTimeWindows.of(Time.seconds(5l)));
// 聚合
window.sum(1).print();
env.execute();
}
输入
java python java scala
输出:并不会立即输出,需要等待窗口关闭之后才能进行输出
5> (python,2)
1> (scala,1)
3> (java,3)
2.滑动窗口(Sliding Windows)
与滚动窗口一样, 滑动窗口也是有固定的长度. 另外一个参数我们叫滑动步长, 用来控制滑动窗口启动的频率.
所以, 如果滑动步长小于窗口长度, 滑动窗口会重叠. 这种情况下, 一个元素可能会被分配到多个窗口中
例如, 滑动窗口长度10分钟, 滑动步长5分钟, 则, 每5分钟会得到一个包含最近10分钟的数据.
- 固定长度
- 滑动步长
- 滑动步长<窗口长度,会造成数据重复
- 滑动步长>窗口长度,会造成数据丢失
public static void main(String[] args) throws Exception {
Configuration config=new Configuration();
config.setInteger("rest.port",8081); // 配置固定端口
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(config);
DataStreamSource source = env.socketTextStream("mydocker", 9999);
// 扁平化
SingleOutputStreamOperator> flatMap = source.flatMap((FlatMapFunction>) (value, out) -> {
Arrays.stream(value.split(" ")).forEach(s -> out.collect(Tuple2.of(s, 1L)));
}).returns(Types.TUPLE(Types.STRING, Types.LONG));
// 聚合
KeyedStream, String> keyBy = flatMap.keyBy(s -> s.f0);
//窗口长度为5,没3秒统计一次。
SlidingProcessingTimeWindows windows = SlidingProcessingTimeWindows.of(Time.seconds(5),Time.seconds(3));
WindowedStream, String, TimeWindow> window = keyBy.window(windows);
// 聚合
window.sum(1).print();
env.execute();
}
输出
12> (,1)
5> (python,9)
3> (java,9)
5> (python,2)
3> (java,2)
3.会话窗口(Session Windows)
会话窗口分配器会根据活动的元素进行分组. 会话窗口不会有重叠, 与滚动窗口和滑动窗口相比, 会话窗口也没有固定的开启和关闭时间.
如果会话窗口有一段时间没有收到数据, 会话窗口会自动关闭, 这段没有收到数据的时间就是会话窗口的gap(间隔)
我们可以配置静态的gap, 也可以通过一个gap extractor 函数来定义gap的长度. 当时间超过了这个gap, 当前的会话窗口就会关闭, 后序的元素会被分配到一个新的会话窗口
- 按照key进行分组(划分一个新的窗口)
- 不会造成数据重叠
- 没有固定开启和关闭时间
- 若一段时间内没有数据,窗口自动关闭
- 可以设置静态gap和动态gap
静态 gap
ProcessingTimeSessionWindows windows = ProcessingTimeSessionWindows.withGap(Time.seconds(3));
@Test
public void test1() throws Exception {
Configuration config=new Configuration();
config.setInteger("rest.port",8081); // 配置固定端口
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(config);
DataStreamSource source = env.socketTextStream("mydocker", 9999);
// 扁平化
SingleOutputStreamOperator> flatMap = source.flatMap((FlatMapFunction>) (value, out) -> {
Arrays.stream(value.split(" ")).forEach(s -> out.collect(Tuple2.of(s, 1L)));
}).returns(Types.TUPLE(Types.STRING, Types.LONG));
// 聚合
KeyedStream, String> keyBy = flatMap.keyBy(s -> s.f0);
// 设置session时间为3秒
ProcessingTimeSessionWindows windows = ProcessingTimeSessionWindows.withGap(Time.seconds(3));
WindowedStream, String, TimeWindow> window = keyBy.window(windows);
// 聚合
window.sum(1).print();
env.execute();
}
动态 gap
DynamicProcessingTimeSessionWindows
@Test
public void test2() throws Exception {
Configuration config=new Configuration();
config.setInteger("rest.port",8081); // 配置固定端口
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(config);
DataStreamSource source = env.socketTextStream("mydocker", 9999);
// 扁平化
SingleOutputStreamOperator> flatMap =
source.flatMap((FlatMapFunction>) (value, out) -> {
Arrays.stream(value.split(" ")).forEach(s -> out.collect(Tuple2.of(s, 1L)));
}).returns(Types.TUPLE(Types.STRING, Types.LONG));
// 聚合
KeyedStream, String> keyBy = flatMap.keyBy(s -> s.f0);
// 设置session时间为3秒
DynamicProcessingTimeSessionWindows
session会将gap时间范围内的窗口合并成一个窗口。
例如:
a1,a2,a3,a4,b1,b2,b3,b4
首先取最后元素(a1)的时间,然后与下一个元素(a2)进行匹配,若两个元素之间的时间进行比较是否超过
gap
,若没有则将他们划分为一个窗口,然后继续与下一个元素(a3)进行比较,以此类推。直到与某个元素的时间超过了gap
就划分为另一个窗口。
4.全局窗口(Global Windows)
全局窗口分配器会分配相同key的所有元素进入同一个 Global window. 这种窗口机制只有指定自定义的触发器时才有用. 否则, 不会做任何计算, 因为这种窗口没有能够处理聚集在一起元素的结束点.
- 需要指定触发器
创建全局窗口
// 创建 全局窗口
WindowedStream, String, GlobalWindow> window =
keyBy.window(org.apache.flink.streaming.api.windowing.assigners.GlobalWindows.create());
@Test
public void test1() throws Exception {
Configuration config=new Configuration();
config.setInteger("rest.port",8081); // 配置固定端口
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(config);
DataStreamSource source = env.socketTextStream("mydocker", 9999);
// 扁平化
SingleOutputStreamOperator> flatMap = source.flatMap((FlatMapFunction>) (value, out) -> {
Arrays.stream(value.split(" ")).forEach(s -> out.collect(Tuple2.of(s, 1L)));
}).returns(Types.TUPLE(Types.STRING, Types.LONG));
// 聚合
KeyedStream, String> keyBy = flatMap.keyBy(s -> s.f0);
// 创建 全局窗口
WindowedStream, String, GlobalWindow> window = keyBy.window(org.apache.flink.streaming.api.windowing.assigners.GlobalWindows.create());
window.process(new ProcessWindowFunction, Object, String, GlobalWindow>() {
/**
*
* @param key 聚合元素
* @param context 上下文
* @param elements 窗口所有的元素
* @param out 收集器
* @throws Exception
*/
@Override
public void process(String key, Context context, Iterable> elements, Collector
输入:
s
s
s
ss
s
s
s
s
s
s
s
输出:此时无论输入了多个元素,都不会触发计算。
所以需要设置触发器
,由我们来设置触发条件。
Trigger
public abstract class Trigger implements Serializable {
Trigger 下所有的子类
ProcessingTimeoutTrigger
(org.apache.flink.streaming.api.windowing.triggers)
基于处理时间超时的触发器
CountTrigger
(org.apache.flink.streaming.api.windowing.triggers)
基于元素个数的触发器
EventTimeTrigger
(org.apache.flink.streaming.api.windowing.triggers)
基于事件时间的触发器
DeltaTrigger
(org.apache.flink.streaming.api.windowing.triggers)
此触发器计算上次触发的数据点与当前到达的数据点之间的增量。如果 delta 高于指定的阈值,它就会触发。
NeverTrigger in GlobalWindows
(org.apache.flink.streaming.api.windowing.assigners)
永远不会触发的触发器,作为 GlobalWindows 的默认触发器。
ContinuousEventTimeTrigger
(org.apache.flink.streaming.api.windowing.triggers)
根据给定的时间间隔连续触发,需要指定水位(Watermarks)
PurgingTrigger
(org.apache.flink.streaming.api.windowing.triggers)
当嵌套触发器触发时,这将返回一个TriggerResult
ContinuousProcessingTimeTrigger
(org.apache.flink.streaming.api.windowing.triggers)
根据运行作业的机器的时钟测量的给定时间间隔连续触发。
ProcessingTimeTrigger
(org.apache.flink.streaming.api.windowing.triggers)
一旦当前系统时间超过窗格所属窗口的末尾,就会触发。
上面的都不会使用,自定义count触发器
// 添加 触发器
window.trigger(new Trigger, GlobalWindow>() {
/**
* 来一个元素触发一次
* @param element
* @param timestamp
* @param window
* @param ctx
* @return
* @throws Exception
*/
@Override
public TriggerResult onElement(Tuple2 element, long timestamp, GlobalWindow window, TriggerContext ctx) throws Exception {
return null;
}
/**
* 基于处理时间
* @param time
* @param window
* @param ctx
* @return
* @throws Exception
*/
@Override
public TriggerResult onProcessingTime(long time, GlobalWindow window, TriggerContext ctx) throws Exception {
return null;
}
/**
* 基于事件时间
* @param time
* @param window
* @param ctx
* @return
* @throws Exception
*/
@Override
public TriggerResult onEventTime(long time, GlobalWindow window, TriggerContext ctx) throws Exception {
return null;
}
/**
* 触发器执行后,清空窗口元素
* @param window
* @param ctx
* @throws Exception
*/
@Override
public void clear(GlobalWindow window, TriggerContext ctx) throws Exception {
}
});
TriggerResult
public enum TriggerResult {
/** 没有对窗口采取任何操作。*/
CONTINUE(false, false),
/** {@code FIRE_AND_PURGE} 计算window函数并发出window结果。*/
FIRE_AND_PURGE(true, true),
/**
* On {@code FIRE}, 窗口被评估并发出结果。窗户没有清洗,
*但是,所有的元素都被保留了。
*/
FIRE(true, false),
/**
* 属性的值将被清除并丢弃窗口中的所有元素
* 窗口函数或发出任何元素。
*/
PURGE(false, true);
}
自定义触发器
@Test
public void test1() throws Exception {
Configuration config=new Configuration();
config.setInteger("rest.port",8081); // 配置固定端口
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(config);
DataStreamSource source = env.socketTextStream("mydocker", 9999);
// 扁平化
SingleOutputStreamOperator> flatMap = source.flatMap((FlatMapFunction>) (value, out) -> {
Arrays.stream(value.split(" ")).forEach(s -> out.collect(Tuple2.of(s, 1L)));
}).returns(Types.TUPLE(Types.STRING, Types.LONG));
// 聚合
KeyedStream, String> keyBy = flatMap.keyBy(s -> s.f0);
// 创建 全局窗口
WindowedStream, String, GlobalWindow> window = keyBy.window(org.apache.flink.streaming.api.windowing.assigners.GlobalWindows.create());
// 添加 触发器
window.trigger(new Trigger, GlobalWindow>() {
int count=0;
/**
* 来一个元素触发一次
* @param element
* @param timestamp
* @param window
* @param ctx
* @return
* @throws Exception
*/
@Override
public TriggerResult onElement(Tuple2 element, long timestamp, GlobalWindow window, TriggerContext ctx) throws Exception {
// 三个元素发送一次
if (count>=3){
count=0;
return TriggerResult.FIRE_AND_PURGE;
}
count++;
// 超时不发送
return TriggerResult.CONTINUE;
}
/**
* 基于处理时间
* @param time
* @param window
* @param ctx
* @return
* @throws Exception
*/
@Override
public TriggerResult onProcessingTime(long time, GlobalWindow window, TriggerContext ctx) throws Exception {
return null;
}
/**
* 基于事件时间
* @param time
* @param window
* @param ctx
* @return
* @throws Exception
*/
@Override
public TriggerResult onEventTime(long time, GlobalWindow window, TriggerContext ctx) throws Exception {
return null;
}
/**
* 触发器执行后,清空窗口元素
* @param window
* @param ctx
* @throws Exception
*/
@Override
public void clear(GlobalWindow window, TriggerContext ctx) throws Exception {
}
});
window.process(new ProcessWindowFunction, Object, String, GlobalWindow>() {
/**
*
* @param key 聚合元素
* @param context 上下文
* @param elements 窗口所有的元素
* @param out 收集器
* @throws Exception
*/
@Override
public void process(String key, Context context, Iterable> elements, Collector
输入
a
a
a
bb
bb
bb
cc
cc
cc
a
cc
bb
输出
11> key=a,window=GlobalWindow ,data=[(a,1), (a,1), (a,1), (a,1)]
16> key=cc,window=GlobalWindow ,data=[(cc,1), (cc,1), (cc,1), (cc,1)]
9> key=bb,window=GlobalWindow ,data=[(bb,1), (bb,1), (bb,1), (bb,1)]
基于元素个数的窗口
按照指定的数据条数生成一个Window,与时间无关
若有N个元素
a、b、c、b、a、c、a、c、b、a、a、b、c
那么会划为三个窗口,不同元素划分为一个窗口(a窗口,b窗口,c窗口
)。
分2类:
1.滚动窗口
默认的CountWindow是一个滚动窗口,只需要指定窗口大小即可,当元素数量达到窗口大小时,就会触发窗口的执行。
WindowedStream, String, GlobalWindow> window = keyBy.countWindow(3); //指定元素窗口的大小
@Test
public void test1() throws Exception {
Configuration config=new Configuration();
config.setInteger("rest.port",8081); // 配置固定端口
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(config);
DataStreamSource source = env.socketTextStream("mydocker", 9999);
// 扁平化
SingleOutputStreamOperator> flatMap = source.flatMap((FlatMapFunction>) (value, out) -> {
Arrays.stream(value.split(" ")).forEach(s -> out.collect(Tuple2.of(s, 1L)));
}).returns(Types.TUPLE(Types.STRING, Types.LONG));
// 聚合
KeyedStream, String> keyBy = flatMap.keyBy(s -> s.f0);
WindowedStream, String, GlobalWindow> window = keyBy.countWindow(3);
// 聚合
window.sum(1).print();
env.execute();
}
}
输入
a
b
c
a
b
c
a
b
c
输出
11> (a,3)
3> (b,3)
8> (c,3)
说明:哪个窗口先达到3个元素, 哪个窗口就关闭. 不影响其他的窗口.
2.滑动窗口
滑动窗口和滚动窗口的函数名是完全一致的,只是在传参数时需要传入两个参数,一个是window_size,一个是sliding_size。下面代码中的sliding_size设置为了2,也就是说,每收到两个相同key的数据就计算一次,每一次计算的window范围最多是3个元素。
WindowedStream, String, GlobalWindow> window = keyBy.countWindow(窗口查长度, 滑动长度);
@Test
public void test2() throws Exception {
Configuration config=new Configuration();
config.setInteger("rest.port",8081); // 配置固定端口
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(config);
DataStreamSource source = env.socketTextStream("mydocker", 9999);
// 扁平化
SingleOutputStreamOperator> flatMap = source.flatMap((FlatMapFunction>) (value, out) -> {
Arrays.stream(value.split(" ")).forEach(s -> out.collect(Tuple2.of(s, 1L)));
}).returns(Types.TUPLE(Types.STRING, Types.LONG));
// 聚合
KeyedStream, String> keyBy = flatMap.keyBy(s -> s.f0);
WindowedStream, String, GlobalWindow> window = keyBy.countWindow(5, 3);
// 聚合
window.sum(1).print();
env.execute();
}
输入(注意这里有6个bb)
bb
bb
bb
bb
bb
bb
输出
9> (bb,3) // 3个输出一次
9> (bb,5) // 5个输出一个
每来3三个元素会关闭窗口,将触发窗口计算。5表示,最多取5个。
窗口位置(Keyed vs Non-Keyed Windows)
其实, 在用window前首先需要确认应该是在keyBy后的流上用, 还是在没有keyBy的流上使用.
在keyed streams上使用窗口, 窗口计算被并行的运用在多个task上, 可以认为每个task都有自己单独窗口. 正如前面的代码所示.
在非non-keyed stream上使用窗口, 流的并行度只能是1
, 所有的窗口逻辑只能在一个单独的task上执行.
.windowAll(TumblingProcessingTimeWindows.of(Time.seconds(10)))
需要注意的是: 非key分区的流上使用window, 如果把并行度强行设置为>1, 则会抛出异常