Flink的window和Time

Flink的window和Time

1、window

对于流式处理,如果我们需要求取总和,平均值,或者最大值,最小值等,是做不到的,因为数据一直在源源不断的产生,即数据是没有边界的,所以没法求最大值,最小值,平均值等,所以为了一些数值统计的功能,我们必须指定时间段,对某一段时间的数据求取一些数据值是可以做到的。或者对某一些数据求取数据值也是可以做到的。

所以,流上的聚合需要由 window 来划定范围,比如 “计算过去的5分钟” ,或者 “最后100个元素的和” 。

window是一种可以把无限数据切割为有限数据块的手段

1.1、窗口的基本类型介绍

窗口可以是 时间驱动的 【Time Window】(比如:每30秒)或者 数据驱动的【Count Window】 (比如:每100个元素)。

窗口类型汇总:
窗口通常被区分为不同的类型:

  • tumbling windows:滚动窗口 【没有重叠】
  • sliding windows:滑动窗口 【有重叠】
  • session windows:会话窗口 ,一般没人用

1.2、Flink的窗口介绍

Time Window窗口的应用:
time window又分为滚动窗口和滑动窗口,这两种窗口调用方法都是一样的,都是调用timeWindow这个方法,如果传入一个参数就是滚动窗口,如果传入两个参数就是滑动窗口

Count Windos窗口的应用:
与timeWindow类型,CountWinodw也可以分为滚动窗口和滑动窗口,这两个窗口调用方法一样,都是调用countWindow,如果传入一个参数就是滚动窗口,如果传入两个参数就是滑动窗口

自定义window的应用
如果time window和 countWindow还不够用的话,我们还可以使用自定义window来实现数据的统计等功能。

1.3、window的数值聚合统计

对于某一个window内的数值统计,我们可以增量的聚合统计或者全量的聚合统计

增量聚合统计:

窗口当中每加入一条数据,就进行一次统计

  • reduce(reduceFunction)
  • aggregate(aggregateFunction)
  • sum(),min(),max()

需求:通过接收socket当中输入的数据,统计每5秒钟数据的累计的值
代码实现:

import org.apache.flink.api.common.functions.ReduceFunction
import org.apache.flink.streaming.api.datastream.DataStreamSink
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
import org.apache.flink.streaming.api.windowing.time.Time

object FlinkTimeCount {
  def main(args: Array[String]): Unit = {
    val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.api.scala._

    val socketStream: DataStream[String] = environment.socketTextStream("node01",9000)
    val print: DataStreamSink[(Int, Int)] = socketStream
      .map(x => (1, x.toInt))
      .keyBy(0)
      .timeWindow(Time.seconds(5))
      .reduce(new ReduceFunction[(Int, Int)] {
        override def reduce(t: (Int, Int), t1: (Int, Int)): (Int, Int) = {
          (t._1, t._2 + t1._2)
        }
      }).print()

    environment.execute("startRunning")
  }
}

全量聚合统计:

等到窗口截止,或者窗口内的数据全部到齐,然后再进行统计.

可以用于求窗口内的数据的最大值,或者最小值,平均值等。属于窗口的数据到齐,才开始进行聚合计算。【可以实现对窗口内的数据进行排序等需求】

  • apply(windowFunction)
  • process(processWindowFunction)

processWindowFunction比windowFunction提供了更多的上下文信息。

需求:通过全量聚合统计,求取每3条数据的平均值

import org.apache.flink.api.java.tuple.Tuple
import org.apache.flink.streaming.api.datastream.DataStreamSink
import org.apache.flink.streaming.api.scala.function.ProcessWindowFunction
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment, WindowedStream}
import org.apache.flink.streaming.api.windowing.windows.{GlobalWindow, TimeWindow}
import org.apache.flink.util.Collector


object FlinkCountWindowAvg {

  def main(args: Array[String]): Unit = {
    val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

    import org.apache.flink.api.scala._
    val socketStream: DataStream[String] = environment.socketTextStream("node01",9000)
    //统计一个窗口内的数据的平均值
    val socketDatas: DataStreamSink[Double] = socketStream.map(x => (1, x.toInt))
      .keyBy(0)
      //.timeWindow(Time.seconds(10))
      .countWindow(3)
      //通过process方法来统计窗口的平均值
      .process(new MyProcessWindowFunctionclass).print()
    //必须调用execute方法,否则程序不会执行
    environment.execute("count avg")
  }
}

