前言

Flink版本:1.12.1

将实时的数据类比于一个车流(带有一个值),如果你想获得所有车值的总和,那该怎么办呢?求和:不断的将数据相加,像聚合函数一样:Flink的常见算子和实例代码。
 但是问题来了:实时数据流是不断的产生数据的,那么作为无界数据流,你永远不可能获得流的完整结果。也许你可以创建一个同样的求和数据流(无界)像这样:

flink 编写 flink实例_flink

关于事件时间和水印与窗口的联合使用及其demo代码:Flink事件时间和水印详解


小结

Flink 内置了几种类型的窗口分配器,如下图所示:

flink 编写 flink实例_flink_02

从大的类型来区分有:

  • 计数count
  • 计时time

从是否对数据分组分为:

  • 键控流
  • 非键控流

一些场景示例

时间窗口持续时间可以使用的一个指定Time.milliseconds(n)Time.seconds(n)Time.minutes(n)Time.hours(n),和Time.days(n)

  • 翻滚的时间窗口
  • 每分钟页面浏览量
  • TumblingEventTimeWindows.of(Time.minutes(1))
  • 滑动时间窗口
  • 每 10 秒计算一次每分钟的页面浏览量
  • SlidingEventTimeWindows.of(Time.minutes(1), Time.seconds(10))
  • 会话窗口
  • 每个会话的页面浏览量,其中会话定义为会话之间至少有 30 分钟的间隔
  • EventTimeSessionWindows.withGap(Time.minutes(30))


非/键控流(keyed Stream)

是否是keyed Stream取决于是否进行了keyBy分组:

键控流

.window

stream.
    .keyBy(<key selector>)
    .window(<window assigner>)
    .reduce|aggregate|process(<window function>);

非键控

.windowAll

stream.
    .windowAll(<window assigner>)
    .reduce|aggregate|process(<window function>);

示例

数据源共用于:Flink事件时间和水印详解的Source。

userStream.windowAll(TumblingProcessingTimeWindows.of(Time.seconds(1))).max(1).printToErr("cbry windowsAll");

值得注意的是:1.12版本中因为TimeCharacteristic(时间特性)被舍弃(默认为EventTime)的缘故,原本的timeWindowAll方法被舍弃,改在windowAll里面创建,如上代码。

flink 编写 flink实例_java_03

意义

使用非键控流会使得程序处理并行度为1,即不会并行完成


滚动窗口

介绍

但是不断变换的结果流,不是我们需要的确切结果。因为它会不断更新计数,更重要的是,某些信息(例如随时间变化)会丢失。

那么回到前言部分Flink的特性:Flink是把批当作一种有限的流,反过来我们将流的元素分组为有限的集合,每分钟计算一次求和,每个集合对应于60秒的间隔。每次求和结果化为一个窗口,此操作称为滚动窗口操作。

flink 编写 flink实例_详解_04

源码

时间

userStream.keyBy((event) -> event.f0).window(TumblingProcessingTimeWindows.of(Time.seconds(1))).max(1).printToErr("滚动时间窗口");

多态方法:带偏移量的滚动窗口,比如说:of(Time.hours(1),Time.minutes(15)):窗口开始时间为 0:15:00、1:15:00、2:15:00 等。

/** 
* 滚动窗口
*  size:  窗口大小
*  offset:窗口作业开始前的偏移量
*  **/
userStream.keyBy((event) -> event.f0).window(TumblingProcessingTimeWindows.of(Time.seconds(2),Time.seconds(1))).max(1).printToErr("滚动时间窗口/偏移量");

记数

countWindow的多态形成了滚动和滑动两种窗口

userStream.keyBy((event) -> event.f0).countWindow(3).sum(1).printToErr("滚动计数窗口");


滑动窗口

介绍

滚动窗口将流离散化为不重叠的窗口,但是不同的情况对数据的交集情况是不同的。如果是独立的分块数据没有交集自然可以滚动无误。但是如果是有交集的数据呢?
 举个例子:每隔三十秒统计一下前一分钟的数据:
 相同颜色部分的数据是会有重复交集的,窗口对数据的采集时平缓的滑过去,对于这样的情况称为滑动窗口操作。

flink 编写 flink实例_java_05

源码

时间

