彻底搞懂 Flink Watermark 和 Window

本文详细介绍了Flink中的Watermark策略与Window操作的概念及应用。内容涵盖Watermark生成器、事件时间戳分配器、水位线原理、窗口触发机制、定时器使用方法以及自定义Watermark策略等方面。

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

WatermarkStrategy 定义了如何在流源中生成 Watermark 。 WatermarkStrategy 是用于构建水印生成器 (WatermarkGenerator) 和事件时间戳分配器( TimestampAssigner) 的工厂。通常不会自己实现这个接口,而是使用静态方法获取

1.有界无序水印和 lambda 函数作为时间戳分配器

WatermarkStrategy
        .<Tuple2<Long, String>>forBoundedOutOfOrderness(Duration.ofSeconds(20))
  			// TimestampAssigner 是可选的,kafka 从元数据获取
        .withTimestampAssigner((event, timestamp) -> event.f0);

2.为时间戳单调递增的情况创建水印策略

WatermarkStrategy
				// 此方法的实现是 有界无序 的一种特烈,乱序时间为 0;
        .<Tuple2<Long, String>>forMonotonousTimestamps()
  			// TimestampAssigner 是可选的,kafka 从元数据获取
        .withTimestampAssigner((event, timestamp) -> event.f0);
WatermarkStrategy.forMonotonousTimestamps()

3.空闲分区数据流

如果输入分区/分片之一在一段时间内不携带事件,则这意味着WatermarkGenerator也没有获得任何新信息作为水印的基础。我们称之为空闲输入空闲源。这是一个问题,因为您的某些分区可能仍然携带事件。在这种情况下,水印将被阻止,因为它被计算为所有不同并行水印的最小值。

WatermarkStrategy
        .<Tuple2<Long, String>>forBoundedOutOfOrderness(Duration.ofSeconds(20))
  			// 装饰模式,在上面 WatermarkStrategy 基础上增加空闲分区的策略
        .withIdleness(Duration.ofMinutes(1));

4.调用 watermark 生成生成器

WatermarkGenerator接口有两个未实现方法

public interface WatermarkGenerator<T> {

 /**
  * 每个事件调用,官方已实现的接口有两个作用
  * 作用1:比如记录所有事件里最大的时间戳(有界乱序型事件、单调递增型事件型)
  * 作用2:记录有特殊标记事件的事件时间(标记型事件水印生成)
  */
 void onEvent(T event, long eventTimestamp, WatermarkOutput output);

 /**
  * 周期性调用发射水印,调用周期取决于 env.getConfig().setAutoWatermarkInterval(),默认 200,单位毫秒
  */
 void onPeriodicEmit(WatermarkOutput output);
}
// 每隔 10 秒钟,Flink 会调用 WatermarkGenerator#onPeriodicEmit() 方法,产生一个水位线。
env.getConfig().setAutoWatermarkInterval(10000);

5.水位线的作用

总结:1.触发事件时间下的定时器;2.通过定时器触发窗口的关闭。

水位线用来平衡延迟性正确性。水位线告诉我们,在触发计算(例如关闭窗口并触发窗口计算)之前,我们需要等待事件多长时间。基于事件时间的操作符根据水位线来衡量系统的逻辑时间的进度。

设定水位线通常需要用到领域知识。举例来说,如果知道事件的迟到时间不会超过5秒,就可以将水位线标记时间设为收到的最大时间戳减去5秒。另一种做法是,采用一个Flink作业监控事件流,学习事件的迟到规律,并以此构建水位线生成模型

6.水位线的原理

水印生成器 WatermarkGenerator 内部会记录截止当前事件的 maxTimestamp(不是窗口内的最大时间戳)。

// 周期性调用的方法 onPeriodicEmit() 内部会发出 watermark
// maxOutOfOrderness 表示设置的乱序时间(WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofMillis(1))
output.emitWatermark(new Watermark( maxTimestamp - maxOutOfOrderness - 1));

当发出的 watermark 等于 window.getEnd() - 1时,触发 window 的 trigger 进行计算。

