一文搞懂 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));
    }
}

实际案例:

// 允许 3 秒乱序
WatermarkStrategy<Event> strategy = WatermarkStrategy
    .<Event>forBoundedOutOfOrderness(Duration.ofSeconds(3))
    .withTimestampAssigner((event, ts) -> event.timestamp);

事件序列(乱序到达):
Time    Event-TS    MaxTS    Watermark (MaxTS - 3000 - 1)
----    --------    -----    -------------------------
0ms     5000        5000     999    (5000-3000-1)
100ms   4000        5000     999    (不变,4000 < 5000)
200ms   8000        8000     4999   (8000-3000-1)
300ms   6000        8000     4999   (不变,6000 < 8000)
400ms   10000       10000    6999   (10000-3000-1)

解释:
- Watermark=4999 时,保证 <= 4999 的事件都到了
- Event(6000)Watermark=4999 后到达 → OK(在窗口内)
- 如果 Event(3000)Watermark=4999 后到达 → Late Event!

4. Watermark 在运行时的传播机制

4.1 Source 算子中的 Watermark 生成

// flink-runtime/.../TimestampsAndWatermarksOperator.java
public class TimestampsAndWatermarksOperator<T> extends AbstractStreamOperator<T> {
    
    private WatermarkGenerator<T> watermarkGenerator;
    private TimestampAssigner<T> timestampAssigner;
    private long watermarkInterval;  // 默认 200ms

    @Override
    public void open() throws Exception {
        // 1. 创建时间戳提取器和 Watermark 生成器
        timestampAssigner = watermarkStrategy.createTimestampAssigner(...);
        watermarkGenerator = watermarkStrategy.createWatermarkGenerator(...);
        
        // 2. 注册周期性 Watermark 发送定时器
        watermarkInterval = getExecutionConfig().getAutoWatermarkInterval();
        if (watermarkInterval > 0) {
            long now = getProcessingTimeService().getCurrentProcessingTime();
            getProcessingTimeService().registerTimer(now + watermarkInterval, this);
        }
    }

    @Override
    public void processElement(StreamRecord<T> element) throws Exception {
        // 1. 提取时间戳
        long newTimestamp = timestampAssigner.extractTimestamp(
            element.getValue(), 
            element.getTimestamp()
        );
        element.setTimestamp(newTimestamp);
        
        // 2. 转发元素到下游
        output.collect(element);
        
        // 3. 通知 Watermark 生成器(可能立即发送 Punctuated Watermark)
        watermarkGenerator.onEvent(element.getValue(), newTimestamp, wmOutput);
    }

    @Override
    public void onProcessingTime(long timestamp) throws Exception {
        // 周期性触发(每 200ms)
        watermarkGenerator.onPeriodicEmit(wmOutput);
        
        // 注册下一次触发
        long now = getProcessingTimeService().getCurrentProcessingTime();
        getProcessingTimeService().registerTimer(now + watermarkInterval, this);
    }
}

执行流程:

Source 产生数据:
┌─────────────────────────────────────────────────┐
│  [Record]processElement()                    │
│      ↓                                          │
│  1. 提取时间戳 (TimestampAssigner)               │
│  2. 设置到 StreamRecord                         │
│  3. 转发到下游                                   │
│  4. onEvent() - 通知 WatermarkGenerator         │
│                                                 │
│  [Timer 200ms]onProcessingTime()             │
│      ↓                                          │
│  onPeriodicEmit() - 周期性发送 Watermark        │
└─────────────────────────────────────────────────┘

4.2 多输入算子的 Watermark 合并

// flink-runtime/.../StatusWatermarkValve.java
public class StatusWatermarkValve {
    
    // 每个输入通道的状态
    private final List<Map<Integer, SubpartitionStatus>> subpartitionStatuses;
    
    // 使用优先队列找最小 Watermark
    private final HeapPriorityQueue<SubpartitionStatus> alignedSubpartitionStatuses;
    
    // 上次输出的 Watermark
    private long lastOutputWatermark;

    public void inputWatermark(Watermark watermark, int channelIndex, DataOutput<?> output) {
        SubpartitionStatus status = subpartitionStatuses.get(channelIndex).get(...);
        
        // 1. 更新该通道的 Watermark
        if (watermark.getTimestamp() > status.watermark) {
            status.watermark = watermark.getTimestamp();
            
            // 2. 调整优先队列
            if (status.isWatermarkAligned) {
                adjustAlignedSubpartitionStatuses(status);
            }
            
            // 3. 计算新的最小 Watermark 并输出
            findAndOutputNewMinWatermarkAcrossAlignedSubpartitions(output);
        }
    }
    