/**ProcessWindowFunction 需要跟四个参数
  * 输入参数类型,输出参数类型,聚合的key的类型,window的下界
  *
  */
class MyProcessWindowFunctionclass extends ProcessWindowFunction[(Int , Int) , Double , Tuple , GlobalWindow]{
  override def process(key: Tuple, context: Context, elements: Iterable[(Int, Int)], out: Collector[Double]): Unit = {
    var totalNum = 0;
    var countNum = 0;
    for(data <-  elements){
      totalNum +=1
      countNum += data._2
    }
    out.collect(countNum/totalNum)
  }
}

2、Time

可以通过window窗口来统计每一段时间或者每多少条数据的一些数值统计,但是也存在另外一个问题,就是如果数据有延迟该如何解决。

例如:一个窗口定义的是每隔五分钟统计一次,我们应该在上午九点至九点零五分这段时间统计一次数据的结果值,但是由于某一条数据由于网络延迟,数据产生时间是在九点零三分,数据到达我们的flink框架已经是在十点零三分了,这种问题怎么解决??

再例如:

原始日志如下:

日志自带时间2018-10-10 10:00:01,134 INFO executor.Executor: Finished task in state 0.0

数据进入flink框架时间:2018-10-10 20:00:00,102
数据被window窗口处理时间2018-10-10 20:00:01,100

为了解决这个问题,flink在实时处理当中,对数据当中的时间规划为以下三个类型。

针对stream数据中的时间,可以分为以下三种

  • Event Time:事件产生的时间,它通常由事件中的时间戳描述。
  • Ingestion time:事件进入Flink的时间
  • Processing Time:事件被处理时当前系统的时间

2.1、三个Time类型

1、EventTime
  • 1.事件生成时的时间,在进入Flink之前就已经存在,可以从event的字段中抽取。
  • 2.必须指定watermarks(水位线)的生成方式。
  • 3.优势:确定性,乱序、延时、或者数据重放等情况,都能给出正确的结果
  • 4.弱点:处理无序事件时性能和延迟受到影响
2、IngestTime
  • 1.事件进入flink的时间,即在source里获取的当前系统的时间,后续操作统一使用该时间。
  • 2.不需要指定watermarks的生成方式(自动生成)
  • 3.弱点:不能处理无序事件和延迟数据
3、ProcessingTime
  • 1.执行操作的机器的当前系统时间(每个算子都不一样)
  • 2.不需要流和机器之间的协调
  • 3.优势:最佳的性能和最低的延迟
  • 4.弱点:不确定性 ,容易受到各种因素影像(event产生的速度、到达flink的速度、在算子之间传输速度等),压根就不管顺序和延迟
4、三种时间的综合比较
  • 性能: ProcessingTime> IngestTime> EventTime
  • 延迟: ProcessingTime< IngestTime< EventTime
  • 确定性: EventTime> IngestTime> ProcessingTime

5、如何设置time类型
在我们创建StreamExecutionEnvironment的时候可以设置time类型,不设置time类型,默认是processingTime,如果设置time类型为eventTime,那么必须要在我们的source之后明确指定Timestamp Assigner & Watermark Generator

// 设置时间特性
val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
//  不设置Time 类型,默认是processingTime。
//   如果使用EventTime则需要在source之后明确指定Timestamp Assigner & Watermark Generator
environment.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime)

2.2、watermark

1、watermark的作用

Flink的waterMark实现解决乱序以及延迟数据
watermark是用于处理乱序事件的,而正确的处理乱序事件,通常用watermark机制结合window来实现。

流处理从事件产生,到流经source,再到operator,中间是有一个过程和时间的。虽然大部分情况下,流到operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络、背压等原因,导致乱序的产生(out-of-order或者说late element)。

但是对于late element,我们又不能无限期的等下去,必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了。这个特别的机制,就是watermark。

2、watermark解决迟到的数据

实时系统中,由于各种原因造成的延时,造成某些消息发到flink的时间延时于事件产生的时间。如果基于event time构建window,但是对于late element,我们又不能无限期的等下去,必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了。这个特别的机制,就是watermark。

