20-04-flink

window api基于keyedStream。

window是桶。

1. window操作两个主要步骤:
窗口分配器(.window),窗口函数(reduce,aggregate,apply,process)
2. window类型
通过窗口分配器来决定,时间窗口和计数窗口
按照窗口起止时间(个数)的定义,可以有滚动窗口、滑动窗口、会话窗口

滑动窗口中,每条数据可以属于多个窗口,属于size/slide个窗口
会话窗口,窗口长度不固定,需要指定间隔时间

3. 窗口函数
窗口函数是基于当前窗口内的数据的,是有界数据集的计算,通常只在窗口关闭时输出一次
增量聚合函数:ReduceFunction, AggregateFunction,流式处理过程,来一条处理一次
全窗口函数;WindowFunction,ProcessWindowFunction,类似于批处理过程,类似于批处理,所有数据存起来,触发计算再同意计算。

4. 程序默认的时间语义,是 Processing Time

---01---

滚动窗口假如我设置一小时的滚动窗口。

小时的起始点如何定义的呢?默认起始点是整小时的。偏移量是起始点的偏移量。 

窗口的默认就是整点的时间点开始的,一般用来处理时区的offset。要是不设置你默认的话就是早上8点到第二天8点的。

太阳从东方升起来所以我们用的是时区是 

---02-03---

事件时间。

需要提取时间戳,这个是提取的是升序的时间戳,就是5秒的来了我就关闭了。

实际情况是分布式的,网络时间是有延迟的,如何提取乱序的时间戳呢?

---

乱序时间是怎么确定的呢?

延时时间2S则5秒来了,水位线时间是3秒,就是当前的事件时间戳减去延时时间就是当前时间。上面的例子可以设置延迟1秒。

在事件时间的语义下,Watermark就是事件时间。

---04---

我们现在是事件时间了。

延迟发车是怎么回事,就是假如5这个数据来了,但是我并不是马上关闭窗口的,而是延时1秒,此时实践时间是4,就是6这个数据来了,我才会关闭窗口。延时3秒,就是假如是7就是7-3为现在到4秒了。

延时1秒,6秒的数据来了,我5秒的发车了,一切都是按照时间戳的。

直观看的是最大的乱序时间。

1. Watermark就是事件时间,代表当前时间的进展

2. Watermark主要用来处理乱序数据,一般就是直接定义一个延迟时间,延迟触发窗口操作
这里的延迟,指的是当前收到的数据内的时间戳

3. Watermark延迟时间的设置,一般要根据数据的乱序情况来定,通常设置成最大乱序程度
如果按照最大乱序程度定,那么就能保证所有窗口的数据都是正确的
要权衡正确性和实时性的话,可以不按最大乱序程度,而是给一个相对较小的watermark延迟
watermark延迟时间,完全是程序自己定义的,可以拍脑袋给一个数
最好的处理方式,是先了解数据的分布情况(抽样、或者根据经验、机器学习算法),可以指定一个合理的延迟,比较小,还能处理绝大多数乱序的情况

4. 关窗操作,必须是时间进展到窗口关闭时间,事件时间语义下就是watermark达到窗口关闭时间
当前Ts最大时间戳-延迟时间 = watermark,如果现在的watermark大于等于窗口结束时间,就关闭窗口

5. watermark代表的含义是,之后就不会再来时间戳比watermark里面的数值小的数据了
如果有不同的上游分区,当前任务会对它们创建各自的分区watermark,当前任务的事件时间就是最小的那个

6. 处理乱序数据,Flink有三重保证
watermark可以设置延迟时间
window的allowedLateness方法,可以设置窗口允许处理迟到数据的时间
window的sideOutputLateData方法,可以将迟到的数据写入侧输出流

窗口有两个重要操作:触发计算,清空状态(关闭窗口)

waterMark可以认为是插入在数据流里面的一条特殊的是数据,一条一条来。

watermark必须是单调递增的,总是和时间戳是相关的。

waterMark就是一条特殊的数据记录,代表之前的数据都到齐了。

