4.Flink水印机制测试及状态介绍

1. 概览

  • DataStream基本转换

  • 迭代:

    package com.xiaofan
    
    
    import org.apache.flink.streaming.api.scala._
    
    /**
     * @author xiaofan
     */
    object IterateDemo {
    
    
      def main(args: Array[String]): Unit = {
    
        val env = StreamExecutionEnvironment.getExecutionEnvironment
    
        val someIntegers: DataStream[Long] = env.generateSequence(0, 10)
        // 返回11个结果
        val iteratedStream: DataStream[Long] = someIntegers.iterate(
          iteration => {
            // 相当于循环体
            val minusOne = iteration.map(v => v - 2)
            // 相当于if
            val stillGreaterThanZero = minusOne.filter(_ > 0)
            // 相当于else
            val lessThanZero = minusOne.filter(_ <= 0)
            // 返回的结果(固定写法)
            (stillGreaterThanZero, lessThanZero)
          }
        )
        iteratedStream.print
    
        env.execute("DataStream API")
    
      }
    }
    
    
    

    在这里插入图片描述

  • 迭代器数据接收器

    • Flink还提供了一个接收器来收集数据流结果,以便进行测试和调试`

      package com.xiaofan
      
      
      import org.apache.flink.streaming.api.datastream.DataStreamUtils
      import org.apache.flink.streaming.api.scala._
      /**
       * @author xiaofan
       */
      object DataSinkTest{
      
      
        def main(args: Array[String]): Unit = {
          import scala.collection.JavaConverters.asScalaIteratorConverter
      
          val env = StreamExecutionEnvironment.getExecutionEnvironment
          val value: DataStream[(String, Int)] = env.fromElements(("a", 1), ("b", 2), ("c", 3))
          val result: Iterator[(String, Int)] = DataStreamUtils.collect(value.javaStream).asScala
          result.foreach(print)
      
        }
      }
      
      
      

      在这里插入图片描述

2. Event Time and Watermarks

在这里插入图片描述

2.1. EventTime时间模型

2.2. 水印测试

  • 测试数据

    spark:1559223685000
    flink:1559223686000
    scala:1559223687000
    python:1559223688000
    java:1559223689000
    kafka:1559223690000
    storm:1559223691000
    hadoop:1559223692000
    spark:1559223693000
    flink:1559223694000
    azkaban:1559223695000
    hbase:1559223696000
    hive:1559223697000
    flume:1559223698000
    
  • 源代码

    package com.xiaofan
    
    
    import org.apache.commons.lang3.time.FastDateFormat
    import org.apache.flink.api.java.tuple.Tuple
    import org.apache.flink.streaming.api.TimeCharacteristic
    import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks
    import org.apache.flink.streaming.api.scala._
    import org.apache.flink.streaming.api.scala.function.WindowFunction
    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
    
    /**
     * 水印测试
     * 水印的目的: 处理乱序的数据问题 需要结合window来处理
     *
     * window触发的条件:
     *  1.window中必须要有数据
     *  2.eventTime = window结束时间 + waterMark  (存在一条数据的事件时间大于等于 window结束时间 + waterMark)
     *
     * @author xiaofan
     */
    object WaterMarkTest01 {
      // 线程安全的时间格式化对象
      val sdf: FastDateFormat = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss:SSS")
      val hostName = "192.168.1.27"
      val port = 9999
    
      def main(args: Array[String]): Unit = {
    
        val env = StreamExecutionEnvironment.getExecutionEnvironment
        // 将EventTime设置为流数据时间类型
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
        env.setParallelism(1)
        val streams: DataStream[String] = env.socketTextStream(WaterMarkTest01.hostName, WaterMarkTest01.port)
    
        // 输入数据格式: name:时间戳
        // spark:1559223685000
        val data: DataStream[(String, Long)] = streams.map(data => {
          try {
            val items: Array[String] = data.split(":")
            (items(0), items(1).toLong)
          } catch {
            case _: Exception => println("Invalid Input:" + data)
              ("0", 0L)
          }
        }).filter(data => {
          !data._1.equals("0") && data._2 != 0L
        })
    
        // 为数据流中的元素分配时间戳, 并定期创建watermark以监控事件时间的进度
        val waterStream: DataStream[(String, Long)] = data.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks[(String, Long)] {
          // 事件时间
          var currentMaxTimestamp = 0L
          var maxOutOfOrderDelay = 3000L
          var lastEmittedWatermark: Long = Long.MinValue
    
          override def getCurrentWatermark: Watermark = {
            // 允许延迟3秒
            val potentialWatermark = currentMaxTimestamp - maxOutOfOrderDelay
            // 保证水印能依次递增
            if (potentialWatermark >= lastEmittedWatermark) {
              lastEmittedWatermark = potentialWatermark
            }
            new Watermark(lastEmittedWatermark)
          }
    
          override def extractTimestamp(element: (String, Long), previousElementTimestamp: Long): Long = {
            val time = element._2
            if (time > currentMaxTimestamp) {
              currentMaxTimestamp = time
            }
            val outData: String = String.format("key: %s    EventTime: %s    waterMark:  %s", element._1, sdf.format(time), sdf.format(getCurrentWatermark.getTimestamp))
            println(outData)
            time
          }
        })
    
        val result: DataStream[String] = waterStream
          .keyBy(0)
          .window(TumblingEventTimeWindows.of(Time.seconds(5L)))
          .apply(new WindowFunction[(String, Long), String, Tuple, TimeWindow] {
            override def apply(key: Tuple, window: TimeWindow, input: Iterable[(String, Long)], out: Collector[String]): Unit = {
              val timeArr = ArrayBuffer[String]()
              val iterator = input.iterator
    
              while (iterator.hasNext) {
                val tup2 = iterator.next()
                timeArr.append(sdf.format(tup2._2))
              }
    
              val outData: String = String.format("key: %s    data: %s    startTime:  %s    endTime:  %s",
                key.toString,
                timeArr.mkString("---"),
                sdf.format(window.getStart),
                sdf.format(window.getEnd))
    
              out.collect(outData)
            }
          })
    
          result.print("window compute result: ")
          env.execute(this.getClass.getName)
    
      }
    }
    
  • 数据有序到达的运行效果
    在这里插入图片描述
    在这里插入图片描述

  • ·模拟数据迟到的运行效果·
    在这里插入图片描述
    在这里插入图片描述

  • 注意

    • 处理乱序的数据问题 需要结合window来处理
    • window触发的条件
      • window中必须要有数据
      • eventTime = window结束时间 + waterMark (存在一条数据的事件时间大于等于 window结束时间 + waterMark)
    • 当时间戳为T的水印出现时,表示事件时间t <= T的数据都已经到达,即水印后面应该只能流入事件时间t > T的数据。也就是说,水印是Flink判断迟到数据的标准,同时也是窗口触发的标记。
    • 随着新数据的到来,waterMark的值会更新为最新数据事件时间-允许乱序时间值,但是如果这时候来了一条历史数据,waterMark值则不会更新。总的来说,waterMark是为了能接收到尽可能多的乱序数据。

2.3. 延迟测试

  • flink对于迟到数据的处理:

    • 如果业务上对迟到的数据有需求,则可以设置允许迟到的时间,那么每个窗口可以在允许等待范围内继续等待迟到的数据
    • 只要当前的watermark时间 < window结束时间+allowedLateness
    • 那么就算该窗口已经进行过计算,再次接收到属于该窗口的数据,仍会继续参与窗口计算。
  • 源代码

    package com.xiaofan
    
    import org.apache.commons.lang3.time.FastDateFormat
    import org.apache.flink.api.java.tuple.Tuple
    import org.apache.flink.streaming.api.TimeCharacteristic
    import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks
    import org.apache.flink.streaming.api.scala._
    import org.apache.flink.streaming.api.scala.function.WindowFunction
    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
    
    /**
     * 延迟测试
     * flink对于迟到数据的处理:
     * 如果业务上对迟到的数据有需求,则可以设置允许迟到的时间,那么每个窗口可以在允许等待范围内继续等待迟到的数据
     * 只要当前的 watermark时间 < window结束时间 + allowedLateness
     * 那么就算该窗口已经进行过计算,再次接收到属于该窗口的数据,仍会继续参与窗口计算。
     *
     * @author xiaofan
     */
    object WaterMarkTest02 {
      // 线程安全的时间格式化对象
      val sdf: FastDateFormat = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss:SSS")
      val hostName = "192.168.1.27"
      val port = 9999
    
      def main(args: Array[String]): Unit = {
    
        val env = StreamExecutionEnvironment.getExecutionEnvironment
        // 将EventTime设置为流数据时间类型
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
        env.setParallelism(1)
        val streams: DataStream[String] = env.socketTextStream(WaterMarkTest01.hostName, WaterMarkTest01.port)
    
        // 输入数据格式: name:时间戳
        // spark:1559223685000
        val data: DataStream[(String, Long)] = streams.map(data => {
          try {
            val items: Array[String] = data.split(":")
            (items(0), items(1).toLong)
          } catch {
            case _: Exception => println("Invalid Input:" + data)
              ("0", 0L)
          }
        }).filter(data => {
          !data._1.equals("0") && data._2 != 0L
        })
    
        // 为数据流中的元素分配时间戳, 并定期创建watermark以监控事件时间的进度
        val waterStream: DataStream[(String, Long)] = data.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks[(String, Long)] {
          // 事件时间
          var currentMaxTimestamp = 0L
          var maxOutOfOrderDelay = 3000L
          var lastEmittedWatermark: Long = Long.MinValue
    
          override def getCurrentWatermark: Watermark = {
            // 允许延迟3秒
            val potentialWatermark = currentMaxTimestamp - maxOutOfOrderDelay
            // 保证水印能依次递增
            if (potentialWatermark >= lastEmittedWatermark) {
              lastEmittedWatermark = potentialWatermark
            }
            new Watermark(lastEmittedWatermark)
          }
    
          override def extractTimestamp(element: (String, Long), previousElementTimestamp: Long): Long = {
            val time = element._2
            if (time > currentMaxTimestamp) {
              currentMaxTimestamp = time
            }
            val outData: String = String.format("key: %s    EventTime: %s    waterMark:  %s", element._1, sdf.format(time), sdf.format(getCurrentWatermark.getTimestamp))
            println(outData)
            time
          }
        })
    
        // 迟到的数据
        val lateData = new OutputTag[(String, Long)]("late")
    
        val result: DataStream[String] = waterStream
          .keyBy(0)
          .window(TumblingEventTimeWindows.of(Time.seconds(5L)))
          .allowedLateness(Time.seconds(2L))  // 允许数据迟到的时间 (注意:该方法只针对于基于event-time的窗口,如果是基于processing-time,并且指定了非零的time值则会抛出异常)
          .sideOutputLateData(lateData)   // 将迟来的数据保存至给定的outputTag参数,而OutputTag则是用来标记延迟数据的一个对象
          .apply(new WindowFunction[(String, Long), String, Tuple, TimeWindow] {
            override def apply(key: Tuple, window: TimeWindow, input: Iterable[(String, Long)], out: Collector[String]): Unit = {
              val timeArr = ArrayBuffer[String]()
              val iterator = input.iterator
    
              while (iterator.hasNext) {
                val tup2 = iterator.next()
                timeArr.append(sdf.format(tup2._2))
              }
    
              val outData: String = String.format("key: %s    data: %s    startTime:  %s    endTime:  %s",
                key.toString,
                timeArr.mkString("---"),
                sdf.format(window.getStart),
                sdf.format(window.getEnd))
    
              out.collect(outData)
            }
          })
    
          result.print("window compute result: ")
    
          val late: DataStream[(String, Long)] = result.getSideOutput(lateData)   // 通过window等操作返回的DataStream调用该方法,传入标记延迟数据的对象来获取延迟的数据
          late.print("late data: ")
    
          env.execute(this.getClass.getName)
    
      }
    }
    
  • 运行效果
    在这里插入图片描述
    在这里插入图片描述

  • 总结

    • 如果延迟的数据有业务需要,则设置好允许延迟的时间,每个窗口都有属于自己的最大等待延迟数据的时间限制
    • 窗口结束时间+延迟时间=最大waterMark值
    • 即当waterMark值大于的上述计算出的最大waterMark值,该窗口内的数据就属于迟到的数据,无法参与window计算

2.4. 再谈Flink事件时间、水印和迟到数据处理

2.5. Generating Timestamps / Watermarks

  • 有两种方法可以分配时间戳生成水印
    • 直接在数据流源中

      • sources流可以直接为它们生成的元素分配时间戳,还可以发出水印,这种应用场景下的程序是不需要时间戳分配器的。注意,如果使用时间戳分配器,则会覆盖sources提供的时间戳和水印。

      • 要直接为源source中的元素分配时间戳,source必须使用SourceContext类中collectWithTimestamp(...) 方法。要生成水印,Source必须调用emitWatermark(Watermark)函数。

    • 时间戳赋值器/水印生成器:在Flink中,时间戳赋值器还定义要发出的水印

      • With Periodic Watermarks(周期水印)
        AssignerWithPeriodicWatermarks分配时间戳并且周期性的生成水印(可能取决于流元素,或者纯粹基于处理时间)。
      • With Punctuated Watermarks(打点水印)
        当使用一个特定的事件指出需要生成一个新的水印时,可以通过使用AssignerWithPunctuatedWatermarks类来实现。在这种情况下,Flink首先会调用extractTimestamp(…)方法来给元素分配一个时间戳,然后立即在该元素上执行checkAndGetNextWatermark(…)方法生产水印。
    • 常用的两种自定义事件时间戳分配器和水印发射器的对比

      • AssignerWithPeriodicWatermarks更常用,而且当数据量很大时,效率更高
      • AssignerWithPunctuatedWatermarks一般适合TPS小于100的场景。
    • Timestamps per Kafka Partition
      在这里插入图片描述
      在这里插入图片描述

  • 总结
    • 如果使用了时间戳分配器,则数据源中的任何时间戳和水印都将被覆盖
    • 时间戳必须在第一个窗口算子之前指定
    • 作为一种特殊情况,当使用Kafka作为数据源时,Flink允许在kafka源(或消费者)内部指定一个时间戳分配器/水印发射器
  • 详细内容

3. 状态与容错

3.1. Working with State

  • Keyed State 与 Operator State

  • Raw State 与 Managed State

    • Raw State 则保存在算子自己的数据结构中

      package com.xiaofan.state
      
      import org.apache.flink.streaming.api.scala.{StreamExecutionEnvironment, _}
      
      /**
       *  mapWithState 方法的传参形式
       *
       *  func: (T, Option[S]) => (R, Option[S])
       *  func: (输入类型, Option[状态类型]) => (返回类型, Option[状态类型])
       *
       *  @author xiaofan
       */
      object State {
        def main(args: Array[String]): Unit = {
          val env = StreamExecutionEnvironment.getExecutionEnvironment
      
          val dataStream = env.fromElements(("one", 1), ("two", 2), ("one", 1), ("two", 3), ("three", 2))
      
          dataStream.keyBy(_._1).mapWithState(func).print()
      
          env.execute(this.getClass.getName)
      
          // 根据输入,将第二个字段进行累加求和(区分第一次求和与非第一次求和)
          def func(in: (String, Int), count: Option[Int]): ((String, Int), Option[Int]) = count match {
            case Some(c) => ((in._1, c + in._2), Some(c + in._2))
            case None => ((in._1, in._2), Some(in._2))
          }
      
        }
      
      }
      
      
    • Managed State 由 Flink 运行时控制的数据结构表示,比如内部的 hash table 或者 RocksDB。 比如 “ValueState”, “ListState” 等

  • 使用 Managed Keyed State

    package com.xiaofan.state
    
    import org.apache.flink.api.common.functions.RichFlatMapFunction
    import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
    import org.apache.flink.configuration.Configuration
    import org.apache.flink.streaming.api.scala.{StreamExecutionEnvironment, _}
    import org.apache.flink.util.Collector
    
    class CountWindowAverage extends RichFlatMapFunction[(Long, Long), (Long, Long)] {
    
      private var sum: ValueState[(Long, Long)] = _
    
      override def flatMap(input: (Long, Long), out: Collector[(Long, Long)]): Unit = {
    
        // access the state value
        val tmpCurrentSum = sum.value
    
        println("tmpCurrentSum: " + tmpCurrentSum + "\tinput: " + input)
    
        // If it hasn't been used before, it will be null
        val currentSum = if (tmpCurrentSum != null) {
          tmpCurrentSum
        } else {
          (0L, 0L)
        }
    
        // update the count
        val newSum = (currentSum._1 + 1, currentSum._2 + input._2)
    
        // update the state
        sum.update(newSum)
    
        // if the count reaches 2, emit the average and clear the state
        if (newSum._1 >= 2) {
          out.collect((input._1, newSum._2 / newSum._1))
          sum.clear()
        }
      }
    
      override def open(parameters: Configuration): Unit = {
        sum = getRuntimeContext.getState(
          new ValueStateDescriptor[(Long, Long)]("average", createTypeInformation[(Long, Long)])
        )
      }
    }
    
    
    object ExampleCountWindowAverage extends App {
      val env = StreamExecutionEnvironment.getExecutionEnvironment
    
      env.fromCollection(List(
        (1L, 3L),
        (1L, 5L),
        (2L, 7L),
        (2L, 4L),
        (1L, 2L)
      )).keyBy(_._1)
        .flatMap(new CountWindowAverage())
        .print()
      // the printed output will be (1,4) and (1,5)
    
      env.execute("ExampleManagedState")
    }
    
    
  • 状态有效期TTL

    package com.xiaofan.state
    
    import org.apache.flink.api.common.state.{StateTtlConfig, ValueStateDescriptor}
    import org.apache.flink.streaming.api.windowing.time.Time
    
    /**
     * 状态有效期
     * @author xiaofan
     */
    object TtlFunc {
      def main(args: Array[String]): Unit = {
        val ttlConfig: StateTtlConfig = StateTtlConfig
          // 生存时间
          .newBuilder(Time.seconds(1))
          .setUpdateType(StateTtlConfig.StateVisibility.NeverReturnExpired)
          .build()
    
        val stateDescriptor = new ValueStateDescriptor[String]("text state", classOf[String])
        stateDescriptor.enableTimeToLive(ttlConfig)
      }
    }
    
  • 使用 Managed Operator State

4. 算子

4.1. 窗口

基于时间的窗口有一个开始时间戳(包含)和一个结束时间戳(排除),它们一起描述窗口的大小。

  • 翻转窗口 (Tumbling Windows)
  • 滑动窗口 (Sliding Windows)
  • 会话窗口 (Session Windows)
  • 由于会话窗口没有固定的开始和结束,因此它们的计算方式与翻滚和滑动窗口不同。在内部,会话窗口操作符为每个到达的记录创建一个新窗口,如果窗口之间的距离比定义的间隔更近,则将它们合并在一起。
  • 为了可以合并,会话窗口操作符需要一个合并触发器和一个合并窗口函数,例如ReduceFunctionAggregateFunctionProcessWindowFunction(FoldFunction不能合并)
  • 全局窗口 (Global Windows)

4.2. Joining

  • Tumbling Window Join
  • Sliding Window Join
  • Session Window Join
  • Interval Join

4.3. 用于外部数据访问的异步 I/O

5. 寄语:效苏秦之刺股折桂还需苦战;学陶潜之惜时付出必有回报

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值