1. 核心认知:它与"传统事件时间水位线"的区别
-
传统含义:事件时间水位线(event-time watermark)是"时间进度指示器",通常取最小上游水位以避免回退。
-
V2 新语义 :Watermark 是一种通用控制事件 ,由你定义标识符 与载荷类型(Long/Bool) ,还能配置多通道合并函数 与是否等待全部上游。
你甚至可以用 Long + MIN + "等待全部上游"去模拟传统"不会倒退"的时间进度信号;但它本质上仍是"自定义事件"。
2. 三步走总览
-
定义并声明(Define & Declare)
- 定义标识符、数据类型、合并函数、是否等待全部上游、以及框架的默认转发策略;
- 在
ProcessFunction#declareWatermarks或Source#declareWatermarks中声明 (每种 Watermark 类型全 Job 只需声明一次)。
-
发出(Emit)
- 在 Source 或 ProcessFunction 中创建并发出 Watermark。
-
处理(Handle)
- 下游
onWatermark(...)收到后,按业务处理并决定由框架转发 还是自行转发。
- 下游
3. 定义与声明:四个关键要素
3.1 必选:Identifier(全局唯一)
- 字符串、区分大小写 、全 Job 唯一 。建议用命名空间前缀:
order.BATCH_ROTATE、feature.FLAG_SWITCH。
3.2 必选:Data Type(Long / Bool)
- 仅支持两种:
Long与Bool。足以表达批次号/时间戳 或开关/就绪。
3.3 必选:合并函数 + 是否等待全部上游
- Long :
MIN/MAX - Bool :
AND/OR - combineWaitForAllChannels :是否"等齐所有上游"再输出合并结果(如类事件时间语义常设为 true 以避免回退)。
3.4 可选:WatermarkHandlingStrategy(框架转发策略)
FORWARD:默认转发到下游。IGNORE:框架忽略;由你在onWatermark里手动转发或不转发。
3.5 代码:用 Builder 定义并声明
java
// 定义一个 Long 型 Watermark:取最大值、不等齐、默认让框架转发
LongWatermarkDeclaration BATCH_WM = WatermarkDeclarations
.newBuilder("order.BATCH_ROTATE")
.typeLong()
.combineFunctionMax()
.combineWaitForAllChannels(false)
.defaultHandlingStrategyForward()
.build();
public class UpstreamPF implements OneInputStreamProcessFunction<Long, Long> {
@Override
public Set<? extends WatermarkDeclaration> declareWatermarks() {
// 每种 Watermark 在整个 Job 生命周期内只需声明一次
return Set.of(BATCH_WM);
}
}
小贴士:把声明集中在少数核心算子里,统一管理标识符与合并策略,避免分散定义导致冲突。
4. 发出 Watermark:在 Source 或 ProcessFunction
4.1 创建与发出
java
public class UpstreamPF implements OneInputStreamProcessFunction<Long, Long> {
@Override
public Set<? extends WatermarkDeclaration> declareWatermarks() { return Set.of(BATCH_WM); }
@Override
public void processRecord(Long record, Collector<Long> out, PartitionedContext<Long> ctx) throws Exception {
// 1) 正常数据处理
out.collect(record);
// 2) 条件满足时发出控制事件(例如:检测到批次切换)
LongWatermark wm = BATCH_WM.newWatermark(123456L); // 自定义载荷
ctx.getNonPartitionedContext().getWatermarkManager().emitWatermark(wm);
}
}
也可在 Source 中通过
sourceReaderContext.emitWatermark(watermark)直接发出。
4.2 发出频率建议
- 控制事件应稀疏 且语义明确(如"批次切换""配置生效"),避免水印风暴;
- 与数据产出打平:例如聚合 N 条/检测到边界时才发。
5. 处理 Watermark:onWatermark 钩子
5.1 处理与返回值语义
-
onWatermark(wm, out, nonPartCtx)返回WatermarkHandlingResult:PEEK:你只是"窥视"水印,由框架 按声明的策略(FORWARD/IGNORE)处理;POLL:你要自行处理/转发这个水印,框架不再干预。
5.2 代码:匹配标识符并处理
java
public class DownstreamPF implements OneInputStreamProcessFunction<Long, Long> {
private static final String ID = "order.BATCH_ROTATE";
@Override
public Set<? extends WatermarkDeclaration> declareWatermarks() { return Set.of(BATCH_WM); }
@Override
public WatermarkHandlingResult onWatermark(
Watermark wm, Collector<Long> out, NonPartitionedContext<Long> ctx) {
if (wm.getIdentifier().equals(ID)) {
// 强类型取值(假设为 LongWatermark)
long batchNo = ((LongWatermark) wm).getValue();
// 业务动作:例如轮转下游文件、重置窗口内计数、刷新本地缓存等
// rotateSink(batchNo);
// 让框架继续按声明策略转发
return WatermarkHandlingResult.PEEK;
// 如果你要自己转发或改写(例如添加附加信息),可:
// ctx.getWatermarkManager().emitWatermark(BATCH_WM.newWatermark(batchNo));
// return WatermarkHandlingResult.POLL;
}
return WatermarkHandlingResult.PEEK;
}
}
6. 多上游合并:什么时候 MIN/MAX/AND/OR?要不要"等齐"?
- 模拟"不会倒退的时间进度" :
Long + MIN + waitAll=true(类似事件时间水位线的合并语义)。 - "只要有一支上游就绪就开闸" :
Bool + OR + waitAll=false(容忍部分上游缺席/降级)。 - "所有上游都就绪才开闸" :
Bool + AND + waitAll=true(强一致 gate)。 - "批次号以最大者为准" :
Long + MAX + waitAll=false(快速跟随最领先分支)。
选择原则:安全优先 (不回退、不漏数) vs 时效优先(更快推进)。在回放/重放/断点续跑时尤其要谨慎。
7. 两个常用模式(可直接拿去用)
7.1 批次切换信号(Long Watermark)
- 定义 :
order.BATCH_ROTATE,typeLong + MAX + waitAll=false + FORWARD - 上游发出 :每当检测到 batch 切换或 checkpoint 后下一个批次开始,发
newWatermark(batchNo) - 下游处理 :
onWatermark中轮转 sink 、刷写当前批次 、重置统计器
7.2 "全链路就绪"门闸(Bool Watermark)
- 定义 :
feature.READY,typeBool + AND + waitAll=true + FORWARD - 上游发出 :当各分支完成 warmup/预加载后发
true - 下游处理 :收到
true才开始产出"对外可见"的结果;否则进入"预热/灰度"模式
8. 工程实践清单(上线前自查)
- 标识符规范 :
<域>.<对象>.<动作>,全局唯一,可观测(日志/指标同名)。 - 声明合一:集中在一个/少数类中管理所有 WatermarkDeclaration,避免冲突与重复。
- 转发策略 :默认
FORWARD最省心;需要"只在特定条件才转发"的,改用IGNORE+POLL自转发。 - 发出频率 :控制事件尽量稀疏;必要时做去重/抖动。
- 多上游合并 :是否需要
waitAll?Long 选MIN是否会引入回退风险?Bool 的AND/OR与业务容错是否一致? - 观测性 :为每种标识符打点到达频率、合并后值、等待通道数、下游处理耗时。
- 回放/重启:确认 Watermark 的"可重放"语义(幂等/去重),避免重复触发下游动作。
9. 完整示例:批次切换 + 下游轮转
java
// 1) 定义
LongWatermarkDeclaration BATCH_WM = WatermarkDeclarations
.newBuilder("order.BATCH_ROTATE")
.typeLong()
.combineFunctionMax()
.combineWaitForAllChannels(false)
.defaultHandlingStrategyForward()
.build();
// 2) 上游:按规则发出批次切换信号
public class BatchAwarePF implements OneInputStreamProcessFunction<Order, Order> {
@Override public Set<? extends WatermarkDeclaration> declareWatermarks() { return Set.of(BATCH_WM); }
private long currentBatch = -1L;
@Override
public void processRecord(Order o, Collector<Order> out, PartitionedContext<Order> ctx) throws Exception {
out.collect(o);
long batchNo = computeBatchNo(o.eventTime); // 你的批次计算逻辑
if (batchNo != currentBatch) {
currentBatch = batchNo;
ctx.getNonPartitionedContext().getWatermarkManager()
.emitWatermark(BATCH_WM.newWatermark(batchNo));
}
}
}
// 3) 下游:收到水印后轮转 sink / 刷写
public class RotateOnBatchPF implements OneInputStreamProcessFunction<Order, EnrichedOrder> {
@Override public Set<? extends WatermarkDeclaration> declareWatermarks() { return Set.of(BATCH_WM); }
@Override
public WatermarkHandlingResult onWatermark(Watermark wm, Collector<EnrichedOrder> out,
NonPartitionedContext<EnrichedOrder> ctx) {
if (wm.getIdentifier().equals("order.BATCH_ROTATE")) {
long batch = ((LongWatermark) wm).getValue();
// rotateFileSink(batch); flushCurrent(); resetAccumulators();
return WatermarkHandlingResult.PEEK; // 让框架继续转发
}
return WatermarkHandlingResult.PEEK;
}
@Override
public void processRecord(Order o, Collector<EnrichedOrder> out, PartitionedContext<EnrichedOrder> ctx) {
// 正常业务处理
out.collect(enrich(o));
}
}
10. 总结
- 把 Watermark 视为一种流内控制事件:可承载 Long/Bool、支持多上游合并与等待策略、可被框架自动转发或由你掌控。
- 三步用法清晰:定义并声明 → 发出 → 处理。
- 工程上用它解决:批次/切窗切换、阶段就绪门闸、策略生效广播、类事件时间进度等。
- 上线前务必完成:标识符规范、合并语义校验、转发策略清晰、发出频率可控、观测完善。