FlinkWindow和水印

Flink Window API

1、TimeWindow

TimeWindow 是将指定时间范围内的所有数据组成一个 window, 一次对一个window 里面的所有数据进行计算。

1.1、滚动窗口

Flink 默认的时间窗口根据 Processing Time 进行窗口的划分,将 Flink 获取到的数据根据进入 Flink 的时间划分到不同的窗口中。

时间间隔可以通过 Time.milliseconds(x),Time.seconds(x),Time.minutes(x)等其中的一个来指定。

1.2、滑动窗口(SlidingEventTimeWindows)

滑动窗口和滚动窗口的函数名是完全一致的, 只是在传参数时需要传入两个参数, 一个是 window_size, 一个是 sliding_size。

时间间隔可以通过 Time.milliseconds(x),Time.seconds(x),Time.minutes(x)等其中的一个来指定。

dataStream
//window(传入一个窗口分配器):滚动时间窗口
.window(TumblingEventTimeWindows.of(Time.seconds(15)))
//滑动时间窗口
.window(SlidingProcessingTimeWindows.of(Time.seconds(15),Time.seconds(3)))
//滚动时间和滑动时间窗口的简写方式
.timeWindow(Time.seconds(15),Time.seconds(3))
//会话窗口
.window(EventTimeSessionWindows.withGap(Time.seconds(10)))

测试代码:

val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val inputStream = env.socketTextStream("single", 7777)
val dataStream = inputStream.map(data => {
    val arr = data.split(",")
    SensorReading(arr(0), arr(1).toLong, arr(2).toDouble)
})

val resultDataStream = dataStream.map(data => {
    (data.id, data.temperature, data.timestamp)
}).keyBy(_._1) 
.timeWindow(Time.seconds(15))
.reduce((curRes, newData) => (curRes._1, curRes._2.min(newData._2), newData._3))

resultDataStream.print("res")
env.execute()

image-20210404022442911

2、CountWindow

CountWindow 根据窗口中相同 key 元素的数量来触发执行, 执行时只计算元素数量达到窗口大小的 key 对应的结果。

注意: CountWindow 的 window_size 指的是相同 Key 的元素的个数, 不是输入的所有元素的总数。

2.1、滚动窗口

默认的 CountWindow 是一个滚动窗口,只需要指定窗口大小即可,当元素数量达到窗口大小时, 就会触发窗口的执行。

2.2、滑动窗口

滑动窗口和滚动窗口的函数名是完全一致的, 只是在传参数时需要传入两个参数, 一个是 window_size, 一个是 sliding_size。

dataStream
//滚动技术窗口
.countWindow(10)
//滑动计数窗口
.countWindow(10,5)

3、window function

window function 定义了要对窗口中收集的数据做的计算操作,主要可以分为两类:

  • 增量聚合函数( incremental aggregation functions):每条数据到来就进行计算,来一个算一个, 保持一个简单的状态。
    典型的增量聚合函数有ReduceFunction, AggregateFunction。
  • 全窗口函数( full window functions):先把窗口所有数据收集起来, 不做任何操作,等到计算的时候会遍历所有数据。
    ProcessWindowFunction 就是一个全窗口函数。

通常情况下的聚合用增量聚合函数,效率更快,并且可以实时看到计算结果

全窗口函数常用在排序,查找中位数等场景,能在上下文里拿到当前的状态信息

4、其它可选API

  • trigger() —— 触发器,定义 window 什么时候关闭, 触发计算并输出结果
  • evitor() —— 移除器,定义移除某些数据的逻辑
  • allowedLateness() —— 允许处理迟到的数据
  • sideOutputLateData() —— 将迟到的数据放入侧输出流
  • getSideOutput() —— 获取侧输出流

5、窗口起始点的确认

public static long getWindowStartWithOffset(long timestamp, long offset, long windowSize) {
    return timestamp - (timestamp - offset + windowSize) % windowSize;
}

通过TimeWindow类中的getWindowStartWithOffset方法可以知道,窗口起始位置跟 当前数据的时间戳timestamp、偏移量offset、窗口尺寸windowSize有关。而其中最核心相关的是数据当前时间戳TimeStamp和windowSize的取余,TimeStamp-timestamp/windowSize也就是把当前时间戳的余数给抹掉就是当前窗口的起始点

时间语义与 Wartermark

1、Flink中的时间语义

在 Flink 的流式处理中, 会涉及到时间的不同概念

  • Event Time: 是事件创建的时间。它通常由事件中的时间戳描述, 例如采集的日志数据中,每一条日志都会记录自己的生成时间,Flink 通过时间戳分配器访问事件时间戳。
  • Ingestion Time: 是数据进入 Flink 的时间。
  • Processing Time: 是每一个执行基于时间操作的算子的本地系统时间, 与机器相关, 默认的时间属性就是 Processing Time。

