Flink窗口机制详解

Flink的窗口机制

6.1.1 窗口概述

窗口window是用来处理无限数据集的有限块。窗口就是把流切成了有限大小的多个存储桶bucket

image-20210124161211304

流处理应用中,数据是连续不断的,因此我们不能等所有的数据来了才开始处理,当然也可以来一条数据,处理一条数据,但是有时候我们需要做一些聚合类的处理,例如:在过去的一分钟内有多少用户点击了网页。这种情况下,就适合定义一个窗口,用来收集最近一分钟内的数据,并对这个窗口的数据进行计算。

6.1.2 窗口分类

基于时间的窗口(时间驱动)

基于元素的窗口(数据驱动)

keyBy之前的窗口函数:sensorDS.windowAll(),并行度只能是1

keyBy之后的窗口函数:sensorDS.window() ★

老版本写法:1.12标记为过时,不建议使用

sensorKS.timeWindow(Time.seconds(3)); // 一个参数,表示滚动窗口
sensorKS.timeWindow(Time.seconds(5),Time.seconds(2)); // 两个参数,表示滑动窗口

新版本写法:

//一个参数,表示滚动窗口
sensorKS.window(TumblingProcessingTimeWindows.of(Time.seconds(3)));
//两个参数,表示滑动窗口
sensorKS.window(SlidingProcessingTimeWindows.of(Time.seconds(5),Time.seconds(2)));
(1)基于时间的窗口
①滚动时间窗口(Tumbling Windows)

keyBy()之前的窗口函数、keyBy()之后的窗口函数

滚动时间窗口,有固定的窗口时间大小。比如一个10s的滚动窗口,从当前窗口开始算,每10s启动一个新的窗口。

window()传入的对象叫窗口分配器。

public class Flink01_TumblingWindows {
    public static void main(String[] args) throws Exception {
        //1 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(2);

        //2 加载数据
        SingleOutputStreamOperator<WaterSensor> sensorDS = env
                .socketTextStream("hadoop102", 9999)
                .map(new MapFunction<String, WaterSensor>() {
                    @Override
                    public WaterSensor map(String value) throws Exception {
                        String[] dats = value.split(",");
                        return new WaterSensor(
                                dats[0],
                                Long.valueOf(dats[1]),
                                Integer.valueOf(dats[2])
                        );
                    }
                });
        //TODO 3 时间窗口 - 滚动窗口TumblingWindows
        //keyBy之前开窗,api都带有 'all',所有数据进入到一个窗口(并且并行度为1)
//        AllWindowedStream<WaterSensor, TimeWindow> sensorWS = sensorDS.windowAll(TumblingProcessingTimeWindows.of(Time.seconds(10)));

        //TODO 时间窗口 - 一般keyBy之后使用 => KeyedStream.window(窗口类型.of(时间参数))
        KeyedStream<WaterSensor, String> sensorKS = sensorDS.keyBy(sensor -> sensor.getId());

        WindowedStream<WaterSensor, String, TimeWindow> sensorWS = sensorKS.window(TumblingProcessingTimeWindows.of(Time.seconds(3)));


        //TODO 4 开窗后,数据的处理
        //keyBy之后开窗,对数据的处理
        sensorWS
                .process(new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
                    @Override
                    public void process(String s, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
                        long start = context.window().getStart();
                        long end = context.window().getEnd();
                        out.collect("==============================================\n" +
                                "窗口为:[" + start + "," + end + ")\n" +
//                                "数据:" + elements + "\n" +
                                "数据条数=" + elements.spliterator().estimateSize() + "\n" +
                                "=======================================\n\n");
                    }
                })
                .print();
    
        //keyBy之前开窗,对数据的处理process(new ProcessAllWindowFunction)
//        sensorWS
//                .process(new ProcessAllWindowFunction<WaterSensor, String, TimeWindow>() {
//                    @Override
//                    public void process(Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
//                        long start = context.window().getStart();
//                        long end = context.window().getEnd();
//                        out.collect("==============================================\n" +
//                                "窗口为:[" + start + "," + end + ")\n" +
                                "数据:" + elements + "\n" +
//                                "数据条数=" + elements.spliterator().estimateSize() + "\n" +
//                                "=======================================\n\n");
//
//                    }
//                })
//                .print();
        env.execute();
    }
}
②滑动时间窗口(Sliding Windows)

SlidingProcessingTimeWindows.of(参数1,参数2),参数1是窗口时间长度,参数2是滑动步长,(参数3是步幅)

