Flink的Windows计算

Flink的Windows计算

  • 1 Windows Assigner窗口分配器
    • 1.1 Keyed和Non-Keyed窗口
    • 1.2 Windows分配器
      • 1.2.1 Tumbling Windows
      • 1.2.2 Sliding Windows
      • 1.2.3 Session Windows
      • 1.2.4 Global Windows
    • 1.3 WindowsFunction
      • 1.3.1 ReduceFunction
      • 1.3.2 AggregateFunction
      • 1.3.3 FoldFunction
      • 1.3.4 ProcessWindowFunction
      • ProcessWindowFunction状态操作
      • 1.3.5 ProcessWindowFunction整合IncrementalAggregateWindowFunction实现
    • 1.4 窗口触发器(Trigger)
      • 1.4.1 Flink的窗口触发器
      • 1.4.2 自定义窗口触发器
    • 1.5 数据剔除器(Evictor)
      • 1.5.1 Flink数据剔除器
      • 1.5.2 自定义数据剔除器
    • 1.6 延迟数据处理

Windows窗口计算是流式计算中非常重要的数据计算方式之一。通过按照固定时间或长度将数据流切分成不同的窗口,让后对数据进行相应额聚合运算,从而得到一定范围内的统计结果。例如统计淘宝网近5分钟内的物品浏览数据,此时用户浏览数据不断的生成,但通过5分钟的窗口将数据限定在固定时间范围内,就可以对该范围内的有界数据进行分析,做好物品推荐。
Flink中DataStream将窗口抽象成独立的Operator。DataStream提供了大量内建窗口算子。

Keyde Windows算子对窗口计算流程

stream.keyBy(...)
     .window(指定窗口分配器)
     [.trigger(指定窗口触发器类型)]
                [.evictor(**指定evicator)]
                [.allowedLateness(指定是否延迟处理数据)]
                [.sideOutputLateData(指定Out Tag)]
                .reduce()[.apply()/fold()/aggregate()等窗口计算函数]
                [.getSideOutput(根据Tag输出数据)]
                

Non-Keyed Windows算子对窗口计算流程

stream
       .windowAll(...)           <-  required: "assigner"
      [.trigger(...)]            <-  optional: "trigger" (else default trigger)
      [.evictor(...)]            <-  optional: "evictor" (else no evictor)
      [.allowedLateness(...)]    <-  optional: "lateness" (else zero)
      [.sideOutputLateData(...)] <-  optional: "output tag" (else no side output for late data)
       .reduce/aggregate/fold/apply()      <-  required: "function"
      [.getSideOutput(...)]      <-  optional: "output tag"
DataStream API 含义 说明
.window/windowAll(…) 指定窗口分配器 所有窗口算子都必须指定Windows Assigner,其指定窗口的类型,定义如何将流数据分配到一个或多个窗口
.trigger() 指定窗口触发器类型(可选) 指定窗口触发的时机,定义窗口满足什么条件时触发计算。
.evictor() 指定evicator(可选) 数据剔除器,主要用于数据剔除
.allowedLateness()(可选) 指定是否延迟处理数据 时延设定,标记是否处理迟到数据,当迟到数据到达窗口中是否触发计算
.sideOutputLateData()(可选) 指定Out Tag 标记输出标签,然后再通过getSideOutput将窗口中的苏家根据标签输出;
.reduce() 窗口计算函数 定义窗口上数据处理逻辑,例如对数据进行reduce操作,此外还有.apply()/fold()/aggregate()等窗口计算函数,Windows function也是必须指定的
.getSideOutput()(可选) 根据Tag输出数据

1 Windows Assigner窗口分配器

1.1 Keyed和Non-Keyed窗口

根据上游数据是否为KeyedStream类型(将数据集安装Key分区),对应的Windows分配器也会有所不同。

  • KeyedStream类型数据集,则调用DataStream的window()方法指定Windows分配器,数据会根据Key在不同的Task实例中并行分别计算,最后得出针对每个Key统计的结果。
  • Non-Keyed类型数据集,则调用windowsAll()方法指定Windows分配器,所有的数据都会在窗口算子中路由到一个Task实例中计算,并得到全局统计结果。

由上面可以知,如果用户选择针对Key进行分区,就能够将相同的Key数据分配在同一个分区,例如统计一个网站在五分钟内不同用户的点击数。如果用户没有指定Key,此时需要对窗口上的数据进行全局统计计算,这种窗口被称为GlobalWindows,例如统计某一段时间内某网站所有的请求数。

//Keyed Stream,调用window方法指定Windows分配器
dataStream.keyBy(0)
          .window(new MyWindowAssigner())
          .sum(1);