==Watermarks(水位线)==就是来处理这种问题的机制

  • 1.参考google的DataFlow。
  • 2.是event time处理进度的标志。
  • 3.表示比watermark更早(更老)的事件都已经到达(没有比水位线更低的数据 )。
  • 4.基于watermark来进行窗口触发计算的判断。

有序的数据流watermark:

在某些情况下,基于Event Time的数据流是有续的(相对event time)。在有序流中,watermark就是一个简单的周期性标记。

无序的数据流watermark:

在更多场景下,基于Event Time的数据流是无续的(相对event time)。
在无序流中,watermark至关重要,她告诉operator比watermark更早(更老/时间戳更小)的事件已经到达, operator可以将内部事件时间提前到watermark的时间戳(可以触发window计算啦)

并行流当中的watermark:

通常情况下, watermark在source函数中生成,但是也可以在source后任何阶段,如果指定多次 watermark,后面指定的 watermarker会覆盖前面的值。 source的每个sub task独立生成水印。
watermark通过operator时会推进operators处的当前event time,同时operators会为下游生成一个新的watermark。

多输入operator(union、 keyBy、 partition)的当前event time是其输入流event time的最小值。
注意:多并行度的情况下,watermark对齐会取所有channel最小的watermark

watermark介绍参考链接

3、watermark如何生成

通常,在接收到source的数据后,应该立刻生成watermark;但是,也可以在source后,应用简单的map或者filter操作,然后再生成watermark。
生成watermark的方式主要有2大类:
(1):With Periodic Watermarks
(2):With Punctuated Watermarks

第一种可以定义一个最大允许乱序的时间,这种情况应用较多。

我们主要来围绕Periodic Watermarks来说明,下面是生成periodic watermark的方法:

4、watermark处理顺序数据

需求:定义一个窗口为10s,通过数据的event time时间结合watermark实现延迟10s的数据也能够正确统计
我们通过数据的eventTime来向前推10s,得到数据的watermark,
代码实现:

import java.text.SimpleDateFormat
import org.apache.flink.api.java.tuple.Tuple
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.{AssignerWithPeriodicWatermarks, AssignerWithPunctuatedWatermarks}
import org.apache.flink.streaming.api.scala.function.WindowFunction
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
import org.apache.flink.streaming.api.watermark.Watermark
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector

import scala.collection.mutable.ArrayBuffer
import scala.util.Sorting

object FlinkWaterMark2 {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.api.scala._

    //设置flink的数据处理时间为eventTime
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    env.setParallelism(1)
    val tupleStream: DataStream[(String, Long)] = env.socketTextStream("node01", 9000).map(x => {
      val strings: Array[String] = x.split(" ")

      (strings(0), strings(1).toLong)
    })

    //注册我们的水印
    val waterMarkStream: DataStream[(String, Long)] = tupleStream.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks[(String, Long)] {
      var currentTimemillis: Long = 0L
      var timeDiff: Long = 10000L
      val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

      /* //获取当前数据的waterMark
       override def getNext: Watermark = {
       }*/
      override def getCurrentWatermark: Watermark = {
        val watermark = new Watermark(currentTimemillis - timeDiff)
        watermark
      }

      //抽取数据的eventTime
      override def extractTimestamp(element: (String, Long), l: Long): Long = {
        val enventTime = element._2
        currentTimemillis = Math.max(enventTime, currentTimemillis)
        val id = Thread.currentThread().getId
        println("currentThreadId:" + id + ",key:" + element._1 + ",eventtime:[" + element._2 + "|" + sdf.format(element._2) + "],currentMaxTimestamp:[" + currentTimemillis + "|" + sdf.format(currentTimemillis) + "],watermark:[" + this.getCurrentWatermark.getTimestamp + "|" + sdf.format(this.getCurrentWatermark.getTimestamp) + "]")
        enventTime
      }
    })
    waterMarkStream.keyBy(0)
      .window(TumblingEventTimeWindows.of(Time.seconds(10)))
      .apply(new MyWindowFunction2).print()
    env.execute()
  }
}


