Flink使用二次聚合实现TopN计算-乱序数据

本文介绍了如何使用Flink处理迟到数据,以实现每隔5秒输出最近10分钟内访问量最多的URL。通过设置watermark策略、使用SlidingEventTimeWindows和MapState,确保了迟到数据的正确处理。在KeyedProcessFunction中,使用两个定时器分别在窗口结束1秒后和61001毫秒后触发,以完成排序输出和清理状态。完整代码展示了这一过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、背景说明:

在上篇文章实现了TopN计算,但是碰到迟到数据则会无法在当前窗口计算,需要对其中的键控状态优化

Flink使用二次聚合实现TopN计算

本次需求是对数据进行统计,要求每隔5秒,输出最近10分钟内访问量最多的前N个URL,数据流预览如下(每次一条从端口传入):

208.115.111.72 - - 17/05/2015:10:25:49 +0000 GET /?N=A&page=21   //15:50-25:50窗口数据
208.115.111.72 - - 17/05/2015:10:25:50 +0000 GET /?N=A&page=21
208.115.111.72 - - 17/05/2015:10:25:51 +0000 GET /?N=A&page=21
208.115.111.72 - - 17/05/2015:10:25:52 +0000 GET /?N=A&page=21   //第一次触发计算,15:50-25:50窗口
208.115.111.72 - - 17/05/2015:10:25:47 +0000 GET /?N=A&          //迟到数据,不同url
208.115.111.72 - - 17/05/2015:10:25:53 +0000 GET /?N=A&page=21   //第二次触发计算,15:50-25:50窗口
208.115.111.72 - - 17/05/2015:10:25:46 +0000 GET /?N=A&page=21   //迟到数据
208.115.111.72 - - 17/05/2015:10:25:54 +0000 GET /?N=A&page=21   //第三次触发计算

最后统计输出结果如下(迟到数据均在25:50窗口):

==============2015-05-17 10:25:50.0==============               //第一次触发计算结果
Top1 Url:/?N=A&page=21 Counts:1
==============2015-05-17 10:25:50.0==============

==============2015-05-17 10:25:50.0==============               //第二次触发计算结果
Top1 Url:/?N=A&page=21 Counts:1
Top2 Url:/?N=A& Counts:1
==============2015-05-17 10:25:50.0==============

==============2015-05-17 10:25:50.0==============               //第三次触发计算结果
Top1 Url:/?N=A&page=21 Counts:2
Top2 Url:/?N=A& Counts:1
==============2015-05-17 10:25:50.0==============

二、实现过程

  1. 实现思路:
    ①建立环境,设置并行度及CK。
    ②定义watermark策略及事件时间,获取数据并对应到JavaBean。
    ③第一次聚合,按url分组开窗聚合,使用aggregate算子进行增量计算。
    ④第二次聚合,按窗口聚合,使用MapState存放数据,定义第一个定时器,在watermark达到后1秒触发,对窗口数据排序输出,定义第二个定时器,窗口关闭后才清楚状态。
    ⑤打印结果及执行。

ps:乱序数据不能使用读取本地文本文件的方式测试,文件读取加载比较快,无法观察到迟到数据处理效果,乱序数据的开发测试这里从服务器端口获取数据的方式测试

  1. 代码细节说明:

只针对优化部分代码说明,其他代码可以在顺序数据篇文章查看,这里提取重写KeyedProcessFunction里面方法的部分代码

@Override
public void processElement(UrlCount value, Context ctx, Collector<String> out) throws Exception {
	//状态装入数据
	mapState.put(value.getUrl(), value);
	//定时器,窗口一秒后触发
	ctx.timerService().registerEventTimeTimer(value.getWindowEnd()+1L);
	//再加一个定时器来清除状态用,在窗口关闭后再清除状态,这样延迟数据到达后窗口还能做排序
	ctx.timerService().registerEventTimeTimer(value.getWindowEnd()+61001L);
}
//定时器内容
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
	if (timestamp == ctx.getCurrentKey()+61001L){
		mapState.clear();
		return;}
...
  • 这里改用MapState,如若使用ListState,进来迟到数据后,则会出现同个url在同个窗口的统计出现多个计数的情况,列表状态不具备去重功能,故在这里使用map状态来实现去重。
  • 这里使用定时器来清除状态,原写法是在onTimer最后排序完直接清除状态,则会导致迟到数据到达后,原窗口其他数据被清除掉无法实现排名的输出,这里定时器的时间是在61001毫秒后清除状态数据。
  • 定时器61001毫秒 = 允许迟到数据1秒(forBoundedOutOfOrderness)+窗口迟到数据1分钟(allowedLateness)+第一个定时器1毫秒。

