本系列包含:
Flink 的四大基石分别是:
窗口概念:将无界流的数据,按时间区间,划分成多份数据,分别进行统计(聚合)。
Flink 支持两种划分窗口的方式(time
和 count
)。第一种,按 时间驱动 进行划分、另一种按 数据驱动 进行划分。
size
和 滑动间隔 interval
),通过窗口长度和滑动间隔来区分滚动窗口和滑动窗口。
通过组合可以得出四种基本窗口:
(1)基于时间的滚动窗口:time-tumbling-window
无重叠数据的时间窗口,设置方式举例:timeWindow(Time.seconds(5))
。
(2)基于时间的滑动窗口:time-sliding-window
有重叠数据的时间窗口,设置方式举例:timeWindow(Time.seconds(10), Time.seconds(5))
。
注:上图中有点小错误,应该是 size > interval
,所以会有重叠数据。
(3)基于数量的滚动窗口:count-tumbling-window
无重叠数据的数量窗口,设置方式举例:countWindow(5)
。
(4)基于数量的滑动窗口:count-sliding-window
有重叠数据的数量窗口,设置方式举例:countWindow(10,5)
。
Flink 中还支持一个特殊的窗口:会话窗口 SessionWindows。
session 窗口分配器通过 session 活动来对元素进行分组,session 窗口跟滚动窗口和滑动窗口相比,不会有重叠和固定的开始时间和结束时间的情况。
session 窗口在一个固定的时间周期内不再收到元素,即非活动间隔产生,那么这个窗口就会关闭。
一个 session 窗口通过一个 session 间隔来配置,这个 session 间隔定义了非活跃周期的长度,当这个非活跃周期产生,那么当前的 session 将关闭并且后续的元素将被分配到新的 session 窗口中去,如下图所示:
以下为窗口机制的流程图:
WindowAssigner
1、窗口算子负责处理窗口,数据流源源不断地进入算子(Window Operator)时,每一个到达的元素首先会被交给 WindowAssigner。WindowAssigner 会决定元素被放到哪个或哪些窗口(Window),可能会创建新窗口。因为一个元素可以被放入多个窗口中(个人理解是滑动窗口,滚动窗口不会有此现象),所以同时存在多个窗口是可能的。注意,Window 本身只是一个 ID 标识符,其内部可能存储了一些元数据,如 TimeWindow 中有开始和结束时间,但是并不会存储窗口中的元素。窗口中的元素实际存储在 Key/Value State 中,Key 为 Window,Value 为元素集合(或聚合值)。为了保证窗口的容错性,该实现依赖了 Flink 的 State 机制。
WindowTrigger
2、每一个 Window 都拥有一个属于自己的 Trigger,Trigger 上会有定时器,用来决定一个窗口何时能够被计算或清除。每当有元素加入到该窗口,或者之前注册的定时器超时了,那么 Trigger 都会被调用。Trigger 的返回结果可以是 :
当数据到来时,调用 Trigger 判断是否需要触发计算,如果调用结果只是 Fire 的话,那么会计算窗口并保留窗口原样,也就是说窗口中的数据不清理,等待下次 Trigger Fire 的时候再次执行计算。窗口中的数据会被反复计算,直到触发结果清理。在清理之前,窗口和数据不会释放,所以窗口会一直占用内存。
Trigger 触发流程
3、当 Trigger Fire了,窗口中的元素集合就会交给 Evictor(如果指定了的话)。Evictor 主要用来遍历窗口中的元素列表,并决定最先进入窗口的多少个元素需要被移除。剩余的元素会交给用户指定的函数进行窗口的计算。如果没有 Evictor 的话,窗口中的所有元素会一起交给函数进行计算。
4、计算函数收到了窗口的元素(可能经过了 Evictor 的过滤),并计算出窗口的结果值,并发送给下游。窗口的结果值可以是一个也可以是多个。DataStream API 上可以接收不同类型的计算函数,包括预定义的 sum()
、min()
、max()
,还有 ReduceFunction
,FoldFunction
,还有 WindowFunction
。WindowFunction
是最通用的计算函数,其他的预定义的函数基本都是基于该函数实现的。
5、Flink 对于一些聚合类的窗口计算(如 sum
、min
)做了优化,因为聚合类的计算不需要将窗口中的所有数据都保存下来,只需要保存一个 result
值就可以了。每个进入窗口的元素都会执行一次聚合函数并修改 result
值。这样可以大大降低内存的消耗并提升性能。但是如果用户定义了 Evictor,则不会启用对聚合窗口的优化,因为 Evictor 需要遍历窗口中的所有元素,必须要将窗口中所有元素都存下来。
在 Flink 的流式处理中,会涉及到时间的不同概念,主要分为三种时间机制,如下图所示:
在 Flink 的流式处理中,绝大部分的业务都会使用 EventTime,一般只在 EventTime 无法使用时,才会被迫使用 ProcessingTime 或者 IngestionTime。
final StreamExecutionEnvironment env
= StreamExecutionEnvironment.getExecutionEnvironrnent();
// 使用处理时间
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime) ;
// 使用摄入时间
env.setStrearnTimeCharacteristic(TimeCharacteristic.IngestionTime);
// 使用事件时间
env.setStrearnTimeCharacteristic(TimeCharacteri stic Eve~tTime);
有遇到过数据延迟问题。举个例子:
案例 1:
案例 2:
一般处理数据延迟、消息乱序等问题,通过 WaterMark 水印来处理。水印是用来解决数据延迟、数据乱序等问题,总结如下图所示:
水印就是一个时间戳(timestamp
),Flink 可以给数据流添加水印:
如下图所示:
窗口是 10 分钟触发一次,现在在 12:00 - 12:10 有一个窗口,本来有一条数据是在 12:00 - 12:10 这个窗口被计算,但因为延迟,12:12 到达,这时 12:00 - 12:10 这个窗口就会被关闭,只能将数据下发到下一个窗口进行计算,这样就产生了数据延迟,造成计算不准确。
现在添加一个水位线:数据时间戳为 2 分钟。这时用数据产生的事件时间 12:12 - 允许延迟的水印 2 分钟 = 12:10 >= 窗口结束时间 。窗口触发计算,该数据就会被计算到这个窗口里。
在 DataStream API 中使用 TimestampAssigner 接口定义时间戳的提取行为,包含两个子接口 AssignerWithPeriodicWatermarks
接口和 AssignerWithPunctuatedWaterMarks
接口。
定义抽取时间戳,以及生成 WaterMark 的方法,有两种类型
AssignerWithPeriodicWatermarks
ExecutionConfig.setAutoWatermarklnterval()
设置。BoundedOutOfOrderness
是基于周期性 WaterMark 的。AssignerWithPunctuatedWatermarks
定期生成 | 根据特殊记录生成 |
---|---|
现实时间驱动 | 数据驱动 |
每一次分配 Timestamp 都会调用生成方法 | 每隔一段时间调用生成方法 |
实现 AssignerWithPeriodicWatermarks |
实现 AssignerWithPunctuatedWatermarks |
使用 WaterMark + EventTimeWindow 机制可以在一定程度上解决数据乱序的问题,但是,WaterMark 水位线也不是万能的,在某些情况下,数据延迟会非常严重,即使通过 Watermark + EventTimeWindow 也无法等到数据全部进入窗口再进行处理,因为窗口触发计算后,对于延迟到达的本属于该窗口的数据,Flink 默认会将这些延迟严重的数据进行丢弃。
那么如果想要让一定时间范围的延迟数据不会被丢弃,可以使用 Allowed Lateness(允许迟到机制 / 侧道输出机制)设定一个允许延迟的时间和侧道输出对象来解决。
即使用 WaterMark + EventTimeWindow + Allowed Lateness 方案(包含侧道输出),可以做到数据不丢失。
API 调用
allowedLateness(lateness:Time)
:设置允许延迟的时间该方法传入一个 Time 值,设置允许数据迟到的时间,这个时间和 WaterMark 中的时间概念不同。
再来回顾一下,WaterMark = 数据的事件时间 - 允许乱序时间值。随着新数据的到来,WaterMark 的值会更新为最新数据事件时间 - 允许乱序时间值,但是如果这时候来了一条历史数据,WaterMark 值则不会更新。
总的来说,WaterMark 永远不会倒退,它是为了能接收到尽可能多的乱序数据。
那这里的 Time 值呢?主要是为了等待迟到的数据,如果属于该窗口的数据到来,仍会进行计算,后面会对计算方式仔细说明。
注意:该方法只针对于基于 EventTime 的窗口。
sideOutputLateData(outputTag:OutputTag[T])
:保存延迟数据该方法是将迟来的数据保存至给定的 outputTag 参数,而 OutputTag 则是用来标记延迟数据的一个对象。
DataStream.getSideOutput(tag:OutputTag[X])
:获取延迟数据通过 window 等操作返回的 DataStream 调用该方法,传入标记延迟数据的对象来获取延迟的数据。