Flink Watermark(水位线)机制详解
Watermark是Flink处理事件时间(Event Time)的核心机制,用于处理乱序数据 和触发窗口计算。让我全面深入地介绍:
一、核心概念
1. 什么是Watermark?
**Watermark(水位线)**是一个时间戳标记,表示:
-
"所有时间戳 ≤ Watermark 的数据都已到达"
-
"时间戳 > Watermark 的数据可能还在路上"
Watermark(t) 的含义:
时间戳 ≤ t 的数据已经全部到达(或大部分到达)
2. 为什么需要Watermark?
在分布式流处理中,数据可能会:
- ⏰ 乱序到达:网络延迟、多数据源等原因
- 🐌 延迟到达:某些数据比其他数据晚很多
- ❓ 何时触发计算:不知道数据是否都到齐了
Watermark解决的核心问题:
在数据可能乱序的情况下,如何确定"某个时间窗口的数据已经全部到达,可以触发计算了"?
3. Watermark示意图
数据流(事件时间):
时间戳: 1 3 2 5 4 8 7 10 9
| | | | | | | | |
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
[==================数据流==================>
Watermark插入(假设允许2秒延迟):
数据: 1 3 2 W(1) 5 4 W(3) 8 7 W(6) 10 9 W(8)
↑ ↑ ↑ ↑
Watermark(1) Watermark(3) ...
Watermark(6)表示:
- 时间戳 ≤ 6 的数据已经全部到达
- 窗口[0,5)可以触发计算了
- 时间戳为3的延迟数据仍能被处理
二、Watermark的工作原理
1. Watermark与窗口触发
java
窗口触发条件:
当 Watermark >= 窗口结束时间 时,窗口触发计算
示例:窗口 [0, 10)
- Watermark = 5 → 窗口不触发(还有数据可能到达)
- Watermark = 9 → 窗口不触发(还有数据可能到达)
- Watermark = 10 → 窗口触发!(时间戳<10的数据已全部到达)
2. 完整流程示例
场景:5秒滚动窗口,允许3秒延迟
数据到达顺序(事件时间):
t=1s → 进入窗口[0,5)
t=3s → 进入窗口[0,5)
t=2s → 进入窗口[0,5)(乱序)
t=7s → 进入窗口[5,10),生成Watermark(4)
t=6s → 进入窗口[5,10)(乱序)
t=10s → 进入窗口[10,15),生成Watermark(7)
t=9s → 进入窗口[5,10)(乱序)
Watermark推进过程:
1. 收到t=7s,当前最大时间戳=7
→ Watermark = 7 - 3 = 4
→ 窗口[0,5)不触发(4 < 5)
2. 收到t=10s,当前最大时间戳=10
→ Watermark = 10 - 3 = 7
→ 窗口[5,10)不触发(7 < 10)
3. 收到t=13s,当前最大时间戳=13
→ Watermark = 13 - 3 = 10
→ 窗口[5,10)触发!(10 >= 10)
→ 输出窗口[5,10)的计算结果
三、Watermark生成策略
1. 周期性生成(Periodic Watermarks)
特点:按固定时间间隔生成Watermark
java
// 方式1:有界乱序(最常用)
WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner((event, timestamp) -> event.getTimestamp());
// 原理:
// Watermark = 当前最大事件时间 - 允许的最大乱序时间
// 例如:最大事件时间=10s,允许乱序=3s → Watermark=7s
java
// 方式2:单调递增(无乱序)
WatermarkStrategy.<Event>forMonotonousTimestamps()
.withTimestampAssigner((event, timestamp) -> event.getTimestamp());
// 原理:
// Watermark = 当前最大事件时间
// 适用于数据严格按时间顺序到达的场景
java
// 方式3:自定义周期性Watermark
WatermarkStrategy
.forGenerator((context) -> new WatermarkGenerator<Event>() {
private long maxTimestamp = Long.MIN_VALUE;
private final long maxOutOfOrderness = 3000L; // 3秒
@Override
public void onEvent(Event event, long eventTimestamp,
WatermarkOutput output) {
// 每条数据到达时更新最大时间戳
maxTimestamp = Math.max(maxTimestamp, event.getTimestamp());
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// 周期性生成Watermark(默认200ms一次)
output.emitWatermark(new Watermark(maxTimestamp - maxOutOfOrderness));
}
})
.withTimestampAssigner((event, timestamp) -> event.getTimestamp());
2. 标点式生成(Punctuated Watermarks)
特点:根据特定数据标记生成Watermark
java
// 自定义标点式Watermark
WatermarkStrategy
.forGenerator((context) -> new WatermarkGenerator<Event>() {
@Override
public void onEvent(Event event, long eventTimestamp,
WatermarkOutput output) {
// 遇到特殊标记数据时生成Watermark
if (event.hasWatermarkMarker()) {
output.emitWatermark(new Watermark(event.getTimestamp()));
}
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// 标点式不使用周期性生成
}
})
.withTimestampAssigner((event, timestamp) -> event.getTimestamp());
// 应用场景:
// 1. 数据源自带Watermark标记
// 2. Kafka等消息队列的特殊控制消息
// 3. 需要精确控制Watermark生成时机
四、完整代码示例
示例1:基础Watermark使用
java
public class BasicWatermarkExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
// 设置Watermark生成间隔(默认200ms)
env.getConfig().setAutoWatermarkInterval(1000L); // 1秒
// 模拟乱序数据
DataStream<Event> events = env.fromElements(
new Event("sensor1", 1000L, 25.5), // 1秒
new Event("sensor1", 3000L, 26.0), // 3秒
new Event("sensor1", 2000L, 25.8), // 2秒(乱序)
new Event("sensor1", 7000L, 27.0), // 7秒
new Event("sensor1", 5000L, 26.5), // 5秒(乱序)
new Event("sensor1", 11000L, 28.0) // 11秒
);
// 分配时间戳和Watermark(允许3秒乱序)
DataStream<Event> withWatermarks = events.assignTimestampsAndWatermarks(
WatermarkStrategy
.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner((event, timestamp) -> event.timestamp)
);
// 5秒滚动窗口
withWatermarks
.keyBy(event -> event.sensorId)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.process(new ProcessWindowFunction<Event, String, String, TimeWindow>() {
@Override
public void process(
String sensorId,
Context ctx,
Iterable<Event> events,
Collector<String> out
) {
double sum = 0;
int count = 0;
for (Event event : events) {
sum += event.temperature;
count++;
}
out.collect(String.format(
"Sensor: %s, Window: [%d-%d], Avg Temp: %.2f, Count: %d",
sensorId,
ctx.window().getStart() / 1000,
ctx.window().getEnd() / 1000,
sum / count,
count
));
}
})
.print();
env.execute("Basic Watermark Example");
}
static class Event {
String sensorId;
Long timestamp;
Double temperature;
Event(String sensorId, Long timestamp, Double temperature) {
this.sensorId = sensorId;
this.timestamp = timestamp;
this.temperature = temperature;
}
}
}
/* 执行过程分析:
数据到达:t=1s, 3s, 2s, 7s, 5s, 11s
Watermark推进:
1. t=1s → maxTimestamp=1s → Watermark=-2s(1-3)
2. t=3s → maxTimestamp=3s → Watermark=0s(3-3)
3. t=2s → maxTimestamp=3s → Watermark=0s(不变)
4. t=7s → maxTimestamp=7s → Watermark=4s(7-3)
5. t=5s → maxTimestamp=7s → Watermark=4s(不变)
6. t=11s → maxTimestamp=11s → Watermark=8s(11-3)
→ 窗口[0,5)触发!(8>=5)
窗口[0,5)包含的数据:
- t=1s ✅
- t=3s ✅
- t=2s ✅(乱序数据被正确处理)
输出:
Sensor: sensor1, Window: [0-5], Avg Temp: 25.77, Count: 3
*/
示例2:观察Watermark推进过程
java
public class WatermarkObserverExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1); // 单并行度便于观察
DataStream<Event> events = env.fromElements(
new Event(1000L),
new Event(3000L),
new Event(2000L), // 乱序
new Event(5000L),
new Event(4000L), // 乱序
new Event(8000L),
new Event(11000L)
);
// 允许2秒乱序
events
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(2))
.withTimestampAssigner((event, ts) -> event.timestamp)
)
.process(new ProcessFunction<Event, String>() {
@Override
public void processElement(
Event event,
Context ctx,
Collector<String> out
) {
long currentWatermark = ctx.timerService().currentWatermark();
out.collect(String.format(
"Event: t=%ds, CurrentWatermark: %ds",
event.timestamp / 1000,
currentWatermark == Long.MIN_VALUE ?
-999 : currentWatermark / 1000
));
}
})
.print();
env.execute("Watermark Observer");
}
static class Event {
Long timestamp;
Event(Long timestamp) { this.timestamp = timestamp; }
}
}
/* 输出:
Event: t=1s, CurrentWatermark: -999s (初始值)
Event: t=3s, CurrentWatermark: 1s (3-2=1)
Event: t=2s, CurrentWatermark: 1s (maxTs仍是3)
Event: t=5s, CurrentWatermark: 3s (5-2=3)
Event: t=4s, CurrentWatermark: 3s (maxTs仍是5)
Event: t=8s, CurrentWatermark: 6s (8-2=6)
Event: t=11s, CurrentWatermark: 9s (11-2=9)
观察:
- Watermark单调递增,不会回退
- 乱序数据不影响Watermark(只看最大时间戳)
- Watermark = 最大事件时间 - 允许延迟
*/
示例3:多流Watermark对齐
java
public class MultiStreamWatermarkExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
// 数据源1:快速流(低延迟)
DataStream<Event> fastStream = env
.fromElements(
new Event("fast", 1000L),
new Event("fast", 2000L),
new Event("fast", 3000L),
new Event("fast", 10000L) // 快速推进到10秒
)
.assignTimestampsAndWatermarks(
WatermarkStrategy.<Event>forMonotonousTimestamps()
.withTimestampAssigner((e, ts) -> e.timestamp)
);
// 数据源2:慢速流(高延迟)
DataStream<Event> slowStream = env
.fromElements(
new Event("slow", 1000L),
new Event("slow", 2000L),
new Event("slow", 3000L) // 只推进到3秒
)
.assignTimestampsAndWatermarks(
WatermarkStrategy.<Event>forMonotonousTimestamps()
.withTimestampAssigner((e, ts) -> e.timestamp)
);
// 合流
fastStream
.union(slowStream)
.keyBy(event -> "key")
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.process(new ProcessWindowFunction<Event, String, String, TimeWindow>() {
public void process(String key, Context ctx,
Iterable<Event> events, Collector<String> out) {
int count = 0;
for (Event e : events) count++;
out.collect(String.format(
"Window [%d-%d]: %d events",
ctx.window().getStart() / 1000,
ctx.window().getEnd() / 1000,
count
));
}
})
.print();
env.execute("Multi-Stream Watermark");
}
static class Event {
String source;
Long timestamp;
Event(String source, Long timestamp) {
this.source = source;
this.timestamp = timestamp;
}
}
}
/* Watermark对齐原理:
合流后的Watermark = min(所有上游的Watermark)
fastStream Watermark: 1s → 2s → 3s → 10s
slowStream Watermark: 1s → 2s → 3s
合流后Watermark: 1s → 2s → 3s → 3s(被慢流拖住)
影响:
- 即使fastStream推进到10s,窗口[0,5)仍不触发
- 因为合流后Watermark只有3s < 5s
- slowStream成为瓶颈(数据倾斜问题)
*/
五、延迟数据处理
1. 什么是延迟数据?
延迟数据:事件时间 < 当前Watermark 的数据
示例:
当前Watermark = 10s
收到一条t=7s的数据 → 延迟数据(7 < 10)
窗口[5,10)已经在Watermark=10s时触发计算
t=7s的数据到达时窗口已关闭 → 默认被丢弃!
2. 延迟数据处理策略
策略1:设置允许的延迟时间(Allowed Lateness)
java
dataStream
.keyBy(...)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.allowedLateness(Time.seconds(2)) // 允许窗口关闭后2秒内的延迟数据
.sum(1);
/* 工作原理:
窗口[0,5):
1. Watermark=5s → 窗口首次触发,输出结果1
2. 收到t=3s的延迟数据 → 重新计算,输出结果2(更新)
3. 收到t=4s的延迟数据 → 重新计算,输出结果3(更新)
4. Watermark=7s → 窗口彻底关闭(5+2=7)
5. 之后的延迟数据被丢弃
优点:
- 容忍一定程度的延迟
- 结果更准确
缺点:
- 需要保持窗口状态更长时间
- 可能产生多次输出
*/
策略2:侧输出流(Side Output)收集延迟数据
java
// 定义延迟数据标签
OutputTag<Event> lateDataTag = new OutputTag<Event>("late-data"){};
SingleOutputStreamOperator<String> result = dataStream
.keyBy(...)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.sideOutputLateData(lateDataTag) // 延迟数据输出到侧输出流
.sum(1);
// 获取延迟数据
DataStream<Event> lateData = result.getSideOutput(lateDataTag);
// 处理延迟数据
lateData.print("Late Data"); // 可以单独处理或记录日志
/* 优点:
- 不丢失任何数据
- 可以单独分析延迟数据
- 用于监控和告警
应用场景:
- 数据质量监控
- 延迟数据统计
- 后续补偿处理
*/
策略3:组合使用
java
OutputTag<Event> lateDataTag = new OutputTag<Event>("late-data"){};
SingleOutputStreamOperator<String> result = dataStream
.keyBy(event -> event.sensorId)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.allowedLateness(Time.seconds(2)) // 允许2秒延迟
.sideOutputLateData(lateDataTag) // 超过2秒的进侧输出
.aggregate(new MyAggregateFunction());
// 主流:正常和轻度延迟的数据
result.print("Main Output");
// 侧输出流:严重延迟的数据
result.getSideOutput(lateDataTag).print("Severe Late Data");
/* 数据分类:
1. 正常数据:t <= Watermark
→ 正常进入窗口
2. 轻度延迟:Watermark < t < Watermark+AllowedLateness
→ 进入窗口,触发重新计算
3. 严重延迟:t >= Watermark+AllowedLateness
→ 输出到侧输出流
时间线示例(窗口[0,5),允许延迟2秒):
Watermark=3s: t=2s → 正常数据
Watermark=5s: 窗口触发,输出结果
Watermark=6s: t=4s → 轻度延迟,重新计算
Watermark=7s: 窗口彻底关闭
Watermark=8s: t=3s → 严重延迟,进侧输出流
*/
完整示例:延迟数据处理
java
public class LateDataHandlingExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 定义延迟数据标签
OutputTag<Event> lateDataTag = new OutputTag<Event>("late-data"){};
DataStream<Event> events = env.fromElements(
new Event("sensor1", 1000L, 25.0),
new Event("sensor1", 2000L, 26.0),
new Event("sensor1", 3000L, 27.0),
new Event("sensor1", 8000L, 28.0), // 推进Watermark到6s
new Event("sensor1", 4000L, 26.5), // 延迟数据1(在允许范围内)
new Event("sensor1", 10000L, 29.0), // 推进Watermark到8s
new Event("sensor1", 2500L, 25.5) // 延迟数据2(超过允许延迟)
);
DataStream<Event> withWatermarks = events.assignTimestampsAndWatermarks(
WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(2))
.withTimestampAssigner((event, ts) -> event.timestamp)
);
SingleOutputStreamOperator<String> result = withWatermarks
.keyBy(event -> event.sensorId)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.allowedLateness(Time.seconds(1)) // 允许1秒延迟
.sideOutputLateData(lateDataTag) // 严重延迟数据输出到侧输出
.process(new ProcessWindowFunction<Event, String, String, TimeWindow>() {
@Override
public void process(
String key,
Context ctx,
Iterable<Event> events,
Collector<String> out
) {
double sum = 0;
int count = 0;
for (Event event : events) {
sum += event.temperature;
count++;
}
out.collect(String.format(
"[%s] Window [%d-%d]: Avg=%.2f, Count=%d, Watermark=%d",
ctx.currentProcessingTime(),
ctx.window().getStart() / 1000,
ctx.window().getEnd() / 1000,
sum / count,
count,
ctx.currentWatermark() / 1000
));
}
});
// 主输出流
result.print("Main");
// 延迟数据流
result.getSideOutput(lateDataTag)
.map(event -> String.format(
"Late Data: t=%ds, temp=%.1f",
event.timestamp / 1000,
event.temperature
))
.print("Late");
env.execute("Late Data Handling");
}
static class Event {
String sensorId;
Long timestamp;
Double temperature;
Event(String sensorId, Long timestamp, Double temperature) {
this.sensorId = sensorId;
this.timestamp = timestamp;
this.temperature = temperature;
}
}
}
/* 输出:
Main> Window [0-5]: Avg=26.00, Count=3, Watermark=6
↑ Watermark=6s时触发,包含t=1s,2s,3s
Main> Window [0-5]: Avg=26.13, Count=4, Watermark=6
↑ t=4s的延迟数据到达,重新计算(4s在允许延迟内)
Late> Late Data: t=2s, temp=25.5
↑ t=2.5s的数据超过允许延迟,进入侧输出流
观察:
1. 窗口首次触发:Watermark=6s (8-2=6 >= 5)
2. t=4s延迟数据触发重算:4s在[5-1, 5+1]范围内
3. t=2.5s严重延迟:Watermark已=8s,窗口在6s彻底关闭
*/
六、Watermark传播机制
1. 单流传播
Source → Map → KeyBy → Window
↓ ↓ ↓ ↓
W1 → W1 → W1 → W1
Watermark在算子间传播:
- 每个算子收到Watermark后向下游转发
- 保持单调递增
2. 多流合并
java
Stream1 (W1=10s) ┐
├→ Union → (Watermark = min(10,5) = 5s)
Stream2 (W2=5s) ┘
规则:合流后的Watermark取所有上游的最小值
原因:保守策略,确保不会漏掉任何数据
3. 分流广播
┌→ Stream1 (W=10s)
Source ─┤
└→ Stream2 (W=10s)
规则:所有分支获得相同的Watermark
七、Watermark最佳实践
1. 选择合适的乱序时间
java
// ❌ 太小:丢失延迟数据
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(1))
// 1秒延迟可能不够
// ✅ 适中:平衡准确性和延迟
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(3))
// ❌ 太大:结果延迟高
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofMinutes(10))
// 10分钟太保守,实时性差
2. 监控Watermark延迟
java
dataStream
.process(new ProcessFunction<Event, Event>() {
@Override
public void processElement(Event event, Context ctx, Collector<Event> out) {
long watermark = ctx.timerService().currentWatermark();
long eventTime = event.timestamp;
long lag = eventTime - watermark; // Watermark延迟
if (lag > 60000) { // 延迟超过1分钟
// 记录日志或发送告警
System.err.println("High watermark lag: " + lag + "ms");
}
out.collect(event);
}
});
3. 处理空闲数据源
java
// 问题:某个分区长时间无数据,Watermark不推进
// 解决:设置空闲超时
WatermarkStrategy
.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withIdleness(Duration.ofMinutes(1)) // 1分钟无数据则视为空闲
.withTimestampAssigner((event, ts) -> event.timestamp);
/* 效果:
- 分区空闲1分钟后,不再影响全局Watermark
- 其他活跃分区的Watermark可以正常推进
*/
4. Kafka数据源的Watermark
java
// Kafka分区独立生成Watermark
FlinkKafkaConsumer<Event> consumer = new FlinkKafkaConsumer<>(...);
DataStream<Event> stream = env
.addSource(consumer)
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withIdleness(Duration.ofMinutes(1)) // 重要!处理空闲分区
.withTimestampAssigner((event, ts) -> event.timestamp)
);
/* 注意事项:
1. Kafka每个分区独立生成Watermark
2. 全局Watermark = min(所有分区的Watermark)
3. 某个分区空闲会拖慢全局Watermark
4. 必须设置withIdleness处理空闲分区
*/
八、关键要点总结
核心概念
- ✅ Watermark定义:时间戳标记,表示"≤该时间戳的数据已全部到达"
- ✅ 触发条件:Watermark >= 窗口结束时间时触发窗口计算
- ✅ 单调递增:Watermark只能前进,不能后退
- ✅ 乱序处理:通过设置允许的乱序时间容忍延迟数据
生成策略
- ✅ 有界乱序:Watermark = 最大事件时间 - 允许延迟(最常用)
- ✅ 单调递增:Watermark = 最大事件时间(无乱序场景)
- ✅ 自定义生成:根据业务需求定制Watermark逻辑
延迟数据
- ⚠️ allowedLateness:允许窗口关闭后继续接收延迟数据
- ⚠️ sideOutputLateData:将严重延迟数据输出到侧输出流
- ⚠️ 多次输出:延迟数据可能导致窗口重复计算并输出
多流场景
- ⚠️ Watermark对齐:多流合并取最小Watermark
- ⚠️ 空闲数据源:使用withIdleness避免空闲分区拖慢Watermark
- ⚠️ 数据倾斜:慢速分区会成为Watermark瓶颈
最佳实践
- ✅ 根据业务容忍度选择合适的乱序时间
- ✅ 监控Watermark延迟,及时发现数据源问题
- ✅ 使用侧输出流记录延迟数据,便于分析和告警
- ✅ Kafka等多分区数据源必须设置空闲超时
Watermark是Flink事件时间处理的核心,理解其原理对于开发高质量的实时应用至关重要!