1. 背景
在当今大数据时代,实时数据处理的需求日益增长,Flink 的窗口计算在这一领域中发挥着至关重要的作用。
窗口计算使得我们能够将无界的数据流切分成有意义的片段,从而进行特定时间段内的数据聚合和分析。它适用于众多场景,比如实时监控系统中对一段时间内的关键指标进行统计,金融交易中对特定窗口内的交易数据进行分析等。
与其他流式计算相比,Flink 窗口计算展现出了显著的优点。它提供了高度灵活的窗口定义方式,能够满足各种复杂的业务需求。无论是基于时间的滚动窗口、滑动窗口,还是基于数据驱动的窗口,Flink 都能轻松应对。同时,Flink 在处理大规模数据时具有出色的性能和稳定性,能够高效地处理高速流入的数据,确保计算结果的及时性和准确性。
2. Watermark
在 Flink 窗口计算中,还有一个关键概念与之紧密相关,那就是 watermark。watermark 对于处理乱序数据以及确保窗口计算的准确性起到了至关重要的作用。它就像是一个进度指示器,帮助我们在面对数据流的不确定性时,依然能够精确地进行窗口相关的操作和分析。
Watermark 是用来标记 Event-Time 的前进过程,表示较早的事件已经全部到达。
对于 顺序事件流1 比较好理解,W(10) 表示 10 之前的数据都已经到达。
对于 有界乱序事件流2,我们通常认为乱序事件流并不能完全乱序,需要在一定的时间限定内,这个时候我们可以指定 maxOutOfOrderness,表示可以容忍的乱序时间,当 11 到达以后,会有 W(7),表示 7 之前的数据都已经到达,当 24 到达以后,会有 W(20),表示 20 之前的数据都已经到达,这个时候如果 19 再来,就会被认为是无效的数据。
在 flink 程序中,通常一个算子会有多个并行度,他们之间 watermark 的传递入上图所示。
通过上面的介绍,我们知道了 watermark 是做什么的,那么他是怎么产生的?为什么每隔几个事件才产生一个 watermark?
这些都与 Watermark Generator 有关,我们可以通过 assignTimestampsAndWatermarks 指定要使用的 Watermark Generator
import cn.hutool.core.date.DateUtil;
import java.time.Duration;
import org.apache.commons.lang3.StringUtils;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class SocketDemo {
public static void main(String[] args) throws Exception {
// nc -lk 12345
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 从 1.12 开始, 默认 TimeCharacteristic 是 EventTime
// env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
DataStreamSource<String> dataStreamSource = env.socketTextStream("localhost", 12345);
// watermark 产生周期, 默认就是 200ms, 一般不需要做修改
// env.getConfig().setAutoWatermarkInterval(200);
dataStreamSource.assignTimestampsAndWatermarks(
WatermarkStrategy
.<String>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((element, recordTimestamp) -> {
// yyyyMMddHHmmss test message
String[] ss = StringUtils.split(element, StringUtils.SPACE);
return DateUtil.parse(ss[0]).getTime();
}));
dataStreamSource.print();
env.execute("demo");
}
}
从上面的示例可以看到,从 flink 1.12 起,为我们提供了以下几种周期发送 watermark 的 Watermark Generator
他们是根据 ExecutionConfig.getAutoWatermarkInterval() 来决定时间,默认是 200 毫秒,也就是说,默认 200 毫秒可能会产生一个新的 watermark
对于顺序事件使用 WatermarkStrategy.forMonotonousTimestamps()
对于乱序事件 WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(5))
3. Watermark 与 Window 之间的关系
window 通过 watermark 来判断是否需要计算窗口的数据,我们可以通过设置 watermark 的生成策略,来处理 window 中的乱序数据,示例如下
设置5秒的滚动窗口
按照顺序流的处理思路,当第四条数据 (20240618164205) 到达,这个时候就会触发窗口 [20240618164200, 20240618164205) 开始计算,那么迟到的第六条数据 (20240618164203) 就不会被统计到窗口中;
如果我们使用乱序事件流的 watermark 生成器,设置 maxOutOfOrderness = 3秒,那么只有当第七条数据 (20240618164209) 来到的时候,经过计算 9-1-3=5,watermark 变成 20240618164205,这个时候才会触发窗口 [20240618164200, 20240618164205) 开始计算,迟到的第六条数据 (20240618164203) 仍然可以被统计到窗口中。
示例代码:
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.Lists;
import java.time.Duration;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.assigners