6.3.5 窗口函数(Window Functions)

本文介绍了ApacheFlink中窗口分配器与窗口函数的作用,区分了增量聚合函数(如ReduceFunction和AggregateFunction)与全窗口函数(如WindowFunction和ProcessWindowFunction),展示了如何在实际场景中结合使用它们以提高实时性和灵活性。
摘要由CSDN通过智能技术生成

介绍

定义了窗口分配器,我们只是知道了数据属于哪个窗口,可以将数据收集起来了;至于收
集起来到底要做什么,其实还完全没有头绪。所以在窗口分配器之后,必须再接上一个定义窗
口如何进行计算的操作,这就是所谓的“窗口函数”(window functions)。
经窗口分配器处理之后,数据可以分配到对应的窗口中,而数据流经过转换得到的数据类
型是 WindowedStream。这个类型并不是 DataStream,所以并不能直接进行其他转换,而必须
进一步调用窗口函数,对收集到的数据进行处理计算之后,才能最终再次得到 DataStream
在这里插入图片描述

窗口函数定义了要对窗口中收集的数据做的计算操作,根据处理的方式可以分为两类:增
量聚合函数和全窗口函数。下面我们来进行分别讲解

1. 增量聚合函数(incremental aggregation functions)

为了提高实时性,我们可以像 DataStream 的简单聚合一样,每来一条数据就立即进行计
算,中间只要保持一个简单的聚合状态就可以了;区别只是在于不立即输出结果,而是要等到
窗口结束时间。等到窗口到了结束时间需要输出计算结果的时候,我们只需要拿出之前聚合的
状态直接输出,这无疑就大大提高了程序运行的效率和实时性。
典型的增量聚合函数有两个:ReduceFunction 和 AggregateFunction。

(1)归约函数(ReduceFunction)

最基本的聚合方式就是归约(reduce)。我们在基本转换的聚合算子中介绍过 reduce 的用
法,窗口的归约聚合也非常类似,就是将窗口中收集到的数据两两进行归约。当我们进行流处
理时,就是要保存一个状态;每来一个新的数据,就和之前的聚合状态做归约,这样就实现了
增量式的聚合。
窗口函数中也提供了 ReduceFunction:只要基于 WindowedStream 调用.reduce()方法,然
后传入 ReduceFunction 作为参数,就可以指定以归约两个元素的方式去对窗口中数据进行聚
合了。这里的 ReduceFunction 其实与简单聚合时用到的 ReduceFunction 是同一个函数类接口,
所以使用方式也是完全一样的。
下面是使用 ReduceFunction 进行增量聚合的代码示例。

import com.atguigu.chapter05.ClickSource
import org.apache.flink.streaming.api.scala._
100
import 
org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
object WindowReduceExample {
 def main(args: Array[String]): Unit = {
 val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 env
 .addSource(new ClickSource)
 // 数据源中的时间戳是单调递增的,所以使用下面的方法,只需要抽取时间戳就好了
 // 等同于最大延迟时间是 0 毫秒
 .assignAscendingTimestamps(_.timestamp)
 .map(r => (r.user, 1L))
 // 使用用户名对数据流进行分组
 .keyBy(_._1)
 // 设置 5 秒钟的滚动事件时间窗口
 .window(TumblingEventTimeWindows.of(Time.seconds(5)))
 // 保留第一个字段,针对第二个字段进行聚合
 .reduce((r1, r2) => (r1._1, r1._2 + r2._2))
 .print()
 env.execute()
 }
}

(2)聚合函数(AggregateFunction)

ReduceFunction 可以解决大多数归约聚合的问题,但是这个接口有一个限制,就是聚合状
态的类型、输出结果的类型都必须和输入数据类型一样。
101
为了更加灵活地处理窗口计算,Flink的Window API提供了更加一般化的aggregate()方法。
直接基于 WindowedStream 调用 aggregate()方法,就可以定义更加灵活的窗口聚合操作。这个
方法需要传入一个 AggregateFunction 的实现类作为参数。AggregateFunction 在源码中的定义
如下:

public interface AggregateFunction<IN, ACC, OUT> extends Function, Serializable 
{
 ACC createAccumulator();
 ACC add(IN value, ACC accumulator);
 OUT getResult(ACC accumulator);
 ACC merge(ACC a, ACC b);
}

