markdown
Flink 延时数据处理,我们第一时间想到的是 **Watermark**,但是 Watermark 真的能够完全解决数据延时问题吗?
**肯定不能**。
通常对于延时数据的处理方式主要有 3 种:
1. **直接丢弃**
少量的数据丢失或许并不影响最终结果,毕竟离线任务还会再处理一遍。
2. **把迟到的部分单独再开一个 window 处理**
典型的"晚到数据再算一次"场景。
3. **把符合要求的数据重新导入到原窗口中**
触发更新(需要配合状态后端 + 查询性表/外部存储)。
下面重点介绍第 1、2 种常用方式。
参考博客:
[Flink笔记-延迟数据处理(Out Of Order & Late、AllowedLateness & OutputTag)](https://blog.csdn.net/yangxiaobo118/article/details/100173001)
## Out Of Order & Late
这两个概念都是为了处理乱序而产生的,区别如下:
- **Out Of Order**:通过 Watermark 机制解决,属于**第一层防护**,是全局性的,通常所说的"乱序问题解决方案"指的就是这一类。
- **Late Element**:通过窗口上的 `allowedLateness` 机制解决,属于**第二层防护**,只针对特定的 window operator。
## AllowedLateness & OutputTag
DataStream API 提供了 `allowedLateness` 方法来指定是否对迟到数据进行额外处理。
- 指定 `allowedLateness` 后,Flink 会把窗口的 **EndTime + allowedLateness** 作为窗口最终被销毁的时间。
- 当某条数据的 EventTime ≤ windowEnd + allowedLateness,但 Watermark 已经 > windowEnd 时,会**立即触发窗口计算**(而不是等待)。
- 如果 EventTime > windowEnd + allowedLateness,这条数据才会被真正丢弃或走 side output。
> 默认情况下,GlobalWindow 的最大 Lateness 是 `Long.MAX_VALUE`,所以数据会一直累积,永远不会被清理。
### 核心代码示例
```java
public class AllowLateness {
// 定义 Side Output Tag
private static final OutputTag<Tuple2<String, Integer>> myTag =
new OutputTag<Tuple2<String, Integer>>("myTag") {};
public static void main(String[] args) throws Exception {
List<Tuple2<String, Integer>> source = Lists.newArrayList();
source.add(new Tuple2<>("qingh1", 1));
source.add(new Tuple2<>("qingh2", 2));
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.enableCheckpointing(20000, CheckpointingMode.EXACTLY_ONCE);
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
SingleOutputStreamOperator<String> result = env.fromCollection(source)
.assignTimestampsAndWatermarks(new AssignerWithPunctuatedWatermarks<Tuple2<String, Integer>>() {
@Override
public long extractTimestamp(Tuple2<String, Integer> element, long previousElementTimestamp) {
return System.currentTimeMillis() - 1000;
}
@Nullable
@Override
public Watermark checkAndGetNextWatermark(Tuple2<String, Integer> lastElement, long extractedTimestamp) {
return new Watermark(System.currentTimeMillis() - 500);
}
})
.keyBy(t -> "key")
.timeWindow(Time.milliseconds(10))
.allowedLateness(Time.milliseconds(10))
.sideOutputLateData(myTag) // 真正的 late 数据会走这里
.process(new ProcessWindowFunction<Tuple2<String, Integer>, String, String, TimeWindow>() {
@Override
public void process(String key, Context context, Iterable<Tuple2<String, Integer>> elements, Collector<String> out) {
for (Tuple2<String, Integer> e : elements) {
out.collect(e.f0);
}
}
});
// 取出 side output(真正迟到的数据)
DataStream<Tuple2<String, Integer>> lateData = result.getSideOutput(myTag);
lateData.print();
env.execute("AllowLateness Demo");
}
}
常见疑问解答
Q:为什么有时候第一条数据没打印出来?
A:可能是因为 isSkippedElement 被标记为 true。
在 InternalTimeServiceManager#advanceWatermark 中会判断当前元素是否已经晚于 窗口最大时间戳 + allowedLateness 。如果满足该条件,isSkippedElement = true,该元素会被直接丢弃或输出到 side output(取决于是否配置了 sideOutputLateData。
加上 result.print() 后可以看到正常计算结果,side output 只包含真正"太晚"的数据。
关于侧输出(Side Output / OutputTag)
OutputTag是带有名称和类型信息的侧输出标识。- 支持侧输出的 Function 有:
- ProcessFunction
- CoProcessFunction
- ProcessWindowFunction
- ProcessAllWindowFunction
- 使用方式:在
Context.output(outputTag, value)中手动输出。 - 获取方式:
SingleOutputStreamOperator.getSideOutput(tag)
手动把数据输出到 Side Output 的例子
java
.timeWindow(Time.milliseconds(10))
.allowedLateness(Time.milliseconds(10))
.sideOutputLateData(myTag)
.process(new ProcessWindowFunction<Tuple2<String, Integer>, String, String, TimeWindow>() {
@Override
public void process(String s, Context context, Iterable<Tuple2<String, Integer>> elements, Collector<String> out) {
for (Tuple2<String, Integer> element : elements) {
// 正常逻辑可以忽略
// 手动把当前窗口所有数据输出到 side output
context.output(myTag, element);
}
}
});
DataStream<Tuple2<String, Integer>> side = result.getSideOutput(myTag);
side.print();
重点:context.output(myTag, element);
小结
| 机制 | 作用范围 | 是否触发计算 | 真正迟到数据处理方式 |
|---|---|---|---|
| Watermark | 全局 | 触发窗口关闭 | 默认丢弃 |
| allowedLateness | 单个窗口 | 延迟触发 | 仍参与计算 |
| sideOutputLateData | 单个窗口 | 不参与计算 | 输出到侧输出流,可单独处理 |
合理组合使用 Watermark + allowedLateness + sideOutputLateData,几乎可以覆盖所有延迟数据的处理需求。