Flink - 尚硅谷- 大数据高级 Flink 技术精讲 - 2

注:次文档参考 【尚硅谷】大数据高级 flink技术精讲(2020年6月) 编写。

1.由于视频中并未涉及到具体搭建流程,Flink 环境搭建部分并未编写。
2.视频教程 Flink 版本为 1.10.0,此文档根据 Flink v1.11.1 进行部分修改。
3.文档中大部分程序在 Windows 端运行会有超时异常,需要打包后在 Linux 端运行。
4.程序运行需要的部分 Jar 包,请很具情况去掉 pom 中的 “scope” 标签的再进行打包,才能在集群上运行。
5.原始文档在 Markdown 中编写,此处目录无法直接跳转。且因字数限制,分多篇发布
此文档仅用作个人学习,请勿用于商业获利。

七、Flink 时间语义与 Watermark

7.1 Flink 中的时间语义

  • Event Time : 事件产生的时间
  • Ingestion Time : 数据到达 Flink 的时间
  • Processing Time : 执行操作算子的本地系统事件,与机器相关

根据哪种时间进行计算要根据不同的计算需求,
比如 星球大战系列电影,前传的上映时间要晚于前三部,对于观影来说更希望按照故事发生先后顺序看。但对于统计票房来说是按照上映时间统计。

7.2 设置 Event Time

    val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    // 从调用时刻开始给 env 创建的每一个 stream 追加时间特征
    environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    // alternatively:
    // environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    // environment.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime)
    // environment.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime)

    val sourceStream: DataStream[String] = environment.socketTextStream("localhost", 7777)

    // Transform
    val sourceDataStream: DataStream[SensorReading] = sourceStream
      .map((data: String) => {
        val dataArray: Array[String] = data.split(",")
        SensorReading(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
      }).assignAscendingTimestamps(_.timestamp * 1000L)

7.3 水位线 - Watermark

7.3.1 基本概念

当 Flink 以 EventTime 模式处理数据流时,他会根据数据里的时间戳来处理基于时间的算子。但由于网络,分布式等原因,会导致乱序数据的产生。

对于乱序数据来说,遇到一个时间戳达到了窗口关闭时间,不应该立刻触发窗口计算,而是等待一段时间,等迟到的数据来了再关闭窗口。

WaterMark 定义

  • WaterMark 是一种衡量 EventTime 进展的机制,可以设定延迟触发窗口
  • WaterMark 是用于处理乱序事件的,而正确的处理乱序事件,通常用 WaterMark 机制结合 window 来实现
  • 数据流中的 WaterMark 用于表示 timestamp 小于 WaterMark 的数据都已经到达了,因此,window 的执行也是由 WaterMark 触发的
  • WaterMark 用来让程序自己平衡延迟和结果正确性

WaterMark 的大小,需要在延迟性 和 计算结果的准确性间衡量。

WaterMark 特点

  • WaterMark 是一条特殊的数据记录
  • WaterMark 必须单调递增,以确保任务的事件时间时钟在向前推进
  • WaterMark 与数据的时间戳相关
7.3.2 WaterMark 传递

多分区之间的 WaterMark 传递中,在每个分区中,会根据当前分区的上游分区个数,创建对应的 PartitionWaterMark。
每个 PartitionWaterMark 中记录了这个上游分区的 WaterMark,并根据上游发送的数据进行更新。
向下游广播的 WaterMark 是这个分区中所有 PartitionWaterMark 最小的那个。

WateMark 的传递

7.3.3 WaterMark 注意点
  • WaterMark 就是事件时间,表示事件的处理程度
  • WaterMark 主要用来处理乱序数据,一般就是直接定义一个延迟时间,延迟触发窗口操作
  • WaterMark 延迟时间的设置,一般要根据数据的乱序情况来定,通常设置成最大乱序程度
  • 关窗操作,必须是时间进展到窗口关闭时间,事件时间语义下就是 WaterMark 达到窗口关闭的时间
  • WaterMark 代表的含义是,之后就不会再来时间戳比 WaterMark 更新的数值小的数据
  • 如果有不同的上游分区,当前任务会对他们创建各自分区的 WaterMark,当前任务的 WaterMark 就是最小的那个

WaterMark 的设定

  • 如果 WaterMark 设置的延迟太久,收到结果的速度可能会很慢,解决办法是在 WaterMark 到达之前输出一个近似结果
  • 如果 WaterMark 到达的太早,则可能收到错误结果,不过 Flink 处理迟到数据的机制可以解决这个问题
    • .allowedLateness(Time.minutes(1)) // 允许窗口在输出结果后保留一段时间,后续到达的这个时间窗内的每条数据都会根据这个时间窗内上次的结果数据重新计算,并再次输出
    • .sideOutputLateData(new OutputTag[SensorReading](“late data”)) // 将迟到数据放到侧输出流

处理乱序数据的三重保证

  • WaterMark 设置延迟时间
  • window 的 allowedLateness 设置窗口允许处理迟到数据的时间
  • window 的 sideOutputLateData 可以将迟到的数据写入侧输出流

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

7.3.4 Watermark Demo

自定义一个周期性生成 WaterMark 的 Assigner

      // .assignTimestampsAndWatermarks(new MyPeriodicWaterMarkAssigner(5000L))
      .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(5)) {
        override def extractTimestamp(element: SensorReading): Long = element.timestamp * 1000L
      })

      