下游的任务waterMark应该广播出去。

应该以最小的watermark来判断,代表之前的数据都到齐了。

---05---

其中三角形是代表的是插入的warterMark。因为延迟3秒所以在8秒的时间戳之后。

延时3秒就是-3s,8s相当于5s,8-3=5就相当于5s,关闭5秒的窗口。

waterMark按照当前得最大的时间戳-延迟时间。

9-4=5才关闭

waterMark就是最大的时间戳-延迟时间去生成的。

1.升序的数据,升序数据是不用定义waterMark的。

2.乱序的数据,这里定义了延时时间和提取什么时间戳。

完整的代码:

---

waterMark是广播出去的。 

---06---

生成的waterMark就是当前的最大的时间戳减去延时的时间。

再来一次:

小时间戳来了还是按照大时间戳算的。

关于watermark的设置的问题:

代码:

正常的窗口是触发计算输出结果然后关闭。

设置了allowedLateness之后,窗口再保持一分钟,先不要关闭,只是触发计算,等到waterMark涨到一分钟后再关闭。先输出一个统计结果但是不关闭。以后来一条更新一次来一条更新一次输出结果,不是往桶里面扔了。

窗口是左闭右开的。

即使这样了,还有乱序数据呢?我还是不想丢呢?侧输出流,注意侧输出流是不能叠加更新了,因为窗口是真的关闭了。

看下这个名字就是sideOutPutlateData再取出来,然后sink出去:

---07---

侧输出流相当于下一班车的时间。

---08---

---09---

如果是升序的不设置延迟时间也是可以的:

1是配置时间戳字段,2是给一个延迟时间。完全是升序的话可以不设置延迟也是可以的。

点进去assign这个方法,可以传入的参数。

可以传两种参数的:

这个是我们自己之前一直用的,是周期的:

第一个是周期的第二个是间断的。

// 自定义一个周期性生成watermark的Assigner
class MyWMAssigner(lateness: Long) extends AssignerWithPeriodicWatermarks[SensorReading]{
  // 需要两个关键参数,延迟时间,和当前所有数据中最大的时间戳
//  val lateness: Long = 1000L
  var maxTs: Long = Long.MinValue + lateness

  // 这个我隔一段时间要调用getWaterMark方法的 插入到DataStream中去 
  override def getCurrentWatermark: Watermark =
    new Watermark(maxTs - lateness)

// 来一个数据我就调用这个方法
  override def extractTimestamp(element: SensorReading, previousElementTimestamp: Long): Long = {
    maxTs = maxTs.max(element.timestamp * 1000L)
    element.timestamp * 1000L
  }
}

看下这个就是隔一段时间自动生成watermark插入进去,看下那个减法满足最大时间戳-当前时间延迟

---

根据数据触发的,来一次触发一次,只是和当前的数据有关的。

// 自定义一个断点式生成watermark的Assigner
class MyWMAssigner2 extends AssignerWithPunctuatedWatermarks[SensorReading]{
  val lateness: Long = 1000L
  override def checkAndGetNextWatermark(lastElement: SensorReading, extractedTimestamp: Long): Watermark = {
    if( lastElement.id == "sensor_1" ){
      new Watermark(extractedTimestamp - lateness)
    } else
      null
  }

  override def extractTimestamp(element: SensorReading, previousElementTimestamp: Long): Long =
    element.timestamp * 1000L
}

大部分是周期的。数据稀疏适合2 数据密集适合1

默认的时间周期是200ms,自己配置:

一般是往小调。

---10---

DataStream keyedStream window之后 都可以调用

可以访问时间戳信息和水位线信息,其他是不能访问的。

processWindow是一个全窗口函数。

我们看下reduceFunction,两个参数,之前的结果和现在的数据。

1. 普通的transform算子,只能获取到当前的数据,或者加上聚合状态
如果是RichFunction,可以有生命周期方法,还可以获取运行时上下文,进行状态编程
但是它们都不能获取时间戳和watermark相关的信息