//Non-Keyed Stream,对DataStream数据集,直接调用windowAll指定Windows分配器
dataStream.windowAll(new MyWindowAssigner())
          .sum(1);

1.2 Windows分配器

Flink支持基于时间的窗口和基于数据的窗口两种类型的窗口。

  • 基于时间的窗口
    窗口基于起始时间戳(闭区间)和终止时间戳(开区间)来决定窗口的大小。数据根据时间戳被分配到不同的窗口汇总完成计算。Flink使用TimeWindow类来获取窗口的起始时间和终止时间,以及该窗口允许进入的最新时间戳信息等元数据。
  • 基于数量的窗口
    根据固定的数量定义窗口的大小,例如每1000条数据形成一个窗口,**窗口中接入的数据依赖于数据接入到算子中的顺序,如果数据出现乱序情况,将导致窗口的计算结果不确定。**Flink中可以通过调用DataStream的countWindows()来定义基于数量的窗口。

Windows分配器将接入数据分配到不同的窗口,根据Windows分配器数据分配方式的不同将Windows分为4大类,分别是滚动窗口(Tumbling Windows)、滑动窗口(Sliding Windows)、会话窗口(Session Windows)和全局窗口(Global Windows),这些窗口都已在Flink中实现了,可直接调用windows()或者windowsALL方法来指定WindowsAssigner即可。

1.2.1 Tumbling Windows

滚动窗口是根据固定时间或大小进行切分,且窗口和窗口之间的元素互不重叠。这种类型的窗口比较简单,但可能会导致某些有前后关系的数据计算结果不正确,而对于按照固定大小和周期统计某一指标的这种类型的窗口计算就比较适合,同时实现起来也比较方便。
DataStream中提供了基于EventTime和ProcessTime两种类型的Tumbling窗口。它们分别是TumblingEventTimeWindows和TumblingProcessingTimeWindows,可以用window或者windowAll指定。示例如下

//Keyed Stream,调用window方法指定Windows分配器
dataStream.keyBy(0)
        //或者
        //.window(TumblingEventTimeWindows.of(Time.minutes(5)))
        .window(TumblingProcessingTimeWindows.of(Time.minutes(5)))
        .process(...);//定义窗口函数
        
//Non-Keyed Stream,对DataStream数据集,直接调用windowAll指定Windows分配器
dataStream.windowAll(TumblingEventTimeWindows.of(Time.minutes(5)))
        //或者
        //.windowAll(TumblingProcessingTimeWindows.of(Time.minutes(5)))
        .process(...);//定义窗口函数

代码中用of方法定义窗口大小,其时间单位可以用Time类指定。
此外,还可以用DataStream的timeWindow()方法,快捷方法定义TumblingEventTimeWindows和TumblingProcessingTimeWindows,如下

 dataStream.keyBy(0)
         //窗口类型根据设置的time characteristic确定。
         .timeWindow(Time.minutes(5))
         .process(...);//定义窗口函数
 //Non-Keyed Stream,对DataStream数据集,直接调用windowAll指定Windows分配器
 // 窗口类型根据设置的time characteristic确定。
 dataStream.timeWindowAll(Time.minutes(5))
         .process(...);//定义窗口函数

窗口类型根据用户在ExecationEnvironment中设定的Time characteristic确定。默认窗口时间的时区是UTC-0,其他区均需要通过设定时间偏移量调整时区,爱国内需要指定Time.hours(-8)的偏移量。

1.2.2 Sliding Windows

滑动窗口是在滚动窗口的基础上增加了窗口的滑动时间(slide time),且允许窗口数据发生重叠。
windows size固定后,窗口并不像滚动窗口按照windows size向前移动,而是根据设定的Slide time向前滑动。滑动窗口的数据重叠大小根据windows size和slide time决定。

  • windows size>slide time,发生窗口重叠
  • window size = slide time,Sliding窗口变成了Tumbling窗口。
  • window size < slide time,窗口不连续,数据可能出现不落入任何一个窗口的情况。

滑动窗口帮助用户根据设定的统计频率计算指定窗口大小的统计指标。如每个30s统计最近5分钟内活跃用户数等。
Flink提供了基于Event Time和基于Process Time的滑动窗口。

dataStream.keyBy(0)
        .window(SlidingProcessingTimeWindows.of(Time.minutes(5), Time.seconds(30)))
        //或者
        // .window(SlidingEventTimeWindows.of(Time.minutes(5),Time.seconds(30)))
        .process(...);//定义窗口函数
dataStream
        .windowAll(SlidingProcessingTimeWindows.of(Time.minutes(5), Time.seconds(30)))
        //或者
        // .windowAll(SlidingEventTimeWindows.of(Time.minutes(5),Time.seconds(30)))
        .process(...);//定义窗口函数