事件时间自增的数据流 maxOutOfOrderness 等于 0,乱序数据流是用户设置的。

7.窗口 trigger

trigger 是通过定时器实现的,处理时间的定时器触发是系统时间;事件时间的定时器触发是通过 watermark 触发的,当 watermark 超过定时器时间时,触发定时器执行 trigger 的,处理 window operator 。

EventTimeTrigger 判断触发的逻辑如下,当 window.maxTimestamp() <= ctx.getCurrentWatermark()时触发窗口计算

  • window.maxTimestamp() = window.getEnd() - 1

  • ctx.getCurrentWatermark() = 当前元素的最大时间戳 - maxOutOfOrderness - 1

也就是说:当前元素的最大时间戳 = window.getEnd() 时触发窗口计算,window 中的元素包含开始不包含结尾

/**
 * 每个元素都会调用,判断是否触发的逻辑
 */
@Override
public TriggerResult onElement(Object element, long timestamp, TimeWindow window, TriggerContext ctx) {
  System.out.println("element = " + element);
  if (window.maxTimestamp() <= ctx.getCurrentWatermark()) {
    // if the watermark is already past the window fire immediately
    System.out.println(TriggerResult.FIRE);
    return TriggerResult.FIRE;
  } else {
    ctx.registerEventTimeTimer(window.maxTimestamp());
    System.out.println(TriggerResult.CONTINUE);
    return TriggerResult.CONTINUE;
  }
}

8.定时器 timer

当定时器 timer 触发时,执行回调函数onTimer()。processElement()方法和onTimer()方法是同步(不是异步)方法,这样可以避免并发访问和操作状态。

针对每一个 key和 timestamp,只能注册一个定期器。

KeyedProcessFunction 默认将所有定时器的时间戳放在一个优先队列中。在Flink做检查点操作时,定时器也会被保存到状态后端中。

9.自定义 WatermarkStrategy

WatermarkStrategy.<Tuple2<String, Long>>forBoundedOutOfOrderness(Duration.ofMillis(1)).withTimestampAssigner((event, timestamp) -> event.f1));

自定义实现下面的例子,乱序时间处理,等同于

package com.zhangjian.learning;

import org.apache.flink.api.common.eventtime.TimestampAssigner;
import org.apache.flink.api.common.eventtime.TimestampAssignerSupplier;
import org.apache.flink.api.common.eventtime.Watermark;
import org.apache.flink.api.common.eventtime.WatermarkGenerator;
import org.apache.flink.api.common.eventtime.WatermarkGeneratorSupplier;
import org.apache.flink.api.common.eventtime.WatermarkOutput;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.datastream.WindowedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
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.time.Duration;

/**
 * watermark 分配
 * 时间戳抽取
 *
 *
 * @author zhangjian
 * @version 1.0.0
 * @since 2021/12/22 16:51
 */
public class WatermarkExample {
  public static void main(String[] args) throws Exception {
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    env.setParallelism(1);
    // 每隔 10 秒钟,Flink 会调用 WatermarkGenerator#onPeriodicEmit() 方法,产生一个水位线
    env.getConfig().setAutoWatermarkInterval(10000);

    // 输入元素格式如 (a,1)(a,4),(a,5)
    DataStreamSource<String> source = env.socketTextStream("localhost", 9999);

    // 转成元组
    SingleOutputStreamOperator<Tuple2<String, Long>> map = source.map(s -> {
      String[] split = s.split("\\s");
      return Tuple2.of(split[0], Long.parseLong(split[1]));
    }).returns(Types.TUPLE(Types.STRING, Types.LONG));

    // 分配水位线
    SingleOutputStreamOperator<Tuple2<String, Long>> watermarks =
        map.assignTimestampsAndWatermarks(
            new WatermarkStrategy<Tuple2<String, Long>>() {

              /**
               * 生成 watermark
               */
              @Override
              public WatermarkGenerator<Tuple2<String, Long>> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
                // 此方法的参数是延迟生成 watermark,时间戳为 6 的事件事件生成 5 的 watermark,参数为 0 代表允许 1 毫秒延迟
                return new BoundedOutOfOrdernessGenerator(Duration.ofMillis(1));
              }

              /**
               * 提取事件戳,作为生成 watermark 的字段
               * @return 为每个事件分配 事件时间 时间戳
               */
              @Override
              public TimestampAssigner<Tuple2<String, Long>> createTimestampAssigner(
                  TimestampAssignerSupplier.Context context) {
                return (element, recordTimestamp) -> element.f1;
              }
            });

