大数据入门--Flink(三)Window相关概念与API

概述

streaming 流式计算是一种被设计用于处理无限数据集的数据处理引擎,而无限数据集是指一种不断增长的本质上无限的数据集,而 window 是一种切割无限数据为有限块进行处理的手段。
Window 是无限数据流处理的核心,Window 将一个无限的 stream 拆分成有限大小的”buckets”桶,我们可以在这些桶上做计算操作。

窗口类型

窗口可以分为两大类:

  • CountWindow:按照指定的数据条数生成一个 Window,与时间无关。
  • TimeWindow:按照时间生成 Window。

窗口的开闭性:前闭后开

根据实现原理的不同又可以分为

  • 滚动窗口
    • 特点:时间对齐,窗口长度固定,没有重叠。
  • 滑动窗口
    • 特点:时间对齐,窗口长度固定,可以有重叠。
  • 会话窗口
    • 特点:时间无对齐。

还有一种比较特殊的窗口是

  • 全局窗口:即将整个输入流看做一个窗口

时间语义与Watermark

  • Ingestion Time:是数据进入 Flink 的时间。
  • Event Time:是事件创建的时间。它通常由事件中的时间戳描述,例如采集的日志数据中,每一条日志都会记录自己的生成时间,Flink 通过时间戳分配器访问事件时间戳。
  • Processing Time:是每一个执行基于时间操作的算子的本地系统时间,与机器相关,默认的时间属性就是 Processing Time。

Processing Time是flink1.12版本以前 默认的窗口分割方式,一般大多数业务中使用的是Event Time。flink 1.12开始,默认使用EventTime

Watermark:主要是用来处理乱序数据的一种机制。可以理解成一个延迟触发机制,我们可以设置Watermark 的延时时长 t,每次系统会校验已经到达的数据中最大的 maxEventTime,然后认定 eventTime小于 maxEventTime - t 的所有数据都已经到达,如果有窗口的停止时间等于maxEventTime – t,那么这个窗口被触发执行。

个人理解:
以EventTime处理为例,Watermark与EvenTime结合使用,用来控制乱序数据的乱序程度。正常的话(即数据有序流入)数据按照EventTime划分窗口进行计算,但是实际场景下数据流大部分情况下可能是乱序流入的,Watermark会在EventTime的基础上,将触发窗口计算、输出的时间节点向后推移。
比如目前数据是顺序为1、2、5、3、6、7、8、4,时间窗口是5,watermark是EventTime - 2。那么目前默认窗口应该是1-5为一个窗口,6-10为一个窗口。下面我们分析数据窗口以及关闭情况。
1号数据,水位线为-1,创建1-5号窗口,放入数据
2号数据,水位线为-0,进入1-5号窗口
5号数据,水位线为3,进入1-5号窗口,此时水位线为3,不能输出数据关闭窗口
6号数据,水位线为4,创建6-10号窗口,放入数据
7号数据,水位线为5,进入6-10号窗口,此时1-5号窗口的已经达到水位线,计算数据输出数据关闭窗口
8号数据,水位线为6,进入6-10号窗口
4号数据,水位线为2,1-5号窗口已关闭,进入侧写流,不能被正常处理,但是通过侧写流最终可以保证正确的结果输出。
通俗讲:也就是当水位线到达或者超过某个窗口的截止时间,将会计算输出并关闭窗口,之后再出现此窗口的数据,也将被写入到侧写流。

Watermark API

watermark是一种特殊事件,从产生的周期来看,watermark的生成方式有两类,分别是

  • 周期性生成:周期性生成主要通过env.getConfig.setAutoWatermarkInterval(5000);设置,默认200ms。适合数据事件密集型的。
  • 间断式生成:即没处理一条事件,就触发一次。适合小量数据流。

在周期性生成watermark的策略中,存在一种特殊的数据,即本身就是有序的,此时可以用AscendingTimestampExtractor,这个类会直接使用数据的时间戳生成 watermark。