timeWindow()方法快捷设置

dataStream.keyBy(0)
        .timeWindow(Time.minutes(5), Time.seconds(30))
        .process(...);//定义窗口函数
dataStream
        .timeWindowAll(Time.minutes(5), Time.seconds(30))
        .process(...);//定义窗口函数

timeWindow指定的参数分别是windows size、slide time。如果在国内设定为Time.hours(-8)。
窗口的类型仍然根据用户在ExecationEnvironment中设定的Time characteristic确定。

1.2.3 Session Windows

会话窗口住哟啊将某短时间内活跃度较高的数据聚合在一个窗口进行计算。窗口的触发条件是Session Gap,是在规定的时间内如果没有数据活跃接入,则认为窗口结束,然后触发窗口计算结果。如果数据一直不间断地进入窗口,会导致窗口始终不触发。Session窗口不需要固定windows size和slide time,只需要定义session gap,来规定不活跃数据的时间上限制,然后窗口根据这个时间来判断市局是否属于同一活跃数据集,从而将数据切分成不同的窗口进行计算。
session窗口适合非连续型数据或周期性数据的场景。同样flink提供了基于EventTime和ProcessTime的session窗口,分别是EventTimeSessionWindows和ProcessingTimeSessionWindows。
SessionWindows提供了两种方法创建对应类型的SessionWindows,分别是withGap和withDynamicGap。

  • withGap方法用于创建固定gap的session窗口。固定session gap的示例
dataStream.keyBy(0)
        //.window(EventTimeSessionWindows.withGap(Time.minutes(4)))
        .window(ProcessingTimeSessionWindows.withGap(Time.minutes(5)))
        .process(...);//定义窗口函数
  • withDynamicGap方法用于创建可动态调整gap的session窗口。动态调整session gap需要实现SessionWindowTimeGapExtractor接口,该接口定义如下:
@PublicEvolving
public interface SessionWindowTimeGapExtractor<T> extends Serializable {
	/**
	 * Extracts the session time gap.
	 * @param element The input element.
	 * @return The session time gap in milliseconds.
	 */
	long extract(T element);
 }

extract方法用于实现动态session gap的抽取逻辑。用户将实现好的动态session Gap抽取器传入withDynamicGap方法中即可。
Session窗口本质上没有固定的起止时间点,其flink底层计算逻辑实现上与Tumbling窗口及Sliding窗口有一定区别。SessionWindows会每个进入的数都创建一个窗口,最后再将距离SessionGap最近的窗口进行合并,然后计算窗口结果。因此SessionWindows需要能够合并的Trigger和WindowsFunction,如ReduceFunction、AggregateFunction、FoldFunction和ProcessWindowFunction等

1.2.4 Global Windows

全局窗口将所有相同Key的数据分配到单个窗口中计算结构,窗口没有起始和结束时间,窗口需要借助Trigger来触发计算,如果不对全局窗口指定Trigger,窗口是不会触发计算的,因此使用该类型窗口需慎重,用户需要非常明确自己在整个窗口中统计出的结果是什么,并指定对应的触发器,同时还需要指定相应的数据清理机制,否则数据将一直留在内存中。

dataStream.keyBy(0)
        //通过GlobalWindows定义全局窗口
        .window(GlobalWindows.create())
        .process(...)

全局窗口通过GlobalWindows创建。关于Trigger的定义,下面会将。

1.3 WindowsFunction

对数据集的处理中定义了Window分配器后,下一步就是实现窗口内数据的计算逻辑,即WindowsFunction的定义。
目前Flink系统提供了WindowsFunction,如ReduceFunction、AggregateFunction、FoldFunction和ProcessWindowFunction等四种类型的WindowsFunction。这四种窗口计算函数按照计算原理可分为两大类:增量聚合函数(如ReduceFunction、AggregateFunction和FoldFunction)和全量窗口函数(如ProcessWindowFunction)。

  • 增量聚合函数
    该类函数基于中间状态的计算结果,窗口中只维护中间结果状态值,不需要缓存原始数据,因此该类函数计算性能较高,占用存储空间少。
  • 全量窗口函数
    相比于增量聚合函数,全量窗口函数性能比较弱,占用存储空间较大,这是因为此时算子需要对所属于该窗口的接入数据进行缓存,然后等到窗口触发的时候,对所有的原始数据进行汇总计算。如果接入数据量比较大或窗口时间比较长,就比较有可能导致计算性能的下降。

1.3.1 ReduceFunction

Reduce函数定义了对输入的两个相同类型的数据按照预先定义的计算方法进行聚合处理,然后输出一个结果数据,输出类型与输入类型相同。在reduce()方法中指定ReduceFunction计算逻辑。有两种方法定义计算逻辑,一种是创建ReduceFunction接口的实现类,另一种是以Lambada表达式定义计算逻辑。

  1. 创建ReduceFunction接口的实现类
    接口ReduceFunction定义如下
