一、概念

1、什么是window

在流式数据中,数据是连续的,通常是无限的,对流中的所有元素进行计数是不可能的,所以在流上的聚合需要由window来划定范围,例如过去五分钟内用户浏览量的计算或者最后100个元素的和。window就是一种可以把无限数据切割为有限数据块的手段。 窗口可以由时间或者数量来做区分 1.根据时间进行截取,比如每10分钟统计一次,即时间驱动的[Time Window] 2.根据消息数量进行统计,比如每100个数据统计一次,即数据驱动[Count Window]

2、时间窗口的类型

时间窗口又分为滚动窗口,滑动窗口,和会话窗口 (1)滚动窗口-tumbling windows 时间对齐,窗口长度固定,没有重叠 如图:以固定的长度进行分割,比如一分钟的内的计数

开窗方法:

//滚动窗口 stream.keyBy(0) 
.window(TumblingEventTimeWindows.of(Time.seconds(2))) 
.sum(1) 
.print();

(2)滑动窗口-sliding windows 时间对齐,窗口长度固定,有重叠,展现的是数据的变化趋势 如图:窗口大小为4,步长为2,每隔两秒统计仅4s的数据

开窗方法:

//滑动窗口 stream.keyBy(0) .window(SlidingProcessingTimeWindows.of(Time.seconds(6),Time.seconds(4)))
.sum(1) 
.print();

(3)会话窗口-session windows 当流中达到多长时间没有新的数据到来,上一个会话窗口就是截至到新数据到来前接收到的最后一条数据,当新数据到来后,上一个窗口将会关闭,开启一个新的窗口。

开窗方法:

stream
    .keyBy(0) 
		.window(ProcessingTimeSessionWindows.withGap(Time.seconds(5)))
		.sum(1) 
		.print();

(4)没有窗口(全局窗口)-global windows global window + trigger 一起配合才能使用

// 单词每出现三次统计一次
stream
		.keyBy(0)
		.window(GlobalWindows.create()) //如果不加这个程序是启动不起来的            
		.trigger(CountTrigger.of(3)) .sum(1) .print();

执行结果:
hello,3 
hello,6 
hello,9

总结:效果跟CountWindow(3)很像,但又有点不像,因为如果是CountWindow(3),单词每次出现的都是3次,不会包含之前的次数,而我们刚刚的这个每次都包含了之前的次数。

二、 Flink流处理时间方式

针对stream数据中的时间,可以分为以下三种: Event Time:事件产生的时间,它通常由事件中的时间戳描述。 Ingestion time:事件进入Flink的时间 Processing Time:事件被处理时当前系统的时间

案例演示: 原始日志如下

2019-11-11 10:00:01,134 INFO executor.Executor: Finished task in state 0.0

这条数据进入Flink的时间是2019-11-11 20:00:00,102 到达window处理的时间为2019-11-11 20:00:01,100 2019-11-11 10:00:01,134 是Event time 2019-11-11 20:00:00,102 是Ingestion time 2019-11-11 20:00:01,100 是Processing time

思考:

如果我们想要统计每分钟内接口调用失败的错误日志个数,使用哪个时间才有意义?

三、事件的无序处理

1、Process Time Window(有序)

需求:每隔5秒计算最近10秒的单词出现的次数 自定义source,模拟:第 13 秒的时候连续发送 2 个事件,第 16 秒的时候再发送 1 个事件

输出结果:

开始发送事件的时间:16:16:40 
(hadoop,2) 
(hadoop,3) 
(hadoop,1)

窗口验证过程:

2、Process Time Window(无序)

需求:每隔5秒计算最近10秒的单词出现的次数 自定义source,模拟:第 13 秒的时候连续发送 2 个事件,第 16 秒的时候再发送 1 个事件 但是这里在13秒的时候正常发送了一个事件,有一个事件由于网络等其他原因,没有成功发送,在19秒的时候才发送出去。

输出结果:

开始发送事件的时间:16:18:50
(hadoop,1)
(1573287543001,1)
(1573287543001,1) 
(hadoop,3) 
(1573287546016,1) 
(1573287543016,1) 
(1573287546016,1) 
(hadoop,2) 
(1573287543016,1)

3、 使用Event Time处理无序

使用Event Time处理

