文章目录

  • 一、时间语义
  • 1.1、设置时间语义
  • 二、WaterMark水位线
  • 2.1、Watermark 原理
  • 2.1.1、本来有序的Stream中的Watermark
  • 2.1.2、乱序事件中的Watermark
  • 2.1.3、并行数据流中的Watermark
  • 2.2、引入 Watermark 和 EventTime
  • 2.2.1、有序数据流中引入 Watermark 和 EventTime
  • 2.2.1.1、案例:每隔5秒统计一下最近10秒内,每个基站中通话时间最长的一次通话发生的时间还有,主叫号码,被叫号码,通话时长,并且还得告诉我们当前发生的时间范围(10秒)
  • 2.2.2、乱序序数据流中引入 Watermark 和 EventTime
  • 2.2.2.1、With Periodic(周期性的) Watermark
  • 第一种方法
  • 第二种方法
  • 2.2.2.2、With Punctuated(间断性的) Watermark
  • 2.2.2.3、案例: 每隔5秒统计一下最近10秒内,每个基站中通话时间最长的一次通话发生的时间还有主叫号码,被叫号码,通话时长,并且还得告诉我们当前发生的时间范围(10秒)
  • 2.3、Window 的allowedLateness
  • 返回总目录


一、时间语义

  对于流式数据处理,最大的特点是数据上具有时间的属性特征,Flimk 根据时间产生的位置不同,将时间分为三种时间语义:

  • Event Time:事件产生的时间,它通常由事件中的时间戳描述。
  • 数据从终端产生,或者从系统中产生的过程中生成的时间为事件生成时间
  • Ingestion Time:事件进入 Flink 的时间。
  • 当数据经过消息中间件传入到 Flink 系统中,在DataSource中接入的时候会生成事件接入时间
  • Processing Time:事件被处理时当前系统的时间。
  • 当数据在 Flink 系统中通过各个算子实例执行转换操作的过程中,算子实例所在系统的时间为数据处理时间。

Flink 已经支持这三种类型时间概念,用户能够根据需要选择时间类型作为对流式数据的依据,这种情况极大地增强了对事件数据处理的灵活性和准确性。

参考官方文档

1.1、设置时间语义

  在 Flink 中默认情况下使用是 Process Time 时间语义,如果用户选择使用 Event Time或者 Ingestion Time 语 义 , 则 需 要 在 创 建 的 StreamExecutionEnvironment 中 调 用setStreamTimeCharacteristic() 方 法 设 定 系 统 的 时 间 概 念 , 如 下 代 码 使 用TimeCharacteristic.EventTime 作为系统的时间语义:

//设置使用EventTime
streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

//设置使用IngestionTime
streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime)

注意:但是上面的代码还没有指定具体的时间到底是什么值,所以后面还有代码需要设置!

二、WaterMark水位线

  在使用EventTime处理Stream 数据的时候会遇到数据乱序的问题,流处理从Event(事件)产生,流经Source,再到Operator,这中间需要一定的时间。虽然大部分情况下,传输到 Operator 的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络延迟等原因而导致乱序的产生,特别是使用 Kafka 的时候,多个分区之间的数据无法保证有序。因此,在进行Window计算的时候,不能无限期地等下去,必须要有个机制来保证在特定的时间后,必须触发 Window 进行计算,这个特别的机制就是 Watermark(水位线)。Watermark 是用于处理乱序事件的。

2.1、Watermark 原理

  在 Flink 的窗口处理过程中,如果确定全部数据到达,就可以对 Window 的所有数据做窗口计算操作(如汇总、分组等),如果数据没有全部到达,则继续等待该窗口中的数据全部到达才开始处理。这种情况下就需要用到水位线(WaterMarks)机制,它能够衡量数据处理进度(表达数据到达的完整性),保证事件数据(全部)到达 Flink 系统,或者在乱序及延迟到达时,也能够像预期一样计算出正确并且连续的结果。当任何 Event 进入到 Flink系统时,会根据当前最大事件时间产生 Watermarks 时间戳。

Flink 是怎么计算 Watermak 的值呢?

  • Watermark = 进入 Flink 的最大的事件时间(mxtEventTime)— 指定的延迟时间(t)

