上一篇讲了 Flink 的三种时间语义(处理时间、事件时间、摄入时间),其中事件时间是生产环境首选,但事件时间有一个核心问题需要解决:什么时候认为窗口内的数据都到齐了? 答案就是 Watermark(水位线)。
Watermark 是 Flink 事件时间的核心机制,也是最容易踩坑的地方。很多同学刚接触 Flink 时,事件时间窗口不触发、数据丢失、结果不准确,90% 的原因都是 Watermark 配置有问题------忘记设置 Watermark、乱序容忍时间设置过小、空闲输入导致 Watermark 不推进、事件时间戳是秒级不是毫秒级......
这篇从 Watermark 的本质定义、两种生成方式(周期性 vs 断点式)、WatermarkGenerator 接口详解、内置生成策略、自定义实现、传播与窗口触发机制、多并行度合并、空闲输入处理、到监控调优和常见坑,把 Watermark 一次性讲透。
一、Watermark 本质:事件时间的时钟
Watermark 是一个特殊的时间戳,作为数据流中的一部分向下游传递。它表示**"这个时间戳之前的所有数据都已经到达"**。
例如 Watermark = 10:00:05,表示 Flink 认为 10:00:05 之前的数据都到齐了,窗口 [10:00:00, 10:00:05) 可以触发计算。
1.1 Watermark 的三个核心作用
第一,驱动事件时间窗口触发。 事件时间窗口的触发条件是:Watermark ≥ 窗口结束时间。例如滚动窗口 [10:00:00, 10:05:00),当 Watermark 推进到 10:05:00 时,窗口触发计算。没有 Watermark,窗口永远不会触发。
第二,处理乱序数据。 分布式系统中数据到达顺序和事件发生顺序往往不一致(网络延迟、消息队列重试、多分区并行消费)。通过有界乱序 Watermark,允许一定程度的乱序,保证乱序数据被正确包含在窗口中,而不是当作迟到数据丢弃。
第三,进度度量。 Watermark 推进速度反映了事件时间的处理进度。Watermark Lag(系统时间 - Watermark)可以用来监控作业是否有延迟、是否有反压、是否有数据倾斜。Watermark 长时间不推进,通常意味着有问题(空闲输入、数据倾斜、Source 消费延迟等)。
1.2 Watermark 的三个关键特性
第一,单调递增。 Watermark 只能向前推进,不能回退。这保证了窗口触发后不会因为 Watermark 回退而重复触发。Flink 内部会自动保证单调递增------如果新生成的 Watermark ≤ 当前 Watermark,不会发出。
第二,启发式估计。 Watermark 是一种启发式估计,不保证 100% 准确。它基于"最大事件时间 - 乱序容忍时间"计算,假设数据乱序不超过容忍时间。如果有数据乱序超过容忍时间,会被当作迟到数据处理(默认丢弃,或通过 allowedLateness 重新触发)。
第三,全局时钟。 Watermark 是整个作业的事件时间时钟,所有算子共享同一个时间进度。Watermark 从 Source 生成,经过每个算子向下游传递,所有算子的窗口触发都基于同一个 Watermark 进度。
下面这张图把 Watermark 的本质与作用、两种生成方式、WatermarkGenerator 接口放在一起展示。