在源数据流中map获取时间时间
.assignTimestampsAndWatermarks(new EventTimeExtractor() )
EventTimeExtractor()实现AssignerWithPeriodicWatermarks接口,获取事件时间
        //拿到第一个事件的 Event Time
        @Override
        public long extractTimestamp(Tuple2<String, Long> element, long previousElementTimestamp) {
            return element.f1;
        }

执行结果: start send event time:16:40:50 (hadoop,1) (hadoop,3) (hadoop,1)

现在我们第三个window的结果已经计算准确了,但是我们还是没有彻底的解决问题(黄色事件应该在第一个窗口中计数,但是没有)。接下来就需要我们使用WaterMark机制来解决了。

4、使用WaterMark机制解决无序

核心实现是在窗口触发的时候延时一段时间
        //window的触发时间
        @Nullable
        @Override
        public Watermark getCurrentWatermark() {
            //window延迟5秒触发
            return new Watermark(System.currentTimeMillis() - 5000);
        }

执行结果: start send event time:16:54:30 (hadoop,2) (hadoop,3) (hadoop,1)

四、WaterMark机制

1、WaterMark的定义

使用eventTime的时候如何处理乱序数据? 在使用eventTime作为处理时间的时候需要考虑乱序时间。 我们知道,流处理从事件产生,到流经source,再到operator,中间是有一个过程和时间的。虽然大部分情况下,流到operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络延迟等原因,导致乱序的产生,特别是使用kafka的话,多个分区的数据无法保证有序(单个分区是保证有序的)。所以在进行window计算的时候,我们又不能无限期的等下去,必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了。 这个特别的机制,就是watermark,watermark是用于处理乱序事件的。watermark可以 翻译为水位线

(1)有序的流的watermarks

(2)无序的流的watermarks

(3)多并行度流的watermarks

2、WaterMark+Window处理乱序事件

需求:得到并打印每隔 3 秒钟统计前 3 秒内的相同的 key 的所有的事件 要求:按事件时间开窗并处理乱序问题 思路: 设置flink的时间处理机制为enventTime, 事件流的事件进入后更新最新的事件时间, 最新的事件时间减掉允许最大的乱序时间为水印时间, 当水印时间大于等于窗口时间时,计算当前窗口数据 核心思想:事件流时间推动窗口的移动和计算

演示数据:

-- window 计算触发的条件 
一条一条输入:
000001,1461756862000 
000001,1461756866000 
000001,1461756872000 
000001,1461756873000 
000001,1461756874000 
000001,1461756876000 
000001,1461756877000

输出结果:
event = (000001,1461756862000)|Event Time:2016-04-27 19:34:22|Max Event Time:2016-04-27 19:34:22|Current Watermark:2016-04-27 19:34:12
water mark...
event = (000001,1461756866000)|Event Time:2016-04-27 19:34:26|Max Event Time:2016-04-27 19:34:26|Current Watermark:2016-04-27 19:34:16
water mark...
event = (000001,1461756872000)|Event Time:2016-04-27 19:34:32|Max Event Time:2016-04-27 19:34:32|Current Watermark:2016-04-27 19:34:22
water mark...
event = (000001,1461756873000)|Event Time:2016-04-27 19:34:33|Max Event Time:2016-04-27 19:34:33|Current Watermark:2016-04-27 19:34:23
water mark...
event = (000001,1461756874000)|Event Time:2016-04-27 19:34:34|Max Event Time:2016-04-27 19:34:34|Current Watermark:2016-04-27 19:34:24
water mark...
event = (000001,1461756876000)|Event Time:2016-04-27 19:34:36|Max Event Time:2016-04-27 19:34:36|Current Watermark:2016-04-27 19:34:26
water mark...

process start time:2021-01-04 18:26:51
window  start time:2016-04-27 19:34:21
[(000001,1461756862000)|2016-04-27 19:34:22]
window  end  time:2016-04-27 19:34:24
water mark...
event = (000001,1461756877000)|Event Time:2016-04-27 19:34:37|Max Event Time:2016-04-27 19:34:37|Current Watermark:2016-04-27 19:34:27
water mark...
process start time:2021-01-04 18:26:52
window  start time:2016-04-27 19:34:24
[(000001,1461756866000)|2016-04-27 19:34:26]
window  end  time:2016-04-27 19:34:27