AggregateFunction 可以看作是 ReduceFunction 的通用版本,这里有三种类型:输入类型
(IN)、累加器类型(ACC)和输出类型(OUT)。输入类型 IN 就是输入流中元素的数据类型;
累加器类型 ACC 则是我们进行聚合的中间状态类型;而输出类型当然就是最终计算结果的类
型了。
AggregateFunction 接口中有四个方法:
⚫ createAccumulator():创建一个累加器,这就是为聚合创建了一个初始状态,每个聚
合任务只会调用一次。
⚫ add():将输入的元素添加到累加器中。这就是基于聚合状态,对新来的数据进行进
一步聚合的过程。方法传入两个参数:当前新到的数据 value,和当前的累加器
accumulator;返回一个新的累加器值,也就是对聚合状态进行更新。每条数据到来之
后都会调用这个方法。
⚫ getResult():从累加器中提取聚合的输出结果。也就是说,我们可以定义多个状态,
然后再基于这些聚合的状态计算出一个结果进行输出。比如之前我们提到的计算平均
值,就可以把 sum 和 count 作为状态放入累加器,而在调用这个方法时相除得到最终
结果。这个方法只在窗口要输出结果时调用。
⚫ merge():合并两个累加器,并将合并后的状态作为一个累加器返回。这个方法只在
需要合并窗口的场景下才会被调用;最常见的合并窗口(Merging Window)的场景
就是会话窗口(Session Windows)。
下面来看一个具体例子。我们计算一下 PV/UV 这个比值,来表示“人均重复访问量”,
也就是平均每个用户会访问多少次页面,这在一定程度上代表了用户的黏度。
代码实现如下:

import com.atguigu.chapter05.{ClickSource, Event}
import org.apache.flink.api.common.functions.AggregateFunction
import org.apache.flink.streaming.api.scala._
import 
102
103
org.apache.flink.streaming.api.windowing.assigners.SlidingEventTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
object AggregateFunctionExample {
 def main(args: Array[String]): Unit = {
 val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 
env
 .addSource(new ClickSource)
 .assignAscendingTimestamps(_.timestamp)
 // 通过为每条数据分配同样的 key,来将数据发送到同一个分区
 .keyBy(_ => "key")
 .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(2)))
 .aggregate(new AvgPv)
 .print()
 env.execute()
 }
 class AvgPv extends AggregateFunction[Event, (Set[String], Double), Double] {
 // 创建空累加器,类型是元组,元组的第一个元素类型为 Set 数据结构,用来对用户名进行去重
 // 第二个元素用来累加 pv 操作,也就是每来一条数据就加一
 override def createAccumulator(): (Set[String], Double) = (Set[String](), 0L)
 // 累加规则
 override def add(value: Event, accumulator: (Set[String], Double)): 
(Set[String], Double) = (accumulator._1 + value.user, accumulator._2 + 1L)
 // 获取窗口关闭时向下游发送的结果
 override def getResult(accumulator: (Set[String], Double)): Double = 
accumulator._2 / accumulator._1.size
 // merge 方法只有在事件时间的会话窗口时,才需要实现,这里无需实现。
 override def merge(a: (Set[String], Double), b: (Set[String], Double)): 
(Set[String], Double) = ???
 }
}

另外,Flink 也为窗口的聚合提供了一系列预定义的简单聚合方法,可以直接基于
WindowedStream 调用。主要包括 sum()/max()/maxBy()/min()/minBy(),与 KeyedStream 的简单
聚合非常相似。它们的底层,其实都是通过 AggregateFunction 来实现的。

2. 全窗口函数(Full Window Functions)

窗口操作中的另一大类就是全窗口函数。与增量聚合函数不同,全窗口函数需要先收集窗
口中的数据,并在内部缓存起来,等到窗口要输出结果的时候再取出数据进行计算。
在 Flink 中,全窗口函数也有两种:WindowFunction 和 ProcessWindowFunction。

(1)窗口函数(WindowFunction)

WindowFunction 字面上就是“窗口函数”,它其实是老版本的通用窗口函数接口。我们可
以基于 WindowedStream 调用.apply()方法,传入一个 WindowFunction 的实现类。

