一、引入:乱序数据
当 Flink 使用 Event Time 模式处理数据流时,它会根据数据里的时间戳来处理基于时间的算子,这样就会由于网络、分布式等原因,|导致数据乱序。如多个并行度从kafka读取数据然后做keyBy操作,虽然kafka在一个分区内部的数据是有序的,但是因为keyBy的下游算子接收到的可能是多个分区的数据,这些数据是没有顺序保证的,那么我们该如何衡量事件时间已经进展到哪一步了呢?Flink提供了一种机制——Watermark。
二、Watermark概念和原理
-
Watermark 是一种衡量 Event Time 进展的机制,可以设定延迟触发。
-
数据流中的 Watermark 用于表示 timestamp 小于 Watermark 的数据都已经到达了,因此,window的执行也是由 Watermark 触发的。
-
Watermark 用来让程序自己平衡延迟和结果的正确性。
根据第一条定义,Wartermark 衡量 Event Time 的进展,即是可以起到 “调慢事件时间” 的作用,让程序误以为没有到达触发的事件。
如上述案例:如果把Watermark的延迟调为 3,那么当 1 到达时,Watermark 为 -2,4到达时,Watermark 为 1(表示 1 以前的数据都到达了),5 到达时,Watermark 为 2(表示 2 以前的数据都到达了),知道 Event Time 为 8 的记录到达时,才表示 5 以前的数据都到达,关闭第一个窗口 [1, 5)。
三、Watermark在任务间的传递
一个 subtask 可能接收到多个上游的 Watermark,它将这些 Watermark 存放到对应的分区中,并且向下游发送最小的 Watermark。
四、Watermark空闲问题
如果输入的数据源的某一个分区在一段时间内不携带事件,这意味着 WatermarkGenerator 也不会获得任何作为水印基础的新信息。我们称之为空闲输入或空闲源。这是一个问题,因为其它分区可能仍然携带事件。在这种情况下,任务将会被阻塞,因为wartermark是从所有分区中取最小值,这些非空闲的分区来的数据将都被缓存起来。
为了解决这个问题,Flink的WatermarkStrategy 提供了检查和标记输入为空闲的机制,用法如下:
WatermarkStrategy
.forBoundedOutOfOrderness[(Long, String)](Duration.ofSeconds(20))
.withIdleness(Duration.ofMinutes(1))
五、Wartermark对齐
在上一段中,我们讨论了数据源空闲并且可能阻止增加水印的情况。与之相反的是,某个数据源的数据可能被非常快地处理,从而导致它比其他源相对更快地增加其水印。这本身并不是问题。但是,对于使用水印来发出一些数据的operator来说,这实际上可能会成为一个问题。
在这种情况下,与空闲源相反,此类下游运算符(如聚合上的窗口连接)的水印可以继续进行。但是,这些oprator必须在状态中缓存来自快速输入的过量数据,等待其他慢的数据源的水印就绪后才能把这些数据发送出去。这可能导致算子状态的不可控制的增长。
为了解决这个问题,Flink引入了水印对齐,这将确保没有数据源的水印将增加得远远超出其他部分。为了实现对齐,Flink 将暂停那些声称太远的水印的源/任务的消费。与此同时,它将继续从其他源/任务读取记录,这可以将组合水印向前移动,从而解锁更快的水印。
使用方法如下
WatermarkStrategy
.<Tuple2<Long, String>>forBoundedOutOfOrderness(Duration.ofSeconds(20))
.withWatermarkAlignment("alignment-group-1", Duration.ofSeconds(20), Duration.ofSeconds(1));
参数解释:
第一个:源应该属于哪个组。用一个标签(例如alignment-group-1)来将共享它的所有源绑定在一起。
第二个:该组的所有源的当前最小水印的最大漂移;
第三个:最大水印应该更新的频率。频繁更新的缺点是,TM 和 JM 之间会传输更多 RPC 消息。
六、代码示例
public class WindowExample {
/**
* 两个数据流集合,对相同key进行内联,分配到同一个窗口下,合并并打印
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//watermark 自动添加水印调度时间
//env.getConfig().setAutoWatermarkInterval(200);
List<Tuple3<String, String, Integer>> tuple3List1 = Arrays.asList(
new Tuple3<>("A", "M", 10),
new Tuple3<>("A", "M2", 10),
new Tuple3<>("B", "M", 2),
new Tuple3<>("C", "M", 3),
new Tuple3<>("D", "M", 3)
);
List<Tuple3<String, String, Integer>> tuple3List2 = Arrays.asList(
new Tuple3<>("伍七", "girl", 18),
new Tuple3<>("吴八", "man", 30),
new Tuple3<>("A", "N", 1000),
new Tuple3<>("B", "N", 200),
new Tuple3<>("C", "N", 300)
);
//Datastream 1
DataStream<Tuple3<String, String, Integer>> dataStream1 = env.fromCollection(tuple3List1)
.assignTimestampsAndWatermarks(WatermarkStrategy
.<Tuple3<String, String, Integer>>forBoundedOutOfOrderness(Duration.ofSeconds(2))
.withTimestampAssigner((element, timestamp) -> System.currentTimeMillis()));
// DataStream<Tuple3<String, String, Integer>> dataStream1 = env.fromCollection(tuple3List1);
//Datastream 2
DataStream<Tuple3<String, String, Integer>> dataStream2 = env.fromCollection(tuple3List2)
//添加水印窗口,如果不添加,则时间窗口会一直等待水印事件时间,不会执行apply
.assignTimestampsAndWatermarks(WatermarkStrategy
.<Tuple3<String, String, Integer>>forBoundedOutOfOrderness(Duration.ofSeconds(2))
.withTimestampAssigner(new SerializableTimestampAssigner<Tuple3<String, String, Integer>>() {
@Override
public long extractTimestamp(Tuple3<String, String, Integer> element, long timestamp) {
return System.currentTimeMillis();
}
})
);
DataStream<Tuple3<String, String, Integer>> dataStream3 = dataStream1.union(dataStream2);
//对dataStream1和dataStream2两个数据流进行关联,没有关联也保留
//Datastream 3
SingleOutputStreamOperator<Tuple3<String, String, Integer>> newDataStream = dataStream3
.keyBy(f -> f.f0)
.window(TumblingEventTimeWindows.of(Time.seconds(1)))
// 对于从集合中加载到流中的case,在程序中会很快,所以要把时间窗口设置小
// .window(TumblingProcessingTimeWindows.of(Time.milliseconds(5L)))
// .window(TumblingProcessingTimeWindows.of(Time.minutes(1L)))
.reduce(new ReduceFunction<Tuple3<String, String, Integer>>() {
@Override
public Tuple3<String, String, Integer> reduce(Tuple3<String, String, Integer> value1, Tuple3<String, String, Integer> value2) throws Exception {
return new Tuple3<>(value1.f0, value1.f1 + value2.f1, value1.f2 + value2.f2);
}
});
newDataStream.print();
env.execute("flink CoGroup job");
}
}