三、完整代码

package com.test.topN;

import bean.ApacheLog;
import bean.UrlCount;
import org.apache.commons.compress.utils.Lists;
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.functions.MapFunction;
import org.apache.flink.api.common.state.MapState;
import org.apache.flink.api.common.state.MapStateDescriptor;
import org.apache.flink.api.java.tuple.Tuple2;
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.WindowFunction;
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 java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
/**
 * @author: Rango
 * @create: 2021-05-26 10:16
 * @description: 每隔5秒,输出最近10分钟内访问量最多的前N个URL
 **/
public class URLTopN3 {
    public static void main(String[] args) throws Exception {

        //1.建立环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment().setParallelism(1);

        //2.读取端口数据并映射到JavaBean,并定义watermark时间语义
        WatermarkStrategy<ApacheLog> wms = WatermarkStrategy
                .<ApacheLog>forBoundedOutOfOrderness(Duration.ofSeconds(1))
                .withTimestampAssigner(new SerializableTimestampAssigner<ApacheLog>() {
                    @Override
                    public long extractTimestamp(ApacheLog element, long recordTimestamp) {
                        return element.getTs();
                    }});

        SingleOutputStreamOperator<ApacheLog> apacheLogDS = env.socketTextStream("hadoop102", 9999)
                .map(new MapFunction<String, ApacheLog>() {
                    @Override
                    public ApacheLog map(String value) throws Exception {
                        SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yy:HH:mm:ss");
                        String[] split = value.split(" ");
                        return new ApacheLog(split[0],
                                split[2],
                                sdf.parse(split[3]).getTime(),
                                split[5],
                                split[6]);
                    }})
                .assignTimestampsAndWatermarks(wms);

        //3.第一次聚合,按url转为tuple2分组,开窗,增量聚合
        SingleOutputStreamOperator<UrlCount> aggregateDS = apacheLogDS
                .map(new MapFunction<ApacheLog, Tuple2<String, Integer>>() {
            @Override
            public Tuple2<String, Integer> map(ApacheLog value) throws Exception {
                return new Tuple2<>(value.getUrl(), 1);
            }}).keyBy(data -> data.f0)
                .window(SlidingEventTimeWindows.of(Time.minutes(10),Time.seconds(5)))
                .allowedLateness(Time.minutes(1))
                .aggregate(new HotUrlAggFunc(), new HotUrlWindowFunc());

        //4.第二次聚合,对第一次聚合输出按窗口分组,再全窗口聚合,建立定时器你,每5秒钟触发一次
        SingleOutputStreamOperator<String> processDS = aggregateDS
                .keyBy(data -> data.getWindowEnd())
                .process(new HotUrlProcessFunc(5));

        processDS.print();
        env.execute();
    }
    //实现AggregateFunction类中的方法
    public static class HotUrlAggFunc implements AggregateFunction<Tuple2<String, Integer>,Integer,Integer>{
        @Override
        public Integer createAccumulator() {return 0;}
        @Override
        public Integer add(Tuple2<String, Integer> value, Integer accumulator) { return accumulator+1;}
        @Override
        public Integer getResult(Integer accumulator) {return accumulator;}
        @Override
        public Integer merge(Integer a, Integer b) {return a+b; }
    }
    //实现窗口函数的apply方法,把累加函数输出的整数结果,转换为javabean类urlcount来做输出,方便后续按窗口聚合
    public static class HotUrlWindowFunc implements WindowFunction<Integer, UrlCount,String, TimeWindow> {
        @Override
        public void apply(String urls, TimeWindow window, Iterable<Integer> input, Collector<UrlCount> out) throws Exception {
            //获取按key相加后的次数并新建javabean(urlcount)作为返回
            Integer count = input.iterator().next();
            out.collect(new UrlCount(urls,window.getEnd(),count));
        }
    }
    //继承KeyedProcessFunction方法,重写processElemnt方法
    public static class HotUrlProcessFunc extends KeyedProcessFunction<Long,UrlCount,String>{
        //定义TopN为入参
        private Integer TopN;
        public HotUrlProcessFunc(Integer topN) {
            TopN = topN;
        }
        //定义状态
        private MapState <String,UrlCount>mapState;
        //open方法中初始化状态
        @Override
        public void open(Configuration parameters) throws Exception {
            mapState = getRuntimeContext()
                    .getMapState(new MapStateDescriptor<String, UrlCount>("map-state",String.class,UrlCount.class));
        }
        @Override
        public void processElement(UrlCount value, Context ctx, Collector<String> out) throws Exception {
            //状态装入数据
            mapState.put(value.getUrl(), value);
            //定时器,窗口一秒后触发
            ctx.timerService().registerEventTimeTimer(value.getWindowEnd()+1L);
            //再加一个定时器来清除状态用,在窗口关闭后再清除状态,这样延迟数据到达后窗口还能做排序
            ctx.timerService().registerEventTimeTimer(value.getWindowEnd()+61001L);
        }
        //定时器内容
        @Override
        public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
            if (timestamp == ctx.getCurrentKey()+61001L){
                mapState.clear();
                return;}