总结

  • WatermarkStrategy.forBoundedOutOfOrderness:周期性生成watermark,数据乱序,延迟=周期间隔长度+无序边界。周期间隔:env.getConfig.setAutoWatermarkInterval(xx),无需边界即入参。
    • WatermarkStrategy.forMonotonousTimestamps:单调递增(事件本身是有序的),边界延迟0ms,术语特殊的BoundedOutOfOrderness。
  • WatermarkStrategy.withIdleness:支持空闲检测,解决数据倾斜问题,提高时延。个人理解,当某分区超过未接收到新数据,则标记为空闲,空闲的分区不参与watermark的生成,自然也就减少了压力,当有新数据产生再次被激活,参与watermark的生成,从而提高flink的效率。
  • env.getConfig().setAutoWatermarkInterval(0):我所使用版本(1.13.2)通过设置间隔为0,禁用周期。即每个事件触发一次。

Window API

官方传送门

Keyed Windows

stream
       .keyBy(...)               <-  keyed versus non-keyed windows
       .window(...)              <-  required: "assigner"
      [.trigger(...)]            <-  optional: "trigger" (else default trigger)
      [.evictor(...)]            <-  optional: "evictor" (else no evictor)
      [.allowedLateness(...)]    <-  optional: "lateness" (else zero)
      [.sideOutputLateData(...)] <-  optional: "output tag" (else no side output for late data)
       .reduce/aggregate/apply()      <-  required: "function"
      [.getSideOutput(...)]      <-  optional: "output tag"

Non-Keyed Windows

stream
       .windowAll(...)           <-  required: "assigner"
      [.trigger(...)]            <-  optional: "trigger" (else default trigger)
      [.evictor(...)]            <-  optional: "evictor" (else no evictor)
      [.allowedLateness(...)]    <-  optional: "lateness" (else zero)
      [.sideOutputLateData(...)] <-  optional: "output tag" (else no side output for late data)
       .reduce/aggregate/apply()      <-  required: "function"
      [.getSideOutput(...)]      <-  optional: "output tag"

常用开窗

  • TumblingProcessingTimeWindows:处理时间的滚动窗口
  • TumblingEventTimeWindows:事件时间的滚动窗口
  • SlidingProcessingTimeWindows:处理时间的滑动窗口
  • SlidingEventTimeWindows:事件时间的滑动窗口

以上方法需要在keyBy后开窗,类比SQL开窗需要group by

滚动时间窗口

案例:
监听socket数据,计算N秒内的wordcount
使用ProcessingTime

public static void main(String[] args) throws Exception {
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    ExecutionConfig config = env.getConfig();
    env.setParallelism(1);
    config.setAutoWatermarkInterval(0);
    DataStream<String> dataStream = env.socketTextStream("hadoop101", 9999);
    dataStream
            .flatMap((FlatMapFunction<String, Tuple2<String, Long>>) (value, out) -> {
                if (StringUtils.isNotBlank(value)) {
                    String[] split = value.split("\\s+");
                    for (String word : split) {
                        out.collect(new Tuple2<>(word, 1L));
                    }
                }
            })
            //lamda表达式,泛型擦除,需要声明返回类型
            .returns(Types.TUPLE(Types.STRING,Types.LONG))
            //全部发往一个分区
            .keyBy(data -> "all")
            //重点:设置滚动窗口,Processing Time
            .window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
            .sum(1)
            .print();
    env.execute();
}

延伸:
需要通过.window(WindowAssigner<? super T, W> assigner)设置窗口

案例升级
使用事件时间
统计N秒内的单词总数
描述:
这次我们把输入的一段文本在文本的最前面追加一个事件时间,如
1631343572195 aa bb cc dd
即第一个字段为事件时间。同时我们要求watermark=3s
此案例需要解决的问题

  • 如何读取数据中的EventTime
  • 如何设置watermark
  • 如何开启一个EventTime滚动时间窗口

测试数据

1631343601000 aa bb
1631343603000 aa bb cc
1631343606000 aa dd 
1631343607000 aa ee 
1631343604000 aa dd ff
1631343608000 aa cc bb

