flink time & window & watermark

本文详细介绍了Flink的时间机制,包括processing time、event time和ingest time,重点解析了watermark的概念和作用,以及如何处理event乱序。此外,还探讨了window的类型、触发器和evictor,提供了watermark策略的实践示例。

time机制

基于时间的流处理,可以对一段时间内(window)的数据做聚合操作,或当某个event到达时做特定操作

processing time

  1. event进入到某个operator(算子)的时间
  2. 划分data stream采用event被处理时所在的machine sytem time
  3. 采用processing time时,所有time-based操作,比如time window都采用所在machine的system时钟,设置time window为hourly processing time时,当application 启动时间为9.15 am时,第一个window区间是[9:15 am, 10:00 am],第二个窗口是[10:00 am, 11:00 am]
  4. data stream和machin之间五协调,性能最好,延迟最低,但是无法提供确定性,因为record到达machin的sytem time无法确定,比如上游系统是message queue时

event time

  1. event产生系统的machine system time,这个时间戳记录在event里,所以采用这种时间机制的event处理不受处理程序所在及其的system time影响,因为data stream的划分依赖event time
  2. 理论上来说,采用event time的处理系统产生的结果是确定的不受任何machine系统时间干扰,但是由于event可能会延迟到达处理系统,而处理系统等待的时间又是有限的,所以如何对待这些延迟数据会导致结果不一样
  3. 必须定义如何生成event time的watermarks

Ingest time

数据进入Apache Flink流处理系统的时间,也就是Flink读取数据源时间

三种time顺序

事件时间(Event Time)----> 提取时间(Ingestion Time)----> 处理时间(Processing Time)

flink设置时间机制

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);

watermark(水位线):

概念:衡量event time的进度(process),是data stream的一部分,带有一个时间戳,比如watermark(t)表示data stream的event time已经到了时刻t,所以当算子收到了时间戳为t的watermark之后,就会假设event time小于等于t的event都到了,data stream中不应该还有时间戳小于等于t的event

作用:告诉flink系统,什么时候触发对窗口的计算,主要时为了处理收到的乱序的event

event乱序:延迟、重试等,导致event到达流处理系统的顺序和event time顺序不一致

参考:

Timely Stream Processing

window

概念

将infinite data stream划分成有限片段,实现了从streaming到batch,然后对batch进行计算

keyed vs non-keyed window

对使用keyby分组的data stream调用window()来划分,没有使用keyby分组的data stream调用windowall()划分

lifecycle

字面意思来看,window就是一个时间区间窗口,有左右端点,左端点是开始时刻start_time,右端点是结束时刻end_time,start_time和end_time都是处理系统的system time

window被创建时间:属于window的第一个元素到达时

window被移除时间:event或porcessing time超过了window end timestamp+用户定义的allowed lateness时间时,flink仅仅移除基于时间的windows

window assigner

决定某个元素被分配到哪个/那些窗口

trigger

触发器,决定了一个窗口何时能被计算或清楚,策略类似于“当窗口中的元素数量大于4时“,或“当水位线通过窗口结束时”。

窗口分类

  1. 滚动窗口(tumbling window):大小固定且不重叠,每个元素被分配给单个窗口,比如指定大小为5分钟的滚动窗口,则将执行当前窗口,并且每五分钟将启动一个新窗口
  2. 滑动窗口(sliding window):大小固定可能有重叠,窗口大小、滑动大小,如果滑动大小小于窗口大小,滑动窗可以重叠,则每个元素可能被分配到多个窗口,比如使用窗口大小为10分钟的窗口,滑动大小为5分钟。这样,每5分钟会生成一个窗口,包含最后10分钟内到达的事件
  3. 会话窗口(session window):不重叠,没固定开始和结束时间,定义不活动时间长度,当时间到期时,会话完毕,窗口关闭,后续元素被分配到新会话窗口

evictor:

窗口触发计算条件

  • watermark的时间戳 > = window endTime
  • 在 [window_start_time,window_end_time] 中有数据存在。

参考:

Watermark

WaterMark时间可以用Flink系统现实时间,也可以用处理数据所携带的Event time