class MyWindowFunction2 extends WindowFunction[(String,Long),String,Tuple,TimeWindow]{
  override def apply(key: Tuple, window: TimeWindow, input: Iterable[(String, Long)], out: Collector[String]): Unit = {
    val keyStr = key.toString
    val arrBuf = ArrayBuffer[Long]()
    val ite = input.iterator
    while (ite.hasNext){
      val tup2 = ite.next()
      arrBuf.append(tup2._2)
    }
    val arr = arrBuf.toArray
    Sorting.quickSort(arr)  //对数据进行排序,按照eventTime进行排序
    val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    val result = "聚合数据的key为:"+keyStr + "," + "窗口当中数据的条数为:"+arr.length + "," + "窗口当中第一条数据为:"+sdf.format(arr.head) + "," +"窗口当中最后一条数据为:"+ sdf.format(arr.last)+ "," + "窗口起始时间为:"+sdf.format(window.getStart) + "," + "窗口结束时间为:"+sdf.format(window.getEnd)  + "!!!!!看到这个结果,就证明窗口已经运行了"
    out.collect(result)
  }
}

输入测验数据
注意:如果需要触发flink的窗口调用,必须满足两个条件

  • 1:waterMarkTime > eventTime
  • 2:窗口内有数据

数据输入测验:
按照十秒钟统计一次,我们程序会将时间划分成为以下时间间隔段

2019-10-01 10:11:002019-10-01 10:11:10
2019-10-01 10:11:102019-10-01 10:11:20
2019-10-01 10:11:202019-10-01 10:11:30
2019-10-01 10:11:302019-10-01 10:11:40
2019-10-01 10:11:402019-10-01 10:11:50
2019-10-01 10:11:502019-10-01 10:12:00
	

顺序计算:
触发数据计算的条件依据为两个

第一个waterMark时间大于数据的eventTime时间,第二个窗口之内有数据

我们这里的waterMark直接使用eventTime的最大值减去10秒钟

0001 1569895882000	 数据eventTime为:2019-10-01 10:11:22  数据waterMark为  2019-10-01 10:11:12
0001 1569895885000	 数据eventTime为:2019-10-01 10:11:25  数据waterMark为  2019-10-01 10:11:15
0001 1569895888000	 数据eventTime为:2019-10-01 10:11:28  数据waterMark为  2019-10-01 10:11:18

0001 1569895890000	 数据eventTime为:2019-10-01 10:11:30  数据waterMark为  2019-10-01 10:11:20
0001 1569895891000	 数据eventTime为:2019-10-01 10:11:31  数据waterMark为  2019-10-01 10:11:21
0001 1569895895000	 数据eventTime为:2019-10-01 10:11:35  数据waterMark为  2019-10-01 10:11:25
0001 1569895898000	 数据eventTime为:2019-10-01 10:11:38  数据waterMark为  2019-10-01 10:11:28

0001 1569895900000	 数据eventTime为:2019-10-01 10:11:40  数据waterMark为  2019-10-01 10:11:30  触发第一条到第三条数据计算,数据包前不包后,不会计算2019-10-01 10:11:30 这条数据
0001 1569895911000	 数据eventTime为:2019-10-01 10:11:51  数据waterMark为  2019-10-01 10:11:41  触发2019-10-01 10:11:202019-10-01 10:11:28时间段的额数据计算,数据包前不包后,不会触发2019-10-01 10:11:30这条数据的计算
5、watermark处理乱序数据

输入测验数据
接着继续输入以下乱序数据,验证flink乱序数据的问题是否能够解决

乱序数据

0001 1569895948000	 数据eventTime为:2019-10-01 10:12:28  数据waterMark为  2019-10-01 10:12:18  
0001 1569895945000	 数据eventTime为:2019-10-01 10:12:25  数据waterMark为  2019-10-01 10:12:18  
0001 1569895947000	 数据eventTime为:2019-10-01 10:12:27  数据waterMark为  2019-10-01 10:12:18  

0001 1569895950000	 数据eventTime为:2019-10-01 10:12:30  数据waterMark为  2019-10-01 10:12:20  

0001 1569895960000	 数据eventTime为:2019-10-01 10:12:40  数据waterMark为  2019-10-01 10:12:30  
触发计算 waterMark > eventTime 并且窗口内有数据,触发 2019-10-01 10:12:282019-10-01 10:12:27 这三条数据的计算,数据包前不包后,
不会触发2019-10-01 10:12:30 这条数据的计算
0001 1569895949000	 数据eventTime为:2019-10-01 10:12:29  数据waterMark为  2019-10-01 10:12:30  
迟到太多的数据,flink直接丢弃,可以设置flink将这些迟到太多的数据保存起来,便于排查问题
6、比watermark更晚的数据如何解决