分析上述数据
当我们输入完毕最后一条数据后,第一个窗口将会输出值,输出结果为:aa,8

public static void main(String[] args) throws Exception {
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    ExecutionConfig config = env.getConfig();
    env.setParallelism(1);
    DataStream<String> dataStream = env.socketTextStream("hadoop101", 9999);
    dataStream
            .assignTimestampsAndWatermarks(
                    WatermarkStrategy.
                            //设置watermark
                            <String>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                            //设置事件时间提取:取切分后第一个字段
                            .withTimestampAssigner((event, timestamp) -> {
                                String[] split = event.split("\\s+");
                                return Long.valueOf(split[0]);
                            }))
            .flatMap((FlatMapFunction<String, Tuple2<String, Long>>) (value, out) -> {
                if (StringUtils.isNotBlank(value)) {
                    String[] split = value.split("\\s+");
                    for (int i = 1; i < split.length; i++) {
                        String word = split[i];
                        out.collect(new Tuple2<>(word, 1L));

                    }
                }
            })
            //lamda表达式,泛型擦除,需要声明返回类型
            .returns(Types.TUPLE(Types.STRING, Types.LONG))
            //全部发往一个分区
            .keyBy(data -> "all")
            //重点:设置滚动窗口,Event Time
            .window(TumblingEventTimeWindows.of(Time.seconds(5)))
            .sum(1)
            .print();
    env.execute();
}

值得注意的一点:

时区问题

假设我们有需求统计一天内的XXX。此时我们可能想到的是按照天进行开窗,由于flink默认是UTC进行窗口计算的,而我们国家的时间是UTC+8,此时就会出现我们实际统计的时间区间是前一天8点到今天8点的数据。
我们看一下下面的案例:
将上面的时间窗口改为1天,水位线改为0.
假设我们现在输入如下数据

#2021-09-11 00:00:00
1631289600000 aa
#2021-09-11 08:00:00
1631318400000 aa bb cc
#2021-09-12 00:00:00
1631376000000 dd

此时我们发现,在输入第二条数据的时候,结果输出了,而且结果是1!!!。
我们的预期是什么?第三条数据时输出,且结果是4。
所以我们需要将水位线向前推移8小时。

//增加了一个时间偏移量,-8小时
.window(TumblingEventTimeWindows.of(Time.days(1),Time.hours(-8)))

当然这个问题的主要原因是因为flink允许时的时间是UTC+0,而我们数据的事件时间时按照UTC+8产生的,所以另一个解决方向是将集群的flink配置时区修改为UTC+8,统一时区也是一种解决方案。传送门

所以我们最终的java代码如下

public static void main(String[] args) throws Exception {
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    ExecutionConfig config = env.getConfig();
    env.setParallelism(1);
    DataStream<String> dataStream = env.socketTextStream("hadoop101", 9999);
    dataStream
            .assignTimestampsAndWatermarks(
                    WatermarkStrategy.
                            //设置watermark
                            <String>forBoundedOutOfOrderness(Duration.ofSeconds(0))
                            //设置事件时间提取:取切分后第一个字段
                            .withTimestampAssigner((event, timestamp) -> {
                                String[] split = event.split("\\s+");
                                return Long.valueOf(split[0]);
                            }))
            .flatMap((FlatMapFunction<String, Tuple2<String, Long>>) (value, out) -> {
                if (StringUtils.isNotBlank(value)) {
                    String[] split = value.split("\\s+");
                    for (int i = 1; i < split.length; i++) {
                        String word = split[i];
                        out.collect(new Tuple2<>(word, 1L));

                    }
                }
            })
            //lamda表达式,泛型擦除,需要声明返回类型
            .returns(Types.TUPLE(Types.STRING, Types.LONG))
            //全部发往一个分区
            .keyBy(data -> "all")
            //重点:设置滚动窗口,Event Time
            //增加了一个时间偏移量,-8小时
            .window(TumblingEventTimeWindows.of(Time.days(1),Time.hours(-8)))
            .sum(1)
            .print();
    env.execute();
}

滑动时间窗口