有 Watermark 的 Window 是怎么触发窗口函数的呢?

  • 如果有窗口的停止时间等于或者小于 maxEventTime – t(当时的 warkmark),那么这个窗口被触发执行。

注意:Watermark 本质可以理解成一个延迟触发机制。
例如: 是一个5秒的滚动窗口, 延时3秒

  • 第一条进入4, watermark=4-3=1 < 5, 不触发
  • 第二条进入6, watermark=6-3=3 < 5, 不触发
  • 第三条进入8, watermark =8-3=5 = 5, 触发窗口执行, 注意: watermark只是让窗口延时触发,并不会将其他窗口数据计算,如本例中,只会计算4的这条数据,其他数据保存在内存中
  • 第四条进入5, watermark=8-3=5=5, 但是注意0~5秒的窗口已经触发了, 所以这条数据丢失
  • 这个地方,我们可以通过测流计算超时过长的数据,避免数据的丢失

Watermark 的使用存在三种情况:

2.1.1、本来有序的Stream中的Watermark

  如果数据元素的事件时间是有序的,Watermark时间戳会随着数据元素的事件时间按顺序生成,此时水位线的变化和事件时间保持一直(因为既然是有序的时间,就不需要设置延迟了,那么 t 就是 0。所以 watermark=maxtime-0 = maxtime),也就是理想状态下的水位线。当 Watermark 时间大于 Windows 结束时间就会触发对 Windows 的数据计算,以此类推,下一个 Window 也是一样。

flink DATEDIFF 时间函数 flink三种时间_Time_02

2.1.2、乱序事件中的Watermark

  现实情况下数据元素往往并不是按照其产生顺序接入到Flink系统中进行处理,而频繁出现乱序或迟到的情况,这种情况就需要使用Watermarks来应对。比如下图,设置延迟时间 t 为 2

flink DATEDIFF 时间函数 flink三种时间_数据_03

2.1.3、并行数据流中的Watermark

  在多并行度的情况下,Watermark会有一个对齐机制,这个对齐机制会取所有Channel中最小的Watermark。

flink DATEDIFF 时间函数 flink三种时间_Time_04

2.2、引入 Watermark 和 EventTime

2.2.1、有序数据流中引入 Watermark 和 EventTime

  对于有序的数据,代码比较简洁,主要需要从源 Event 中抽取 EventTime。

//设置时间语义
        streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

        //读取数据源
        val stream: DataStream[StationLog] = streamEnv.socketTextStream("10.0.0.201", 8888)
            .map(line => {
                var arr = line.split(",")
                new StationLog(arr(0).trim, arr(1).trim, arr(2).trim, arr(3).trim, arr(4).trim.toLong, arr(5).trim.toLong)
            })
            //引入Watermark(数据是有序)
            .assignAscendingTimestamps(_.callTime) //参数中指定Eventtime具体的值是什么
2.2.1.1、案例:每隔5秒统计一下最近10秒内,每个基站中通话时间最长的一次通话发生的时间还有,主叫号码,被叫号码,通话时长,并且还得告诉我们当前发生的时间范围(10秒)
package com.chb.flink.time

import java.text.SimpleDateFormat
import java.util.Date

import com.chb.flink.source.{MyCustomerSource, StationLog}
import org.apache.flink.api.common.functions.ReduceFunction
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.api.scala.function.WindowFunction
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector

/**
 * 每隔5秒统计一下最近10秒内,每个基站中通话时间最长的一次通话发生的时间还有,
 * 主叫号码,被叫号码,通话时长,并且还得告诉我们当前发生的时间范围(10秒)
 */
object MaxLongCallTime {

    def main(args: Array[String]): Unit = {
        val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
        import org.apache.flink.streaming.api.scala._
        streamEnv.setParallelism(1)

        //设置时间语义
        streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

        //读取数据源
        //        val stream: DataStream[StationLog] = streamEnv.socketTextStream("10.0.0.201", 8888)
        //            .map(line => {
        //                var arr = line.split(",")
        //                new StationLog(arr(0).trim, arr(1).trim, arr(2).trim, arr(3).trim, arr(4).trim.toLong, arr(5).trim.toLong)
        //            })
        //            //引入Watermark(数据是有序)
        //            .assignAscendingTimestamps(_.callTime) //参数中指定EventTime具体的值是什么

        val stream = streamEnv.addSource(new MyCustomerSource)
            .assignAscendingTimestamps(_.callTime) // 引入WaterMark(数据时有序的), 参数指定EventTime的字段
        stream.filter(_.callType == "success").print()

        //分组、开窗
        stream.filter(_.callType.equals("success")).keyBy(_.sid)
            .timeWindow(Time.seconds(5), Time.seconds(3))
            .reduce(new MyReduceFunction(), new ReturnMaxTimeWindowFunction)
            .print()

        streamEnv.execute()

    }