如果我们设置数据的watermark为每条数据的eventtime往后一定的时间,例如数据的eventtime为2019-08-20 15:30:30,程序的window窗口为10s,然后我们设置的watermark为2019-08-20 15:30:40
那么如果某一条数据eventtime为2019-08-20 15:30:32,到达flink程序的时间为2019-08-20 15:30:45 该怎么办,这条数据比窗口的watermark时间还要晚了5S钟该怎么办??

对于这种比watermark还要晚的数据,flink有三种处理方式
1、直接丢弃
我们输入一个乱序很多的(其实只要 Event Time < watermark 时间)数据来测试下: 输入:【输入两条内容】

late element
0001 1569895948000	 数据eventTime为:2019-10-01 10:12:28  数据直接丢弃 
0001 1569895945000	 数据eventTime为:2019-10-01 10:12:25  数据直接丢弃

注意:此时并没有触发 window。因为输入的数据所在的窗口已经执行过了,flink 默认对这 些迟到的数据的处理方案就是丢弃。

2、allowedLateness 指定允许数据延迟的时间

在某些情况下,我们希望对迟到的数据再提供一个宽容的时间。

Flink 提供了 allowedLateness 方法可以实现对迟到的数据设置一个延迟时间,在指定延迟时间内到达的数据还是可以触发 window 执行的。

修改代码:

waterMarkStream
  .keyBy(0)
  .window(TumblingEventTimeWindows.of(Time.seconds(3)))
  .allowedLateness(Time.seconds(2))//允许数据迟到2S
  //function: (K, W, Iterable[T], Collector[R]) => Unit
  .apply(new MyWindowFunction).print()

验证数据迟到性:
输入数据:

更改代码之后重启我们的程序,然后从新输入之前的数据

0001 1569895882000
0001 1569895885000
0001 1569895888000
0001 1569895890000
0001 1569895891000
0001 1569895895000
0001 1569895898000
0001 1569895900000
0001 1569895911000
0001 1569895948000
0001 1569895945000
0001 1569895947000
0001 1569895950000
0001 1569895960000
0001 1569895949000

验证数据的延迟性:定义数据仅仅延迟2S的数据重新接收,重新计算

0001 1569895948000	 数据eventTime为:2019-10-01 10:12:28  触发数据计算  数据waterMark为  2019-10-01 10:12:30
0001 1569895945000	 数据eventTime为:2019-10-01 10:12:25  触发数据计算  数据waterMark为  2019-10-01 10:12:30


0001 1569895958000	 数据eventTime为:2019-10-01 10:12:38  不会触发数据计算 数据waterMark为  2019-10-01 10:12:30  waterMarkTime  <  eventTime,所以不会触发计算

将数据的waterMark调整为41秒就可以触发上面这条数据的计算了
0001 1569895971000	 数据eventTime为:2019-10-01 10:12:51  数据waterMark为  2019-10-01 10:12:41
又会继续触发0001 1569895958000	这条数据的计算了

3、sideOutputLateData 收集迟到的数据

通过 sideOutputLateData 可以把迟到的数据统一收集,统一存储,方便后期排查问题。 需要先调整代码:

import java.text.SimpleDateFormat

import org.apache.flink.api.java.tuple.Tuple
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.datastream.DataStreamSink
import org.apache.flink.streaming.api.functions.AssignerWithPunctuatedWatermarks
import org.apache.flink.streaming.api.scala.function.WindowFunction
import org.apache.flink.streaming.api.scala.{DataStream, OutputTag, StreamExecutionEnvironment}
import org.apache.flink.streaming.api.watermark.Watermark
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector

import scala.collection.mutable.ArrayBuffer
import scala.util.Sorting


object FlinkWaterMark {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.api.scala._
    //设置time类型为eventtime
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    //暂时定义并行度为1
    env.setParallelism(1)
    val text = env.socketTextStream("node01",9000)
    val inputMap: DataStream[(String, Long)] = text.map(line => {
      val arr = line.split(" ")
      (arr(0), arr(1).toLong)
    })

