文章目录
- 一、时间语义
- 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 已经支持这三种类型时间概念,用户能够根据需要选择时间类型作为对流式数据的依据,这种情况极大地增强了对事件数据处理的灵活性和准确性。
参考官方文档
- Time
- More details on how to handle time are in the event time docs
- Flink accesses event timestamps via timestamp assigners.
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 也是一样。
2.1.2、乱序事件中的Watermark
现实情况下数据元素往往并不是按照其产生顺序接入到Flink系统中进行处理,而频繁出现乱序或迟到的情况,这种情况就需要使用Watermarks来应对。比如下图,设置延迟时间 t 为 2
2.1.3、并行数据流中的Watermark
在多并行度的情况下,Watermark会有一个对齐机制,这个对齐机制会取所有Channel中最小的Watermark。
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())
}
}
}