Flink窗口与WaterMark

本文目录

  • 窗口的生命周期

  • Window Assigners

  • 窗口函数(Window Functions)

  • Triggers

  • Evictors

  • Allowed Lateness

窗口

窗口(Window)是处理无界流的关键所在。窗口可以将数据流装入大小有限的“桶”中,再对每个“桶”加以处理。本文的重心将放在 Flink 如何进行窗口操作以及开发者如何尽可能地利用 Flink 所提供的功能。

下面展示了 Flink 窗口在 keyed streams 和 non-keyed streams 上使用的基本结构。我们可以看到,这两者唯一的区别仅在于:keyed streams 要调用 keyBy(...)后再调用 window(...) , 而 non-keyed streams 只用直接调用 windowAll(...)。留意这个区别,它能帮我们更好地理解后面的内容。

Keyed Windows

stream
       .keyBy(...)               <-  仅 keyed 窗口需要
       .window(...)              <-  必填项:"assigner"
      [.trigger(...)]            <-  可选项:"trigger" (省略则使用默认 trigger)
      [.evictor(...)]            <-  可选项:"evictor" (省略则不使用 evictor)
      [.allowedLateness(...)]    <-  可选项:"lateness" (省略则为 0)
      [.sideOutputLateData(...)] <-  可选项:"output tag" (省略则不对迟到数据使用 side output)
       .reduce/aggregate/apply()      <-  必填项:"function"
      [.getSideOutput(...)]      <-  可选项:"output tag"

这里有个demo,比如统计百度热搜,百度热搜一般是最近15分钟,为了演示简单起见,这里3s,并且是滚动窗口:

public static void demo() throws Exception {
    StreamExecutionEnvironment senv = StreamExecutionEnvironment.getExecutionEnvironment();

    DataStream<Tuple2<String, Integer>> dataStream = senv.socketTextStream("192.168.20.130", 9999)
            .flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {

                @Override
                public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
                    String[] values = value.split(" ");
                    for(String v : values) {
                        out.collect(Tuple2.of(v, 1));
                    }
                }
            });
    dataStream.keyBy(item -> item.f0)
            .window(TumblingProcessingTimeWindows.of(Time.milliseconds(3000)))
            .reduce((a,b) -> Tuple2.of(a.f0, a.f1 + b.f1))
            .print();

    senv.execute("WindowDemo");
}

启动程序,并且nc输入:flink hadoop flink,结果:

7> (flink,2)
8> (hadoop,1)

Non-Keyed Windows

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

windowAll所有数据进入一个窗口,不需要keyBy()算子:

public static void demo2() throws Exception {
    StreamExecutionEnvironment senv = StreamExecutionEnvironment.getExecutionEnvironment();

    DataStream<Tuple2<String, Integer>> dataStream = senv.socketTextStream("192.168.20.130", 9999)
            .flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {

                @Override
                public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
                    String[] values = value.split(" ");
                    for(String v : values) {
                        out.collect(Tuple2.of(v, 1));
                    }
                }
            });
    dataStream.windowAll(TumblingProcessingTimeWindows.of(Time.milliseconds(3000)))
            .reduce((a,b) -> Tuple2.of(a.f0, a.f1 + b.f1))
            .print();

    senv.execute("WindowDemo");
}

启动程序,nc键入数据:flink hadoop flink,结果

4> (flink,3)

首先必须要在定义窗口前确定的是你的 stream 是 keyed 还是 non-keyed。keyBy(...) 会将你的无界 stream 分割为逻辑上的 keyed stream。如果 keyBy(...) 没有被调用,你的 stream 就不是 keyed。

对于 keyed stream,其中数据的任何属性都可以作为 key (详见此处)。使用 keyed stream 允许你的窗口计算由多个 task 并行,因为每个逻辑上的 keyed stream 都可以被单独处理。属于同一个 key 的元素会被发送到同一个 task。

对于 non-keyed stream,原始的 stream 不会被分割为多个逻辑上的 stream, 所以所有的窗口计算会被同一个 task 完成,也就是 parallelism 为 1。

窗口的生命周期

简单来说,一个窗口在第一个属于它的元素到达时就会被创建,然后在时间(event 或 processing time) 超过窗口的“结束时间戳 + 用户定义的 allowed lateness (详见 Allowed Lateness)”时 被完全删除。Flink 仅保证删除基于时间的窗口,其他类型的窗口不做保证, 比如全局窗口(详见 Window Assigners)。例如,对于一个基于 event time 且范围互不重合(滚动)的窗口策略, 如果窗口设置的时长为五分钟、可容忍的迟到时间(allowed lateness)为 1 分钟, 那么第一个元素落入 12:00 至 12:05 这个区间时,Flink 就会为这个区间创建一个新的窗口。当 watermark 越过 12:05 时,这个窗口将被摧毁。

另外,每个窗口会设置自己的 Trigger (详见 Triggers)和 function (ProcessWindowFunction、ReduceFunction、或 AggregateFunction, 详见 Window Functions)。该 function 决定如何计算窗口中的内容, 而 Trigger 决定何时窗口中的数据可以被 function 计算。Trigger 的触发(fire)条件可能是“当窗口中有多于 4 条数据”或“当 watermark 越过窗口的结束时间”等。Trigger 还可以在 window 被创建后、删除前的这段时间内定义何时清理(purge)窗口中的数据。这里的数据仅指窗口内的元素,不包括窗口的 meta data。也就是说,窗口在 purge 后仍然可以加入新的数据。

