watermark是用于处理乱序事件的,而正确的处理乱序事件,通常用watermark机制结合window来实现。
我们知道,流处理从事件产生,到流经source,再到operator,中间是有一个过程和时间的。虽然大部分情况下,流到operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络、背压等原因,导致乱序的产生(out-of-order或者说late element)。
但是对于late element,我们又不能无限期的等下去,必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了。这个特别的机制,就是watermark。
总结:水位线WaterMark提供了一种数据窗口延迟触发的机制。
示例讲解:
Watermark = 进入 Flink 的最大的事件时间(mxtEventTime)— 指定的延迟时间(t)
如果有窗口的停止时间等于或者小于 maxEventTime – t(当时的 warkmark),那么 这个窗口被触发执行。
程序目标:对进20秒内时间窗口的数据统计最大通话时间duration的信息。通过水位线允许数据延迟10秒。
运行流程如下:
输入项:station_4,18600001941,18900003949,success,1617826650000,0
callTime:2021-04-08 04:17:30 1617826650000 最大延迟10秒。
watermarkTime:2021-04-08 04:17:20 1617826640000
此时需要窗口结束时间小于watermarkTime才会触发窗口。 注:窗口结束时间是固定的,只有改变watermarkTime才能使公式生效,所以是否触发,由输入数据流中的最大事件时间决定。
Watermark = 进入 Flink 的最大的事件时间(mxtEventTime)— 指定的延迟时间(t)
所以只有当新输入的数据中的最大事件时间>=2021-04-08 04:17:40,才会触发【2021-04-08 04:17:10 ~ 2021-04-08 04:17:30】的窗口计算。
示例:
输入项:station_4,18600001941,18900003949,success,1617826650000,0
callTime:2021-04-08 04:17:30 1617826650000 最大延迟10秒。
watermarkTime:2021-04-08 04:17:20 1617826640000
此时需要窗口结束时间小于watermarkTime才会触发窗口。 注:窗口结束时间是固定的,只有改变watermarkTime才能使公式生效,所以是否触发,由输入数据流中的最大事件时间决定。
所以只有当新输入的数据中的最大时间>=2021-04-08 04:17:30,才会触发【2021-04-08 04:17:10 ~ 2021-04-08 04:17:30】的窗口计算。
输入数据窗口在【2021-04-08 04:17:00 ~ 2021-04-08 04:17:20】窗口的数据,此时最大事件时间未更新,不触发计算。
窗口结束时间:1617826640000
输入项:station_4,18600001941,18900003949,success,1617826630000,0
输入项:station_4,18600001941,18900003949,success,1617826635000,21
输入项:station_4,18600001941,18900003949,success,1617826640000,32
输入项:station_4,18600001941,18900003949,success,1617826645000,34
输入新数据2021-04-08 04:17:31,此时更新最大事件时间,且【2021-04-08 04:17:10 ~ 2021-04-08 04:17:30】的窗口结束时间(1617826650000)<=watermarkTime(1617826660000)
watermarkTime=1617826660000-10000=1617826650000
输入项:station_4,18600001941,18900003949,success,1617826660000,0
触发【2021-04-08 04:17:10 ~ 2021-04-08 04:17:30】窗口的计算。
得到station_4,18600001941,18900003949,success,1617826635000,34
package com.hyr.flink.datastream.eventtime
import java.text.SimpleDateFormat
import com.hyr.flink.common.StationLog
import com.hyr.flink.common.watermarkgenerator.BoundedOutOfOrdernessGenerator
import org.apache.flink.api.common.eventtime._
import org.apache.flink.api.common.functions.ReduceFunction
import org.apache.flink.streaming.api.scala.function.WindowFunction
import org.apache.flink.streaming.api.scala.{StreamExecutionEnvironment, _}
import org.apache.flink.streaming.api.windowing.assigners.TumblingProcessingTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector
/** *****************************************************************************
* @Description: 滚动窗口的无序的数据流处理。 注:WaterMark只是决定数据窗口是否进行延迟触发。
******************************************************************************/
object OutOfOrdernessTumblingWaterMarkDemo {
def main(args: Array[String]): Unit = {
val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
// 多并行度会自动对齐WaterMark,取最小的WaterMark。避免干扰,将并行度设为1
streamEnv.setParallelism(1)
// 周期性的引入WaterMark 间隔100毫秒
streamEnv.getConfig.setAutoWatermarkInterval(1000)
//读取数据源
// val stream: DataStream[StationLog] = streamEnv.addSource(new MyCustomSource)
val stream: DataStream[StationLog] = streamEnv.socketTextStream("127.0.0.1", 8888)
.map(line => {
val arr = line.split(",")
StationLog(arr(0).trim, arr(1).trim, arr(2).trim, arr(3).trim, arr(4).trim.toLong, arr(5).trim.toLong)
})
// 周期性/间断性
val data = stream.assignAscendingTimestamps(_.callTime).assignTimestampsAndWatermarks(new WatermarkStrategy[StationLog] {
override def createWatermarkGenerator(context: WatermarkGeneratorSupplier.Context): WatermarkGenerator[StationLog] = {
// 最长延迟10秒
new BoundedOutOfOrdernessGenerator(10 * 1000L)
}
})
// 有序的情况,为数据流中的元素分配时间戳,并定期创建水印以表示事件时间进度。 设置事件时间
val result = data
// 通话成功的记录
.filter(_.callType.equals("success"))
.keyBy(_.sid)
// 每隔10秒统计最近20秒内,每个基站通话时间最长的一次通话记录的基站的id、通话时长、呼叫时间 (毫秒),已经当前发生的时间范围(20秒) 窗口范围左闭右开 延迟的数据会丢掉
.window(TumblingEventTimeWindows.of(Time.seconds(20)))
.reduce(new MyReduceWindowFunction(), new ReturnMaxCallTimeStationLogWindowFunction)
result.print()
streamEnv.execute(this.getClass.getName)
}
/**
* 增量聚合,找到通话时间最长的记录
*/
class MyReduceWindowFunction extends ReduceFunction[StationLog] {
override def reduce(s1: StationLog, s2: StationLog): StationLog = {
if (s1.duration > s2.duration)
s1
else
s2
}
}
/**
* 返回窗口内最大通话时间的记录
*/
class ReturnMaxCallTimeStationLogWindowFunction extends WindowFunction[StationLog, String, String, TimeWindow] {
override def apply(key: String, window: TimeWindow, input: Iterable[StationLog], out: Collector[String]): Unit = {
val sb = new StringBuilder
val stationLog = input.iterator.next()
sb.append("当前时间:").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis())).append(" 窗口范围是:").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(window.getStart)).append("---").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(window.getEnd))
.append("\n")
.append("value:").append(stationLog)
out.collect(sb.toString())
}
}
}
/**
* 基站日志
*
* @param sid 基站的id
* @param callOut 主叫号码
* @param callInt 被叫号码
* @param callType 呼叫类型
* @param callTime 呼叫时间 (毫秒)
* @param duration 通话时长 (秒)
*/
case class StationLog(sid: String, var callOut: String, var callInt: String, callType: String, callTime: Long, duration: Long)
package com.hyr.flink.common.watermarkgenerator
import java.text.SimpleDateFormat
import com.hyr.flink.common.StationLog
import org.apache.flink.api.common.eventtime.{Watermark, WatermarkGenerator, WatermarkOutput}
/**
* 自定义周期性 Watermark 生成器
* 该 watermark 生成器可以覆盖的场景是:数据源在一定程度上乱序。
* 即某个最新到达的时间戳为 t 的元素将在最早到达的时间戳为 t 的元素之后最多 n 毫秒到达。
*/
class BoundedOutOfOrdernessGenerator(_maxOutOfOrderness: Long) extends WatermarkGenerator[StationLog] {
// 最大延迟间隔 n
private val maxOutOfOrderness: Long = _maxOutOfOrderness
// 最大事件时间 maxTime
var currentMaxEventTimestamp: Long = _
/**
* 针对每个事件进行调用,使水印生成器可以检查并记住事件时间戳,或者根据事件本身发出水印。
*
* @param event 事件数据本身
* @param eventTimestamp 时间时间错
* @param output 输出水印
*/
override def onEvent(event: StationLog, eventTimestamp: Long, output: WatermarkOutput): Unit = {
println("callTime:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(event.callTime))
currentMaxEventTimestamp = currentMaxEventTimestamp.max(event.callTime)
// 记录最大事件时间
println("currentMaxTimestamp:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(currentMaxEventTimestamp))
println("currentMaxTimestamp:" + currentMaxEventTimestamp)
val watermarkTime = currentMaxEventTimestamp - maxOutOfOrderness
println("watermarkTime:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(watermarkTime))
println("watermarkTime:" + watermarkTime)
}
override def onPeriodicEmit(output: WatermarkOutput): Unit = {
// 发出的 watermark = 当前最大事件时间 - 最大延迟时间
// Watermark = 进入 Flink 的最大的事件时间(mxtEventTime)— 指定的延迟时间(t)
// 如果有窗口的停止时间等于或者小于 maxEventTime – t(当时的 warkmark),那么 这个窗口被触发执行。
val watermarkTime = currentMaxEventTimestamp - maxOutOfOrderness
output.emitWatermark(new Watermark(watermarkTime))
}
}
Github地址:
https://github.com/huangyueranbbc/FlinkDemo