大数据开发 | Flink的Watermark(水印机制)

Flink水印机制,简而言之,就是在Flink使用Event Time的情况下,窗口处理事件乱序和事件延迟的一种设计方案。本文从基本的概念入手,来看下Flink 水印机制的原理和使用方式。

一、什么是水位线

Watermark是Apache Flink提出的一种用来解决乱序、延迟数据等情况的解决方案,通常和窗口结合使用。例如在一个窗口内,对于延迟数据,我们不能一直无限期等待所有延迟数据到来后才触发窗口计算,因此提出了Watermark机制,由用户来决定等待延迟数据多久后触发计算。本质上来说Watermark就是单调递增的时间戳,来控制等待延迟数据的最大时长。对于watermark,可以在flink应用程序中两个地方使用:

1.直接在数据源上使用。该方式相对会比较好,因为数据源可以利用 watermark 生成逻辑中有关分片/分区(shards/partitions/splits)的信息。使用这种方式,数据源通常可以更精准地跟踪 Watermark,整体Watermark生成将更精确。

FlinkKafkaConsumer<MyType> kafkaSource = new FlinkKafkaConsumer<>("myTopic", schema, props);

kafkaSource.assignTimestampsAndWatermarks(

WatermarkStrategy

.forBoundedOutOfOrderness(Duration.ofSeconds(20)));

DataStream<MyType> stream = env.addSource(kafkaSource);

2.在操作算子上使用。当无法在数据源上使用时,则可以在算子操作上进行使用。

DataStream<MyEvent> withTimestampsAndWatermarks = stream

.filter( event -> event.severity() == WARNING )

.assignTimestampsAndWatermarks(<watermark strategy>);

由于类似于水流中用来做标志的记号,在Flink中,这种用来衡量事件时间(Event Time)进展的标记,就被称作“水位线”(Watermark)。

有序流中的水位线

如果当前数据量非常大,可能会有很多数据的时间戳是相同的,这时每来一条数据就提取时间戳、插入水位线就做了大量的无用功。而且即使时间戳不同,同时涌来的数据时间差会非常小(比如几毫秒),往往对处理计算也没什么影响。所以为了提高效率,一般会每隔一段时间生成一个水位线,这个水位线的时间戳,就是当前最新数据的时间戳,如图下图所示。所以这时的水位线,其实就是有序流中的一个周期性出现的时间标记。

 

注意: 对于水位线的周期性生成,周期时间是指处理时间(系统时间),而不是事件时间。

乱序流中的水位线

我们知道,在分布式系统中,数据在节点间传输,会因为网络传输延迟的不确定性,导致顺序发生改变,这就是所谓的“乱序数据”。

 

为了让窗口能够正确收集到迟到的数据,我们也可以等上2秒;也就是用当前已有数据的最大时间戳减去2秒,就是要插入的水位线的时间戳,如下图所示。这样的话,9秒的数据到来之后,事件时钟不会直接推进到9秒,而是进展到了7秒;必须等到11秒的数据到来之后,事件时钟才会进展到9秒,这时迟到数据也都已收集齐,0~9秒的窗口就可以正确计算结果了。

 

这里是等2秒,如果发现有的数据迟到时间超过2秒,那自然我们可以试着多等几秒,也就是把时钟调得更慢一些。最终的目的,就是要让窗口能够把所有迟到数据都收进来,得到正确的计算结果。对应到水位线上,其实就是要保证,当前时间已经进展到了这个时间戳,在这之后不可能再有迟到数据来了。

水位线的特性

现在我们可以知道,水位线就代表了当前的事件时间时钟,而且可以在数据的时间戳基础上加一些延迟来保证不丢数据,这一点对于乱序流的正确处理非常重要。我们可以总结一下水位线的特性:

水位线是Flink流处理中保证结果正确性的核心机制,它往往会跟窗口一起配合,完成对乱序数据的正确处理。

    • 水位线是插入到数据流中的一个标记,可以认为是一个特殊的数据。
    • 水位线主要的内容是一个时间戳,用来表示当前事件时间的进展。
    • 水位线是基于数据的时间戳生成的。
    • 水位线的时间戳必须单调递增,以确保任务的事件时间时钟一直向前推进。
    • 水位线可以通过设置延迟,来保证正确处理乱序数据。
    • 一个水位线Watermark(t),表示在当前流中事件时间已经达到了时间戳t, 这代表t之前的所有数据都到齐了,之后流中不会出现时间戳t’ ≤ t的数据。

二、如何生成水位线

2.1总体原则

我们知道,完美的水位线是“绝对正确”的,也就是一个水位线一旦出现,就表示这个时间之前的数据已经全部到齐、之后再也不会出现了。而完美的东西总是可望不可即,我们只能尽量去保证水位线的正确。如果对结果正确性要求很高、想要让窗口收集到所有数据,我们该怎么做呢?

                                                             图片源于网络,侵删

一个字,等。由于网络传输的延迟不确定,为了获取所有迟到数据,我们只能等待更长的时间。作为筹划全局的程序员,我们当然不会傻傻地一直等下去。那到底等多久呢?这就需要对相关领域有一定的了解了。比如,如果我们知道当前业务中事件的迟到时间不会超过5秒,那就可以将水位线的时间戳设为当前已有数据的最大时间戳减去5秒,相当于设置了5秒的延迟等待。

更多的情况下,我们或许没那么大把握。毕竟未来是没有人能说得准的,我们怎么能确信未来不会出现一个超级迟到数据呢?所以Flink中的水位线,其实是流处理中对低延迟和结果正确性的一个权衡机制,而且把控制的权力交给了程序员,我们可以在代码中定义水位线的生成策略。

2.2生成策略

在Flink的DataStreamAPI中,有一个单独用于生成水位线的方法:.assignTimestampsAndWatermarks(),它主要用来为流中的数据分配时间戳,并生成水位线来指示事件时间。具体使用时,直接用DataStream调用该方法即可,与普通的transform方法完全一样。

DataStream stream = env.addSource(new ClickSource());

DataStream withTimestampsAndWatermarks = stream.assignTimestampsAndWatermarks();

assignTimestampsAndWatermarks()方法需要传入一个WatermarkStrategy作为参数,这就是所谓的“水位线生成策略”。WatermarkStrategy中包含了一个“时间戳分配器”TimestampAssigner和一个“水位线生成器”WatermarkGenerator。

    • TimestampAssigner:主要负责从流中数据元素的某个字段中提取时间戳,并分配给元素。时间戳的分配是生成水位线的基础。
    • WatermarkGenerator:主要负责按照既定的方式,基于时间戳生成水位线。在WatermarkGenerator接口中,主要又有两个方法:onEvent()和onPeriodicEmit()。
    • onEvent:每个事件(数据)到来都会调用的方法,它的参数有当前事件、时间戳,以及允许发出水位线的一个WatermarkOutput,可以基于事件做各种操作。
    • onPeriodicEmit:周期性调用的方法,可以由WatermarkOutput发出水位线。周期时间为处理时间,可以调用环境配置的.setAutoWatermarkInterval()方法来设置,默认为200ms。env.getConfig().setAutoWatermarkInterval(60 * 1000L);

public interface WatermarkStrategy extends

TimestampAssignerSupplier, WatermarkGeneratorSupplier{

@Override TimestampAssigner createTimestampAssigner(TimestampAssignerSupplier.Context context);

@Override WatermarkGenerator createWatermarkGenerator(WatermarkGeneratorSupplier.Context context);

}

2.3Flink内置水位线生成器

生成器可以通过调用WatermarkStrategy的静态辅助方法来创建。它们都是周期性生成水位线的,分别对应着处理有序流和乱序流的场景。

(1)有序流

对于有序流,主要特点就是时间戳单调增长(Monotonously Increasing Timestamps),所以永远不会出现迟到数据的问题。这是周期性生成水位线的最简单的场景,直接调用 WatermarkStrategy.forMonotonousTimestamps()方法就可以实现。简单来说,就是直接拿当前最大的时间戳作为水位线就可以了。