    //给我们的数据注册waterMark
    val waterMarkStream: DataStream[(String, Long)] = inputMap
      .assignTimestampsAndWatermarks(new AssignerWithPunctuatedWatermarks[(String, Long)] {
      var currentMaxTimestamp = 0L

      //watermark基于eventTime向后推迟10秒钟,允许消息最大乱序时间为10s
      val waterMarkDiff: Long = 10000L

      val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
      //获取下一个水印
      override def checkAndGetNextWatermark(t: (String, Long), l: Long): Watermark = {
        val watermark = new Watermark(currentMaxTimestamp - waterMarkDiff)
        watermark
      }
      //抽取当前数据的时间作为eventTime
      override def extractTimestamp(element: (String, Long), l: Long): Long = {
        val eventTime = element._2
        currentMaxTimestamp = Math.max(eventTime, currentMaxTimestamp)
        val id = Thread.currentThread().getId
        println("currentThreadId:"+id+",key:"+element._1+",eventtime:["+element._2+"|"+sdf.format(element._2)+"],currentMaxTimestamp:["+currentMaxTimestamp+"|"+ sdf.format(currentMaxTimestamp)+"],watermark:["+this.checkAndGetNextWatermark(element,l).getTimestamp+"|"+sdf.format(this.checkAndGetNextWatermark(element,l).getTimestamp)+"]")
        eventTime
      }
    })


    val outputTag: OutputTag[(String, Long)] = new OutputTag[(String,Long)]("late_data")
    val outputWindow: DataStream[String] = waterMarkStream
      .keyBy(0)
      .window(TumblingEventTimeWindows.of(Time.seconds(3)))
      // .allowedLateness(Time.seconds(2))//允许数据迟到2S
      .sideOutputLateData(outputTag)
      //function: (K, W, Iterable[T], Collector[R]) => Unit
      .apply(new MyWindowFunction)


    val sideOuptut: DataStream[(String, Long)] = outputWindow.getSideOutput(outputTag)

    sideOuptut.print()
    outputWindow.print()

    //执行程序
    env.execute()

  }
}

class MyWindowFunction extends WindowFunction[(String,Long),String,Tuple,TimeWindow]{
  override def apply(key: Tuple, window: TimeWindow, input: Iterable[(String, Long)], out: Collector[String]): Unit = {
    val keyStr = key.toString
    val arrBuf = ArrayBuffer[Long]()
    val ite = input.iterator
    while (ite.hasNext){
      val tup2 = ite.next()
      arrBuf.append(tup2._2)
    }
    val arr = arrBuf.toArray
    Sorting.quickSort(arr)
    val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    val result = keyStr + "," + arr.length + "," + sdf.format(arr.head) + "," + sdf.format(arr.last)+ "," + sdf.format(window.getStart) + "," + sdf.format(window.getEnd)
    out.collect(result)
  }
}

我们来输入一些数据验证一下 输入:

0001 1569895882000
0001 1569895885000
0001 1569895888000
0001 1569895890000
0001 1569895891000
0001 1569895895000
0001 1569895898000
0001 1569895900000
0001 1569895911000
0001 1569895948000
0001 1569895945000
0001 1569895947000
0001 1569895950000
0001 1569895960000
0001 1569895949000

输入两条迟到的数据,会被收集起来

0001 1569895948000
0001 1569895945000

此时,针对这几条迟到的数据,都通过 sideOutputLateData 保存到了 outputTag 中。

7、多并行度的watermark机制

前面代码中设置了并行度为 1 ,env.setParallelism(1);

如果这里不设置的话,代码在运行的时候会默认读取本机 CPU 数量设置并行度。 把代码的并行度代码注释掉//env.setParallelism(1)

然后在输出内容前面加上线程 id

会出现如下数据: 输入如下几行内容:

输出:

会发现 window 没有被触发。
因为此时,这 7 条数据都是被不同的线程处理的。每个线程都有一个 watermark。
因为在多并行度的情况下,watermark 对齐会取所有 channel 最小的 watermark 但是我们现在默认有 8 个并行度,这 7 条数据都被不同的线程所处理,到现在还没获取到最 小的 watermark,所以 window 无法被触发执行。

下面我们来验证一下,把代码中的并行度调整为 2. env.setParallelism(2)
输入如下内容:

0001 1569895890000
0001 1569895903000
0001 1569895908000

输出:

此时会发现,当第三条数据输入完以后,[10:11:30,10:11:33)这个 window 被触发了。 前两条数据输入之后,获取到的最小 watermark 是 10:11:20,这个时候对应的 window 中没 有数据。
第三条数据输入之后,获取到的最小 watermark 是 10:11:33,这个时候对应的窗口就是 [10:11:30,10:11:33)。所以就触发了。

3、广播变量,累加器,计数器以及分布式缓存

3.1、广播变量

广播变量主要分为两种方式:dataStream当中的广播变量以及dataSet当中的广播变量,这两个地方的广播变量还有一定的不一样的各自的特性,一句话解释,可以理解为是一个公共的共享变量,我们可以把一个dataset 数据集广播出去,然后不同的task在节点上都能够获取到,这个数据在每个节点上只会存在一份,节约内存。

1、dataStream当中的广播分区

将数据广播给所有的分区,数据可能会被重复处理,一般用于某些公共的配置信息读取,不会涉及到更改的数据
将公共数据广播到所有分区

import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}

object FlinkBroadCast {
  def main(args: Array[String]): Unit = {
    val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    environment.setParallelism(4)
    import org.apache.flink.api.scala._
    val result: DataStream[String] = environment.fromElements("hello").setParallelism(1)
    val resultValue: DataStream[String] = result.broadcast.map(x => {
      println(x)
      x
    })
    resultValue.print()
    environment.execute()
  }
}
2、dataSet当中的广播变量

广播变量允许编程人员在每台机器上保持1个只读的缓存变量,而不是传送变量的副本给tasks
广播变量创建后,它可以运行在集群中的任何function上,而不需要多次传递给集群节点。另外需要记住,不应该修改广播变量,这样才能确保每个节点获取到的值都是一致的。

一句话解释,可以理解为是一个公共的共享变量,我们可以把一个dataset 数据集广播出去,然后不同的task在节点上都能够获取到,这个数据在每个节点上只会存在一份。如果不使用broadcast,则在每个节点中的每个task中都需要拷贝一份dataset数据集,比较浪费内存(也就是一个节点中可能会存在多份dataset数据)。
用法
1:初始化数据

DataSet<Integer> toBroadcast = env.fromElements(1, 2, 3)

2:广播数据

.withBroadcastSet(toBroadcast, "broadcastSetName");

3:获取数据

Collection<Integer> broadcastSet = getRuntimeContext().getBroadcastVariable("broadcastSetName");

注意:

  • 1:广播出去的变量存在于每个节点的内存中,所以这个数据集不能太大。因为广播出去的数据,会常驻内存,除非程序执行结束
  • 2:广播变量在初始化广播出去以后不支持修改,这样才能保证每个节点的数据都是一致的。

需求:求取订单对应的商品
将订单和商品数据进行合并成为一条数据

注意:数据格式参见附件中的orders.txt以及product.txt,商品表当中的第1个字段表示商品id,订单表当中的第3个字段表示商品id,字段之间都是使用,进行切割
使用广播变量,将商品数据广播到每一个节点,然后通过订单数据来进行join即可

代码实现

import java.util
import org.apache.flink.api.common.functions.RichMapFunction
import org.apache.flink.api.scala.ExecutionEnvironment
import org.apache.flink.configuration.Configuration
import scala.collection.mutable

object FlinkDataSetBroadCast {
  def main(args: Array[String]): Unit = {
    val environment: ExecutionEnvironment = ExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.api.scala._
    val productData: DataSet[String] = environment.readTextFile("file:///D:\\开课吧课程资料\\Flink实时数仓\\订单与商品表\\product.txt")
    val productMap = new mutable.HashMap[String,String]()

    val prouctMapSet: DataSet[mutable.HashMap[String, String]] = productData.map(x => {
      val strings: Array[String] = x.split(",")
      productMap.put(strings(0), x)
      productMap
    })

    //获取商品数据
    val ordersDataset: DataSet[String] = environment.readTextFile("file:///D:\\开课吧课程资料\\Flink实时数仓\\订单与商品表\\orders.txt")

    //将商品数据转换成为map结构,key为商品id,value为一行数据
    val resultLine: DataSet[String] = ordersDataset.map(new RichMapFunction[String, String] {
      var listData: util.List[Map[String, String]] = null
      var allMap = Map[String, String]()

      override def open(parameters: Configuration): Unit = {
        this.listData = getRuntimeContext.getBroadcastVariable[Map[String, String]]("productBroadCast")
        val listResult: util.Iterator[Map[String, String]] = listData.iterator()
        while (listResult.hasNext) {
          allMap =  allMap.++(listResult.next())
        }
      }

      //获取到了订单数据,将订单数据与商品数据进行拼接成为一整
      override def map(eachOrder: String): String = {
        val str: String = allMap.getOrElse(eachOrder.split(",")(2),"暂时没有值")
        eachOrder + ","+str
      }
    }).withBroadcastSet(prouctMapSet, "productBroadCast")
    resultLine.print()
    environment.execute("broadCastJoin")
  }
}