上面计算的都是每个滚动N秒窗口的单词总量
现在我们需要计算当前5秒内的窗口单词总量,所以需要用到滑动窗口

滑动窗口有两个参数,分别是窗口大小和滑动步长
关键代码
每1秒更新一次5秒内的单词总量

//重点:设置滑动窗口,Event Time
.window(SlidingEventTimeWindows.of(Time.seconds(5),Time.seconds(1)))

滚动计数窗口

根据窗口中相同 key 元素的数量来触发执行,执行时只计算元素数量达到窗口大小的 key 对应的结果。
只需要指定一个窗口大小参数。

.countWindow(10)

滑动计数窗口

滑动窗口和滚动窗口的函数名是完全一致的,只是在传参数时需要传入两个参数,一个是 window_size,一个是 sliding_size。

.countWindow(10,1)

Window Function

主要分为两类

  • 增量聚合函数(incremental aggregation functions)
    • 每条数据到来就进行计算,保持一个简单的状态。
    • 典型的增量聚合函数有ReduceFunction, AggregateFunction。
  • 全窗口函数(full window functions)
    • 先把窗口所有数据收集起来,等到计算的时候会遍历所有数据。
    • ProcessWindowFunction 就是一个全窗口函数。

其它可选 API

  • .trigger() —— 触发器
    • 定义 window 什么时候关闭,触发计算并输出结果
  • .evitor() —— 移除器
    • 定义移除某些数据的逻辑
  • .allowedLateness() —— 允许处理迟到的数据(窗口延迟关闭,稍微等一会迟到数据)
  • .sideOutputLateData() —— 将迟到的数据放入侧输出流
  • .getSideOutput() —— 获取侧输出流

ProcessFunction API(底层API)

Flink 提供了 8 个 Process Function:
• ProcessFunction
• KeyedProcessFunction
• CoProcessFunction
• ProcessJoinFunction
• BroadcastProcessFunction
• KeyedBroadcastProcessFunction
• ProcessWindowFunction
• ProcessAllWindowFunction

KeyedProcessFunction

  • processElement(I value, Context ctx, Collector<O> out)
    流中的每一个元素都会调用这个方法,调用结果将会放在 Collector 数据类型中输出。Context以访问元素的时间戳,元素的 key,以及 TimerService 时间服务。Context 还可以将结果输出到别的流(side outputs)。
  • onTimer(long timestamp, OnTimerContext ctx, Collector<O> out)
    是一个回调函数。当之前注册的定时器触发时调用。参数 timestamp 为定时器所设定的触发的时间戳。Collector 为输出结果的集合。OnTimerContext 和processElement 的 Context 参数一样,提供了上下文的一些信息,例如定时器触发的时间信息(事件时间或者处理时间)。

案例:
计算某API接口10秒内的TPS连续上升
分析:
此案例无论是滚动窗口还是滑动窗口,都无法实现检测连续10S内的TPS上升。

测试的数据,这里的时间无意义,因为定时器是使用的processing time

2021-09-12 12:00:00|aa|1
2021-09-12 12:00:01|aa|10
2021-09-12 12:00:01|aa|5
2021-09-12 12:00:01|aa|50
public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        ExecutionConfig config = env.getConfig();
        config.setAutoWatermarkInterval(0);
        env.setParallelism(1);
        DataStream<String> dataStream = env.socketTextStream("hadoop101", 9999);
        dataStream.map((value)->{
            String[] split = value.split("\\|+");
            ApiBean apiBean = new ApiBean();
            apiBean.setInputTime(split[0]);
            apiBean.setInterfaceId(split[1]);
            apiBean.setTps(Integer.valueOf(split[2]));
            return apiBean;
        }).returns(ApiBean.class)
