窗口概念
在大多数场景下,我们需要统计的数据流都是无界的,因此我们无法等待整个数据流终止后才进行统计。通常情况下,我们只需要对某个时间范围或者数量范围内的数据进行统计分析:如每隔五分钟统计一次过去一小时内所有商品的点击量;或者每发生1000次点击后,都去统计一下每个商品点击率的占比。在 Flink 中,我们使用窗口 (Window) 来实现这类功能。按照统计维度的不同,Flink 中的窗口可以分为 时间窗口 (Time Windows) 和 计数窗口 (Count Windows) 。
窗口(window)就是将无限流切割为有限流的一种方式,它会将流数据分发到有限大小的桶(bucket)中进行分析。
时间语义
在Flink中,如果以时间段划分边界的话,那么时间就是一个极其重要的字段。
Flink中的时间有三种类型,如下图所示:
- Event Time:是事件创建的时间。它通常由事件中的时间戳描述,例如采集的日志数据中,每一条日志都会记录自己的生成时间,Flink通过时间戳分配器访问事件时间戳。
- Ingestion Time:数据进入Flink的时间,进入datasource的时间
- Processing Time:是每一个执行基于时间操作的算子的本地系统时间,与机器相关,默认的时间属性就是Processing Time。
我们往往更关心事件时间(Event Time)
Window
官方解释:流式计算是一种被设计用于处理无限数据集的数据处理引擎,而无限数据集是指一种不断增长的本质上无限的数据集,而window是一种切割无限数据为有限块进行处理的手段。
所以Window是无限数据流处理的核心,Window将一个无限的stream拆分成有限大小的”buckets”桶,我们可以在这些桶上做计算操作。
并且开窗操作必须在keyby之后才能进行。
划分窗口就两种方式:
时间窗口(Time Window)
- 滚动时间窗口
- 滑动时间窗口
- 会话窗口
计数窗口(Count Window) - 滚动计数窗口
- 滑动计数窗口
Time Windows
Time Windows 用于以时间为维度来进行数据聚合。
滚动窗口(Tumbling Window)
滚动窗口 (Tumbling Windows) 是指彼此之间没有重叠的窗口。例如:每隔1小时统计过去1小时内的商品点击量,那么 1 天就只能分为 24 个窗口,每个窗口彼此之间是不存在重叠的,具体如下:
图解:
- user1,user2,user3代表不同的分组。
- 滚动窗口分配器将每个元素分配到一个指定窗口大小的窗口中,滚动窗口有一个固定的大小,并且不会出现重叠。
TumblingTimeWindow API
假设滚动窗口的大小为15s,那么滑动步长也是15s,也就是每隔20s做一次计算并输出,然后滑动到下一个窗口。
输入数据:
sensor_1,1547718147,12
sensor_6,1547718201,15.0
sensor_6,1547718202,15.0
sensor_6,1547718203,6.7
sensor_8,1547718204,38.0
sensor_8,1547718201,38.0
sensor_8,1547718202,15.4
sensor_1,1547718203,6.7
sensor_6,1547718204,38.1
import org.apache.commons.collections.IteratorUtils;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.SlidingProcessingTimeWindows;
import org.apache.flink.streaming.api.windowing.assigners.TumblingProcessingTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
public class TransFormTimeWindow {
public static void main(String[] args) throws Exception {
// 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 从nc中读取数据
DataStreamSource<String> stringDataStreamSource = env.socketTextStream("localhost", 7777);
// map操作
SingleOutputStreamOperator<SensorReading2> dataStream = stringDataStreamSource.map((String value) -> {
String[] fields = value.split(",");
return new SensorReading2(fields[0], new Long(fields[1]), new Double(fields[2]));
}).setParallelism(1);
KeyedStream<SensorReading2, String> keyedStream = dataStream.
keyBy((SensorReading2 value) -> {
return value.getId();
});
keyedStream
// 效果就是一个滚动时间窗口,以Processing Time为基准,每一个分组到达15s就统计一次并输出结果
.window(TumblingProcessingTimeWindows.of(Time.seconds(15)))
.aggregate(new AggregateFunction<SensorReading2, Integer, Integer>() {
/*
* @param <IN> The type of the values that are aggregated (input values)
* @param <ACC> The type of the accumulator (intermediate aggregate state).
* @param <OUT> The type of the aggregated result
*/
/*
创建累加器,给定初始值
*/
@Override
public Integer createAccumulator() {
return 0;
}
/*
对数据进行聚合
*/
@Override
public Integer add(SensorReading2 value, Integer accumulator) {
return accumulator + 1;
}
/*
累加器作为输出结果
*/
@Override
public Integer getResult(Integer accumulator) {
return accumulator;
}
/*
一般不用于滚动和滑动窗口,而用于session窗口
*/
@Override
public Integer merge(Integer a, Integer b) {
return null;
}
}).print();
env.execute();
}
public static class MyFlatMapper implements FlatMapFunction<String, Tuple2<String, Integer>> {
private static final long serialVersionUID = -5224012503623543819L;
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception{
// 分词
String[] words = value.split(" ");
for (String word : words){
out.collect(new Tuple2<String,Integer>(word,1));
}
}
}
}
滑动窗口(Sliding Windows)
滑动窗口是固定窗口的更广义的一种形式,滑动窗口由固定的窗口长度和滑动间隔组成。不过滚动窗口的滑动步长等于窗口长度。一般来说滑动步长应该小于窗口大小。
图解:
- 比较重要的是window要理解滑动窗口的初始状态
- 如果是时间滑动窗口,当过了滑动步长的时间的时候,就应该在窗口内做一次统计。比如,滑动窗口15s,滑动步长5s,也就是初始状态过了5s就开始统计,但是窗口没有到达15s怎么办?这时候初始状态就是当成15s的窗口。这时候又过了5s,也就是一共过了10s,又会做一次统计,但是是在10s内做的一次统计,同理当成15s。又过了5s,这个时候一个15s窗口才出现,从此之后就会过5s完整统计15s内的数据,前几次可能会有误差。
- 同理,如果是计数滑动窗口,例如,滑动窗口10个数据,滑动步长为5个数据,这个时候,初始状态一旦数据到了5个数据就会做一次统计,但是窗口还没有到10个数据,也就把它当成10个数据,然后开始滑步,每过5个,就在10个数据中做一统计。
SlidingTimeWindow API
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.SlidingProcessingTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.benchmark.SensorReading2;
public class TransFormSlidingTimeWindow {
public static void main(String[] args)throws Exception {
// 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 从nc中读取数据
DataStreamSource<String> stringDataStreamSource = env.socketTextStream("localhost", 7777);
// map操作
SingleOutputStreamOperator<SensorReading2> dataStream = stringDataStreamSource.map((String value) -> {
String[] fields = value.split(",");
return new SensorReading2(fields[0], new Long(fields[1]), new Double(fields[2]));
}).setParallelism(2);
KeyedStream<SensorReading2, String> keyedStream = dataStream.keyBy((SensorReading2 value) -> {
return value.getId();
});
keyedStream
// 每隔15s做一次统计,初始状态就算没到30s也要记继续滑动
.window(SlidingProcessingTimeWindows.of(Time.seconds(30),Time.seconds(15)))
// 增量聚合函数,对数据个数进行统计
.aggregate(new AggregateFunction<SensorReading2, Integer, Integer>() {
/*
* @param <IN> The type of the values that are aggregated (input values)
* @param <ACC> The type of the accumulator (intermediate aggregate state).
* @param <OUT> The type of the aggregated result
*/
/*
创建累加器,给定初始值
*/
@Override
public Integer createAccumulator() {
return 0;
}
/*
对数据进行聚合
*/
@Override
public Integer add(SensorReading2 value, Integer accumulator) {
return accumulator + 1;
}
/*
累加器作为输出结果
*/
@Override
public Integer getResult(Integer accumulator) {
return accumulator;
}
/*
一般不用于滚动和滑动窗口,而用于session窗口
*/
@Override
public Integer merge(Integer a, Integer b) {
return a + b;
}
}).print();
env.execute();
}
}
会话窗口
session窗口分配器通过session活动来对元素进行分组,session窗口跟滚动窗口和滑动窗口相比,不会有重叠和固定的开始时间和结束时间的情况,相反,当它在一个固定的时间周期内不再收到元素,即非活动间隔产生,那个这个窗口就会关闭。一个session窗口通过一个session间隔来配置,这个session间隔定义了非活跃周期的长度,当这个非活跃周期产生,那么当前的session将关闭并且后续的元素将被分配到新的session窗口中去。
总的来说就是设定一个时间间隔,如果这个间隔内没有数据,就会开启另一个窗口
SessionWindow API
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.ProcessingTimeSessionWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.benchmark.SensorReading2;
public class TransFormTimeSessionWindow {
public static void main(String[] args)throws Exception {
// 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 从nc中读取数据
DataStreamSource<String> stringDataStreamSource = env.socketTextStream("localhost", 7777);
// map操作
SingleOutputStreamOperator<SensorReading2> dataStream = stringDataStreamSource.map((String value) -> {
String[] fields = value.split(",");
return new SensorReading2(fields[0], new Long(fields[1]), new Double(fields[2]));
}).setParallelism(2);
KeyedStream<SensorReading2, String> keyedStream = dataStream.keyBy((SensorReading2 value) -> {
return value.getId();
});
keyedStream
// 过了15s还没有来数据,就计算一次
.window(ProcessingTimeSessionWindows.withGap(Time.seconds(15)))
// 增量聚合函数,对数据个数进行统计
.aggregate(new AggregateFunction<SensorReading2, Integer, Integer>() {
/*
* @param <IN> The type of the values that are aggregated (input values)
* @param <ACC> The type of the accumulator (intermediate aggregate state).
* @param <OUT> The type of the aggregated result
*/
/*
创建累加器,给定初始值
*/
@Override
public Integer createAccumulator() {
return 0;
}
/*
对数据进行聚合
*/
@Override
public Integer add(SensorReading2 value, Integer accumulator) {
return accumulator + 1;
}
/*
累加器作为输出结果
*/
@Override
public Integer getResult(Integer accumulator) {
return accumulator;
}
/*
一般不用于滚动和滑动窗口,而用于session窗口
*/
@Override
public Integer merge(Integer a, Integer b) {
return a + b;
}
}).print();
env.execute();
}
}
CountWindow
CountWindow根据窗口中相同key元素的数量来触发执行,执行时只计算元素数量达到窗口大小的key对应的结果。
滚动窗口(Tumbling Window)
CountWindow的window_size指的是相同Key的元素的个数,不是输入的所有元素的总数。例如,滚动窗口大小为10个,一个key为1的数据流到达10个就做一次统计,一个key为2的数据流到达了5个就不做你统计。
Tumbling CountWindow API
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.benchmark.SensorReading2;
public class TransFormTumblingCountWindow {
public static void main(String[] args) throws Exception{
// 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 从nc中读取数据
DataStreamSource<String> stringDataStreamSource = env.socketTextStream("localhost", 7777);
// map操作
SingleOutputStreamOperator<SensorReading2> dataStream = stringDataStreamSource.map((String value) -> {
String[] fields = value.split(",");
return new SensorReading2(fields[0], new Long(fields[1]), new Double(fields[2]));
}).setParallelism(2);
KeyedStream<SensorReading2, String> keyedStream = dataStream.keyBy((SensorReading2 value) -> {
return value.getId();
});
keyedStream
// 每5个做一次统计数据数量,所以输出结果一直为5
.countWindow(5)
// 增量聚合函数,对数据个数进行统计
.aggregate(new AggregateFunction<SensorReading2, Integer, Integer>() {
/*
* @param <IN> The type of the values that are aggregated (input values)
* @param <ACC> The type of the accumulator (intermediate aggregate state).
* @param <OUT> The type of the aggregated result
*/
/*
创建累加器,给定初始值
*/
@Override
public Integer createAccumulator() {
return 0;
}
/*
对数据进行聚合
*/
@Override
public Integer add(SensorReading2 value, Integer accumulator) {
return accumulator + 1;
}
/*
累加器作为输出结果
*/
@Override
public Integer getResult(Integer accumulator) {
return accumulator;
}
/*
一般不用于滚动和滑动窗口,而用于session窗口
*/
@Override
public Integer merge(Integer a, Integer b) {
return a + b;
}
}).print();
env.execute();
}
}
滑动窗口(Sliding Windows)
同样也是窗口长度和滑动窗口的操作:窗口长度是10,滑动长度是5,每隔5个做一次统计,但是初始状态到了5个数据的滑动长度却没有到达10个数据的窗口长度,所以初始状态窗口大小可以看做是10个数据的窗口,其余的位置为空。
如下图,窗口1中只有五个数据那么就对这五个数据做一次统计,滑动之后,才能达到10个数据大小,这个时候才是对10个数据进行统计。
Sliding CountWindow API
这里就做了一个计算数据个数的功能。
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.benchmark.SensorReading2;
public class TransFormSlidingCountWindow {
public static void main(String[] args)throws Exception {
// 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 从nc中读取数据
DataStreamSource<String> stringDataStreamSource = env.socketTextStream("localhost", 7777);
// map操作
SingleOutputStreamOperator<SensorReading2> dataStream = stringDataStreamSource.map((String value) -> {
String[] fields = value.split(",");
return new SensorReading2(fields[0], new Long(fields[1]), new Double(fields[2]));
}).setParallelism(2);
KeyedStream<SensorReading2, String> keyedStream = dataStream.keyBy((SensorReading2 value) -> {
return value.getId();
});
keyedStream
// 初始状态就是5个,对这5个数据进行计算输出,之后每个窗口就是10个,对诗歌数据进行计算
.countWindow(10,5)
// 增量聚合函数,对数据个数进行统计
.aggregate(new AggregateFunction<SensorReading2, Integer, Integer>() {
/*
* @param <IN> The type of the values that are aggregated (input values)
* @param <ACC> The type of the accumulator (intermediate aggregate state).
* @param <OUT> The type of the aggregated result
*/
/*
创建累加器,给定初始值
*/
@Override
public Integer createAccumulator() {
return 0;
}
/*
对数据进行聚合
*/
@Override
public Integer add(SensorReading2 value, Integer accumulator) {
return accumulator + 1;
}
/*
累加器作为输出结果
*/
@Override
public Integer getResult(Integer accumulator) {
return accumulator;
}
/*
一般不用于滚动和滑动窗口,而用于session窗口
*/
@Override
public Integer merge(Integer a, Integer b) {
return a + b;
}
}).print();
env.execute();
}
}
窗口函数(window function)
在窗口处理之后,还需要聚合操作,如下所示:
可以分为两类
增量聚合函数(incremental aggregation functions)
- 每条数据到来就进行计算,保持一个简单的状态
- ReduceFunction, AggregateFunction
除此之外还可以直接使用max,maxby,sum等
全窗口函数(full window functions)
主要是使用apply函数
- 先把窗口所有数据收集起来,等到计算的时候会遍历所有数据
- ProcessWindowFunction,WindowFunction
ProcessWindowFunction和WindowFunction基本功能一直,不过ProcessWindowFunction可以获取的信息更多。
其它可选 API
- .trigger() —— 触发器:定义 window 什么时候关闭,触发计算并输出结果
- .evictor() —— 移除器:定义移除某些数据的逻辑
- .allowedLateness() —— 允许处理迟到的数据
- sideOutputLateData() —— 将迟到的数据放入侧输出流,这个和上面的允许处理迟到的数据配合使用,但是只能用于时间时间语义
- getSideOutput() —— 获取侧输出流