如果使用处理数据所携带的Event time作为WaterMark时间,需要注意两点:

  • 因为数据到达并不是循序的,注意保存一个当前最大时间戳作为WaterMark时间
  • 并行同步问题

water设定方法

标点(punctuated watermark):

  1. 通过数据流中某些特殊标记事件来触发新水位线的生成,此时窗口的触发与时间无关,而是决定于何时收到标记事件。
  2. 实际的生产中Punctuated方式在TPS很高的场景下会产生大量的Watermark在一定程度上对下游算子造成压力,所以只有在实时性要求非常高的场景才会选择Punctuated的方式进行Watermark的生成
  3. 调用WatermarkGenerator的对每个event调用onEvent()方法去提取event time时间戳

定期(periodic watermark):

  1. 周期性的(允许一定时间间隔或者达到一定的记录条数)产生一个Watermark。水位线提升的时间间隔是由用户设置的,在两次水位线提升时隔内会有一部分消息流入,用户可以根据这部分数据来计算出新的水位线。
  2. 在实际的生产中Periodic的方式必须结合时间和积累条数两个维度继续周期性产生Watermark,否则在极端情况下会有很大的延时,比如如果只设置时间间隔为10s,但是数据发送很快,10s就收集到了1w条,延迟就很高 ,或者只设置收集条数,但是数据发送很慢,10条数据花了2min,则延迟很夸张。
  3. 最简单的水位线算法就是取目前为止最大的事件时间,然而这种方式比较暴力,对乱序事件的容忍程度比较低,容易出现大量迟到事件。
  4. 调用WatermarkGenerator的对每个event调用onEvent()方法去提取信息,当发现该event是被标记的特殊event时立马emit一个watermark

Watermark生成器源码

/**
 * The {@code WatermarkGenerator} generates watermarks either based on events or
 * periodically (in a fixed interval).
 *
 * <p><b>Note:</b> This WatermarkGenerator subsumes the previous distinction between the
 * {@code AssignerWithPunctuatedWatermarks} and the {@code AssignerWithPeriodicWatermarks}.
 */
@Public
public interface WatermarkGenerator<T> {

    /**
     * Called for every event, allows the watermark generator to examine 
     * and remember the event timestamps, or to emit a watermark based on
     * the event itself.
     */
    void onEvent(T event, long eventTimestamp, WatermarkOutput output);

    /**
     * Called periodically, and might emit a new watermark, or not.
     *
     * <p>The interval in which this method is called and Watermarks 
     * are generated depends on {@link ExecutionConfig#getAutoWatermarkInterval()}.
     */
    void onPeriodicEmit(WatermarkOutput output);
}

自定义周期watermarkGenerator

可以基于event time或者process time来生成水印,watermark生成的interval(间隔)由 ExecutionConfig.setAutoWatermarkInterval(...) 定义,每次生成水印时调用WatermarkGenerator的onPeriodicEmit(),如果返回的watermark不为null且时间戳大于之前的watermark

/**
 * This generator generates watermarks assuming that elements arrive out of order,
 * but only to a certain degree. The latest elements for a certain timestamp t will arrive
 * at most n milliseconds after the earliest elements for timestamp t.
 */
public class BoundedOutOfOrdernessGenerator implements WatermarkGenerator<MyEvent> {

    private final long maxOutOfOrderness = 3500; // 3.5 seconds

    private long currentMaxTimestamp;

    @Override
    public void onEvent(MyEvent event, long eventTimestamp, WatermarkOutput output) {
        currentMaxTimestamp = Math.max(currentMaxTimestamp, eventTimestamp);
    }

    @Override
    public void onPeriodicEmit(WatermarkOutput output) {
        // emit the watermark as current highest timestamp minus the out-of-orderness bound
        output.emitWatermark(new Watermark(currentMaxTimestamp - maxOutOfOrderness - 1));
    }

}

/**
 * This generator generates watermarks that are lagging behind processing time 
 * by a fixed amount. It assumes that elements arrive in Flink after a bounded delay.
 */
public class TimeLagWatermarkGenerator implements WatermarkGenerator<MyEvent> {

