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 核心逻辑流程图
下面这张图展示了两种策略的核心流程对比,我建议你仔细看看:
4.5 如何选择?我的建议
说实话,没有的好坏。我个人的经验是:
- 90% 的场景用周期性生成。它简单、稳定、可控。你只需要调好
maxOutOfOrderness和发射间隔,基本不会出大问题。 - 剩下 10% 的场景用断点生成。比如你明确知道数据流里每隔几条数据就有一个「水位标记」,而且你对延迟要求极高(毫秒级)。
一个小技巧:如果你不确定用哪种,可以先从周期性生成开始。等上线后观察 Watermark 的推进情况,如果发现延迟太大,再考虑切换到断点生成。别一上来就搞复杂的策略,容易翻车。
4.6 避坑总结
最后,我把自己踩过的坑整理一下:
- 周期性生成的时间间隔别设太小。我曾经设成 50ms,结果 CPU 直接飙到 90%。后来改成 200ms,稳如老狗。
- 断点生成一定要有兜底。如果标记数据迟迟不来,Watermark 就卡住了。可以加一个超时机制,强制发射。
- 多分区场景要小心。Flink 的 Watermark 是取所有分区的最小值。如果一个分区迟迟不推进,整个任务的 Watermark 都会卡住。
- 测试时用真实数据。模拟数据往往太完美,真实数据的乱序程度会让你怀疑人生。
嗯,Watermark 的生成策略就讲到这里。记住:选对策略,你的实时任务就成功了一半。