处理函数应用案例——TopN

窗口的计算处理,在实际应用中非常常见。对于一些比较复杂的需求,如果增量聚合函数无法满足,就需要考虑使用窗口处理函数这样的“大招”了。网站中一个非常经典的例子,就是实时统计一段时间内的热门url。例如,需要统计最近10秒钟内最热门的两个url链接,并且每5秒钟更新一次。这可以用一个滑动窗口来实现,而“热门度”一般可以直接用访问量来表示。于是就需要开滑动窗口收集url的访问数据,按照不同的url进行统计,而后汇总排序并最终输出前两名。这其实就是著名的“Top N”问题。很显然,简单的增量聚合可以得到url链接的访问量,但是后续的排序输出Top N就很难实现了。所以接下来用窗口处理函数进行实现。

1、思路

一是对数据进行按键分区,分别统计浏览量;二是进行增量聚合,得到结果最后再做排序输出。所以,可以使用增量聚合函数AggregateFunction进行浏览量的统计,然后结合ProcessWindowFunction排序输出来实现Top N的需求。具体实现思路就是,先按照url对数据进行keyBy分区,然后开窗进行增量聚合。这里就会发现一个问题:进行按键分区之后,窗口的计算就会只针对当前key有效了;也就是说,每个窗口的统计结果中,只会有一个url的浏览量,这是无法直接用ProcessWindowFunction进行排序的。所以只能分成两步:先对每个url链接统计出浏览量,然后再将统计结果收集起来,排序输出最终结果。因为最后的排序还是基于每个时间窗口的,所以为了让输出的统计结果中包含窗口信息,可以借用第六章中定义的POJO类UrlViewCount来表示,它包含了url、浏览量(count)以及窗口的起始结束时间。之后对UrlViewCount的处理,可以先按窗口分区,然后用KeyedProcessFunction来实现。 总结处理流程如下:
  • 读取数据源;
  • 筛选浏览行为(pv);
  • 提取时间戳并生成水位线;
  • 按照url进行keyBy分区操作;
  • 开长度为10 min、步长为5 min的事件时间滑动窗口;
  • 使用增量聚合函数AggregateFunction,并结合全窗口函数WindowFunction进行窗口聚合,得到每个url、在每个统计窗口内的浏览量,包装成UrlViewCount;
  • 按照窗口进行keyBy分区操作;
  • 对同一窗口的统计结果数据,使用KeyedProcessFunction进行收集并排序输出;

这里又会带来另一个问题。最后用KeyedProcessFunction来收集数据做排序,这时面对的就是窗口聚合之后的数据流,而窗口已经不存在了;那到底什么时候会收集齐所有数据呢?这问题听起来似乎有些没道理。统计浏览量的窗口已经关闭,就说明了当前已经到了要输出结果的时候,直接输出不就行了吗?没有这么简单。因为数据流中的元素是逐个到来的,所以即使理论上应该“同时”收到很多url的浏览量统计结果,实际也是有先后的、只能一条一条处理。下游任务(就是定义的KeyedProcessFunction)看到一个url的统计结果,并不能保证这个时间段的统计数据不会再来了,所以也不能贸然进行排序输出。解决的办法,自然就是要等所有数据到齐了——这很容易让联想起水位线设置延迟时间的方法。这里也可以“多等一会儿”,等到水位线真正超过了窗口结束时间,要统计的数据就肯定到齐了。具体实现上,可以采用一个延迟触发的事件时间定时器。基于窗口的结束时间来设定延迟,其实并不需要等太久——因为是靠水位线的推进来触发定时器,而水位线的含义就是“之前的数据都到齐了”。所以只需要设置1毫秒的延迟,就一定可以保证这一点。而在等待过程中,之前已经到达的数据应该缓存起来,这里用一个自定义的“列表状态”(ListState)来进行存储,如下图所示。这个状态需要使用富函数类的getRuntimeContext()方法获取运行时上下文来定义,一般把它放在open()生命周期方法中。之后每来一个UrlViewCount,就把它添加到当前的列表状态中,并注册一个触发时间为窗口结束时间加1毫秒(windowEnd + 1)的定时器。待到水位线到达这个时间,定时器触发,可以保证当前窗口所有url的统计结果UrlViewCount都到齐了;于是从状态中取出进行排序输出。