滑动窗口,也有一个固定的窗口大小,另外还有一个滑动步长。

  • 当滑动步长 = 窗口大小的时候,和滚动窗口就一样了

  • 当滑动步长 < 窗口大小的时候,窗口会重叠,这种情况会出现数据分配到多个窗口的情况了

    ​ 例如:滑动窗口10s,滑动步长5s

    image-20210125213216791

public class Flink02_SlidingWindows {
    public static void main(String[] args) throws Exception {
        //1 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(2);

        //2 加载数据
        SingleOutputStreamOperator<WaterSensor> sensorDS = env
                .socketTextStream("hadoop102", 9999)
                .map(new MapFunction<String, WaterSensor>() {
                    @Override
                    public WaterSensor map(String value) throws Exception {
                        String[] dats = value.split(",");
                        return new WaterSensor(
                                dats[0],
                                Long.valueOf(dats[1]),
                                Integer.valueOf(dats[2])
                        );
                    }
                });
        KeyedStream<WaterSensor, String> sensorKS = sensorDS.keyBy(sensor -> sensor.getId());

        //TODO 滑动时间窗口
        WindowedStream<WaterSensor, String, TimeWindow> sensorWS = sensorKS.window(SlidingProcessingTimeWindows.of(Time.seconds(5), Time.seconds(3)));


        sensorWS
                .process(new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
            @Override
            public void process(String s, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
                long start = context.window().getStart();
                long end = context.window().getEnd();
                out.collect("==============================================\n" +
                        "窗口为:[" + start + "," + end + ")\n" +
//                                "数据:" + elements + "\n" +
                        "数据条数=" + elements.spliterator().estimateSize() + "\n" +
                        "=======================================\n\n");
            }
        })
                .print();
        env.execute();
    }
}
③会话窗口(Session Windows)

会话窗口:定义一个时间间隔Gap,如果达到时间间隔,没有新的数据来的时候,那么之前的数据就会划分为一个窗口。

  1. 静态Gap:时间间隔 不变

    比如Gap=10,只要10s没有接收到数据,那么就关闭这个窗口,有新的数据来,会再开启一个新的窗口

    只要10s内有新的数据来,那么这个10s会被刷新。

  2. 动态Gap:时间间隔 动态改变

    Gap是一个动态的值,也是达到这个动态的值,那么就关闭窗口,有新的数据来,会再开启一个新窗口

    只要动态值内 有新的数据来,那么这个动态的值会被刷新。

会话窗口没有固定的开启和关闭时间。在Flink内部, 每到达一个新的元素都会创建一个新的会话窗口, 如果这些窗口彼此相距比较定义的gap小, 则会对他们进行合并. 为了能够合并, 会话窗口算子需要合并触发器和合并窗口函数: ReduceFunction, AggregateFunction, or ProcessWindowFunction

静态Gap

public class Flink03_SessionWindows {
    public static void main(String[] args) throws Exception {
        //1 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(3);

        //2 读取数据
        SingleOutputStreamOperator<WaterSensor> sensorDS = env
                .socketTextStream("hadoop102", 9999)
                .map(new MapFunction<String, WaterSensor>() {
                    @Override
                    public WaterSensor map(String value) throws Exception {
                        String[] dats = value.split(",");
                        return new WaterSensor(
                                dats[0],
                                Long.valueOf(dats[1]),
                                Integer.valueOf(dats[2])
                        );
                    }
                });
        KeyedStream<WaterSensor, String> sensorKS = sensorDS.keyBy(sensor -> sensor.getId());

        //3 TODO 会话窗口 - SessionWindow 静态Gap,固定会话时间间隔
        WindowedStream<WaterSensor, String, TimeWindow> sensorWS = sensorKS.window(ProcessingTimeSessionWindows.withGap(Time.seconds(10)));


        //4 TODO 开窗后对数据的处理
        sensorWS
                .process(new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
            @Override
            public void process(String s, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
                long start = context.window().getStart();
                long end = context.window().getEnd();
                out.collect("==============================================\n" +
                        "窗口为:[" + start + "," + end + ")\n" +
//                                "数据:" + elements + "\n" +
                        "数据条数=" + elements.spliterator().estimateSize() + "\n" +
                        "=======================================\n\n");
            }
        })
                .print();
        env.execute();
    }
}

动态Gap

