flink学习日记3-window

23 阅读7分钟

窗口

  • 在数据流中,数据是连续、无界的,所以无法对一个没有终点的数据流进行全局的聚合计算
  • 需要将无界流切割成一个个有界数据块,再进行计算,以实现类似批处理的聚合操作

分类

时间/数量

  • 时间窗口:以时间为单位来划分窗口,如每分钟计算一次
  • 计数窗口:按数据元素的数量来划分窗口,如每100条数据计算一次

触发形态

  • 滚动窗口:固定大小,不重叠,无间隔,每个元素只属于一个窗口
  • 滑动窗口:固定大小,可以重叠,有滑动间隔
  • 会话窗口:大小不确定,通过活动间隙来划分,不重叠
  • 全局窗口:把所有相同keyed数据划分到一个全局唯一的窗口中,默认永远不会触发,除非编写自定义触发器

窗口函数

Reduce

  • 每来一条数据就更新一次结果
  • 输入和输出的数据类型必须一致
  • 第一条数据不会触发ReduceFunction,只会被缓存
  • reduce(WaterSensor t1, WaterSensor t2)其中t1是缓存数据
windowed.reduce(new ReduceFunction<WaterSensor>() {
    @Override
    public WaterSensor reduce(WaterSensor t1, WaterSensor t2) throws Exception {
        System.out.println("reducing:"+t1);
        return new WaterSensor(t1.getId(), t1.getTs(), t1.getVal() + t2.getVal());
    }
}).print();

Aggregate

  • 比Reduce更灵活:可以自定义累加器类型和输出类型
  • merge方法支持多字段聚合
windowed.aggregate(new AggregateFunction<WaterSensor, Integer, Integer>() {
    @Override
    public Integer createAccumulator() {
        return 0;
    }
​
    @Override
    public Integer add(WaterSensor waterSensor, Integer integer) {
        return integer+waterSensor.getVal();
    }
​
    @Override
    public Integer getResult(Integer integer) {
        return integer;
    }
​
    @Override
    public Integer merge(Integer integer, Integer acc1) {
        return acc1+integer;
    }
}).print();

全窗口函数

  • 功能最强:可以访问窗口全部数据、状态、上下文;获取窗口元信息;输出多条结果
  • 性能最低:必须缓存窗口中的所有数据
windowed.process(new ProcessWindowFunction<WaterSensor, Tuple3<String,String,Integer>, String, TimeWindow>() {
​
    @Override
    public void process(String s, ProcessWindowFunction<WaterSensor, Tuple3<String, String, Integer>, String, TimeWindow>.Context context, Iterable<WaterSensor> iterable, Collector<Tuple3<String, String, Integer>> collector) throws Exception {
        long start = context.window().getStart();
        long end = context.window().getEnd();
        String windowStart = DateFormatUtils.format(start, "yyyy-MM-dd-HH:mm:ss");
        String windowEnd = DateFormatUtils.format(end, "yyyy-MM-dd-HH:mm:ss");
        Integer maxn=0;
        for (WaterSensor waterSensor : iterable) {
            maxn=max(maxn,waterSensor.getVal());
        }
        collector.collect(Tuple3.of(windowStart,windowEnd,maxn));
    }
}).print();

Aggregate / Reduce + ProcessWindowFunction组合

  • ProcessWindowFunction的iterable中存储的是一条最终的聚合数据
windowed.aggregate(new AggregateFunction<WaterSensor, Integer, Integer>() {
    @Override
    public Integer createAccumulator() {
        return 0;
    }
​
    @Override
    public Integer add(WaterSensor waterSensor, Integer integer) {
        return integer+ waterSensor.getVal();
    }
​
    @Override
    public Integer getResult(Integer integer) {
        return integer;
    }
​
    @Override
    public Integer merge(Integer integer, Integer acc1) {
        return null;
    }
}, new ProcessWindowFunction<Integer, Tuple3<String,String,Integer>, String, TimeWindow>() {
    @Override
    public void process(String s, ProcessWindowFunction<Integer, Tuple3<String, String,Integer>, String, TimeWindow>.Context context, Iterable<Integer> iterable, Collector<Tuple3<String, String,Integer>> collector) throws Exception {
        long start = context.window().getStart();
        long end = context.window().getEnd();
        Integer res=0;
        for (Integer i : iterable) {
            res=res+i;
        }
        String windowStart = DateFormatUtils.format(start, "yyyy-MM-dd-HH:mm:ss");
        String windowEnd = DateFormatUtils.format(end, "yyyy-MM-dd-HH:mm:ss");
        collector.collect(Tuple3.of(windowStart,windowEnd,res));
    }
}).print();

