Flink watermark+window 处理数据乱序、迟到问题

        在流计算中,我们一般会选择 Storm、Spark、Flink 等计算框架,而对消息队列的选则 一般是Rabbitmq、Kafka 等,本片文章我们主要介绍 Kafka + Flink 框架在流计算中所遇到的问题及解决方案。

        先聊下一个很古老的问题,Flink 消费Kafka中的数据,Kafka 有好几个分区,而如何保证Flink按顺序消费呢?(说详细一点儿:假设 Kafka topic 的分区为 8 个,生产者向Kakfa 发送数据,这些数据会均匀分布在这 8个分区中,而Flink消费Kafka中的数据时,如何保证消费的顺序就是按照生产者生产的顺序进行消费呢?),这个问题我们很久之前就在讨论,最终的结论是无法在消费者端保证顺序消费(这里Kafka的分区不为1),但我们可以在Kafka的生产端实现让同一特征数据分布在同一个分区里面,从而保证在消费端对这一特征数据顺序消费。而现在我们讨论一下如何在消费端尽量的保证顺序消费

思路如下:

        在Spark 、Flink 等计算框架中的窗口函数可以很好的实现这一问题(但只是让数据更加精确一点儿),我们利用Flink 的窗口函数,让一段时间内的数据落在同一窗口内,然后对这个窗口内的数据按照时间排序,然后在进行计算,但是在实际生产中我们又会遇到迟到的数据,此时我们的窗口时间又不能太长,而对于迟到的数据该如何监控、我们又该如何解决这些迟到的数据呢 ?在 Flink 计算框架中 解决了这一问题,现在我们来看下具体的代码实现 (若有更好的解决方案,希望读者能够留言探讨)。

举一个我在生产中的一个简单的案例:

/**
    * @Description 业务逻辑处理
    * @param stream
    */
  private def businessLogic(stream: DataStream[String]): Unit = {
    val dataStream = stream.filter(_.nonEmpty).map(line => {
      val record = JSON.parseObject(line)
      val time = record.getString("query.$time")
      val preEvent = record.getString("query.$event")
      val preIp = record.getString("clientIP")
      val preProject = record.getString("query.$project")
      (time, preEvent, preIp, preProject)
    })
    val watermarkStream = dataStream.assignTimestampsAndWatermarks(new MyTimestampsAndWatermarks)
    val outPutTag = new OutputTag[Tuple4[String,String,String,String]]("late-data") {}
    watermarkStream.keyBy(_._3)
      .timeWindow(Time.seconds(3))
      .allowedLateness(Time.seconds(2)) //允许数据迟到2秒
      .sideOutputLateData(outPutTag)
      .trigger(EventTimeTrigger.create())
      .process(new MyProcessWindowFunction)
  }

     这里的 MyTimestampsAndWatermarks  (   dataStream.assignTimestampsAndWatermarks(new MyTimestampsAndWatermarks)    ) 需要自己实现  AssignerWithPeriodicWatermarks 接口,具体代码如下:

  /**
    * 定义 MyTimestampsAndWatermarks
    */
  case class MyTimestampsAndWatermarks() extends AssignerWithPeriodicWatermarks[(String, String, String, String)] {
    var currentMaxTimestamp: Long = _
    // 最大允许的乱序时间是10s
    var maxOutOfOrderness = 10000L

    /** 定义生成watermark的逻辑 */
    override def getCurrentWatermark: Watermark = {
      new Watermark(currentMaxTimestamp - maxOutOfOrderness)
    }

    /** 定义如何提取timestamp */
    override def extractTimestamp(element: (String, String, String, String), previousElementTimestamp: Long): Long = {
      //TODO timeStamp 应该取当前系统时间,这里做测试
      val timeStamp = element._1.toLong
      currentMaxTimestamp = Math.max(timeStamp, currentMaxTimestamp)
      timeStamp
    }
  }
这里的  MyProcessWindowFunction 需要自己实现 ProcessWindowFunction 抽象类,具体代码如下:
  /**
    * 定义 MyProcessWindowFunction
    */
  case class MyProcessWindowFunction() extends ProcessWindowFunction[(String, String, String, String), (String, String, String, String), String, TimeWindow] {
    override def process(key: String, context: Context, elements: Iterable[(String, String, String, String)],
                         out: Collector[(String, String, String, String)]): Unit = {
      val keyStr = key.toString
      val arrBuf = ArrayBuffer[Long]()
      val mapValue = new mutable.HashMap[ String, mutable.HashMap[String, String]]()
      val arr = arrBuf.toArray
      //对窗口内的数据按照时间进行排序
      Sorting.quickSort(arr)
      //组装结果
      elements.map(line => {
        val map = new mutable.HashMap[String,String]()
        val timeStr = line._1
        arrBuf.append(timeStr.toLong)
        map.put("preEvent",line._2)
        map.put("preIp",line._3)
        map.put("preProject",line._3)
        mapValue.put(timeStr,map)
      })
      val arrayLength = arr.length
      for (i <- 0 until arrayLength ){
        val time = arr(i).toString
        val listValue = mapValue.get(time).get
        val preEvent = listValue.get("preEvent").get
        val preIp = listValue.get("preIp").get
        val preProject = listValue.get("preProject").get
        val res = (time,preEvent,preIp,preProject)
        //将组装好的结果输出
        out.collect(res)
      }
    }
  }

在窗口内我们实现了对数据的排序,当窗口触发时将窗口内的数据输出 ,从而保证顺序消费计算。

而对于迟到的数据,我们之前定义了  outPutTag  ( val outPutTag = new OutputTag[Tuple4[String,String,String,String]]("late-data") {} ),现在我们获取到迟到的数据,

//获取迟到太久的数据
    val sideOutput = watermarkStream.getSideOutput[Tuple4[String, String, String,String]](outPutTag)

而对于迟到的数据该如何处理,这里就不再讨论,根据自己的业务去处理,可进一步研究自己的数据为何迟到,该如何去解决。

对于这一问题,就介绍到这里,如有不合适的地方希望读者能够积极留言讨论,若转载请说明出处, 谢谢 !

 

你可能感兴趣的:(flink)