文章目录
- 一、三种时间语义
- [二、为什么需要 Watermark?](#二、为什么需要 Watermark?)
- [三、Watermark 如何生成?](#三、Watermark 如何生成?)
- [四、Watermark 的本质作用](#四、Watermark 的本质作用)
- 五、何时触发窗口计算?
- [六、真实案例分析:Watermark 不触发窗口的原因](#六、真实案例分析:Watermark 不触发窗口的原因)
- [七、迟到事件(Late Event)如何判断?](#七、迟到事件(Late Event)如何判断?)
- [八、Flink 示例代码(直接可用)](#八、Flink 示例代码(直接可用))
- 九、总结
在 Apache Flink 做实时流计算时,很多人会被以下核心概念绕晕:
- 🟡 时间语义(Time Semantics)
- 🟡 Watermark(水印)
- 🟡 窗口(Window)
这三个概念是 Flink 基于 事件时间(Event Time) 做正确统计与窗口触发的基础。本文用最简单的语言和真实案例解释它们之间的关系,帮助你彻底理解。
一、三种时间语义
Flink 支持三种时间语义:
| 时间语义 | 含义 | 适用场景 |
|---|---|---|
| Processing Time | 系统当前时间 | 延迟低但不可复现 |
| Ingestion Time | 进入 Flink 的时间 | 折中方案 |
| Event Time | 事件自身发生时间 | 最符合业务语义 |
事件时间代表事件真实产生时间,非常适合业务窗口统计场景。
二、为什么需要 Watermark?
现实中事件多是乱序到达的,例如:
- 事件在设备 10:01 产生,但因为网络延迟 10:03 才到 Flink;
- 多个分区并行消费时,消息乱序更明显;
为了能 按 event time 做窗口计算 ,Flink 引入了 Watermark 机制,它告诉系统:
我认为事件时间已经推进到某个时间点了,时间 ≤ Watermark 的事件大概率已到达,可以安全触发窗口计算了。
Watermark 本身并不是事件,它是 Flink 内部推进事件时间的逻辑标记。
三、Watermark 如何生成?
最常见的生成方式是基于允许乱序的最大延迟:
java
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((event, ts) -> event.timestamp)
)
它的含义:
✔ 从事件中提取事件时间(event.timestamp)
✔ 最大允许乱序时间 = 5s
✔ Watermark = maxSeenEventTime − allowedLateness(最大观察到的事件时间减去允许的乱序延迟)
四、Watermark 的本质作用
Watermark 的核心是推进事件时间,让 Flink 知道:
当前事件时间已经推进到某个时间点
它允许系统在面对乱序事件时,不必无限等待,而是在认为大部分事件已到达后触发计算。
五、何时触发窗口计算?
Flink 中时间窗口(如 Tumbling Window / Sliding Window)触发条件非常简单:
当 Watermark ≥ 窗口结束时间 时,触发窗口计算
📌 这条规则是 Flink 事件时间窗口触发的核心逻辑,不是按 Processing Time 触发的。
六、真实案例分析:Watermark 不触发窗口的原因
假设我们设置:
最大乱序时间 = 5s
窗口大小 = 5s(即 [0--5),[5--10),[10--15) ...)
并接收到以下事件(乱序):
| 顺序 | 事件 | eventTime(秒) |
|---|---|---|
| 1 | A | 1 |
| 2 | B | 6 |
| 3 | C | 4 |
| 4 | D | 8 |
| 5 | E | 11 |
| 6 | F | 7 |
| 7 | G | 12 |
| 8 | H | 10 |
我们根据上述 Watermark 生成规则,计算得到的 Watermark:
| 到达事件 | maxSeenEventTime | Watermark = max − 5 |
|---|---|---|
| A | 1 | −4 |
| B | 6 | 1 |
| C | 6 | 1 |
| D | 8 | 3 |
| E | 11 | 6 |
| F | 11 | 6 |
| G | 12 | 7 |
| H | 12 | 7 |
🟦 窗口触发结果(正确版)
📌 [0--5s) 窗口
停止时间 = 5
当 Watermark 推进到 6 时:
6 ≥ 5 → 触发 [0--5s) 窗口
该窗口包含事件:
✔ A(1s), C(4s)
📌 [5--10s) 窗口
结束时间 = 10
现有 Watermark 最大:
7 < 10 → 不会触发
该窗口包含:
✔ B(6s), D(8s), F(7s)
但它 永远不会被触发,因为没有更多更高的事件时间推进 Watermark ≥ 10。
📌 [10--15s) 窗口
结束时间 = 15
当前最大 Watermark 只有 7:
7 < 15 → 不触发
包含:
✔ E(11s), H(10s), G(12s)
无法触发。
🔍 为什么会出现这种情况?
核心在于 Watermark 的推进受最大乱序时间约束:
Watermark = maxSeenEventTime − allowedLateness
这里:
maxSeenEventTime = 12
allowedLateness = 5
→ Watermark = 7
由于 Watermark 受允许乱序时间减值影响,它无法跨过窗口结束时间去触发后续窗口。
因此:
- Watermark 不能达到 10 → 所以 [5--10s) 窗口 不会触发
- Watermark 不能达到 15 → 所以 [10--15s) 窗口 也不会触发
七、迟到事件(Late Event)如何判断?
Flink 判断事件是否迟到的规则是:
event.timestamp < current Watermark → 是迟到事件
⚠ 注意,不是简单判断 event.timestamp > Watermark 就不是迟到,而必须 event.timestamp 小于当前 Watermark 才是迟到。
八、Flink 示例代码(直接可用)
java
dataStream
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((event, ts) -> event.timestamp)
)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.allowedLateness(Time.seconds(5))
.sideOutputLateData(lateOutputTag)
.process(new MyProcessWindowFunction());
这段代码配置:
✔ 提取事件时间
✔ 每个窗口 5 秒
✔ 允许最大乱序 5 秒
✔ 配置迟到事件侧输出流,避免丢失数据
九、总结
📌 Watermark 是 Flink 事件时间推进和窗口触发的核心机制 ,用于告知系统"事件时间已经推进到某个时间点"。
📌 窗口触发的判断规则:
Watermark ≥ 窗口结束时间
📌 真正的迟到事件判断:
event.timestamp < Watermark
📌 Watermark 生成策略决定了 Flink 如何 处理乱序事件 、触发窗口计算 与 延迟容忍。