重要的两个概念: 状态管理、时间语义
Event Time:是事件创建的时间。它通常由事件中的时间戳描述,例如采集的 日志数据中,每一条日志都会记录自己的生成时间,Flink 通过时间戳分配器访问事 件时间戳。
Ingestion Time:是数据进入 Flink 的时间。
Processing Time:是每一个执行基于时间操作的算子的本地系统时间,与机器 相关,默认的时间属性就是 Processing Time。
对于业务来说,要统计 1min 内的故障日志个数,哪个时间是最有意义的?
——时间事件 eventTime,因为我们要根据日志的生成时间进行统计。
EventTime 的引入
在 Flink 的流式处理中,绝大部分的业务都会使用 eventTime,一般只在 eventTime 无法使用时,才会被迫使用 ProcessingTime 或者 IngestionTime。
如果要使用 EventTime,那么需要引入 EventTime 的时间属性,引入方式如下所 示:
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 从调用时刻开始给 env 创建的每一个 stream 追加时间特征
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
Watermark
如果数据传输中一旦出现乱序,若只根据 eventTime 决定 window 的运行,我们不能明确数据是否全部到位,但又不能无限期的等下去,此时必须要有个机制来保证一个特定的时间后,必须触发 window 去进行计算了,这个特别的机制,就是 Watermark
Watermark 是一种衡量 Event Time 进展的机制。
Watermark 是用于处理乱序事件的,而正确的处理乱序事件,通常用 Watermark 机制结合 window 来实现。
数据流中的 Watermark 用于表示 timestamp 小于 Watermark 的数据,都已经 到达了,因此,window 的执行也是由 Watermark 触发的。
Watermark 可以理解成一个延迟触发机制,我们可以设置 Watermark 的延时 时长 t,每次系统会校验已经到达的数据中最大的 maxEventTime,然后认定 eventTime 小于 maxEventTime - t 的所有数据都已经到达,如果有窗口的停止时间等于 maxEventTime – t,那么这个窗口被触发执行。
Watermark 就是触发前一窗口的“关窗时间”,一旦触发关门那么以当前时刻 为准在窗口范围内的所有所有数据都会收入窗中。
只要没有达到水位那么不管现实中的时间推进了多久都不会触发关窗。
Watermark 的引入
watermark 的引入很简单,对于乱序数据,最常见的引用方式如下:
dataStream.assignTimestampsAndWatermarks( // 同时分配时间戳和水位线
new BoundedOutOfOrdernessTimestampExtractor[SensorReading] // 处理乱序数据
(Time.milliseconds(1000)) {
override def extractTimestamp(element: SensorReading): Long = {
element.timestamp * 1000 // 取时间戳作为水位线
}
} )
如果是排好序的,则就不需要延迟触发
Event Time 的使用一定要指定数据源中的时间戳。否则程序无法知道事件的事 件时间是什么(数据源里的数据没有时间戳的话,就只能使用 Processing Time 了)。
TimestampAssigner
自定义如何从事件数据中抽取时间戳
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 从调用时刻开始给 env 创建的每一个 stream 追加时间特性
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
val readings: DataStream[SensorReading] = env
.addSource(new SensorSource)
.assignTimestampsAndWatermarks(new MyAssigner())
MyAssigner 有两种类型
AssignerWithPeriodicWatermarks
AssignerWithPunctuatedWatermarks
以上两个接口都继承自 TimestampAssigner。
一种简单的特殊情况是,如果我们事先得知数据流的时间戳是单调递增的,也 就是说没有乱序,那我们可以使用 assignAscendingTimestamps,这个方法会直接使 用数据的时间戳生成 watermark。
例子,自定义一个周期性的时间戳抽取:
class PeriodicAssigner extends AssignerWithPeriodicWatermarks[SensorReading] {
val bound: Long = 60 * 1000 // 延时为 1 分钟
var maxTs: Long = Long.MinValue // 观察到的最大时间戳
// 返回的就是水位线
override def getCurrentWatermark: Watermark = {
new Watermark(maxTs - bound)
}
// 抽取时间戳的方法
override def extractTimestamp(r: SensorReading, previousTS: Long) = {
maxTs = maxTs.max(r.timestamp) // 比较时间戳,保存最大的
r.timestamp
}
}
间断式生成 watermark。和周期性生成的方式不同,这种方式不是固定时间的, 而是可以根据需要对每条数据进行筛选和处理。
直接上代码来举个例子,我们只给 sensor_1 的传感器的数据流插入 watermark:
class PunctuatedAssigner extends AssignerWithPunctuatedWatermarks[SensorReading] {
val bound: Long = 60 * 1000 // 定义了一个延迟
// 根据某种标准进行检查后生成水位线
override def checkAndGetNextWatermark(r: SensorReading, extractedTS: Long): Watermark = {
if (r.id == "sensor_1") {
new Watermark(extractedTS - bound) // 当前的-延迟的
} else {
null
}
}
override def extractTimestamp(r: SensorReading, previousTS: Long): Long = {
r.timestamp // 这里是直接提取时间戳
}
}
object WindowTest {
case class SensorReading(id: String, timestamp: Long, temperature: Double)
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
// 设置EventTime时间特性( 不做任何时间定义就认为是processTime)
// 还要在dataStream上指定时间戳和水位线
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.getConfig.setAutoWatermarkInterval(500)// 周期性生成水位线
// 读入数据
// val inputStream = env.readTextFile("D:\\Projects\\BigData\\FlinkTutorial\\src\\main\\resources\\sensor.txt")
val inputStream = env.socketTextStream("localhost", 7777)
val dataStream = inputStream.map(data => {
val dataArray = data.split(",")
SensorReading(dataArray(0).trim, dataArray(1).trim.toLong, dataArray(2).trim.toDouble)
})
// .assignAscendingTimestamps(_.timestamp * 1000L) // 指定时间戳和水位线
// 处理乱序数据,延迟一秒钟
.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(1)) {
override def extractTimestamp(element: SensorReading): Long = element.timestamp * 1000L
// dataStream一做好之后就马上分配时间戳和水位线
})
// .assignTimestampsAndWatermarks( new MyAssigner() ) // 自定义来处理乱序的
.map(data => (data.id, data.temperature)) // mqp成一个二元组
.keyBy(_._1) // 因为是元组,所以只能用0或者_ ,不能用字符串
// .process( new MyProcess() )
.timeWindow(Time.seconds(10), Time.seconds(3)) // 开时间窗口(一个参数是滚动,两个参数是滑动)
.reduce((result, data) => (data._1, result._2.min(data._2)))
// 用reduce做增量聚合 result是聚合的结果,data是最新的数据
// 统计10秒内的最低温度值
//(reduce后返回的还是一个DataStreaming)
// 左闭右开,包含开头不包含结尾
dataStream.print()
env.execute("window api test")
}
}
class MyAssigner() extends AssignerWithPeriodicWatermarks[SensorReading]{
// 定义固定延迟为3秒
val bound: Long = 3 * 1000L
// 定义当前收到的最大的时间戳
var maxTs: Long = Long.MinValue
override def getCurrentWatermark: Watermark = {
new Watermark(maxTs - bound)
}
override def extractTimestamp(element: SensorReading, previousElementTimestamp: Long): Long = {
maxTs = maxTs.max(element.timestamp * 1000L)
element.timestamp * 1000L
}
}
class MyAssigner2() extends AssignerWithPunctuatedWatermarks[SensorReading]{
val bound: Long = 1000L
override def checkAndGetNextWatermark(lastElement: SensorReading, extractedTimestamp: Long): Watermark = {
if( lastElement.id == "sensor_1" ){
new Watermark(extractedTimestamp - bound)
}else{
null
}
}
override def extractTimestamp(element: SensorReading, previousElementTimestamp: Long): Long = {
element.timestamp * 1000L
}
}