    private final long maxTimeLag = 5000; // 5 seconds

    @Override
    public void onEvent(MyEvent event, long eventTimestamp, WatermarkOutput output) {
        // don't need to do anything because we work on processing time
    }

    @Override
    public void onPeriodicEmit(WatermarkOutput output) {
        output.emitWatermark(new Watermark(System.currentTimeMillis() - maxTimeLag));
    }
}

public class PunctuatedAssigner implements WatermarkGenerator<MyEvent> {

    @Override
    public void onEvent(MyEvent event, long eventTimestamp, WatermarkOutput output) {
        if (event.hasWatermarkMarker()) {
            output.emitWatermark(new Watermark(event.getWatermarkTimestamp()));
        }
    }

    @Override
    public void onPeriodicEmit(WatermarkOutput output) {
        // don't need to do anything because we emit in reaction to events above
    }
}

当tps很大时,使用标记水位线生成,如果特殊event太多会导致生成太多水位线,而每个水位线都会导致一次下游算子的计算,会导致性能低下

迟到事件

概念:由于延迟,event到达算子时,窗口已经关闭的,但是该事件本该被包含在窗口中,主要是因为延迟太大,导致到达前窗口关闭

三种处理方式

  1. 重新激活已经关闭的窗口并重新计算以修正结果。(allowed lateness)
  2. 将迟到事件收集起来另外处理。(side output)
  3. 将迟到事件视为错误消息并丢弃(flink默认处理)

side output:迟到事件单独放入一个数据流分支,这会作为 window 计算结果的副产品,以便用户获取并对其进行特殊处理

Allowed Lateness:

  1. 允许用户设置一个允许的最大迟到时长。Flink 会在窗口关闭后一直保存窗口的状态直至超过允许迟到时长,这期间的迟到事件不会被丢弃,而是默认会触发窗口重新计算
  2. 保存窗口状态需要额外内存,并且如果窗口计算使用了 ProcessWindowFunction API 还可能使得每个迟到事件触发一次窗口的全量计算,代价比较大
  3. 所以允许迟到时长不宜设得太长,迟到事件也不宜过多,否则应该考虑降低水位线提高的速度或者调整算法。

window & watermark总结

总结:

  1. 窗口window 的作用是为了周期性的获取数据。
  2. watermark的作用是防止数据出现乱序(经常),事件时间内获取不到指定的全部数据,而做的一种保险方法。
  3. allowLateNess是将窗口关闭时间再延迟一段时间。
  4. sideOutPut是最后兜底操作,所有过期延迟数据,指定窗口已经彻底关闭了,就会把数据放到侧输出流。

触发窗口的一个例子

假如我们设置10s的时间窗口(window),那么0-10s,10s-20s都是一个窗口,以0~10s为例,0为start-time,10为end-time。假如有4个数据的event-time分别是A=8,B=12.5,C=9,D=13.5,我们设置Watermarks为当前所有到达数据event-time的最大值减去延迟值3.5秒

当A到达的时候,Watermarks为max{8}-3.5=8-3.5 = 4.5 < 10,不会触发计算
当B到达的时候,Watermarks为max(12.5,8)-3.5=12.5-3.5 = 9 < 10,不会触发计算
当C到达的时候,Watermarks为max(12.5,8,9)-3.5=12.5-3.5 = 9 < 10,不会触发计算
当D到达的时候,Watermarks为max(13.5,12.5,8,9)-3.5=13.5-3.5 = 10 = 10,触发计算
触发计算的时候,会将A,C(因为他们都小于10)都计算进去,其中C是迟到的。

max这个很关键,就是当前窗口内,所有事件的最大事件。

这里的延迟3.5s是我们假设一个数据到达的时候,比他早3.5s的数据肯定也都到达了,这个是需要根据经验推算。假设加入D到达以后有到达了一个E,event-time=6,但是由于0~10的时间窗口已经开始计算了,所以E就丢了。

从这里上面E的丢失说明,水位线也不是万能的,但是如果根据我们自己的生产经验+侧道输出等方案,可以做到数据不丢失。