/** 滑动窗口 
 *  size:  窗口大小
 *  slide:滑动距离
 *  offset:窗口作业开始前的偏移量
 *  **/
userStream.keyBy((event) -> event.f0).window(SlidingProcessingTimeWindows.of(Time.seconds(10),Time.seconds(4))).sum(1).printToErr("滑动时间窗口");
userStream.keyBy((event) -> event.f0).window(SlidingProcessingTimeWindows.of(Time.seconds(10),Time.seconds(1),Time.seconds(4))).sum(1).printToErr("滑动时间窗口/偏移量");

记数

countWindow的多态形成了滚动和滑动两种窗口:1+2 2+3 3+4

userStream.keyBy((event) -> event.f0).countWindow(2,1).sum(1).printToErr("滑动计数窗口");

会话窗口

介绍

概述:

会话窗口主要是将某段时间内活跃度较高的数据聚合成一个窗口进行计算,窗口的触发的条件是Session Gap,是指在规定的时间内如果没有数据活跃接入,则认为窗口结束,然后触发窗口计算结果。

flink 编写 flink实例_flink 编写_06

注意:

需要注意的是如果数据一直不间断地进入窗口,也会导致窗口始终不触发的情况.

与滑动窗口不同的是,Session Windows不需要有固定Window size和slide tinme ,需要定义session gap(不活跃等待时间)来规定不活跃数据的时间上限即可

源码

会话窗口:会话窗口在一定时间段内未收到元素时(即,发生不活动间隙时),它将关闭。 这个时间间隙分为静态和动态两种方法

静态等待不活跃时间

userStream.keyBy((event) -> event.f0).window(ProcessingTimeSessionWindows.withGap(Time.seconds(6))).sum(1).printToErr();

EventTimeSessionWindows亦可,下同。

动态等待不活跃时间

lambda更简洁一些:

userStream.keyBy((event) -> event.f0).window(ProcessingTimeSessionWindows.withDynamicGap((element) -> { 	
		return element.f1;
	})).sum(1).printToErr("动态等待不活跃时间");

实现接口更明了一些:

userStream.keyBy((event) -> event.f0).window(ProcessingTimeSessionWindows.withDynamicGap(new SessionWindowTimeGapExtractor<Tuple2<String, Long>>() {

	    /**
	     * Extracts the session time gap.
	     *
	     * @param element The input element.
	     * @return The session time gap in milliseconds.	指定会话窗口生成间隙
	     */
		@Override
		public long extract(Tuple2<String, Long> element) {
			// TODO Auto-generated method stub
			return element.f1;
		}
	}
	)).sum(1).printToErr("动态等待不活跃时间");


全局窗口

将整个输入看作一个简单的窗口,因为Flink是基于事件的流式无界处理,所以我们需要指定一个“触发器Trigger”才能触发窗口执行(结束窗口)执行聚合运算,否则他不会进行任何运算:全局窗口(GlobalWindow)的默认触发器是永不触发的NeverTrigger。

flink 编写 flink实例_java_07


Triggers

官方提示到:请注意,抽象的 Trigger 类,该 API 仍在不断发展,可能会在 Flink 的未来版本中发生变化。

触发器的使用

DataStream<EventData> dataStream = 
                    keyedEvents.window(GlobalWindows.create())
                    .trigger(new ServiceWindowTrigger())
userStream.keyBy((event) -> event.f0).window( GlobalWindows.create() ).trigger(CountTrigger.of(1)).sum(1).printToErr("全局窗口trigger");

触发器的定义

一个Trigger确定窗口(由窗口分配器形成)何时准备好由窗口函数处理。每个WindowAssigner都有一个默认值Trigger。如果默认触发器不符合需求,则可以使用指定自定义触发器trigger(...)


开箱即用的九种触发器

  • CountTrigger
  • EventTimeTrigger
  • ProcessingTimeTrigger
  • ContinuousEventTimeTrigger
  • ContinuousProcessingTimeTrigger
  • ProcessingTimeoutTrigger
  • PurgingTrigger
  • DeltaTrigger
  • NeverTrigger

出了官方的九种也可以自定义trigger,几种触发器具体描述暂时放在这没有去实操,具体可以参考Flink自定义窗口触发器。


触发器的几种方法

