3. Watermark 概念:Watermark 是什么?如何表示数据流的进度?

好,咱们进入 Event Time 处理的核心——Watermark。

说实话,我刚接触 Flink 时,对 Watermark 的理解也绕了好一阵子。它不像窗口、算子那么直观。你想想看,数据流是源源不断的,系统怎么知道「某个时间点之前的数据都到齐了」?

答案是:它不知道。但 Watermark 给了它一个「合理的猜测」。

3.1 Watermark 的本质:一个时间戳断言

Watermark 是什么?说白了,它是一个插入到数据流中的特殊标记。这个标记携带一个时间戳 T,它的含义是:「我断言,所有事件时间小于等于 T 的数据,都已经到达了」。

注意,是「断言」,不是「保证」。在分布式系统中,你永远无法 100% 保证数据不会迟到。Watermark 只是告诉 Flink:「到这个点了,你可以放心地触发窗口计算了」。

核心理解:

  • Watermark 是一个时间戳,不是一条普通数据
  • 它表示「进度」,而不是「数据」
  • 它告诉 Flink:在这个时间点之前的数据,大概率已经到齐了

我在项目中遇到过一位同事,他把 Watermark 理解成「数据的时间戳」,结果窗口一直不触发。嗯,这里要注意:Watermark 和数据的事件时间是两码事。

3.2 Watermark 如何表示数据流的进度?

数据流的进度,不能靠「等」,要靠「推」。Watermark 就是那个推动者。

我们来看一个简单的例子。假设数据流中有三条记录:

数据 A: 事件时间 = 10:00
数据 B: 事件时间 = 10:05
数据 C: 事件时间 = 10:03

如果 Watermark 策略是「当前最大事件时间减去 2 秒」,那么:

  • 收到 A 后,Watermark = 10:00 - 2s = 09:58
  • 收到 B 后,Watermark = 10:05 - 2s = 10:03
  • 收到 C 后,Watermark = 10:05 - 2s = 10:03(因为最大事件时间还是 10:05)

当 Watermark 推进到 10:03 时,Flink 就知道:所有事件时间 ≤ 10:03 的数据都已经到了。这时候,如果有一个 [10:00, 10:05) 的窗口,就可以触发了。

我的习惯: 调试 Watermark 时,我会在 Flink Web UI 的 Watermark 监控页面上盯着看。如果 Watermark 一直不涨,大概率是数据源没设置事件时间,或者 Watermark 策略写错了。

3.3 Watermark 的两种生成方式

Flink 提供了两种 Watermark 生成方式,我分别说说。

方式 特点 适用场景
周期性生成 每隔 N 毫秒生成一次 Watermark 数据流比较稳定,延迟可控
断点式生成 每来一条数据都可能生成 Watermark 数据流稀疏,或需要精确控制

周期性生成是默认方式,也是我推荐大多数场景使用的。你只需要实现 AssignerWithPeriodicWatermarks 接口,或者用 Flink 提供的 WatermarkStrategy 工具类。

// 周期性 Watermark 示例
DataStream<MyEvent> stream = env
    .addSource(mySource)
    .assignTimestampsAndWatermarks(
        WatermarkStrategy.<MyEvent>forBoundedOutOfOrderness(Duration.ofSeconds(5))
            .withTimestampAssigner((event, timestamp) -> event.getEventTime())
    );

这段代码的意思是:允许数据最多迟到 5 秒。Watermark 会随着数据不断推进,但始终比当前最大事件时间慢 5 秒。

我曾经踩过的坑: 把 forBoundedOutOfOrderness 的参数设得太大,比如 1 小时。结果窗口要等 1 小时才触发,业务方直接炸了。记住:这个参数是「容忍乱序的程度」,不是「数据延迟的保证」。设得越大,窗口触发越晚,实时性越差。

3.4 Watermark 的传播机制

Watermark 在算子之间是怎么传递的?

每个算子都会维护一个「当前 Watermark」。当它从上游收到新的 Watermark 时,会取所有输入分区的最小值作为自己的 Watermark,然后广播给下游。

为什么取最小值?因为只要有一个分区还没到,就不能断言全局进度。你想想看,如果两个分区分别推进到 10:00 和 10:05,取最小值 10:00 才是安全的。

Watermark 传播机制 上游算子 A Watermark: 10:00 上游算子 B Watermark: 10:05 上游算子 C Watermark: 10:03 取最小值:min(10:00, 10:05, 10:03) = 10:00 下游算子收到 Watermark: 10:00 关键:Watermark 取所有输入分区的最小值,保证全局进度安全

3.5 Watermark 与窗口的关系

Watermark 和窗口是「搭档」关系。窗口定义了「对哪些数据做计算」,Watermark 决定了「什么时候触发计算」。

具体来说:

  • 窗口的结束时间 = 窗口起始时间 + 窗口大小
  • 当 Watermark ≥ 窗口结束时间时,窗口触发计算
  • 触发后,窗口内的数据被处理,然后窗口关闭

举个例子,一个 [10:00, 10:05) 的滚动窗口:

  • 窗口结束时间 = 10:05
  • 当 Watermark 推进到 ≥ 10:05 时,窗口触发
  • 所有事件时间在 [10:00, 10:05) 的数据都会被计算

避坑指南: 我曾经遇到一个场景,Watermark 一直不推进,窗口死活不触发。排查了半天,发现是数据源的时间戳字段解析错了,所有事件时间都是 1970-01-01。Watermark 策略基于这些错误的时间戳,自然永远达不到窗口的结束时间。所以,先确认事件时间解析正确,再调 Watermark。

3.6 总结一下

Watermark 是 Flink Event Time 处理的「进度条」。它告诉系统:到这个点了,之前的数据可以放心处理了。

记住三个关键点:

  1. Watermark 是一个断言,不是保证。它说「数据到齐了」,但实际可能还有迟到数据。
  2. Watermark 取所有分区的最小值,保证全局进度安全。
  3. Watermark 推进窗口触发,两者配合才能完成基于事件时间的计算。