            //取出状态数据
            Iterator<Map.Entry<String, UrlCount>> iterator = mapState.iterator();
            ArrayList<Map.Entry<String, UrlCount>> entries = Lists.newArrayList(iterator);

            //排序
            entries.sort(((o1, o2) -> o2.getValue().getCount()-o1.getValue().getCount()));

            //排序后装入StringBulider作为输出TopN
            StringBuilder sb = new StringBuilder();
            sb.append("==============")
                    .append(new Timestamp(timestamp - 1L))
                    .append("==============")
                    .append("\n");
            for (int i = 0; i < Math.min(TopN,entries.size()); i++) {
                UrlCount urlCount = entries.get(i).getValue();
                sb.append("Top").append(i+1);
                sb.append(" Url:").append(urlCount.getUrl());
                sb.append(" Counts:").append(urlCount.getCount());
                sb.append("\n");
            }
            sb.append("==============")
                    .append(new Timestamp(timestamp - 1L))
                    .append("==============")
                    .append("\n")
                    .append("\n");

            out.collect(sb.toString());
            Thread.sleep(200);
            }}}
           

映射数据源的JavaBean

package bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApacheLog {
    private String ip;
    private String userId;
    private Long ts;
    private String method;
    private String url;
}

第一次聚合输出的JavaBean

package bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UrlCount {
    private String url;
    private Long windowEnd;
    private Integer count;
}

学习交流,有任何问题还请随时评论指出交流。

