4. Watermark的生成策略:周期性生成(Periodic)与断点生成(Punctuated)

Watermark 怎么来?说白了,就是 Flink 根据你指定的规则,在数据流里「插」进去的特殊时间戳。我刚开始接触 Flink 时,总觉得 Watermark 是个很玄乎的东西,后来自己动手写了几次生成逻辑,才明白它其实就是个「进度条」——告诉系统:到这个点了,再晚的数据我就当乱序处理了。

Flink 提供了两种生成策略:周期性生成(Periodic) 和 断点生成(Punctuated)。这两种策略各有各的适用场景,选错了可能会让你吃不少苦头。

4.1 周期性生成(Periodic Watermark)

周期性生成,顾名思义,就是每隔一段时间自动发射一个 Watermark。这是最常用的策略,也是我项目中用得最多的方式。

它的核心逻辑是:定时触发。你设定一个间隔(比如 200 毫秒),Flink 就会按这个频率去检查当前最大的 Event Time,然后生成一个 Watermark。

关键点:周期性 Watermark 的发射频率由 ExecutionConfig.setAutoWatermarkInterval() 控制,默认是 200ms。这个值不是越小越好,也不是越大越好,得根据你的业务容忍度来调。

4.1.1 实现方式

你需要实现 AssignerWithPeriodicWatermarks 接口。看个例子:

public class MyPeriodicAssigner implements AssignerWithPeriodicWatermarks<MyEvent> {
    private long maxSeenTimestamp = Long.MIN_VALUE;
    private final long maxOutOfOrderness = 5000L; // 5秒乱序容忍

    @Nullable
    @Override
    public Watermark getCurrentWatermark() {
        // 生成 Watermark = 最大时间戳 - 乱序容忍
        return new Watermark(maxSeenTimestamp - maxOutOfOrderness);
    }

    @Override
    public long extractTimestamp(MyEvent element, long previousElementTimestamp) {
        long timestamp = element.getEventTime();
        // 更新最大时间戳
        maxSeenTimestamp = Math.max(maxSeenTimestamp, timestamp);
        return timestamp;
    }
}

这段代码里,getCurrentWatermark() 就是周期性调用的方法。每次调用时,它都会基于当前看到的最大时间戳减去一个「容忍度」,生成 Watermark。

我的经验:我曾经在一个实时大屏项目里,把 maxOutOfOrderness 设成了 10 秒,结果数据延迟了 10 秒才出结果。业务方直接炸了。后来我改成 2 秒,配合 Kafka 的日志保留时间,效果好了很多。记住:容忍度越大,延迟越高,但数据越完整。

4.1.2 适用场景

  • 数据源稳定:比如 Kafka 消费的日志数据,乱序程度相对可控。
  • 对延迟不敏感:可以接受几秒到几十秒的延迟,换取更高的数据完整性。
  • 需要统一处理:所有分区共享同一个 Watermark 生成逻辑。

4.2 断点生成(Punctuated Watermark)

断点生成就有点「看心情」了。它不是定时发射,而是每来一条数据就判断一次——要不要发射 Watermark?

这种策略适合那些数据本身携带「标记」的场景。比如,数据流里每隔一段时间会有一条特殊的「水位线」数据,告诉你:后面的数据时间戳不会比这条更小了。

4.2.1 实现方式

你需要实现 AssignerWithPunctuatedWatermarks 接口:

public class MyPunctuatedAssigner implements AssignerWithPunctuatedWatermarks<MyEvent> {
    private long maxSeenTimestamp = Long.MIN_VALUE;
    private final long maxOutOfOrderness = 3000L;

    @Nullable
    @Override
    public Watermark checkAndGetNextWatermark(MyEvent lastElement, long extractedTimestamp) {
        // 只有特定类型的数据才触发 Watermark
        if (lastElement.isWatermarkMarker()) {
            return new Watermark(extractedTimestamp - maxOutOfOrderness);
        }
        return null; // 不发射
    }

    @Override
    public long extractTimestamp(MyEvent element, long previousElementTimestamp) {
        long timestamp = element.getEventTime();
        maxSeenTimestamp = Math.max(maxSeenTimestamp, timestamp);
        return timestamp;
    }
}

注意看 checkAndGetNextWatermark 方法:它返回 null 表示不发射 Watermark,返回 Watermark 对象才发射。这个判断逻辑完全由你控制。

避坑指南:我曾经在一个 IoT 项目里用了断点生成,结果上游数据源偶尔会连续几秒没有「标记数据」,导致 Watermark 一直不更新,窗口迟迟不触发。最后数据积压到 OOM。所以,用断点生成前,一定要确保你的标记数据是可靠且频繁的。

4.2.2 适用场景

  • 数据自带标记:比如金融交易流里,每条结算数据都带一个「批次结束」标志。
  • 低延迟要求:一旦收到标记,立即推进 Watermark,窗口马上触发。
  • 数据量极大:周期性生成每次都要遍历所有分区,断点生成可以按需触发,减少计算开销。

4.3 两种策略的对比

我整理了一张表,方便你快速对比:

对比维度 周期性生成(Periodic) 断点生成(Punctuated)
触发方式 定时触发(如每 200ms) 每来一条数据判断一次
实现接口 AssignerWithPeriodicWatermarks AssignerWithPunctuatedWatermarks
延迟控制 延迟固定(取决于间隔) 延迟不固定(取决于标记频率)
计算开销 较低(定时批量处理) 较高(每条数据都判断)
适用数据 乱序程度稳定、数据量大 数据自带标记、低延迟要求
典型场景 日志分析、监控告警 金融交易、IoT 事件流

4.4 核心逻辑流程图

下面这张图展示了两种策略的核心流程对比,我建议你仔细看看:

Watermark 生成策略对比流程图 Watermark 生成策略核心流程 数据流 周期性生成 定时触发(如每200ms) 断点生成 每来一条数据判断 getCurrentWatermark() checkAndGetNextWatermark() 返回 Watermark 对象 (可能为 null) 返回 Watermark 或 null (按条件判断) Watermark 注入数据流 两种策略最终都会将 Watermark 注入数据流 区别在于触发时机和判断逻辑

4.5 如何选择?我的建议

说实话,没有的好坏。我个人的经验是:

  • 90% 的场景用周期性生成。它简单、稳定、可控。你只需要调好 maxOutOfOrderness 和发射间隔,基本不会出大问题。
  • 剩下 10% 的场景用断点生成。比如你明确知道数据流里每隔几条数据就有一个「水位标记」,而且你对延迟要求极高(毫秒级)。

一个小技巧:如果你不确定用哪种,可以先从周期性生成开始。等上线后观察 Watermark 的推进情况,如果发现延迟太大,再考虑切换到断点生成。别一上来就搞复杂的策略,容易翻车。

4.6 避坑总结

最后,我把自己踩过的坑整理一下:

  1. 周期性生成的时间间隔别设太小。我曾经设成 50ms,结果 CPU 直接飙到 90%。后来改成 200ms,稳如老狗。
  2. 断点生成一定要有兜底。如果标记数据迟迟不来,Watermark 就卡住了。可以加一个超时机制,强制发射。
  3. 多分区场景要小心。Flink 的 Watermark 是取所有分区的最小值。如果一个分区迟迟不推进,整个任务的 Watermark 都会卡住。
  4. 测试时用真实数据。模拟数据往往太完美,真实数据的乱序程度会让你怀疑人生。

嗯,Watermark 的生成策略就讲到这里。记住:选对策略,你的实时任务就成功了一半。