//未定义事件时间,则使用的是Processing Time        
//                .assignTimestampsAndWatermarks(
//                        WatermarkStrategy.
//                                //设置watermark
//                                        <ApiBean>forMonotonousTimestamps()
//                                //设置事件时间提取:取切分后第一个字段
//                                .withTimestampAssigner((event, timestamp) -> {
//                                    String inputTime = event.getInputTime();
//                                    try {
//                                        return DateUtils.parseDate(inputTime,"yyyy-MM-dd HH:mm:ss").getTime();
//                                    } catch (ParseException e) {
//                                        e.printStackTrace();
//                                    }
//                                    return -1;
//                                }))
                .keyBy(data -> data.getInterfaceId())
                .process(new KeyedProcessFunction<String, ApiBean, String>() {
                    private Integer interval = 10*1000;
                    private Long timer;
                    private Integer lastTps;

                    @Override
                    public void open(Configuration parameters) throws Exception {
                        super.open(parameters);
                    }

                    @Override
                    public void processElement(ApiBean value, Context ctx, Collector<String> out) throws Exception {
                        /*
                         * 判断当前tps是否大于上次tps,且timer为空,成立则设置一个10S的定时器
                         */
                        Integer tps = value.getTps();
                        if(timer == null && (lastTps == null || tps > lastTps)){
                            this.timer = ctx.timerService().currentProcessingTime()+ interval;
                            //注册定时器
                            ctx.timerService().registerProcessingTimeTimer(this.timer);
                        }else if(timer != null && (lastTps != null && tps <= lastTps)){
                            //清除定时器
                            ctx.timerService().deleteProcessingTimeTimer(timer);
                            this.timer = null;
                        }
                        this.lastTps = tps;
                    }

                    /*
                     * timestamp : 定时器触发的时间戳
                     */
                    @Override
                    public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
                        String msg = "时间区间"+ DateFormatUtils.format(new Date(timestamp),"yyyy-MM-dd HH:mm:ss") + "至"+ DateFormatUtils.format(new Date(timestamp+interval),"yyyy-MM-dd HH:mm:ss")+",TPS持续上升,当前最大TPS为"+this.lastTps;
                        out.collect(msg);
                        this.timer = null;
                    }
                })
                .print();
        env.execute();
    }

实现一个侧写流
TPS小于50的,侧写至lowTps一份

public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        ExecutionConfig config = env.getConfig();
        config.setAutoWatermarkInterval(0);
        env.setParallelism(1);
        DataStream<String> dataStream = env.socketTextStream("hadoop101", 9999);

        //值得注意:tag是匿名内部类
        OutputTag<ApiBean> lowTpsTag = new OutputTag<ApiBean>("lowTps"){};

        SingleOutputStreamOperator<ApiBean> allDataStream = dataStream.map((value) -> {
            String[] split = value.split("\\|+");
            ApiBean apiBean = new ApiBean();
            apiBean.setInputTime(split[0]);
            apiBean.setInterfaceId(split[1]);
            apiBean.setTps(Integer.valueOf(split[2]));
            return apiBean;
        }).returns(ApiBean.class)
                .assignTimestampsAndWatermarks(
                        WatermarkStrategy.
                                //设置watermark
                                        <ApiBean>forMonotonousTimestamps()
                                //设置事件时间提取:取切分后第一个字段
                                .withTimestampAssigner((event, timestamp) -> {
                                    String inputTime = event.getInputTime();
                                    try {
                                        return DateUtils.parseDate(inputTime, "yyyy-MM-dd HH:mm:ss").getTime();
                                    } catch (ParseException e) {
                                        e.printStackTrace();
                                    }
                                    return -1;
                                }));
        allDataStream.print("allData");

        //获取侧写流
        allDataStream
                .process(new ProcessFunction<ApiBean, String>() {
                    @Override
                    public void open(Configuration parameters) throws Exception {
                        super.open(parameters);
                    }

                    @Override
                    public void processElement(ApiBean value, Context ctx, Collector<String> out) throws Exception {
                        Integer tps = value.getTps();
                        if(tps<50){
                            ctx.output(lowTpsTag,value);
                        }
                    }

                    /*
                     * timestamp : 定时器触发的时间戳
                     */
                    @Override
                    public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
                    }
                })
                .getSideOutput(lowTpsTag)
                .print("lowTps");
        env.execute();
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值