@Public
@FunctionalInterface
public interface ReduceFunction<T> extends Function, Serializable {

	/**
	 * The core method of ReduceFunction, combining two values into one value of the same type.
	 * The reduce function is consecutively applied to all values of a group until only a single value remains.
	 *
	 * @param value1 The first value to combine.
	 * @param value2 The second value to combine.
	 * @return The combined value of both input values.
	 *
	 * @throws Exception This method may throw exceptions. Throwing an exception will cause the operation
	 *                   to fail and may trigger recovery.
	 */
	T reduce(T value1, T value2) throws Exception;
}

其中,T reduce(T value1, T value2)函数主要用来实现计算逻辑,其有两个参数,第一个参数value1是上一次调用reduce函数计算的结果,value2是数据集下一个元素。因此Reduce是一种基于中间状态的计算结果的增量计算函数,即下一次的reduce计算依赖于本次调用reduce函数计算的结果。一个简单的接口实现示例如下:

class MyReduceFunction implements ReduceFunction<Tuple2<String, Long>> {
    @Override
    public Tuple2<String, Long> reduce(Tuple2<String, Long> value1, Tuple2<String, Long> value2) throws Exception {
        return new Tuple2<>(value1.f0, value1.f1 * value1.f1);
    }
}

示例中ReduceFunction逻辑是将数据的第二个元素求积。
reduce函数指定ReduceFunction

dataStream.assignTimestampsAndWatermarks(new AscendingTimestampExtractor<Tuple2<String, Long>>() {
            @Override
            public long extractAscendingTimestamp(Tuple2<String, Long> element) {
                return element.f1;
            }
        })
                .keyBy(0)
                                .timeWindow(Time.seconds(60))
                .reduce(new MyReduceFunction())
                .print();
  1. Lambada表达式定义ReduceFunction计算逻辑
    Lambada表达式实现比较简单,直接在reduce方法中用lambada表达式方式实现计算逻辑。
//Lambada表达式实现
dataStream.
        ...
        .reduce((v1, v2) -> new Tuple2<>(v1.f0, v1.f1 * v2.f1));

1.3.2 AggregateFunction

AggregateFunction与ReduceFunction一样,也是一种基于中间计算结果的增量计算函数。AggregateFunction接口定义如下。

/**
 * @param   待聚合数据的类型(输入数据)
 * @param  accumulator类型 (中间聚集状态).
 * @param  聚合结果的类型
 */
@PublicEvolving
public interface AggregateFunction<IN, ACC, OUT> extends Function, Serializable {

	/**
	 * 创建一个accumulator,开始聚合
	 * 除非通过add(Object, Object)添加值,否则新的accumulator通常是没有意义的
	 * The accumulator is the state of a running aggregation. When a program has multiple
	 * aggregates in progress (such as per key and window), the state (per key and window)
	 * is the size of the accumulator.
	 * @return A new accumulator, corresponding to an empty aggregate.
	 */
	ACC createAccumulator();

	/**
	 * 将给定的输入值添加到给定的accumulator,并返回新的累加器值。
	 * 为了提高效率,可以修改输入的累加器,然后再返回。
	 * @param value The value to add
	 * @param accumulator The accumulator to add the value to
	 */
	ACC add(IN value, ACC accumulator);

	/**
	 * 从accumulator获取聚合的结果
	 * @param accumulator The accumulator of the aggregation
	 * @return The final aggregation result.
	 */
	OUT getResult(ACC accumulator);

	/**
	 * 合并两个accumulators,并返回带有合并状态的accumulator 。
	 * 

This function may reuse any of the given accumulators as the target for the merge * and return that. The assumption is that the given accumulators will not be used any * more after having been passed to this function. * * @param a An accumulator to merge * @param b Another accumulator to merge * * @return The accumulator with the merged state */ ACC merge(ACC a, ACC b); }

在所提供的函数中,add()方法定义数据的添加逻辑,getResult()定义由accumulator计算结果的逻辑,merge()方法定义合并accumnlator的逻辑。以下示例实现了对数据集中字段求平均值的聚合运算。

import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.java.tuple.Tuple2;

public class AverageAggregateFunction implements AggregateFunction<Tuple2<String, Long>, Tuple2<Long, Long>, Double> {
    // 创建累加器
    @Override
    public Tuple2<Long, Long> createAccumulator() {
        return new Tuple2<>(0L, 0L);
    }

    // 定义输入的数据累加地逻辑
    @Override
    public Tuple2<Long, Long> add(Tuple2<String, Long> value, Tuple2<Long, Long> accumulator) {
        return new Tuple2<>(accumulator.f0 + value.f1, ++accumulator.f1);
    }

