Flink 全量窗口聚合函数

与增量聚合函数不同,全窗口函数需要先收集窗口中的数据,并在内部缓存起来,等到窗口要输出结果的时候再取出数据进行计算。很明显,这就是典型的批处理思路了——先攒数据,等一批都到齐了再正式启动处理流程。这样做毫无疑问是低效的:因为窗口全部的计算任务都积压在了要输出结果的那一瞬间,而在之前收集数据的漫长过程中却无所事事。这就好比平时不用功,到考试之前通宵抱佛脚,肯定不如把工夫花在日常积累上。那为什么还需要有全窗口函数呢?这是因为有些场景下,我们要做的计算必须基于全部的数据才有效,这时做增量聚合就没什么意义了;另外,输出的结果有可能要包含上下文中的一些信息(比如窗口的起始时间),这是增量聚合函数做不到的。所以,我们还需要有更丰富的窗口计算方式,这就可以用全窗口函数来实现。在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中最底层的通用窗口函数接口。之所以说它“最底层”,是因为除了可以拿到窗口中的所有数据之外,ProcessWindowFunction还可以获取到一个“上下文对象”(Context)。这个上下文对象非常强大,不仅能够获取窗口信息,还可以访问当前的时间和状态信息。这里的时间就包括了处理时间(processing time)和事件时间水位线(event time watermark)。这就使得ProcessWindowFunction更加灵活、功能更加丰富。事实上,ProcessWindowFunction是Flink底层API——处理函数(process function)中的一员,当然,这些好处是以牺牲性能和资源为代价的。作为一个全窗口函数,ProcessWindowFunction同样需要将所有数据缓存下来、等到窗口触发计算时才使用。它其实就是一个增强版的WindowFunction。具体使用跟WindowFunction非常类似,可以基于WindowedStream调用.process()方法,传入一个ProcessWindowFunction的实现类。下面是一个电商网站统计每小时UV的例子:

/**
 * 统计每10s 的 UV
 */
public class WindowProcessFunction0627 {
    public static void main(String[] args) throws Exception {
        //获取执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        SingleOutputStreamOperator<Event> eventStream = env.addSource(new ClickSource())
                .assignTimestampsAndWatermarks(
                        WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                                .withTimestampAssigner(
                                        new SerializableTimestampAssigner<Event>() {
                                            @Override
                                            public long extractTimestamp(Event element, long recordTimestamp) {
                                                return element.timestamp;
                                            }
                                        }
                                )
                );

        //方案1 使用全量窗口函数实现:10s 输出一次Uv
        eventStream.keyBy(data -> true)
                .window(TumblingEventTimeWindows.of(Time.seconds(10)))
                .process(new MyProcessWindowFun0627()).print();

        //execut
        env.execute();
    }

    public static class MyProcessWindowFun0627 extends ProcessWindowFunction<Event, String, Boolean, TimeWindow> {


        @Override
        public void process(Boolean aBoolean, Context context, Iterable<Event> elements, Collector<String> out) throws Exception {
            //声明用户数据的结合
            HashSet<String> uSet = new HashSet<>();
            //循环保存
            for (Event element : elements) {
                uSet.add(element.user);
            }
            //获取 Uv
            Long aLong = Long.valueOf(uSet.size());
//获取窗口开始结束时间
            long end = context.window().getEnd();
            long start = context.window().getStart();
            out.collect("窗口  " + new Timestamp(start)+ " ~ " + new Timestamp(end) + "  --> " +aLong);
        }
    }
}

输出结果

窗口  2022-06-27 22:46:20.0 ~ 2022-06-27 22:46:30.0  --> 3
窗口  2022-06-27 22:46:30.0 ~ 2022-06-27 22:46:40.0  --> 5
窗口  2022-06-27 22:46:40.0 ~ 2022-06-27 22:46:50.0  --> 4
窗口  2022-06-27 22:46:50.0 ~ 2022-06-27 22:47:00.0  --> 4

这里使用的是事件时间语义。定义10秒钟的滚动事件窗口后,直接使用ProcessWindowFunction来定义处理的逻辑。我们可以创建一个HashSet,将窗口所有数据的userId写入实现去重,最终得到HashSet的元素个数就是UV值。当然,这里我们并没有用到上下文中其他信息,所以其实没有必要使用ProcessWindowFunction。全窗口函数因为运行效率较低,很少直接单独使用,往往会和增量聚合函数结合在一起,共同实现窗口的处理计算。

posted @ 2022-06-29 06:36  晓枫的春天  阅读(719)  评论(0编辑  收藏  举报