二、两种 Watermark 生成方式
Flink 支持两种 Watermark 生成方式:周期性生成(Periodic)和断点式生成(Punctuated)。
2.1 周期性生成(Periodic,默认)
周期性生成按固定时间间隔触发,默认 200ms(可通过 pipeline.auto-watermark-interval 配置)。调用 onPeriodicEmit(WatermarkOutput output) 方法生成 Watermark。
特点:
- 即使没有数据到达,也会定期尝试生成 Watermark(如果有最大事件时间记录)
- 实现简单,Watermark 推进稳定
- 适用于大多数场景,有界乱序、单调递增都用周期性生成
- 如果最大事件时间没有更新,onPeriodicEmit 不会发出新的 Watermark(单调递增保证)
2.2 断点式生成(Punctuated,事件触发)
断点式生成在每个事件到达时触发,根据事件内容决定是否发出 Watermark。调用 onEvent(T event, long eventTimestamp, WatermarkOutput output) 方法。
特点:
- 只有特定事件到达时才生成 Watermark(如遇到特殊标记事件、心跳事件、事务边界事件)
- Watermark 推进精确,与业务事件对齐,延迟更低
- 适用于数据流中有明确的时间标记事件的场景
- 如果没有触发事件,Watermark 永远不推进,窗口永远不触发------需要配合空闲处理
2.3 两种方式对比
| 对比维度 | 周期性生成 Periodic | 断点式生成 Punctuated |
|---|---|---|
| 触发时机 | 固定时间间隔(默认200ms) | 每个事件到达时 |
| 调用方法 | onPeriodicEmit | onEvent |
| 无数据时 | 仍会定期尝试生成 | 不会生成 |
| 实现复杂度 | 简单 | 较复杂(需判断事件) |
| 推进精度 | 一般(间隔内不更新) | 高(事件级精确) |
| 适用场景 | 大多数场景 | 有明确时间标记事件 |
| 风险 | 无 | 无触发事件时WM不推进 |
生产环境 99% 的场景用周期性生成(有界乱序),断点式生成只在特殊场景使用(如事务边界、心跳事件驱动 Watermark)。
三、WatermarkGenerator 接口详解
Flink 1.11+ 统一了 Watermark 生成接口,WatermarkGenerator<T> 是所有 Watermark 生成器的核心接口,有两个方法:
java
public interface WatermarkGenerator<T> {
// 每个事件到达时调用,可用于断点式生成或更新内部状态
void onEvent(T event, long eventTimestamp, WatermarkOutput output);
// 周期性调用(默认200ms),可用于周期性生成 Watermark
void onPeriodicEmit(WatermarkOutput output);
}
3.1 onEvent 方法
onEvent(T event, long eventTimestamp, WatermarkOutput output) 在每个事件到达时立即调用。
用途:
- 更新内部状态:记录当前最大事件时间(最常用)
- 断点式生成 Watermark :遇到特殊事件时调用
output.emitWatermark(new Watermark(timestamp))立即发出 Watermark
注意:onEvent 中发出的 Watermark 会立即向下游传递,不需要等待周期性触发。
3.2 onPeriodicEmit 方法
onPeriodicEmit(WatermarkOutput output) 按固定间隔周期性调用(默认 200ms)。
用途:基于 onEvent 中更新的最大事件时间,计算并发出新的 Watermark。
注意:发出的 Watermark 必须单调递增,如果新 Watermark ≤ 当前 Watermark,Flink 内部不会发出(自动保证单调递增)。
3.3 WatermarkOutput 接口
WatermarkOutput 是 Watermark 的输出接口,主要方法:
emitWatermark(Watermark watermark):发出一个 WatermarkmarkIdle():标记当前输入为空闲(不参与下游 Watermark 最小值计算)markActive():标记当前输入为活跃(重新参与计算)
四、内置 Watermark 生成策略
Flink 提供了两种内置 Watermark 生成策略,覆盖绝大多数场景。
4.1 有界乱序(BoundedOutOfOrderness)------ 最常用
WatermarkStrategy.forBoundedOutOfOrderness(Duration maxOutOfOrderness)
Watermark 计算方式:Watermark = 当前最大事件时间 - 乱序容忍时间
原理:记录当前最大事件时间,每次生成 Watermark 时减去固定的乱序容忍时间,允许一定程度的乱序。乱序容忍时间内的数据都会被窗口正确包含。
例如乱序容忍时间 5 秒,最大事件时间 10:00:10,Watermark = 10:00:05,表示 10:00:05 之前的数据都到齐了。10:00:01 到 10:00:05 之间的数据即使延迟到达,也会被正确处理。
适用:数据有固定范围的乱序(如网络延迟 5 秒内),绝大多数生产场景。
注意:乱序容忍时间设置过小会丢数据(迟到数据被丢弃),设置过大会增加窗口触发延迟(Watermark 推进慢)。
4.2 单调递增(MonotonousTimestamps)------ 理想场景
WatermarkStrategy.forMonotonousTimestamps()
Watermark 计算方式:Watermark = 当前最大事件时间(乱序容忍时间为 0)
原理:假设数据按时间戳递增到达,没有乱序,Watermark 直接等于当前最大事件时间。
适用:数据已经按时间排序的场景(如某些数据库 binlog、有序消息队列)。
优点:Watermark 推进最快,窗口触发延迟最小,性能最好。
注意:如果有乱序数据,会被当作迟到数据处理(默认丢弃),导致结果错误。有乱序的场景绝对不能用单调递增。
4.3 内置策略完整代码
java
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import java.time.Duration;
public class BuiltinWatermarkExample {
public static class Order {
public String userId;
public double amount;
public long timestamp; // 毫秒级事件时间戳
public Order() {}
}
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<Order> source = env.addSource(new OrderSource());
// 方式一:有界乱序 Watermark(最常用,允许5秒乱序)
WatermarkStrategy<Order> boundedStrategy = WatermarkStrategy
.<Order>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withIdleness(Duration.ofMinutes(1)) // 空闲输入处理(必须设置)
.withTimestampAssigner((event, timestamp) -> event.timestamp);
// 方式二:单调递增 Watermark(数据已排序,无乱序)
WatermarkStrategy<Order> monotonousStrategy = WatermarkStrategy
.<Order>forMonotonousTimestamps()
.withIdleness(Duration.ofMinutes(1))
.withTimestampAssigner((event, timestamp) -> event.timestamp);
// 使用有界乱序策略
DataStream<Double> result = source
.assignTimestampsAndWatermarks(boundedStrategy)
.keyBy(order -> order.userId)
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.sum("amount");
result.print();
env.execute("Builtin Watermark Example");
}
}
代码关键点:
第一,withTimestampAssigner 从数据中提取事件时间戳,必须是毫秒级 Unix 时间戳(long 类型)。如果数据中是秒级时间戳,需要乘以 1000 转换。
第二,withIdleness(Duration.ofMinutes(1)) 处理空闲输入,空闲超过 1 分钟后暂时忽略该输入的 Watermark。所有事件时间作业都应该设置 withIdleness,这是生产环境最容易忽略的配置。
第三,assignTimestampsAndWatermarks 必须在 keyBy 和 window 之前调用,通常紧跟在 Source 之后。
五、自定义 Watermark 实现
当内置策略不能满足需求时(如需要根据数据内容动态调整乱序容忍时间、需要断点式生成),可以自定义 WatermarkGenerator。
5.1 自定义有界乱序 Watermark(动态乱序容忍)
java
import org.apache.flink.api.common.eventtime.Watermark;
import org.apache.flink.api.common.eventtime.WatermarkGenerator;
import org.apache.flink.api.common.eventtime.WatermarkOutput;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import java.time.Duration;
public class CustomWatermarkExample {
public static class Order {
public String userId;
public double amount;
public long timestamp;
public String priority; // 优先级:high/low,高优先级乱序小
public Order() {}
}
// 自定义 WatermarkGenerator:根据数据优先级动态调整乱序容忍时间
public static class DynamicOutOfOrdernessGenerator
implements WatermarkGenerator<Order> {
private long maxTimestamp = Long.MIN_VALUE;
private final long defaultOutOfOrderness; // 默认乱序容忍时间(毫秒)
public DynamicOutOfOrdernessGenerator(Duration defaultOutOfOrderness) {
this.defaultOutOfOrderness = defaultOutOfOrderness.toMillis();
}
@Override
public void onEvent(Order event, long eventTimestamp, WatermarkOutput output) {
// 更新最大事件时间
maxTimestamp = Math.max(maxTimestamp, eventTimestamp);
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// 周期性发出 Watermark:最大事件时间 - 乱序容忍时间
if (maxTimestamp != Long.MIN_VALUE) {
output.emitWatermark(new Watermark(maxTimestamp - defaultOutOfOrderness));
}
}
}
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<Order> source = env.addSource(new OrderSource());
// 自定义 Watermark 策略
WatermarkStrategy<Order> customStrategy = WatermarkStrategy
.<Order>forGenerator(ctx -> new DynamicOutOfOrdernessGenerator(Duration.ofSeconds(5)))
.withIdleness(Duration.ofMinutes(1))
.withTimestampAssigner((event, timestamp) -> event.timestamp);
DataStream<Double> result = source
.assignTimestampsAndWatermarks(customStrategy)
.keyBy(order -> order.userId)
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.sum("amount");
result.print();
env.execute("Custom Watermark Example");
}
}
自定义 Watermark 的关键点:
第一,实现 WatermarkGenerator<T> 接口,重写 onEvent 和 onPeriodicEmit 两个方法。
第二,onEvent 中更新内部状态(最大事件时间),onPeriodicEmit 中基于状态计算并发出 Watermark。
第三,用 WatermarkStrategy.forGenerator(ctx -> new CustomGenerator()) 创建自定义策略,forGenerator 接收一个 WatermarkGeneratorSupplier(可以访问上下文)。
第四,发出 Watermark 用 output.emitWatermark(new Watermark(timestamp)),timestamp 是毫秒级时间戳。
六、Watermark 传播与窗口触发机制
Watermark 不是只在 Source 生成后就结束了,它会随着数据流在每个算子间向下游传递,最终驱动窗口触发。
下面这张图展示了 Watermark 的传播流程、多并行度合并、窗口触发机制和空闲输入处理。