2、EventTime 的引入

在 Flink 的流式处理中, 绝大部分的业务都会使用 eventTime, 一般只在eventTime 无法使用时, 才会被迫使用 ProcessingTime 或者 IngestionTime。通常用来解决由于网络、分布式等原因导致的乱序数据的产生

如果要使用 EventTime,那么需要引入 EventTime 的时间属性,引入方式如下所示:

val env = StreamExecutionEnvironment.getExecutionEnvironment
// 从调用时刻开始给env 创建的每一个stream 追加时间特征
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
// 后面需要在事件的时间提取一个时间戳

3、Watermark基本概念

流处理从事件产生,到流经 source,再到 operator,中间是有一个过程和时间的, 虽然大部分情况下, 流到 operator 的数据都是按照事件产生的时间顺序来的, 但是也不排除由于网络、分布式等原因, 导致乱序的产生, 所谓乱序, 就是指 Flink 接收到的事件的先后顺序不是严格按照事件的 Event Time 顺序排列的。

image-20210404023729259

那么此时出现一个问题,一旦出现乱序,如果只根据 eventTime 决定 window 的运行, 我们不能明确数据是否全部到位, 但又不能无限期的等下去, 此时必须要有个机制来保证一个特定的时间后, 必须触发 window 去进行计算了, 这个特别的机制, 就是 Watermark。

  • Watermark 是一种衡量 Event Time 进展的机制。
  • Watermark 是用于处理乱序事件的, 而正确的处理乱序事件, 通常用Watermark 机制结合 window 来实现。
  • 数据流中的 Watermark 用于表示 timestamp 小于 Watermark 的数据,都已经到达了, 因此, window的执行也是由 Watermark 触发的。
  • Watermark 可以理解成一个延迟触发机制,我们可以设置 Watermark 的延时时长 t,每次系统会校验已经到达的数据中最大的 maxEventTime,然后认定 eventTime小于 maxEventTime - t 的所有数据都已经到达, 如果有窗口的停止时间等于maxEventTime – t, 那么这个窗口被触发执行。

有序流的 Watermarker 如下图所示:( Watermark 设置为 0)

image-20210404023745016

乱序流的 Watermarker 如下图所示:( Watermark 设置为 2)

image-20210404023750183

当 Flink 接收到数据时, 会按照一定的规则去生成 Watermark, 这条 Watermark 就等于当前所有到达数据中的 maxEventTime - 延迟时长,也就是说,Watermark 是基于数据携带的时间戳生成的, 一旦 Watermark 比当前未触发的窗口的停止时间要晚, 那么就会触发相应窗口的执行。由于 event time 是由数据携带的, 因此, 如果运行过程中无法获取新的数据, 那么没有被触发的窗口将永远都不被触发。

以当前时间戳里的最大值减去固定延迟得到的时间被称作水印,我不管你到没到,反正我认为时间戳小于我的消息是全部都到了。迟到的数据每来一条就到一条。

分桶规则:如果时间大于当前桶的最大值,则会被分到另一个桶,不会造成数据污染

上图中,我们设置的允许最大延迟到达时间为 2s,所以时间戳为 7s 的事件对应的 Watermark 是 5s, 时间戳为 12s 的事件的 Watermark 是 10s, 如果我们的窗口 1 是 1s~5s, 窗口 2 是 6s~10s, 那么时间戳为 7s 的事件到达时的 Watermarker 恰好触发窗口 1, 时间戳为 12s 的事件到达时的 Watermark 恰好触发窗口 2。

Watermark 就是触发前一窗口的“ 关窗时间”, 一旦触发关门那么以当前时刻为准在窗口范围内的所有所有数据都会收入窗中。

只要没有达到水位那么不管现实中的时间推进了多久都不会触发关窗。

4、Watermark 的引入

4.1、乱序数据的水印引入

watermark 的引入很简单, 对于乱序数据, 最常见的引用方式就是实现一个分配时间戳的接口:

dataStream.assignTimestampsAndWatermarks( 
    new BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.milliseconds(1000)) {
        override def extractTimestamp(element: SensorReading): Long = { 
            element.timestamp * 1000
        }
    } 
)

Event Time 的使用一定要指定数据源中的时间戳。否则程序无法知道事件的事件时间是什么(数据源里的数据没有时间戳的话, 就只能使用 Processing Time 了)。我们看到上面的例子中创建了一个看起来有点复杂的类, 这个类实现的其实就是分配时间戳的接口。Flink 暴露了 TimestampAssigner 接口供我们实现, 使我们可以自定义如何从事件数据中抽取时间戳。