stream.assignTimestampsAndWatermarks(

WatermarkStrategy.<Event>forMonotonousTimestamps()

.withTimestampAssigner(new SerializableTimestampAssigner<Event>()

{

@Override

public long extractTimestamp(Event element, long recordTimestamp)

{

return element.timestamp;

}

})

);

上面代码中我们调用.withTimestampAssigner()方法,将数据中的timestamp字段提取出来,作为时间戳分配给数据元素;然后用内置的有序流水位线生成器构造出了生成策略。这样,提取出的数据时间戳,就是我们处理计算的事件时间。这里需要注意的是,时间戳和水位线的单位,必须都是毫秒。

(2)乱序流

由于乱序流中需要等待迟到数据到齐,所以必须设置一个固定量的延迟时间(FixedAmount of Lateness)。这时生成水位线的时间戳,就是当前数据流中最大的时间戳减去延迟的结果,相当于把表调慢,当前时钟会滞后于数据的最大时间戳。调用WatermarkStrategy.forBoundedOutOfOrderness()方法就可以实现。这个方法需要传入一个maxOutOfOrderness参数,表示“最大乱序程度”,它表示数据流中乱序数据时间戳的最大差值;如果我们能确定乱序程度,那么设置对应时间长度的延迟,就可以等到所有的乱序数据了。代码示例如下:

import org.apache.flink.api.common.eventtime.WatermarkStrategy;

import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;

import java.time.Duration;

public class WatermarkTest {

public static void main(String[] args) throws Exception {

StreamExecutionEnvironment env =

StreamExecutionEnvironment.getExecutionEnvironment();

env.setParallelism(1);

env

.addSource(new ClickSource())

// 插入水位线的逻辑

.assignTimestampsAndWatermarks(

// 针对乱序流插入水位线,延迟时间设置为 5s

WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))

.withTimestampAssigner(new

SerializableTimestampAssigner<Event>() {

// 抽取时间戳的逻辑

@Override

public long extractTimestamp(Event element, long

recordTimestamp) {

return element.timestamp;

}

})

)

.print();

env.execute();

}

}

上面代码中,我们同样提取了timestamp字段作为时间戳,并且以5秒的延迟时间创建了处理乱序流的水位线生成器。

这里需要注意的是,乱序流中生成的水位线真正的时间戳,其实是当前最大时间戳–延迟时间–1,这里的单位是毫秒。这一点可以在BoundedOutOfOrdernessWatermarks的源码中明显地看到:

public void onPeriodicEmit(WatermarkOutput output) {

output.emitWatermark(newWatermark(

maxTimestamp - outOfOrdernessMillis - 1)); }

2.4自定义水位线策略

一般来说,Flink内置的水位线生成器就可以满足应用需求了。不过有时我们的业务逻辑可能非常复杂,这时对水位线生成的逻辑也有更高的要求,我们就必须自定义实现水位线策略WatermarkStrategy了。

整体说来,Flink有两种不同的生成水位线的方式:一种是周期性的(Periodic),另一种是断点式的(Punctuated)。周期性调用的方法中发出水位线,自然就是周期性生成水位线;而在事件触发的方法中发出水位线,自然就是断点式生成了。两种方式的不同就集中体现在这两个方法的实现上。

(1)周期性水位线生成器(PeriodicGenerator)

周期性生成器一般是通过onEvent()观察判断输入的事件,而在onPeriodicEmit()里发出水位线。下面是一段自定义周期性生成水位线的代码:

import org.apache.flink.api.common.eventtime.*;

import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

// 自定义水位线的产生