    // 由累加器计算结果
    @Override
    public Double getResult(Tuple2<Long, Long> accumulator) {
        return accumulator.f0 * 1.0 / accumulator.f1;
    }

    // 定义累加器合并的逻辑
    @Override
    public Tuple2<Long, Long> merge(Tuple2<Long, Long> a, Tuple2<Long, Long> b) {
        return new Tuple2<>(a.f0 + a.f0, a.f1 + a.f1);
    }
}

使用定义好的聚合函数

dataStream.assignTimestampsAndWatermarks(new AscendingTimestampExtractor<Tuple2<String, Long>>() {
    @Override
    public long extractAscendingTimestamp(Tuple2<String, Long> element) {
        return element.f1;
    }
}).keyBy(0)
        .timeWindow(Time.seconds(60))
        //指定聚合函数逻辑
        .aggregate(new AverageAggregateFunction())
        .print("avg");

1.3.3 FoldFunction

FoldFunction定义了如何将窗口中的输入元素与外部的元素合并的逻辑。但FoldFunction已经被@Deprecated标记,未来可能会被移除,Flink建议用AggregateFunction来替换实现。

1.3.4 ProcessWindowFunction

ProcessWindowFunction比ReduceFunction和AggregateFunction更加灵活,用户可以利用其实现更加复杂的计算逻辑,更加灵活的支持基于窗口中全部数据或者需要操作窗口中的某些状态数据和窗口元数据等更复杂的指标统计逻辑,诸如统计窗口数据某一字段的中位数和众数。抽象类ProcessWindowFunction定义如下。

@PublicEvolving
public abstract class ProcessWindowFunction<IN, OUT, KEY, W extends Window> extends AbstractRichFunction {
	/**
	 * 评估窗口并且定义窗口输出的元素
	 * @param key The key for which this window is evaluated.
	 * @param context The context in which the window is being evaluated.
	 * @param elements The elements in the window being evaluated.
	 * @param out A collector for emitting elements.
	 *
	 * @throws Exception The function may throw exceptions to fail the program and trigger recovery.
	 */
	public abstract void process(KEY key, Context context, Iterable<IN> elements, Collector<OUT> out) throws Exception;

	/**
	 * 定义每个窗口计算结束后中间状态的清理逻辑。
	 * @param context The context to which the window is being evaluated
	 * @throws Exception The function may throw exceptions to fail the program and trigger recovery.
	 */
	public void clear(Context context) throws Exception {}

	/**
	 * 承载窗口元数据的上下文
	 */
	public abstract class Context implements java.io.Serializable {
		/**
		 * 返回正在评估的窗口
		 */
		public abstract W window();

		/** 返回窗口当前的处理时间. */
		public abstract long currentProcessingTime();

		/** 返回窗口当前的event-time的watermark. */
		public abstract long currentWatermark();

		/**
		 * State accessor for per-key and per-window state.
		 * 返回每个窗口中间状态
		 *
		 * 

NOTE:If you use per-window state you have to ensure that you clean it up * by implementing {@link ProcessWindowFunction#clear(Context)}. */ public abstract KeyedStateStore windowState(); /** * 返回每个key对应的中间状态 */ public abstract KeyedStateStore globalState(); /** * 根据OutputTag的标志输出数据记录 * @param outputTag the {@code OutputTag} that identifies the side output to emit to. * @param value The record to emit. */ public abstract <X> void output(OutputTag<X> outputTag, X value); } }

下面展示了一个基于Key统计数据最小值、最大值、平均值和求和的ProcessWindowFunction子类示例。

public class StatisticsProcessWindowFunction extends ProcessWindowFunction<Tuple2<String, Long>/*IN*/,
        Tuple6<String, Long, Long, Long, Double, Long>/*OUT*/,
        String/*KEY*/,
        TimeWindow/*W*/> {

    @Override
    public void process(String key, Context context,
                        Iterable<Tuple2<String, Long>> elements,
                        Collector<Tuple6<String, Long, Long, Long, Double, Long>> out) throws Exception {


        Long size = 0L;
        Tuple6<String, Long, Long, Long, Double, Long> tuple6 = new Tuple6<>(key, 0L, null, null, 0.0, 0L);
        elements.forEach(t -> {
            tuple6.f1 += t.f1;
            tuple6.f2 = min(tuple6.f2, t.f1);
            tuple6.f3 = max(tuple6.f3, t.f1);
            tuple6.f4 += 1;
        });
        Double avg = tuple6.f1 * 1.0 / tuple6.f4;
        Long windowsEndTime = context.window().getEnd();
        tuple6.f4 = avg;
        tuple6.f5 = windowsEndTime;

        //通过out.collect()返回计算结果
        out.collect(tuple6);

    }

    private Long min(Long a, Long b) {
        if (null == a) {
            return b;
        }
        if (null == b) {
            return b;
        }
        return Math.min(a, b);
    }

    private Long max(Long a, Long b) {
        if (null == a) {
            return b;
        }
        if (null == b) {
            return b;
        }
        return Math.max(a, b);
    }
}