public class Flink04_SessionWindows {
    public static void main(String[] args) throws Exception {
        //1 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(3);

        //2 读取数据
        SingleOutputStreamOperator<WaterSensor> sensorDS = env
                .socketTextStream("hadoop102", 9999)
                .map(new MapFunction<String, WaterSensor>() {
                    @Override
                    public WaterSensor map(String value) throws Exception {
                        String[] dats = value.split(",");
                        return new WaterSensor(
                                dats[0],
                                Long.valueOf(dats[1]),
                                Integer.valueOf(dats[2])
                        );
                    }
                });
        KeyedStream<WaterSensor, String> sensorKS = sensorDS.keyBy(sensor -> sensor.getId());

        //TODO 3 时间窗口-SessionWindows - 动态Gap,动态会话间隔
        WindowedStream<WaterSensor, String, TimeWindow> sensorWS = sensorKS.window(ProcessingTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor<WaterSensor>() {
            @Override
            public long extract(WaterSensor element) {
                //TODO 动态:根据watersensor的ts字段去动态调整会话时间间隔
                return element.getTs() * 1000L;
            }
        }));

        //4 开窗后 对数据的处理
        sensorWS
                .process(new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
                    @Override
                    public void process(String s, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
                        long start = context.window().getStart();
                        long end = context.window().getEnd();
                        out.collect("==============================================\n" +
                                "窗口为:[" + start + "," + end + ")\n" +
//                                "数据:" + elements + "\n" +
                                "数据条数=" + elements.spliterator().estimateSize() + "\n" +
                                "=======================================\n\n");
                    }
                })
                .print();
        env.execute();
    }
}
④全局窗口(Global Windows)

分配相同key的所有元素进入到同一个Global Window(全局窗口)中,那么怎么判断窗口结束了?怎样重新启动一个新的窗口呢?

所以全局窗口需要自定义触发器

其实上面的TrumblingWindows、SlidingWindows、SessionWindows都有自己的触发器。

image-20210125221036714

//TODO 全局窗口
 sensorKS
     .window(GlobalWindows.create())
	 .trigger(Trigger类型)
(2)基于元素个数的窗口

按照指定的数据条数生成window,与时间无关。

①滚动窗口

countWindow(参数1)

滚动计数窗口,参数1是窗口每到达指定条数据,就会关闭这个窗口,开启一个新的窗口

②滑动窗口

countWindow(参数1,参数2)

滑动计数窗口,参数1是窗口大小,参数2是滑动步长,单位是数据的条数。

public class Flink06_CountWindow {
    public static void main(String[] args) throws Exception {
        //1 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(3);

        //2 读取数据
        SingleOutputStreamOperator<WaterSensor> sensorDS = env
                .socketTextStream("hadoop102", 9999)
                .map(new MapFunction<String, WaterSensor>() {
                    @Override
                    public WaterSensor map(String value) throws Exception {
                        String[] dats = value.split(",");
                        return new WaterSensor(
                                dats[0],
                                Long.valueOf(dats[1]),
                                Integer.valueOf(dats[2])
                        );
                    }
                });
        KeyedStream<WaterSensor, String> sensorKS = sensorDS.keyBy(sensor -> sensor.getId());

        //TODO 计数窗口
        //    窗口并不是以第一条数据为起始点的,而是有自己的划分逻辑
        //    窗口的触发,依赖于滑动步长,每经过一个滑动步长,都有一个窗口关闭并输出
        sensorKS
//                .countWindow(5)     //滚动窗口
                .countWindow(5, 2)      //滑动窗口
                .sum("vc")
                .print();

        env.execute();
    }
}
(3)Window Function

image-20210125230749677

窗口函数就是对一个窗口内的数据的操作。

分为聚合函数:reduceFunction和AggregateFunction,来一条聚合一条,只在窗口关闭时才会输出

​ 全窗口函数:ProcessWindowFunction,来一条保存一条,只有在窗口关闭的时候才聚合,输出结果

①ReduceFunction

注意:这里的reduce()是WindowedStream的;前面的reduce()是键控流KeyedStream的

public class Flink07_WindowFunction_ReduceFunction {
    public static void main(String[] args) throws Exception {
        //1 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(3);

        //2 读取数据
        SingleOutputStreamOperator<WaterSensor> sensorDS = env
                .socketTextStream("hadoop102", 9999)
                .map(new MapFunction<String, WaterSensor>() {
                    @Override
                    public WaterSensor map(String value) throws Exception {
                        String[] dats = value.split(",");
                        return new WaterSensor(
                                dats[0],
                                Long.valueOf(dats[1]),
                                Integer.valueOf(dats[2])
                        );
                    }
                });
        KeyedStream<WaterSensor, String> sensorKS = sensorDS.keyBy(sensor -> sensor.getId());

        //3 开启滚动时间窗口
        WindowedStream<WaterSensor, String, TimeWindow> sensorWS = sensorKS.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));

        //4 TODO 增量函数ReduceFunction
        SingleOutputStreamOperator<WaterSensor> resultDS = sensorWS.reduce(new ReduceFunction<WaterSensor>() {
            @Override
            public WaterSensor reduce(WaterSensor value1, WaterSensor value2) throws Exception {
                System.out.println(value1 + " <=========> " + value2);
                return new WaterSensor(value1.getId(), 1L, value1.getVc() + value2.getVc());
            }
        });

        //5 打印结果
        resultDS.print();

        env.execute();
    }
}
②AggregateFunction