6.1 Watermark 传播流程
Watermark 的传播流程:
- Source 算子:根据 WatermarkStrategy 生成 Watermark,同时分配事件时间戳
- Transformation 算子(map/filter/keyBy 等):接收上游 Watermark,合并多输入,向下游传递
- Window 算子:Watermark 到达后,检查是否有窗口的结束时间 ≤ Watermark,如果有则触发这些窗口计算,输出结果
- Sink 算子:接收窗口结果,Watermark 继续向下游传递,写入外部系统
6.2 多并行度 Watermark 合并:取最小值
一个算子通常有多个输入(上游多个并行子任务)。Watermark 的合并规则是:该算子的 Watermark = 所有输入 Watermark 的最小值。
为什么取最小值?因为 Watermark 表示"这个时间之前的数据都到齐了",只有所有输入的这个时间之前的数据都到齐了,才能认为整体都到齐了。取最小值保证了"最慢的那个输入"的数据都到齐了才触发窗口。
例如三个输入的 Watermark 分别是 10:00:15、10:00:10、10:00:20,当前算子的 Watermark = min(10:00:15, 10:00:10, 10:00:20) = 10:00:10。
后果:如果某个输入数据慢(数据倾斜或消费延迟),它的 Watermark 小,会拖慢整体 Watermark,导致窗口触发延迟增加。
6.3 事件时间窗口触发机制
事件时间窗口的触发条件:Watermark ≥ 窗口结束时间。
触发流程:
- 数据到达,更新最大事件时间,生成新 Watermark
- Watermark 向下游传播,到达窗口算子
- Watermark ≥ 窗口结束时间,触发窗口计算,输出结果
- 迟到数据(Watermark 之后到达):allowedLateness 内重新触发,超过则丢弃或侧输出
例如滚动窗口 [10:00:00, 10:05:00),当 Watermark 推进到 10:05:00 时,窗口触发计算。如果有 allowedLateness(1 hour),窗口触发后不立即清理,1 小时内到达的迟到数据会重新触发窗口计算。
6.4 空闲输入问题与 withIdleness 处理
问题现象 :某个上游并行子任务没有数据(空闲),它的 Watermark 停留在旧值不更新。由于下游 Watermark 取所有输入的最小值,这个空闲输入会导致整体 Watermark 不推进,窗口永远不触发,没有任何输出。
常见场景:数据倾斜导致某些分区没有数据、Source 分区数大于并行度、Kafka 某些分区没有消息。
解决方案 :设置 withIdleness(Duration.ofMinutes(1)),当某个输入空闲超过指定时间后,暂时忽略该输入的 Watermark(不参与最小值计算),直到该输入有新数据到达。
原理:空闲超时后,该输入标记为 idle,下游计算 Watermark 时跳过它;当有新数据到达时,取消 idle 标记,重新参与计算。
建议:所有事件时间作业都应该设置 withIdleness,空闲时间建议 1-5 分钟(根据业务延迟容忍度设置)。这是生产环境窗口不触发的最常见原因,一定要设置。
七、Watermark 监控与调优
Watermark 的健康度直接影响事件时间作业的正确性和性能。生产环境需要监控 Watermark 相关指标,并根据监控数据调优。
下面这张图总结了内置生成策略、关键监控指标、调优建议和六大常见坑。