除此之外,你也可以指定一个 Evictor (详见 Evictors),在 trigger 触发之后,Evictor 可以在窗口函数的前后删除数据。

接下来我们会更详细地介绍上面提到的内容。开头的例子中有必填项和可选项。我们先从必填项开始(详见 Keyed vs Non-Keyed Windows、 Window Assigners、Window Functions)。

Window Assigners

指定了你的 stream 是否为 keyed 之后,下一步就是定义 window assigner。

Window assigner 定义了 stream 中的元素如何被分发到各个窗口。你可以在 window(...)(用于 keyed streams)或 windowAll(...) (用于 non-keyed streams)中指定一个 WindowAssigner。WindowAssigner 负责将 stream 中的每个数据分发到一个或多个窗口中。Flink 为最常用的情况提供了一些定义好的 window assigner,也就是 tumbling windows、 sliding windows、 session windows 和 global windows。你也可以继承 WindowAssigner 类来实现自定义的 window assigner。所有内置的 window assigner(除了 global window)都是基于时间分发数据的,processing time 或 event time 均可。请阅读我们对于 event time 的介绍来了解这两者的区别, 以及 timestamp 和 watermark 是如何产生的。

基于时间的窗口用 start timestamp(包含)和 end timestamp(不包含)描述窗口的大小。在代码中,Flink 处理基于时间的窗口使用的是 TimeWindow, 它有查询开始和结束 timestamp 以及返回窗口所能储存的最大 timestamp 的方法 maxTimestamp()。

接下来我们会说明 Flink 内置的 window assigner 如何工作,以及他们如何用在 DataStream 程序中。下面的图片展示了每种 assigner 如何工作。紫色的圆圈代表 stream 中按 key 划分的元素(本例中是按 user 1、user 2 和 user 3 划分)。x 轴表示时间的进展。

通过一些示例来展示关于这些窗口如何使用,或者如何区分它们:

滚动时间窗口

  • 每分钟页面浏览量

  • TumblingEventTimeWindows.of(Time.minutes(1))

滑动时间窗口

  • 每10秒钟计算前1分钟的页面浏览量

  • SlidingEventTimeWindows.of(Time.minutes(1), Time.seconds(10))

会话窗口

  • 每个会话的网页浏览量,其中会话之间的间隔至少为30分钟

  • EventTimeSessionWindows.withGap(Time.minutes(30))

以下都是一些可以使用的间隔时间 Time.milliseconds(n), Time.seconds(n), Time.minutes(n), Time.hours(n), 和 Time.days(n)。

滚动窗口(Tumbling Windows)

滚动窗口的 assigner 分发元素到指定大小的窗口。滚动窗口的大小是固定的,且各自范围之间不重叠。比如说,如果你指定了滚动窗口的大小为 5 分钟,那么每 5 分钟就会有一个窗口被计算,且一个新的窗口被创建(如下图所示)。

5e7e463722056c05b5ec49e3eda552b7.jpeg
tumb-window

下面的代码展示了如何使用滚动窗口。

DataStream<T> input = ...;

// 滚动 event-time 窗口
input
    .keyBy(<key selector>)
    .window(TumblingEventTimeWindows.of(Time.seconds(5)))
    .<windowed transformation>(<window function>);

// 滚动 processing-time 窗口
input
    .keyBy(<key selector>)
    .window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
    .<windowed transformation>(<window function>);

// 长度为一天的滚动 event-time 窗口, 偏移量为 -8 小时。
input
    .keyBy(<key selector>)
    .window(TumblingEventTimeWindows.of(Time.days(1), Time.hours(-8)))
    .<windowed transformation>(<window function>);

时间间隔可以用 Time.milliseconds(x)、Time.seconds(x)、Time.minutes(x) 等来指定。

如上一个例子所示,滚动窗口的 assigners 也可以传入可选的 offset 参数。这个参数可以用来对齐窗口。比如说,不设置 offset 时,长度为一小时的滚动窗口会与 linux 的 epoch 对齐。你会得到如 1:00:00.000 - 1:59:59.999、2:00:00.000 - 2:59:59.999 等。如果你想改变对齐方式,你可以设置一个 offset。如果设置了 15 分钟的 offset, 你会得到 1:15:00.000 - 2:14:59.999、2:15:00.000 - 3:14:59.999 等。一个重要的 offset 用例是根据 UTC-0 调整窗口的时差。比如说,在中国你可能会设置 offset 为 Time.hours(-8)。

滑动窗口(Sliding Windows)

与滚动窗口类似,滑动窗口的 assigner 分发元素到指定大小的窗口,窗口大小通过 window size 参数设置。滑动窗口需要一个额外的滑动距离(window slide)参数来控制生成新窗口的频率。因此,如果 slide 小于窗口大小,滑动窗口可以允许窗口重叠。这种情况下,一个元素可能会被分发到多个窗口。

比如说,你设置了大小为 10 分钟,滑动距离 5 分钟的窗口,你会在每 5 分钟得到一个新的窗口, 里面包含之前 10 分钟到达的数据(如下图所示)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

大数据技术派

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

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

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

打赏作者

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

抵扣说明:

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

余额充值