Window概述
一般真实的流都是无界的,怎么处理无界的数据?可以把无限的数据流进行切分,得到有限的数据集进行处理----也就是得到有界流。
streaming流式计算是一种被设计用于处理无限数据集的数据处理引擎,而无限数据集是指一种不断增长的本质上无限的数据集;
而window是把无限数据流为有限流的一种方式,Window将一个无限的stream拆分成有限大小的”buckets”桶,我们可以在这些桶上做计算操作。
Window API
窗口分配器---window( )方法
我们可以用.window( )来定义一个窗口,然后基于这个window去做一些聚合或者其他处理操作。注意window( )方法必须在KeyBy之后才能用。
Flink提供了更加简单的.timeWindow和.countWindow方法,用于定义时间窗口和计数窗口。
val minTempPerWindow = dataStream.map(r => (r.id, r.temperature))
.keyBy(_._1)
.timeWindow(Time.seconds(15))
.reduce((r1, r2) => (r1._1, r1._2.min(r2._2)))
窗口分配器(window assigner)
window()方法接收的输入参数是一个WindowAssigner,WindowAssigner负责将每条输入的数据分发到正确的window中;
Flink提供了通用的WindowAssigner:滚动窗口(tumbling window)、滑动窗口(sliding window)、会话窗口(session window)、全局窗口(global window)
窗口操作至少分成2步,①是.timewindow ②是聚合操作;
创建不同类型的窗口:
滚动时间窗口(tumbling time window).timeWindow(Time.seconds(15)) 滑动时间窗口(sliding time window).timeWindow(Time.seconds(15), Time.seconds(5)) 会话窗口(session window).window(EventTimeSessionWindows.withGap(Time.minutes(10)) 滚动计数窗口(tumbling count window).countWindow(5) 滑动计数窗口(sliding count window).countWindow(10, 2)
窗口函数(window function)
window function定义了要对窗口中收集的数据做的计算操作,可以分为两类:
增量聚合函数(incremental aggregation functions):每条数据到来就进行计算,保持一个简单的状态;ReduceFunction, AggregateFunction
全窗口函数(full window functions):先把窗口所有数据都收集起来,等到计算的时候会遍历所有数据ProcessFunctionWindow
其他可选API
.trigger()----触发器:定义window什么时候关闭,触发计算输出结果 .evitor()----移除器:定义移除某些数据的逻辑; .allowedLateness() ---允许处理迟到的数据 .sideOutputLateData() ---将迟到的数据放入侧输出流 .getSideOutput() ----获取侧输出流
Time--解决乱序问题
在Flink的流式处理中,会涉及到时间的不同概念,如下图所示
不同的时间语义有不同的应用场景,我们往往更关心事件时间(Event Time--故事发生的时间)。
Event Time:是事件创建的时间。它通常由事件中的时间戳描述,例如采集的日志数据中,每一条日志都会记录自己的生成时间,Flink通过时间戳分配器访问事件时间戳。
Ingestion Time:是数据进入Flink的时间。
Processing Time:是每一个执行基于时间操作的算子的本地系统时间,与机器相关,默认的时间属性就是Processing Time。
例如,一条日志进入Flink的时间为2017-11-12 10:00:00.123,到达Window的系统时间为2017-11-12 10:00:01.234,日志的内容如下:
2017-11-02 18:37:15.624 INFO Fail over to rm2
对于业务来说,要统计1min内的故障日志个数,哪个时间是最有意义的?—— eventTime,因为我们要根据日志的生成时间进行统计。
在代码中设置Event Time
可直接在代码中,对执行环境调用setStreamTimeCharacteristic方法,设置流的时间特性; 具体的时间,还需要从数据中提取时间戳(timestamp); val env = StreamExecutionEnvironment.getExecutionEnvironment //从调用时刻开始给env创建的每一个stream追加时间特征 env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) 没有设置系统默认按processing time;
Watermark
我们知道,流处理从事件产生,到流经source,再到operator,中间是有一个过程和时间的,虽然大部分情况下,流到operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络、分布式等原因,导致乱序的产生,所谓乱序,就是指Flink接收到的事件的先后顺序不是严格按照事件的Event Time顺序排列的。
乱序数据的影响
那么此时出现一个问题,一旦出现乱序,如果只根据eventTime决定window的运行(遇到一个时间戳达到了窗口的时间,不应该立刻触发窗口的计算,而是等待一段时间,等迟到的数据来了再关闭窗口),我们不能明确数据是否全部到位,但又不能无限期的等下去,此时必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了,这个特别的机制,就是Watermark。
- Watermark是一种衡量Event Time进展的机制,它是数据本身的一个隐藏属性,数据本身携带着对应的Watermark。
- Watermark是用于处理乱序事件的,而正确的处理乱序事件,通常用Watermark机制结合window来实现。
- 数据流中的Watermark用于表示timestamp小于Watermark的数据,都已经到达了,因此,window的执行也是由Watermark触发的。
- Watermark可以理解成一个延迟触发机制,我们可以设置Watermark的延时时长t,每次系统会校验已经到达的数据中最大的maxEventTime,然后认定eventTime小于maxEventTime - t的所有数据都已经到达,如果有窗口的停止时间等于maxEventTime – t,那么这个窗口被触发执行。
- watermark用来让程序自己平衡延迟和结果正确性;
watermark的特点:
watermark的引入:
Event Time的使用一定要指定数据源的时间戳; 对于排好序的数据,只需要指定时间戳就够了,不需要延迟触发。
//注意单位是毫秒,所以根据时间戳的不同,可能需要乘1000
dataStream.assignAscendingTimestamps(_.timestamp * 1000)
dataStream.assignTimestampsAndWatermarks(
new BoundOutOfOrdernessTimestampExtractor[SensorReading]
(Time.milliseconds(1000)){
override def extractTimestamp(element: SensorReading): Long = {
element.timestamp * 1000
}
})
Flink暴露了TimestampAssigner接口供我们实现,使我们可以自定义如何从事件数据中抽取时间戳和生产watermark
dataStream.assignTimestampsAndWatermarks(new MyAssigner()) //MyAssigner可以有两种类型,都继承自TimestampAssigner
TimestampAssigner
定义了抽取时间戳,以及生产watermark的方法,有两种类型:
①AssignerWithPeriodicWatermarks
- 周期性的生产watermark:系统会周期性的将watermark插入到流中;
- 默认周期是200毫秒,可以使用ExecutionConfig.setAutoWatermarkInterval()方法进行设置;
- 升序和前面乱序的处理BoundedOutOfOrderness,都是基于周期性watermark的。
②AssignerWithPunctuatedWatermarks
- 没有时间周期规律,可打断的生产watermark。
watermark的设定:
- 在Flink中,watermark由应用程序开发人员生成,这通常需要对应的领域有一定的了解;
- 如果watermark设置的延迟太久,收到结果的速度可能就会很慢,解决办法是在水位线到达之前输出一个近似结果;
- 而如果watermark到达得太早,则可能收到错误结果,不过Flink处理迟到数据的机制可以解决这个问题。
Window可以分成两类:
时间窗口TimeWindow:滚动时间窗口(Tumbling Window)、滑动时间窗口(Sliding Window)、会话时间窗口(Session Window)。
计数窗口CountWindow:按照指定的数据条数生成一个Window,与时间无关。分为滚动计数窗口、滑动计数窗口。
TimeWindow
- 1. 滚动窗口(Tumbling Windows)
将数据依据固定的窗口长度对数据进行切片。
特点:时间对齐,窗口长度固定,没有重叠。 它是步长 = site的滑动窗口;
使用场景:商业BI分析统计(关注的商业指标往往是某个时间段的指标,如一天或一周的销售额,每个时间段的聚合操作);
滚动窗口分配器将每个元素分配到一个指定窗口大小的窗口中,滚动窗口有一个固定的大小,并且不会出现重叠。例如:如果你指定了一个5分钟大小的滚动窗口,窗口的创建如下图所示:
import org.apache.flink.api.java.tuple.Tuple import org.apache.flink.streaming.api.TimeCharacteristic import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor import org.apache.flink.streaming.api.scala.{DataStream, KeyedStream, StreamExecutionEnvironment, WindowedStream} import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows import org.apache.flink.streaming.api.windowing.time.Time import org.apache.flink.streaming.api.windowing.windows.TimeWindow import org.apache.flink.api.scala._ object StreamEventTimeApp { def main(args: Array[String]): Unit = { //环境 val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment //声明使用eventTime;引入EventTime 从调用时刻开始给env创建的每一个stream追加时间特征 env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) val dstream: DataStream[String] = env.socketTextStream("hadoop101", 7777) val textWithTsDStream: DataStream[(String, Long, Int)] = dstream.map { text => val arr: Array[String] = text.split(" ") (arr(0), arr(1).toLong, 1) } // 1 告知 flink如何获取数据中的event时间戳 2 告知延迟的watermark为 3s val textWithEventTimeDStream: DataStream[(String, Long, Int)] = textWithTsDStream.assignTimestampsAndWatermarks( new BoundedOutOfOrdernessTimestampExtractor[(String, Long, Int)](Time.milliseconds(3000)) { //time别导错包了 override def extractTimestamp(element: (String, Long, Int)): Long = { return element._2 } }).setParallelism(1) //每5秒开一个窗口 统计key的个数 5秒是一个数据的时间戳为准 val textKeyStream: KeyedStream[(String, Long, Int), Tuple] = textWithEventTimeDStream.keyBy(0) textKeyStream.print("textKey: ") //滚动窗口 val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(TumblingEventTimeWindows.of(Time.milliseconds(5000))) windowStream.sum(2).print("windows: ").setParallelism(1) env.execute() }
[kris@hadoop101 gmall]$ nc -lk 7777 abc 1000 abc 3000 abc 4000 abc 5000 abc 6000 abc 7000 abc 7500 abc 8000 abc 9000 abc 12000 abc 12999 abc 14000 abc 15000 abc 17000 abc 18000 textKey: :8> (abc,1000,1) textKey: :8> (abc,3000,1) textKey: :8> (abc,4000,1) textKey: :8> (abc,5000,1) textKey: :8> (abc,6000,1) textKey: :8> (abc,7000,1) textKey: :8> (abc,7500,1) textKey: :8> (abc,8000,1) Window: > (abc,1000,3) textKey: :8> (abc,9000,1) textKey: :8> (abc,12000,1) textKey: :8> (abc,12999,1) Window: > (abc,5000,6) textKey: :8> (abc,14000,1) textKey: :8> (abc,15000,1) textKey: :8> (abc,17000,1) textKey: :8> (abc,18000,1) Window: > (abc,12000,3) 滚动窗口: X秒开一个窗口,上例中5s开一个窗; 上例watermark 3s 第n次发车时间:nX+3,车上携带的[X, nX)秒内的 如第一次车上携带 [0, 5)以内的,在第 5 + 3 = 8s时间点发车
第二次车上携带 [5, 10)以内的,在第10 + 3 = 13s时间点发车
第三次车上携带 [10, 15)以内的,在第15 + 3 = 18s时间点发车;
- 2. 滑动窗口(Sliding Windows)
滑动窗口是固定窗口的更广义的一种形式,滑动窗口由固定的窗口长度和滑动间隔组成。
特点:窗口长度固定,有重叠。
适用场景:对最近一个时间段内的统计(求某接口最近5min的失败率来决定是否要报警);
灵活;连续的波浪;比如股票交易所它是最近24小时的涨跌幅度,随时往后算随时往后划;
滑动窗口分配器将元素分配到固定长度的窗口中,与滚动窗口类似,窗口的大小由窗口大小参数来配置,另一个窗口滑动参数控制滑动窗口开始的频率。因此,滑动窗口如果滑动参数小于窗口大小的话,窗口是可以重叠的,在这种情况下元素会被分配到多个窗口中。
例如,你有10分钟的窗口和5分钟的滑动,那么每个窗口中5分钟的窗口里包含着上个10分钟产生的数据,如下图所示:
object StreamEventTimeApp { def main(args: Array[String]): Unit = { //环境 val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment //声明使用eventTime env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) val dstream: DataStream[String] = env.socketTextStream("hadoop101", 7777) val textWithTsDStream: DataStream[(String, Long, Int)] = dstream.map { text => val arr: Array[String] = text.split(" ") (arr(0), arr(1).toLong, 1) } // 1 告知 flink如何获取数据中的event时间戳 2 告知延迟的watermark val textWithEventTimeDStream: DataStream[(String, Long, Int)] = textWithTsDStream.assignTimestampsAndWatermarks( new BoundedOutOfOrdernessTimestampExtractor[(String, Long, Int)](Time.milliseconds(3000)) { //time别导错包了 override def extractTimestamp(element: (String, Long, Int)): Long = { return element._2 } }).setParallelism(1) //每5秒开一个窗口 统计key的个数 5秒是一个数据的时间戳为准 val textKeyStream: KeyedStream[(String, Long, Int), Tuple] = textWithEventTimeDStream.keyBy(0) textKeyStream.print("textKey: ") //滚动窗口 //val windowDStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(TumblingEventTimeWindows.of(Time.milliseconds(5000))) //滑动窗口 val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(SlidingEventTimeWindows.of(Time.milliseconds(5000L), Time.milliseconds(2000L))) windowStream.sum(2).print("windows: ").setParallelism(1) env.execute() } } [kris@hadoop101 gmall]$ nc -lk 7777 aaa 100 aaa 500 aaa 1000 aaa 3000 aaa 3999 abc 100 abc 1000 abc 3998 abc 3999 abc 5000 abc 8000 abc 10000 textKey: :8> (abc,100,1) textKey: :8> (abc,1000,1) textKey: :8> (abc,3998,1) textKey: :8> (abc,3999,1) //窗口大小0-4999;前面这些都是在4999窗口以下的范围内,但是开车的时机是在步长+watermark=4000,但开车的时候只有100这一个在里边;步长为1 windows: > (abc,100,1) textKey: :8> (abc,5000,1) //开车取决于时间间隔步长1s, 每隔1s发一次;第二次发车是在2s的时候,延迟3s,即5s的时候发车,但这个时候车里就只有100和1000两个; windows: > (abc,100,2) textKey: :8> (abc,8000,1) //一车接5s的人;8000--5000--4000--3000--(这个时候它俩已经开车走了,不要了)-2000-1000 windows: > (abc,100,2) //3000那辆车 windows: > (abc,100,4) //走的是4000那辆车--100、1000、3998、3999 windows: > (abc,100,4)//5000,走的还是100、1000、3998、3999这四个,5000应该是在下一个窗口大小的范围; textKey: :8> (abc,10000,1) //10000-3000 ==> 7000s,到5000是走完第一个窗口大小, 6000走一辆(5999--1000);7000(发车的6999-2000) windows: > (abc,1000,4) //6000: 1000/3998/3999/5000/ windows: > (abc,3998,3) //7000: 3998/3999/5000
窗口大小设置为5s,步长为2s,watermark为3s;
开车时间:每隔2s,例如下例4s、6s、8s、10s...
第一次开车4s, 携带[0,1);
第二次开车6s, 携带[0,2];
第三次开车8s,携带[0, 4];
第四次开车10s,携带[2, 6]
第五次开车12s,携带[4, 8]
第六次开车14s,携带[6, 10]
[kris@hadoop101 ~]$ nc -lk 7777 abc 399 abc 800 abc 1000 abc 1800 abc 2000 abc 3000 abc 4000 abc 5000 abc 5900 abc 6000 abc 7000 abc 8000 abc 9000 abc 10000 abc 11000 abc 11800 abc 12000 abc 13000 abc 14000 ============>>> textKey: :8> (abc,399,1) textKey: :8> (abc,800,1) textKey: :8> (abc,1000,1) textKey: :8> (abc,1800,1) textKey: :8> (abc,2000,1) textKey: :8> (abc,3000,1) textKey: :8> (abc,4000,1) windows: > (abc,399,2) textKey: :8> (abc,5000,1) textKey: :8> (abc,5900,1) textKey: :8> (abc,6000,1) windows: > (abc,399,5) textKey: :8> (abc,7000,1) textKey: :8> (abc,8000,1) windows: > (abc,399,7) textKey: :8> (abc,9000,1) textKey: :8> (abc,10000,1) windows: > (abc,2000,6) textKey: :8> (abc,11000,1) textKey: :8> (abc,11800,1) textKey: :8> (abc,12000,1) windows: > (abc,4000,6) textKey: :8> (abc,13000,1) textKey: :8> (abc,14000,1) windows: > (abc,6000,5)
如果watermark = 0,窗口大小为5,步长为2s的滑动窗口:
val textWithEventTimeDStream: DataStream[(String, Long, Int)] = textWithTsDStream.assignTimestampsAndWatermarks( new BoundedOutOfOrdernessTimestampExtractor[(String, Long, Int)](Time.milliseconds(0)) { //time别导错包了 override def extractTimestamp(element: (String, Long, Int)): Long = { return element._2 } }).setParallelism(1) //滑动窗口 val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(SlidingEventTimeWindows.of(Time.milliseconds(5000L), Time.milliseconds(2000L))) windowStream.sum(2).print("windows: ").setParallelism(1) [kris@hadoop101 ~]$ nc -lk 7777 abc 299 abc 500 abc 1000 abc 2000 abc 2800 abc 3000 abc 3800 abc 4800 abc 5000 abc 6000 abc 6800 abc 7000 abc 8000 abc 8600 abc 9000 abc 9500 abc 10000 textKey: :8> (abc,299,1) textKey: :8> (abc,500,1) textKey: :8> (abc,1000,1) 为什么以1s为基点呢? windows: > (abc,299,2) textKey: :8> (abc,2000,1) textKey: :8> (abc,2800,1) textKey: :8> (abc,3000,1) windows: > (abc,299,5) textKey: :8> (abc,3800,1) textKey: :8> (abc,4800,1) textKey: :8> (abc,5000,1) windows: > (abc,299,8) textKey: :8> (abc,6000,1) textKey: :8> (abc,6800,1) textKey: :8> (abc,7000,1) windows: > (abc,2000,8) textKey: :8> (abc,8000,1) textKey: :8> (abc,8600,1) textKey: :8> (abc,9000,1) windows: > (abc,4800,7) textKey: :8> (abc,9500,1) textKey: :8> (abc,10000,1)
- 3. 会话窗口(Session Windows)
由一系列事件组合一个指定时间长度的timeout间隙组成,类似于web应用的session,也就是一段时间没有接收到新数据就会生成新的窗口。
特点:时间无对齐。
session窗口分配器通过session活动来对元素进行分组,session窗口跟滚动窗口和滑动窗口相比,不会有重叠和固定的开始时间和结束时间的情况,相反,当它在一个固定的时间周期内不再收到元素,即非活动间隔产生,那个这个窗口就会关闭。一个session窗口通过一个session间隔来配置,这个session间隔定义了非活跃周期的长度,当这个非活跃周期产生,那么当前的session将关闭并且后续的元素将被分配到新的session窗口中去。
object StreamEventTimeApp { def main(args: Array[String]): Unit = { //环境 val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment //声明使用eventTime env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) val dstream: DataStream[String] = env.socketTextStream("hadoop101", 7777) val textWithTsDStream: DataStream[(String, Long, Int)] = dstream.map { text => val arr: Array[String] = text.split(" ") (arr(0), arr(1).toLong, 1) } // 1 告知 flink如何获取数据中的event时间戳 2 告知延迟的watermark val textWithEventTimeDStream: DataStream[(String, Long, Int)] = textWithTsDStream.assignTimestampsAndWatermarks( new BoundedOutOfOrdernessTimestampExtractor[(String, Long, Int)](Time.milliseconds(3000)) { //time别导错包了 override def extractTimestamp(element: (String, Long, Int)): Long = { return element._2 } }).setParallelism(1) //每5秒开一个窗口 统计key的个数 5秒是一个数据的时间戳为准 val textKeyStream: KeyedStream[(String, Long, Int), Tuple] = textWithEventTimeDStream.keyBy(0) textKeyStream.print("textKey: ") //滚动窗口 //val windowDStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(TumblingEventTimeWindows.of(Time.milliseconds(5000))) //滑动窗口 //val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(SlidingEventTimeWindows.of(Time.milliseconds(5000L), Time.milliseconds(1000L))) //会话窗口 val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(EventTimeSessionWindows.withGap(Time.milliseconds(5000L))) windowStream.sum(2).print("windows: ").setParallelism(1) env.execute() } } 只能两次时间的间隔是否满足条件 在触发水位5s的基础上再加延迟3s, [kris@hadoop101 gmall]$ nc -lk 7777 abc 1000 abc 7000 abc 10000 =======>>> textKey: :8> (abc,1000,1) textKey: :8> (abc,7000,1) textKey: :8> (abc,10000,1) //在上一个基础上+延迟时间3s才会开车 windows: > (abc,1000,1) [kris@hadoop101 gmall]$ nc -lk 7777 aaa 1000 aaa 2000 aaa 7001 aaa 9000 aaa 10000 =====>> textKey: :5> (aaa,1000,1) textKey: :5> (aaa,2000,1) textKey: :5> (aaa,7001,1) //两个时间点之间相差达到鸿沟5s了,在这个基础之上再加3s才能开车; textKey: :5> (aaa,9000,1) textKey: :5> (aaa,10000,1) windows: > (aaa,1000,2)
CountWindow
CountWindow根据窗口中相同key元素的数量来触发执行,执行时只计算元素数量达到窗口大小的key对应的结果。
注意:CountWindow的window_size指的是相同Key的元素的个数,不是输入的所有元素的总数。
滚动窗口
默认的CountWindow是一个滚动窗口,只需要指定窗口大小即可,当元素数量达到窗口大小时,就会触发窗口的执行。
滑动窗口
滑动窗口和滚动窗口的函数名是完全一致的,只是在传参数时需要传入两个参数,一个是window_size,一个是sliding_size。
下面代码中的sliding_size设置为了2,也就是说,每收到两个相同key的数据就计算一次,每一次计算的window范围是5个元素。
WindowAPI
Windowall是所有数据都在一个分区上;keyBy之后是分到各个分区再window去处理