当2016-04-27 19:34:37的事件时间更新为最大的currentMaxEventTime,此时获得的时间水印是2016-04-27 19:34:27,触发窗口[2016-04-27 19:34:24—2016-04-27 19:34:27)的计算

再输入:
000001,1461756885000
000001,1461756892000

输出:
event = (000001,1461756885000)|Event Time:2016-04-27 19:34:45|Max Event Time:2016-04-27 19:34:45|Current Watermark:2016-04-27 19:34:35
water mark...
process start time:2021-01-04 18:27:05
window  start time:2016-04-27 19:34:30
[(000001,1461756872000)|2016-04-27 19:34:32]
window  end  time:2016-04-27 19:34:33
water mark...
event = (000001,1461756892000)|Event Time:2016-04-27 19:34:52|Max Event Time:2016-04-27 19:34:52|Current Watermark:2016-04-27 19:34:42
water mark...
process start time:2021-01-04 18:30:38
window  start time:2016-04-27 19:34:33
[(000001,1461756873000)|2016-04-27 19:34:33, (000001,1461756874000)|2016-04-27 19:34:34]
window  end  time:2016-04-27 19:34:36
process start time:2021-01-04 18:30:38
window  start time:2016-04-27 19:34:36
[(000001,1461756876000)|2016-04-27 19:34:36, (000001,1461756877000)|2016-04-27 19:34:37]
window  end  time:2016-04-27 19:34:39

新的事件进入后更新了最新的时间时间,触发新的窗口计算

如果再输入:
000001,1461756862000
输出:
event = (000001,1461756862000)|Event Time:2016-04-27 19:34:22|Max Event Time:2016-04-27 19:34:52|Current Watermark:2016-04-27 19:34:42
可以看到此时窗口时间已超过事件时间,会丢弃这个事件,不做处理

总结:window触发的时间

  1. watermark时间 >= window_end_time
  2. 在 [window_start_time, window_end_time) 区间中有数据存在,注意是左闭右开的区间,而且是 以 event time 来计算的

3、 迟到太多的事件处理

(1)丢弃 这个是默认的处理方式 (2)allowedLateness 指定允许数据延迟的时间 核心思想:在允许最大延迟的基础上再加一个容忍时间。

).assignTimestampsAndWatermarks(new EventTimeExtractor() ) 
.keyBy(0) 
.timeWindow(Time.seconds(3)) 
.allowedLateness(Time.seconds(2)) // 允许事件迟到 2 秒 
.process(new SumProcessWindowFunction()) 
.print().setParallelism(1);


输入数据:
000001,1461756870000 
000001,1461756883000 

000001,1461756870000 
000001,1461756871000 
000001,1461756872000
000001,1461756884000 

000001,1461756870000 
000001,1461756871000 
000001,1461756872000 

000001,1461756885000 

000001,1461756870000 
000001,1461756871000 
000001,1461756872000

总结: 当我们设置允许迟到 2 秒的事件,第一次 window 触发的条件是 watermark >= window_end_time; 第二次(或者多次)触发的条件是 watermark < window_end_time + allowedLateness。

(3)sideOutputLateData 收集迟到的数据

输入:

000001,1461756870000 
000001,1461756883000 
迟到的数据 
000001,1461756870000 
000001,1461756871000 
000001,1461756872000

4、多并行度下的WaterMark

一个window可能会接受到多个waterMark,我们以最小的为准。

//把并行度设置为2 
env.setParallelism(2);

输入数据:
000001,1461756870000 
000001,1461756883000 
000001,1461756888000

输出结果:
当前线程ID:55event = (000001,1461756883000)|19:34:43|19:34:43|19:34:33 

当前线程ID:56event = (000001,1461756870000)|19:34:30|19:34:30|19:34:20 
当前线程ID:56event = (000001,1461756888000)|19:34:48|19:34:48|19:34:38 
处理时间:19:31:25 
window start time : 19:34:30 
2> [(000001,1461756870000)|19:34:30] 
window end time : 19:34:33

ID为56的线程有两个WaterMark:20,38 那么38这个会替代20,所以ID为56的线程的WaterMark是38 然后ID为55的线程的WaterMark是33,而ID为56是WaterMark是38,会在里面求一个小的值作为 waterMark,就是33,这个时候会触发Window为[30-33)的窗口,那这个窗口里面就有 (000001,1461756870000)这条数据。