AggregateFunction比ReduceFunction更加灵活。因为reduceFunction的输入和输出类型必须是一样的。

  • AggregateFunction<泛型1,泛型2,泛型3>,泛型1是输入类型,泛型2是累加器结果类型,泛型3是输出类型。

  • 需要重写四个方法:累加器的初始化、聚合逻辑、获取结果、会话窗口的合并窗口。

public class Flink08_WindowFunction_AggregateFunction {
    public static void main(String[] args) throws Exception {
        //1 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(3);

        //2 读取数据
        SingleOutputStreamOperator<WaterSensor> sensorDS = env
                .socketTextStream("hadoop102", 9999)
                .map(new MapFunction<String, WaterSensor>() {
                    @Override
                    public WaterSensor map(String value) throws Exception {
                        String[] dats = value.split(",");
                        return new WaterSensor(
                                dats[0],
                                Long.valueOf(dats[1]),
                                Integer.valueOf(dats[2])
                        );
                    }
                });
        KeyedStream<WaterSensor, String> sensorKS = sensorDS.keyBy(sensor -> sensor.getId());

        //3 开启滚动时间窗口
        WindowedStream<WaterSensor, String, TimeWindow> sensorWS = sensorKS.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));

        //TODO 4 增量函数AggregateFunction
        sensorWS.aggregate(new AggregateFunction<WaterSensor, Integer, String>() {
            @Override
            public Integer createAccumulator() {
                //累加器的初始化
                return 0;
            }

            @Override
            public Integer add(WaterSensor value, Integer accumulator) {
                //聚合逻辑
                return accumulator + 1; //每来一条数据就+1
            }

            @Override
            public String getResult(Integer accumulator) {
                //获取结果
                return accumulator.toString();
            }

            @Override
            public Integer merge(Integer a, Integer b) {
                //合并窗口,注意,只有会话窗口才会调用
                return a + b;
            }
        })
                .print();

        env.execute();
    }
}
③ProcessWindowFunction

全窗口函数-process(new ProcessWindowFunction)

public class Flink09_WindowFunction_ProcessFunction {
    public static void main(String[] args) throws Exception {
        //1 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(3);

        //2 读取数据
        SingleOutputStreamOperator<WaterSensor> sensorDS = env
                .socketTextStream("hadoop102", 9999)
                .map(new MapFunction<String, WaterSensor>() {
                    @Override
                    public WaterSensor map(String value) throws Exception {
                        String[] dats = value.split(",");
                        return new WaterSensor(
                                dats[0],
                                Long.valueOf(dats[1]),
                                Integer.valueOf(dats[2])
                        );
                    }
                });
        KeyedStream<WaterSensor, String> sensorKS = sensorDS.keyBy(sensor -> sensor.getId());

        //3 开启滚动时间窗口
        WindowedStream<WaterSensor, String, TimeWindow> sensorWS = sensorKS.window(TumblingProcessingTimeWindows.of(Time.seconds(10)));

        //TODO 4 全窗口函数
        //泛型1输入类型,泛型2输出类型,泛型3分组key的类型,泛型4窗口类型
        sensorWS.process(new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
            @Override
            public void process(String s, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
                //参数1分组key,参数2上下文,参数3数据(可迭代类型,保存多个数据),参数4采集器
                out.collect("key=" + s + "\n" +
                                "数据为:" + elements + "\n" +
                                "数据条数:" + elements.spliterator().estimateSize() + "\n" +
                                "窗口为:【" + context.window().getStart() + ", " + context.window().getEnd() + "】\n" +
                                "=======================================================\n\n");
            }
        })
                .print();

        env.execute();
    }
}
6.1.3 窗口源码分析

以 事件时间的 滚动窗口为例