<think>好的,我现在需要帮助用户了解Flink的时间窗口开窗。首先,用户之前问过如何设置队列最大长度,现在转向了Flink的时间窗口,可能是在处理实时数据流时需要窗口功能,比如统计某段时间的数据。我需要先确认用户的使用场景,可能是实时数据处理或流计算,但用户没有详细说明,所以需要覆盖基础概念和常见窗口类型。 接下来,用户可能对时间窗口的基本概念不太熟悉,所以需要从基础讲起,比如时间窗口的作用和类型。然后,用户可能想知道如何具体在Flink实现这些窗口,包括代码示例。需要分清楚处理时间、事件时间和摄入时间的区别,因为这是Flink中时间语义的重要部分。 用户可能还需要知道不同的窗口类型,比如滚动窗口、滑动窗口和会话窗口,以及它们的应用场景。需要提供每种窗口的定义、特点和使用方法,最好有代码片段帮助理解。此外,用户可能对窗口的生命周期和触发器感兴趣,但根据问题,可能暂时不需要深入这部分,但可以简要提及。 另外,用户之前的问题涉及队列长度设置,可能现在在处理流数据时遇到了窗口相关的性能或配置问题,所以需要强调窗口配置的注意事项,比如时间语义的选择、延迟数据处理和水位线生成。这部分内容能帮助用户正确配置窗口,避免常见错误。 需要检查是否有必要对比不同窗口类型的适用场景,帮助用户根据需求选择合适的窗口。同时,生产环境的建议,比如水位线策略和延迟数据处理,对用户的实际应用可能有很大帮助,需要详细说明。 最后,总结部分要简明扼要,强调重点,让用户能快速回顾核心内容。可能还需要提醒用户根据具体业务需求调整窗口参数,并考虑性能优化。要确保回答结构清晰,步骤明确,覆盖用户可能的需求点,同时保持语言简洁易懂。</think>以下是关于Flink时间窗口开窗的详细技术解析,包含核心概念、窗口类型及实战示例: --- ### 一、时间窗口核心原理 Flink通过**时间窗口(Time Windows)**将无界数据流划分为有限的数据块进行处理。其核心要素包含: 1. **时间语义**: - $处理时间(Processing Time)$:以系统时钟为基准(最快但不可靠) - $事件时间(Event Time)$:以数据自带时间戳为基准(需要水位线机制) - $摄入时间(Ingestion Time)$:以数据进入Flink的时间为基准 2. **窗口生命周期**: ```mermaid graph LR 创建窗口-->数据分配 数据分配-->触发计算 触发计算-->窗口销毁 ``` --- ### 二、基础窗口类型详解 #### 1. 滚动窗口(Tumbling Window) $$ \text{窗口长度}=T,\ \text{窗口间无重叠} $$ ```java // 事件时间滚动窗口(5分钟) DataStream<T> input = ...; input .keyBy(<key selector>) .window(TumblingEventTimeWindows.of(Time.minutes(5))) .reduce(<ReduceFunction>); // 处理时间滚动窗口(10秒) .window(TumblingProcessingTimeWindows.of(Time.seconds(10))) ``` #### 2. 滑动窗口(Sliding Window) $$ \text{窗口长度}=T,\ \text{滑动步长}=S\ (S < T) $$ ```java // 事件时间滑动窗口(1小时窗口,15分钟滑动) .window(SlidingEventTimeWindows.of(Time.hours(1), Time.minutes(15))) // 带偏移量的滑动窗口(解决时区问题) .of(Time.hours(24), Time.hours(1), Time.hours(-8)) // UTC+8时区对齐 ``` #### 3. 会话窗口(Session Window) $$ \text{动态窗口,通过间隙(gap)分割} $$ ```java // 事件时间会话窗口(10分钟不活动则关闭) .window(EventTimeSessionWindows.withGap(Time.minutes(10))) // 动态间隙配置(根据数据特征调整) .withDynamicGap((element) -> { // 根据数据内容返回不同gap时长 return determineGap(element); }); ``` --- ### 三、高级窗口操作 #### 1. 全局窗口 + 触发器 ```java // 自定义每100个元素触发计算 .window(GlobalWindows.create()) .trigger(CountTrigger.of(100)) ``` #### 2. 延迟数据处理(Allowed Lateness) ```java .window(...) .allowedLateness(Time.minutes(5)) // 允许迟到5分钟 .sideOutputLateData(lateOutputTag) // 侧输出迟到数据 ``` #### 3. 窗口函数对比 | 函数类型 | 特点 | 适用场景 | |------------------|-----------------------------|----------------------| | ReduceFunction | 增量计算,输入输出类型一致 | 简单聚合(sum/max) | | AggregateFunction| 中间状态可维护复杂聚合 | 平均值、复杂统计 | | ProcessWindowFunction | 全量访问窗口数据 | 排序TopN操作 | --- ### 四、水位线(Watermark)关键配置 ```java // 有序事件水位线(无乱序) .assignTimestampsAndWatermarks( WatermarkStrategy .<Event>forMonotonousTimestamps() .withTimestampAssigner((event, ts) -> event.timestamp) ); // 乱序事件水位线(最大允许延迟2秒) .assignTimestampsAndWatermarks( WatermarkStrategy .<Event>forBoundedOutOfOrderness(Duration.ofSeconds(2)) .withTimestampAssigner(...) ); ``` --- ### 五、生产环境最佳实践 1. **窗口长度选择公式**: $$ \text{窗口长度} = \frac{\text{业务周期}}{\text{精度要求系数}} $$ 示例:每分钟统计的日粒度数据: $$ \frac{24小时}{1440} = 1分钟 $$ 2. **内存控制**: ```java // 设置状态后端及TTL env.setStateBackend(new RocksDBStateBackend(...)); env.getConfig().setAutoWatermarkInterval(1000); // 1秒生成一次水位线 ``` 3. **性能调优参数**: ```shell # 调整Flink配置 taskmanager.memory.task.heap.size: 4096m taskmanager.numberOfTaskSlots: 4 parallelism.default: 8 ``` --- ### 六、典型问题排查指南 1. **窗口不触发**: - 检查水位线生成是否正常 - 验证事件时间是否远小于系统时间 - 使用`debug`模式输出水位线日志 2. **结果数据缺失**: ```java // 添加迟到数据侧输出 OutputTag<Event> lateDataTag = new OutputTag<>("late-data"){}; .sideOutputLateData(lateDataTag) ``` --- 通过合理选择窗口类型与参数配置,可以实现从秒级实时监控到天级离线统计的全场景覆盖。建议结合Flink Web UI的窗口监控指标进行动态调优。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值