            /*WatermarkStrategy.<Tuple2<String, Long>>forBoundedOutOfOrderness(Duration.ofMillis(1))
                .withTimestampAssigner((event, timestamp) -> event.f1));*/


    KeyedStream<Tuple2<String, Long>, String> keyedStream = watermarks.keyBy(s -> s.f0);

    WindowedStream<Tuple2<String, Long>, String, TimeWindow> window = keyedStream
        .window(TumblingEventTimeWindows.of(Time.milliseconds(5)));

    // 增量聚合窗口函数
    SingleOutputStreamOperator<Tuple2<String, Long>> reduce = window.reduce((a, b) -> Tuple2.of(a.f0, a.f1 + b.f1));

    SingleOutputStreamOperator<String> process = window.process(new MyProcessWindowFunction());

    process.print();

    env.execute();
  }

  /**
   * 自定义 有界无序 事件水位线的生成器
   */
  static class BoundedOutOfOrdernessGenerator implements WatermarkGenerator<Tuple2<String, Long>> {

    private long maxOutOfOrderness;

    private long maxTimestamp;

    public BoundedOutOfOrdernessGenerator(Duration maxOutOfOrdernes){
      this.maxOutOfOrderness = maxOutOfOrdernes.toMillis();
      this.maxTimestamp = Long.MIN_VALUE + maxOutOfOrdernes.toMillis() + 1;
    }

    /**
     * 此方法每个事件都会调用,更新内部字段 maxTimestamp
     *
     * @param event 进入的事件
     * @param eventTimestamp 事件事件, WatermarkStrategy#createTimestampAssigner() 提取出来的时间戳
     * @param output 输出
     */
    @Override
    public void onEvent(Tuple2<String, Long> event, long eventTimestamp, WatermarkOutput output) {
      /*System.out.println("事件 = " + event);
      System.out.println("事件事件 = " + eventTimestamp);*/
      maxTimestamp = Math.max(maxTimestamp, eventTimestamp);
      System.out.println("maxTimestamp = " + maxTimestamp);
    }

    /**
     * 调用此方法被周期性调用,发送 Watermarks
     * 调用间隔取决于 ExecutionConfig.getAutoWatermarkInterval()
     * 间隔时间是系统时间
     *
     * @param output 当前水位线
     */
    @Override
    public void onPeriodicEmit(WatermarkOutput output) {
      // System.out.println(LocalTime.now() + ", output = " + new Watermark(currentMaxTimestamp - maxOutOfOrderness - 1));
      // 发出的 watermark = 当前最大时间戳 - 最大乱序时间
      output.emitWatermark(new Watermark( maxTimestamp - maxOutOfOrderness - 1));
    }

  }

  /**
   * 全窗口处理函数: 所有的数据到齐了才触发计算
   */
  static class MyProcessWindowFunction extends ProcessWindowFunction<Tuple2<String, Long>, String, String, TimeWindow> {

    @Override
    public void process(String key, Context context, Iterable<Tuple2<String, Long>> input, Collector<String> out) {
      long count = 0;
      StringBuilder sb = new StringBuilder();
      for (Tuple2<String, Long> in : input) {
        sb.append(in);
        count++;
      }
      String result = String.format("key=%s, window=%s, count=%d, maxTimestamp=%d, currentWatermark=%d, 元素=%s",
          key,
          context.window(),
          count,
          context.window().maxTimestamp(), context.currentWatermark(), sb.toString());
      out.collect(result);
    }
  }
}

测试1:设置允许的乱序时间是0(单调递增事件)