public class CustomWatermarkTest {

public static void main(String[] args) throws Exception {

StreamExecutionEnvironment env =

StreamExecutionEnvironment.getExecutionEnvironment();

env.setParallelism(1);

env

.addSource(new ClickSource())

.assignTimestampsAndWatermarks(new CustomWatermarkStrategy())

.print();

env.execute();

}

public static class CustomWatermarkStrategy implements

WatermarkStrategy<Event> {

@Override

public TimestampAssigner<Event>

createTimestampAssigner(TimestampAssignerSupplier.Context context) {

return new SerializableTimestampAssigner<Event>() {

@Override

136

public long extractTimestamp(Event element, long recordTimestamp)

{

return element.timestamp; // 告诉程序数据源里的时间戳是哪一个字段

}

};

}

@Override

public WatermarkGenerator<Event>

createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {

return new CustomPeriodicGenerator();

}

}

public static class CustomPeriodicGenerator implements

WatermarkGenerator<Event> {

private Long delayTime = 5000L; // 延迟时间

private Long maxTs = Long.MIN_VALUE + delayTime + 1L; // 观察到的最大时间戳

@Override

public void onEvent(Event event, long eventTimestamp, WatermarkOutput

output) {

// 每来一条数据就调用一次

maxTs = Math.max(event.timestamp, maxTs); // 更新最大时间戳

}

@Override

public void onPeriodicEmit(WatermarkOutput output) {

// 发射水位线,默认 200ms 调用一次

output.emitWatermark(new Watermark(maxTs - delayTime - 1L));

}

}

}

我们在onPeriodicEmit()里调用output.emitWatermark(),就可以发出水位线了;这个方法由系统框架周期性地调用,默认200ms一次。所以水位线的时间戳是依赖当前已有数据的最大时间戳的(这里的实现与内置生成器类似,也是减去延迟时间再减1),但具体什么时候生成与数据无关。

(2)断点式水位线生成器(PunctuatedGenerator)

断点式生成器会不停地检测onEvent()中的事件,当发现带有水位线信息的特殊事件时,就立即发出水位线。一般来说,断点式生成器不会通过onPeriodicEmit()发出水位线。自定义的断点式水位线生成器代码如下:

public class CustomPunctuatedGenerator implements WatermarkGenerator<Event> {

137

@Override

public void onEvent(Event r, long eventTimestamp, WatermarkOutput output) {

// 只有在遇到特定的 itemId 时,才发出水位线

if (r.user.equals("Mary")) {

output.emitWatermark(new Watermark(r.timestamp - 1));

}

}

@Override

public void onPeriodicEmit(WatermarkOutput output) {

// 不需要做任何事情,因为我们在 onEvent 方法中发射了水位线

}

}

我们在onEvent()中判断当前事件的user字段,只有遇到“Mary”这个特殊的值时,才调用output.emitWatermark()发出水位线。这个过程是完全依靠事件来触发的,所以水位线的生成一定在某个数据到来之后。

总结

水位线在事件时间的世界里面,承担了时钟的角色。也就是说在事件时间的流中,水位线是唯一的时间尺度。如果想要知道现在几点,就要看水位线的大小。水位线是一种特殊的事件,由程序员通过编程插入的数据流里面,然后跟随数据流向下游流动。

水位线的默认计算公式:水位线=观察到的最大事件时间–最大延迟时间–1毫秒。所以这里涉及到一个问题,就是不同的算子看到的水位线的大小可能是不一样的。因为下游的算子可能并未接收到来自上游算子的水位线,导致下游算子的时钟要落后于上游算子的时钟。

比如map->reduce这样的操作,如果在map中编写了非常耗时间的代码,将会阻塞水位线的向下传播,因为水位线也是数据流中的一个事件,位于水位线前面的数据如果没有处理完毕,那么水位线不可能弯道超车绕过前面的数据向下游传播,也就是说会被前面的数据阻塞。

这样就会影响到下游算子的聚合计算,因为下游算子中无论由窗口聚合还是定时器的操作,都需要水位线才能触发执行。这也就告诉了我们,在编写Flink程序时,一定要谨慎的编写每一个算子的计算逻辑,尽量避免大量计算或者是大量的IO操作,这样才不会阻塞水位线的向下传递。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值