触发器接口具有五种方法,这些方法允许aTrigger对不同事件做出反应:

  • onElement()对于添加到窗口中的每个元素,都会调用该方法。
  • onEventTime()当注册的事件时间计时器触发时,将调用该方法。
  • onProcessingTime()当注册的处理时间计时器触发时,将调用该方法。
  • onMerge()方法与有状态触发器相关,并且在两个触发器的相应窗口合并时(*例如,*在使用会话窗口时)合并两个触发器的状态。
  • 最后,该clear()方法执行删除相应窗口后所需的任何操作。

关于上述方法,需要注意两件事:

1)前三个通过返回来决定如何对它们的调用事件采取行动TriggerResult。该动作可以是以下之一:

  • CONTINUE: 没做什么,
  • FIRE:触发计算,
  • PURGE:清除窗口中的元素,然后
  • FIRE_AND_PURGE:触发计算并随后清除窗口中的元素。

2)这些方法中的任何一种均可用于注册处理或事件时间计时器以用于将来的操作。


窗口函数:Window Functions

Reduce

跟算子的reduce操作类似,不赘述:Flink的常见算子和实例代码

.keyBy(<key selector>)
    .window(<window assigner>)
    .reduce(new ReduceFunction<Tuple2<String, Long>> {
      public Tuple2<String, Long> reduce(Tuple2<String, Long> v1, Tuple2<String, Long> v2) {
        return new Tuple2<>(v1.f0, v1.f1 + v2.f1);
      }
    });

聚合函数AverageAggregate

描述

AggregateFunction是一个广义版本ReduceFunction,父子关系,其具有三种类型:输入类型(IN),蓄压式(ACC),和一个输出类型(OUT)。

DataStream<Tuple2<String, Long>> input = ...;
input
    .keyBy(<key selector>)
    .window(<window assigner>)
    .aggregate(new AverageAggregate());

实现上面的AverageAggregate接口:

聚合示例

我们以计算流数据里面的第二个字段的平均值为例:

/**
 * The accumulator is used to keep a running sum and a count. The {@code getResult} method
 * computes the average.
 */
private static class AverageAggregate
    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 + 1L);
  }

  @Override
  public Double getResult(Tuple2<Long, Long> accumulator) {
    return ((double) accumulator.f0) / accumulator.f1;
  }

  @Override
  public Tuple2<Long, Long> merge(Tuple2<Long, Long> a, Tuple2<Long, Long> b) {
    return new Tuple2<>(a.f0 + b.f0, a.f1 + b.f1);
  }
}


处理窗口函数ProcessWindowFunction

描述

将窗口的所有数据放入一个Iterable里面用于遍历,提供一个可以访问时间和状态信息的 Context 对象,这使其能够提供比其他窗口函数更大的灵活性。这是以性能和资源消耗为代价的。因为他要不断的缓存,相当于批处理了。具体实现我们在:Flink事件时间和水印详解里面的*自定义水印搭配滚动时间窗口效果*目录下的源码中有用到:

.window(TumblingEventTimeWindows.of(Time.seconds(6)))
			//.max(1)
			.process(new ProcessWindowFunction<Tuple2<String,Long>, String, String, TimeWindow>() {

				private static final long serialVersionUID = 1L;

			@Override
			  public void process(String key, Context context, Iterable<Tuple2<String, Long>> input, Collector<String> out) {
			    long count = 0;
			    //集合
			    ArrayList<Long> conllect = new ArrayList<Long>();
			    for (Tuple2<String, Long> in: input) {
			      conllect.add(in.f1);
			      count++;
			    }
			    out.collect("Window: " + context.window() + "count: " + count + " 数据:" + input.toString());
			  }
			})
			.printToErr("out ")
			;

增量聚合处理函数

如其名是上面两个函数的组合使用:

input
  .keyBy(<key selector>)
  .window(<window assigner>)
  .reduce(new MyReduceFunction(), new MyProcessWindowFunction());

窗口和事件时间及水印的交融

详情查看Flink事件时间和水印详解里面的关于迟到数据处理目录及后续目录下的内容。

窗口Window会在以下的条件满足时被触发执行:

  • watermark时间 >= window_end_time(闭窗);
  • 在[window_start_time,window_end_time)中有数据存在(入窗);

参考

Flink官方文档:窗口

Flink官方文档:流分析