stream
 .keyBy(<key selector>)
 .window(<window assigner>)
 .apply(new MyWindowFunction())

这个类中可以获取到包含窗口所有数据的可迭代集合(Iterable),还可以拿到窗口
(Window)本身的信息。WindowFunction 接口在源码中实现如下:

public interface WindowFunction<IN, OUT, KEY, W extends Window> extends Function, 
Serializable {
void apply(KEY key, W window, Iterable<IN> input, Collector<OUT> out) throws 
Exception;
}

当窗口到达结束时间需要触发计算时,就会调用这里的 apply 方法。我们可以从 input 集
合中取出窗口收集的数据,结合 key 和 window 信息,通过收集器(Collector)输出结果。这
里 Collector 的用法,与 FlatMapFunction 中相同。
不过我们也看到了,WindowFunction 能提供的上下文信息较少,也没有更高级的功能。
事实上,它的作用可以被 ProcessWindowFunction 全覆盖,所以之后可能会逐渐弃用。一般在
实际应用,直接使用 ProcessWindowFunction 就可以了。

(2)处理窗口函数(ProcessWindowFunction)

ProcessWindowFunction 是 Window API 中最底层的通用窗口函数接口。之所以说它“最底
104
105
层”,是因为除了可以拿到窗口中的所有数据之外,ProcessWindowFunction 还可以获取到一个
“上下文对象”(Context)。这个上下文对象非常强大,不仅能够获取窗口信息,还可以访问当
前的时间和状态信息。这里的时间就包括了处理时间(processing time)和事件时间水位线(event
time watermark)。这就使得 ProcessWindowFunction 更加灵活、功能更加丰富,可以认为是一
个增强版的 WindowFunction。
具体使用跟 WindowFunction 非常类似,我们可以基于 WindowedStream 调用 process()方
法,传入一个 ProcessWindowFunction 的实现类。下面是一个电商网站统计每小时 UV 的例子:

import com.atguigu.chapter05.{ClickSource, Event}
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.ProcessWindowFunction
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.util.Collector
import java.sql.Timestamp
import scala.collection.mutable.Set
object UvCountByWindowExample {
 def main(args: Array[String]): Unit = {
 val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 env
 .addSource(new ClickSource)
 .assignAscendingTimestamps(_.timestamp)
 // 为所有数据都指定同一个 key,可以将所有数据都发送到同一个分区
 .keyBy(_ => "key")
 .window(TumblingEventTimeWindows.of(Time.seconds(10)))
 .process(new UvCountByWindow)
 .print()
 env.execute()
 }
 // 自定义窗口处理函数
 class UvCountByWindow extends ProcessWindowFunction[Event, String, String, 
TimeWindow] {
 override def process(key: String, context: Context, elements: Iterable[Event], 
out: Collector[String]): Unit = {
 // 初始化一个 Set 数据结构,用来对用户名进行去重
 var userSet = Set[String]()
 // 将所有用户名进行去重
 elements.foreach(userSet += _.user)
 // 结合窗口信息,包装输出内容
 val windowStart = context.window.getStart
 val windowEnd = context.window.getEnd
 out.collect(" 窗 口 : " + new Timestamp(windowStart) + "~" + new 
Timestamp(windowEnd) + "的独立访客数量是:" + userSet.size)
 }
 }
}

3. 增量聚合和全窗口函数的结合使用

我们已经了解了 Window API 中两类窗口函数的用法,下面我们先来做个简单的总结。
增量聚合函数处理计算会更高效。增量聚合相当于把计算量“均摊”到了窗口收集数据的
过程中,自然就会比全窗口聚合更加高效、输出更加实时。
而全窗口函数的优势在于提供了更多的信息,可以认为是更加“通用”的窗口操作,窗口
计算更加灵活,功能更加强大。
所以在实际应用中,我们往往希望兼具这两者的优点,把它们结合在一起使用。Flink 的
Window API 就给我们实现了这样的用法。
我们之前在调用 WindowedStream 的 reduce()和 aggregate()方法时,只是简单地直接传入
了一个 ReduceFunction 或 AggregateFunction 进行增量聚合。除此之外,其实还可以传入第二
106
107
个参数:一个全窗口函数,可以是 WindowFunction 或者 ProcessWindowFunction。

