streaming 流式计算是一种被设计用于处理无限数据集的数据处理引擎,而无限数据集是指一种不断增长的本质上无限的数据集,而 window 是一种切割无限数据为有限块进行处理的手段。
Window 是无限数据流处理的核心,Window 将一个无限的 stream 拆分成有限大小的”buckets”桶,我们可以在这些桶上做计算操作。
一、Window可以分为两类:
- CountWindow:按照指定的数据条数生成一个Window,与时间无关。
- TimeWindow:按照时间生成Window。
对于TimeWindow,可以分为:
- 滚动窗口(Tumling Window)
- 将数据依据固定的窗口长度对数据进行切分
- 特点:时间对齐,窗口长度固定,没有重叠
- 适用场景:BI统计等(做每个时间段的聚合计算)
- 滑动窗口(Sliding Window)
- 特点:窗口长度固定,可以有重叠
- 当窗口的滑动长度=窗口长度时,就相当于滚动窗口,也就是说,滚动窗口是一种特殊的滑动窗口
- 适用场景:对最近一个时间段内的统计(求每个接口最近5min的失败率来决定是否要报警)
- 会话窗口(Session Window)
二、窗口分配器(Window Assingers):
指定完数据流是分组的还是非分组的之后,接卸来就需要定义一个窗口分配器(window assinger),窗口分配器定义了元素如何分配到窗口中,分组数据流中使用.window()来定义一个窗口,方法接收的参数是一个窗口分配器,窗口分配器负责将每条输入的数据分发到正确的窗口中。
Flink提供了通用的窗口分配器:
- 滚动窗口分配器
- 滑动窗口分配器
- 会话窗口分配器
- 全局窗口分配器
具体类:
红框为常用的窗口分配器
三、窗口函数(Window Functions)(非常重要)
定义完窗口分配器后,我们还需要为每一个窗口执行我们需要执行的计算,这是窗口的责任,
window函数可以是 ReduceFunction, FoldFunction 或者 WindowFunction 中的一个。
前面两个更高效一些,因为在每个窗口中增量地对每一个到达的元素执行聚合操作。
一个 WindowFunction 可以获取一个窗口中的所有元素的一个迭代以及哪个元素属于哪个窗口的额外元信息。
所以,窗口函数的分类:
- 增量聚合函数
- 可选函数
- ReduceFunction
- 使用方法:.reduce(ReduceFunction,[...])
- FoldFunction
- 使用方法:.fold(FoldFunction,[...])
- AggregateFunction
- 使用方法:.aggregate(AggregateFunction,[...])
- 继承自ReduceFunction
- 已被标记为删除状态,以后可能不能用了
- 使用方法:.fold(FoldFunction,[...])
- 特点
- 计算性能比较高,占用存储空间少
- 每条数据到来就进行计算,保持一个简单的状态,不需要缓存原始数据。
- 通俗说,来一条数据计算一次,将结果保存成一个状态,到窗口关闭的时间点时将最后一次计算的结果输出
- 全窗口函数
- 可选函数
- WindowFunction
- 使用方法:.apply(WindowFunction)
- ProcessWindowFunction
- 使用WindowFunction的地方都可以使用ProcessWindowFunction
- 功能与WindowFunction类似,多了允许查询更多关于context的信息
- context是window发生的地方的一些上下文信息
- 使用方法:.process(ProcessWindowFunction)
- 特点
- 先把窗口中所有数据缓存起来,等到窗口触发时,对所有数据进行汇总统计
- 如果数据量比较大或者窗口时间比较长,就比较有可能导致计算机性能下降。
- 自己的思考:窗口的数据存在哪儿?会不会OOM?
WindowFunction 的窗口化操作会比其他的操作效率要差一些,因为Flink内部在调用函数之前会将窗口中的额所有元素都缓存起来。
当我们需要使用 WindowFunction 中窗口额外的元数据信息,又希望能提升一些性能时应该怎么做呢?
可以通过 WindowFunction 和 ReduceFunction 或者 WindowFunction 和 FoldFunction 结合使用,来获取窗口中所有元素的增量聚合和WindowFunction接收的额外元数据。
具体实现:
使用.reduce(ReduceFunction,WindowFunction),
- 第一个参数传的是一个ReduceFunction,当窗口开启时,来一条数据调用一次,用来做数据的增量聚合,聚合后的数据保存成一个状态,当窗口关闭前,将这个状态数据传给第二个参数对应的函数
- 第二个参数传的是一个WindowFunction,当窗口关闭时调用,此时,WindowFunction中对应的"所有"数据就是 参数一对应函数做增量聚合得到的结果,一般情况下就只有一条记录。此时,在WindowFunction中取出增量函数计算的结果,再填充出当时窗口对应的元数据信息,即可输出结果,此时的结果就是这一次窗口计算最终的结果。
接下来我们看每一种Function的演示:
1.ReduceFunction
ReduceFunction指定了如何通过两个输入的参数进行合并,输出一个同类型的参数的过程,增量聚合。
如:
windowedDS.reduce(
new ReduceFunction[ProductStatsV2]() {
override def reduce(stats1: ProductStatsV2, stats2: ProductStatsV2): ProductStatsV2 = {
stats1.setDisplay_ct(stats1.getDisplay_ct() + stats2.getDisplay_ct())
// 其他聚合操作
stats1
}
})
2.FoldFunction
FoldFunction 函数不常用,不做演示
3.窗口函数(WindowFunction) —— 一般用法
一个 WindowFunction 将获得一个包含window中的所有元素迭代(Iterable),并且提供所有窗口函数的最大灵活性。这些带来了性能的成本和资源的消耗。
因为window中的元素无法进行增量迭代,而是缓存起来直到window窗口关闭时(被认为是可以处理时)为止。
WindowFunction 的使用说明如下:
trait WindowFunction[IN, OUT, KEY, W <: Window] extends Function with Serializable {
/**
// Evaluates the window and outputs none or several elements.
// @param key 此窗口的 key
// @param window 窗口信息(窗口起始时间,结束时间等元信息)
// @param input 窗口中的元素(所有数据)
// @param out 元素输出收集器
// @throws Exception The function may throw exceptions to fail the program and trigger recovery.
*/
def apply(key: KEY, window: W, input: Iterable[IN], out: Collector[OUT])
}
使用示例:
val input: DataStream[(String, Long)] = ...
input
.keyBy(<key selector>)
.window(<window assigner>)
.apply(new MyWindowFunction())
/* ... */
class MyWindowFunction extends WindowFunction[(String, Long), String, String, TimeWindow] {
def apply(key: String, window: TimeWindow, input: Iterable[(String, Long)], out: Collector[String]): () = {
var count = 0L
// 窗口计算逻辑
for (in <- input) {
count = count + 1
}
// 获取窗口元信息
window.getStart
window.getend
out.collect(s"Window $window count: $count")
}
}
上面的例子展示了统计一个window中元素个数的WindowFunction,此外,还将window的信息添加到输出中。
注意:使用WindowFunction来做简单的聚合操作如计数操作,性能是相当差的。
下面我们将展示如何将ReduceFunction跟WindowFunction结合起来,来获取增量聚合和添加到WindowFunction中的信息。
4.ProcessWindowFunction
在使用WindowFunction的地方你也可以用ProcessWindowFunction,这跟WindowFunction很类似,还允许查询更多关于context的信息,context是window评估发生的地方。
下面是ProcessWindowFunction的接口:
abstract class ProcessWindowFunction[IN, OUT, KEY, W <: Window] extends Function {
/**
// 。。
*/
@throws[Exception]
def process(
key: KEY,
context: Context,
elements: Iterable[IN],
out: Collector[OUT])
/**
// The context holding window metadata
*/
abstract class Context {
/**
// @return The window that is being evaluated.
*/
def window: W
}
}
可以通过这种方式调用:
val input: DataStream[(String, Long)] = ...
input
.keyBy(<key selector>)
.window(<window assigner>)
.process(new MyProcessWindowFunction())
5.有增量聚合功能的WindowFunction(ReduceFunction与WindowFunction结合)
WindowFunction可以跟ReduceFunction或者FoldFunction结合来增量地对到达window中的元素进行聚合,
当window关闭之后,WindowFunction就能提供聚合结果。当获取到WindowFunction额外的window元信息后就可以进行增量计算窗口了。
标注:你也可以使用ProcessWindowFunction替换WindowFunction来进行增量窗口聚合。
使用ReduceFunction与WindowFunction结合进行增量窗口聚合(FoldFunction 方法不做演示)的示例:
// 需要提取开窗信息 使用reduce算子,传递两个参数:reducefunction windowfunction
// 来一条数据,使用reducefunction聚合一次,等窗口关闭要准备输出数据时,将reducefunction算好的数据传给 windowfunction 中进行加工
// 此时 windowfunction 中只有一条数据,这条数就是被reducefunction加工后的数据
val reduceDS = windowedDS.reduce(
// 增量聚合,来一条数据聚合一次,待窗口准备关闭前交给windowFunction
new ReduceFunction[ProductStatsV2]() {
override def reduce(stats1: ProductStatsV2, stats2: ProductStatsV2): ProductStatsV2 = {
stats1.setDisplay_ct(stats1.getDisplay_ct() + stats2.getDisplay_ct())
// 其他增量聚合逻辑
stats1
}
},
// 窗口准备关闭前,reduceFunction 将加工好的数据传送过来,windowFunction中填充窗口信息
new WindowFunction[ProductStatsV2, ProductStatsV2, Long, TimeWindow]() {
override def apply(key: Long, window: TimeWindow, input: Iterable[ProductStatsV2], out: Collector[ProductStatsV2]): Unit = {
// 取出数据
val productStats = input.iterator.next() // 增量聚合的结果,此时只有一条数据
// 设置窗口时间 (从window中获取)
productStats.setStt(DateTimeUtil.toYMDhms(new Date(window.getStart)))
productStats.setEdt(DateTimeUtil.toYMDhms(new Date(window.getEnd)))
// 设置订单数量
productStats.setOrder_ct(productStats.getOrderIdSet().size())
productStats.setPaid_order_ct(productStats.getPaidOrderIdSet().size())
productStats.setRefund_order_ct(productStats.getRefundOrderIdSet().size())
// 将数据写出
out.collect(productStats)
}
})
其他AggregateFunction、FoldFunction 与 WindowFunction/ProcessWindowFunction 结合实现增量聚合的方法类似
四、在.window()方法后面,还可以定义其他可选的API
- .trigger() —— 触发器
- 定义 window 什么时候关闭,触发计算并输出结果
- .evitor() —— 移除器
- 定义移除某些数据的逻辑
- 类似于filter,将不想要的数据从窗口中移除
- .allowedLateness() —— 允许处理迟到的数据
- 分布式中,有可能出现数据乱序,可能出现本来先发生的事件数据后面才到,等到窗口关闭时,这个数据还没到怎么办?
- 可以定义allowedLateness() 里面传入一个时间(假如1 s),表示允许处理迟到1s的数据
- .sideOutputLateData() —— 将迟到的数据放入侧输出流
- 这个函数一般配合上面的一起使用
- allowedLateness()允许处理迟到的数据,那么应该设置多长时间呢,如果设的时间太长的话,对内存压力会比较大,不可能一直这么等下去,所以一般都会设一个比较小的值。
- 那么问题来了,仍然有数据超过了这个时间还没到的数据怎么办呢?把这些迟到的数据放到侧输出流中。(什么是侧输出流?)
- 那侧输出流的数据该如何获取呢?下面的函数实现。
- .getSideOutput() —— 获取侧输出流
- 经过上面的函数处理后,会得到一个DataStream,调用.getSideOutput()就能获取侧输出流的数据。
|