水位线

  • 在窗口的处理过程中,基于数据的时间戳自定义一个“逻辑时钟”,这个时钟的时间不会自动流逝,而是靠新到达数据的时间戳来推动。基于此,计算的过程可以完全不依赖处理时间,在一般实时流处理的场景中,事件事件可以基本与处理时间保持同步,同时保证了计算的正确。
  • 在flink中用来衡量事件时间进展的标记被称为水位线。
  • 理想状态下,数据按照生成的先后顺序进入流中,每一条数据都生成一个水位线。但是,当数据量非常大且同时涌入的数据时差非常小时,为了提高效率,可以每隔一段时间生成一个水位线。
  • 在分布式系统中,数据在节点间传输。由于网络传输延迟,可能导致乱序。此时要生成水位线,可以对每个新到达的数据提取时间戳,并判断时间戳是否比之前的大,是则生成新的水位线。也就是说只有新到达数据的时间戳比当前时钟大才能推动时钟前进。当数据量很大时,为了提高效率,选择一段时间内到达数据中的最大时间戳,作为这段时间的水位线。
  • 对于迟到数据,为了让窗口能正确收集,可以让时钟进度延迟一会(比如延迟2秒)。这种情况下,要生成的水位线的时间戳就是,当前数据最大时间戳减去2秒。
  • 水位线表示当前流中所有数据的时间戳都小于等于水位线的时间戳。

forMonotonousTimestamps

  • 事件时间单调递增,适用于有序流
SingleOutputStreamOperator<WaterSensor> assigned = mapped.assignTimestampsAndWatermarks(WatermarkStrategy.
        <WaterSensor>forMonotonousTimestamps().
        withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
            @Override
            public long extractTimestamp(WaterSensor waterSensor, long l) {
                System.out.println(waterSensor + "at:" + l);
                return waterSensor.getTs() * 1000L;
            }
        }));

forBoundedOutOfOrderness

  • 可以指定等待时间,适用于有限乱序的流
SingleOutputStreamOperator<WaterSensor> assigned = mapped.assignTimestampsAndWatermarks(WatermarkStrategy
        .<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
        .withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
            @Override
            public long extractTimestamp(WaterSensor waterSensor, long l) {
                System.out.println(waterSensor + "at:" + l);
                return waterSensor.getTs();
            }
        }));

传递

  • 必须遵循最小水位线原则
  • 下游算子的水位线是所有上游水位线的最小值
  • 水位线只允许向前,不会倒退

空闲等待

  • 当上游某个子任务长时间没有数据,并且其水位线是上游所有子任务的最小值,造成了下游水位线停滞,窗口被卡住的问题。flink引入空闲检测来解决这一问题。
  • withIdleness方法可以设置空闲等待时间,超过该时间没有收到数据,相应子任务被判定为空闲,其水位线不再影响下游,直到收到数据
  • 当在测试过程中遇到水位线停滞的问题时,可以先把测试代码的并行度设置为1,验证该问题是否为空闲分区引起
WatermarkStrategy
        .<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
        .withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
            @Override
            public long extractTimestamp(WaterSensor waterSensor, long l) {
                System.out.println(waterSensor + "at:" + l);
                return waterSensor.getTs()*1000L;
            }
        })
        .withIdleness(Duration.ofSeconds(20)));

允许迟到

  • 可以配置allowedLateness方法来延迟窗口的关闭,窗口结束后,在允许迟到的时间内,到达的数据仍能被处理(例如可以触发计算并输出)
  • 与窗口等待时间的区别:窗口等待是为了处理乱序的流,本该先到但是后到的数据也能被划分到窗口内,然后统一触发计算。允许迟到是为了处理窗口结束后才到达的数据,这些数据会马上触发计算。
keyed.window(TumblingEventTimeWindows.of(Time.seconds(5)))
        .allowedLateness(Time.seconds(2)).process(new ProcessWindowFunction<WaterSensor, Tuple3<String,String,Integer>, String, TimeWindow>() {
​
    @Override
    public void process(String s, ProcessWindowFunction<WaterSensor, Tuple3<String, String, Integer>, String, TimeWindow>.Context context, Iterable<WaterSensor> iterable, Collector<Tuple3<String, String, Integer>> collector) throws Exception {
        long start = context.window().getStart();
        long end = context.window().getEnd();
        String windowStart = DateFormatUtils.format(start, "yyyy-MM-dd-HH:mm:ss");
        String windowEnd = DateFormatUtils.format(end, "yyyy-MM-dd-HH:mm:ss");
        Integer maxn=0;
        for (WaterSensor waterSensor : iterable) {
            maxn=max(maxn,waterSensor.getVal());
        }
        collector.collect(Tuple3.of(windowStart,windowEnd,maxn));
    }
}).print();