    class MyReduceFunction() extends ReduceFunction[StationLog] { //增量聚合
        override def reduce(value1: StationLog, value2: StationLog): StationLog = {
            // 比较通话时长最长的
            if (value1.duration > value2.duration) value1 else value2
        }
    }

    class ReturnMaxTimeWindowFunction extends WindowFunction[StationLog, String, String, TimeWindow] {
        //在窗口触发的才调用一次
        override def apply(key: String, window: TimeWindow, input: Iterable[StationLog], out: Collector[String]): Unit = {
            var value = input.iterator.next()
            var sb = new StringBuilder
            val df = new SimpleDateFormat("HH:mm:ss")
            sb.append("窗口范围是:").append(df.format(new Date(window.getStart))).append("----").append(df.format(new Date(window.getEnd)))
            sb.append(", 基站id:").append(value.sid)
            sb.append(", 呼叫时间:").append(value.callTime)
                .append(", 主叫号码:").append(value.callOut)
                .append(", 被叫号码:").append(value.callIn)
                .append(", 通话时长:").append(value.duration)
                .append("\n")
            out.collect(sb.toString())
        }
    }

}

2.2.2、乱序序数据流中引入 Watermark 和 EventTime

对于乱序数据流,有两种常见的引入方法:周期性间断性

2.2.2.1、With Periodic(周期性的) Watermark

  周期性地生成Watermark的生成,默认是 100ms。每隔N毫秒自动向流里注入一个Watermark,时间间隔由 streamEnv.getConfig.setAutoWatermarkInterval()决定。最简单的写法如下

第一种方法
//设置时间语义
        streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
        streamEnv.getConfig.setAutoWatermarkInterval(100L) //周期引入Watermark的设置,默认就是100毫秒

            //引入Watermark(数据乱序的),并且通过观察延迟的时间是3秒,采用周期性的Watermark引入
            //代码有两种写法,
            //第一种:直接采用AssignerWithPeriodicWatermarks接口的实现类(Flink提供的)
          data.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[StationLog](Time.seconds(3)) {
                override def extractTimestamp(element: StationLog) = { //设置我们的EventTime
                    element.callTime //就是EventTime
                }
            })
第二种方法
//如果EventTime是乱序的,需要考虑一个延迟时间t
   //当前代码设置的延迟时间为3秒
   data.assignTimestampsAndWatermarks(new MyCustomerPeriodicWatermark(3000L)) //自定义延迟3秒
   
    class MyCustomerPeriodicWatermark(delay: Long) extends
        AssignerWithPeriodicWatermarks[StationLog]{
        var maxTime :Long=0
        override def getCurrentWatermark: Watermark = {
            new Watermark(maxTime-delay) //创建水位线
        }
        override def extractTimestamp(element: StationLog, previousElementTimestamp: Long): Long = {
            maxTime=maxTime.max(element.callTime) //maxtime永远是最大值
            element.callTime
        }
    }
2.2.2.2、With Punctuated(间断性的) Watermark

  间断性的生成Watermark一般是基于某些事件触发 Watermark 的生成和发送,比如:在我们的基站数据中,有一个基站的 CallTime 总是没有按照顺序传入,其他基站的时间都是正常的,那我们需要对这个基站来专门生成Watermark。

// 只有sid3的EventTime是无序的, 只需要对sid3处理
    class MyPunctuatedWaterMarks(delay: Long) extends AssignerWithPunctuatedWatermarks[StationLog] {
        var maxEventTime: Long = 0

        override def checkAndGetNextWatermark(t: StationLog, l: Long): Watermark = {
            if (t.sid.equals("sid3")) { // 只对sid3处理
                maxEventTime = maxEventTime.max(t.callTime)
                new Watermark(maxEventTime - delay)
            } else {
                null // 其他基站不返回watermark
            }
        }

        override def extractTimestamp(t: StationLog, l: Long): Long = t.callTime
    }


        // 接入数据源, 数据是乱序, 引入间断性的WaterMark, 此处只对sid3处理
        val data = streamEnv.addSource(new MyCustomerSource)
            .filter(_.callType == "success")
            .assignTimestampsAndWatermarks(new MyPunctuatedWaterMarks(3000L))