    private void findAndOutputNewMinWatermarkAcrossAlignedSubpartitions(DataOutput<?> output) {
        // 从优先队列取最小值
        SubpartitionStatus minStatus = alignedSubpartitionStatuses.peek();
        
        if (minStatus != null && minStatus.watermark > lastOutputWatermark) {
            lastOutputWatermark = minStatus.watermark;
            // 向下游发送新的 Watermark
            output.emitWatermark(new Watermark(lastOutputWatermark));
        }
    }
}

多输入合并规则:

算子有 2 个输入流:

Input 1:  ─── W(100) ─── W(150) ─── W(200) ───>
Input 2:  ─── W(80) ──── W(120) ─── W(180) ───>
            ↓            ↓            ↓
合并输出:   W(80)        W(120)       W(180)
           (取最小)      (取最小)      (取最小)

原因:确保所有输入的 Watermark 都推进到某个时间点
     才能保证该时间点前的事件都已到达

5. 空闲源(Idle Source)处理

5.1 为什么需要 Idle 检测?

问题场景:
┌────────────────────────────────────────────────┐
│  Kafka Partition 1:  高流量,Watermark 快速推进 │
│  [W(1000)] [W(2000)] [W(3000)] ...             │
│                                                │
│  Kafka Partition 2:  低流量,长时间无数据        │
│  [W(100)] ... (空闲 5分钟) ...                 │
│                                                │
│  合并结果:                                     │
│  min(3000, 100) = 100 ← 被 Partition 2 阻塞!  │
│                                                │
│  后果:下游窗口无法触发,整个作业停滞!           │
└────────────────────────────────────────────────┘

解决方案:将 Partition 2 标记为 Idle,合并时忽略它

5.2 Idle 检测源码实现

// flink-core/.../WatermarksWithIdleness.java
@Public
public class WatermarksWithIdleness<T> implements WatermarkGenerator<T> {
    
    private final WatermarkGenerator<T> watermarks;  // 包装的原始生成器
    private final IdlenessTimer idlenessTimer;       // 空闲检测定时器
    private boolean isIdleNow = false;

    @Override
    public void onEvent(T event, long eventTimestamp, WatermarkOutput output) {
        // 1. 委托给原始生成器
        watermarks.onEvent(event, eventTimestamp, output);
        
        // 2. 记录活动,重置空闲计时器
        idlenessTimer.activity();
        isIdleNow = false;
    }

    @Override
    public void onPeriodicEmit(WatermarkOutput output) {
        // 检查是否空闲
        if (idlenessTimer.checkIfIdle()) {
            if (!isIdleNow) {
                output.markIdle();  // 标记为空闲
                isIdleNow = true;
            }
        } else {
            watermarks.onPeriodicEmit(output);  // 正常发送 Watermark
        }
    }
    
    static class IdlenessTimer {
        private long counter;           // 活动计数器
        private long lastCounter;       // 上次检查的计数
        private long startOfInactivityNanos;  // 不活跃开始时间
        private final long maxIdleTimeNanos;  // 空闲超时

        public void activity() {
            counter++;  // 每次事件到来,计数器+1
        }

        public boolean checkIfIdle() {
            if (counter != lastCounter) {
                // 有新活动,重置
                lastCounter = counter;
                startOfInactivityNanos = 0L;
                return false;
            } else if (startOfInactivityNanos == 0L) {
                // 首次检测到无活动,开始计时
                startOfInactivityNanos = clock.relativeTimeNanos();
                return false;
            } else {
                // 检查是否超时
                return clock.relativeTimeNanos() - startOfInactivityNanos > maxIdleTimeNanos;
            }
        }
    }
}

使用方式:

WatermarkStrategy<Event> strategy = WatermarkStrategy
    .<Event>forBoundedOutOfOrderness(Duration.ofSeconds(3))
    .withIdleness(Duration.ofMinutes(1))  // 1分钟无数据则标记为空闲
    .withTimestampAssigner((event, ts) -> event.timestamp);

空闲状态传播:

Partition 1:  [Active, W(1000)]
Partition 2:  [Idle]          ← 被忽略
            ↓
合并输出:    W(1000)          (只考虑 Active 分区)Partition 2 恢复数据:
Partition 2:  [Active, W(500)] ← 重新参与合并
            ↓
合并输出:    min(1000, 500) = W(500)

6. Watermark Alignment(对齐)

6.1 解决的问题

问题:多个 Source 速度不一致导致数据倾斜