示例并不操作状态数据,只需要实现Process方法即可。使用StatisticsProcessWindowFunction 只需要在API中用process方法指定即可,如下所示。

dataStream.assignTimestampsAndWatermarks(new AscendingTimestampExtractor<Tuple2<String, Long>>() {
    @Override
    public long extractAscendingTimestamp(Tuple2<String, Long> element) {
        return element.f1;
    }
}).keyBy(0)
        .timeWindow(Time.seconds(60))
        .process(new StaticProcessWindowFunction())
        .print("pro");

使用ProcessWindowFunction完成简单的聚合运算非常浪费,使用ProcessWindowFunction时,要明确自己的业务场景,选择合适的WindowFunction来统计,没必要不不建议用ProcessWindowFunction。
增量聚合函数如ReduceFunction和AggregateFunction虽然在一定程度上能够提升窗口计算的性能,但这些窗口的灵活性不足。若计算逻辑涉及到对窗口桩体数据的操作以及对窗口中元数据信息的获取等就无法用ReduceFunction等函数实现,要用ProcessWindowFunction完成。
如果用ProcessWindowFunction完成一些基础的增量计算运算计算相对比较浪费系统资源,此时可以利用IncrementalAggregateWindowFunction和ProcessWindowFunction相结合的方式实现,以充分利用两种函数各自的优势。Flink DataStream API提供了实现ProcessWindowFunction和IncrementalAggregateWindowFunction整合的方法。

ProcessWindowFunction状态操作

与RichFunction所操作的Keyed State不同,ProcessWindowFunction操作的是基于窗口之上的状态数据:Per-Window State。状态数据针对指定的Key在窗口上存储,例如将用户ID作为Key,计算每个用户最近一个小时在线情况,如果平台上一共藕1000用户,则窗口计算中会创建1000个窗口实例,每个窗口实例中都会保存每个key的状态数据。可以通过ProcessWindowFunction提供的上下文Context获取并操作Per-window State数据。Per-window state在ProcessWindowFunction有两种类型:

  • globalSate
    窗口中的keyed state数据不限定在某个窗口中;
  • windowState
    Key state限定在固定的窗口中。

这些状态数据适合于针对迟到数据触发窗口计算,或在同一窗口多次触发计算的场景。使用Per-window state数据要注意及时清理状态数据,清理状态数据可调用ProcessWindowFunction的clear()方法。

1.3.5 ProcessWindowFunction整合IncrementalAggregateWindowFunction实现

Flink DataStream API提供了实现ProcessWindowFunction和IncrementalAggregateWindowFunction整合的方法。

1.4 窗口触发器(Trigger)

Trigger主要触发windowFunction的计算。Trigger定义了触发窗口计算的条件,不同类型的窗口有不同的窗口触发机制。

1.4.1 Flink的窗口触发器

目前Flink的每类窗口都有响应的Trigger,保证每次接入窗口的数据都能够安装触发逻辑触发计算。Flink定义了EventTimeTrigger、ProcessTimeTrigger和CountTrigger等窗口触发机制。

  • EventTimTrigger
    通过对比Watermark和窗口EndTime确定是否触发计算。如果Watermark大于EndTime则触发计算,否则窗口继续等待;
  • ProcessTrigger
    通过对比ProcessTime和窗口EndTime确定是否触发窗口计算。如果ProcessTime大于EndTime则触发计算,否则窗口继续等待;
  • ContinuousEventTimeTrigger
    根据间隔时间周期性触发窗口计算或者window的结束时间小于当前EventTime触发计算。
  • ContinuousProcessingTrigger
    根据间隔周期性触发窗口计算或者window的结束时间小于当前ProcessTime触发计算。
  • CountTrigger
    根据接入的数据量是否超过设定的阈值确定是否触发计算。
  • DeltaTrigger
    根据接入的数据计算出的Delta指标是否超过设定的阈值,判断是否触发计算。
  • PurgingTrigger
    可以将任意触发器作为参数转换为Purge类型触发器,数据将在计算完成后被清理掉。

1.4.2 自定义窗口触发器

如果以上Flink提供的触发器无法满足用户需求,可以通过继承并实现抽象类Trigger自定义触发器。

@PublicEvolving
public abstract class Trigger<T, W extends Window> implements Serializable {