2.2.2.3、案例: 每隔5秒统计一下最近10秒内,每个基站中通话时间最长的一次通话发生的时间还有主叫号码,被叫号码,通话时长,并且还得告诉我们当前发生的时间范围(10秒)
package com.chb.flink.time

import com.chb.flink.source.StationLog
import com.chb.flink.time.MaxLongCallTime.ReturnMaxTimeWindowFunction
import org.apache.flink.api.common.functions.ReduceFunction
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
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

/**
 * 每隔5秒统计一下最近10秒内,每个基站中通话时间最长的一次通话发生的时间还有,
 * 主叫号码,被叫号码,通话时长,并且还得告诉我们当前发生的时间范围(10秒)
 */
object MaxLongCallTime2 {

    def main(args: Array[String]): Unit = {
        val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
        import org.apache.flink.streaming.api.scala._
        streamEnv.setParallelism(1)

        //设置时间语义
        streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
        streamEnv.getConfig.setAutoWatermarkInterval(100L) //周期引入Watermark的设置,默认就是100毫秒

        //读取数据源
        val stream: DataStream[StationLog] = streamEnv.socketTextStream("10.0.0.201", 8888)
            .map(line => {
                var arr = line.split(",")
                new StationLog(arr(0).trim, arr(1).trim, arr(2).trim, arr(3).trim, arr(4).trim.toLong, arr(5).trim.toLong)
            })
            //引入Watermark(数据乱序的),并且通过观察延迟的时间是3秒,采用周期性的Watermark引入
            //代码有两种写法,
            //            //第一种:直接采用AssignerWithPeriodicWatermarks接口的实现类(Flink提供的)
            //            .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[StationLog](Time.seconds(3)) {
            //                override def extractTimestamp(element: StationLog) = { //设置我们的EventTime
            //                    element.callTime //就是EventTime
            //                }
            //            })


            // 第二种:自己定义一个AssignerWithPeriodicWatermarks接口的实现类
            .assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks[StationLog] {
                var maxEventTime: Long = _

                override def getCurrentWatermark = { //周期性的生成Watermark
                    // Watermark = 进入 Flink 的最大的事件时间(mxtEventTime)— 指定的延迟时间(t)
                    new Watermark(maxEventTime - 3000L) // 创建水位线
                }

                //设定EventTime是哪个属性
                override def extractTimestamp(element: StationLog, previousElementTimestamp: Long) = {
                    maxEventTime = maxEventTime.max(element.callTime)
                    element.callTime
                }
            })

        //分组、开窗
        stream.filter(_.callType.equals("success")).keyBy(_.sid)
            .timeWindow(Time.seconds(10), Time.seconds(5))
            .reduce(new MyReduceFunction(), new ReturnMaxTimeWindowFunction)
            .print()

        streamEnv.execute()

    }

    /**
     * 聚合函数
     */
    class MyReduceFunction() extends ReduceFunction[StationLog] { //增量聚合
        override def reduce(value1: StationLog, value2: StationLog): StationLog = {
            if (value1.duration > value2.duration) value1 else value2
        }
    }


}

2.3、Window 的allowedLateness

  基于 Event-Time 的窗口处理流式数据,虽然提供了Watermark 机制,却只能在一定程度上解决了数据乱序的问题。但在某些情况下数据可能延时会非常严重,即使通过Watermark 机制也无法等到数据全部进入窗口再进行处理。Flink 中默认会将这些迟到的数据做丢弃处理,但是有些时候用户希望即使数据延迟到达的情况下,也能够正常按照流程处理并输出结果,此时就需要使用 Allowed Lateness 机制来对迟到的数据进行额外的处理。

  通常情况下用户虽然希望对迟到的数据进行窗口计算,但并不想将结果混入正常的计算流程中,例如用户大屏数据展示系统,即使正常的窗口中没有将迟到的数据进行统计,但为了保证页面数据显示的连续性,后来接入到系统中迟到数据所统计出来的结果不希望显示在屏幕上,而是将延时数据和结果存储到数据库中,便于后期对延时数据进行分析。对于这种情况需要借助 SideOutput 来处理,通过使用sideOutputLateData(OutputTag)来标记迟到数据计算的结果,然后使用getSideOutput(lateOutputTag)从窗口结果中获取lateOutputTag 标签对应的数据,之后转成独立的 DataStream 数据集进行处理,创建late-data 的 OutputTag,再通过该标签从窗口结果中将迟到数据筛选出来。