Source A ():  W(10000) ─────────> 大量数据被处理
Source B ():  W(100) ───────────> 阻塞整体进度

Watermark Alignment 目标:
- 限制快的 Source,使其不超过慢的 Source 太多
- 避免过多的状态积压和内存压力

6.2 对齐机制

// flink-core/.../WatermarksWithWatermarkAlignment.java
final class WatermarksWithWatermarkAlignment<T> implements WatermarkStrategy<T> {
    
    private final String watermarkGroup;              // 对齐组名称
    private final Duration maxAllowedWatermarkDrift;  // 最大允许漂移
    private final Duration updateInterval;            // 更新间隔

    // 工作原理:
    // 1. 同组的所有 Source 定期向 JobManager 报告当前 Watermark
    // 2. JobManager 计算组内最小 Watermark (minWatermark)
    // 3. 如果某个 Source 的 Watermark > minWatermark + maxDrift
    //    则暂停该 Source 的数据消费
    // 4. 等待其他 Source 追上后再恢复
}

配置示例:

WatermarkStrategy<Event> strategy = WatermarkStrategy
    .<Event>forBoundedOutOfOrderness(Duration.ofSeconds(3))
    .withWatermarkAlignment(
        "my-watermark-group",           // 组名
        Duration.ofSeconds(20)          // 最大漂移 20 秒
    );

对齐效果:

不对齐:
Source A:  W(0)W(1000)W(2000)W(3000)...
Source B:  W(0)W(100) ── W(200) ── W(300) ── ...
           产生大量中间状态!

对齐(maxDrift=500):
Source A:  W(0)W(500)[暂停]W(700)W(1200)[暂停] ...
Source B:  W(0)W(100)W(200)W(300)W(700)...
           Source A 被限速,等待 B 追上

7. Watermark 与窗口的交互

7.1 窗口触发条件

// 窗口触发逻辑(伪代码)
class WindowOperator {
    
    @Override
    public void processWatermark(Watermark mark) {
        // 遍历所有活跃窗口
        for (Window window : activeWindows) {
            // 当 Watermark >= 窗口结束时间,触发窗口
            if (mark.getTimestamp() >= window.maxTimestamp()) {
                // 1. 触发窗口计算
                triggerWindow(window);
                
                // 2. 清理窗口状态
                if (!allowedLateness) {
                    cleanupWindow(window);
                }
            }
        }
        
        // 转发 Watermark 到下游
        output.emitWatermark(mark);
    }
}

示例:

窗口: [10:00:00, 10:00:10)  (10秒滚动窗口)

事件流:
t=10:00:02  Event(user1, 100)  ─┐
t=10:00:05  Event(user2, 200)   ├─> 进入窗口
t=10:00:08  Event(user1, 150)  ─┘

Watermark 推进:
W(10:00:03) → 窗口未触发
W(10:00:07) → 窗口未触发
W(10:00:10) → 窗口触发! (Watermark >= 10:00:10)
            → 输出: sum = 450

7.2 迟到数据处理

// 允许 1 分钟延迟
stream
    .keyBy(...)
    .window(TumblingEventTimeWindows.of(Time.minutes(1)))
    .allowedLateness(Time.minutes(1))  // 允许迟到
    .sideOutputLateData(lateDataTag)   // 迟到数据输出到侧输出流
    .sum("value");

时间线:
Window: [10:00, 10:01)
W(10:01) 到达 → 触发窗口,但不删除状态
W(10:02) 到达 → 清理窗口状态

10:00:30 的事件在 W(10:01:30) 前到达 → 更新窗口结果
10:00:30 的事件在 W(10:02) 后到达 → 发送到侧输出流

8. 总结:Watermark 设计精髓

核心设计原则

  1. 渐进性保证:Watermark 提供的是"尽力而为"的保证,允许迟到事件
  2. 分布式协调:通过取最小值合并,确保全局一致性
  3. 灵活性:支持多种生成策略和扩展机制
  4. 性能优化:
  • 周期性生成减少开销
  • Idle 检测避免阻塞
  • Alignment 控制状态膨胀

关键实现技巧

// 1. 分层设计
WatermarkStrategy  → 策略定义(用户层)
WatermarkGenerator → 生成逻辑(实现层)
StatusWatermarkValve → 多流合并(运行时层)

// 2. 装饰器模式
baseStrategy
  .withIdleness(...)        // 添加空闲检测
  .withAlignment(...)       // 添加对齐
  .withTimestampAssigner()  // 添加时间戳提取

// 3. 时间语义隔离
Watermark(事件时间) + ProcessingTime(系统时间) 
→ 混合使用,各司其职
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值