// ReduceFunction 与 WindowFunction 结合
public <R> SingleOutputStreamOperator<R> reduce(
 ReduceFunction<T> reduceFunction, WindowFunction<T, R, K, W> function) 
// ReduceFunction 与 ProcessWindowFunction 结合
public <R> SingleOutputStreamOperator<R> reduce(
 ReduceFunction<T> reduceFunction, ProcessWindowFunction<T, R, K, W> 
function)
// AggregateFunction 与 WindowFunction 结合
public <ACC, V, R> SingleOutputStreamOperator<R> aggregate(
 AggregateFunction<T, ACC, V> aggFunction, WindowFunction<V, R, K, W> 
windowFunction)
// AggregateFunction 与 ProcessWindowFunction 结合
public <ACC, V, R> SingleOutputStreamOperator<R> aggregate(
 AggregateFunction<T, ACC, V> aggFunction,
 ProcessWindowFunction<V, R, K, W> windowFunction)

这样调用的处理机制是:基于第一个参数(增量聚合函数)来处理窗口数据,每来一个数
据就做一次聚合;等到窗口需要触发计算时,则调用第二个参数(全窗口函数)的处理逻辑输
出结果。需要注意的是,这里的全窗口函数就不再缓存所有数据了,而是直接将增量聚合函数
的结果拿来当作了 Iterable 类型的输入。一般情况下,这时的可迭代集合中就只有一个元素了。
下面我们举一个具体的实例来说明。我们这里统计 10 秒钟的 url 浏览量,每 5 秒钟更新
一次;另外为了更加清晰地展示,还应该把窗口的起始结束时间一起输出。我们可以定义滑动
窗口,并结合增量聚合函数和全窗口函数来得到统计结果。
具体实现代码如下:

import com.atguigu.chapter05.Event
import com.atguigu.chapter06.EmitWatermarkInSourceFunction.ClickSource
import org.apache.flink.api.common.functions.AggregateFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.ProcessWindowFunction
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.util.Collector
object UrlViewCountExample {
 def main(args: Array[String]): Unit = {
 val env = StreamExecutionEnvironment.getExecutionEnvironment
 env.setParallelism(1)
 env
 .addSource(new ClickSource)
 .assignAscendingTimestamps(_.timestamp)
 // 使用 url 作为 key 对数据进行分区
 .keyBy(_.url)
 .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
 // 注意这里调用的是 aggregate 方法
 // 增量聚合函数和全窗口聚合函数结合使用
 .aggregate(new UrlViewCountAgg, new UrlViewCountResult)
 .print()
 env.execute()
 }
 class UrlViewCountAgg extends AggregateFunction[Event, Long, Long] {
 override def createAccumulator(): Long = 0L
 // 每来一个事件就加一
 override def add(value: Event, accumulator: Long): Long = accumulator + 1L
 // 窗口闭合时发送的计算结果
 override def getResult(accumulator: Long): Long = accumulator
 override def merge(a: Long, b: Long): Long = ???
 }
 class UrlViewCountResult extends ProcessWindowFunction[Long, UrlViewCount, 
String, TimeWindow] {
 // 迭代器中只有一个元素,是增量聚合函数在窗口闭合时发送过来的计算结果
108
109
 override def process(key: String, context: Context, elements: Iterable[Long], 
out: Collector[UrlViewCount]): Unit = {
 out.collect(UrlViewCount(
 key,
 elements.iterator.next(),
 context.window.getStart,
 context.window.getEnd
 ))
 }
 }
 case class UrlViewCount(url: String, count: Long, windowStart: Long, windowEnd: 
Long)
}

这里我们为了方便处理,单独定义了一个样例类 UrlViewCount 来表示聚合输出结果的数
据类型,包含了 url、浏览量以及窗口的起始结束时间。用一个 AggregateFunction 来实现增量
聚合,每来一个数据就计数加一;得到的结果交给 ProcessWindowFunction,结合窗口信息包
装成我们想要的 UrlViewCount,最终输出统计结果。

  • 21
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值