注意:如果有 Watermark 同时也有 Allowed Lateness。那么窗口函数再次触发的条件
是:watermark < end-of-window + allowedLateness

案例:每隔5秒统计最近10秒,每个基站的呼叫数量。要求:

  • 1、每个基站的数据会存在乱序
  • 2、大多数数据延迟2秒到,但是有些数据迟到时间比较长
  • 3、迟到时间超过两秒的数据不能丢弃,放入侧流
package com.chb.flink.time

import com.chb.flink.source.StationLog
import org.apache.flink.api.common.functions.AggregateFunction
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.api.scala.function.WindowFunction
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector

/**
 * 案例:每隔 5 秒统计最近 10 秒,每个基站的呼叫数量。要求:
 * 1、每个基站的数据会存在乱序
 * 2、大多数数据延迟 2 秒到,但是有些数据迟到时间比较长
 * 3、迟到时间超过两秒的数据不能丢弃,放入侧流
 */
object LatenessDataOnWindow {

    def main(args: Array[String]): Unit = {
        val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
        import org.apache.flink.streaming.api.scala._
        streamEnv.setParallelism(1)
        //设置时间语义
        streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

        //读取数据源
        val stream: DataStream[StationLog] = streamEnv.socketTextStream("hadoop101", 8888)
            .map(line => {
                var arr = line.split(",")
                new StationLog(arr(0).trim, arr(1).trim, arr(2).trim, arr(3).trim, arr(4).trim.toLong, arr(5).trim.toLong)
            })

            //引入Watermark,数据是乱序的,并且大多数数据延迟2秒
            .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[StationLog](Time.seconds(2)) { //水位线延迟2秒
                override def extractTimestamp(element: StationLog) = {
                    element.callTime
                }
            })

        //定义一个侧输出流的标签
        var lateTag = new OutputTag[StationLog]("late")

        //分组,开窗
        val result: DataStream[String] = stream.keyBy(_.sid)
            .timeWindow(Time.seconds(10), Time.seconds(5))
            //设置迟到的数据超出了2秒的情况下,怎么办。交给AllowedLateness处理
            //也分两种情况,第一种:允许数据迟到5秒(迟到2-5秒),再次延迟触发窗口函数。触发的条是:Watermark < end-of-window + allowedlateness
            //第二种:迟到的数据在5秒以上,输出到则流中
            .allowedLateness(Time.seconds(5)) //运行数据迟到5秒,还可以触发窗口
            .sideOutputLateData(lateTag)
            .aggregate(new MyAggregateCountFunction, new OutputResultWindowFunction)

        result.getSideOutput(lateTag).print("late") //迟到时间超过5秒的数据,本来需要另外处理的。
        result.print("main")

        streamEnv.execute()


    }

    //增量聚会的函数
    class MyAggregateCountFunction extends AggregateFunction[StationLog, Long, Long] {
        override def createAccumulator(): Long = 0

        override def add(value: StationLog, accumulator: Long): Long = accumulator + 1

        override def getResult(accumulator: Long): Long = accumulator

        override def merge(a: Long, b: Long): Long = a + b
    }

    class OutputResultWindowFunction extends WindowFunction[Long, String, String, TimeWindow] {
        override def apply(key: String, window: TimeWindow, input: Iterable[Long], out: Collector[String]): Unit = {
            var value = input.iterator.next()
            var sb = new StringBuilder
            sb.append("窗口的范围:").append(window.getStart).append("---").append(window.getEnd)
            sb.append("\n")
            sb.append("当前的基站ID是:").append(key)
                .append(", 呼叫的数量是:").append(value)
            out.collect(sb.toString())
        }
    }

}