2. Process Function是唯一可以获取到时间相关信息的API
RichFunction能做的ProcessFunction都能做,三实际上是特殊的richFunction。
另外,可以获取到timestamp和watermark
可以注册定时器,指定某个时间点发生的操作
还可以输出侧输出流

看下继承的关系

---11---

可以获取当前运行时上下文。需要获得当前的时间戳的信息的话就要用到和水位线的信息。

可以注册定时器。

可以分流的操作。

这个每个窗口都是不是连续上升的,但是虚线是连续上升的。

滚动和滑动都不靠谱的。

滚动窗口可以完成吗?---不可以

滑动可以吗?---不可以

全窗口函数:https://blog.csdn.net/weixin_46266718/article/details/109521248

这里可以定义闹钟的。

------------------------------

keyBy之后可以传processFunction也可以传KeyedProcessFunction,因为KeyedStream继承了DataStream

------------------------------

注意这个注册的是注册的10s后的定时器。

不是来一个就注册一个定时器,如果比之前的温度高并且没有定时器才会注册定时器的。

package day4

import com.atguigu.apitest.SensorReading
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.api.java.tuple.{Tuple, Tuple1}
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector

/**
  * Copyright (c) 2018-2028 尚硅谷 All Rights Reserved
  *
  * Project: FlinkTutorial
  * Package: day4
  * Version: 1.0
  *
  * Created by wushengran on 2020/4/20 15:40
  */
object ProcessFunctionTest {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)
    val inputStream = env.socketTextStream("192.168.244.133", 7777)
    val dataStream: DataStream[SensorReading] = inputStream
      .map( data => {
        val dataArray = data.split(",")
        SensorReading( dataArray(0), dataArray(1).toLong, dataArray(2).toDouble )
      } )
    // 检测每一个传感器温度是否连续上升,在10秒之内
    val warningStream: DataStream[String] = dataStream
      .keyBy("id")
      .process( new TempIncreWarning(10000L) )// 注意这里是process方法
    warningStream.print()
    env.execute("process function job")
  }
}

// 自定义 KeyedProcessFunction k i o keyBy("id")的时候必须要元组的
class TempIncreWarning(interval: Long) extends KeyedProcessFunction[Tuple, SensorReading, String]{
  // 由于需要跟之前的温度值做对比,所以将上一个温度保存成状态 在类开始就创建可能还没有上下文呢
  lazy val lastTempState: ValueState[Double] = getRuntimeContext.getState( new ValueStateDescriptor[Double]("lastTemp", classOf[Double]))
  // 为了方便删除定时器,还需要保存定时器的时间戳
  lazy val curTimerTsState: ValueState[Long] = getRuntimeContext.getState( new ValueStateDescriptor[Long]("cur-timer-ts", classOf[Long]) )
  // 每来一条数据就会调用这个方法
  override def processElement(value: SensorReading, ctx: KeyedProcessFunction[Tuple, SensorReading, String]#Context, out: Collector[String]): Unit = {
    // 首先取出状态
    val lastTemp = lastTempState.value()
    val curTimerTs = curTimerTsState.value()
    // 将上次温度值的状态更新为当前数据的温度值
    lastTempState.update(value.temperature)
    // 判断当前温度值,如果比之前温度高,并且没有定时器的话,注册10秒后的定时器,默认值是0,就是没有注册定时器
    if( value.temperature > lastTemp && curTimerTs == 0 ){
      val ts = ctx.timerService().currentProcessingTime() + interval
      ctx.timerService().registerProcessingTimeTimer(ts)
      curTimerTsState.update(ts)
    }
    // 如果温度下降,删除定时器
    else if( value.temperature < lastTemp ){
      ctx.timerService().deleteProcessingTimeTimer(curTimerTs)
      // 清空状态
      curTimerTsState.clear()
    }
  }
  // 定时器触发,说明10秒内没有来下降的温度值,报警
  override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Tuple, SensorReading, String]#OnTimerContext, out: Collector[String]): Unit = {
    val key = ctx.getCurrentKey.asInstanceOf[Tuple1[String]].f0
    out.collect( "温度值连续" + interval/1000 + "秒上升" )
    curTimerTsState.clear()
  }
}