输入数据nc -lk 9999

# 输入
a 1
a 4
a 5
# 触发窗口关闭,输出
maxTimestamp = 1
maxTimestamp = 4
maxTimestamp = 5
key=a, window=TimeWindow{start=0, end=5}, count=2, maxTimestamp=4, currentWatermark=4, 元素=(a,1)(a,4)
# 结论:currentWatermark == context.window().maxTimestamp() 时触发窗口计算;水位线生成器会在提出水位线函数的返回值上减 1,并不是直接使用

测试2:设置允许的乱序时间是1(单调递增事件)(copy 上面的代码)

输入数据nc -lk 9999

# 输入
a 1
a 4
a 5
a 6
# 触发窗口关闭,输出
maxTimestamp = 1
maxTimestamp = 4
maxTimestamp = 5
maxTimestamp = 6
key=a, window=TimeWindow{start=0, end=5}, count=2, maxTimestamp=4, currentWatermark=4, 元素=(a,1)(a,4)
# 结论:修改乱序时间本质上修改的是 watermark 的发送,比原本发送的小。

10.窗口生命周期

当一个事件来到窗口操作符,首先将会传给 WindowAssigner 来处理。WindowAssigner 决定了事件将被分配到哪些窗口。如果窗口不存在,WindowAssigner 将会创建一个新的窗口。所以不存在没有元素的窗口。

如果一个 window operator 接受了一个增量聚合函数作为参数,例如 ReduceFunction 或者 AggregateFunction,新到的元素将会立即被聚合,而聚合结果 result 将存储在 window 中。

如果 window operator 没有使用增量聚合函数,那么新元素将被添加到ListState中,ListState中保存了所有分配给窗口的元素。

新元素被添加到窗口时,这个新元素同时也被传给了 window 的 trigger。trigger 定义了 window 何时准备好求值,何时 window 被清空。trigger可以基于window被分配的元素和注册的定时器来对窗口的所有元素求值或者在特定事件清空window中所有的元素。

evictor是一个可选的组件,可以被注入到ProcessWindowFunction之前或者之后调用。evictor可以清除掉window中收集的元素。由于evictor需要迭代所有的元素,所以evictor只能使用在没有增量聚合函数作为参数的情况下。

stream
  .keyBy(...)
  .window(...)
 [.trigger(...)]
 [.evictor(...)]
  .reduce/aggregate/process(...)

注意:每个WindowAssigner都有一个默认的trigger。

11.GlobalWindow

GlobalWindow继承了Window,它的maxTimestamp方法与TimeWindow不同,TimeWindow有start和end属性,其maxTimestamp方法返回的是end-1;而GlobalWindow的maxTimestamp方法返回的是Long.MAX_VALUE;

全局窗口分配器将具有相同键的所有元素分配给同一个全局窗口。 此窗口方案仅在您还指定自定义触发器时才有用。

input
    .keyBy(<key selector>)
    .window(GlobalWindows.create())
    .<windowed transformation>(<window function>);
// GlobalWindow 的 maxTimestamp,永远不会关闭,所以必须自自定触发器
public class GlobalWindow extends Window {
		@Override
    public long maxTimestamp() {
        return Long.MAX_VALUE;
    }
}

// TimeWindow 的 maxTimestamp,等于 windowEnd - 1
public class TimeWindow extends Window {

	@Override
	public long maxTimestamp() {
		return end - 1;
	}
}

/* 
 * GlobalWindow 的窗口分配器

WindowAssigner<T, W extends Window> : 
类型参数:
<T> – 此 WindowAssigner 给窗口分配的元素类型。
<W> – 此 WindowAssigner 的 Window类型。
**/
public class GlobalWindows extends WindowAssigner<Object, GlobalWindow> {
  // 默认触发器:永远不会触发的触发器
  @Override
	public Trigger<Object, GlobalWindow> getDefaultTrigger(StreamExecutionEnvironment env) {
		return new NeverTrigger();
	}
  
  // 非事件时间
  @Override
	public boolean isEventTime() {
		return false;
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值