val env = StreamExecutionEnvironment.getExecutionEnvironment
// 从调用时刻开始给env 创建的每一个stream 追加时间特性
// Characteristic:特征
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
val readings: DataStream[SensorReading] = env
.addSource(
    new SensorSource
).assignTimestampsAndWatermarks(new MyAssigner())

MyAssigner 有两种类型(Assigner:分配;Periodic:周期性;Punctuated:加标记)

  • AssignerWithPeriodicWatermarks:大数据量,用这个性能更好
  • AssignerWithPunctuatedWatermarks:如果是稀疏的数据,用这个性能更好

以上两个接口都继承自 TimestampAssigner。

4.2、Assigner with periodic watermarks

周期性的生成 watermark: 系统会周期性的将 watermark 插入到流中(水位线也是一种特殊的事件!)。默认周期是 200 毫秒。可以使用ExecutionConfig.setAutoWatermarkInterval()方法进行设置。

val env = StreamExecutionEnvironment.getExecutionEnvironment env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

// 每隔5 秒产生一个watermark
env.getConfig.setAutoWatermarkInterval(5000)

产生 watermark 的逻辑: 每隔 5 秒钟, Flink 会调用AssignerWithPeriodicWatermarks 的 getCurrentWatermark()方法。如果方法返回一个时间戳大于之前水位的时间戳, 新的 watermark 会被插入到流中。这个检查保证了水位线是单调递增的。如果方法返回的时间戳小于等于之前水位的时间戳, 则不会产生新的 watermark。

例子, 自定义一个周期性的时间戳抽取:

class PeriodicAssigner extends AssignerWithPeriodicWatermarks[SensorReading] {
  val bound: Long = 60 * 1000 // 延时为1 分钟
  var maxTs: Long = Long.MinValue // 观察到的最大时间戳
  
  override def getCurrentWatermark: Watermark = {
    new Watermark(maxTs - bound)
  }

  override def extractTimestamp(r: SensorReading, previousTS: Long) = { 
    maxTs = maxTs.max(r.timestamp)
    r.timestamp
  }
}

一种简单的特殊情况是, 如果我们事先得知数据流的时间戳是单调递增的, 也就是说没有乱序, 那我们可以使用 assignAscendingTimestamps, 这个方法会直接使用数据的时间戳生成 watermark。

val stream: DataStream[SensorReading] = ...
val withTimestampsAndWatermarks = stream.assignAscendingTimestamps(e => e.timestamp)

>> result:	E(1), W(1), E(2), W(2), ...

而对于乱序数据流, 如果我们能大致估算出数据流中的事件的最大延迟时间, 就可以使用如下代码:

val stream: DataStream[SensorReading] = ...
val withTimestampsAndWatermarks = stream.assignTimestampsAndWatermarks(
    new SensorTimeAssigner
)

class SensorTimeAssigner extends BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(5)) {
    // 抽取时间戳
    override def extractTimestamp(r: SensorReading): Long = r.timestamp
}

>> relust:	E(10), W(0), E(8), E(7), E(11), W(1), ...

4.3、Assigner with punctuated watermarks

间断式地生成 watermark。和周期性生成的方式不同,这种方式不是固定时间的, 而是可以根据需要对每条数据进行筛选和处理。直接上代码来举个例子, 我们只给sensor_1 的传感器的数据流插入 watermark:

class PunctuatedAssigner extends AssignerWithPunctuatedWatermarks[SensorReading] {
  val bound: Long = 60 * 1000

  override def checkAndGetNextWatermark(r: SensorReading, extractedTS: Long): Watermark = {
    if (r.id == "sensor_1") {
      new Watermark(extractedTS - bound)
    } else {
      null
    }
  }

  override def extractTimestamp(r: SensorReading, previousTS: Long): Long = {
    r.timestamp
  }
}

4.4、生产环境中如何设置水印?

  • 我先设置一个小的延迟,可以hold住大部分的延迟,少部分延迟比较高会漏掉但是近似准确
  • 如果要求非常高,我可以用window窗口里的allowedLateness(Time.minutes(1))方法设置允许处理迟到数据
  • 但不能等太长资源一直不释放,最后可以把迟到数据放到侧输出流.sideOutputLateData(latetag)中兜底,保证数据不丢失
.timeWindow(Time.seconds(15))
       .allowedLateness(Time.minutes(1))
       .sideOutputLateData(latetag)

val latetag = new OutputTag[(String, Double, Long)]("late")

5、EvnetTime 在window 中的使用

5.0、常用使用结构:

  • 获取Flink运行环境
  • 设置时间类型(三种:数据自带的时间戳eventtime;数据进入flinkStreaming流的时间ingestiontime;机器的系统时间processingtime)
  • 通过source获取datastream
  • 对datastream设置水位线assignTimestampsAndWatermarks,指定时间戳从哪抽取
  • 然后是业务流程
  • 指定窗口类型(滑动、滚动、会话等)并设置窗口宽度,然后根据业务进行聚合
  • 最后输出