3.2、累加器

Accumulators(累加器)是非常简单的,通过一个add操作累加最终的结果,在job执行后可以获取最终结果。

最简单的累加器是counter(计数器):你可以通过Accumulator.add(V value)这个方法进行递增。在任务的最后,flink会吧所有的结果进行合并,然后把最终结果发送到client端。累加器在调试或者你想更快了解你的数据的时候是非常有用的。

Flink现在有一下内置累加器。每个累加器都实现了Accumulator接口。
需求:统计tomcat日志当中exception关键字出现了多少次
代码实现:

import org.apache.flink.api.common.accumulators.LongCounter
import org.apache.flink.api.common.functions.RichMapFunction
import org.apache.flink.api.scala.ExecutionEnvironment
import org.apache.flink.configuration.Configuration

object FlinkCounterAndAccumulator {

  def main(args: Array[String]): Unit = {
    val env=ExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.api.scala._
    //统计tomcat日志当中exception关键字出现了多少次
    val sourceDataSet: DataSet[String] = env.readTextFile("file:///D:\\开课吧课程资料\\Flink实时数仓\\catalina.out")

    sourceDataSet.map(new RichMapFunction[String,String] {

      var counter=new LongCounter()

      override def open(parameters: Configuration): Unit = {
        getRuntimeContext.addAccumulator("my-accumulator",counter)
      }
      override def map(value: String): String = {
        if(value.toLowerCase().contains("exception")){
          counter.add(1)

        }
        value
      }
    }).setParallelism(4).writeAsText("c:\\t4")

    val job=env.execute()
    //获取累加器,并打印累加器的值
    val a=job.getAccumulatorResult[Long]("my-accumulator")
    println(a)
  }
}

3.3、分布式缓存DistributedCache

Flink提供了一个分布式缓存,类似于hadoop,可以使用户在并行函数中很方便的读取本地文件
此缓存的工作机制如下:程序注册一个文件或者目录(本地或者远程文件系统,例如hdfs或者s3),通过ExecutionEnvironment注册缓存文件并为它起一个名称。当程序执行,Flink自动将文件或者目录复制到所有taskmanager节点的本地文件系统,用户可以通过这个指定的名称查找文件或者目录,然后从taskmanager节点的本地文件系统访问它
用法:
1:注册一个文件

env.registerCachedFile("hdfs:///path/to/your/file", "hdfsFile")  

2:访问数据

File myFile = getRuntimeContext().getDistributedCache().getFile("hdfsFile");

代码实现:

import org.apache.commons.io.FileUtils
import org.apache.flink.api.common.functions.RichMapFunction
import org.apache.flink.api.scala.ExecutionEnvironment
import org.apache.flink.configuration.Configuration

object FlinkDistributedCache {
  def main(args: Array[String]): Unit = {
    //将缓存文件,拿到每台服务器的本地磁盘进行存储,然后需要获取的时候,直接从本地磁盘文件进行获取
    val env = ExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.api.scala._
    //1:注册分布式缓存文件
    env.registerCachedFile("D:\\开课吧课程资料\\Flink实时数仓\\advert.csv","advert")
    val data = env.fromElements("hello","flink","spark","dataset")
    val result = data.map(new RichMapFunction[String,String] {

      override def open(parameters: Configuration): Unit = {
        super.open(parameters)
        val myFile = getRuntimeContext.getDistributedCache.getFile("advert")
        val lines = FileUtils.readLines(myFile)
        val it = lines.iterator()
        while (it.hasNext){
          val line = it.next();
          println("line:"+line)
        }
      }
      override def map(value: String) = {
        value
      }
    }).setParallelism(2)
    result.print()
    env.execute()
  }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值