Kakfa connector & water strategies

kafka的每个partition数据隔离,所以时间戳也是隔离的,所以flink需要对kafka的每个partition单独生成watermark

FlinkKafkaConsumer<MyType> kafkaSource = new FlinkKafkaConsumer<>("myTopic", schema, props);
kafkaSource.assignTimestampsAndWatermarks(
        WatermarkStrategy
                .forBoundedOutOfOrderness(Duration.ofSeconds(20)));

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

operator对watermarkd的处理

operator需要在给下游转发watermark之前,完成会由watermark触发的所有操作

例子:windowoperator首先评估watermark是否触发window计算,有那些windows要被计算,只有把收到的watermark触发的所有window计算完毕并把计算结果先发送给下游之后才会转发水印给下游

对于TwoInputStreamOperator而言,收到2个水印,水印值定义为两个输入的最小值

flink的water strategies

watermark strategies包含了TimestampAssigner,WatermarkGenerator

  • TimestampAssigner 从元素中的某个字段访问/提取时间戳来完成的。
  • 时间戳分配与生成水位线密切相关,水位线告诉系统事件时间的进度。 您可以通过指定 WatermarkGenerator 来配置它。
public interface WatermarkStrategy<T> 
    extends TimestampAssignerSupplier<T>,
            WatermarkGeneratorSupplier<T>{

    /**
     * Instantiates a {@link TimestampAssigner} for assigning timestamps according to this
     * strategy.
     */
    @Override
    TimestampAssigner<T> createTimestampAssigner(TimestampAssignerSupplier.Context context);

    /**
     * Instantiates a WatermarkGenerator that generates watermarks according to this strategy.
     */
    @Override
    WatermarkGenerator<T> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context);
}

一般情况下不需要自己首先WatermarkStrategy,而使用 WatermarkStrategy 上的静态辅助方法来实现常见的水印策略,或者将自定义 TimestampAssigner 与 WatermarkGenerator 捆绑在一起

例子:使用lambda函数作为 timestamp assigner和有界无序水印生成器的水印策略

// lambda函数:(event, timestamp) -> event.f0
// 无界水印生成器:forBoundedOutOfOrderness
WatermarkStrategy
        .<Tuple2<Long, String>>forBoundedOutOfOrderness(Duration.ofSeconds(20))
        .withTimestampAssigner((event, timestamp) -> event.f0);

大多数情况下不需要自己指定TimestampAssigner ,kafka或kinesis直接使用record上的timestamp不需要指定

指定watermark的位置:

  1. source
  2. 如果source无法指定,则在source之后的某个位置指定

例子:在source之后的某个操作指定watermakr strategies

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

DataStream<MyEvent> stream = env.readFile(
        myFormat, myFilePath, FileProcessingMode.PROCESS_CONTINUOUSLY, 100,
        FilePathFilter.createDefaultFilter(), typeInfo);

DataStream<MyEvent> withTimestampsAndWatermarks = stream
        .filter( event -> event.severity() == WARNING )
        .assignTimestampsAndWatermarks(<watermark strategy>);

withTimestampsAndWatermarks
        .keyBy( (event) -> event.getGroup() )
        .window(TumblingEventTimeWindows.of(Time.seconds(10)))
        .reduce( (a, b) -> a.add(b) )
        .addSink(...);

解决某个分区没有事件的问题

如果某个分区(splits/partitions/shards)在一段时间内没有携带事件,这意味着 WatermarkGenerator 也没有获得任何新信息作为水印的基础。 我们称之为空闲输入或空闲源。 这是一个问题,因为您的某些分区可能仍然携带事件。 在这种情况下,水印将被阻止,因为它被计算为所有不同并行水印的最小值

可以使用 WatermarkStrategy 来检测空闲并将输入标记为空闲

WatermarkStrategy
        .<Tuple2<Long, String>>forBoundedOutOfOrderness(Duration.ofSeconds(20))
        .withIdleness(Duration.ofMinutes(1));

参考:

Flink的Watermark机制

Generating Watermarks

flink data stream api practice demo

GitHub - cbfsuper/flink-demo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值