外观
Watermark 与迟到数据怎么处理
难度与前置
难度:进阶
前置:会写 Flink DataStream / SQL 窗口;理解事件时间 ≠ 处理时间。
读完能做什么:为乱序链路选定 Watermark 策略与迟到策略,用侧输出观察迟到量,并用窗口结果与对账清单验收。
适用场景
- 按事件时间做窗口聚合(每分钟消耗、每 5 分钟 UV)
- Kafka 分区内大致有序、分区之间或跨上游时钟存在乱序
- 业务允许有限迟到,但要知道「丢了多少」
不适用:
- 纯处理时间统计(不需要 Watermark)
- 离线 T+1 可重算窗口(用批补数往往更简单)
- 上游时间字段大量为 null / 未来时间未治理(先修质量)
真实场景(一条)
实时消耗 1 分钟窗口:Watermark = event_ts - 5s,乱序实际常达 30~90 秒。窗口关闭后大量数据被丢,分钟指标比离线对账低 5%~8%。
改法:Watermark 改为 event_ts - 1 min;窗口 allowedLateness = 2 min;迟到超出部分进侧输出告警并落 Kafka 供补数。对账分钟误差降到 < 0.5%,侧输出有监控。
怎么做(步骤清单)
1. 先分清三个时间
| 时间 | 含义 | 用途 |
|---|---|---|
| 事件时间 | 业务发生时刻(埋点/账单时间) | 窗口正确归属 |
| 摄入时间 | Source 读到的时间 | 近似,少用于严谨指标 |
| 处理时间 | 算子墙钟 | 触发、定时器;易受反压影响 |
口径文档写死:窗口按哪一个时间切。投放对账几乎总是事件时间。
2. 评估乱序再定 Watermark
sql
-- 离线粗估乱序:同一分钟窗口内,处理时间相对事件时间的延迟分布
-- 用 ODS 抽样即可
SELECT
approx_percentile(unix_timestamp(ingest_ts) - unix_timestamp(event_ts), ARRAY(0.5, 0.9, 0.99)) AS delay_pct
FROM ods_ad_spend_rt
WHERE dt = '${biz_date}';经验:
- Watermark 延迟 ≈ p99 乱序(或业务可接受的端到端延迟预算)
- 过大 → 结果触发慢;过小 → 丢数多
3. Watermark 策略(DataStream 示意)
java
WatermarkStrategy
.<SpendEvent>forBoundedOutOfOrderness(Duration.ofMinutes(1))
.withTimestampAssigner((e, ts) -> e.getEventTsEpochMilli())
.withIdleness(Duration.ofMinutes(2)); // 空闲分区推进,避免死等要点:
- 有界乱序最常用;单调源可用 ascending
- idleness:某 Kafka 分区长时间无数据时,仍允许 Watermark 前进
- 多源 Union 时注意 Watermark 取最小——慢源会拖住整图
4. 窗口 + 迟到策略
| 策略 | 行为 | 适用 |
|---|---|---|
| 默认(无 lateness) | 超过 Watermark 的数据丢弃 | 可丢、延迟敏感 |
allowedLateness | 窗口在关闭后仍可更新一段时间 | 可接受修订结果 |
侧输出 sideOutputLateData | 捕获仍迟到的元素 | 观测与补数 |
| 累积窗口 / 会话 | 按业务语义另建模 | 特殊场景 |
java
WindowedStream<...> windowed = stream
.keyBy(SpendEvent::getChannelId)
.window(TumblingEventTimeWindows.of(Time.minutes(1)))
.allowedLateness(Time.minutes(2))
.sideOutputLateData(lateTag);
SingleOutputStreamOperator<Agg> result = windowed.aggregate(...);
DataStream<SpendEvent> late = result.getSideOutput(lateTag);Flink SQL 对应思路:WATERMARK FOR event_ts AS event_ts - INTERVAL '1' MINUTE,并理解窗口输出时机。
5. 下游如何消化「窗口修订」
若开了 allowedLateness,同一窗口可能多次输出:
- Sink 必须 幂等 upsert(主键含窗口起止 + 维度)
- 或下游按版本号 / 最后更新时间覆盖
- 看板若不能接受数字回跳,改为「延迟更久但只输出一次」
6. 监控项(没有监控等于没做完)
| 指标 | 含义 |
|---|---|
| 当前 Watermark | 是否卡住(空闲分区、时钟回拨) |
| 迟到条数 / 字节 | 侧输出速率 |
| 窗口输出延迟 | 事件时间到结果可见的滞后 |
| 空闲分区数 | idleness 是否生效 |
7. 观测与告警
- 指标:当前 Watermark、事件时间最大/最小差、侧输出条数、窗口触发次数
- 告警:Watermark 落后事件时间 > 阈值(如 10min);侧输出速率突增
- 日志:抽样打印
event_ts与当前 Watermark,便于确认单位与时区
7. 补数与回溯
当侧输出持续超阈,或对账发现某段窗口系统性偏低:
- 把迟到事件落到补数 Topic / 表,带上原
event_ts - 用批作业或 Flink 有界源按事件时间重算问题窗口
- 结果 upsert 覆盖在线表同主键
- 回写事故记录:乱序分位数变化、上游发布、网络抖动等根因
日常路径:观测 → 告警 → 自动/半自动补数;不要只靠「把 Watermark 再加大」掩盖质量问题。
常见坑(至少牢记这些)
用处理时间窗口却按事件时间对账
反压时处理时间窗口会错位,对账永远对不上。Watermark 过小硬扛乱序
丢数被当成「Flink 不准」;先量化乱序再调参数。多源取最小 Watermark 被慢源拖死
某维表流 / 冷分区无数据,整条链路窗口不触发。加 idleness 或拆图。允许迟到但 Sink 非幂等
窗口多次发射导致重复累加,指标偏大。事件时间在未来或远古
坏时钟让 Watermark 狂奔或永不动;Source 侧过滤并打质量指标。
如何验收(可量化)
- [ ] 乱序分位数有抽样依据;Watermark 延迟书面可解释
- [ ] 开启侧输出;迟到量有告警(阈值按业务)
- [ ] 同窗口结果与离线重放(或 Kafka 可重放区间)差异在约定内
- [ ] Sink 对窗口更新幂等;人为注入迟到事件可看到修订而非双计
- [ ] Watermark 卡住演练:停某空闲分区后窗口仍能推进(idleness)
sql
-- 流批对账示例(分钟粒度)
SELECT
r.window_start,
r.cost_rt,
b.cost_batch,
r.cost_rt - b.cost_batch AS diff
FROM rt_channel_cost_1m r
JOIN batch_channel_cost_1m b
ON r.window_start = b.window_start
AND r.channel_id = b.channel_id
WHERE r.window_start >= TIMESTAMP '${start}'
AND r.window_start < TIMESTAMP '${end}';bash
# Flink Web UI:检查 Watermark、算子迟到侧输出吞吐相关面试题
占位:与题库小程序对齐后补链接(建议关联:事件时间 vs 处理时间、Watermark 原理、allowedLateness、侧输出)。
改写说明:结构与判断标准参考 Wood 原创数仓/面试文,已改写成可执行清单;未包含未公开公司数据与内部表名。