2、代码

具体代码如下:

import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.SlidingEventTimeWindows;
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 org.wdh01.bean.Event;
import org.wdh01.bean.UrlViewCount;
import org.wdh01.chapter05.ClickSource;

import java.sql.Timestamp;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Comparator;

public class KeyedProcessTopN0704 {
    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(1))
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event element, long recordTimestamp) {
                                return element.timestamp;
                            }
                        }));
        // 需要按照url分组,求出每个url的访问量
        SingleOutputStreamOperator<UrlViewCount> aggregateStream = eventStream.keyBy(data -> data.url)
                .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
                .aggregate(new AggregateFunction<Event, Long, Long>() {
                               @Override
                               public Long createAccumulator() {
                                   return 0L;
                               }

                               @Override
                               public Long add(Event value, Long accumulator) {
                                   return accumulator + 1L;
                               }

                               @Override
                               public Long getResult(Long accumulator) {
                                   return accumulator;
                               }

                               @Override
                               public Long merge(Long a, Long b) {
                                   return a + b;
                               }
                           }, new ProcessWindowFunction<Long, UrlViewCount, String, TimeWindow>() {

                               //包装数据
                               @Override
                               public void process(String url, Context context, Iterable<Long> elements, Collector<UrlViewCount> out) throws Exception {
                                   long start = context.window().getStart();
                                   long end = context.window().getEnd();
                                   //输出包装信息
                                   out.collect(new UrlViewCount(url, elements.iterator().next(), start, end));
                               }
                           }
                );

        aggregateStream.keyBy(data -> data.end)
                .process(new TopN(2)).print("res");


        env.execute();
    }

    public static class TopN extends KeyedProcessFunction<Long, UrlViewCount, String> {
        //入参
        private Integer n;
        //定义列表状态
        private ListState<UrlViewCount> listState;

        public TopN(Integer n) {
            this.n = n;
        }

        @Override
        public void open(Configuration parameters) throws Exception {
            //获取列表状态
            listState = getRuntimeContext().getListState(
                    new ListStateDescriptor<UrlViewCount>(" url-view-cnt-list", Types.POJO(UrlViewCount.class))
            );
        }

        @Override
        public void processElement(UrlViewCount value, Context ctx, Collector<String> out) throws Exception {
            //将 cnt 数据保存在 列表状态中
            listState.add(value);
            //注册一个window end+1s 定时器,等待所有数据到齐开始排序
            ctx.timerService().registerEventTimeTimer(ctx.timestamp() + 1);
        }

        @Override
        public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {

            //从列表状态中获取数据
            ArrayList<UrlViewCount> arrayList = new ArrayList<>();
            for (UrlViewCount urlViewCount : listState.get()) {
                arrayList.add(urlViewCount);
            }
            //清空状态释放资源
            listState.clear();
            //排序
            arrayList.sort(new Comparator<UrlViewCount>() {
                @Override
                public int compare(UrlViewCount o1, UrlViewCount o2) {
                    return o2.cnt.intValue() - o1.cnt.intValue();
                }
            });
            //取前3名,包装输出信息
            StringBuilder res = new StringBuilder();
            res.append("------------------------");
            res.append("窗口结束时间 " + new Timestamp(timestamp - 1) + "\n");
            for (Integer i = 0; i < this.n; i++) {
                UrlViewCount urlViewCount = arrayList.get(i);
                String info = "No. " + (i + 1) + " "
                        + "url: " + urlViewCount.url + " "
                        + "浏览量:" + urlViewCount.cnt + "\n";
                res.append(info);
            }
            res.append("------------------------");
            out.collect(res.toString());
        }
    }
}
posted @ 2022-07-26 15:57  晓枫的春天  阅读(157)  评论(0编辑  收藏  举报