class MyPeriodicWaterMarkAssigner(lateness: Long) extends AssignerWithPeriodicWatermarks[SensorReading] {
  // 需要两个关键参数,延迟时间 和 当前所有数据中的最大时间戳
  //  val lateness: Long = 5000L
  var currentMaxTimestampMillis: Long = Long.MinValue + lateness
  var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")

  // 默认每隔 200ms 调用 getCurrentWatermark 生成 Watermark
  // 修改生成 Watermark 的时间间隔 environment.getConfig.setAutoWatermarkInterval(100)
  override def getCurrentWatermark: Watermark = new Watermark(currentMaxTimestampMillis - lateness)

  // 每条数据调用 extractTimestamp 生成 EventTime
  override def extractTimestamp(element: SensorReading, recordTimestamp: Long): Long = {
    currentMaxTimestampMillis = currentMaxTimestampMillis.max(element.timestamp * 1000L)
    printInfo(element: SensorReading)
    // 获取 event time
    element.timestamp * 1000L
  }

  def printInfo(element: SensorReading): Unit = {
    println("Key : [" + element.id +
      "], EventTime : [" + element.timestamp * 1000L + "|" + sdf.format(element.timestamp * 1000L) +
      "], CurrentMaxTimeMillis : [" + currentMaxTimestampMillis + "|" + sdf.format(currentMaxTimestampMillis) +
      "], Watermark : [" + this.getCurrentWatermark.getTimestamp + "|" + sdf.format(this.getCurrentWatermark.getTimestamp) +
      "]")
  }
}

自定义一个断点式生成 WaterMark 的 Assigner

      .assignTimestampsAndWatermarks(new MyPunctuatedWaterMarkAssigner)


// 每条数据都会触发下面两个操作,更新 WaterMark
class MyPunctuatedWaterMarkAssigner extends AssignerWithPunctuatedWatermarks[SensorReading] {
  val lateness: Long = 5000L

  override def checkAndGetNextWatermark(lastElement: SensorReading, extractedTimestamp: Long): Watermark = {
    if (lastElement.id == "sensor_1") new Watermark(extractedTimestamp - lateness) else null
  }

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

WatermarkStrategy

      .assignTimestampsAndWatermarks(
        WatermarkStrategy.forBoundedOutOfOrderness[SensorReading](Duration.ofSeconds(5))
          .withTimestampAssigner(new SerializableTimestampAssigner[SensorReading] {
            override def extractTimestamp(element: SensorReading, recordTimestamp: Long): Long = element.timestamp
          })
      )

Full Code

package com.mso.flink.stream.time

import java.text.SimpleDateFormat
import java.time.Duration

import org.apache.flink.api.common.eventtime._
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.{AssignerWithPeriodicWatermarks, AssignerWithPunctuatedWatermarks}
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.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

// 输入数据的样例类
case class SensorReading(id: String, timestamp: Long, temperature: Double)

object WaterMarkDemo {
  def main(args: Array[String]): Unit = {
    val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    // 从调用时刻开始给 env 创建的每一个 stream 追加时间特征
    environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    environment.getConfig.setAutoWatermarkInterval(100)

    val sourceStream: DataStream[String] = environment.socketTextStream("localhost", 7777)

    // Transform
    val waterMarkStream: DataStream[SensorReading] = sourceStream
      .map((data: String) => {
        val dataArray: Array[String] = data.split(",")
        SensorReading(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
      })
      .assignTimestampsAndWatermarks(
        WatermarkStrategy.forBoundedOutOfOrderness[SensorReading](Duration.ofSeconds(5))
          .withTimestampAssigner(new SerializableTimestampAssigner[SensorReading] {
            override def extractTimestamp(element: SensorReading, recordTimestamp: Long): Long = element.timestamp
          })
      )

    //      .assignTimestampsAndWatermarks(new MyPeriodicWaterMarkAssigner(5000L))
    //      .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(5)) {
    //        override def extractTimestamp(element: SensorReading): Long = element.timestamp * 1000L
    //      })
    //
    //      .assignTimestampsAndWatermarks(new MyPunctuatedWaterMarkAssigner)

    waterMarkStream.keyBy(data => data.id)
      // 使用滚动窗口,窗口大小为 10s
      .timeWindow(Time.seconds(10))
      .apply(new MyWindowFunction)
      .print("WaterMark demo")

    environment.execute()
  }
}

// 自定义一个周期性生成 WaterMark 的 Assigner
class MyPeriodicWaterMarkAssigner(lateness: Long) extends AssignerWithPeriodicWatermarks[SensorReading] {
  // 需要两个关键参数,延迟时间 和 当前所有数据中的最大时间戳
  //  val lateness: Long = 5000L
  var currentMaxTimestampMillis: Long = Long.MinValue + lateness
  var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")

  // 默认每隔 200ms 调用 getCurrentWatermark 生成 Watermark
  // 修改生成 Watermark 的时间间隔 environment.getConfig.setAutoWatermarkInterval(100)
  override def getCurrentWatermark: Watermark = new Watermark(currentMaxTimestampMillis - lateness)

  // 每条数据调用 extractTimestamp 生成 EventTime
  override def extractTimestamp(element: SensorReading, recordTimestamp: Long): Long = {
    cur
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值