注册定义一个闹钟是10s之后去触发的。

如果有10s下降的就删除定时器。

只有当前的数据肯定是搞不定的,还要知道上一个数据。

所以还涉及到状态编程的。

为了方便删除定时器,还要保留定时器设置时候的时间戳。

这个默认值是0。

最后注意一个细节:为什么是lazy呢?

注意下这个:开始只是定义,等到调用才去创建,你那么着急创建的话可能还没有运行时上下文呢。

来一个数据,注册一个10s之后的定时器,如果温度是一直上升的,那么我就什么也不干,等着定时器触发。

假如中间来了下降的数据。

---12---

开始做10s温度上升的测试:

传感器10s温度上升用窗口做是不合适的。

package day4

import com.atguigu.apitest.SensorReading
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.api.java.tuple.{Tuple, Tuple1}
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector

/**
  * Copyright (c) 2018-2028 尚硅谷 All Rights Reserved
  *
  * Project: FlinkTutorial
  * Package: day4
  * Version: 1.0
  *
  * Created by wushengran on 2020/4/20 15:40
  */
object ProcessFunctionTest {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)
    val inputStream = env.socketTextStream("hadoop102", 7777)
    val dataStream: DataStream[SensorReading] = inputStream
      .map( data => {
        val dataArray = data.split(",")
        SensorReading( dataArray(0), dataArray(1).toLong, dataArray(2).toDouble )
      } )
    // 检测每一个传感器温度是否连续上升,在10秒之内
    val warningStream: DataStream[String] = dataStream
      .keyBy("id")
      .process( new TempIncreWarning(10000L) )
    warningStream.print()
    env.execute("process function job")
  }
}

// 自定义 KeyedProcessFunction k i o keyBy("id")的时候必须要元组的
class TempIncreWarning(interval: Long) extends KeyedProcessFunction[Tuple, SensorReading, String]{
  // 由于需要跟之前的温度值做对比,所以将上一个温度保存成状态
  lazy val lastTempState: ValueState[Double] = getRuntimeContext.getState( new ValueStateDescriptor[Double]("lastTemp", classOf[Double]))
  // 为了方便删除定时器,还需要保存定时器的时间戳
  lazy val curTimerTsState: ValueState[Long] = getRuntimeContext.getState( new ValueStateDescriptor[Long]("cur-timer-ts", classOf[Long]) )
  // 每来一条数据就会调用这个方法
  override def processElement(value: SensorReading, ctx: KeyedProcessFunction[Tuple, SensorReading, String]#Context, out: Collector[String]): Unit = {
    // 首先取出状态
    val lastTemp = lastTempState.value()
    val curTimerTs = curTimerTsState.value()
    // 将上次温度值的状态更新为当前数据的温度值
    lastTempState.update(value.temperature)
    // 判断当前温度值,如果比之前温度高,并且没有定时器的话,注册10秒后的定时器
    if( value.temperature > lastTemp && curTimerTs == 0 ){
      val ts = ctx.timerService().currentProcessingTime() + interval
      ctx.timerService().registerProcessingTimeTimer(ts)
      curTimerTsState.update(ts)
    }
    // 如果温度下降,删除定时器
    else if( value.temperature < lastTemp ){
      ctx.timerService().deleteProcessingTimeTimer(curTimerTs)
      // 清空状态
      curTimerTsState.clear()
    }
  }
  // 定时器触发,说明10秒内没有来下降的温度值,报警
  override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Tuple, SensorReading, String]#OnTimerContext, out: Collector[String]): Unit = {
    val key = ctx.getCurrentKey.asInstanceOf[Tuple1[String]].f0
    out.collect( "温度值连续" + interval/1000 + "秒上升" )
    curTimerTsState.clear()
  }
}

思路就是定义一个闹钟,10s触发,来一个定义一个。

有下降就删掉。

---12---

复习:

略。

---13---

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值