(1)窗口怎样划分?开始时间、结束时间?
        1)开始时间 = timestamp - (timestamp - offset + windowSize) % windowSize
                    对 窗口长度 取整数倍(以1970110点 为基准 => 因为是时间戳)
                    并不是以第一条数据 作为窗口的 起始点
        2) 结束时间 = 开始时间 + 窗口长度

        比如:
        事件时间=1s,窗口大小为10s,那么窗口就是[0-10)
        事件时间=11s,窗口大小为10s,那么窗口就是[10-21)
(2)窗口为什么是左闭右开?
            属于窗口的最大时间戳: maxTimestamp = end - 1ms
                => [0,10) => 属于本窗口的最大时间戳为, 10s -1ms = 9999ms
                    => 所以10s这条数据,不属于本窗口,所以是开区间
(3)窗口什么时候触发计算?
--事件时间触发器:EventTimeTrigger
watermark >= 窗口的最大时间戳	就fire触发计算

--窗口触发分析:
window.maxTimestamp() <= ctx.getCurrentWatermark()
	watermark >= 窗口的最大时间戳 \
		=> 比如,[0,10)的窗口,当 watermark >= 10s - 1ms = 9999ms时触发
		=> 此时,watermark = 事件时间 - 等待3s - 1ms => 事件时间 = 13s时触发
(4)窗口的生命周期:什么时候创建的窗口?
    4、窗口的生命周期: 什么时候创建的窗口?
        1)属于本窗口的第一条数据来的时候,new出来的
        2)属于本窗口的每条数据来的时候,都会new,但是集合是 singleton类型,不会重复存放 => 也就是说,只会有一个窗口对象
        
--详细解释:
         return Collections.singletonList(new TimeWindow(start, start + size));
                     => 每来一条数据,都会 new一个窗口对象
                     => 将窗口对象,放入一个 SingletonList,是单例的,所以不会重复
                     => 窗口结束时间 end = 计算出来的 start + 窗口长度
(5)窗口的生命周期:什么时候关闭窗口?

窗口结束时间 + 等待时间

    5、窗口的生命周期: 什么时候关闭窗口?
        窗口关闭时间 = 窗口最大时间戳 + allowedLateness
        注意:如果 allowedLateness = 0(不设置),那么触发和关窗的时间一样
(6)详细分析过程
详细分析过程:
    WindowOperator
        => assignWindows()
            long start = TimeWindow.getWindowStartWithOffset(timestamp, (globalOffset + staggerOffset) % size, size);
                        => timestamp - (timestamp - offset + windowSize) % windowSize
                         => 1 - (1 -0 + 10) % 10  =>  start = 0s
                         => 7 - (7 -0 + 10) % 10  =>  start = 0s
                         => 12 - (12 - 0 + 10) % 10 => start = 10s
                         => 这个算法,相当于,对 窗口长度 取整数倍(以1970年1月1日0点 为基准 => 因为是时间戳)

                         => 并不是以第一条数据 作为窗口的 起始点

         return Collections.singletonList(new TimeWindow(start, start + size));
                     => 每来一条数据,都会 new一个窗口对象
                     => 将窗口对象,放入一个 SingletonList,是单例的,所以不会重复
                     => 窗口结束时间 end = 计算出来的 start + 窗口长度

            // Gets the largest timestamp that still belongs to this window.
            // 获取属于本窗口的 最大时间戳
         public long maxTimestamp() {
                  return end - 1;
           }
                => [0,10) => 属于本窗口的最大时间戳为, 10s -1ms = 9999ms => 所以10s这条数据,不属于本窗口,所以是开区间

        =>     long cleanupTime = window.maxTimestamp() + allowedLateness;
         return cleanupTime >= window.maxTimestamp() ? cleanupTime : Long.MAX_VALUE;
         =》 窗口关闭时间 = 窗口最大时间戳 + allowedLateness

      窗口触发分析:

           window.maxTimestamp() <= ctx.getCurrentWatermark()
            watermark >= 窗口的最大时间戳 \java
                => 比如,[0,10)的窗口,当 watermark >= 10s - 1ms = 9999ms时触发
                => 此时,watermark = 事件时间 - 等待3s - 1ms => 事件时间 = 13s时触发
 */

6.2 Keyed & None-Keyed Windows

在用window前首先需要确认应该是在keyBy前的流上用,还是在keyBy后的流上用。

  • 在keyBy前的流上用:流的并行度只能是1,所有的窗口逻辑只能在一个task上执行。

  • 在keyBy后的流上用:窗口计算并行的运用在多个task上,每个分组都有自己的单独窗口

    image-20210126001724262

  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

最佳第六六六人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值