	/**
	 * Called for every element that gets added to a pane. The result of this will determine
	 * whether the pane is evaluated to emit results.
	 *
	 * @param element The element that arrived.
	 * @param timestamp The timestamp of the element that arrived.
	 * @param window The window to which the element is being added.
	 * @param ctx A context object that can be used to register timer callbacks.
	 */
	public abstract TriggerResult onElement(T element, long timestamp, W window, TriggerContext ctx) throws Exception;

	/**
	 * Called when a processing-time timer that was set using the trigger context fires.
	 *
	 * @param time The timestamp at which the timer fired.
	 * @param window The window for which the timer fired.
	 * @param ctx A context object that can be used to register timer callbacks.
	 */
	public abstract TriggerResult onProcessingTime(long time, W window, TriggerContext ctx) throws Exception;

	/**
	 * Called when an event-time timer that was set using the trigger context fires.
	 *
	 * @param time The timestamp at which the timer fired.
	 * @param window The window for which the timer fired.
	 * @param ctx A context object that can be used to register timer callbacks.
	 */
	public abstract TriggerResult onEventTime(long time, W window, TriggerContext ctx) throws Exception;

	/**
	 * Returns true if this trigger supports merging of trigger state and can therefore
	 * be used with a
	 * {@link org.apache.flink.streaming.api.windowing.assigners.MergingWindowAssigner}.
	 *
	 * 

If this returns {@code true} you must properly implement * {@link #onMerge(Window, OnMergeContext)} */ public boolean canMerge() { return false; } /** * Called when several windows have been merged into one window by the * {@link org.apache.flink.streaming.api.windowing.assigners.WindowAssigner}. * * @param window The new window that results from the merge. * @param ctx A context object that can be used to register timer callbacks and access state. */ public void onMerge(W window, OnMergeContext ctx) throws Exception { throw new UnsupportedOperationException("This trigger does not support merging."); } /** * Clears any state that the trigger might still hold for the given window. This is called * when a window is purged. Timers set using {@link TriggerContext#registerEventTimeTimer(long)} * and {@link TriggerContext#registerProcessingTimeTimer(long)} should be deleted here as * well as state acquired using {@link TriggerContext#getPartitionedState(StateDescriptor)}. */ public abstract void clear(W window, TriggerContext ctx) throws Exception; }

函数 说明
onElement 对每一个接入窗口的数据元素决定是否触发操作
onProcessingTime 根据接入窗口 eventTime进行触发操作
onEventTime 根据接入窗口的processTime进行触发操作
onMerge 对多个窗口进行合并操作,同时进行状态的合并
clear 执行窗口及状态数据的清除

onElement方法返回结果TriggerResult是一个枚举类,有以下类型。

类型 说明
CONTINUE 当前不触发计算,继续等待
FIRE_AND_PURGE 触发计算,并清除对应的数据。
FIRE 触发计算,但是数据继续保留
PURGE 清除窗口内部数据,但不触发计算

onElemen根据预先定义的触发逻辑,返回以上状态给Flink,由Flink在窗口计算过程中,根据返回的状态决定是否触发对当前窗口的数据进行计算。

1.5 数据剔除器(Evictor)

数据剔除器是窗口机制的可选组件,其主要作用是在trigger触发后,数据进入窗口,WindowFunction被执行之前或执行之后的数据进行剔除处理。Evictor由方法evictor(...)指定。

1.5.1 Flink数据剔除器

  1. CountEvictor
    保持在窗口汇总具有预先指定的最大数量的数据。超过指定的最大数量maxCount的数据将在窗口中剔除。其核心剔除函数实现源码如
private void evict(Iterable<TimestampedValue<Object>> elements, int size, EvictorContext ctx) {
	if (size <= maxCount) {
		return;
	} else {
		int evictedCount = 0;
		for (Iterator<TimestampedValue<Object>> iterator = elements.iterator(); iterator.hasNext();){
			iterator.next();
			evictedCount++;
			if (evictedCount > size - maxCount) {
				break;
			} else {
				iterator.remove();
			}
		}
	}
}

其中size是窗口中元素的总数,maxCount是设定的窗口中应保留的数据最大量。
2. DeltaEvictor
基于定义的DeltaFunction函数和指定的threshold,计算窗口中元素与新进入元素之间额Delta,如果超过了阈值,将剔除超过阈值之后的新进入元素。其核心剔除函数实现源码如:

private void evict(Iterable<TimestampedValue<T>> elements, int size, EvictorContext ctx) {
		TimestampedValue<T> lastElement = Iterables.getLast(elements);
		for (Iterator<TimestampedValue<T>> iterator = elements.iterator(); iterator.hasNext();){
			TimestampedValue<T> element = iterator.next();
			if (deltaFunction.getDelta(element.getValue(), lastElement.getValue()) >= this.threshold) {
				iterator.remove();
			}
		}
	}
  1. TimeEvictor
    通过指定windowSize,以窗口中最新元素的Timestamp作为current_time减去windowSize,计算相应的current_time - keep_time,其中current_time是窗口中最新元素的Timestamp,keep_time是windowSize。如果元素的Timestamp时间小于该值,就剔除该值。TimeEvictor的本质是讲具有最新时间的数据选择处理,删掉过时的数据。TimeEvictor的核心剔除函数实现源码如:
private void evict(Iterable<TimestampedValue<Object>> elements, int size, EvictorContext ctx) {
   	if (!hasTimestamp(elements)) {
   		return;
   	}

   	long currentTime = getMaxTimestamp(elements);
   	long evictCutoff = currentTime - windowSize;

   	for (Iterator<TimestampedValue<Object>> iterator = elements.iterator(); iterator.hasNext(); ) {
   		TimestampedValue<Object> record = iterator.next();
   		if (record.getTimestamp() <= evictCutoff) {
   			iterator.remove();
   		}
   	}
   }

参数elements是窗口中所有的元素,currentTime = getMaxTimestamp(elements)

以上每个剔除器都有多个of方法用于创建剔除器,默认是在WindowsFunction计算前剔除数据,如要在之后剔除数据,可以用相应的of()方法将参数doEvictAfter设为true。

1.5.2 自定义数据剔除器

flink提供的剔除器不满足需求时,可以通过实现Evictor接口自定义剔除器。接口Evictor有两个方法,如下

void evictBefore(Iterable<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);
void evictAfter(Iterable<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);

方法evictBefore() 定义了数据在window function计算之前剔除的逻辑, 而 evictAfter() 则定义了在计算之后剔除的逻辑。参数elements代表当前窗口中所有的数据元素。CountEvictor等剔除器都是在evict方法中定义了一个共用的剔除逻辑,然后分别在evictBefore和evictAfter调用evict方法。
应用剔除器需要注意

  • Specifying an evictor prevents any pre-aggregation, as all the elements of a window have to be passed to the evictor before applying the computation.
  • Flink provides no guarantees about the order of the elements within a window. This implies that although an evictor may remove elements from the beginning of the window, these are not necessarily the ones that arrive first or last.

1.6 延迟数据处理

watermark机制一定程度上解决了数据乱序问题,但是如果数据延时非常严重,watermark机制也无法保证数据全部进入到窗口再处理。Flink默认会将这些延迟数据丢弃不处理。但有些场景即使数据延迟到达,要希望这些延迟数据也能按照流程处理并输出结果,此时就需要使用allowed Lateness机制来对延迟 数据进行额外处理。DataStreamAPI中提供了allowedLateness方法来指定是否对延迟数据进行处理。

DataStream<T> input = ...;
input
    .keyBy(<key selector>)
    .window(<window assigner>)
    .allowedLateness(<time>)
    .>(<window function>);

allowedLateness参数time是Time类型的时间大小,表示允许延时的最大时间,window函数计算过程中会将窗口EndTime加上该值作为窗口最后被释放的结束时间(EndTime+time),当数据的EventTime<(EndTime+time),但watermark已经超过EndTime时,直接触发窗口计算。如果EventTime>(EndTime+time),则丢弃数据。
GlobalWindows的最大时延时间为 Long.MAX_VALUE,即永不超时,数据会源源不断地累积到窗口中,等待触发。其他窗口的默认延迟时间为0,即不允许有延时数据。

  • 延时数据的处理结果的单独输出

延时数据处理后,并不一定要立刻混入正常的计算流程中,而是希望将延时数据或处理结果存储到数据库或其他存储系统中,便于后期对延时数据的分析。此时可利用side output机制处理,先调用sideOutputLateData(OutputTag) 方法标记延时数据计算的结果,然后调用getSideOutput(lateOutputTag)方法从窗口结果流中获取lateOutputTag标签标记的数据,最后再用DataStream流处理方法对数据做处理。

//创建延时数据的OutputTag
final OutputTag<T> lateOutputTag = new OutputTag<T>("late-data"){};
DataStream<T> input = ...;

SingleOutputStreamOperator<T> result = input
    .keyBy(<key selector>)
    .window(<window assigner>)
    .allowedLateness(<time>)
    //对结果延时数据进行标记
    .sideOutputLateData(lateOutputTag)
    .>(<window function>);
//通过lateOutputTag从窗口结果红获取标记的结果数据
DataStream<T> lateStream = result.getSideOutput(lateOutputTag);
未完待续

参考文档

你可能感兴趣的:(Flink)