Flink四大基石
一、Window
Flink 认为 Batch 是 Streaming 的一个特例,所以Flink 底层引擎是一个流式引擎,在上面实现了流处理和批处理。而窗口(window)就是从 Streaming 到 Batch 的一个桥梁。Flink 提供了非常完善的窗口机制。
https://nightlies.apache.org/flink/flink-docs-release-1.13/zh/docs/dev/datastream/operators/windows/
为什么需要Window?
在流处理应用中,数据是连续不断的,有时我们需要做一些聚合类的处理,例如:在过去的1分钟内有多少用户点击了我们的网页。
在这种情况下,我们必须定义一个窗口(window),用来收集最近1分钟内的数据,并对这个窗口内的数据进行计算。
Windows 是处理无限流的核心。Windows 将流拆分为有限大小的“桶”,我们可以对其进行计算。
在实时计算领域, 经常会有如下的需求:
每隔xx时间, 计算最近xx时间的数据,
如:
每隔10min,计算最近24h的热搜词
每隔5s,计算最近1min的股票行情数据
每隔10min,计算最近1h的广告点击量
....
这些实时需求的实现就需要借助窗口!
窗口就是 从什么地方开始 到什么地方结束的 一种表示方法。
Window有哪些控制属性?
为了完成上面提到的需求, 需要使用窗口来完成, 但是窗口需要有如下的属性才可以
窗口的长度(大小): 决定了要计算最近多长时间的数据
窗口的间隔: 决定了每隔多久计算一次
举例:每隔10min,计算最近24h的热搜词,24小时是长度,每隔10分钟是间隔。
Flink窗口应用代码结构
Flink的窗口算子为我们提供了方便易用的API,我们可以将数据流切分成一个个窗口,对窗口内的数据进行处理。本文将介绍如何在Flink上进行窗口的计算。
一个Flink窗口应用的大致骨架结构如下所示:
l Keyed Window
// Keyed Window
stream
.keyBy(...) <- 按照一个Key进行分组
.window(...) <- 将数据流中的元素分配到相应的窗口中
[.trigger(...)] <- 指定触发器Trigger(可选)
[.evictor(...)] <- 指定清除器Evictor(可选)
.reduce/aggregate/process/apply() <- 窗口处理函数Window Function
l Non-Keyed Window
// Non-Keyed Window
stream
.windowAll(...) <- 不分组,将数据流中的所有元素分配到相应的窗口中
[.trigger(...)] <- 指定触发器Trigger(可选)
[.evictor(...)] <- 指定清除器Evictor(可选)
.reduce/aggregate/process() <- 窗口处理函数Window Function
在上面,方括号([…]) 中的命令是可选的。这表明 Flink 允许您以多种不同的方式自定义窗口逻辑,使其最适合您的需求。
首先:我们要决定是否对一个DataStream按照Key进行分组,这一步必须在窗口计算之前进行。经过keyBy的数据流将形成多组数据,下游算子的多个实例可以并行计算。windowAll不对数据流进行分组,所有数据将发送到下游算子单个实例上。决定是否分组之后,窗口的后续操作基本相同,经过windowAll的算子是不分组的窗口(Non-Keyed Window),它们的原理和操作与Keyed Window类似,唯一的区别在于所有数据将发送给下游的单个实例,或者说下游算子的并行度为1。
窗口的生命周期
Flink窗口的骨架结构中有两个必须的两个操作:
- 使用窗口分配器(WindowAssigner)将数据流中的元素分配到对应的窗口。
- 当满足窗口触发条件后,对窗口内的数据使用窗口处理函数(Window Function)进行处理,常用的Window Function有reduce、aggregate、process。
其他的trigger、evictor则是窗口的触发和销毁过程中的附加选项,主要面向需要更多自定义的高级编程者,如果不设置则会使用默认的配置。
上图是窗口的生命周期示意图,假如我们设置的是一个10分钟的滚动窗口,第一个窗口的起始时间是0:00,结束时间是0:10,后面以此类推。当数据流中的元素流入后,窗口分配器会根据时间(Event Time或Processing Time)分配给相应的窗口。相应窗口满足了触发条件,比如已经到了窗口的结束时间,会触发相应的Window Function进行计算。注意,本图只是一个大致示意图,不同的Window Function的处理方式略有不同。
从数据类型上来看,一个DataStream经过keyBy转换成KeyedStream,再经过window转换成WindowedStream,我们要在之上进行reduce、aggregate或process等Window Function,对数据进行必要的聚合操作。
Window的分类
Window可以分成两类:
CountWindow:按照指定的数据条数生成一个Window,与时间无关。
滚动计数窗口,每隔N条数据,统计前N条数据
滑动计数窗口,每隔N条数据,统计前M条数据
TimeWindow:按照时间生成Window。(重点)
滚动时间窗口,每隔N时间,统计前N时间范围内的数据,窗口长度N,滑动距离N
滑动时间窗口,每隔N时间,统计前M时间范围内的数据,窗口长度M,滑动距离N
会话窗口,按照会话划定的窗口
基于时间的滑动和滚动窗口 [重点]
滚动窗口- TumblingWindow概念
流是连续的,无界的(有明确的开始,无明确的结束)
假设有个红绿灯,提出个问题:计算一下通过这个路口的汽车数量
对于这个问题,肯定是无法回答的,为何?
因为,统计是一种对固定数据进行计算的动作。
因为流的数据是源源不断的,无法满足固定数据的要求(因为不知道何时结束)
那么,我们换个问题:统计1分钟内通过的汽车数量
那么,对于这个问题,我们就可以解答了。因为这个问题确定了数据的边界,从无界的流数据中,取出了一部分有边界的数据子集合进行计算。
描述完整就是:每隔1分钟,统计这1分钟内通过汽车的数量。窗口长度是1分钟,时间间隔是1分钟,所以这样的窗口就是滚动窗口。
那么,这个行为或者说这个统计的数据边界,就称之为窗口。
同时,我们的问题,是以时间来划分被处理的数据边界的,那么按照时间划分边界的就称之为:时间窗口
反之,如果换个问题,统计100辆通过的车里面有多少宝马品牌,那么这个边界的划分就是按照数量的,这样的称之为:计数窗口
同时,这样的窗口被称之为滚动窗口,按照窗口划分依据分为:滚动时间窗口、滚动计数窗口。
滑动窗口– SlidingWindow概念
同样是需求,改为:
每隔1分钟,统计前面2分钟内通过的车辆数
对于这个需求我们可以看出,窗口长度是2分钟,每隔1分钟统计一次,窗口长度和时间间隔不相等,并且是大于关系,就是滑动窗口
或者:每通过100辆车,统计前面通过的50辆车的品牌占比
对于这个需求可以看出,窗口长度是50辆车,但是每隔100辆车统计一次
对于这样的窗口,我们称之为滑动窗口。
那么在这里面,统计多少数据是窗口长度(如统计2分钟内的数据,统计50辆车中的数据)
隔多久统计一次称之为滑动距离(如,每隔1分钟,每隔100辆车)
那么可以看出,滑动窗口,就是滑动距离不等于窗口长度的一种窗口
比如,每隔1分钟 统计先前5分钟的数据,窗口长度5分钟,滑动距离1分钟,不相等
比如,每隔100条数据,统计先前50条数据,窗口长度50条,滑动距离100条,不相等
那如果相等呢?相等就是比如:每隔1分钟统计前面1分钟的数据,窗口长度1分钟,滑动距离1分钟,相等。
对于这样的需求可以简化成:每隔1分钟统计一次数据,这就是前面说的滚动窗口
那么,我们可以看出:
滚动窗口:窗口长度= 滑动距离
滑动窗口:窗口长度!= 滑动距离
总结:其中可以发现,对于滑动窗口:
滑动距离> 窗口长度,会漏掉数据,比如:每隔5分钟,统计前面1分钟的数据(滑动距离5分钟,窗口长度1分钟,漏掉4分钟的数据)这样的东西,没人用。
滑动距离< 窗口长度,会重复处理数据,比如:每隔1分钟,统计前面5分钟的数据(滑动距离1分钟,窗口长度5分钟,重复处理4分钟的数据)
滑动距离= 窗口长度,不漏也不会重复,也就是滚动窗口
窗口的长度(大小) > 窗口的间隔 : 如每隔5s, 计算最近10s的数据 【滑动窗口】
窗口的长度(大小) = 窗口的间隔: 如每隔10s,计算最近10s的数据 【滚动窗口】
窗口的长度(大小) < 窗口的间隔: 每隔15s,计算最近10s的数据 【没有名字,不用】
需求:
有如下数据表示:
信号灯编号和通过该信号灯的车的数量
9,3
9,2
9,7
4,9
2,6
1,5
2,3
5,7
5,4
需求1:每5秒钟统计一次,最近5秒钟内,各个路口通过红绿灯汽车的数量--基于时间的滚动窗口
需求2:每5秒钟统计一次,最近10秒钟内,各个路口通过红绿灯汽车的数量--基于时间的滑动窗口
没有添加窗口的写法:
package com.bigdata;
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class Demo {
public static void main(String[] args) throws Exception {
//1. env-准备环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
//2. source-加载数据
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9999);
// 9,2 --> (9,2)
//3. transformation-数据处理转换
streamSource.map(new MapFunction<String, Tuple2<Integer,Integer>>() {
@Override
public Tuple2<Integer, Integer> map(String line) throws Exception {
String[] arr = line.split(",");
int monitor_id = Integer.valueOf(arr[0]);
int num = Integer.valueOf(arr[1]);
return Tuple2.of(monitor_id,num);
}
}).keyBy(tuple->tuple.f0).sum(1).print();
//4. sink-数据输出
//5. execute-执行
env.execute();
}
}
此处的sum求和,中count ,其实是CartInfo中的一个字段而已。
演示:
滚动窗口演示
需求1:每5秒钟统计一次,最近5秒钟内,各个路口通过红绿灯汽车的数量--基于时间的滚动窗口
package com.bigdata.Windows;
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.TumblingProcessingTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
/**
* @基本功能:
* @program:FlinkDemo
* @author: 乐友陈
* @create:2024-11-25 10:49:33
**/
public class SingleLight {
public static void main(String[] args) throws Exception {
//1. env-准备环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
//2. source-加载数据
DataStreamSource<String> streamSource = env.socketTextStream("bigdata", 8889);
// 9,2 --> (9,2)
//3. transformation-数据处理转换
streamSource.map(new MapFunction<String, Tuple2<Integer,Integer>>() {
@Override
public Tuple2<Integer, Integer> map(String line) throws Exception {
String[] arr = line.split(",");
int monitor_id = Integer.valueOf(arr[0]);
int num = Integer.valueOf(arr[1]);
return Tuple2.of(monitor_id,num);
}
}).keyBy(tuple->tuple.f0)
.window(TumblingProcessingTimeWindows.of(Time.minutes(1)))
// 1 .window(SlidingProcessingTimeWindows.of(Time.minutes(1),Time.seconds(30)))
.sum(1).print();
//4. sink-数据输出
//5. execute-执行
env.execute();
}
}
以上代码的时间最好修改为1分钟,假如时间间隔是1分钟,那么48分03秒时输入的信号灯数据,49分整点会统计出来结果,原因是底层有一个算法。
滑动窗口的话,不太容易看到效果,因为有些数据被算到了多个窗口中,需要我们拿笔自己计算一下,对比一下:
接着进行滑动窗口的演示:
需求二:每5秒钟统计一次,最近5秒钟内,各个路口通过红绿灯汽车的数量
package com.bigdata.Windows;
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
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;
/**
* @基本功能:
* @program:FlinkDemo
* @author: 乐友陈
* @create:2024-11-25 10:49:33
**/
public class SingleLight {
public static void main(String[] args) throws Exception {
//1. env-准备环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
//2. source-加载数据
DataStreamSource<String> streamSource = env.socketTextStream("bigdata", 8889);
// 9,2 --> (9,2)
//3. transformation-数据处理转换
streamSource.map(new MapFunction<String, Tuple2<Integer,Integer>>() {
@Override
public Tuple2<Integer, Integer> map(String line) throws Exception {
String[] arr = line.split(",");
int monitor_id = Integer.valueOf(arr[0]);
int num = Integer.valueOf(arr[1]);
return Tuple2.of(monitor_id,num);
}
}).keyBy(tuple->tuple.f0)
// .window(TumblingProcessingTimeWindows.of(Time.minutes(1)))
.window(SlidingProcessingTimeWindows.of(Time.seconds(10),Time.seconds(5)))
.sum(1).print();
//4. sink-数据输出
//5. execute-执行
env.execute();
}
}
在spark中:
打印rdd 用什么 ? foreach(lambda x: print(x))
打印dataFrame: show()
在flink中:打印dataStream 使用 print()