window与watermark理解

转载自:https://blog.csdn.net/lmalds/article/details/52704170
    https://blog.csdn.net/sxiaobei/article/details/81147723
    https://blog.csdn.net/xiao_jun_0820/article/details/79786517

  实时计算中,对数据时间比较敏感,有eventTime和processTime区分,一般来说eventTime是从原始的消息中提取过来的,processTime是Flink自己提供的,Flink是可以基于eventTime计算,这个功能很有用,因为实时数据可能会经过比较长的链路,多少会有延时,并且有很大的不确定性,对于一些需要精确体现事件变化趋势的场景中,单纯使用processTime显然是不合理的。

  在Flink中基于eventTime计算,需要注意两点,首先要设置数据流的时间特征,下面的代码的意思是基于eventTime处理数据,
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
  其次,需要提取eventTime和设置WaterMark,因为数据格式不相同,设置warterMark的方式也有多种,下面具体分析一下,eventTime结合Watermark的工作方式。

window划分

  window是flink中划分数据一个基本单位,window的划分方式是固定的,默认会根据自然时间划分window,并且划分方式是前闭后开。
  比如定义了一个TumblingEventTimeWindows.of(Time.seconds(3)),那么会生成类似如下的窗口(左闭右开):

  [2018-03-03 03:30:00,2018-03-03 03:30:03)
  [2018-03-03 03:30:03,2018-03-03 03:30:06]
   …
  [2018-03-03 03:30:57,2018-03-03 03:31:00]

  当一个event time=2018-03-03 03:30:01的消息到来时,就会生成[2018-03-03 03:30:00,2018-03-03 03:30:03)这个窗口(而不是[2018-03-03 03:30:01,2018-03-03 03:30:04)这样的窗口),然后将消息buffer在这个窗口中(此时还不会触发窗口的计算),
  划分了window之后,触发window的计算,就可以得到这个window中的聚合结果了,其实基于eventTime和基于processTime计算最大的不同点就是在触发window的计算实际上不相同,通常数据流基于processTime,在window的endTime等于当前时间的时候就会触发计算,而eventTime因为数据有可能是乱序的,所以需要watermark的协助,完成window计算的触发。

Watermark

  watermark(水位线),只会不断上升,不会下降,用来保证在水位线之前的数据一定都到。
  提取WaterMark的方式有两类,一类是定时提取watermark,对应DataStream#assignTimestampsAndWatermarks(AssignerWithPeriodicWatermarks<T>),这种方式会定时提取更新wartermark,超过窗口的endTime时,才会真正触发窗口计算逻辑,然后清除掉该窗口。
  另一类伴随event的到来就提取watermark,就是每一个event到来的时候,就会提取一次Watermark,对应AssignerWithPunctuatedWatermarks,这样的方式当然设置watermark更为精准,但是当数据量大的时候,频繁的更新wartermark会比较影响性能。通常情况下采用定时提取就足够了。

  需要注意的是,watermark的提取工作在taskManager中完成,意味着这项工作是并行进行的的,而watermark是一个全局的概念。那么warkermark一般是怎么提取呢,这里引用官网的两个例子来说明。

/**
 * 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 AssignerWithPeriodicWatermarks<MyEvent> {
 
    private final long maxOutOfOrderness = 3500; // 3.5 seconds
 
    private long currentMaxTimestamp;
 
    @Override
    public long extractTimestamp(MyEvent element, long previousElementTimestamp) {
        long timestamp = element.getCreationTime();
        currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp);
        return timestamp;
    }
 
    @Override
    public Watermark getCurrentWatermark() {
        // return the watermark as current highest timestamp minus the out-of-orderness bound
        return new Watermark(currentMaxTimestamp - maxOutOfOrderness);
    }
}

  这个例子中extractTimestamp方法,在每一个event到来之后就会被调用,这里其实就是为了设置watermark的值,关键代码在于Math.max(timestamp,currentMaxTimestamp),意思是在当前的水位和当前事件时间中选取一个较大值,来让watermark流动。
  为什么要选取最大值,因为理想状态下,消息的事件时间肯定是递增的,实际处理中,消息乱序是大概率事件,所以为了保证watermark递增,要取最大值。而getCurrentWatermarker会被定时调用,可以看到方法中减了一个常量,这个原因在下面阐述。就这样,不断从eventTime中提取并更新watermark。

  而下面的例子中,并没有在提取eventTime的时候更新watermark的值,而是直接取系统当前时间减去一个常量,作为新的watermark。

/**
 * 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 AssignerWithPeriodicWatermarks<MyEvent> {
 
	private final long maxTimeLag = 5000; // 5 seconds
 
	@Override
	public long extractTimestamp(MyEvent element, long previousElementTimestamp) {
		return element.getCreationTime();
	}
 
	@Override
	public Watermark getCurrentWatermark() {
		// return the watermark as current time minus the maximum time lag
		return new Watermark(System.currentTimeMillis() - maxTimeLag);
	}
}

  上面两种代码中,提取watermark的时候都要减去一个常量,为了理解这么做的原因,需要了解,watermark的工作方式,上文提到在基于eventTime的计算中,需要watermark的协助来触发window的计算,触发规则是watermark大于等于window的结束时间,并且这个窗口中有数据的时候,就会触发window计算。

  举个例子说明其工作方式,当前window为10s,设想理想情况下消息都没有延迟,那么eventTime等于系统当前时间,假如设置watermark等于eventTIme的时候,当watermark = 00:00:10的时候,就会触发w1的计算,这个时候因为消息都没有延迟,watermark之前的消息[00:00:00~00:00:10)都已经落入到window中,所以会计算window中全量的数据。
  那么假如有一条消息data1,eventTime是00:00:01应该属于w1,在00:00:11才到达,因为假设消息没有延迟,那么watermark等于当前时间,00:00:11这个时候w1已经计算完毕,那么这条消息就会被丢弃,没有加入计算,这样就会出现问题。这是已经可以理解,代码中为什么要减去一个常量作为watermark,假设每次提取eventTime的时后,减去2s,那么当data1在00:00:11到达的时候,watermark才是00:00:09这个时候,w1还没有触发计算,那么data1会被加入w1,这个时候计算完全没有问题,所以减去一个常量是为了对延时的消息进行容错的。
  容忍一定时间的数据延迟,是可以通过WindowedStream#allowedLatenes方法来指定一个允许的延迟时间,比如allowedLateness(Time.seconds(5))允许5秒延迟。

猜你喜欢

转载自blog.csdn.net/zero__007/article/details/86656805