一文搞懂 Flink Watermark 设计理念(附源码
1. Watermark 的核心设计理念
1.1 为什么需要 Watermark?
问题背景:Event Time vs Processing Time
场景:统计每小时的交易金额
Processing Time 方式:
系统时间 10:00-11:00 的所有事件 → 10点窗口
问题:网络延迟、重放历史数据时结果不确定
Event Time 方式:
事件时间戳 10:00-11:00 的所有事件 → 10点窗口
问题:如何知道"10点的所有事件都到齐了"?
↓
需要 Watermark!
1.2 Watermark 的语义定义
// flink-core/src/main/java/org/apache/flink/api/common/eventtime/Watermark.java
/**
* Watermark(t) 声明:
* 事件时间已经到达时间 t,意味着不应该再有时间戳 <= t 的事件到来
*
* 如果后续还有 timestamp <= t 的事件,则为"迟到事件"(Late Event)
*/
public final class Watermark implements Serializable {
// 本质就是一个时间戳
private final long timestamp; // 毫秒时间戳
public static final Watermark MAX_WATERMARK = new Watermark(Long.MAX_VALUE);
}
关键概念:
Watermark(T) 的含义:
┌─────────────────────────────────────────────────┐
│ 时间轴: ... ─────|─────|─────|─────────> │
│ T-2 T-1 T 当前Watermark│
│ │
│ 保证:timestamp <= T 的事件"应该"都已到达 │
│ 允许:后续可能有迟到事件 (Late Events) │
└─────────────────────────────────────────────────┘
2. Watermark 核心接口设计
2.1 设计的三层抽象
// 层次1: WatermarkStrategy - 策略工厂
@Public
public interface WatermarkStrategy<T> {
// 创建 Watermark 生成器
WatermarkGenerator<T> createWatermarkGenerator(Context context);
// 创建时间戳提取器
TimestampAssigner<T> createTimestampAssigner(Context context);
}
// 层次2: WatermarkGenerator - 生成逻辑
@Public
public interface WatermarkGenerator<T> {
// 每个事件到来时调用
void onEvent(T event, long eventTimestamp, WatermarkOutput output);
// 周期性调用(默认200ms)
void onPeriodicEmit(WatermarkOutput output);
}
// 层次3: WatermarkOutput - 输出接口
public interface WatermarkOutput {
void emitWatermark(Watermark watermark);
void markIdle(); // 标记空闲
void markActive(); // 标记活跃
}
设计优势:
- 分离关注点:时间戳提取 vs Watermark 生成
- 灵活性:支持 Punctuated 和 Periodic 两种模式的统一
- 可组合:通过装饰器模式扩展功能(Idleness、Alignment)
3. Watermark 生成策略(源码实现)
3.1 策略一:单调递增时间戳
// 适用场景:事件严格按时间顺序到达(如日志文件)
@Public
public class AscendingTimestampsWatermarks<T> implements WatermarkGenerator<T> {
private long maxTimestamp = Long.MIN_VALUE + 1;
@Override
public void onEvent(T event, long eventTimestamp, WatermarkOutput output) {
// 每个事件到达,更新最大时间戳
maxTimestamp = Math.max(maxTimestamp, eventTimestamp);
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// 周期性发送 Watermark = 当前最大时间戳 - 1
output.emitWatermark(new Watermark(maxTimestamp - 1));
}
}
核心思想:
事件流: [ts=100] [ts=105] [ts=110] [ts=115] ...
↓ ↓ ↓ ↓
Watermark: 99 104 109 114
(紧跟最大时间戳)
3.2 策略二:有界乱序(最常用)
// flink-core/.../BoundedOutOfOrdernessWatermarks.java
@Public
public class BoundedOutOfOrdernessWatermarks<T> implements WatermarkGenerator<T> {
private long maxTimestamp; // 见过的最大时间戳
private final long outOfOrdernessMillis; // 允许的最大乱序时间
public BoundedOutOfOrdernessWatermarks(Duration maxOutOfOrderness) {
this.outOfOrdernessMillis = maxOutOfOrderness.toMillis();
// 初始化为最小值 + 乱序时间 + 1
this.maxTimestamp = Long.MIN_VALUE + outOfOrdernessMillis + 1;
}
@Override
public void onEvent(T event, long eventTimestamp, WatermarkOutput output) {
// 持续跟踪最大时间戳
maxTimestamp = Math.max(maxTimestamp, eventTimestamp);
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// Watermark = 最大时间戳 - 允许乱序时间 - 1
output.emitWatermark(new Watermark(maxTimestamp - outOfOrdernessMillis - 1));