侧输出流处理迟到

  • sideOutputLateData方法可以将关闭窗口后到达的数据发送到侧输出流
SingleOutputStreamOperator<Tuple3<String, String, Integer>> processed = keyed.window(TumblingEventTimeWindows.of(Time.seconds(5)))
        .allowedLateness(Time.seconds(2))
        .sideOutputLateData(outputTag)
        .process(new ProcessWindowFunction<WaterSensor, Tuple3<String, String, Integer>, String, TimeWindow>() {

            @Override
            public void process(String s, ProcessWindowFunction<WaterSensor, Tuple3<String, String, Integer>, String, TimeWindow>.Context context, Iterable<WaterSensor> iterable, Collector<Tuple3<String, String, Integer>> collector) throws Exception {
                long start = context.window().getStart();
                long end = context.window().getEnd();
                String windowStart = DateFormatUtils.format(start, "yyyy-MM-dd-HH:mm:ss");
                String windowEnd = DateFormatUtils.format(end, "yyyy-MM-dd-HH:mm:ss");
                Integer maxn = 0;
                for (WaterSensor waterSensor : iterable) {
                    maxn = max(maxn, waterSensor.getVal());
                }
                collector.collect(Tuple3.of(windowStart, windowEnd, maxn));
            }
        });

processed.getSideOutput(outputTag).print();
processed.print();

合流

window join

  • 对于两条已经分配WatermarkStrategy的数据流,先调用join方法连接,再设置window分配窗口,由此实现简单的双流联结窗口。
  • 优点:语义清晰,易于理解;直接支持各种 WindowAssigner;事件时间驱动,支持乱序;
  • 缺点:由于join匹配是笛卡尔积的形式,这种写法只适用于数据量小,窗口粒度的情况;不能使用更灵活的匹配逻辑;当匹配的两个数据刚好卡在窗口边缘两侧时会丢失
SingleOutputStreamOperator<String> assigned1 = ds1.assignTimestampsAndWatermarks(strategy);
SingleOutputStreamOperator<String> assigned2 = ds2.assignTimestampsAndWatermarks(strategy);
assigned1.join(assigned2).where(keySelector).equalTo(keySelector)
        .window(TumblingEventTimeWindows.of(Time.seconds(15)))
        .apply(new RichJoinFunction<String, String, Tuple3<String,String,Integer>>() {
            @Override
            public Tuple3<String,String,Integer> join(String s, String s2) throws Exception {
                String[] water1 = s.split(" ");
                String[] water2 = s2.split(" ");

                return Tuple3.of(water1[1],water2[1],Integer.valueOf(water1[2])+Integer.valueOf(water2[2]));
            }
        }).print();

IntervalJoin

  • 一条流中的任意数据以其时间戳为中心,从其下界点到上界点的闭区间,为该数据可以跟其它流匹配的范围
  • 只支持事件时间
  • 只支持keyed状态
  • 可以通过设置sideoutput获取迟到数据
KeyedStream<String, String> ds11 = ds1.assignTimestampsAndWatermarks(strategy).keyBy(keySelector);
        KeyedStream<String, String> ds22 = ds2.assignTimestampsAndWatermarks(strategy).keyBy(keySelector);
​
OutputTag<String>l=new OutputTag<>("left-late");
OutputTag<String>r=new OutputTag<>("right-late");
​
SingleOutputStreamOperator<Tuple3<String, String, Integer>> processed = ds11.intervalJoin(ds22)
        .between(Time.seconds(-2), Time.seconds(2))
        .sideOutputLeftLateData(l).sideOutputRightLateData(r)
        .process(new ProcessJoinFunction<String, String, Tuple3<String, String, Integer>>() {
            @Override
            public void processElement(String s, String s2, ProcessJoinFunction<String, String, Tuple3<String, String, Integer>>.Context context, Collector<Tuple3<String, String, Integer>> collector) throws Exception {
                String[] w1 = s.split(" ");
                String[] w2 = s2.split(" ");
​
                collector.collect(Tuple3.of(w1[1], w2[1], Integer.valueOf(w1[2]) + Integer.valueOf(w2[2])));
            }
        });
processed.print();
processed.getSideOutput(l).print();
processed.getSideOutput(r).print();