5.1、滚动窗口(TumblingEventTimeWindows)

def main(args: Array[String]): Unit = {
    // 环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    env.setParallelism(1)

    val dstream: DataStream[String] = env.socketTextStream("single", 7777)

    val textWithTsDstream: DataStream[(String, Long, Int)] = dstream.map{text =>
        val arr: Array[String] = text.split(",")
        (arr(0), arr(1).toLong, 1)
    }

    val textWithEventTimeDstream: DataStream[(String, Long, Int)] = textWithTsDstream.assignTimestampsAndWatermarks(
        new BoundedOutOfOrdernessTimestampExtractor[(String,Long,Int)](Time.milliseconds(1000)) {
            override def extractTimestamp(element: (String, Long, Int)): Long = {
                element._2*1000L
            }
        }
    )

    val textKeyStream: KeyedStream[(String, Long, Int), Tuple] = textWithEventTimeDstream.keyBy(0)
    textKeyStream.print("textkey:")
    val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(TumblingEventTimeWindows.of(Time.seconds(2)))
    val groupDstream: DataStream[mutable.HashSet[Long]] = windowStream.fold(new mutable.HashSet[Long]()) {
        case (set, (key, ts, count)) => set += ts
    }

    groupDstream.print("window::::").setParallelism(1)
    env.execute()
}

结果是按照 Event Time 的时间窗口计算得出的, 而无关系统的时间( 包括输入的快慢)。

5.2、滑动窗口(SlidingEventTimeWindows)

def main(args: Array[String]): Unit = {
    // 环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    env.setParallelism(1)

    val dstream: DataStream[String] = env.socketTextStream("single", 7777)

    val textWithTsDstream: DataStream[(String, Long, Int)] = dstream.map {
        text =>
        val arr: Array[String] = text.split(" ")
        (arr(0), arr(1).toLong, 1)
    }

    val textWithEventTimeDstream: DataStream[(String, Long, Int)] = textWithTsDstream.assignTimestampsAndWatermarks(
        new BoundedOutOfOrdernessTimestampExtractor[(String, Long, Int)](Time.milliseconds(1000)) {
            override def extractTimestamp(element: (String, Long, Int)): Long = {
                element._2*1000L
            }
        })
    val textKeyStream: KeyedStream[(String, Long, Int), Tuple] = textWithEventTimeDstream.keyBy(0)
    textKeyStream.print("textkey:")
    val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(SlidingEventTimeWindows.of(Time.seconds(2), Time.millis econds (500)))
    val groupDstream: DataStream[mutable.HashSet[Long]] = windowStream.fold(new mutable.HashSet[Long]()) {
        case (set, (key, ts, count)) => set += ts
    }

    groupDstream.print("window::::").setParallelism(1)
    env.execute()
}

5.3、会话窗口(EventTimeSessionWindows)

相邻两次数据的 EventTime 的时间差超过指定的时间间隔就会触发执行。如果加入 Watermark,会在符合窗口触发的情况下进行延迟。到达延迟水位再进行窗口触发。

def main(args: Array[String]): Unit = {
    // 环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    env.setParallelism(1)

    val dstream: DataStream[String] = env.socketTextStream("single", 7777)
    val textWithTsDstream: DataStream[(String, Long, Int)] = dstream.map {
        text =>
        val arr: Array[String] = text.split(" ")
        (arr(0), arr(1).toLong, 1)
    }

    val textWithEventTimeDstream: DataStream[(String, Long, Int)] = textWithTsDstream.assignTimestampsAndWatermarks(
        new BoundedOutOfOrdernessTimestampExtractor[(String, Long, Int)](
            Time.milliseconds(1000)) {
            override def extractTimestamp(element: (String, Long, Int)): Long = {
                element._2*1000L
            }
        }
    )
    val textKeyStream: KeyedStream[(String, Long, Int), Tuple] = textWithEventTimeDstream.keyBy(0)
    textKeyStream.print("textkey:")
    val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(
        EventTimeSessionWindows.withGap(
            Time.milliseconds(500)
        )
    )
    windowStream.reduce(
        (text1, text2) => (text1._1, 0L, text1._3 + text2._3)
    ).map(_._3)
    .print("windows:::").setParallelism(1)

    env.execute()
}

yBy(0)
textKeyStream.print(“textkey:”)
val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(
EventTimeSessionWindows.withGap(
Time.milliseconds(500)
)
)
windowStream.reduce(
(text1, text2) => (text1._1, 0L, text1._3 + text2.3)
).map(
._3)
.print(“windows:::”).setParallelism(1)

env.execute()

}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值