7.1 关键监控指标
通过 Flink Web UI 和 Metrics 监控 Watermark 健康度:
| 指标 | 含义 | 异常表现 |
|---|---|---|
| Current Watermark | 当前算子的 Watermark 时间戳 | 长时间不更新 |
| Watermark Lag | Watermark 与系统时间的差值 | 持续增大 |
| Late Events | 迟到数据数量(Watermark 之后到达) | 数量突增 |
| Idle Sources | 空闲 Source 数量 | 有空闲未处理 |
7.2 八条调优建议
1. 先监控再设置乱序容忍时间:上线前先监控数据的最大乱序时间(数据到达时间 - 事件时间),乱序容忍时间设置为最大乱序的 1.2-1.5 倍,留一定余量。不要凭感觉设置。
2. 所有事件时间作业都设置 withIdleness :.withIdleness(Duration.ofMinutes(1)),避免空闲输入导致 Watermark 不推进、窗口不触发。这是生产环境最容易忽略的配置。
3. 合理设置 Watermark 生成间隔 :默认 200ms(pipeline.auto-watermark-interval),大多数场景合适。低延迟场景可减小到 100ms,高吞吐场景可增大到 500ms 减少开销。
4. 迟到数据不要直接丢弃 :设置 allowedLateness 允许短时间迟到数据重新触发,或用 sideOutputLateData 侧输出单独处理。直接丢弃会导致统计结果偏小。
5. 多并行度注意数据倾斜:Watermark 取所有输入的最小值,数据倾斜会导致慢输入拖慢整体 Watermark。合理设置并行度和分区策略,保证各输入数据量均衡。
6. 事件时间字段必须是毫秒级时间戳:Flink 事件时间默认是毫秒级 Unix 时间戳(long 类型)。如果数据中是秒级时间戳,需要乘以 1000 转换,否则 Watermark 会完全错误(差 1000 倍)。
7. 不要在非 Source 算子生成 Watermark:Watermark 应该在 Source 或紧邻 Source 的 assignTimestampsAndWatermarks 中生成。在下游算子生成会导致时间语义混乱,且 Flink 1.11+ 不推荐。
8. 监控 Watermark Lag 告警:配置 Watermark Lag(系统时间 - Watermark)的告警阈值,超过阈值(如 5 分钟)时告警,及时发现 Watermark 不推进或数据延迟问题。
八、六大常见坑与解决方案
8.1 坑一:忘记设置 Watermark 导致窗口不触发
现象:用事件时间但没有调用 assignTimestampsAndWatermarks 设置 Watermark 策略,窗口永远不触发,没有任何输出。
原因:Watermark 默认是 Long.MIN_VALUE,永远不会超过窗口结束时间,窗口永远不触发。
解决方案:必须设置 WatermarkStrategy,最常用 forBoundedOutOfOrderness,并通过 withTimestampAssigner 从数据中提取时间戳。
8.2 坑二:乱序容忍时间设置过小导致数据丢失
现象:forBoundedOutOfOrderness(Duration.ofSeconds(2)) 设置的乱序容忍时间小于实际数据乱序程度(实际乱序 10 秒),迟到数据被丢弃,统计结果偏小。
原因:Watermark = 最大事件时间 - 乱序容忍时间。如果乱序容忍时间过小,Watermark 推进过快,迟到数据被当作过期数据丢弃。
解决方案:先监控最大乱序时间,设置为其 1.2-1.5 倍。或设置 allowedLateness 允许迟到数据重新触发。
8.3 坑三:空闲输入导致 Watermark 不推进
现象:某个上游子任务空闲,其 Watermark 不更新,下游取最小值导致整体不推进,窗口不触发。
原因:多输入 Watermark 取最小值,空闲输入的 Watermark 停留在旧值,拖慢整体。
解决方案:设置 withIdleness(Duration.ofMinutes(1)),空闲超时后忽略该输入。
8.4 坑四:事件时间戳是秒级不是毫秒级
现象:数据中时间戳是秒级 Unix 时间戳,直接传给 Flink(期望毫秒级),导致 Watermark 完全错误(差 1000 倍),窗口不触发或触发异常。
原因:Flink 事件时间默认是毫秒级时间戳,秒级时间戳差 1000 倍,Watermark 计算完全错误。
解决方案:秒级时间戳乘以 1000 转换为毫秒级:withTimestampAssigner((event, ts) -> event.timestamp * 1000)。
8.5 坑五:多并行度数据倾斜拖慢 Watermark
现象:某个输入数据慢(数据倾斜),其 Watermark 小,拖慢整体 Watermark,窗口触发延迟增加。
原因:多输入 Watermark 取最小值,慢输入的 Watermark 小,拖慢整体。
解决方案:合理设置并行度,调整分区策略保证数据均衡,监控各输入 Watermark,必要时用 withIdleness 处理慢输入。
8.6 坑六:单调递增策略用于有乱序的数据
现象:用 forMonotonousTimestamps 但实际数据有乱序,乱序数据被当作迟到数据默认丢弃,结果错误。
原因:单调递增策略假设数据按时间戳递增,没有乱序,Watermark = 最大事件时间。有乱序的数据会被当作迟到数据丢弃。
解决方案:有乱序必须用 forBoundedOutOfOrderness,不要用单调递增。单调递增只适用于数据已排序的理想场景。
九、总结与下一篇预告
Watermark 是 Flink 事件时间的核心机制,要点回顾:
第一,Watermark 本质是事件时间的时钟,表示"某个时间之前的数据都到齐了"。核心作用是驱动窗口触发、处理乱序数据、进度度量。关键特性是单调递增、启发式估计、全局时钟。
第二,两种生成方式:周期性生成(默认 200ms,大多数场景)和断点式生成(事件触发,特殊场景)。WatermarkGenerator 接口有两个方法:onEvent(每个事件到达时调用,更新状态或断点式生成)和 onPeriodicEmit(周期性调用,基于状态生成 Watermark)。
第三,内置策略:有界乱序(最常用,Watermark = 最大事件时间 - 乱序容忍时间)和单调递增(理想场景,Watermark = 最大事件时间)。有乱序的场景绝对不能用单调递增。
第四,传播与触发:Watermark 从 Source 生成,经过每个算子向下游传递。多并行度合并取最小值(保证最慢输入数据到齐才触发)。窗口触发条件是 Watermark ≥ 窗口结束时间。空闲输入必须用 withIdleness 处理,否则 Watermark 不推进、窗口不触发。
第五,监控与调优:监控 Current Watermark、Watermark Lag、Late Events、Idle Sources 四个指标。八条调优建议:先监控再设置乱序容忍、都设置 withIdleness、合理设置生成间隔、迟到数据不丢弃、注意数据倾斜、毫秒级时间戳、不在非 Source 生成、监控 Lag 告警。
第六,六大常见坑:忘记设置 Watermark(窗口不触发)、乱序容忍过小(数据丢失)、空闲输入(Watermark 不推进)、秒级时间戳(差 1000 倍)、数据倾斜(拖慢 Watermark)、单调递增用于有乱序数据(结果错误)。